1575 lines
51 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="add-question-container">
<div class="header-section">
<div class="header-left">
<n-button quaternary circle size="large" @click="goBack" class="back-button">
<template #icon>
<n-icon>
<ArrowBackOutline />
</n-icon>
</template>
</n-button>
<h1 class="title">{{ isEditMode ? '编辑试题' : '添加试题' }}</h1>
</div>
<div class="header-actions">
<n-button @click="goBack" ghost>取消</n-button>
<n-button type="primary" @click="saveQuestion" :loading="saving">
{{ saving ? (isEditMode ? '更新中...' : '保存中...') : (isEditMode ? '更新试题' : '保存试题') }}
</n-button>
</div>
</div>
<div class="main-content">
<!-- 左侧编辑区域 -->
<div class="form-container">
<n-form ref="formRef" :model="questionForm" :rules="formRules" label-placement="left" label-width="auto"
require-mark-placement="right-hanging">
<!-- 题型选择 -->
<div class="form-section">
<n-card size="small">
<div class="question-type-tabs">
<h3 class="section-title required">题库类型</h3>
<button v-for="option in questionTypeOptions" :key="option.value" :class="[
'type-tab-button',
{ 'active': questionForm.type === option.value }
]" @click="handleTypeChange(option.value)">
{{ option.label }}
</button>
</div>
</n-card>
</div>
<!-- 题型选项设置 -->
<QuestionTypeContainer v-if="questionForm.type" :questionType="questionForm.type"
v-model:options="questionForm.options" v-model:correctAnswer="questionForm.correctAnswer"
v-model:correctAnswers="questionForm.correctAnswers"
v-model:trueFalseAnswer="questionForm.trueFalseAnswer"
v-model:fillBlankAnswers="questionForm.fillBlankAnswers"
v-model:shortAnswer="questionForm.shortAnswer"
v-model:compositeData="questionForm.compositeData"
v-model:title="questionForm.title"
v-model:explanation="questionForm.explanation" />
<!-- 基本信息 -->
<div class="form-section">
<n-card size="small">
<n-form-item label="分类" path="category">
<n-select v-model:value="questionForm.category" :options="categoryOptions"
placeholder="请选择分类" />
</n-form-item>
<n-form-item label="难度" path="difficulty">
<n-select v-model:value="questionForm.difficulty" :options="difficultyOptions"
placeholder="请选择难度" />
</n-form-item>
<n-form-item label="分值" path="score">
<n-input type="number" v-model:value="questionForm.score" :min="1" :max="100"
placeholder="请输入分值" />
</n-form-item>
</n-card>
</div>
</n-form>
</div>
<!-- 右侧预览区域 -->
<div class="preview-container">
<div class="preview-header">
<h3>题目预览</h3>
</div>
<div class="preview-content">
<div v-if="!questionForm.title && !questionForm.type" class="preview-empty">
<n-empty description="请选择题型并输入题目内容" />
</div>
<div v-else class="question-preview">
<!-- 题目序号和内容 -->
<div class="question-header">
<span class="question-number">1.</span>
<span class="question-text">{{ questionForm.title || '请输入题目内容...' }}</span>
</div>
<!-- 单选题选项 -->
<div v-if="questionForm.type === 'single_choice'" class="preview-options">
<div v-for="(option, index) in questionForm.options" :key="index"
class="preview-option-item"
:class="{ 'correct-answer': questionForm.correctAnswer === index }">
<span class="option-letter">{{ String.fromCharCode(65 + index) }}</span>
<span class="option-content">{{ option.content || '请输入内容' }}</span>
</div>
</div>
<!-- 多选题选项 -->
<div v-if="questionForm.type === 'multiple_choice'" class="preview-options">
<div v-for="(option, index) in questionForm.options" :key="index"
class="preview-option-item"
:class="{ 'correct-answer': questionForm.correctAnswers.includes(index) }">
<span class="option-letter">{{ String.fromCharCode(65 + index) }}</span>
<span class="option-content">{{ option.content || '请输入内容' }}</span>
</div>
</div>
<!-- 判断题选项 -->
<div v-if="questionForm.type === 'true_false'" class="preview-options">
<div class="preview-option-item"
:class="{ 'correct-answer': questionForm.trueFalseAnswer === true }">
<span class="option-letter">A</span>
<span class="option-content"></span>
</div>
<div class="preview-option-item"
:class="{ 'correct-answer': questionForm.trueFalseAnswer === false }">
<span class="option-letter">B</span>
<span class="option-content"></span>
</div>
</div>
<!-- 填空题答案 -->
<div v-if="questionForm.type === 'fill_blank'" class="preview-fill-blanks">
<div v-for="(answer, index) in questionForm.fillBlankAnswers" :key="index"
class="preview-fill-blank-item">
<div class="blank-number">{{ index + 1 }}.</div>
<div class="blank-content">
<span class="blank-answer">{{ answer.content || '请输入答案' }}</span>
<span class="blank-score">({{ answer.score }})</span>
<span v-if="answer.caseSensitive" class="blank-case">区分大小写</span>
</div>
</div>
</div>
<!-- 简答题答案 -->
<div v-if="questionForm.type === 'short_answer'" class="preview-short-answer">
<div class="short-answer-label">参考答案</div>
<div class="short-answer-content">
{{ questionForm.shortAnswer || '请输入参考答案' }}
</div>
</div>
<!-- 复合题预览 -->
<div v-if="questionForm.type === 'composite'" class="preview-composite">
<div v-if="questionForm.compositeData.subQuestions.length === 0" class="composite-empty">
<span class="empty-text">请添加小题</span>
</div>
<div v-else class="composite-sub-questions">
<div v-for="(subQuestion, index) in questionForm.compositeData.subQuestions"
:key="subQuestion.id"
class="composite-sub-question">
<div class="sub-question-header">
<span class="sub-question-number">{{ index + 1 }}.</span>
<span class="sub-question-title">{{ subQuestion.title || '请输入小题内容...' }}</span>
<span class="sub-question-type">[{{ getQuestionTypeLabel(subQuestion.type) }}]</span>
<span class="sub-question-score">({{ subQuestion.score }})</span>
</div>
<!-- 小题选项预览 -->
<div v-if="subQuestion.type === 'single' && subQuestion.data" class="sub-question-options">
<div v-for="(option, optIndex) in subQuestion.data" :key="optIndex"
class="preview-option-item"
:class="{ 'correct-answer': subQuestion.correctAnswer === optIndex }">
<span class="option-letter">{{ String.fromCharCode(65 + optIndex) }}</span>
<span class="option-content">{{ option.option || '请输入内容' }}</span>
</div>
</div>
<div v-else-if="subQuestion.type === 'multiple' && subQuestion.data" class="sub-question-options">
<div v-for="(option, optIndex) in subQuestion.data" :key="optIndex"
class="preview-option-item"
:class="{ 'correct-answer': subQuestion.correctAnswers && subQuestion.correctAnswers.includes(optIndex) }">
<span class="option-letter">{{ String.fromCharCode(65 + optIndex) }}</span>
<span class="option-content">{{ option.option || '请输入内容' }}</span>
</div>
</div>
<div v-else-if="subQuestion.type === 'truefalse'" class="sub-question-options">
<div class="preview-option-item"
:class="{ 'correct-answer': subQuestion.answer === true }">
<span class="option-letter">A</span>
<span class="option-content"></span>
</div>
<div class="preview-option-item"
:class="{ 'correct-answer': subQuestion.answer === false }">
<span class="option-letter">B</span>
<span class="option-content"></span>
</div>
</div>
<div v-else-if="subQuestion.type === 'fillblank' && subQuestion.answers" class="sub-question-fill-blanks">
<div v-for="(answer, answerIndex) in subQuestion.answers" :key="answerIndex"
class="preview-fill-blank-item">
<div class="blank-number">{{ answerIndex + 1 }}.</div>
<div class="blank-content">
<span class="blank-answer">{{ answer.value || '请输入答案' }}</span>
<span class="blank-score">({{ answer.score }})</span>
<span v-if="answer.caseSensitive" class="blank-case">区分大小写</span>
</div>
</div>
</div>
<div v-else-if="subQuestion.type === 'shortanswer'" class="sub-question-short-answer">
<div class="short-answer-label">参考答案</div>
<div class="short-answer-content">
{{ subQuestion.data || '请输入参考答案' }}
</div>
</div>
<!-- 小题解析 -->
<div v-if="subQuestion.explanation" class="sub-question-explanation">
<div class="explanation-label">解析</div>
<div class="explanation-text">{{ subQuestion.explanation }}</div>
</div>
</div>
</div>
</div>
<!-- 答案解析 -->
<div v-if="questionForm.explanation" class="preview-explanation">
<div class="explanation-label">答案解析</div>
<div class="explanation-text">{{ questionForm.explanation }}</div>
</div>
<!-- 底部信息 -->
<div class="preview-footer">
<div class="info-row">
<span class="info-label">分类</span>
<span class="info-value">{{ getCategoryLabel(questionForm.category) || '' }}</span>
</div>
<div class="info-row">
<span class="info-label">难度</span>
<span class="info-value">{{ getDifficultyLabel(questionForm.difficulty) || '' }}</span>
</div>
<div class="info-row">
<span class="info-label">分值</span>
<span class="info-value">{{ questionForm.score || null }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useMessage } from 'naive-ui';
import { ArrowBackOutline } from '@vicons/ionicons5';
import QuestionTypeContainer from '@/components/teacher/QuestionTypeContainer.vue';
import { ExamApi } from '@/api';
// 路由和消息
const router = useRouter();
const route = useRoute();
const message = useMessage();
// 编辑模式判断
const questionId = route.params.questionId as string | undefined;
const isEditMode = ref(!!questionId);
// 题目类型映射
const getQuestionTypeKey = (type: number): string => {
const typeMap: { [key: number]: string } = {
0: 'single_choice', // 单选题
1: 'multiple_choice', // 多选题
2: 'true_false', // 判断题
3: 'fill_blank', // 填空题
4: 'short_answer', // 简答题
5: 'composite' // 复合题
};
return typeMap[type] || 'single_choice';
};
// 表单引用
const formRef = ref();
const saving = ref(false);
// 题目类型选项
const questionTypeOptions = ref([
{ label: '单选题', value: 'single_choice' },
{ label: '多选题', value: 'multiple_choice' },
{ label: '判断题', value: 'true_false' },
{ label: '填空题', value: 'fill_blank' },
{ label: '简答题', value: 'short_answer' },
{ label: '复合题', value: 'composite' }
]);
// 分类选项(从试题管理页面同步)
const categoryOptions = ref([
{ label: '分类试题', value: 'category' },
{ label: '考试试题', value: 'exam' },
{ label: '练习试题', value: 'practice' },
{ label: '模拟试题', value: 'simulation' }
]);
// 难度选项
const difficultyOptions = ref([
{ label: '简单', value: 0 },
{ label: '中等', value: 1 },
{ label: '困难', value: 2 }
]);
// 试题表单数据
const questionForm = reactive({
type: 'single_choice', // 默认单选题
category: '',
difficulty: 0, // 默认简单
score: 10,
title: '',
options: [
{ content: '' },
{ content: '' },
{ content: '' },
{ content: '' }
],
correctAnswer: null as number | null, // 单选题正确答案索引
correctAnswers: [] as number[], // 多选题正确答案索引数组
trueFalseAnswer: null as boolean | null, // 判断题答案
fillBlankAnswers: [{ content: '', score: 1, caseSensitive: false }] as Array<{content: string, score: number, caseSensitive: boolean}>, // 填空题答案
shortAnswer: '', // 简答题答案
compositeData: { subQuestions: [] } as { subQuestions: any[] }, // 复合题数据
explanation: '' // 解析
});
// 表单验证规则
const formRules = {
type: {
required: true,
message: '请选择题型',
trigger: 'change'
},
category: {
required: false,
message: '请选择分类',
trigger: 'change'
},
difficulty: {
required: true,
type: 'number',
message: '请选择难度',
trigger: 'change'
},
score: {
required: true,
type: 'number',
message: '请输入分值',
trigger: 'blur'
},
title: {
required: true,
message: '请输入题目描述',
trigger: 'blur'
}
};
// 题型变化处理
const handleTypeChange = (type: string) => {
// 如果题型没有变化,直接返回
if (questionForm.type === type) {
return;
}
// 设置新的题型
questionForm.type = type;
// 只在新增模式下重置数据,编辑模式下保留现有数据
if (!isEditMode.value) {
// 重置相关数据
questionForm.correctAnswer = null;
questionForm.correctAnswers = [];
questionForm.trueFalseAnswer = null;
questionForm.fillBlankAnswers = [{ content: '', score: 1, caseSensitive: false }];
questionForm.shortAnswer = '';
questionForm.compositeData = { subQuestions: [] };
// 根据题型设置选项
if (type === 'single_choice' || type === 'multiple_choice') {
// 重新初始化选项确保有至少2个选项
questionForm.options = [
{ content: '' },
{ content: '' }
];
} else {
// 对于非选择题,清空选项
questionForm.options = [];
}
}
};
// 返回上一页
const goBack = () => {
router.back();
};
// 类型映射函数:将字符串类型转换为数字类型
const getQuestionTypeNumber = (type: string): number => {
const typeMap: Record<string, number> = {
'single_choice': 0, // 单选题
'multiple_choice': 1, // 多选题
'true_false': 2, // 判断题
'fill_blank': 3, // 填空题
'short_answer': 4, // 简答题
'composite': 5 // 复合题
};
return typeMap[type] || 0;
};
// 难度映射函数:将数字难度转换为数字(保持原值)
const getDifficultyNumber = (difficulty: number | string): number => {
// 如果已经是数字,直接返回
if (typeof difficulty === 'number') {
return difficulty;
}
// 如果是字符串,进行转换
const difficultyMap: Record<string, number> = {
'easy': 0, // 简单
'medium': 1, // 中等
'hard': 2 // 困难
};
return difficultyMap[difficulty] || 0;
};
// 保存试题
const saveQuestion = async () => {
try {
// 表单验证
await formRef.value?.validate();
// 验证答案设置
if (!validateAnswers()) {
return;
}
saving.value = true;
// 获取题库ID可能从路由参数或者查询参数获取
// 如果从题库管理页面跳转过来应该有bankId或者通过其他方式传递
let bankId = route.params.bankId as string || route.params.id as string || route.query.bankId as string;
if (!bankId) {
// 尝试从浏览器历史记录或者本地存储获取
const referrer = document.referrer;
const bankIdMatch = referrer.match(/question-bank\/([^\/]+)\/questions/);
if (bankIdMatch) {
bankId = bankIdMatch[1];
}
}
if (!bankId) {
message.error('缺少题库ID参数请从题库管理页面进入');
return;
}
// 根据编辑模式调用不同接口
if (isEditMode.value && questionId) {
// TODO: 实现编辑模式的接口调用
message.info('编辑模式暂未实现');
return;
} else {
// 新增模式 - 按照接口调用顺序执行
await createNewQuestion(bankId);
}
} catch (error: any) {
console.error('保存试题失败:', error);
// 区分不同类型的错误
if (Array.isArray(error) && error.length > 0) {
// Naive UI 表单验证错误(数组格式)
const firstError = error[0]; // 获取第一个错误
if (firstError && firstError[0] && firstError[0].message) {
message.error(`表单验证失败:${firstError[0].message}`);
} else {
message.error('请检查表单填写是否完整');
}
} else if (error?.message) {
// 有具体错误信息的系统错误
message.error(`保存失败:${error.message}`);
} else if (error?.code) {
// 有错误码的情况
message.error(`保存失败,错误代码:${error.code}`);
} else {
// 未知错误
message.error('保存失败,请检查网络连接或联系管理员');
}
} finally {
saving.value = false;
}
};
// 创建新题目的完整流程
const createNewQuestion = async (bankId: string) => {
try {
// 只调用一次API创建题目
const questionData = {
parentId: null, // 父题目ID普通题目为null
type: getQuestionTypeNumber(questionForm.type),
content: questionForm.title,
analysis: questionForm.explanation || '',
difficulty: getDifficultyNumber(questionForm.difficulty),
score: questionForm.score
};
console.log('🚀 创建题目,数据:', questionData);
const response = await ExamApi.createQuestion(questionData);
console.log('📊 创建题目API响应:', response);
// 处理API响应
let success = false;
let questionId = null;
if (response.data) {
const apiResponse = response.data as any;
// 检查是否是包装格式 {success, code, result}
if (typeof apiResponse === 'object' && ('success' in apiResponse || 'code' in apiResponse)) {
success = apiResponse.success === true || apiResponse.code === 200 || apiResponse.code === 0;
questionId = apiResponse.result || apiResponse.data;
} else {
// 直接是题目ID
success = true;
questionId = apiResponse;
}
}
if (!success) {
throw new Error('创建题目失败');
}
console.log('✅ 题目创建成功题目ID:', questionId);
message.success('题目保存成功');
// 返回题库管理页面
router.push(`/teacher/exam-management/question-bank/${bankId}/questions`);
} catch (error: any) {
console.error('创建题目流程失败:', error);
throw error;
}
};
// 处理单选题的选项和答案
const handleSingleChoiceQuestion = async (questionId: string) => {
try {
// 添加选项
const optionPromises = questionForm.options.map((option, index) => {
const isCorrect = questionForm.correctAnswer === index ? 1 : 0;
return ExamApi.createQuestionOption({
questionId,
content: option.content,
izCorrent: isCorrect,
orderNo: index + 1
});
});
console.log('🚀 第二步:添加选项,选项数量:', questionForm.options.length);
await Promise.all(optionPromises);
console.log('✅ 选项添加成功');
// 添加正确答案
if (questionForm.correctAnswer !== null) {
const correctOption = questionForm.options[questionForm.correctAnswer];
const answerData = {
questionId,
answerText: correctOption.content,
orderNo: 1
};
console.log('🚀 第三步:添加答案:', answerData);
await ExamApi.createQuestionAnswer(answerData);
console.log('✅ 答案添加成功');
}
} catch (error: any) {
console.error('处理单选题失败:', error);
throw error;
}
};
// 验证答案设置
const validateAnswers = (): boolean => {
console.log(questionForm);
if (questionForm.title === "") {
message.error('请填写题目内容');
return false;
}
switch (questionForm.type) {
case 'single_choice':
// 检查选项内容
if (questionForm.options.length < 2) {
message.error('单选题至少需要2个选项');
return false;
}
if (questionForm.options.some(option => !option.content.trim())) {
message.error('请填写所有选项的内容');
return false;
}
if (questionForm.correctAnswer === null) {
message.error('请设置单选题的正确答案');
return false;
}
break;
case 'multiple_choice':
// 检查选项内容
if (questionForm.options.length < 2) {
message.error('多选题至少需要2个选项');
return false;
}
if (questionForm.options.some(option => !option.content.trim())) {
message.error('请填写所有选项的内容');
return false;
}
if (questionForm.correctAnswers.length === 0) {
message.error('请设置多选题的正确答案');
return false;
}
break;
case 'true_false':
if (questionForm.trueFalseAnswer === null) {
message.error('请设置判断题的正确答案');
return false;
}
break;
case 'fill_blank':
if (questionForm.fillBlankAnswers.length === 0 || questionForm.fillBlankAnswers.every(answer => !answer.content.trim())) {
message.error('请设置填空题的参考答案');
return false;
}
break;
case 'short_answer':
if (!questionForm.shortAnswer.trim()) {
message.error('请设置简答题的参考答案');
return false;
}
break;
case 'composite':
return validateCompositeQuestion();
}
return true;
};
// 验证复合题设置
const validateCompositeQuestion = (): boolean => {
const compositeData = questionForm.compositeData;
// 检查是否有小题
if (!compositeData.subQuestions || compositeData.subQuestions.length === 0) {
message.error('复合题至少需要添加一道小题');
return false;
}
// 逐个验证小题
for (let i = 0; i < compositeData.subQuestions.length; i++) {
const subQuestion = compositeData.subQuestions[i];
const subIndex = i + 1;
// 检查小题题型是否选择
if (!subQuestion.type) {
message.error(`${subIndex}小题请选择题型`);
return false;
}
// 检查小题标题
if (!subQuestion.title || !subQuestion.title.trim()) {
message.error(`${subIndex}小题请输入题目内容`);
return false;
}
// 根据小题题型验证答案
switch (subQuestion.type) {
case 'single':
if (!subQuestion.data || subQuestion.data.length < 2) {
message.error(`${subIndex}小题单选题至少需要2个选项`);
return false;
}
// 检查选项内容
let validOptionsCount = 0;
for (let j = 0; j < subQuestion.data.length; j++) {
if (subQuestion.data[j].option && subQuestion.data[j].option.trim()) {
validOptionsCount++;
}
}
if (validOptionsCount < 2) {
message.error(`${subIndex}小题单选题至少需要2个有效选项`);
return false;
}
// 检查是否设置了正确答案
if (subQuestion.correctAnswer === null || subQuestion.correctAnswer === undefined) {
message.error(`${subIndex}小题(单选题)请设置正确答案`);
return false;
}
// 检查正确答案索引是否有效
if (subQuestion.correctAnswer >= subQuestion.data.length || subQuestion.correctAnswer < 0) {
message.error(`${subIndex}小题(单选题)正确答案设置错误`);
return false;
}
break;
case 'multiple':
if (!subQuestion.data || subQuestion.data.length < 2) {
message.error(`${subIndex}小题多选题至少需要2个选项`);
return false;
}
// 检查选项内容
let validMultipleOptionsCount = 0;
for (let j = 0; j < subQuestion.data.length; j++) {
if (subQuestion.data[j].option && subQuestion.data[j].option.trim()) {
validMultipleOptionsCount++;
}
}
if (validMultipleOptionsCount < 2) {
message.error(`${subIndex}小题多选题至少需要2个有效选项`);
return false;
}
// 检查是否设置了正确答案
if (!subQuestion.correctAnswers || subQuestion.correctAnswers.length === 0) {
message.error(`${subIndex}小题(多选题)请设置正确答案`);
return false;
}
// 检查正确答案索引是否有效
for (let j = 0; j < subQuestion.correctAnswers.length; j++) {
if (subQuestion.correctAnswers[j] >= subQuestion.data.length || subQuestion.correctAnswers[j] < 0) {
message.error(`${subIndex}小题(多选题)正确答案设置错误`);
return false;
}
}
break;
case 'truefalse':
if (!subQuestion.answer) {
message.error(`${subIndex}小题(判断题)请设置正确答案`);
return false;
}
break;
case 'fillblank':
if (!subQuestion.answers || subQuestion.answers.length === 0) {
message.error(`${subIndex}小题(填空题)请至少添加一个答案`);
return false;
}
// 检查每个填空答案
for (let j = 0; j < subQuestion.answers.length; j++) {
if (!subQuestion.answers[j].value || !subQuestion.answers[j].value.trim()) {
message.error(`${subIndex}小题(填空题)第${j + 1}个答案内容不能为空`);
return false;
}
}
break;
case 'shortanswer':
if (!subQuestion.data || !subQuestion.data.trim()) {
message.error(`${subIndex}小题(简答题)请设置参考答案`);
return false;
}
break;
default:
message.error(`${subIndex}小题题型设置错误`);
return false;
}
// 检查分值设置
if (!subQuestion.score || subQuestion.score <= 0) {
message.error(`${subIndex}小题请设置有效的分值`);
return false;
}
}
return true;
};
// 组件挂载
onMounted(async () => {
console.log('AddQuestion 组件挂载完成');
console.log('编辑模式:', isEditMode.value);
console.log('题目ID:', questionId);
// 如果是编辑模式,加载题目数据
if (isEditMode.value && questionId) {
// 优先从路由参数中获取数据
if (route.query.questionData) {
try {
const questionData = JSON.parse(route.query.questionData as string);
console.log('📊 从路由参数获取题目数据:', questionData);
renderQuestionData(questionData);
} catch (error) {
console.error('❌ 解析路由参数中的题目数据失败:', error);
// 如果解析失败尝试从API加载
await loadQuestionData(questionId);
}
} else {
// 如果没有路由参数从API加载
await loadQuestionData(questionId);
}
}
});
// 渲染题目数据
const renderQuestionData = (questionData: any) => {
console.log('🎨 开始渲染题目数据:', questionData);
if (!questionData) return;
const { question, answer = [], children = [] } = questionData;
if (question) {
// 设置基本信息
questionForm.type = getQuestionTypeKey(question.type);
questionForm.title = question.content || '';
questionForm.explanation = question.analysis || '';
questionForm.score = question.score || 10;
questionForm.difficulty = question.difficulty || 0;
console.log('📝 题目基本信息:', {
type: questionForm.type,
title: questionForm.title,
explanation: questionForm.explanation,
score: questionForm.score
});
// 根据题目类型处理选项和答案
if (question.type === 0) { // 单选题
renderSingleChoiceData(answer);
} else if (question.type === 1) { // 多选题
renderMultipleChoiceData(answer);
} else if (question.type === 2) { // 判断题
renderTrueFalseData(answer);
} else if (question.type === 3) { // 填空题
renderFillBlankData(answer);
} else if (question.type === 4) { // 简答题
renderShortAnswerData(answer);
} else if (question.type === 5) { // 复合题
renderCompositeData(children);
}
}
};
// 渲染单选题数据
const renderSingleChoiceData = (answers: any[]) => {
console.log('🔘 渲染单选题数据:', answers);
if (answers && answers.length > 0) {
// 按orderNo排序
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
// 设置选项
questionForm.options = sortedAnswers.map(answer => ({
content: answer.content || ''
}));
// 设置正确答案orderNo从1开始数组索引从0开始
const correctAnswer = sortedAnswers.find(answer => answer.izCorrent === 1);
if (correctAnswer) {
questionForm.correctAnswer = correctAnswer.orderNo - 1;
}
console.log('✅ 单选题渲染完成:', {
options: questionForm.options,
correctAnswer: questionForm.correctAnswer
});
}
};
// 渲染多选题数据
const renderMultipleChoiceData = (answers: any[]) => {
console.log('☑️ 渲染多选题数据:', answers);
if (answers && answers.length > 0) {
// 按orderNo排序
const sortedAnswers = answers.sort((a, b) => a.orderNo - b.orderNo);
// 设置选项
questionForm.options = sortedAnswers.map(answer => ({
content: answer.content || ''
}));
// 设置正确答案(可能有多个)
questionForm.correctAnswers = sortedAnswers
.filter(answer => answer.izCorrent === 1)
.map(answer => answer.orderNo - 1);
console.log('✅ 多选题渲染完成:', {
options: questionForm.options,
correctAnswers: questionForm.correctAnswers
});
}
};
// 渲染判断题数据
const renderTrueFalseData = (answers: any[]) => {
console.log('✔️ 渲染判断题数据:', answers);
if (answers && answers.length > 0) {
const correctAnswer = answers.find(answer => answer.izCorrent === 1);
if (correctAnswer) {
// 假设判断题的正确答案content为"正确"或"错误"
questionForm.trueFalseAnswer = correctAnswer.content === '正确' || correctAnswer.content === 'true';
}
console.log('✅ 判断题渲染完成:', {
trueFalseAnswer: questionForm.trueFalseAnswer
});
}
};
// 渲染填空题数据
const renderFillBlankData = (answers: any[]) => {
console.log('📝 渲染填空题数据:', answers);
if (answers && answers.length > 0) {
questionForm.fillBlankAnswers = answers.map(answer => ({
content: answer.content || '',
score: 1,
caseSensitive: false
}));
console.log('✅ 填空题渲染完成:', {
fillBlankAnswers: questionForm.fillBlankAnswers
});
}
};
// 渲染简答题数据
const renderShortAnswerData = (answers: any[]) => {
console.log('📄 渲染简答题数据:', answers);
if (answers && answers.length > 0) {
questionForm.shortAnswer = answers[0]?.content || '';
console.log('✅ 简答题渲染完成:', {
shortAnswer: questionForm.shortAnswer
});
}
};
// 渲染复合题数据
const renderCompositeData = (children: any[]) => {
console.log('🔗 渲染复合题数据:', children);
if (children && children.length > 0) {
questionForm.compositeData = {
subQuestions: children.map((child: any) => ({
id: child.question?.id,
type: getQuestionTypeKey(child.question?.type || 0),
title: child.question?.content || '',
explanation: child.question?.analysis || '',
score: child.question?.score || 1,
options: child.answer || [],
correctAnswer: null,
correctAnswers: []
}))
};
console.log('✅ 复合题渲染完成:', {
subQuestions: questionForm.compositeData.subQuestions.length
});
}
};
// 加载题目数据(编辑模式使用)
const loadQuestionData = async (id: string) => {
try {
console.log('🚀 开始加载题目数据题目ID:', id);
// 调用题目详情接口
const response = await ExamApi.getQuestionDetail(id);
console.log('📊 题目详情API响应:', response);
// 处理API响应
let questionData = null;
let success = false;
if (response.data) {
const apiResponse = response.data as any;
// 检查是否是包装格式 {success, code, result}
if (typeof apiResponse === 'object' && 'result' in apiResponse) {
success = apiResponse.success === true || apiResponse.code === 200 || apiResponse.code === 0;
questionData = apiResponse.result;
} else {
// 直接是题目数据
success = true;
questionData = apiResponse;
}
}
if (success && questionData) {
console.log('✅ 获取题目详情成功,开始渲染数据');
renderQuestionData(questionData);
} else {
console.error('❌ 获取题目详情失败');
message.error('获取题目详情失败');
}
} catch (error) {
console.error('❌ 加载题目数据异常:', error);
message.error('加载题目数据失败,请检查网络连接');
}
};
const getDifficultyLabel = (difficulty: number | string): string => {
const difficultyMap: { [key: number]: string } = {
0: '简单',
1: '中等',
2: '困难'
};
// 如果是数字,直接映射
if (typeof difficulty === 'number') {
return difficultyMap[difficulty] || '未知';
}
// 如果是字符串,先转换为数字再映射
const stringMap: { [key: string]: string } = {
'easy': '简单',
'medium': '中等',
'hard': '困难'
};
return stringMap[difficulty] || difficulty;
};
const getCategoryLabel = (category: string): string => {
const categoryMap: { [key: string]: string } = {
'category': '分类试题',
'exam': '考试试题',
'practice': '练习试题',
'simulation': '模拟试题'
};
return categoryMap[category] || category;
};
const getQuestionTypeLabel = (type: string): string => {
const typeMap: { [key: string]: string } = {
'single': '单选题',
'multiple': '多选题',
'truefalse': '判断题',
'fillblank': '填空题',
'shortanswer': '简答题'
};
return typeMap[type] || type;
};
</script>
<style scoped>
.add-question-container {
background-color: #fff;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #E6E6E6;
background-color: #fff;
position: sticky;
top: 0;
z-index: 10;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.title {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #333;
}
.header-actions {
display: flex;
gap: 12px;
}
/* 主体内容区域 */
.main-content {
display: flex;
flex: 1;
gap: 24px;
padding: 24px;
background-color: #f8f9fa;
min-height: calc(100vh - 80px);
}
/* 左侧表单区域 */
.form-container {
flex: 1;
background-color: #fff;
border-radius: 2px;
padding: 20px;
height: fit-content;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
}
/* 右侧预览区域 */
.preview-container {
width: 420px;
background-color: #fff;
border-radius: 2px;
height: fit-content;
position: sticky;
top: 100px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border: 1px solid #e8f4fd;
}
.preview-header {
padding: 20px 24px;
border-bottom: 1px solid #f0f0f0;
}
.preview-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.preview-content {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
.preview-empty {
text-align: center;
padding: 40px 20px;
}
/* 题目预览样式 */
.question-preview {
font-size: 14px;
line-height: 1.6;
}
.question-header {
margin-bottom: 20px;
display: flex;
gap: 8px;
align-items: center
}
.question-number {
font-weight: 600;
color: #333;
font-size: 16px;
margin-top: 2px;
}
.question-text {
flex: 1;
color: #333;
font-size: 14px;
line-height: 1.5;
}
.preview-options {
margin-bottom: 20px;
}
.preview-option-item {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
padding: 8px 0;
}
.preview-option-item.correct-answer .option-letter {
background-color: #1890ff;
color: white;
}
.option-letter {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background-color: #f5f5f5;
border-radius: 50%;
font-size: 12px;
font-weight: 500;
color: #666;
margin-top: 2px;
flex-shrink: 0;
}
.option-content {
flex: 1;
color: #333;
font-size: 14px;
line-height: 1.5;
}
.preview-explanation {
margin-bottom: 20px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
}
.explanation-label {
font-weight: 500;
color: #333;
margin-bottom: 6px;
font-size: 14px;
}
.explanation-text {
color: #666;
font-size: 14px;
line-height: 1.5;
}
.preview-fill-blanks {
margin-bottom: 20px;
}
.preview-fill-blank-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
}
.blank-number {
font-weight: 500;
color: #1890ff;
font-size: 14px;
min-width: 24px;
}
.blank-content {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.blank-answer {
font-weight: 500;
color: #333;
background-color: #e6f7ff;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #91d5ff;
}
.blank-score {
font-size: 12px;
color: #52c41a;
font-weight: 500;
}
.blank-case {
font-size: 12px;
color: #fa8c16;
background-color: #fff7e6;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #ffd591;
}
.preview-short-answer {
margin-bottom: 20px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
}
.short-answer-label {
font-weight: 500;
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.short-answer-content {
color: #666;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
background-color: #fff;
padding: 12px;
border-radius: 4px;
border: 1px solid #e0e0e0;
min-height: 60px;
}
/* 复合题预览样式 */
.preview-composite {
margin-bottom: 20px;
}
.composite-empty {
padding: 20px;
text-align: center;
background-color: #f8f9fa;
border-radius: 6px;
border: 1px dashed #d9d9d9;
}
.empty-text {
color: #999;
font-size: 14px;
}
.composite-sub-questions {
display: flex;
flex-direction: column;
gap: 16px;
}
.composite-sub-question {
background-color: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #e8e8e8;
}
.sub-question-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #e0e0e0;
}
.sub-question-number {
font-weight: 600;
color: #333;
min-width: 25px;
}
.sub-question-title {
flex: 1;
font-size: 14px;
color: #333;
}
.sub-question-type {
font-size: 12px;
color: #666;
background-color: #fff;
padding: 2px 6px;
border-radius: 3px;
border: 1px solid #d9d9d9;
}
.sub-question-score {
font-size: 12px;
color: #1890ff;
font-weight: 500;
}
.sub-question-options {
margin-bottom: 12px;
}
.sub-question-fill-blanks {
margin-bottom: 12px;
}
.sub-question-short-answer {
margin-bottom: 12px;
padding: 8px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.sub-question-explanation {
margin-top: 8px;
padding: 8px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.preview-footer {
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 14px;
color: #666;
margin-right: 8px;
min-width: 40px;
}
.info-value {
font-size: 14px;
color: #333;
font-weight: 500;
}
.form-section {
margin-bottom: 16px;
padding: 0;
background-color: transparent;
border-radius: 0;
}
.section-header {
display: flex;
align-items: center;
gap: 12px;
}
.section-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background-color: #1890ff;
color: white;
border-radius: 50%;
font-size: 14px;
font-weight: 500;
}
.section-title {
margin: 0;
font-size: 16px;
font-weight: 500;
color: #333;
padding-bottom: 8px;
}
.required::before {
content: '*';
color: red;
}
.question-type-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.question-type-tabs h3{
padding: 0;
}
.type-tab-button {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 6px;
background-color: #fff;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.type-tab-button:hover {
border-color: #1890ff;
color: #1890ff;
}
.type-tab-button.active {
background-color: #1890ff;
border-color: #1890ff;
color: white;
}
.type-tab-button.active:hover {
background-color: #096dd9;
border-color: #096dd9;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
gap: 16px;
}
.preview-container {
width: 100%;
position: static;
order: -1;
}
}
@media (max-width: 768px) {
.header-section {
flex-direction: column;
align-items: stretch;
gap: 16px;
padding: 16px;
}
.header-left {
justify-content: space-between;
}
.header-actions {
justify-content: center;
}
.main-content {
padding: 16px;
}
.form-container {
padding: 20px;
}
.form-section {
margin-bottom: 20px;
}
}
@media (max-width: 480px) {
.title {
font-size: 18px;
}
.option-header {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.option-label-wrapper {
justify-content: space-between;
}
}
</style>