1575 lines
51 KiB
Vue
1575 lines
51 KiB
Vue
<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> |