1636 lines
63 KiB
Vue
1636 lines
63 KiB
Vue
<template>
|
||
<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>
|
||
</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">
|
||
<n-input v-model:value="subQuestion.title" type="textarea" placeholder="请输入题目内容"
|
||
style="flex: 1;" />
|
||
<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>
|
||
<n-button type="primary" ghost @click="openQuestionBankModal(index)">
|
||
<template #icon>
|
||
<n-icon>
|
||
<BookSharp />
|
||
</n-icon>
|
||
</template>
|
||
题库选择题目
|
||
</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>
|
||
<n-button type="primary" :ghost="!examForm.useAIGrading" size="large"
|
||
@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">
|
||
<template #icon>
|
||
<n-icon>
|
||
<SettingsOutline />
|
||
</n-icon>
|
||
</template>
|
||
试卷设置
|
||
</n-button>
|
||
<n-button type="primary" ghost size="large">
|
||
预览试卷
|
||
</n-button>
|
||
</n-space>
|
||
</div>
|
||
|
||
<!-- 中间统计信息 -->
|
||
<div class="footer-center">
|
||
<span>题目数量:{{ examForm.questions.length }} 道</span>
|
||
<span>总分:{{examForm.questions.reduce((total, q) => total + q.totalScore, 0)}} 分</span>
|
||
</div>
|
||
|
||
<!-- 右侧按钮 -->
|
||
<div class="footer-right">
|
||
<n-space>
|
||
<n-button strong type="primary" secondary size="large">
|
||
取消
|
||
</n-button>
|
||
<n-button strong type="primary" secondary size="large" @click="previewSubQuestion">
|
||
预览
|
||
</n-button>
|
||
<n-button strong type="primary" size="large" @click="saveExam">
|
||
保存试卷
|
||
</n-button>
|
||
</n-space>
|
||
</div>
|
||
</div>
|
||
</n-card>
|
||
</n-space>
|
||
|
||
<!-- 批量设置分数模态框 -->
|
||
<BatchSetScoreModal v-model:visible="showBatchScoreModal" :questions="examForm.questions"
|
||
@confirm="handleBatchScoreConfirm" @cancel="handleBatchScoreCancel" />
|
||
|
||
<!-- 试卷设置模态框 -->
|
||
<ExamSettingsModal v-model:visible="showExamSettingsModal" :exam-data="examSettingsData"
|
||
@confirm="handleExamSettingsConfirm" @cancel="handleExamSettingsCancel" />
|
||
|
||
<!-- 题库选择模态框 -->
|
||
<QuestionBankModal v-model:visible="showQuestionBankModal" @confirm="handleQuestionBankConfirm"
|
||
@cancel="handleQuestionBankCancel" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref } from 'vue';
|
||
import { createDiscreteApi } from 'naive-ui';
|
||
import { useRouter } from 'vue-router';
|
||
import { AddCircle, SettingsOutline, TrashOutline, ChevronUpSharp, BookSharp, ArrowBackOutline } from '@vicons/ionicons5'
|
||
import BatchSetScoreModal from '@/components/admin/ExamComponents/BatchSetScoreModal.vue';
|
||
import ExamSettingsModal from '@/components/admin/ExamComponents/ExamSettingsModal.vue';
|
||
import QuestionBankModal from '@/components/admin/ExamComponents/QuestionBankModal.vue';
|
||
|
||
// 创建独立的 dialog API
|
||
const { dialog } = createDiscreteApi(['dialog'])
|
||
|
||
// 路由
|
||
const router = useRouter()
|
||
|
||
// 返回上一个页面
|
||
const goBack = () => {
|
||
router.back()
|
||
}
|
||
|
||
// 题型枚举
|
||
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;
|
||
|
||
// 显示状态变更提示
|
||
// 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;
|
||
|
||
// 重新计算试卷总分
|
||
examForm.totalScore = examForm.questions.reduce((total, bigQ) => total + bigQ.totalScore, 0);
|
||
};
|
||
|
||
// 处理批量设置分数取消
|
||
const handleBatchScoreCancel = () => {
|
||
// 取消操作,不需要特殊处理
|
||
};
|
||
|
||
// 试卷设置相关状态和方法
|
||
const showExamSettingsModal = ref(false);
|
||
|
||
// 题库选择相关状态和方法
|
||
const showQuestionBankModal = ref(false);
|
||
const currentBigQuestionIndex = ref(0);
|
||
|
||
// 试卷设置数据
|
||
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,
|
||
|
||
// 考试模式专用
|
||
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,
|
||
|
||
// 练习模式专用
|
||
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;
|
||
|
||
// TODO 可以根据需要更新更多字段
|
||
console.log('试卷设置数据:', settings);
|
||
};
|
||
|
||
// 处理试卷设置取消
|
||
const handleExamSettingsCancel = () => {
|
||
// 取消操作,不需要特殊处理
|
||
};
|
||
|
||
// 题库选择相关方法
|
||
const openQuestionBankModal = (bigQuestionIndex: number) => {
|
||
currentBigQuestionIndex.value = bigQuestionIndex;
|
||
showQuestionBankModal.value = true;
|
||
};
|
||
|
||
// 处理题库选择确认
|
||
const handleQuestionBankConfirm = (selectedQuestions: any[]) => {
|
||
const bigQuestionIndex = currentBigQuestionIndex.value;
|
||
|
||
// 将选择的题目转换为当前系统的题目格式并添加到对应大题
|
||
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()
|
||
};
|
||
|
||
// 根据题型初始化不同的字段
|
||
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 = '';
|
||
}
|
||
|
||
examForm.questions[bigQuestionIndex].subQuestions.push(newSubQuestion);
|
||
});
|
||
|
||
// 重新计算总分
|
||
updateBigQuestionScore(bigQuestionIndex);
|
||
|
||
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';
|
||
};
|
||
|
||
// 保存试卷
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存题目
|
||
// 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: '确定'
|
||
// });
|
||
// }
|
||
|
||
// 预览题目
|
||
const previewSubQuestion = (bigQuestionIndex: number, subQuestionIndex: number) => {
|
||
const subQuestion = examForm.questions[bigQuestionIndex].subQuestions[subQuestionIndex];
|
||
console.log('预览题目:', subQuestion);
|
||
// 这里可以实现题目预览功能,比如打开一个模态框显示题目
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
|
||
.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);
|
||
}
|
||
|
||
.group {
|
||
border: 1px solid #F1F3F4;
|
||
padding: 12px;
|
||
margin-bottom: 15px 0;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.sub-question-number {
|
||
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;
|
||
}
|
||
|
||
.footer-center {
|
||
position: static;
|
||
transform: none;
|
||
order: 2;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.footer-left {
|
||
order: 1;
|
||
}
|
||
|
||
.footer-right {
|
||
order: 3;
|
||
}
|
||
}
|
||
</style>
|