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 }">
-
+ 添加选项
@@ -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 @@
subQuestion.correctAnswer = val"
+ @update:correctAnswer="(val: number | null) => {
+ console.log('🎯 AddExam: 更新正确答案', val, '选项内容:', subQuestion.options?.[val as number]?.content);
+ subQuestion.correctAnswer = val;
+ }"
v-model:title="subQuestion.title" v-model:explanation="subQuestion.explanation" />
@@ -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,