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 模板
+
+
+
+
+
+
+ handleCheckboxChange(option.value, checked)"
+ >
+ {{ 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 @@
-
+
+
\ No newline at end of file
diff --git a/src/views/teacher/student/StudentLibrary.vue b/src/views/teacher/student/StudentLibrary.vue
new file mode 100644
index 0000000..bec7eef
--- /dev/null
+++ b/src/views/teacher/student/StudentLibrary.vue
@@ -0,0 +1,362 @@
+
+
+
+
+
+
+
+
+
+