feat:添加课程-管理页面;考试管理菜单也同步添加到课程管理下;优化了一些页面

This commit is contained in:
yuk255 2025-09-02 20:21:02 +08:00
parent f888b0b458
commit 739ad411c1
9 changed files with 898 additions and 100 deletions

View File

@ -1,17 +1,8 @@
<template>
<n-modal v-model:show="showModal" :mask-closable="false" preset="dialog" title="数据导入" style="width: 600px;">
<n-modal v-model:show="showModal" :mask-closable="false" preset="dialog" :title="title" style="width: 600px;">
<div class="import-modal-content">
<!-- 模板下载区域 -->
<div class="template-section">
<div class="section-title">
<n-icon size="18" color="#0288d1">
<CodeDownloadOutline />
</n-icon>
<span>模板下载</span>
</div>
<div class="template-description">
请先下载导入模板按照模板格式填写数据后再上传
</div>
<n-button type="primary" ghost @click="downloadTemplate">
<template #icon>
<n-icon>
@ -32,16 +23,9 @@
</n-icon>
<span>文件上传</span>
</div>
<n-upload
ref="uploadRef"
:file-list="fileList"
:max="1"
accept=".xlsx,.xls"
:show-file-list="false"
:custom-request="handleUpload"
@change="handleFileChange"
>
<n-upload ref="uploadRef" :file-list="fileList" :max="1" accept=".xlsx,.xls" :show-file-list="false"
:custom-request="handleUpload" @change="handleFileChange">
<n-upload-dragger>
<div class="upload-area">
<n-icon size="48" color="#0288d1" class="upload-icon">
@ -71,7 +55,7 @@
</n-icon>
</n-button>
</div>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<n-progress :percentage="uploadProgress" :show-indicator="false" />
@ -95,32 +79,13 @@
</n-alert>
</div>
</div>
<!-- 导入说明 -->
<div class="import-notes">
<div class="section-title">
<n-icon size="18" color="#fa8c16">
<InformationCircleOutline />
</n-icon>
<span>导入说明</span>
</div>
<ul class="notes-list">
<li>请严格按照模板格式填写数据不要修改表头</li>
<li>必填字段不能为空否则导入失败</li>
<li>导入前请检查数据格式确保数据的准确性</li>
</ul>
</div>
</div>
<template #action>
<n-space>
<n-button @click="closeModal">取消</n-button>
<n-button
type="primary"
@click="startImport"
:disabled="!selectedFile || uploading"
:loading="importing"
>
<n-button type="primary" @click="startImport" :disabled="!selectedFile || uploading"
:loading="importing">
{{ importing ? '导入中...' : '开始导入' }}
</n-button>
</n-space>
@ -130,19 +95,18 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import {
CodeDownloadOutline,
DownloadOutline,
CloudUploadOutline,
DocumentTextOutline,
CloseOutline,
InformationCircleOutline
import {
DownloadOutline,
CloudUploadOutline,
DocumentTextOutline,
CloseOutline,
} from '@vicons/ionicons5';
import { useMessage } from 'naive-ui';
import type { UploadFileInfo, UploadCustomRequestOptions } from 'naive-ui';
// Props
interface Props {
title?: string;
show: boolean;
templateName?: string;
importType?: string;
@ -156,6 +120,7 @@ interface Emits {
}
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
templateName: 'import_template.xlsx',
importType: 'default'
});
@ -218,7 +183,7 @@ const handleFileChange = (options: { fileList: UploadFileInfo[] }) => {
//
const handleUpload = (options: UploadCustomRequestOptions) => {
const { file } = options;
// (10MB)
if (file.file && file.file.size > 10 * 1024 * 1024) {
message.error('文件大小不能超过 10MB');
@ -246,7 +211,7 @@ const handleUpload = (options: UploadCustomRequestOptions) => {
clearInterval(progressInterval);
uploadProgress.value = 100;
uploading.value = false;
options.onFinish();
console.log('文件上传完成:', file.name);
}, 2000);
@ -273,7 +238,7 @@ const startImport = async () => {
try {
//
await new Promise(resolve => setTimeout(resolve, 3000));
//
const mockResult: ImportResult = {
success: true,
@ -285,20 +250,20 @@ const startImport = async () => {
};
importResult.value = mockResult;
if (mockResult.success) {
message.success('导入成功!');
emit('success', mockResult);
//
setTimeout(() => {
closeModal();
}, 2000);
}
console.log('导入完成:', mockResult);
// TODO: API
} catch (error) {
importResult.value = {
success: false,
@ -319,7 +284,7 @@ const closeModal = () => {
uploading.value = false;
importing.value = false;
uploadProgress.value = 0;
showModal.value = false;
};
</script>
@ -459,15 +424,15 @@ const closeModal = () => {
.upload-area {
padding: 24px 12px;
}
.upload-icon {
margin-bottom: 12px;
}
.section-title {
font-size: 14px;
}
.template-description {
font-size: 13px;
}

View File

@ -0,0 +1,398 @@
<template>
<div class="team-management">
<div class="toolbar">
<n-select v-model:value="selectedDepartment" :options="departmentOptions" placeholder="班级名称"
style="width: 200px" />
<NSpace>
<n-button type="primary" ghost>
<template #icon>
<NIcon>
<AddCircleOutline />
</NIcon>
</template>
添加班级
</n-button>
<n-button type="primary" ghost>
<template #icon>
<NIcon>
<SettingsOutline />
</NIcon>
</template>
管理班级
</n-button>
<n-button type="primary" ghost @click="showInviteModal = true">
<template #icon>
<NIcon>
<QrCode />
</NIcon>
</template>
邀请码
</n-button>
</NSpace>
</div>
<n-divider />
<div class="toolbar">
<div class="student-count">
全部学员{{ totalStudents }}
</div>
<NSpace>
<n-button type="primary" @click="showAddModal = true">
添加学员
</n-button>
<n-button type="primary" ghost>
导入
</n-button>
<n-button type="primary" ghost>
导出
</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
<n-button type="primary">
搜索
</n-button>
</NSpace>
</div>
<n-data-table :columns="columns" :data="data" :pagination="pagination" :loading="loading"
:row-key="(row: StudentItem) => row.id" striped size="small" />
<!-- 添加学员弹窗 -->
<n-modal v-model:show="showAddModal" title="添加学员">
<n-card style="width: 600px" title="添加学员" :bordered="false" size="huge" role="dialog" aria-modal="true">
<n-form ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="auto"
require-mark-placement="right-hanging">
<n-form-item label="姓名" path="teacherName">
<n-input v-model:value="formData.teacherName" placeholder="请输入学员姓名" />
</n-form-item>
<n-form-item label="学号" path="teacherId">
<n-input v-model:value="formData.teacherId" placeholder="请输入学员学号" />
</n-form-item>
</n-form>
<template #footer>
<div class="modal-footer">
<n-button @click="showAddModal = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">添加</n-button>
</div>
</template>
</n-card>
</n-modal>
<!-- 邀请码弹窗 -->
<n-modal v-model:show="showInviteModal" title="邀请码">
<n-card style="width: 400px" title="邀请码" :bordered="false" size="huge" role="dialog" aria-modal="true">
<div class="invite-content">
<div class="invite-code-display">
<div class="invite-title">邀请码</div>
<div class="invite-code">{{ inviteCode }}</div>
<n-button ghost type="primary" @click="copyInviteCode">复制</n-button>
</div>
</div>
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import { AddCircleOutline, SettingsOutline, QrCode } from '@vicons/ionicons5'
import {
NDataTable,
NButton,
NSpace,
NSelect,
NInput,
NModal,
NCard,
NForm,
NFormItem,
NIcon,
NDivider,
useMessage,
type FormInst,
type FormRules
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
//
interface StudentItem {
id: string
studentName: string
accountNumber: string
className: string
college: string
loginName: string
joinTime: string
}
//
interface FormData {
teacherName: string
teacherId: string
}
const totalStudents = ref(1333)
const inviteCode = ref('56685222')
const message = useMessage()
//
const selectedDepartment = ref('')
const searchKeyword = ref('')
const showAddModal = ref(false)
const showInviteModal = ref(false)
const formRef = ref<FormInst | null>(null)
//
const formData = ref<FormData>({
teacherName: '',
teacherId: ''
})
//
const rules: FormRules = {
teacherName: [
{ required: true, message: '请输入教师姓名', trigger: 'blur' }
],
teacherId: [
{ required: true, message: '请输入工号', trigger: 'blur' }
]
}
//
const departmentOptions = ref([
{ label: '默认班级', value: '' },
{ label: '软件工程1班', value: 'software1' },
{ label: '软件工程2班', value: 'software2' },
{ label: '计算机科学1班', value: 'cs1' }
])
//
const columns: DataTableColumns<StudentItem> = [
{
title: '姓名',
key: 'studentName',
width: 120,
align: 'center'
},
{
title: '账号',
key: 'accountNumber',
width: 140,
align: 'center'
},
{
title: '班级',
key: 'className',
width: 120,
align: 'center'
},
{
title: '所在学院',
key: 'college',
width: 200,
align: 'center'
},
{
title: '登录名',
key: 'loginName',
width: 150,
align: 'center'
},
{
title: '加入时间',
key: 'joinTime',
width: 140,
align: 'center'
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
render: (row) => {
return h(
NSpace,
{ size: 'small', justify: 'center' },
{
default: () => [
h(
NButton,
{
size: 'small',
type: 'primary',
ghost: true,
onClick: () => handleEdit(row)
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
type: 'info',
ghost: true,
onClick: () => handleReset(row)
},
{ default: () => '重置' }
),
h(
NButton,
{
size: 'small',
type: 'error',
ghost: true,
onClick: () => handleDelete(row)
},
{ default: () => '移除' }
)
]
}
)
}
}
]
//
const data = ref<StudentItem[]>([])
const loading = ref(false)
//
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
pagination.value.page = page
loadData()
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
loadData()
}
})
//
const handleEdit = (row: StudentItem) => {
message.info(`编辑学员:${row.studentName}`)
}
const handleReset = (row: StudentItem) => {
message.info(`重置学员密码:${row.studentName}`)
}
const handleDelete = (row: StudentItem) => {
message.warning(`移除学员:${row.studentName}`)
}
const copyInviteCode = () => {
navigator.clipboard.writeText(inviteCode.value).then(() => {
message.success('邀请码已复制到剪贴板')
}).catch(() => {
message.error('复制失败,请手动复制')
})
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
//
message.success('添加教师成功')
showAddModal.value = false
//
formData.value = {
teacherName: '',
teacherId: ''
}
//
loadData()
} catch (error) {
message.error('请检查表单信息')
}
}
//
const loadData = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const mockData: StudentItem[] = Array.from({ length: 3 }, (_, index) => ({
id: `student_${index + 1}`,
studentName: ['张华', '李明', '王丽'][index],
accountNumber: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
className: '计算机1',
college: '清华大学经管学院',
loginName: [`${(1660340 + index + 1).toString()}`, `${(1660340 + index + 2).toString()}`, `${(1660340 + index + 3).toString()}`][index],
joinTime: '2025.07.25 08:20'
}))
data.value = mockData
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.team-management {
padding: 16px 0;
}
.toolbar {
margin: 16px 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.student-count {
color: #838383;
font-size: 14px;
}
.modal-footer {
display: flex;
justify-content: end;
gap: 12px;
}
.invite-content {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
background-color: #F5F8FB;
}
.invite-code-display {
text-align: center;
}
.invite-title {
font-size: 16px;
color: #333;
margin-bottom: 10px;
}
.invite-code {
font-size: 32px;
font-weight: bold;
color: #1890ff;
margin-bottom: 10px;
letter-spacing: 2px;
}
.invite-note {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<div class="operation-log">
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
:loading="loading"
striped
size="small"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { NDataTable } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
//
interface LogItem {
id: string
role: string
userName: string
studentId: string
operationContent: string
operatorIp: string
operationTime: string
}
//
const columns: DataTableColumns<LogItem> = [
{
title: '序号',
key: 'id',
width: 80,
align: 'center',
render: (_, index) => index + 1
},
{
title: '角色',
key: 'role',
width: 100,
align: 'center'
},
{
title: '姓名',
key: 'userName',
width: 120,
align: 'center'
},
{
title: '学号',
key: 'studentId',
width: 120,
align: 'center'
},
{
title: '操作内容',
key: 'operationContent',
minWidth: 200,
align: 'center'
},
{
title: '操作IP',
key: 'operatorIp',
width: 120,
align: 'center'
},
{
title: '操作时间',
key: 'operationTime',
width: 180,
align: 'center'
}
]
//
const data = ref<LogItem[]>([])
const loading = ref(false)
//
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
pagination.value.page = page
loadData()
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
loadData()
}
})
//
const loadData = async () => {
loading.value = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
//
const mockData: LogItem[] = Array.from({ length: 15 }, (_, index) => ({
id: `${index + 1}`,
role: '教师',
userName: '刘桂人',
studentId: '56566652',
operationContent: '操作内容',
operatorIp: 'ip地址',
operationTime: '2025.08.02 07:30'
}))
data.value = mockData
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
//
onMounted(() => {
loadData()
})
</script>
<style scoped>
.operation-log {
padding: 16px 0;
}
</style>

View File

@ -0,0 +1,262 @@
<template>
<div class="team-management">
<div class="toolbar">
<n-select v-model:value="selectedDepartment" :options="departmentOptions" placeholder="班级名称"
style="width: 200px" />
<NSpace>
<n-button type="primary" @click="showAddModal = true">
添加老师
</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入关键词" style="width: 200px" />
<n-button type="primary">
搜索
</n-button>
</NSpace>
</div>
<n-data-table :columns="columns" :data="data" :pagination="pagination" :loading="loading"
:row-key="(row: TeacherItem) => row.id" striped size="small" />
<!-- 添加教师弹窗 -->
<n-modal v-model:show="showAddModal" title="添加老师">
<n-card style="width: 600px" title="添加老师" :bordered="false" size="huge" role="dialog" aria-modal="true">
<n-form ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="auto"
require-mark-placement="right-hanging">
<n-form-item label="姓名" path="teacherName">
<n-input v-model:value="formData.teacherName" placeholder="请输入老师姓名" />
</n-form-item>
<n-form-item label="工号" path="teacherId">
<n-input v-model:value="formData.teacherId" placeholder="请输入老师工号" />
</n-form-item>
</n-form>
<template #footer>
<div class="modal-footer">
<n-button @click="showAddModal = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">添加</n-button>
</div>
</template>
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, h } from 'vue'
import {
NDataTable,
NButton,
NSpace,
NSelect,
NInput,
NModal,
NCard,
NForm,
NFormItem,
useMessage,
type FormInst,
type FormRules
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
//
interface TeacherItem {
id: string
teacherName: string
teacherId: string
role: string
studentId: string
teachingClass: string
department: string
joinTime: string
}
//
interface FormData {
teacherName: string
teacherId: string
}
const message = useMessage()
//
const selectedDepartment = ref('')
const searchKeyword = ref('')
const showAddModal = ref(false)
const formRef = ref<FormInst | null>(null)
//
const formData = ref<FormData>({
teacherName: '',
teacherId: ''
})
//
const rules: FormRules = {
teacherName: [
{ required: true, message: '请输入教师姓名', trigger: 'blur' }
],
teacherId: [
{ required: true, message: '请输入工号', trigger: 'blur' }
]
}
//
const departmentOptions = ref([
{ label: '全部班级', value: '' },
{ label: '软件工程1班', value: 'software1' },
{ label: '软件工程2班', value: 'software2' },
{ label: '计算机科学1班', value: 'cs1' }
])
//
const columns: DataTableColumns<TeacherItem> = [
{
title: '姓名',
key: 'teacherName',
width: 120,
align: 'center'
},
{
title: '角色',
key: 'role',
width: 100,
align: 'center'
},
{
title: '成员',
key: 'studentId',
width: 120,
align: 'center'
},
{
title: '任教班级',
key: 'teachingClass',
width: 150,
align: 'center'
},
{
title: '所属机构',
key: 'department',
width: 150,
align: 'center'
},
{
title: '加入时间',
key: 'joinTime',
width: 140,
align: 'center'
},
{
title: '操作',
key: 'actions',
width: 100,
align: 'center',
render: (row) => {
return h(
NButton,
{
size: 'small',
type: 'error',
ghost: true,
onClick: () => handleDelete(row)
},
{ default: () => '移除' }
)
}
}
]
//
const data = ref<TeacherItem[]>([])
const loading = ref(false)
//
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
pagination.value.page = page
loadData()
},
onUpdatePageSize: (pageSize: number) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
loadData()
}
})
//
const handleDelete = (row: TeacherItem) => {
message.warning(`移除教师:${row.teacherName}`)
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
//
message.success('添加教师成功')
showAddModal.value = false
//
formData.value = {
teacherName: '',
teacherId: ''
}
//
loadData()
} catch (error) {
message.error('请检查表单信息')
}
}
//
const loadData = async () => {
loading.value = true
try {
await new Promise(resolve => setTimeout(resolve, 500))
const roles = ['刘桂人']
const departments = ['北京大学经济学理学院']
const mockData: TeacherItem[] = Array.from({ length: 1 }, (_, index) => ({
id: `teacher_${index + 1}`,
teacherName: roles[0],
teacherId: `56566652`,
role: '刘桂人',
studentId: '56566652',
teachingClass: '北京大学经济学理学院',
department: departments[0],
joinTime: '2025.08.02 07:30'
}))
data.value = mockData
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.team-management {
padding: 16px 0;
}
.toolbar {
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-footer{
display: flex;
justify-content: end;
gap: 12px;
}
</style>

View File

@ -197,18 +197,20 @@ const routes: RouteRecordRaw[] = [
path: 'practice',
name: 'Practice',
redirect: (to) => `/teacher/course-editor/${to.params.id}/practice/exam`,
meta: { title: '练考通' },
meta: { title: '考试管理' },
children: [
{
path: 'exam',
name: 'PracticeExam',
component: () => import('../views/teacher/course/PracticeExam.vue'),
// component: () => import('../views/teacher/course/PracticeExam.vue'),
component: ExamLibrary,
meta: { title: '试卷' }
},
{
path: 'review',
name: 'PracticeReview',
component: () => import('../views/teacher/course/PracticeReview.vue'),
// component: () => import('../views/teacher/course/PracticeReview.vue'),
component: MarkingCenter,
meta: { title: '阅卷中心' }
}
]
@ -216,7 +218,7 @@ const routes: RouteRecordRaw[] = [
{
path: 'question-bank',
name: 'QuestionBankManagement',
component: QuestionBankManagement,
component: ExamQuestionBankManagement,
meta: { title: '题库管理' }
},
{

View File

@ -27,7 +27,9 @@
<!-- 考试管理 - 可展开菜单 -->
<div class="nav-item" :class="{ active: activeNavItem === 4 }" @click="toggleExamMenu">
<img :src="activeNavItem === 4 ? '/images/teacher/examination-active.png' : '/images/teacher/examination.png'" alt="">
<img
:src="activeNavItem === 4 ? '/images/teacher/examination-active.png' : '/images/teacher/examination.png'"
alt="">
<span>考试管理</span>
<n-icon class="expand-icon" :class="{ expanded: examMenuExpanded }">
<ChevronDownOutline />
@ -37,8 +39,7 @@
<!-- 考试管理子菜单 -->
<div class="submenu-container" :class="{ expanded: examMenuExpanded }">
<router-link to="/teacher/exam-management/question-bank" class="submenu-item"
:class="{ active: activeSubNavItem === 'question-bank' }"
@click="setActiveSubNavItem('question-bank')">
:class="{ active: activeSubNavItem === 'question-bank' }" @click="setActiveSubNavItem('question-bank')">
<span>题库管理</span>
</router-link>
<router-link to="/teacher/exam-management/exam-library" class="submenu-item"
@ -349,10 +350,10 @@ const breadcrumbPathItems = computed(() => {
}
);
}
} else if (currentPath.includes('practice')) {
} else if (currentPath.includes('practice/exam')) {
breadcrumbs.push(
{
title: '练考通',
title: '考试管理',
path: currentPath
},
{
@ -360,9 +361,10 @@ const breadcrumbPathItems = computed(() => {
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('practice/review')) {
breadcrumbs.push(
{
title: '练考通',
title: '阅卷中心',
path: currentPath
},
{
@ -370,6 +372,7 @@ const breadcrumbPathItems = computed(() => {
path: `/teacher/course-editor/${courseId}`
}
);
} else if (currentPath.includes('certificate')) {
breadcrumbs.push(
{
@ -505,7 +508,7 @@ const updateActiveNavItem = () => {
} else if (path.includes('exam-management')) {
activeNavItem.value = 4; //
examMenuExpanded.value = true;
const arr = ['question-bank', 'exam-library', 'marking-center'];
const found = arr.find(item => path.includes(item));
activeSubNavItem.value = found || '';
@ -890,6 +893,7 @@ const updateActiveNavItem = () => {
align-items: center;
gap: 8px;
}
.breadcrumb-item {
display: flex;
align-items: center;

View File

@ -61,6 +61,12 @@
</div>
</div>
</div>
<ImportModal
v-model:show="showImportModal"
template-name="custom_template.xlsx"
import-type="custom"
@success="handleImportSuccess"
@template-download="handleTemplateDownload" />
</div>
</template>
@ -69,6 +75,7 @@ import { ref, computed, h } from 'vue'
import { NButton, useMessage, NDataTable, NInput, NSpace } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useRouter, useRoute } from 'vue-router'
import ImportModal from '@/components/common/ImportModal.vue'
const router = useRouter()
const route = useRoute()
@ -88,6 +95,16 @@ interface Chapter {
expanded?: boolean
}
const showImportModal = ref(false)
const handleImportSuccess = () => {
message.success('章节导入成功')
}
const handleTemplateDownload = () => {
message.success('模板下载成功')
}
//
const searchKeyword = ref('')
@ -351,7 +368,7 @@ const addChapter = () => {
}
const importChapters = () => {
message.info('导入章节功能')
showImportModal.value = true
}
const exportChapters = () => {

View File

@ -19,7 +19,7 @@
<div class="menu-header" @click="toggleHomework">
<img src="/images/teacher/作业.png" alt="作业" />
<span>作业</span>
<i class="n-base-icon" :class="{ expanded: homeworkExpanded || shouldExpandHomework }">
<i class="n-base-icon" :class="{ expanded: homeworkExpanded }">
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5.64645 3.14645C5.45118 3.34171 5.45118 3.65829 5.64645 3.85355L9.79289 8L5.64645 12.1464C5.45118 12.3417 5.45118 12.6583 5.64645 12.8536C5.84171 13.0488 6.15829 13.0488 6.35355 12.8536L10.8536 8.35355C11.0488 8.15829 11.0488 7.84171 10.8536 7.64645L6.35355 3.14645C6.15829 2.95118 5.84171 2.95118 5.64645 3.14645Z"
@ -27,7 +27,7 @@
</svg>
</i>
</div>
<div class="submenu" v-show="homeworkExpanded || shouldExpandHomework">
<div class="submenu" v-show="homeworkExpanded">
<router-link :to="`/teacher/course-editor/${courseId}/homework/library`" class="submenu-item"
:class="{ active: $route.path.includes('/homework/library') || $route.path.includes('/homework/add-homework') }">
<span>作业库</span>
@ -53,6 +53,10 @@
</i>
</div>
<div class="submenu" v-show="practiceExpanded">
<router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="submenu-item"
:class="{ active: $route.path.includes('/question-bank') }">
<span>题库管理</span>
</router-link>
<router-link :to="`/teacher/course-editor/${courseId}/practice/exam`" class="submenu-item"
:class="{ active: $route.path.includes('/practice/exam') }">
<span>试卷管理</span>
@ -63,12 +67,6 @@
</router-link>
</div>
</div>
<!-- <router-link :to="`/teacher/course-editor/${courseId}/question-bank`" class="menu-item"
:class="{ active: $route.path.includes('question-bank') }">
<img :src="$route.path.includes('question-bank') ? '/images/teacher/题库-选中.png' : '/images/teacher/题库.png'"
alt="题库" />
<span>题库</span>
</router-link> -->
<router-link :to="`/teacher/course-editor/${courseId}/certificate`" class="menu-item"
:class="{ active: $route.path.includes('certificate') }">
<img :src="$route.path.includes('certificate') ? '/images/teacher/证书-选中.png' : '/images/teacher/证书.png'"
@ -124,11 +122,6 @@ const courseId = route.params.id
//
const homeworkExpanded = ref(false)
//
const shouldExpandHomework = computed(() => {
return route.path.includes('/homework/')
})
// /
const toggleHomework = () => {
homeworkExpanded.value = !homeworkExpanded.value
@ -142,6 +135,12 @@ const togglePractice = () => {
practiceExpanded.value = !practiceExpanded.value
}
if (route.path.includes('/homework/')) {
homeworkExpanded.value = true
}else if(route.path.includes('/practice/') || route.path.includes('/question-bank')){
practiceExpanded.value = true
}
//
const hideSidebar = computed(() => {
const currentPath = route.path

View File

@ -1,14 +1,28 @@
<template>
<div class="general-management">
<div class="content-placeholder">
<h2>综合管理</h2>
<p>综合管理功能正在开发中...</p>
</div>
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="class" tab="班级管理">
<ClassManagement />
</n-tab-pane>
<n-tab-pane name="team" tab="教师团队管理">
<TeamManagement />
</n-tab-pane>
<n-tab-pane name="log" tab="操作日志">
<OperationLog />
</n-tab-pane>
</n-tabs>
</div>
</template>
<script setup lang="ts">
//
import { ref } from 'vue'
import { NTabs, NTabPane } from 'naive-ui'
import ClassManagement from '@/components/teacher/ClassManagement.vue'
import TeamManagement from '@/components/teacher/TeamManagement.vue'
import OperationLog from '@/components/teacher/OperationLog.vue'
// tab
const activeTab = ref('class')
</script>
<style scoped>
@ -18,19 +32,22 @@
height: 100%;
}
.content-placeholder {
text-align: center;
padding: 60px 20px;
/* 自定义tab样式 */
:deep(.n-tabs .n-tabs-nav) {
padding: 0;
}
.content-placeholder h2 {
font-size: 24px;
color: #333;
margin-bottom: 20px;
:deep(.n-tabs .n-tabs-tab) {
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
}
.content-placeholder p {
font-size: 16px;
color: #666;
:deep(.n-tabs .n-tabs-tab.n-tabs-tab--active) {
color: #1890ff;
}
:deep(.n-tabs .n-tabs-bar) {
background-color: #1890ff;
}
</style>