diff --git a/package-lock.json b/package-lock.json index 98fe67b..bfdb2f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/src/components/admin/StudentManagement.vue b/src/components/admin/StudentManagement.vue index 1c02a1e..bd441ef 100644 --- a/src/components/admin/StudentManagement.vue +++ b/src/components/admin/StudentManagement.vue @@ -1,14 +1,45 @@ - +/* 页面过渡动画 */ +.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; +} + \ No newline at end of file diff --git a/src/components/common/ImportModal.vue b/src/components/common/ImportModal.vue index 3a4a931..e97e49e 100644 --- a/src/components/common/ImportModal.vue +++ b/src/components/common/ImportModal.vue @@ -3,14 +3,31 @@
- - - 下载 Excel 模板 - +
+ + + 下载 Excel 模板 + + + +
+ +
+ + {{ option.label }} + +
+
+
@@ -24,8 +41,19 @@ 文件上传
- +
@@ -41,27 +69,7 @@ - -
-
- - - - {{ selectedFile.name }} - ({{ formatFileSize(selectedFile.file?.size || 0) }}) - - - - - -
- -
- - 上传中... {{ uploadProgress }}% -
-
@@ -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(), { title: '导入数据', templateName: 'import_template.xlsx', - importType: 'default' + importType: 'default', + showRadioOptions: false, + radioLabel: '选择选项', + radioField: 'radioValue' }); const emit = defineEmits(); @@ -136,7 +150,8 @@ const fileList = ref([]); const selectedFile = ref(null); const uploading = ref(false); const importing = ref(false); -const uploadProgress = ref(0); +const selectedRadioValue = ref(''); +const checkboxValues = ref>({}); // 导入结果 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; + } } diff --git a/src/components/teacher/ClassManagement.vue b/src/components/teacher/ClassManagement.vue index 9ea5ae0..06e14fd 100644 --- a/src/components/teacher/ClassManagement.vue +++ b/src/components/teacher/ClassManagement.vue @@ -1,10 +1,28 @@