2025-08-22 16:42:55 +08:00
|
|
|
|
<template>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<div class="exam-container">
|
|
|
|
|
<n-space vertical>
|
|
|
|
|
<div class="header-section">
|
|
|
|
|
<div class="header-content">
|
|
|
|
|
<n-button
|
|
|
|
|
quaternary
|
|
|
|
|
circle
|
|
|
|
|
size="large"
|
|
|
|
|
@click="goBack"
|
|
|
|
|
class="back-button"
|
|
|
|
|
>
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<ArrowBackOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
<h1>添加试卷</h1>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<n-card size="small">
|
|
|
|
|
<div class="group required">
|
|
|
|
|
组卷方式:
|
|
|
|
|
<n-tag :checked="examType" checkable @click="changeType(1)"
|
|
|
|
|
style="margin-right: 20px; border: 1px solid #F1F3F4;">固定试卷组</n-tag>
|
|
|
|
|
<n-tag :checked="!examType" checkable @click="changeType(2)"
|
|
|
|
|
style="margin-right: 20px; border: 1px solid #F1F3F4;">随机抽题组卷</n-tag>
|
2025-08-22 16:42:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
<div class="group required">
|
|
|
|
|
<n-row>试卷名称:</n-row>
|
|
|
|
|
<n-input v-model:value="examForm.title" type="textarea" placeholder="请输入试卷名称" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<n-row class="flex justify-between">
|
|
|
|
|
<n-button type="primary" ghost @click="addBigQuestion">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<AddCircle />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
添加大题
|
|
|
|
|
</n-button>
|
|
|
|
|
|
|
|
|
|
<n-button strong secondary>
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<SettingsOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
试卷设置
|
|
|
|
|
</n-button>
|
|
|
|
|
</n-row>
|
|
|
|
|
</n-card>
|
|
|
|
|
<template v-for="(item, index) in examForm.questions" :key="index">
|
|
|
|
|
<n-card size="small">
|
|
|
|
|
<div class="group">
|
|
|
|
|
<n-row>第{{ index + 1 }}题:</n-row>
|
|
|
|
|
<div class="questionRow">
|
|
|
|
|
<n-input class="input-title" v-model:value="item.title" placeholder="请输入题目名称" />
|
|
|
|
|
<n-button strong quaternary @click="deleteBigQuestion(index)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
删除
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-button strong quaternary :disabled="index === 0"
|
|
|
|
|
@click="moveBigQuestion(index, index - 1)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
|
|
|
|
|
<path
|
|
|
|
|
d="M334 624h46.9c10.2 0 19.9-4.9 25.9-13.2L512 465.4l105.2 145.4c6 8.3 15.6 13.2 25.9 13.2H690c6.5 0 10.3-7.4 6.5-12.7l-178-246a7.95 7.95 0 0 0-12.9 0l-178 246A7.96 7.96 0 0 0 334 624z"
|
|
|
|
|
fill="currentColor"></path>
|
|
|
|
|
<path
|
|
|
|
|
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"
|
|
|
|
|
fill="currentColor"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
上移
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-button strong quaternary :disabled="index === examForm.questions.length - 1"
|
|
|
|
|
@click="moveBigQuestion(index, index + 1)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon class="iconRotate">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
|
|
|
|
|
<path
|
|
|
|
|
d="M334 624h46.9c10.2 0 19.9-4.9 25.9-13.2L512 465.4l105.2 145.4c6 8.3 15.6 13.2 25.9 13.2H690c6.5 0 10.3-7.4 6.5-12.7l-178-246a7.95 7.95 0 0 0-12.9 0l-178 246A7.96 7.96 0 0 0 334 624z"
|
|
|
|
|
fill="currentColor"></path>
|
|
|
|
|
<path
|
|
|
|
|
d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"
|
|
|
|
|
fill="currentColor"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
下移
|
|
|
|
|
</n-button>
|
|
|
|
|
|
|
|
|
|
<div class="total flex justify-between">
|
|
|
|
|
共{{ item.subQuestions.length }}题,合计{{ item.totalScore }}分
|
|
|
|
|
<n-icon class="iconRotate">
|
|
|
|
|
<ChevronUpSharp />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="group" v-if="item.subQuestions.length === 0">
|
|
|
|
|
<div class="empty_tip">请为当前大题添加题目</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 小题列表显示 -->
|
|
|
|
|
<div v-else>
|
|
|
|
|
<div v-for="(subQuestion, subIndex) in item.subQuestions" :key="subQuestion.id"
|
|
|
|
|
class="sub-question-item">
|
|
|
|
|
<!-- 小题标题栏 -->
|
|
|
|
|
<div class="sub-question-header">
|
|
|
|
|
<span class="sub-question-number">*{{ index + 1 }}.{{ subIndex + 1 }} {{
|
|
|
|
|
getQuestionTypeName(subQuestion.type) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 题目内容输入 -->
|
|
|
|
|
<div class="sub-question-content">
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<n-input v-model:value="subQuestion.title" type="textarea" placeholder="请输入题目内容"
|
|
|
|
|
style="flex: 1;" />
|
2025-08-22 16:42:55 +08:00
|
|
|
|
<n-button size="small" quaternary @click="deleteSubQuestion(index, subIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 根据题型显示不同的编辑界面 -->
|
|
|
|
|
<!-- 单选题 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'single_choice'" class="question-options">
|
|
|
|
|
<div v-for="(option, optionIndex) in subQuestion.options" :key="option.id"
|
|
|
|
|
class="option-item">
|
|
|
|
|
<n-radio :checked="subQuestion.correctAnswer === option.id"
|
|
|
|
|
@update:checked="updateSingleChoice(index, subIndex, option.id)"
|
|
|
|
|
:name="`question_${subQuestion.id}`">
|
|
|
|
|
{{ String.fromCharCode(65 + optionIndex) }}
|
|
|
|
|
</n-radio>
|
|
|
|
|
<n-input v-model:value="option.content" placeholder="请输入选项内容"
|
|
|
|
|
style="flex: 1; margin-left: 8px; margin-right: 8px;" />
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteOption(index, subIndex, optionIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" quaternary @click="addOption(index, subIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加选项
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多选题 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'multiple_choice'" class="question-options">
|
|
|
|
|
<div v-for="(option, optionIndex) in subQuestion.options" :key="option.id"
|
|
|
|
|
class="option-item">
|
|
|
|
|
<n-checkbox
|
|
|
|
|
:checked="Array.isArray(subQuestion.correctAnswer) && subQuestion.correctAnswer.includes(option.id)"
|
|
|
|
|
@update:checked="updateMultipleChoice(index, subIndex, option.id, $event)">
|
|
|
|
|
{{ String.fromCharCode(65 + optionIndex) }}
|
|
|
|
|
</n-checkbox>
|
|
|
|
|
<n-input v-model:value="option.content" placeholder="请输入选项内容"
|
|
|
|
|
style="flex: 1; margin-left: 8px; margin-right: 8px;" />
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteOption(index, subIndex, optionIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" quaternary @click="addOption(index, subIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加选项
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 判断题 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'true_false'" class="question-options">
|
|
|
|
|
<n-space>
|
|
|
|
|
<n-radio :checked="subQuestion.trueFalseAnswer === true"
|
|
|
|
|
@update:checked="updateTrueFalse(index, subIndex, true)"
|
|
|
|
|
:name="`question_${subQuestion.id}`">
|
|
|
|
|
正确
|
|
|
|
|
</n-radio>
|
|
|
|
|
<n-radio :checked="subQuestion.trueFalseAnswer === false"
|
|
|
|
|
@update:checked="updateTrueFalse(index, subIndex, false)"
|
|
|
|
|
:name="`question_${subQuestion.id}`">
|
|
|
|
|
错误
|
|
|
|
|
</n-radio>
|
|
|
|
|
</n-space>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 填空题 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'fill_blank'" class="question-options">
|
|
|
|
|
<div v-for="(blank, blankIndex) in subQuestion.fillBlanks" :key="blank.id"
|
|
|
|
|
class="fill-blank-item">
|
|
|
|
|
<span>第{{ blankIndex + 1 }}个填空答案:</span>
|
|
|
|
|
<n-input v-model:value="blank.content" placeholder="请输入正确答案"
|
|
|
|
|
style="flex: 1; margin-left: 8px; margin-right: 8px;" />
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteFillBlank(index, subIndex, blankIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" dashed @click="addFillBlank(index, subIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加填空
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 简答题 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'short_answer'" class="question-options">
|
|
|
|
|
<div class="answer-section">
|
|
|
|
|
<span>参考答案:</span>
|
|
|
|
|
<n-input v-model:value="subQuestion.textAnswer" type="textarea"
|
|
|
|
|
placeholder="请输入参考答案" :autosize="{ minRows: 3, maxRows: 8 }" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 复合题子题目 -->
|
|
|
|
|
<div v-if="subQuestion.type === 'composite' && subQuestion.subQuestions"
|
|
|
|
|
class="composite-sub-questions">
|
|
|
|
|
<n-divider />
|
|
|
|
|
<div v-for="(compositeSubQ, compSubIndex) in subQuestion.subQuestions"
|
|
|
|
|
:key="compositeSubQ.id" class="composite-sub-item">
|
|
|
|
|
<div class="composite-sub-header">
|
|
|
|
|
<div class="sub-question-number">
|
|
|
|
|
<span>{{ compSubIndex + 1 }}. </span>
|
|
|
|
|
<n-select v-model:value="compositeSubQ.type" :options="[
|
|
|
|
|
{ label: '单选题', value: 'single_choice' },
|
|
|
|
|
{ label: '多选题', value: 'multiple_choice' },
|
|
|
|
|
{ label: '判断题', value: 'true_false' }
|
|
|
|
|
]" size="small" style="width: 100px; margin-right: 8px;"
|
|
|
|
|
@update:value="changeCompositeSubQuestionType(index, subIndex, compSubIndex, $event)" />
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteCompositeSubQuestion(index, subIndex, compSubIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 子题目内容 -->
|
|
|
|
|
<div class="composite-sub-content">
|
|
|
|
|
<n-input v-model:value="compositeSubQ.title" type="textarea"
|
|
|
|
|
placeholder="请输入子题目内容" :autosize="{ minRows: 2, maxRows: 4 }"
|
|
|
|
|
style="margin-bottom: 12px;" />
|
|
|
|
|
|
|
|
|
|
<!-- 单选题选项 -->
|
|
|
|
|
<div v-if="compositeSubQ.type === 'single_choice'" class="composite-options">
|
|
|
|
|
<div v-for="(option, optionIndex) in compositeSubQ.options" :key="option.id"
|
|
|
|
|
class="option-item">
|
|
|
|
|
<n-radio :name="`composite_question_${compositeSubQ.id}`"
|
|
|
|
|
:checked="compositeSubQ.correctAnswer === option.id"
|
|
|
|
|
@update:checked="updateCompositeChoice(index, subIndex, compSubIndex, option.id)">
|
|
|
|
|
{{ String.fromCharCode(65 + optionIndex) }}
|
|
|
|
|
</n-radio>
|
|
|
|
|
<n-input v-model:value="option.content" placeholder="请输入选项内容"
|
|
|
|
|
style="flex: 1; margin-left: 8px; margin-right: 8px;" />
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteCompositeOption(index, subIndex, compSubIndex, optionIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" dashed
|
|
|
|
|
@click="addCompositeOption(index, subIndex, compSubIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加选项
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 多选题选项 -->
|
|
|
|
|
<div v-if="compositeSubQ.type === 'multiple_choice'" class="composite-options">
|
|
|
|
|
<div v-for="(option, optionIndex) in compositeSubQ.options" :key="option.id"
|
|
|
|
|
class="option-item">
|
|
|
|
|
<n-checkbox
|
|
|
|
|
:checked="Array.isArray(compositeSubQ.correctAnswer) && compositeSubQ.correctAnswer.includes(option.id)"
|
|
|
|
|
@update:checked="updateCompositeMultiChoice(index, subIndex, compSubIndex, option.id, $event)">
|
|
|
|
|
{{ String.fromCharCode(65 + optionIndex) }}
|
|
|
|
|
</n-checkbox>
|
|
|
|
|
<n-input v-model:value="option.content" placeholder="请输入选项内容"
|
|
|
|
|
style="flex: 1; margin-left: 8px; margin-right: 8px;" />
|
|
|
|
|
<n-button size="small" quaternary
|
|
|
|
|
@click="deleteCompositeOption(index, subIndex, compSubIndex, optionIndex)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<TrashOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" dashed
|
|
|
|
|
@click="addCompositeOption(index, subIndex, compSubIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加选项
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 判断题 -->
|
|
|
|
|
<div v-if="compositeSubQ.type === 'true_false'" class="composite-options">
|
|
|
|
|
<n-space>
|
|
|
|
|
<n-radio :checked="compositeSubQ.trueFalseAnswer === true"
|
|
|
|
|
@update:checked="updateCompositeTrueFalse(index, subIndex, compSubIndex, true)"
|
|
|
|
|
:name="`composite_question_${compositeSubQ.id}`">
|
|
|
|
|
正确
|
|
|
|
|
</n-radio>
|
|
|
|
|
<n-radio :checked="compositeSubQ.trueFalseAnswer === false"
|
|
|
|
|
@update:checked="updateCompositeTrueFalse(index, subIndex, compSubIndex, false)"
|
|
|
|
|
:name="`composite_question_${compositeSubQ.id}`">
|
|
|
|
|
错误
|
|
|
|
|
</n-radio>
|
|
|
|
|
</n-space>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<n-button size="small" dashed @click="addCompositeSubQuestion(index, subIndex)"
|
|
|
|
|
class="add-option-btn">
|
|
|
|
|
+ 添加子题目
|
|
|
|
|
</n-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 答案解析 -->
|
|
|
|
|
<div class="explanation-section">
|
|
|
|
|
<span>答案解析:</span>
|
|
|
|
|
<n-input v-model:value="subQuestion.explanation" type="textarea"
|
|
|
|
|
placeholder="请输入答案解析(选填)" :autosize="{ minRows: 2, maxRows: 4 }" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 底部操作按钮 -->
|
|
|
|
|
<div class="sub-question-footer">
|
|
|
|
|
<div class="sub-footer-item">
|
|
|
|
|
分数:
|
|
|
|
|
<n-input-number v-model:value="subQuestion.score" size="small" :min="0"
|
|
|
|
|
style="width: 80px" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sub-footer-item">
|
|
|
|
|
难度:<n-select v-model:value="subQuestion.difficulty" size="small" style="width: 80px"
|
|
|
|
|
:options="difficultyOptions" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="sub-footer-item">
|
|
|
|
|
题目必填:<n-select v-model:value="subQuestion.required" size="small" style="width: 80px"
|
|
|
|
|
:options="[{ label: '是', value: true }, { label: '否', value: false }]" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<n-row class="flex">
|
|
|
|
|
<n-popselect v-model:value="questionTypeValue" :options="questionTypeOptions">
|
|
|
|
|
<n-button type="primary" @click="addQuestion(index)">
|
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<AddCircle />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
|
|
|
|
{{questionTypeOptions.find(opt => opt.value === questionTypeValue)?.label || '添加题目'}}
|
|
|
|
|
</n-button>
|
|
|
|
|
</n-popselect>
|
|
|
|
|
<div class="mr-10"></div>
|
2025-08-22 21:17:10 +08:00
|
|
|
|
<n-button type="primary" ghost @click="openQuestionBankModal(index)">
|
2025-08-22 16:42:55 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<BookSharp />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
2025-08-22 21:17:10 +08:00
|
|
|
|
题库选择题目
|
2025-08-22 16:42:55 +08:00
|
|
|
|
</n-button>
|
|
|
|
|
</n-row>
|
|
|
|
|
</n-card>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<!-- 操作按钮 -->
|
|
|
|
|
<n-card size="small">
|
|
|
|
|
<div class="footer-btn">
|
|
|
|
|
<!-- 左侧按钮 -->
|
|
|
|
|
<div class="footer-left">
|
|
|
|
|
<n-space>
|
|
|
|
|
<n-button type="primary" ghost size="large" @click="openBatchScoreModal">
|
|
|
|
|
批量设置分数
|
|
|
|
|
</n-button>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<n-button type="primary" :ghost="!examForm.useAIGrading" size="large"
|
2025-08-22 16:42:55 +08:00
|
|
|
|
@click="toggleAIGrading">
|
|
|
|
|
<!-- <template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
|
|
|
|
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2zM9.29 16.29L5.7 12.7a.996.996 0 1 1 1.41-1.41L10 14.17l6.88-6.88a.996.996 0 1 1 1.41 1.41l-7.59 7.59a.996.996 0 0 1-1.41 0z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template> -->
|
|
|
|
|
{{ examForm.useAIGrading ? '已启用AI阅卷' : '使用AI阅卷功能' }}
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-button type="primary" ghost size="large" @click="openExamSettingsModal">
|
2025-08-22 21:17:10 +08:00
|
|
|
|
<template #icon>
|
|
|
|
|
<n-icon>
|
|
|
|
|
<SettingsOutline />
|
|
|
|
|
</n-icon>
|
|
|
|
|
</template>
|
2025-08-22 16:42:55 +08:00
|
|
|
|
试卷设置
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-button type="primary" ghost size="large">
|
|
|
|
|
预览试卷
|
|
|
|
|
</n-button>
|
|
|
|
|
</n-space>
|
|
|
|
|
</div>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
<!-- 中间统计信息 -->
|
|
|
|
|
<div class="footer-center">
|
|
|
|
|
<span>题目数量:{{ examForm.questions.length }} 道</span>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<span>总分:{{examForm.questions.reduce((total, q) => total + q.totalScore, 0)}} 分</span>
|
2025-08-22 16:42:55 +08:00
|
|
|
|
</div>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
<!-- 右侧按钮 -->
|
|
|
|
|
<div class="footer-right">
|
|
|
|
|
<n-space>
|
|
|
|
|
<n-button strong type="primary" secondary size="large">
|
|
|
|
|
取消
|
|
|
|
|
</n-button>
|
2025-08-22 21:17:10 +08:00
|
|
|
|
<n-button strong type="primary" secondary size="large" @click="previewSubQuestion">
|
2025-08-22 16:42:55 +08:00
|
|
|
|
预览
|
|
|
|
|
</n-button>
|
|
|
|
|
<n-button strong type="primary" size="large" @click="saveExam">
|
|
|
|
|
保存试卷
|
|
|
|
|
</n-button>
|
|
|
|
|
</n-space>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</n-card>
|
|
|
|
|
</n-space>
|
|
|
|
|
|
|
|
|
|
<!-- 批量设置分数模态框 -->
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<BatchSetScoreModal v-model:visible="showBatchScoreModal" :questions="examForm.questions"
|
|
|
|
|
@confirm="handleBatchScoreConfirm" @cancel="handleBatchScoreCancel" />
|
2025-08-22 16:42:55 +08:00
|
|
|
|
|
|
|
|
|
<!-- 试卷设置模态框 -->
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<ExamSettingsModal v-model:visible="showExamSettingsModal" :exam-data="examSettingsData"
|
|
|
|
|
@confirm="handleExamSettingsConfirm" @cancel="handleExamSettingsCancel" />
|
2025-08-22 21:17:10 +08:00
|
|
|
|
|
|
|
|
|
<!-- 题库选择模态框 -->
|
2025-08-23 18:27:07 +08:00
|
|
|
|
<QuestionBankModal v-model:visible="showQuestionBankModal" @confirm="handleQuestionBankConfirm"
|
|
|
|
|
@cancel="handleQuestionBankCancel" />
|
|
|
|
|
</div>
|
2025-08-22 16:42:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, reactive, ref } from 'vue';
|
2025-08-23 18:27:07 +08:00
|
|
|
|
import { createDiscreteApi } from 'naive-ui';
|
|
|
|
|
import { useRouter } from 'vue-router';
|
|
|
|
|
import { AddCircle, SettingsOutline, TrashOutline, ChevronUpSharp, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
|
2025-08-22 16:42:55 +08:00
|
|
|
|
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
|
|
|
|
|
import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue';
|
2025-08-22 21:17:10 +08:00
|
|
|
|
import QuestionBankModal from '@/components/admin/ExamComponents/QuestionBankModal.vue';
|
2025-08-22 16:42:55 +08:00
|
|
|
|
|
|
|
|
|
// 创建独立的 dialog API
|
|
|
|
|
const { dialog } = createDiscreteApi(['dialog'])
|
|
|
|
|
|
2025-08-23 18:27:07 +08:00
|
|
|
|
// 路由
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
|
|
// 返回上一个页面
|
|
|
|
|
const goBack = () => {
|
|
|
|
|
router.back()
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 题型枚举
|
|
|
|
|
enum QuestionType {
|
|
|
|
|
SINGLE_CHOICE = 'single_choice', // 单选题
|
|
|
|
|
MULTIPLE_CHOICE = 'multiple_choice', // 多选题
|
|
|
|
|
TRUE_FALSE = 'true_false', // 判断题
|
|
|
|
|
FILL_BLANK = 'fill_blank', // 填空题
|
|
|
|
|
SHORT_ANSWER = 'short_answer', // 简答题
|
|
|
|
|
COMPOSITE = 'composite' // 复合题
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 选择题选项接口
|
|
|
|
|
interface ChoiceOption {
|
|
|
|
|
id: string;
|
|
|
|
|
content: string;
|
|
|
|
|
isCorrect: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 填空题答案接口
|
|
|
|
|
interface FillBlankAnswer {
|
|
|
|
|
id: string;
|
|
|
|
|
content: string;
|
|
|
|
|
position: number; // 填空位置
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 小题接口
|
|
|
|
|
interface SubQuestion {
|
|
|
|
|
id: string;
|
|
|
|
|
type: QuestionType;
|
|
|
|
|
title: string; // 题目内容
|
|
|
|
|
score: number; // 分值
|
|
|
|
|
difficulty: 'easy' | 'medium' | 'hard'; // 难度
|
|
|
|
|
required: boolean; // 题目必填
|
|
|
|
|
|
|
|
|
|
// 选择题相关字段
|
|
|
|
|
options?: ChoiceOption[]; // 选择题选项
|
|
|
|
|
correctAnswer?: string | string[]; // 正确答案(单选为string,多选为string[])
|
|
|
|
|
|
|
|
|
|
// 填空题相关字段
|
|
|
|
|
fillBlanks?: FillBlankAnswer[]; // 填空答案
|
|
|
|
|
|
|
|
|
|
// 判断题相关字段
|
|
|
|
|
trueFalseAnswer?: boolean; // 判断题答案
|
|
|
|
|
|
|
|
|
|
// 简答题/复合题相关字段
|
|
|
|
|
textAnswer?: string; // 文本答案
|
|
|
|
|
answerKeywords?: string[]; // 答案关键词
|
|
|
|
|
|
|
|
|
|
// 复合题子题目
|
|
|
|
|
subQuestions?: SubQuestion[]; // 复合题的子题目
|
|
|
|
|
|
|
|
|
|
// 通用字段
|
|
|
|
|
explanation?: string; // 答案解析
|
|
|
|
|
tags?: string[]; // 标签
|
|
|
|
|
createTime: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 大题接口
|
|
|
|
|
interface BigQuestion {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string; // 大题标题
|
|
|
|
|
description?: string; // 大题描述
|
|
|
|
|
sort: number; // 排序
|
|
|
|
|
totalScore: number; // 总分值
|
|
|
|
|
subQuestions: SubQuestion[]; // 小题列表
|
|
|
|
|
createTime: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const questionTypeValue = ref<string>('single_choice')
|
|
|
|
|
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 difficultyOptions = ref([
|
|
|
|
|
{ label: '简单', value: 'easy' },
|
|
|
|
|
{ label: '中等', value: 'medium' },
|
|
|
|
|
{ label: '困难', value: 'hard' }
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const examForm = reactive({
|
|
|
|
|
title: '',
|
|
|
|
|
type: 1, // 1: 固定试卷组, 2: 随机抽题组卷
|
|
|
|
|
description: '',
|
|
|
|
|
totalScore: 0, // 试卷总分
|
|
|
|
|
duration: 60, // 考试时长(分钟)
|
|
|
|
|
passScore: 60, // 及格分数
|
|
|
|
|
instructions: '', // 考试说明
|
|
|
|
|
useAIGrading: false, // 是否启用AI阅卷功能
|
|
|
|
|
questions: [
|
|
|
|
|
{
|
|
|
|
|
id: '1',
|
|
|
|
|
title: '',
|
|
|
|
|
description: '',
|
|
|
|
|
sort: 1,
|
|
|
|
|
totalScore: 0,
|
|
|
|
|
subQuestions: [],
|
|
|
|
|
createTime: new Date().toISOString()
|
|
|
|
|
}
|
|
|
|
|
] as BigQuestion[],
|
|
|
|
|
createTime: new Date().toISOString(),
|
|
|
|
|
updateTime: new Date().toISOString()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const examType = computed(() => {
|
|
|
|
|
return examForm.type === 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const changeType = (e: number) => {
|
|
|
|
|
examForm.type = e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addQuestion = (index: number) => {
|
|
|
|
|
const questionType = questionTypeValue.value as QuestionType;
|
|
|
|
|
const newSubQuestion: SubQuestion = {
|
|
|
|
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
type: questionType,
|
|
|
|
|
title: '',
|
|
|
|
|
score: 5, // 默认分值
|
|
|
|
|
difficulty: 'medium',
|
|
|
|
|
required: true, // 默认必填
|
|
|
|
|
createTime: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 根据题型初始化不同的字段
|
|
|
|
|
switch (questionType) {
|
|
|
|
|
case QuestionType.SINGLE_CHOICE:
|
|
|
|
|
newSubQuestion.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
newSubQuestion.correctAnswer = '';
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.MULTIPLE_CHOICE:
|
|
|
|
|
newSubQuestion.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
newSubQuestion.correctAnswer = [];
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.TRUE_FALSE:
|
|
|
|
|
newSubQuestion.trueFalseAnswer = undefined; // 初始状态不选择任何选项
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.FILL_BLANK:
|
|
|
|
|
newSubQuestion.fillBlanks = [
|
|
|
|
|
{ id: '1', content: '', position: 1 }
|
|
|
|
|
];
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.SHORT_ANSWER:
|
|
|
|
|
newSubQuestion.textAnswer = '';
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.COMPOSITE:
|
|
|
|
|
newSubQuestion.subQuestions = [];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加到对应大题的小题列表
|
|
|
|
|
examForm.questions[index].subQuestions.push(newSubQuestion);
|
|
|
|
|
|
|
|
|
|
// 更新大题总分
|
|
|
|
|
updateBigQuestionScore(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新大题总分的辅助函数
|
|
|
|
|
const updateBigQuestionScore = (bigQuestionIndex: number) => {
|
|
|
|
|
const bigQuestion = examForm.questions[bigQuestionIndex];
|
|
|
|
|
bigQuestion.totalScore = bigQuestion.subQuestions.reduce((total, subQ) => total + subQ.score, 0);
|
|
|
|
|
|
|
|
|
|
// 更新试卷总分
|
|
|
|
|
examForm.totalScore = examForm.questions.reduce((total, bigQ) => total + bigQ.totalScore, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新的大题
|
|
|
|
|
const addBigQuestion = () => {
|
|
|
|
|
const newBigQuestion: BigQuestion = {
|
|
|
|
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
title: `第${examForm.questions.length + 1}大题`,
|
|
|
|
|
description: '',
|
|
|
|
|
sort: examForm.questions.length + 1,
|
|
|
|
|
totalScore: 0,
|
|
|
|
|
subQuestions: [],
|
|
|
|
|
createTime: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
examForm.questions.push(newBigQuestion);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取题型名称
|
|
|
|
|
const getQuestionTypeName = (type: QuestionType): string => {
|
|
|
|
|
const typeMap = {
|
|
|
|
|
[QuestionType.SINGLE_CHOICE]: '单选题',
|
|
|
|
|
[QuestionType.MULTIPLE_CHOICE]: '多选题',
|
|
|
|
|
[QuestionType.TRUE_FALSE]: '判断题',
|
|
|
|
|
[QuestionType.FILL_BLANK]: '填空题',
|
|
|
|
|
[QuestionType.SHORT_ANSWER]: '简答题',
|
|
|
|
|
[QuestionType.COMPOSITE]: '复合题'
|
|
|
|
|
};
|
|
|
|
|
return typeMap[type] || '未知题型';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除小题
|
|
|
|
|
const deleteSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
examForm.questions[bigQuestionIndex].subQuestions.splice(subQuestionIndex, 1);
|
|
|
|
|
updateBigQuestionScore(bigQuestionIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新单选题答案
|
|
|
|
|
const updateSingleChoice = (bigQuestionIndex: number, subQuestionIndex: number, optionId: string) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
subQuestion.correctAnswer = optionId;
|
|
|
|
|
|
|
|
|
|
// 更新选项的正确状态
|
|
|
|
|
if (subQuestion.options) {
|
|
|
|
|
subQuestion.options.forEach(option => {
|
|
|
|
|
option.isCorrect = option.id === optionId;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新多选题答案
|
|
|
|
|
const updateMultipleChoice = (bigQuestionIndex: number, subQuestionIndex: number, optionId: string, checked: boolean) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
let answers = (subQuestion.correctAnswer as string[]) || [];
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
if (!answers.includes(optionId)) {
|
|
|
|
|
answers.push(optionId);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
answers = answers.filter(id => id !== optionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subQuestion.correctAnswer = answers;
|
|
|
|
|
|
|
|
|
|
// 更新选项的正确状态
|
|
|
|
|
if (subQuestion.options) {
|
|
|
|
|
subQuestion.options.forEach(option => {
|
|
|
|
|
option.isCorrect = answers.includes(option.id);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新判断题答案
|
|
|
|
|
const updateTrueFalse = (bigQuestionIndex: number, subQuestionIndex: number, value: boolean) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
subQuestion.trueFalseAnswer = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加选项
|
|
|
|
|
const addOption = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (subQuestion.options) {
|
|
|
|
|
const newOptionId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
const optionLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
|
|
|
|
const nextLabel = optionLabels[subQuestion.options.length] || String.fromCharCode(65 + subQuestion.options.length);
|
|
|
|
|
|
|
|
|
|
subQuestion.options.push({
|
|
|
|
|
id: newOptionId,
|
|
|
|
|
content: `选项${nextLabel}`,
|
|
|
|
|
isCorrect: false
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除选项
|
|
|
|
|
const deleteOption = (bigQuestionIndex: number, subQuestionIndex: number, optionIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (subQuestion.options && subQuestion.options.length > 2) { // 至少保留2个选项
|
|
|
|
|
const deletedOption = subQuestion.options[optionIndex];
|
|
|
|
|
subQuestion.options.splice(optionIndex, 1);
|
|
|
|
|
|
|
|
|
|
// 如果删除的是正确答案,需要更新correctAnswer
|
|
|
|
|
if (subQuestion.type === QuestionType.SINGLE_CHOICE && subQuestion.correctAnswer === deletedOption.id) {
|
|
|
|
|
subQuestion.correctAnswer = '';
|
|
|
|
|
} else if (subQuestion.type === QuestionType.MULTIPLE_CHOICE && Array.isArray(subQuestion.correctAnswer)) {
|
|
|
|
|
subQuestion.correctAnswer = subQuestion.correctAnswer.filter(id => id !== deletedOption.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加填空
|
|
|
|
|
const addFillBlank = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (subQuestion.fillBlanks) {
|
|
|
|
|
const newBlankId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
subQuestion.fillBlanks.push({
|
|
|
|
|
id: newBlankId,
|
|
|
|
|
content: '',
|
|
|
|
|
position: subQuestion.fillBlanks.length + 1
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除填空
|
|
|
|
|
const deleteFillBlank = (bigQuestionIndex: number, subQuestionIndex: number, blankIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (subQuestion.fillBlanks && subQuestion.fillBlanks.length > 1) { // 至少保留1个填空
|
|
|
|
|
subQuestion.fillBlanks.splice(blankIndex, 1);
|
|
|
|
|
|
|
|
|
|
// 重新排序position
|
|
|
|
|
subQuestion.fillBlanks.forEach((blank, index) => {
|
|
|
|
|
blank.position = index + 1;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除大题
|
|
|
|
|
const deleteBigQuestion = (bigQuestionIndex: number) => {
|
|
|
|
|
if (examForm.questions.length <= 1) {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '操作限制',
|
|
|
|
|
content: '至少需要保留一个大题',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bigQuestion = examForm.questions[bigQuestionIndex];
|
|
|
|
|
const content = bigQuestion.subQuestions.length > 0
|
|
|
|
|
? `确定要删除第${bigQuestionIndex + 1}题吗?该大题包含${bigQuestion.subQuestions.length}道小题,删除后无法恢复!`
|
|
|
|
|
: `确定要删除第${bigQuestionIndex + 1}题吗?`;
|
|
|
|
|
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '删除确认',
|
|
|
|
|
content: content,
|
|
|
|
|
positiveText: '确定删除',
|
|
|
|
|
negativeText: '取消',
|
|
|
|
|
onPositiveClick: () => {
|
|
|
|
|
examForm.questions.splice(bigQuestionIndex, 1);
|
|
|
|
|
|
|
|
|
|
// 重新排序
|
|
|
|
|
examForm.questions.forEach((question, index) => {
|
|
|
|
|
question.sort = index + 1;
|
|
|
|
|
question.title = `第${index + 1}大题`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 更新总分
|
|
|
|
|
examForm.totalScore = examForm.questions.reduce((total, bigQ) => total + bigQ.totalScore, 0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 移动大题位置
|
|
|
|
|
const moveBigQuestion = (fromIndex: number, toIndex: number) => {
|
|
|
|
|
if (toIndex >= 0 && toIndex < examForm.questions.length) {
|
|
|
|
|
const question = examForm.questions.splice(fromIndex, 1)[0];
|
|
|
|
|
examForm.questions.splice(toIndex, 0, question);
|
|
|
|
|
|
|
|
|
|
// 重新排序
|
|
|
|
|
examForm.questions.forEach((question, index) => {
|
|
|
|
|
question.sort = index + 1;
|
|
|
|
|
question.title = `第${index + 1}大题`;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AI阅卷功能切换
|
|
|
|
|
const toggleAIGrading = () => {
|
|
|
|
|
examForm.useAIGrading = !examForm.useAIGrading;
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 显示状态变更提示
|
|
|
|
|
// const statusText = examForm.useAIGrading ? '已启用' : '已关闭';
|
|
|
|
|
// dialog.success({
|
|
|
|
|
// title: 'AI阅卷功能',
|
|
|
|
|
// content: `AI阅卷功能${statusText}`,
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 批量设置分数相关状态和方法
|
|
|
|
|
const showBatchScoreModal = ref(false);
|
|
|
|
|
|
|
|
|
|
// 打开批量设置分数模态框
|
|
|
|
|
const openBatchScoreModal = () => {
|
|
|
|
|
// 检查是否有题目
|
|
|
|
|
let hasQuestions = false;
|
|
|
|
|
for (const bigQuestion of examForm.questions) {
|
|
|
|
|
if (bigQuestion.subQuestions.length > 0) {
|
|
|
|
|
hasQuestions = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasQuestions) {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '提示',
|
|
|
|
|
content: '请先添加题目后再进行批量设置分数',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showBatchScoreModal.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理批量设置分数确认
|
|
|
|
|
const handleBatchScoreConfirm = (updatedQuestions: BigQuestion[]) => {
|
|
|
|
|
// 更新试卷数据
|
|
|
|
|
examForm.questions = updatedQuestions;
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 重新计算试卷总分
|
|
|
|
|
examForm.totalScore = examForm.questions.reduce((total, bigQ) => total + bigQ.totalScore, 0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理批量设置分数取消
|
|
|
|
|
const handleBatchScoreCancel = () => {
|
|
|
|
|
// 取消操作,不需要特殊处理
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 试卷设置相关状态和方法
|
|
|
|
|
const showExamSettingsModal = ref(false);
|
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// 题库选择相关状态和方法
|
|
|
|
|
const showQuestionBankModal = ref(false);
|
|
|
|
|
const currentBigQuestionIndex = ref(0);
|
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 试卷设置数据
|
|
|
|
|
const examSettingsData = computed(() => ({
|
|
|
|
|
title: examForm.title,
|
|
|
|
|
startTime: null,
|
|
|
|
|
endTime: null,
|
|
|
|
|
category: (examForm.type === 1 ? 'exam' : 'practice') as 'exam' | 'practice',
|
|
|
|
|
timeLimit: 'limited' as 'unlimited' | 'limited' | 'no_limit',
|
|
|
|
|
timeLimitValue: 0,
|
|
|
|
|
examTimes: 'unlimited' as 'unlimited' | 'limited' | 'each_day',
|
|
|
|
|
examTimesValue: 1,
|
|
|
|
|
dailyLimit: 1,
|
|
|
|
|
chapter: '',
|
|
|
|
|
passScore: examForm.passScore,
|
|
|
|
|
participants: 'all' as 'all' | 'by_school',
|
|
|
|
|
selectedClasses: [],
|
|
|
|
|
instructions: examForm.instructions,
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 考试模式专用
|
|
|
|
|
enforceOrder: false,
|
|
|
|
|
enforceInstructions: false,
|
|
|
|
|
readingTime: 10,
|
|
|
|
|
submitSettings: {
|
|
|
|
|
allowEarlySubmit: true,
|
|
|
|
|
},
|
|
|
|
|
gradingDelay: 60,
|
|
|
|
|
scoreDisplay: 'show_all' as 'show_all' | 'show_score' | 'hide_all',
|
|
|
|
|
detailedSettings: {
|
|
|
|
|
showQuestions: false,
|
|
|
|
|
showAnalysis: false,
|
|
|
|
|
showSubmissionTime: false,
|
|
|
|
|
},
|
|
|
|
|
timerEnabled: false,
|
|
|
|
|
timerDuration: examForm.duration,
|
|
|
|
|
answerType: 'auto_save' as 'auto_save' | 'manual_save' | 'multiple_submit',
|
|
|
|
|
detailScoreMode: 'question' as 'question' | 'automatic' | 'show_current' | 'show_all',
|
|
|
|
|
showRanking: false,
|
|
|
|
|
courseProgress: 0,
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 练习模式专用
|
|
|
|
|
correctnessMode: 'no_limit' as 'no_limit' | 'limit_wrong',
|
|
|
|
|
wrongLimit: 10,
|
|
|
|
|
practiceSettings: {
|
|
|
|
|
showCorrectAnswer: false,
|
|
|
|
|
showWrongAnswer: false,
|
|
|
|
|
showAnalysis: false,
|
|
|
|
|
keepPreviousAnswers: false,
|
|
|
|
|
useLastScore: false,
|
|
|
|
|
},
|
|
|
|
|
paperMode: 'show_all' as 'show_all' | 'show_current' | 'hide_all',
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// 打开试卷设置模态框
|
|
|
|
|
const openExamSettingsModal = () => {
|
|
|
|
|
showExamSettingsModal.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理试卷设置确认
|
|
|
|
|
const handleExamSettingsConfirm = (settings: any) => {
|
|
|
|
|
// 更新试卷数据
|
|
|
|
|
examForm.title = settings.title;
|
|
|
|
|
examForm.type = settings.category === 'exam' ? 1 : 2;
|
|
|
|
|
examForm.passScore = settings.passScore;
|
|
|
|
|
examForm.instructions = settings.instructions;
|
|
|
|
|
examForm.duration = settings.timerDuration || examForm.duration;
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// TODO 可以根据需要更新更多字段
|
2025-08-22 16:42:55 +08:00
|
|
|
|
console.log('试卷设置数据:', settings);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理试卷设置取消
|
|
|
|
|
const handleExamSettingsCancel = () => {
|
|
|
|
|
// 取消操作,不需要特殊处理
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// 题库选择相关方法
|
|
|
|
|
const openQuestionBankModal = (bigQuestionIndex: number) => {
|
|
|
|
|
currentBigQuestionIndex.value = bigQuestionIndex;
|
|
|
|
|
showQuestionBankModal.value = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理题库选择确认
|
|
|
|
|
const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
|
|
|
|
|
const bigQuestionIndex = currentBigQuestionIndex.value;
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// 将选择的题目转换为当前系统的题目格式并添加到对应大题
|
|
|
|
|
selectedQuestions.forEach(question => {
|
|
|
|
|
const questionType = getQuestionTypeFromString(question.type);
|
|
|
|
|
const newSubQuestion: SubQuestion = {
|
|
|
|
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
type: questionType as QuestionType,
|
|
|
|
|
title: question.title,
|
|
|
|
|
score: question.score,
|
|
|
|
|
difficulty: question.difficulty,
|
|
|
|
|
required: true,
|
|
|
|
|
createTime: new Date().toISOString()
|
|
|
|
|
};
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// 根据题型初始化不同的字段
|
|
|
|
|
if (questionType === 'single_choice') {
|
|
|
|
|
newSubQuestion.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
newSubQuestion.correctAnswer = '';
|
|
|
|
|
} else if (questionType === 'multiple_choice') {
|
|
|
|
|
newSubQuestion.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
newSubQuestion.correctAnswer = [];
|
|
|
|
|
} else if (questionType === 'true_false') {
|
|
|
|
|
newSubQuestion.trueFalseAnswer = undefined;
|
|
|
|
|
} else if (questionType === 'fill_blank') {
|
|
|
|
|
newSubQuestion.fillBlanks = [
|
|
|
|
|
{ id: '1', content: '', position: 1 }
|
|
|
|
|
];
|
|
|
|
|
} else if (questionType === 'short_answer') {
|
|
|
|
|
newSubQuestion.textAnswer = '';
|
|
|
|
|
}
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
|
|
|
|
|
});
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
// 重新计算总分
|
|
|
|
|
updateBigQuestionScore(bigQuestionIndex);
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 21:17:10 +08:00
|
|
|
|
dialog.success({
|
|
|
|
|
title: '成功',
|
|
|
|
|
content: `成功导入${selectedQuestions.length}道题目`,
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
showQuestionBankModal.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 处理题库选择取消
|
|
|
|
|
const handleQuestionBankCancel = () => {
|
|
|
|
|
showQuestionBankModal.value = false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 辅助函数:将字符串题型转换为系统题型
|
|
|
|
|
const getQuestionTypeFromString = (typeString: string) => {
|
|
|
|
|
const typeMap: { [key: string]: string } = {
|
|
|
|
|
'单选题': 'single_choice',
|
|
|
|
|
'多选题': 'multiple_choice',
|
|
|
|
|
'判断题': 'true_false',
|
|
|
|
|
'填空题': 'fill_blank',
|
|
|
|
|
'简答题': 'short_answer'
|
|
|
|
|
};
|
|
|
|
|
return typeMap[typeString] || 'single_choice';
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
// 保存试卷
|
|
|
|
|
const saveExam = () => {
|
|
|
|
|
// 验证数据
|
|
|
|
|
if (!examForm.title.trim()) {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '输入提示',
|
|
|
|
|
content: '请输入试卷标题',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (examForm.questions.length === 0) {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '输入提示',
|
|
|
|
|
content: '请至少添加一个大题',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let hasQuestions = false;
|
|
|
|
|
for (const bigQuestion of examForm.questions) {
|
|
|
|
|
if (bigQuestion.subQuestions.length > 0) {
|
|
|
|
|
hasQuestions = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hasQuestions) {
|
|
|
|
|
dialog.warning({
|
|
|
|
|
title: '输入提示',
|
|
|
|
|
content: '请至少添加一道题目',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 这里实现保存逻辑
|
|
|
|
|
console.log('保存试卷数据:', examForm);
|
|
|
|
|
dialog.success({
|
|
|
|
|
title: '保存成功',
|
|
|
|
|
content: '试卷保存成功!',
|
|
|
|
|
positiveText: '确定'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 复合题相关方法
|
|
|
|
|
const addCompositeSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (!subQuestion.subQuestions) {
|
|
|
|
|
subQuestion.subQuestions = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newSubSubQuestion: SubQuestion = {
|
|
|
|
|
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
|
|
|
type: QuestionType.SINGLE_CHOICE,
|
|
|
|
|
title: '',
|
|
|
|
|
score: 2,
|
|
|
|
|
difficulty: 'medium',
|
|
|
|
|
required: true,
|
|
|
|
|
options: [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
],
|
|
|
|
|
correctAnswer: '',
|
|
|
|
|
createTime: new Date().toISOString()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
subQuestion.subQuestions.push(newSubSubQuestion);
|
|
|
|
|
updateBigQuestionScore(bigQuestionIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteCompositeSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
if (subQuestion.subQuestions) {
|
|
|
|
|
subQuestion.subQuestions.splice(compSubIndex, 1);
|
|
|
|
|
updateBigQuestionScore(bigQuestionIndex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 复合题选项管理
|
|
|
|
|
const updateCompositeChoice = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number, optionId: string) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ) {
|
|
|
|
|
compositeSubQ.correctAnswer = optionId;
|
|
|
|
|
// 更新选项的正确状态
|
|
|
|
|
if (compositeSubQ.options) {
|
|
|
|
|
compositeSubQ.options.forEach(option => {
|
|
|
|
|
option.isCorrect = option.id === optionId;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateCompositeMultiChoice = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number, optionId: string, checked: boolean) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ) {
|
|
|
|
|
let answers = (compositeSubQ.correctAnswer as string[]) || [];
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
if (!answers.includes(optionId)) {
|
|
|
|
|
answers.push(optionId);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
answers = answers.filter(id => id !== optionId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
compositeSubQ.correctAnswer = answers;
|
|
|
|
|
|
|
|
|
|
// 更新选项的正确状态
|
|
|
|
|
if (compositeSubQ.options) {
|
|
|
|
|
compositeSubQ.options.forEach(option => {
|
|
|
|
|
option.isCorrect = answers.includes(option.id);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新复合题判断题答案
|
|
|
|
|
const updateCompositeTrueFalse = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number, value: boolean) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ) {
|
|
|
|
|
compositeSubQ.trueFalseAnswer = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addCompositeOption = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ && compositeSubQ.options) {
|
|
|
|
|
const newOptionId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
const optionLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
|
|
|
|
const nextLabel = optionLabels[compositeSubQ.options.length] || String.fromCharCode(65 + compositeSubQ.options.length);
|
|
|
|
|
|
|
|
|
|
compositeSubQ.options.push({
|
|
|
|
|
id: newOptionId,
|
|
|
|
|
content: `选项${nextLabel}`,
|
|
|
|
|
isCorrect: false
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteCompositeOption = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number, optionIndex: number) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ && compositeSubQ.options && compositeSubQ.options.length > 2) { // 至少保留2个选项
|
|
|
|
|
const deletedOption = compositeSubQ.options[optionIndex];
|
|
|
|
|
compositeSubQ.options.splice(optionIndex, 1);
|
|
|
|
|
|
|
|
|
|
// 如果删除的是正确答案,需要更新correctAnswer
|
|
|
|
|
if (compositeSubQ.type === QuestionType.SINGLE_CHOICE && compositeSubQ.correctAnswer === deletedOption.id) {
|
|
|
|
|
compositeSubQ.correctAnswer = '';
|
|
|
|
|
} else if (compositeSubQ.type === QuestionType.MULTIPLE_CHOICE && Array.isArray(compositeSubQ.correctAnswer)) {
|
|
|
|
|
compositeSubQ.correctAnswer = compositeSubQ.correctAnswer.filter(id => id !== deletedOption.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 修改复合题子题目类型
|
|
|
|
|
const changeCompositeSubQuestionType = (bigQuestionIndex: number, subQuestionIndex: number, compSubIndex: number, newType: string) => {
|
|
|
|
|
const compositeSubQ = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex].subQuestions?.[compSubIndex];
|
|
|
|
|
if (compositeSubQ) {
|
|
|
|
|
compositeSubQ.type = newType as QuestionType;
|
|
|
|
|
|
|
|
|
|
// 根据新题型重新初始化相关字段
|
|
|
|
|
switch (newType as QuestionType) {
|
|
|
|
|
case QuestionType.SINGLE_CHOICE:
|
|
|
|
|
compositeSubQ.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
compositeSubQ.correctAnswer = '';
|
|
|
|
|
delete compositeSubQ.trueFalseAnswer;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.MULTIPLE_CHOICE:
|
|
|
|
|
compositeSubQ.options = [
|
|
|
|
|
{ id: '1', content: '选项A', isCorrect: false },
|
|
|
|
|
{ id: '2', content: '选项B', isCorrect: false },
|
|
|
|
|
{ id: '3', content: '选项C', isCorrect: false },
|
|
|
|
|
{ id: '4', content: '选项D', isCorrect: false }
|
|
|
|
|
];
|
|
|
|
|
compositeSubQ.correctAnswer = [];
|
|
|
|
|
delete compositeSubQ.trueFalseAnswer;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case QuestionType.TRUE_FALSE:
|
|
|
|
|
compositeSubQ.trueFalseAnswer = undefined; // 初始状态不选择任何选项
|
|
|
|
|
delete compositeSubQ.options;
|
|
|
|
|
delete compositeSubQ.correctAnswer;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 保存题目
|
2025-08-22 17:08:52 +08:00
|
|
|
|
// const saveSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
// const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
|
|
|
|
|
// // 验证题目数据
|
|
|
|
|
// if (!subQuestion.title.trim()) {
|
|
|
|
|
// dialog.warning({
|
|
|
|
|
// title: '输入提示',
|
|
|
|
|
// content: '请输入题目内容',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// // 根据题型验证必要字段
|
|
|
|
|
// switch (subQuestion.type) {
|
|
|
|
|
// case QuestionType.SINGLE_CHOICE:
|
|
|
|
|
// case QuestionType.MULTIPLE_CHOICE:
|
|
|
|
|
// if (!subQuestion.options || subQuestion.options.length < 2) {
|
|
|
|
|
// dialog.warning({
|
|
|
|
|
// title: '输入提示',
|
|
|
|
|
// content: '选择题至少需要2个选项',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// if (!subQuestion.correctAnswer ||
|
|
|
|
|
// (Array.isArray(subQuestion.correctAnswer) && subQuestion.correctAnswer.length === 0)) {
|
|
|
|
|
// dialog.warning({
|
|
|
|
|
// title: '输入提示',
|
|
|
|
|
// content: '请设置正确答案',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// break;
|
|
|
|
|
|
|
|
|
|
// case QuestionType.FILL_BLANK:
|
|
|
|
|
// if (!subQuestion.fillBlanks || subQuestion.fillBlanks.length === 0) {
|
|
|
|
|
// dialog.warning({
|
|
|
|
|
// title: '输入提示',
|
|
|
|
|
// content: '填空题至少需要1个填空',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// if (subQuestion.fillBlanks.some(blank => !blank.content.trim())) {
|
|
|
|
|
// dialog.warning({
|
|
|
|
|
// title: '输入提示',
|
|
|
|
|
// content: '请填写所有填空的正确答案',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// return;
|
|
|
|
|
// }
|
|
|
|
|
// break;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// updateBigQuestionScore(bigQuestionIndex);
|
|
|
|
|
// dialog.success({
|
|
|
|
|
// title: '保存成功',
|
|
|
|
|
// content: '题目保存成功!',
|
|
|
|
|
// positiveText: '确定'
|
|
|
|
|
// });
|
|
|
|
|
// }
|
2025-08-22 16:42:55 +08:00
|
|
|
|
|
|
|
|
|
// 预览题目
|
2025-08-22 21:17:10 +08:00
|
|
|
|
const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
|
|
|
|
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
|
|
|
|
console.log('预览题目:', subQuestion);
|
|
|
|
|
// 这里可以实现题目预览功能,比如打开一个模态框显示题目
|
|
|
|
|
}
|
2025-08-22 16:42:55 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
|
|
|
|
.exam-container{
|
|
|
|
|
background-color: #fff;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-content h1 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #333;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-button {
|
|
|
|
|
color: #666;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.back-button:hover {
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
background-color: rgba(24, 144, 255, 0.1);
|
2025-08-22 16:42:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.group {
|
|
|
|
|
border: 1px solid #F1F3F4;
|
|
|
|
|
padding: 12px;
|
2025-08-23 18:27:07 +08:00
|
|
|
|
margin-bottom: 15px 0;
|
2025-08-22 16:42:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.required::before {
|
|
|
|
|
content: '*';
|
|
|
|
|
color: red;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.flex {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.justify-between {
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mr-10 {
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.iconRotate {
|
|
|
|
|
transform: rotate(180deg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.input-title {
|
|
|
|
|
flex: 1;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.questionRow {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty_tip {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 新增样式 */
|
|
|
|
|
.question-item {
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
background-color: #fafafa;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-type {
|
|
|
|
|
background-color: #0288d1;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-content {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-question-content {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-options {
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.option-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.choice-option {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.fill-blank-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
background-color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.add-option-btn {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
color: #0288d1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-actions {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.total {
|
|
|
|
|
color: #666;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-question-footer {
|
|
|
|
|
margin: 10px 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: end;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sub-footer-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin: 0 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 复合题样式 */
|
|
|
|
|
.composite-sub-questions {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.composite-sub-item {
|
|
|
|
|
border: 1px solid #e0e0e0;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.composite-sub-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding-bottom: 8px;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.composite-sub-content {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.composite-options {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 18:27:07 +08:00
|
|
|
|
.sub-question-number {
|
2025-08-22 16:42:55 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
gap: 6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-btn {
|
|
|
|
|
position: relative;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-left {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-center {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 50%;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
gap: 4px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: #666;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
z-index: 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-center span {
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
margin: 0 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.footer-right {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式布局 */
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
.footer-btn {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
.footer-center {
|
|
|
|
|
position: static;
|
|
|
|
|
transform: none;
|
|
|
|
|
order: 2;
|
|
|
|
|
margin: 8px 0;
|
|
|
|
|
}
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
.footer-left {
|
|
|
|
|
order: 1;
|
|
|
|
|
}
|
2025-08-23 18:27:07 +08:00
|
|
|
|
|
2025-08-22 16:42:55 +08:00
|
|
|
|
.footer-right {
|
|
|
|
|
order: 3;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|