From 3d4fa1abb42328cd69c4a68a49ca4e1377125bb0 Mon Sep 17 00:00:00 2001 From: yuk255 Date: Fri, 29 Aug 2025 18:11:47 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=AF=B9naive=20ui=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=9B=BD=E9=99=85=E5=8C=96=E9=85=8D=E7=BD=AE=EF=BC=9B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=80=83=E8=AF=95=E7=9B=B8=E5=85=B3api?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=9B=E8=80=83=E8=AF=95=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E8=8F=9C=E5=8D=95=E4=B8=8B=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=9B6=E7=A7=8D=E9=A2=98=E5=9E=8B?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=8F=90=E5=8F=96=E4=B8=BA=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 36 +- src/api/index.ts | 32 + src/api/modules/exam.ts | 289 +++++ src/api/types.ts | 131 ++ .../ExamComponents/BatchSetScoreModal.vue | 145 ++- .../ExamComponents/ExamSettingsModal.vue | 9 + src/components/teacher/CompositeQuestion.vue | 5 +- src/main.ts | 2 + src/router/index.ts | 21 +- src/views/teacher/AdminDashboard.vue | 12 +- src/views/teacher/ExamPages/AddExam.vue | 932 +++++--------- src/views/teacher/ExamPages/AddQuestion.vue | 380 +++--- src/views/teacher/ExamPages/ExamLibrary.vue | 3 - src/views/teacher/ExamPages/ExamPreview.vue | 200 ++- src/views/teacher/ExamPages/ExamTaking.vue | 1104 +++++++++++++++++ src/views/teacher/ExamPages/GradingPage.vue | 63 +- .../ExamPages/QuestionBankManagement.vue | 648 ++++++++++ .../teacher/ExamPages/QuestionManagement.vue | 75 +- src/views/teacher/ExamPages/StudentList.vue | 10 +- 19 files changed, 3188 insertions(+), 909 deletions(-) create mode 100644 src/api/modules/exam.ts create mode 100644 src/views/teacher/ExamPages/ExamTaking.vue create mode 100644 src/views/teacher/ExamPages/QuestionBankManagement.vue 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/teacher/CompositeQuestion.vue b/src/components/teacher/CompositeQuestion.vue index 67462d3..aab5801 100644 --- a/src/components/teacher/CompositeQuestion.vue +++ b/src/components/teacher/CompositeQuestion.vue @@ -166,7 +166,10 @@ let nextId = 1 // 监听props变化 watch(() => props.modelValue, (newValue) => { if (newValue?.subQuestions) { - subQuestions.value = newValue.subQuestions + subQuestions.value = newValue.subQuestions.map(sq => ({ ...sq })); + console.log('CompositeQuestion props 更新:', { + newSubQuestions: subQuestions.value.map(sq => ({ id: sq.id, title: sq.title, score: sq.score })) + }); } }, { deep: true }) diff --git a/src/main.ts b/src/main.ts index a680591..58457e9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -85,6 +85,7 @@ import { NTimeline, NTimelineItem, NMessageProvider, + NDialogProvider, NPopselect } from 'naive-ui' @@ -159,6 +160,7 @@ const naive = create({ NTimeline, NTimelineItem, NMessageProvider, + NDialogProvider, NPopselect ] }) diff --git a/src/router/index.ts b/src/router/index.ts index 1faa399..a4694c9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -62,6 +62,7 @@ import HomeworkTemplateImport from '@/views/teacher/course/HomeworkTemplateImpor // 考试管理组件 import ExamManagement from '@/views/teacher/ExamPages/ExamPage.vue' +import ExamQuestionBankManagement from '@/views/teacher/ExamPages/QuestionBankManagement.vue' import QuestionManagement from '@/views/teacher/ExamPages/QuestionManagement.vue' import ExamLibrary from '@/views/teacher/ExamPages/ExamLibrary.vue' import MarkingCenter from '@/views/teacher/ExamPages/MarkingCenter.vue' @@ -69,6 +70,7 @@ import AddExam from '@/views/teacher/ExamPages/AddExam.vue' import AddQuestion from '@/views/teacher/ExamPages/AddQuestion.vue' import StudentList from '@/views/teacher/ExamPages/StudentList.vue' import GradingPage from '@/views/teacher/ExamPages/GradingPage.vue' +import ExamTaking from '@/views/teacher/ExamPages/ExamTaking.vue' import ChapterEditor from '@/views/teacher/course/ChapterEditor.vue' @@ -293,10 +295,16 @@ const routes: RouteRecordRaw[] = [ name: 'ExamManagement', component: ExamManagement, meta: { title: '考试管理' }, - redirect: '/teacher/exam-management/question-management', + redirect: '/teacher/exam-management/question-bank', children: [ { - path: 'question-management', + path: 'question-bank', + name: 'ExamQuestionBankManagement', + component: ExamQuestionBankManagement, + meta: { title: '题库管理' } + }, + { + path: 'question-bank/:bankId/questions', name: 'QuestionManagement', component: QuestionManagement, meta: { title: '试题管理' } @@ -347,7 +355,7 @@ const routes: RouteRecordRaw[] = [ meta: { title: '试卷预览' } }, { - path: 'add-question/:id?', + path: 'add-question/:id/:questionId?', name: 'AddQuestionPage', component: AddQuestion, meta: { title: '添加试题' } @@ -357,7 +365,12 @@ const routes: RouteRecordRaw[] = [ ] }, - + { + path: '/taking/:id', + name: 'ExamTaking', + component: ExamTaking, + meta: { title: '考试进行中' } + }, // 帮助中心 { diff --git a/src/views/teacher/AdminDashboard.vue b/src/views/teacher/AdminDashboard.vue index a6734bf..2ee9bd4 100644 --- a/src/views/teacher/AdminDashboard.vue +++ b/src/views/teacher/AdminDashboard.vue @@ -36,10 +36,10 @@