diff --git a/public/images/personal/切换_switch备份 2@2x.png b/public/images/personal/切换_switch备份 2@2x.png new file mode 100644 index 0000000..c0ae819 Binary files /dev/null and b/public/images/personal/切换_switch备份 2@2x.png differ diff --git a/public/images/personal/切换_switch备份@2x.png b/public/images/personal/切换_switch备份@2x.png new file mode 100644 index 0000000..759ad18 Binary files /dev/null and b/public/images/personal/切换_switch备份@2x.png differ diff --git a/public/images/personal/未读消息_message-unread备份@2x.png b/public/images/personal/未读消息_message-unread备份@2x.png new file mode 100644 index 0000000..55e4799 Binary files /dev/null and b/public/images/personal/未读消息_message-unread备份@2x.png differ diff --git a/public/images/personal/用户_user备份 2@2x.png b/public/images/personal/用户_user备份 2@2x.png new file mode 100644 index 0000000..cfb4531 Binary files /dev/null and b/public/images/personal/用户_user备份 2@2x.png differ diff --git a/public/images/personal/用户_user备份@2x.png b/public/images/personal/用户_user备份@2x.png new file mode 100644 index 0000000..547d5ce Binary files /dev/null and b/public/images/personal/用户_user备份@2x.png differ diff --git a/public/images/personal/矩形备份 35@2x.png b/public/images/personal/矩形备份 35@2x.png new file mode 100644 index 0000000..497b99c Binary files /dev/null and b/public/images/personal/矩形备份 35@2x.png differ diff --git a/public/images/personal/退出_logout备份 2@2x.png b/public/images/personal/退出_logout备份 2@2x.png new file mode 100644 index 0000000..7ac3b32 Binary files /dev/null and b/public/images/personal/退出_logout备份 2@2x.png differ diff --git a/public/images/personal/退出_logout备份 3@2x.png b/public/images/personal/退出_logout备份 3@2x.png new file mode 100644 index 0000000..85b09ed Binary files /dev/null and b/public/images/personal/退出_logout备份 3@2x.png differ diff --git a/src/App.vue b/src/App.vue index 8b8b214..3c6f089 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3,8 +3,14 @@ import { onMounted, computed } from 'vue' import { RouterView, useRoute } from 'vue-router' import { useUserStore } from '@/stores/user' import AppLayout from '@/components/layout/AppLayout.vue' -import { NConfigProvider, NMessageProvider } from 'naive-ui' +import { NConfigProvider, NMessageProvider, NDialogProvider, zhCN, dateZhCN, enUS, dateEnUS } from 'naive-ui' import type { GlobalThemeOverrides } from 'naive-ui'; +import { useI18n } from 'vue-i18n' +const { locale } = useI18n() + +// 响应式获取naive-ui语言包 +const naiveLocale = computed(() => locale.value === 'en' ? enUS : zhCN) +const naiveDateLocale = computed(() => locale.value === 'en' ? dateEnUS : dateZhCN) // 自定义naive-ui主题颜色 @@ -71,19 +77,21 @@ onMounted(() => { diff --git a/src/api/index.ts b/src/api/index.ts index d030f0f..484d7a9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -10,6 +10,7 @@ export { default as FavoriteApi } from './modules/favorite' export { default as OrderApi } from './modules/order' export { default as UploadApi } from './modules/upload' export { default as StatisticsApi } from './modules/statistics' +export { default as ExamApi } from './modules/exam' // API 基础配置 export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/jeecgboot' @@ -182,6 +183,37 @@ export const API_ENDPOINTS = { COMMENTS: '/statistics/comments', EXPORT: '/statistics/export/:type', }, + + // 考试题库相关 + EXAM: { + // 题库管理 + REPO_CREATE: '/biz/repo/courseAdd', + REPO_LIST: '/biz/repo/repoList', + REPO_DELETE: '/gen/repo/repo/delete', + REPO_EDIT: '/gen/repo/repo/edit', + + // 题目管理 + QUESTION_LIST: '/biz/repo/questionList/:repoId', + QUESTION_DETAIL: '/biz/repo/repoList/:questionId', + QUESTION_ADD: '/gen/question/question/add', + QUESTION_EDIT: '/gen/question/question/edit', + QUESTION_DELETE: '/gen/question/question/delete', + + // 题目选项管理 + QUESTION_OPTION_ADD: '/gen/questionoption/questionOption/add', + QUESTION_OPTION_EDIT: '/gen/questionoption/questionOption/edit', + QUESTION_OPTION_DELETE: '/gen/questionoption/questionOption/delete', + + // 题目答案管理 + QUESTION_ANSWER_ADD: '/gen/questionanswer/questionAnswer/add', + QUESTION_ANSWER_EDIT: '/gen/questionanswer/questionAnswer/edit', + QUESTION_ANSWER_DELETE: '/gen/questionanswer/questionAnswer/delete', + + // 题库题目关联管理 + QUESTION_REPO_ADD: '/gen/questionrepo/questionRepo/add', + QUESTION_REPO_EDIT: '/gen/questionrepo/questionRepo/edit', + QUESTION_REPO_DELETE: '/gen/questionrepo/questionRepo/delete', + }, // 学习进度相关 LEARNING: { diff --git a/src/api/modules/exam.ts b/src/api/modules/exam.ts new file mode 100644 index 0000000..6b42605 --- /dev/null +++ b/src/api/modules/exam.ts @@ -0,0 +1,289 @@ +// 考试题库相关API接口 +import { ApiRequest } from '../request' +import type { + ApiResponse, + Repo, + Question, + CreateRepoRequest, + UpdateRepoRequest, + CreateQuestionRequest, + UpdateQuestionRequest, + CreateQuestionOptionRequest, + UpdateQuestionOptionRequest, + CreateQuestionAnswerRequest, + UpdateQuestionAnswerRequest, + CreateQuestionRepoRequest, + UpdateQuestionRepoRequest, +} from '../types' + +/** + * 考试题库API模块 + */ +export class ExamApi { + + // ========== 题库管理 ========== + + /** + * 课程新建题库 + */ + static async createCourseRepo(data: CreateRepoRequest): Promise> { + console.log('🚀 创建课程题库:', data) + const response = await ApiRequest.post('/biz/repo/courseAdd', data) + console.log('✅ 创建课程题库成功:', response) + return response + } + + /** + * 获取课程题库 + */ + static async getCourseRepoList(): Promise> { + const response = await ApiRequest.get(`/biz/repo/repoList`) + console.log('✅ 获取课程题库列表成功:', response) + return response + } + + /** + * 删除题库 + */ + static async deleteRepo(id: string): Promise> { + console.log('🚀 删除题库:', { id }) + const response = await ApiRequest.delete('/gen/repo/repo/delete', { + params: { id } + }) + console.log('✅ 删除题库成功:', response) + return response + } + + /** + * 编辑题库 + */ + static async updateRepo(data: UpdateRepoRequest): Promise> { + console.log('🚀 编辑题库:', data) + const response = await ApiRequest.put('/gen/repo/repo/edit', data) + console.log('✅ 编辑题库成功:', response) + return response + } + + // ========== 题目管理 ========== + + /** + * 查询题库下题目 + */ + static async getQuestionsByRepo(repoId: string): Promise> { + console.log('🚀 查询题库下题目:', { repoId }) + const response = await ApiRequest.get(`/biz/repo/questionList/${repoId}`) + console.log('✅ 查询题库下题目成功:', response) + return response + } + + /** + * 查询题目详情 + */ + static async getQuestionDetail(questionId: string): Promise> { + console.log('🚀 查询题目详情:', { questionId }) + const response = await ApiRequest.get(`/biz/repo/repoList/${questionId}`) + console.log('✅ 查询题目详情成功:', response) + return response + } + + /** + * 添加题目 + */ + static async createQuestion(data: CreateQuestionRequest): Promise> { + console.log('🚀 添加题目:', data) + const response = await ApiRequest.post('/gen/question/question/add', data) + console.log('✅ 添加题目成功:', response) + return response + } + + /** + * 编辑题目 + */ + static async updateQuestion(data: UpdateQuestionRequest): Promise> { + console.log('🚀 编辑题目:', data) + const response = await ApiRequest.put('/gen/question/question/edit', data) + console.log('✅ 编辑题目成功:', response) + return response + } + + /** + * 删除题目 + */ + static async deleteQuestion(id: string): Promise> { + console.log('🚀 删除题目:', { id }) + const response = await ApiRequest.delete('/gen/question/question/delete', { + params: { id } + }) + console.log('✅ 删除题目成功:', response) + return response + } + + // ========== 题目选项管理 ========== + + /** + * 添加题目选项 + */ + static async createQuestionOption(data: CreateQuestionOptionRequest): Promise> { + console.log('🚀 添加题目选项:', data) + const response = await ApiRequest.post('/gen/questionoption/questionOption/add', data) + console.log('✅ 添加题目选项成功:', response) + return response + } + + /** + * 编辑题目选项 + */ + static async updateQuestionOption(data: UpdateQuestionOptionRequest): Promise> { + console.log('🚀 编辑题目选项:', data) + const response = await ApiRequest.put('/gen/questionoption/questionOption/edit', data) + console.log('✅ 编辑题目选项成功:', response) + return response + } + + /** + * 删除题目选项 + */ + static async deleteQuestionOption(id: string): Promise> { + console.log('🚀 删除题目选项:', { id }) + const response = await ApiRequest.delete('/gen/questionoption/questionOption/delete', { + params: { id } + }) + console.log('✅ 删除题目选项成功:', response) + return response + } + + // ========== 题目答案管理 ========== + + /** + * 添加题目答案 + */ + static async createQuestionAnswer(data: CreateQuestionAnswerRequest): Promise> { + console.log('🚀 添加题目答案:', data) + const response = await ApiRequest.post('/gen/questionanswer/questionAnswer/add', data) + console.log('✅ 添加题目答案成功:', response) + return response + } + + /** + * 编辑题目答案 + */ + static async updateQuestionAnswer(data: UpdateQuestionAnswerRequest): Promise> { + console.log('🚀 编辑题目答案:', data) + const response = await ApiRequest.put('/gen/questionanswer/questionAnswer/edit', data) + console.log('✅ 编辑题目答案成功:', response) + return response + } + + /** + * 删除题目答案 + */ + static async deleteQuestionAnswer(id: string): Promise> { + console.log('🚀 删除题目答案:', { id }) + const response = await ApiRequest.delete('/gen/questionanswer/questionAnswer/delete', { + params: { id } + }) + console.log('✅ 删除题目答案成功:', response) + return response + } + + // ========== 题库题目关联管理 ========== + + /** + * 添加题库题目关联 + */ + static async createQuestionRepo(data: CreateQuestionRepoRequest): Promise> { + console.log('🚀 添加题库题目关联:', data) + const response = await ApiRequest.post('/gen/questionrepo/questionRepo/add', data) + console.log('✅ 添加题库题目关联成功:', response) + return response + } + + /** + * 编辑题库题目关联 + */ + static async updateQuestionRepo(data: UpdateQuestionRepoRequest): Promise> { + console.log('🚀 编辑题库题目关联:', data) + const response = await ApiRequest.put('/gen/questionrepo/questionRepo/edit', data) + console.log('✅ 编辑题库题目关联成功:', response) + return response + } + + /** + * 删除题库题目关联 + */ + static async deleteQuestionRepo(id: string): Promise> { + console.log('🚀 删除题库题目关联:', { id }) + const response = await ApiRequest.delete('/gen/questionrepo/questionRepo/delete', { + params: { id } + }) + console.log('✅ 删除题库题目关联成功:', response) + return response + } + + // ========== 常用工具方法 ========== + + /** + * 题目类型映射 + */ + static getQuestionTypeText(type: number): string { + const typeMap: Record = { + 0: '单选题', + 1: '多选题', + 2: '判断题', + 3: '填空题', + 4: '简答题', + 5: '复合题' + } + return typeMap[type] || '未知类型' + } + + /** + * 难度等级映射 + */ + static getDifficultyText(difficulty: number): string { + const difficultyMap: Record = { + 1: '简单', + 2: '中等', + 3: '困难' + } + return difficultyMap[difficulty] || '未知难度' + } + + /** + * 批量添加题目选项 + */ + static async batchCreateQuestionOptions( + questionId: string, + options: Omit[] + ): Promise[]> { + console.log('🚀 批量添加题目选项:', { questionId, options }) + + const promises = options.map(option => + this.createQuestionOption({ ...option, questionId }) + ) + + const responses = await Promise.all(promises) + console.log('✅ 批量添加题目选项成功:', responses) + return responses + } + + /** + * 批量添加题目答案 + */ + static async batchCreateQuestionAnswers( + questionId: string, + answers: Omit[] + ): Promise[]> { + console.log('🚀 批量添加题目答案:', { questionId, answers }) + + const promises = answers.map(answer => + this.createQuestionAnswer({ ...answer, questionId }) + ) + + const responses = await Promise.all(promises) + console.log('✅ 批量添加题目答案成功:', responses) + return responses + } +} + +export default ExamApi \ No newline at end of file diff --git a/src/api/types.ts b/src/api/types.ts index 97ad735..cf332b8 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -684,3 +684,134 @@ export interface Statistics { count: number }> } + +// 考试题库相关类型 +export interface Repo { + id: string + title: string + remark: string + questionCount?: number + createBy: string + createTime: string + updateBy: string + updateTime: string +} + +export interface Question { + id: string + parentId: string + type: number // 0单选题 1多选题 2判断题 3填空题 4简答题 5复合题 + content: string + analysis: string + difficulty: number + score: number + createBy: string + createTime: string + updateBy: string + updateTime: string + options?: QuestionOption[] + answers?: QuestionAnswer[] +} + +export interface QuestionOption { + id: string + questionId: string + content: string + izCorrent: number // 是否正确答案 + orderNo: number + createBy: string + createTime: string + updateBy: string + updateTime: string +} + +export interface QuestionAnswer { + id: string + questionId: string + answerText: string + orderNo: number + createBy: string + createTime: string + updateBy: string + updateTime: string +} + +export interface QuestionRepo { + id: string + repoId: string + questionId: string + createBy: string + createTime: string + updateBy: string + updateTime: string +} + +// 题库相关请求参数类型 +export interface CreateRepoRequest { + title: string + remark?: string +} + +export interface UpdateRepoRequest { + id: string + title: string + remark?: string +} + +export interface CreateQuestionRequest { + parentId?: string + type: number + content: string + analysis?: string + difficulty: number + score: number +} + +export interface UpdateQuestionRequest { + id: string + parentId?: string + type: number + content: string + analysis?: string + difficulty: number + score: number +} + +export interface CreateQuestionOptionRequest { + questionId: string + content: string + izCorrent: number + orderNo: number +} + +export interface UpdateQuestionOptionRequest { + id: string + questionId: string + content: string + izCorrent: number + orderNo: number +} + +export interface CreateQuestionAnswerRequest { + questionId: string + answerText: string + orderNo: number +} + +export interface UpdateQuestionAnswerRequest { + id: string + questionId: string + answerText: string + orderNo: number +} + +export interface CreateQuestionRepoRequest { + repoId: string + questionId: string +} + +export interface UpdateQuestionRepoRequest { + id: string + repoId: string + questionId: string +} diff --git a/src/components/admin/ExamComponents/BatchSetScoreModal.vue b/src/components/admin/ExamComponents/BatchSetScoreModal.vue index 561047a..9168882 100644 --- a/src/components/admin/ExamComponents/BatchSetScoreModal.vue +++ b/src/components/admin/ExamComponents/BatchSetScoreModal.vue @@ -12,24 +12,73 @@ class="big-question-section">
-
- {{ bigIndex + 1 }}.{{ subIndex + 1 }} -
- {{ subQuestion.title }} + + + + +
@@ -159,6 +208,33 @@ const updateQuestionScore = (bigIndex: number, subIndex: number, score: number) } }; +// 获取复合题总分 +const getCompositeQuestionTotalScore = (subQuestion: SubQuestion): number => { + if (subQuestion.type === QuestionType.COMPOSITE && subQuestion.subQuestions) { + return subQuestion.subQuestions.reduce((total, sq) => total + (sq.score || 0), 0); + } + return subQuestion.score || 0; +}; + +// 更新复合题子题目分数 +const updateCompositeSubQuestionScore = (bigIndex: number, subIndex: number, compositeIndex: number, score: number) => { + const bigQuestion = questionList.value[bigIndex]; + const subQuestion = bigQuestion?.subQuestions[subIndex]; + + if (subQuestion && subQuestion.type === QuestionType.COMPOSITE && subQuestion.subQuestions) { + const compositeSubQ = subQuestion.subQuestions[compositeIndex]; + if (compositeSubQ) { + compositeSubQ.score = score || 0; + + // 重新计算复合题总分 + subQuestion.score = subQuestion.subQuestions.reduce((total, sq) => total + (sq.score || 0), 0); + + // 重新计算大题总分 + bigQuestion.totalScore = bigQuestion.subQuestions.reduce((total, sub) => total + (sub.score || 0), 0); + } + } +}; + // 取消批量设置 const cancelBatchSet = () => { showModal.value = false; @@ -231,6 +307,41 @@ const confirmBatchSet = () => { border-color: #d1e7dd; } +.composite-question { + background-color: #f0f8ff; + border-left: 3px solid #1890ff; +} + +.composite-sub-question { + margin: 8px 0 8px 20px; + padding: 8px 12px; + border: 1px solid #d0d0d0; + border-radius: 4px; + background-color: #ffffff; + position: relative; +} + +.composite-sub-question::before { + content: "└"; + position: absolute; + left: -15px; + top: 8px; + color: #1890ff; + font-weight: bold; +} + +.composite-sub-header { + font-weight: 500; + color: #666; + font-size: 14px; + margin-bottom: 6px; +} + +.total-score { + font-weight: 600; + color: #1890ff; +} + .question-info { display: flex; align-items: center; diff --git a/src/components/admin/ExamComponents/ExamSettingsModal.vue b/src/components/admin/ExamComponents/ExamSettingsModal.vue index e8e772f..a96c790 100644 --- a/src/components/admin/ExamComponents/ExamSettingsModal.vue +++ b/src/components/admin/ExamComponents/ExamSettingsModal.vue @@ -21,6 +21,11 @@ + +
+ + +
@@ -323,6 +328,8 @@ interface ExamSettings { useLastScore: boolean; // 最后一次练习成绩为最终成绩 }; paperMode: 'show_all' | 'show_current' | 'hide_all'; + // 新增考试人数限制字段 + maxParticipants: number | null; } // Props 定义 @@ -396,6 +403,8 @@ const formData = ref({ useLastScore: false, }, paperMode: 'show_all', + // 新增考试人数限制字段 + maxParticipants: null, }); // 监听 props.examData 变化,更新本地副本 diff --git a/src/components/course/DPlayerVideo.vue b/src/components/course/DPlayerVideo.vue index 30f13af..bf97d14 100644 --- a/src/components/course/DPlayerVideo.vue +++ b/src/components/course/DPlayerVideo.vue @@ -286,11 +286,33 @@ const initializePlayer = async (videoUrl?: string) => { }) player.on('error', (error: any) => { - console.error('DPlayer 播放错误:', error) + // 检查是否为无害的错误 + const isHarmlessError = ( + !error.message || // 没有错误消息通常是无害的 + error.message === undefined || + (player && player.video && !player.video.error) // 播放器本身没有错误 + ) + + if (isHarmlessError && player && player.video && player.video.readyState > 0) { + console.warn('⚠️ 检测到可能的假阳性错误,但视频仍可播放:', { + type: error.type, + message: error.message, + url: url, + videoReadyState: player.video.readyState, + videoPaused: player.video.paused + }) + // 不触发错误事件,因为视频实际上是可以播放的 + return + } + + console.error('❌ DPlayer 播放错误:', error) console.error('错误详情:', { type: error.type, message: error.message, - url: url + url: url, + videoError: player?.video?.error, + networkState: player?.video?.networkState, + readyState: player?.video?.readyState }) emit('error', error) }) @@ -442,7 +464,8 @@ const switchQuality = (quality: any) => { from: getCurrentQualityLabel(), to: quality.label, currentTime: currentTime, - wasPlaying: wasPlaying + wasPlaying: wasPlaying, + newUrl: quality.url }) // 暂停播放 @@ -450,37 +473,30 @@ const switchQuality = (quality: any) => { player.pause() } - // 切换视频源 - if (typeof player.switchVideo === 'function') { - player.switchVideo({ - url: quality.url, - type: 'auto' - }) + // 销毁当前播放器 + if (player) { + player.destroy() + player = null + } - // 恢复播放状态 + // 重新初始化播放器使用新的URL + initializePlayer(quality.url).then(() => { + console.log('✅ 播放器重新初始化完成,新URL:', quality.url) + // 恢复播放时间 setTimeout(() => { if (player && player.video) { player.seek(currentTime) if (wasPlaying) { player.play() } + console.log('✅ 恢复播放状态:', { currentTime, wasPlaying }) } - }, 1000) - } else { - // 重新初始化播放器 - initializePlayer(quality.url).then(() => { - // 恢复播放时间 - setTimeout(() => { - if (player && player.video) { - player.seek(currentTime) - if (wasPlaying) { - player.play() - } - } - }, 500) - }) - } + }, 500) + }).catch(error => { + console.error('❌ 重新初始化播放器失败:', error) + }) + // 通知父组件清晰度已切换 emit('qualityChange', quality.value) console.log('✅ 切换清晰度到:', quality.label) } catch (error) { diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue index e582fad..d01b5c5 100644 --- a/src/components/layout/AppHeader.vue +++ b/src/components/layout/AppHeader.vue @@ -38,10 +38,6 @@ - -
@@ -54,10 +50,10 @@
- + - +
@@ -84,31 +80,32 @@ {{ t('header.learningCenter') }} - +
- +
- + - +
@@ -117,14 +114,11 @@ - - - diff --git a/src/views/teacher/ExamPages/GradingPage.vue b/src/views/teacher/ExamPages/GradingPage.vue index cfc9a62..5436e8e 100644 --- a/src/views/teacher/ExamPages/GradingPage.vue +++ b/src/views/teacher/ExamPages/GradingPage.vue @@ -80,7 +80,7 @@
未答 - + 提交批阅 @@ -176,6 +176,7 @@
@@ -187,6 +188,7 @@
@@ -197,8 +199,21 @@
- - + +
+ + +
+ +
+

阅卷评语:

+
+
+
+
+ 暂无评语 +
+
@@ -208,15 +223,19 @@ + + diff --git a/src/views/teacher/ExamPages/QuestionManagement.vue b/src/views/teacher/ExamPages/QuestionManagement.vue index 8a5cd60..de07558 100644 --- a/src/views/teacher/ExamPages/QuestionManagement.vue +++ b/src/views/teacher/ExamPages/QuestionManagement.vue @@ -1,7 +1,17 @@