1636 lines
63 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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