1382 lines
45 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';
// 路由和消息
const router = useRouter();
const route = useRoute();
const message = useMessage();
// 编辑模式判断
const questionId = route.params.id as string | undefined;
const isEditMode = ref(!!questionId);
// 表单引用
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: 'easy' },
{ label: '中等', value: 'medium' },
{ label: '困难', value: 'hard' }
]);
// 试题表单数据
const questionForm = reactive({
type: 'single_choice', // 默认单选题
category: '',
difficulty: '',
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: true,
message: '请选择分类',
trigger: 'change'
},
difficulty: {
required: true,
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 saveQuestion = async () => {
try {
// 表单验证
await formRef.value?.validate();
// 验证答案设置
if (!validateAnswers()) {
return;
}
saving.value = true;
// 构建保存数据
const saveData = buildSaveData();
// 模拟保存API - 根据编辑模式调用不同接口
if (isEditMode.value && questionId) {
await mockUpdateQuestion(questionId, saveData);
console.log('更新试题数据:', saveData);
message.success('试题更新成功');
} else {
await mockCreateQuestion(saveData);
console.log('保存试题数据:', saveData);
message.success('试题保存成功');
}
// 返回试题管理页面
router.push('/teacher/exam-management/question-management');
} 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 validateAnswers = (): boolean => {
console.log(questionForm);
if (questionForm.title === "") {
message.error('请填写题目内容');
return false;
}
switch (questionForm.type) {
case 'single_choice':
if (questionForm.correctAnswer === null) {
message.error('请设置单选题的正确答案');
return false;
}
break;
case 'multiple_choice':
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;
};
// 构建保存数据
const buildSaveData = () => {
const baseData = {
type: questionForm.type,
category: questionForm.category,
difficulty: questionForm.difficulty,
score: questionForm.score,
title: questionForm.title,
explanation: questionForm.explanation
};
switch (questionForm.type) {
case 'single_choice':
case 'multiple_choice':
return {
...baseData,
options: questionForm.options,
correctAnswer: questionForm.type === 'single_choice' ? questionForm.correctAnswer : undefined,
correctAnswers: questionForm.type === 'multiple_choice' ? questionForm.correctAnswers : undefined
};
case 'true_false':
return {
...baseData,
answer: questionForm.trueFalseAnswer
};
case 'fill_blank':
return {
...baseData,
answers: questionForm.fillBlankAnswers.filter(answer => answer.content.trim())
};
case 'short_answer':
return {
...baseData,
answer: questionForm.shortAnswer
};
case 'composite':
return {
...baseData,
compositeData: questionForm.compositeData
};
default:
return baseData;
}
};
// 组件挂载
onMounted(async () => {
// 如果是编辑模式,加载题目数据
if (isEditMode.value && questionId) {
await loadQuestionData(questionId);
}
});
// 加载题目数据
const loadQuestionData = async (id: string) => {
try {
// 模拟API调用
const response = await mockGetQuestionById(id);
if (response.success && response.data) {
const questionData = response.data as any; // 使用any类型避免复杂的类型定义
// 回显基本信息
questionForm.type = questionData.type;
questionForm.category = questionData.category;
questionForm.difficulty = questionData.difficulty;
questionForm.score = questionData.score;
questionForm.title = questionData.title;
questionForm.explanation = questionData.explanation || '';
// 根据题型回显具体数据
switch (questionData.type) {
case 'single_choice':
questionForm.options = questionData.options || [];
questionForm.correctAnswer = questionData.correctAnswer;
break;
case 'multiple_choice':
questionForm.options = questionData.options || [];
questionForm.correctAnswers = questionData.correctAnswers || [];
break;
case 'true_false':
questionForm.trueFalseAnswer = questionData.answer;
break;
case 'fill_blank':
questionForm.fillBlankAnswers = questionData.answers || [];
break;
case 'short_answer':
questionForm.shortAnswer = questionData.answer || '';
break;
case 'composite':
questionForm.compositeData = questionData.compositeData || { subQuestions: [] };
break;
}
message.success('题目数据加载成功');
} else {
message.error('加载题目数据失败');
}
} catch (error) {
console.error('加载题目数据错误:', error);
message.error('加载题目数据失败,请检查网络连接');
}
};
// 模拟获取题目数据的API
const mockGetQuestionById = async (id: string) => {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟返回数据
const mockQuestions = {
'1': {
type: 'single_choice',
category: 'exam',
difficulty: 'medium',
score: 5,
title: '以下哪个是Vue.js的核心特性',
explanation: 'Vue.js的核心特性包括响应式数据绑定、组件系统等',
options: [
{ content: '响应式数据绑定' },
{ content: '虚拟DOM' },
{ content: '组件系统' },
{ content: '以上都是' }
],
correctAnswer: 3
},
'2': {
type: 'composite',
category: 'exam',
difficulty: 'hard',
score: 20,
title: '阅读以下关于JavaScript的材料并回答相关问题。\\n\\nJavaScript是一种高级的、解释型的编程语言具有动态类型、原型继承等特性。它最初被设计用于网页开发但现在已经扩展到服务器端、移动应用等多个领域。',
explanation: '这是一道综合性的复合题',
compositeData: {
subQuestions: [
{
id: 1,
type: 'single',
title: 'JavaScript属于什么类型的编程语言',
score: 5,
data: [
{ option: '编译型', id: 1 },
{ option: '解释型', id: 2 },
{ option: '汇编型', id: 3 },
{ option: '机器型', id: 4 }
],
correctAnswer: 1,
explanation: 'JavaScript是解释型编程语言'
},
{
id: 2,
type: 'multiple',
title: 'JavaScript有哪些特性',
score: 10,
data: [
{ option: '动态类型', id: 1 },
{ option: '原型继承', id: 2 },
{ option: '静态类型', id: 3 },
{ option: '面向对象', id: 4 }
],
correctAnswers: [0, 1, 3],
explanation: 'JavaScript具有动态类型、原型继承和面向对象等特性'
}
]
}
}
};
const question = mockQuestions[id as keyof typeof mockQuestions];
if (question) {
return {
success: true,
data: question
};
} else {
return {
success: false,
message: '题目不存在'
};
}
};
// 模拟创建题目的API
const mockCreateQuestion = async (data: any) => {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 这里应该调用真实的API
console.log('创建题目:', data);
return {
success: true,
data: { id: Math.random().toString(36).substr(2, 9) }
};
};
// 模拟更新题目的API
const mockUpdateQuestion = async (id: string, data: any) => {
// 模拟API延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 这里应该调用真实的API
console.log('更新题目:', id, data);
return {
success: true,
data: { id }
};
};
const getDifficultyLabel = (difficulty: string): string => {
const difficultyMap: { [key: string]: string } = {
'easy': '简单',
'medium': '中等',
'hard': '困难'
};
return difficultyMap[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>