feat:添加课程管理菜单下的班级管理;优化导入组件;添加学员库页面
This commit is contained in:
parent
a0cb27afe4
commit
7b993f0648
72
package-lock.json
generated
72
package-lock.json
generated
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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
@ -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',
|
||||
|
@ -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')) {
|
||||
|
@ -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 />
|
||||
|
11
src/views/teacher/student/ClassManagement.vue
Normal file
11
src/views/teacher/student/ClassManagement.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
课程管理开发中
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
362
src/views/teacher/student/StudentLibrary.vue
Normal file
362
src/views/teacher/student/StudentLibrary.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user