feat:添加课程管理菜单下的班级管理;优化导入组件;添加学员库页面

This commit is contained in:
yuk255 2025-09-03 20:51:13 +08:00
parent a0cb27afe4
commit 7b993f0648
9 changed files with 1944 additions and 217 deletions

72
package-lock.json generated
View File

@ -15,11 +15,13 @@
"axios": "^1.11.0",
"ckplayer": "^3.1.2",
"dplayer": "^1.27.1",
"echarts": "5.6.0",
"naive-ui": "^2.42.0",
"naive-ui-editor": "^1.0.6",
"pinia": "^3.0.3",
"quill": "^2.0.3",
"vue": "^3.5.17",
"vue-echarts": "7.0.3",
"vue-i18n": "^9.14.5",
"vue-quill-editor": "^3.0.6",
"vue-router": "^4.5.1"
@ -2486,6 +2488,16 @@
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.187",
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
@ -4096,6 +4108,12 @@
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/type": {
"version": "2.7.3",
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
@ -4392,6 +4410,51 @@
}
}
},
"node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/vue-echarts": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-7.0.3.tgz",
"integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==",
"license": "MIT",
"dependencies": {
"vue-demi": "^0.13.11"
},
"peerDependencies": {
"@vue/runtime-core": "^3.0.0",
"echarts": "^5.5.1",
"vue": "^2.7.0 || ^3.1.1"
},
"peerDependenciesMeta": {
"@vue/runtime-core": {
"optional": true
}
}
},
"node_modules/vue-i18n": {
"version": "9.14.5",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.5.tgz",
@ -4591,6 +4654,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
}
}
}

View File

@ -1,14 +1,45 @@
<template>
<div>
<h1>学员管理</h1>
<router-view v-slot="{ Component, route }">
<transition
name="fade-slide"
mode="out-in"
appear
>
<component :is="Component" :key="route.path" />
</transition>
</router-view>
</div>
</template>
<script setup lang="ts">
</script>
<style scoped>
</style>
/* 页面过渡动画 */
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.3s ease-in-out;
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(20px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-20px);
}
.fade-slide-enter-to,
.fade-slide-leave-from {
opacity: 1;
transform: translateX(0);
}
/* 可选:添加更丰富的过渡效果 */
.fade-slide-enter-active {
transition-delay: 0.1s;
}
</style>

View File

