From b20cc50f44381d203cc1c59c1e77f93c3779c4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Thu, 18 Sep 2025 02:18:39 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E8=AE=A8=E8=AE=BA=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E9=A1=B5=E9=9D=A2=E6=A1=86=E6=9E=B6=E6=90=AD=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/modules/exam.ts | 2 +- src/api/types.ts | 62 +++++ .../teacher/SingleChoiceQuestion.vue | 46 +++- src/views/teacher/ExamPages/AddExam.vue | 238 +++++++++++++++--- src/views/teacher/ExamPages/AddQuestion.vue | 12 +- 5 files changed, 309 insertions(+), 51 deletions(-) diff --git a/src/api/modules/exam.ts b/src/api/modules/exam.ts index cb1bc0f..7dc8ca1 100644 --- a/src/api/modules/exam.ts +++ b/src/api/modules/exam.ts @@ -16,7 +16,7 @@ import type { UpdateQuestionAnswerRequest, CreateQuestionRepoRequest, UpdateQuestionRepoRequest, - ExamInfo, + ExamInfo } from '../types' /** diff --git a/src/api/types.ts b/src/api/types.ts index 3a3926b..be28d1a 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -890,3 +890,65 @@ export interface PaperInfo { updateBy: string updateTime: string } + +// 试卷创建请求类型 +export interface CreatePaperRequest { + /** + * 0固定组卷,1随机组卷,组卷模式 + */ + generateMode?: number; + /** + * 及格分 + */ + passScore?: number; + /** + * 题库id + */ + repoId?: string; + /** + * 是否需要批阅:0不需要 1需要,是否需要批阅 + */ + requireReview?: number; + /** + * 组卷规则 + * 示例: { + * "type0_count": 1, "type0_score": 10, + * "type1_count": 20, "type1_score": 20, + * "type2_count": 10, "type2_score": 10, + * "type3_count": 10, "type3_score": 10, + * "type4_count": 5, "type4_score": 5, + * "type5_count": 1, "type5_score": 1 + * } + */ + rules?: string; + /** + * 试卷标题 + */ + title?: string; + /** + * 总分 + */ + totalScore?: number; + [property: string]: any; +} + +// 试卷题目关联请求类型 +export interface AddPaperQuestionRequest { + /** + * 排序 + */ + orderNo?: number; + /** + * 试卷id + */ + paperId?: string; + /** + * 题目id + */ + questionId?: string; + /** + * 分值 + */ + score?: number; + [property: string]: any; +} diff --git a/src/components/teacher/SingleChoiceQuestion.vue b/src/components/teacher/SingleChoiceQuestion.vue index 75e7af5..638b99b 100644 --- a/src/components/teacher/SingleChoiceQuestion.vue +++ b/src/components/teacher/SingleChoiceQuestion.vue @@ -30,20 +30,22 @@ :class="{ 'is-selected': correctAnswer === index }">
- + {{ String.fromCharCode(65 + index) }}
- - - 删除 - + + + 删除 + +
- + 添加选项 @@ -128,8 +130,25 @@ const handleUpdateOption = (index: number, content: string) => { }; // 设置正确答案 -const handleSetCorrectAnswer = (index: number) => { - emit('update:correctAnswer', index); +const handleSetCorrectAnswer = (value: string | null) => { + console.log('🔍 设置正确答案,偏移值:', value, '当前正确答案:', props.correctAnswer); + + try { + // 将偏移后的字符串转换为实际的索引(减1) + const actualIndex = value !== null ? parseInt(value, 10) - 1 : null; + console.log('🔍 实际索引:', actualIndex); + + if (actualIndex !== null && !isNaN(actualIndex) && actualIndex >= 0) { + console.log('🔍 当前选项内容:', props.modelValue[actualIndex]?.content); + console.log('🔍 所有选项:', props.modelValue.map((opt, i) => `${String.fromCharCode(65 + i)}: ${opt.content}`)); + } + + // 发送实际索引给父组件 + emit('update:correctAnswer', actualIndex); + } catch (error) { + console.error('🚨 设置正确答案时出错:', error); + emit('update:correctAnswer', null); + } }; @@ -236,4 +255,5 @@ const handleSetCorrectAnswer = (index: number) => { .correct-radio { color: #52c41a; } + diff --git a/src/views/teacher/ExamPages/AddExam.vue b/src/views/teacher/ExamPages/AddExam.vue index 64fe17d..f733e8f 100644 --- a/src/views/teacher/ExamPages/AddExam.vue +++ b/src/views/teacher/ExamPages/AddExam.vue @@ -139,7 +139,10 @@ @@ -1330,8 +1333,8 @@ const saveExam = async () => { const apiData = { title: examForm.title, generateMode: examForm.type === 1 ? 0 : 1, // 0: 固定试卷组, 1: 随机抽题组卷 - rules: '', // 组卷规则,暂时为空 - repoId: '', // 题库ID,暂时为空 + rules: examForm.type === 1 ? '' : generateRandomRules(), // 固定组卷不需要规则,随机组卷需要规则 + repoId: examForm.type === 1 ? '' : '', // 固定组卷不需要题库ID,随机组卷需要题库ID totalScore: examForm.totalScore, passScore: examForm.passScore || Math.floor(examForm.totalScore * 0.6), // 及格分 requireReview: examForm.useAIGrading ? 1 : 0 // 是否需要批阅 @@ -1453,6 +1456,36 @@ const convertNumberToQuestionType = (type: number): QuestionType => { return typeMap[type] || QuestionType.SINGLE_CHOICE; }; +// 将前端难度转换为API需要的数字类型 +const getDifficultyNumber = (difficulty: string): number => { + const difficultyMap: Record = { + 'easy': 1, // 简单 + 'medium': 2, // 中等 + 'hard': 3 // 困难 + }; + return difficultyMap[difficulty] || 1; +}; + +// 生成随机组卷规则(暂时返回空字符串,因为我们先实现固定组卷) +const generateRandomRules = (): string => { + // 随机组卷规则示例: + // { + // "type0_count": 1, // 单选题数量 + // "type0_score": 10, // 单选题分数 + // "type1_count": 20, // 多选题数量 + // "type1_score": 20, // 多选题分数 + // "type2_count": 10, // 判断题数量 + // "type2_score": 10, // 判断题分数 + // "type3_count": 10, // 填空题数量 + // "type3_score": 10, // 填空题分数 + // "type4_count": 5, // 简答题数量 + // "type4_score": 5, // 简答题分数 + // "type5_count": 1, // 复合题数量 + // "type5_score": 1 // 复合题分数 + // } + return ''; +}; + // 保存试卷题目 const saveExamQuestions = async (paperId: string) => { try { @@ -1484,12 +1517,12 @@ const saveExamQuestions = async (paperId: string) => { try { // 创建题目 const questionData = { - repoId: '1958492351656955905', // 使用从题库模态框获取的题库ID + repoId: '', // 固定组卷模式下不需要题库ID parentId: '', // 父题目ID,暂时为空 type: convertQuestionTypeToNumber(subQuestion.type), // 题目类型 content: subQuestion.title || '', // 题干 - analysis: '', // 题目解析,暂时为空 - difficulty: 1, // 难度,默认1 + analysis: subQuestion.explanation || '', // 题目解析 + difficulty: getDifficultyNumber(subQuestion.difficulty), // 难度 score: subQuestion.score || 0, // 分值 degree: 1, // 程度,默认1 ability: 1 // 能力,默认1 @@ -1512,39 +1545,182 @@ const saveExamQuestions = async (paperId: string) => { console.log('✅ 题目创建成功,ID:', questionId); console.log('📊 题目响应详情:', questionResponse); - // 创建题目选项(如果是选择题) - if (subQuestion.options && subQuestion.options.length > 0) { - console.log('📊 题目选项数据:', subQuestion.options); - for (let i = 0; i < subQuestion.options.length; i++) { - const option = subQuestion.options[i]; - console.log('📊 处理选项:', option); + // 根据题目类型处理选项和答案 + const questionType = convertQuestionTypeToNumber(subQuestion.type); - const optionData = { + if (questionType === 0 || questionType === 1 || questionType === 2) { + // 单选题、多选题、判断题 - 创建选项 + console.log('📊 处理选择题选项,题目类型:', questionType); + + if (questionType === 0) { + // 单选题 + if (subQuestion.options && subQuestion.options.length > 0) { + console.log('📊 单选题选项数据:', subQuestion.options); + for (let i = 0; i < subQuestion.options.length; i++) { + const option = subQuestion.options[i]; + const optionData = { + questionId: questionId, + content: option.content || '', + izCorrent: subQuestion.correctAnswer === i ? 1 : 0, + orderNo: i + }; + console.log('📝 创建单选题选项:', optionData); + await ExamApi.createQuestionOption(optionData); + } + } + } else if (questionType === 1) { + // 多选题 + if (subQuestion.options && subQuestion.options.length > 0) { + console.log('📊 多选题选项数据:', subQuestion.options); + for (let i = 0; i < subQuestion.options.length; i++) { + const option = subQuestion.options[i]; + const isCorrect = subQuestion.correctAnswers && subQuestion.correctAnswers.includes(i); + const optionData = { + questionId: questionId, + content: option.content || '', + izCorrent: isCorrect ? 1 : 0, + orderNo: i + }; + console.log('📝 创建多选题选项:', optionData); + await ExamApi.createQuestionOption(optionData); + } + } + } else if (questionType === 2) { + // 判断题 + const optionData1 = { questionId: questionId, - content: (option as any).text || option.content || '', - izCorrent: (option as any).isCorrect ? 1 : 0, - orderNo: i + 1 + content: '正确', + izCorrent: subQuestion.trueFalseAnswer === true ? 1 : 0, + orderNo: 0 }; + const optionData2 = { + questionId: questionId, + content: '错误', + izCorrent: subQuestion.trueFalseAnswer === false ? 1 : 0, + orderNo: 1 + }; + console.log('📝 创建判断题选项:', optionData1, optionData2); + await ExamApi.createQuestionOption(optionData1); + await ExamApi.createQuestionOption(optionData2); + } + } else if (questionType === 3 || questionType === 4) { + // 填空题、简答题 - 创建答案 + console.log('📊 处理主观题答案,题目类型:', questionType); - console.log('📝 创建题目选项:', optionData); - await ExamApi.createQuestionOption(optionData); + if (questionType === 3) { + // 填空题 + if (subQuestion.fillBlanks && subQuestion.fillBlanks.length > 0) { + console.log('📊 填空题答案数据:', subQuestion.fillBlanks); + for (let i = 0; i < subQuestion.fillBlanks.length; i++) { + const blank = subQuestion.fillBlanks[i]; + const answerData = { + questionId: questionId, + answerText: blank.content || '', + orderNo: i + }; + console.log('📝 创建填空题答案:', answerData); + await ExamApi.createQuestionAnswer(answerData); + } + } + } else if (questionType === 4) { + // 简答题 + if (subQuestion.textAnswer) { + const answerData = { + questionId: questionId, + answerText: subQuestion.textAnswer, + orderNo: 0 + }; + console.log('📝 创建简答题答案:', answerData); + await ExamApi.createQuestionAnswer(answerData); + } + } + } else if (questionType === 5) { + // 复合题 - 递归处理子题目 + console.log('📊 处理复合题,题目类型:', questionType); + + if (subQuestion.subQuestions && subQuestion.subQuestions.length > 0) { + console.log('📊 复合题子题目数据:', subQuestion.subQuestions); + + // 为复合题的每个子题目创建题目、选项/答案 + for (let subIndex = 0; subIndex < subQuestion.subQuestions.length; subIndex++) { + const compositeSubQuestion = subQuestion.subQuestions[subIndex]; + console.log('📊 处理复合题子题目:', compositeSubQuestion.title, '类型:', compositeSubQuestion.type); + + // 创建子题目 + const subQuestionData = { + repoId: '', + parentId: questionId, // 设置父题目ID为复合题的ID + type: convertQuestionTypeToNumber(compositeSubQuestion.type), + content: compositeSubQuestion.title || '', + analysis: compositeSubQuestion.explanation || '', + difficulty: getDifficultyNumber(compositeSubQuestion.difficulty), + score: compositeSubQuestion.score || 0, + degree: 1, + ability: 1 + }; + + console.log('📝 创建复合题子题目:', subQuestionData); + const subQuestionResponse = await ExamApi.createQuestion(subQuestionData); + + if (subQuestionResponse.data) { + const subQuestionId = subQuestionResponse.data; + console.log('✅ 复合题子题目创建成功,ID:', subQuestionId); + + // 根据子题目类型创建选项或答案 + const subQuestionType = convertQuestionTypeToNumber(compositeSubQuestion.type); + + if (subQuestionType === 0 || subQuestionType === 1 || subQuestionType === 2) { + // 选择题类型的子题目 - 创建选项 + if (compositeSubQuestion.options && compositeSubQuestion.options.length > 0) { + for (let i = 0; i < compositeSubQuestion.options.length; i++) { + const option = compositeSubQuestion.options[i]; + const isCorrect = subQuestionType === 0 ? + compositeSubQuestion.correctAnswer === i : + subQuestionType === 1 ? + compositeSubQuestion.correctAnswers && compositeSubQuestion.correctAnswers.includes(i) : + compositeSubQuestion.trueFalseAnswer === (i === 0); + + const optionData = { + questionId: subQuestionId, + content: option.content || '', + izCorrent: isCorrect ? 1 : 0, + orderNo: i + }; + console.log('📝 创建复合题子题目选项:', optionData); + await ExamApi.createQuestionOption(optionData); + } + } + } else if (subQuestionType === 3 || subQuestionType === 4) { + // 主观题类型的子题目 - 创建答案 + if (subQuestionType === 3 && compositeSubQuestion.fillBlanks) { + // 填空题 + for (let i = 0; i < compositeSubQuestion.fillBlanks.length; i++) { + const blank = compositeSubQuestion.fillBlanks[i]; + const answerData = { + questionId: subQuestionId, + answerText: blank.content || '', + orderNo: i + }; + console.log('📝 创建复合题子题目填空答案:', answerData); + await ExamApi.createQuestionAnswer(answerData); + } + } else if (subQuestionType === 4 && compositeSubQuestion.textAnswer) { + // 简答题 + const answerData = { + questionId: subQuestionId, + answerText: compositeSubQuestion.textAnswer, + orderNo: 0 + }; + console.log('📝 创建复合题子题目简答答案:', answerData); + await ExamApi.createQuestionAnswer(answerData); + } + } + } + } } } - // 创建题目答案(如果有答案) - if (subQuestion.correctAnswer !== null && subQuestion.correctAnswer !== undefined) { - console.log('📊 题目答案数据:', subQuestion.correctAnswer); - const answerData = { - questionId: questionId, - answerText: String(subQuestion.correctAnswer), - orderNo: 1 - }; - console.log('📝 创建题目答案:', answerData); - await ExamApi.createQuestionAnswer(answerData); - } else { - console.log('📝 题目无答案,跳过答案创建'); - } // 将题目关联到试卷 const paperQuestionData = { diff --git a/src/views/teacher/ExamPages/AddQuestion.vue b/src/views/teacher/ExamPages/AddQuestion.vue index 9d8d4d4..7d9d0ff 100644 --- a/src/views/teacher/ExamPages/AddQuestion.vue +++ b/src/views/teacher/ExamPages/AddQuestion.vue @@ -828,7 +828,7 @@ const updateQuestionOptions = async (questionId: string, questionType: number) = id: existingOptionsIds.value[index] || '', content: option.content || '', izCorrent: index === correctAnswer ? 1 : 0, - orderNo: index + orderNo: index + 1 // orderNo从1开始 })).filter(option => option.id); // 只更新有ID的选项 } else if (questionType === 1) { // 多选题 @@ -839,7 +839,7 @@ const updateQuestionOptions = async (questionId: string, questionType: number) = id: existingOptionsIds.value[index] || '', content: option.content || '', izCorrent: correctAnswers.includes(index) ? 1 : 0, - orderNo: index + orderNo: index + 1 // orderNo从1开始 })).filter(option => option.id); // 只更新有ID的选项 } else if (questionType === 2) { // 判断题 @@ -1249,10 +1249,10 @@ const renderSingleChoiceData = (options: any[]) => { content: option.content || '' })); - // 设置正确答案 + // 设置正确答案 - 将orderNo转换为数组索引(orderNo从1开始,索引从0开始) const correctOption = sortedOptions.find(option => option.izCorrent === 1); if (correctOption) { - questionForm.correctAnswer = correctOption.orderNo; + questionForm.correctAnswer = correctOption.orderNo - 1; // 转换为数组索引 } console.log('✅ 单选题渲染完成:', { @@ -1279,10 +1279,10 @@ const renderMultipleChoiceData = (options: any[]) => { content: option.content || '' })); - // 设置正确答案(可能有多个) + // 设置正确答案(可能有多个) - 将orderNo转换为数组索引 questionForm.correctAnswers = sortedOptions .filter(option => option.izCorrent === 1) - .map(option => option.orderNo); + .map(option => option.orderNo - 1); // 转换为数组索引 console.log('✅ 多选题渲染完成:', { options: questionForm.options,