1575 lines
51 KiB
Vue
Raw Normal View History

<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>