@ -3,14 +3,31 @@
<div class="import-modal-content">
<!-- 模板下载区域 -->
<div class="template-section">
<n-button type="primary" ghost @click="downloadTemplate">
<template #icon>
<n-icon>
<DownloadOutline />
</n-icon>
</template>
下载 Excel 模板
</n-button>
<div class="template-row">
<n-button type="primary" ghost @click="downloadTemplate">
<template #icon>
<n-icon>
<DownloadOutline />
</n-icon>
</template>
下载 Excel 模板
</n-button>
<!-- 可选的复选框区域 -->
<div v-if="props.showRadioOptions && props.radioOptions" class="checkbox-section">
<!-- <div class="checkbox-label">{{ props.radioLabel }}</div> -->
<div class="checkbox-group">
<n-checkbox
v-for="option in props.radioOptions"
:key="option.value"
v-model:checked="checkboxValues[option.value]"
@update:checked="(checked: boolean) => handleCheckboxChange(option.value, checked)"
>
{{ option.label }}
</n-checkbox>
</div>
</div>
</div>
</div>
<n-divider />
@ -24,8 +41,19 @@
<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"
v-model:file-list="fileList"
:max="1"
accept=".xlsx,.xls"
:custom-request="handleUpload"
@change="handleFileChange"
@remove="handleRemoveFile"
show-file-list
list-type="text"
:default-upload="false"
directory-dnd
>
<n-upload-dragger>
<div class="upload-area">
<n-icon size="48" color="#0288d1" class="upload-icon">
@ -41,27 +69,7 @@
</n-upload-dragger>
</n-upload>
<!-- 文件信息显示 -->
<div v-if="selectedFile" class="file-info">
<div class="file-item">
<n-icon size="20" color="#52c41a">
<DocumentTextOutline />
</n-icon>
<span class="file-name">{{ selectedFile.name }}</span>
<span class="file-size">({{ formatFileSize(selectedFile.file?.size || 0) }})</span>
<n-button text type="error" @click="removeFile">
<n-icon>
<CloseOutline />
</n-icon>
</n-button>
</div>
<!-- 上传进度 -->
<div v-if="uploading" class="upload-progress">
<n-progress :percentage="uploadProgress" :show-indicator="false" />
<span class="progress-text">上传中... {{ uploadProgress }}%</span>
</div>
</div>
<!-- 导入结果 -->
<div v-if="importResult" class="import-result">
@ -98,10 +106,8 @@ import { ref, computed } from 'vue';
import {
DownloadOutline,
CloudUploadOutline,
DocumentTextOutline,
CloseOutline,
} from '@vicons/ionicons5';
import { useMessage } from 'naive-ui';
import { useMessage, NCheckbox, NUploadDragger } from 'naive-ui';
import type { UploadFileInfo, UploadCustomRequestOptions } from 'naive-ui';
// Props
@ -110,6 +116,11 @@ interface Props {
show: boolean;
templateName?: string;
importType?: string;
//
showRadioOptions?: boolean;
radioLabel?: string;
radioOptions?: Array<{ label: string; value: string | number }>;
radioField?: string;
}
// Emits
@ -122,7 +133,10 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), {
title: '导入数据',
templateName: 'import_template.xlsx',
importType: 'default'
importType: 'default',
showRadioOptions: false,
radioLabel: '选择选项',
radioField: 'radioValue'
});
const emit = defineEmits<Emits>();
@ -136,7 +150,8 @@ const fileList = ref<UploadFileInfo[]>([]);
const selectedFile = ref<UploadFileInfo | null>(null);
const uploading = ref(false);
const importing = ref(false);
const uploadProgress = ref(0);
const selectedRadioValue = ref<string | number>('');
const checkboxValues = ref<Record<string | number, boolean>>({});
//
interface ImportResult {
@ -156,15 +171,6 @@ const showModal = computed({
set: (value: boolean) => emit('update:show', value)
});
//
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
//
const downloadTemplate = () => {
emit('template-download', props.importType);
@ -174,56 +180,61 @@ const downloadTemplate = () => {
//
const handleFileChange = (options: { fileList: UploadFileInfo[] }) => {
if (options.fileList.length > 0) {
selectedFile.value = options.fileList[0];
fileList.value = options.fileList;
selectedFile.value = options.fileList.length > 0 ? options.fileList[0] : null;
if (selectedFile.value) {
importResult.value = null; //
}
};
//
const handleRemoveFile = (options: { file: UploadFileInfo; fileList: UploadFileInfo[] }) => {
fileList.value = options.fileList;
selectedFile.value = null;
importResult.value = null;
uploading.value = false;
};
//
const handleUpload = (options: UploadCustomRequestOptions) => {
const { file } = options;
const { file, onProgress, onFinish, onError } = options;
// (10MB)
if (file.file && file.file.size > 10 * 1024 * 1024) {
message.error('文件大小不能超过 10MB');
onError();
return;
}
//
if (file.file && !file.file.name.match(/\.(xlsx|xls)$/i)) {
message.error('只支持 Excel 文件格式');
onError();
return;
}
uploading.value = true;
uploadProgress.value = 0;
//
let progress = 0;
const progressInterval = setInterval(() => {
if (uploadProgress.value < 90) {
uploadProgress.value += Math.random() * 20;
if (progress < 90) {
progress += Math.random() * 20;
onProgress({ percent: Math.min(progress, 90) });
}
}, 200);
//
setTimeout(() => {
clearInterval(progressInterval);
uploadProgress.value = 100;
onProgress({ percent: 100 });
uploading.value = false;
options.onFinish();
onFinish();
console.log('文件上传完成:', file.name);
}, 2000);
};
//
const removeFile = () => {
selectedFile.value = null;
fileList.value = [];
importResult.value = null;
uploadProgress.value = 0;
};
//
const startImport = async () => {
@ -236,6 +247,16 @@ const startImport = async () => {
importResult.value = null;
try {
//
const selectedOptions = getSelectedOptions();
const importData = {
file: selectedFile.value,
[props.radioField]: selectedOptions,
importType: props.importType
};
console.log('导入数据:', importData);
//
await new Promise(resolve => setTimeout(resolve, 3000));
@ -253,7 +274,7 @@ const startImport = async () => {
if (mockResult.success) {
message.success('导入成功!');
emit('success', mockResult);
emit('success', { ...mockResult, importData });
//
setTimeout(() => {
@ -261,7 +282,6 @@ const startImport = async () => {
}, 2000);
}
console.log('导入完成:', mockResult);
// TODO: API
} catch (error) {
@ -275,6 +295,22 @@ const startImport = async () => {
}
};
//
const handleCheckboxChange = (value: string | number, checked: boolean) => {
checkboxValues.value[value] = checked;
};
//
const getSelectedOptions = (): (string | number)[] => {
return Object.entries(checkboxValues.value)
.filter(([_, checked]) => checked)
.map(([value, _]) => {
//
const numValue = Number(value);
return isNaN(numValue) ? value : numValue;
});
};
//
const closeModal = () => {
//
@ -283,7 +319,8 @@ const closeModal = () => {
importResult.value = null;
uploading.value = false;
importing.value = false;
uploadProgress.value = 0;
selectedRadioValue.value = '';
checkboxValues.value = {};
showModal.value = false;
};
@ -308,6 +345,34 @@ const closeModal = () => {
margin-bottom: 16px;
}
.template-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 20px;
}
.checkbox-section {
flex-shrink: 0;
}
.checkbox-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
font-weight: 500;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
:deep(.n-checkbox) {
margin-right: 0;
}
.template-description {
color: #666;
font-size: 14px;
@ -321,11 +386,23 @@ const closeModal = () => {
.upload-area {
text-align: center;
padding: 32px 16px;
padding: 40px 20px;
transition: all 0.3s ease;
border-radius: 8px;
}
.upload-icon {
margin-bottom: 16px;
transition: transform 0.3s ease;
}
.upload-area:hover .upload-icon {
transform: scale(1.1);
}
.upload-text {
position: relative;
z-index: 1;
}
.upload-title {
@ -339,41 +416,6 @@ const closeModal = () => {
color: #999;
}
.file-info {
margin-top: 16px;
padding: 12px;
background-color: #f6f8fa;
border-radius: 6px;
}
.file-item {
display: flex;
align-items: center;
gap: 8px;
}
.file-name {
flex: 1;
color: #333;
font-weight: 500;
}
.file-size {
color: #666;
font-size: 12px;
}
.upload-progress {
margin-top: 12px;
}
.progress-text {
font-size: 12px;
color: #666;
margin-top: 4px;
display: block;
}
.import-result {
margin-top: 16px;
}
@ -399,30 +441,43 @@ const closeModal = () => {
margin-bottom: 4px;
}
/* 上传拖拽区域样式优化 */
/* 上传组件样式优化 */
:deep(.n-upload-dragger) {
border: 2px dashed #d9d9d9;
border-radius: 6px;
transition: border-color 0.3s ease;
border-radius: 8px;
transition: all 0.3s ease;
background: transparent;
}
:deep(.n-upload-dragger:hover) {
border-color: #0288d1;
background-color: rgba(2, 136, 209, 0.02);
}
:deep(.n-upload-dragger.n-upload-dragger--disabled) {
cursor: not-allowed;
:deep(.n-upload-dragger.n-upload-dragger--drag-over) {
border-color: #0288d1;
background-color: rgba(2, 136, 209, 0.08);
transform: scale(1.02);
}
/* 进度条样式 */
:deep(.n-progress .n-progress-graph .n-progress-graph-line-fill) {
background-color: #0288d1;
/* 文件列表样式优化 */
:deep(.n-upload-file-list) {
margin-top: 16px;
}
:deep(.n-upload-file) {
border-radius: 6px;
transition: all 0.3s ease;
}
:deep(.n-upload-file:hover) {
background-color: rgba(2, 136, 209, 0.04);
}
/* 响应式设计 */
@media (max-width: 768px) {
.upload-area {
padding: 24px 12px;
padding: 28px 16px;
}
.upload-icon {
@ -436,5 +491,14 @@ const closeModal = () => {
.template-description {
font-size: 13px;
}
.template-row {
flex-direction: column;
gap: 16px;
}
.checkbox-section {
align-self: flex-start;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -63,7 +63,9 @@ import HomeworkReviewDetail from '@/views/teacher/course/HomeworkReviewDetail.vu
import AddHomework from '@/views/teacher/course/AddHomework.vue'
import HomeworkTemplateImport from '@/views/teacher/course/HomeworkTemplateImport.vue'
// 考试管理组件
// 学员管理组件
import StudentLibrary from '@/views/teacher/student/StudentLibrary.vue'
import ClassManagement from '@/views/teacher/student/ClassManagement.vue'
import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue'
import ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue'
import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue'
@ -294,7 +296,22 @@ const routes: RouteRecordRaw[] = [
path: 'student-management',
name: 'StudentManagement',
component: StudentManagement,
meta: { title: '学员管理' }
meta: { title: '学员管理' },
redirect: '/teacher/student-management/student-library',
children: [
{
path: 'student-library',
name: 'StudentLibrary',
component: StudentLibrary,
meta: { title: '学员库' }
},
{
path: 'class-management',
name: 'ClassManagement',
component: ClassManagement,
meta: { title: '班级管理' }
}
]
},
{
path: 'certificate/new',

View File

@ -53,17 +53,36 @@
</div>
<router-link to="/teacher/student-management" class="nav-item" :class="{ active: activeNavItem === 1 }"
@click="setActiveNavItem(1)">
<!-- 学员中心 - 可展开菜单 -->
<div class="nav-item" :class="{ active: activeNavItem === 1 }" @click="toggleStudentMenu">
<img :src="activeNavItem === 1 ? '/images/teacher/学院管理(选中).png' : '/images/teacher/学员管理.png'" alt="">
<span>学员管理</span>
</router-link>
<span>学员中心</span>
<n-icon class="expand-icon" :class="{ expanded: studentMenuExpanded }">
<ChevronDownOutline />
</n-icon>
</div>
<!-- 学员中心子菜单 -->
<div class="submenu-container" :class="{ expanded: studentMenuExpanded }">
<router-link to="/teacher/student-management/student-library" class="submenu-item"
:class="{ active: activeSubNavItem === 'student-library' }" @click="setActiveSubNavItem('student-library')">
<span>学员库</span>
</router-link>
<router-link to="/teacher/student-management/class-management" class="submenu-item"
:class="{ active: activeSubNavItem === 'class-management' }" @click="setActiveSubNavItem('class-management')">
<span>班级管理</span>
</router-link>
</div>
<router-link to="/teacher/my-resources" class="nav-item" :class="{ active: activeNavItem === 2 }"
@click="setActiveNavItem(2)">
<img :src="activeNavItem === 2 ? '/images/teacher/我的资源(选中).png' : '/images/teacher/我的资源.png'" alt="">
<span>我的资源</span>
</router-link>
<!-- <router-link to="/teacher/my-resources" class="nav-item" :class="{ active: activeNavItem === 2 }"
@click="setActiveNavItem(2)">
<img :src="activeNavItem === 2 ? '/images/teacher/我的资源(选中).png' : '/images/teacher/我的资源.png'" alt="">
<span>消息中心</span>
</router-link> -->
<router-link to="/teacher/personal-center" class="nav-item" :class="{ active: activeNavItem === 3 }"
@click="setActiveNavItem(3)">
@ -115,6 +134,7 @@ console.log(`当前屏幕宽度: ${width}px, 高度: ${height}px`);
const activeNavItem = ref(0); // 0: , 1: , 2: , 3:
const activeSubNavItem = ref(''); //
const examMenuExpanded = ref(false); //
const studentMenuExpanded = ref(false); //
const showTopImage = ref(true); // /
const route = useRoute();
const router = useRouter();
@ -132,6 +152,13 @@ const setActiveNavItem = (index: number) => {
//
if (index !== 4) {
examMenuExpanded.value = false;
}
//
if (index !== 1) {
studentMenuExpanded.value = false;
}
//
if (index !== 4 && index !== 1) {
activeSubNavItem.value = '';
}
}
@ -147,11 +174,31 @@ const toggleExamMenu = () => {
}
}
//
const toggleStudentMenu = () => {
studentMenuExpanded.value = !studentMenuExpanded.value;
activeNavItem.value = 1;
//
if (studentMenuExpanded.value && !activeSubNavItem.value) {
activeSubNavItem.value = 'student-library';
}
}
//
const setActiveSubNavItem = (subItem: string) => {
activeSubNavItem.value = subItem;
activeNavItem.value = 4;
examMenuExpanded.value = true;
//
if (subItem === 'question-bank' || subItem === 'exam-library' || subItem === 'marking-center') {
//
activeNavItem.value = 4;
examMenuExpanded.value = true;
} else if (subItem === 'student-library' || subItem === 'class-management') {
//
activeNavItem.value = 1;
studentMenuExpanded.value = true;
}
}
//
@ -468,6 +515,48 @@ const breadcrumbPathItems = computed(() => {
return breadcrumbs;
}
//
if (currentPath.includes('student-management')) {
console.log('学员管理页面路径:', currentPath);
let breadcrumbs: Array<{ title: string, path: string }> = [];
if (currentPath.includes('student-library')) {
console.log('匹配到学员库页面');
breadcrumbs = [
{
title: '学员中心',
path: '/teacher/student-management'
},
{
title: '学员库',
path: '/teacher/student-management/student-library'
}
];
} else if (currentPath.includes('class-management')) {
console.log('匹配到班级管理页面');
breadcrumbs = [
{
title: '学员中心',
path: '/teacher/student-management'
},
{
title: '班级管理',
path: '/teacher/student-management/class-management'
}
];
} else if (currentPath.endsWith('/student-management')) {
console.log('匹配到学员管理主页面');
breadcrumbs = [
{
title: '学员中心',
path: '/teacher/student-management'
}
];
}
console.log('学员管理页面面包屑:', breadcrumbs);
return breadcrumbs;
}
//
@ -501,6 +590,14 @@ const updateActiveNavItem = () => {
activeNavItem.value = 0; //
} else if (path.includes('student-management')) {
activeNavItem.value = 1; //
studentMenuExpanded.value = true;
//
if (path.includes('student-library')) {
activeSubNavItem.value = 'student-library';
} else if (path.includes('class-management')) {
activeSubNavItem.value = 'class-management';
}
} else if (path.includes('my-resources')) {
activeNavItem.value = 2; //
} else if (path.includes('personal-center')) {

View File

@ -2,7 +2,7 @@
<div class="general-management">
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="class" tab="班级管理">
<ClassManagement />
<ClassManagement type="course" />
</n-tab-pane>
<n-tab-pane name="team" tab="教师团队管理">
<TeamManagement />

View File

@ -0,0 +1,11 @@
<template>
<div>
课程管理开发中
</div>
</template>
<script lang="ts" setup>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,362 @@
<template>
<div class="student-library" v-if="false">
<!-- 页面标题 -->
<div class="header-section" :bordered="false">
<h1 class="page-title">全部学员</h1>
<div class="header-actions">
<n-button type="primary" @click="handleAddStudent">添加学员</n-button>
<n-button type="primary" ghost @click="handleStats">统计分析</n-button>
<n-button type="primary" ghost @click="handleExport">导入</n-button>
<n-button type="primary" ghost @click="handleImport">导出</n-button>
<n-button type="error" ghost @click="handleBatchDelete">删除</n-button>
<n-input v-model:value="searchKeyword" placeholder="请输入人员姓名学号" style="width: 200px;"
@keyup.enter="handleSearch">
<template #suffix>
<n-button text @click="handleSearch">
<template #icon>
<n-icon>
<SearchOutline />
</n-icon>
</template>
</n-button>
</template>
</n-input>
<n-button type="primary" @click="handleSearch">搜索</n-button>
</div>
</div>
<!-- 数据表格 -->
<div class="table-card" :bordered="false">
<n-data-table :columns="columns" :data="filteredStudentList" :loading="loading"
:pagination="paginationReactive" :row-key="rowKey" :checked-row-keys="checkedRowKeys"
@update:checked-row-keys="handleCheck" striped size="medium" />
</div>
</div>
<div class="student-library" v-else>
<ClassManagement type="student"></ClassManagement>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, h, computed } from 'vue'
import { NButton, useMessage, useDialog } from 'naive-ui'
import { SearchOutline } from '@vicons/ionicons5'
import ClassManagement from '@/components/teacher/ClassManagement.vue'
const message = useMessage()
const dialog = useDialog()
//
const searchKeyword = ref('')
//
const loading = ref(false)
const checkedRowKeys = ref<Array<string | number>>([])
//
const studentList = ref([
{
id: 1,
sequence: 1,
name: '王琪琨',
studentId: '18653354882',
gender: '男',
school: '北京大学',
class: '北京清华大学-班级—/北京清华大学-班级—/北京清华大学-班级—',
joinTime: '2025.07.25 09:20',
status: 'active'
},
{
id: 2,
sequence: 2,
name: '李明',
studentId: '18653354883',
gender: '女',
school: '清华大学',
class: '清华大学-软件工程-1班/清华大学-软件工程-2班',
joinTime: '2025.07.26 10:15',
status: 'active'
},
{
id: 3,
sequence: 3,
name: '张伟',
studentId: '18653354884',
gender: '男',
school: '复旦大学',
class: '复旦大学-计算机科学-A班',
joinTime: '2025.07.27 14:30',
status: 'active'
},
{
id: 4,
sequence: 4,
name: '刘红',
studentId: '18653354885',
gender: '女',
school: '上海交通大学',
class: '上海交通大学-信息工程-1班/上海交通大学-信息工程-2班',
joinTime: '2025.07.28 09:45',
status: 'active'
},
{
id: 5,
sequence: 5,
name: '陈小明',
studentId: '18653354886',
gender: '男',
school: '浙江大学',
class: '浙江大学-电子信息-1班',
joinTime: '2025.07.29 11:20',
status: 'active'
},
{
id: 6,
sequence: 6,
name: '王小丽',
studentId: '18653354887',
gender: '女',
school: '中山大学',
class: '中山大学-软件工程-A班/中山大学-软件工程-B班',
joinTime: '2025.07.30 16:10',
status: 'active'
}
])
//
const paginationReactive = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: (page: number) => {
paginationReactive.page = page
},
onUpdatePageSize: (pageSize: number) => {
paginationReactive.pageSize = pageSize
paginationReactive.page = 1
}
})
//
const filteredStudentList = computed(() => {
if (!searchKeyword.value) {
return studentList.value
}
return studentList.value.filter(student =>
student.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
student.studentId.includes(searchKeyword.value)
)
})
// key
const rowKey = (row: any) => row.id
//
const columns = [
{
type: 'selection'
},
{
title: '序号',
key: 'sequence',
width: 80,
align: 'center'
},
{
title: '姓名',
key: 'name',
width: 100,
align: 'center'
},
{
title: '学号',
key: 'studentId',
width: 140,
align: 'center'
},
{
title: '性别',
key: 'gender',
width: 80,
align: 'center'
},
{
title: '所属学校',
key: 'school',
width: 120,
align: 'center'
},
{
title: '所属班级',
key: 'class',
width: 300,
align: 'center',
ellipsis: {
tooltip: true
}
},
{
title: '加入时间',
key: 'joinTime',
width: 160,
align: 'center'
},
{
title: '操作',
key: 'actions',
width: 200,
align: 'center',
render(row: any) {
return h('div', { style: 'display: flex; gap: 8px; justify-content: center;' }, [
h(
NButton,
{
size: 'small',
type: 'info',
ghost: true,
onClick: () => handleViewProgress(row)
},
{ default: () => '学习进度' }
),
h(
NButton,
{
size: 'small',
type: 'primary',
ghost: true,
onClick: () => handleEditStudent(row)
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
type: 'error',
ghost: true,
onClick: () => handleDeleteStudent(row)
},
{ default: () => '删除' }
)
])
}
}
]
//
const handleSearch = () => {
// computed
message.success('搜索完成')
}
const handleAddStudent = () => {
message.info('添加学员功能开发中...')
}
const handleStats = () => {
message.info('统计分析功能开发中...')
}
const handleExport = () => {
message.info('导入功能开发中...')
}
const handleImport = () => {
message.info('导出功能开发中...')
}
const handleBatchDelete = () => {
if (checkedRowKeys.value.length === 0) {
message.warning('请先选择要删除的学员')
return
}
dialog.warning({
title: '确认删除',
content: `确定要删除选中的 ${checkedRowKeys.value.length} 名学员吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
//
studentList.value = studentList.value.filter(student =>
!checkedRowKeys.value.includes(student.id)
)
checkedRowKeys.value = []
message.success('删除成功')
}
})
}
const handleCheck = (keys: Array<string | number>) => {
checkedRowKeys.value = keys
}
const handleViewProgress = (row: any) => {
message.info(`查看 ${row.name} 的学习进度`)
}
const handleEditStudent = (row: any) => {
message.info(`编辑学员 ${row.name}`)
}
const handleDeleteStudent = (row: any) => {
dialog.warning({
title: '确认删除',
content: `确定要删除学员 ${row.name} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
const index = studentList.value.findIndex(student => student.id === row.id)
if (index > -1) {
studentList.value.splice(index, 1)
message.success('删除成功')
}
}
})
}
onMounted(() => {
//
loading.value = false
})
</script>
<style scoped>
.student-library {
background-color: #fff;
padding: 20px;
border-radius: 8px;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20px;
border-bottom: 1px solid #E6E6E6;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 500;
}
.table-card {
margin-top: 20px;
}
.header-actions {
display: flex;
align-items: center;
gap: 12px;
}
</style>