<script lang="ts" setup>
|
import type { OnlineExamPaper, OnlineExamQuestionItem } from './types';
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
import { useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { Modal } from 'ant-design-vue';
|
|
import { submitOnlineExam } from '#/api/x/tms/onlineExam';
|
import { labelOfType } from '#/views/x/tms/question/constants';
|
|
defineOptions({ name: 'TmsOnlineExamTake' });
|
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
|
const paper = ref<OnlineExamPaper | null>(null);
|
const currentIndex = ref(0);
|
const answers = reactive<Record<string, string | string[]>>({});
|
const submitted = ref(false);
|
const scoreText = ref('');
|
const submitting = ref(false);
|
|
const current = computed(() => paper.value?.questions?.[currentIndex.value]);
|
const total = computed(() => paper.value?.questions?.length || 0);
|
|
onMounted(() => {
|
const raw = sessionStorage.getItem('tms_online_exam_paper');
|
if (!raw) {
|
createMessage.warning('请先从试卷列表选择考试');
|
router.replace('/tms/onlineExam');
|
return;
|
}
|
try {
|
paper.value = JSON.parse(raw);
|
} catch {
|
router.replace('/tms/onlineExam');
|
}
|
});
|
|
function stripHtml(html?: string) {
|
if (!html) return '';
|
return html.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim();
|
}
|
|
function goPrev() {
|
if (currentIndex.value > 0) currentIndex.value -= 1;
|
}
|
|
function goNext() {
|
if (currentIndex.value < total.value - 1) currentIndex.value += 1;
|
}
|
|
function goBack() {
|
router.push('/tms/onlineExam');
|
}
|
|
function isCorrect(q: OnlineExamQuestionItem): boolean {
|
const ans = answers[q.id];
|
const correctLabels = q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
|
if (q.questionType === 'multi') {
|
const selected = Array.isArray(ans) ? [...ans].sort() : [];
|
return selected.join(',') === [...correctLabels].sort().join(',');
|
}
|
return String(ans || '') === String(correctLabels[0] || '');
|
}
|
|
function calcScore(): number {
|
if (!paper.value) return 0;
|
let got = 0;
|
paper.value.questions.forEach((q) => {
|
if (isCorrect(q)) got += Number(q.score || 0);
|
});
|
return got;
|
}
|
|
function handleSubmit() {
|
if (!paper.value || submitted.value) return;
|
Modal.confirm({
|
title: '确认交卷',
|
content: '交卷后不可再修改答案,确定交卷吗?',
|
onOk: doSubmit,
|
});
|
}
|
|
async function doSubmit() {
|
if (!paper.value) return;
|
submitting.value = true;
|
try {
|
const got = calcScore();
|
await submitOnlineExam(paper.value.examId, { score: got, answers: { ...answers } });
|
submitted.value = true;
|
scoreText.value = `${got} / ${paper.value.totalScore}`;
|
createMessage.success(`交卷成功,得分 ${got} 分(满分 ${paper.value.totalScore})`);
|
} catch (e: any) {
|
createMessage.error(e?.message || '交卷失败');
|
} finally {
|
submitting.value = false;
|
}
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper">
|
<div class="jnpf-content-wrapper-center">
|
<div v-if="paper" class="jnpf-content-wrapper-content tms-online-exam-page">
|
<div class="tms-exam-header">
|
<div>
|
<div class="text-base font-medium">{{ paper.paperName }}</div>
|
<div class="mt-1 text-gray-400 text-sm">
|
第 {{ currentIndex + 1 }} / {{ total }} 题
|
<span v-if="paper.durationMin" class="ml-3">时长 {{ paper.durationMin }} 分钟</span>
|
<span v-if="submitted" class="ml-3 text-primary">得分:{{ scoreText }}</span>
|
</div>
|
</div>
|
<a-space>
|
<a-button @click="goBack">返回列表</a-button>
|
<a-button type="primary" :loading="submitting" :disabled="submitted" @click="handleSubmit">
|
交卷
|
</a-button>
|
</a-space>
|
</div>
|
|
<div v-if="current" class="tms-exam-body">
|
<div class="mb-3 text-sm text-gray-500">
|
{{ labelOfType(current.questionType) }}
|
<span class="ml-2">({{ current.score }} 分)</span>
|
</div>
|
<div class="stem mb-4">{{ stripHtml(current.stem) }}</div>
|
|
<a-radio-group
|
v-if="current.questionType === 'single' || current.questionType === 'judge'"
|
v-model:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
:disabled="submitted"
|
>
|
<a-radio v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
|
{{ opt.optionLabel }}. {{ opt.optionContent }}
|
</a-radio>
|
</a-radio-group>
|
|
<a-checkbox-group
|
v-else-if="current.questionType === 'multi'"
|
v-model:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
:disabled="submitted"
|
>
|
<a-checkbox v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
|
{{ opt.optionLabel }}. {{ opt.optionContent }}
|
</a-checkbox>
|
</a-checkbox-group>
|
|
<div
|
v-if="submitted"
|
class="mt-4 text-sm"
|
:class="isCorrect(current) ? 'text-green-600' : 'text-red-500'"
|
>
|
{{ isCorrect(current) ? '回答正确' : '回答错误' }}
|
· 正确答案:
|
{{ current.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel).join('、') }}
|
</div>
|
</div>
|
|
<div class="tms-exam-footer">
|
<a-button :disabled="currentIndex <= 0" @click="goPrev">上一题</a-button>
|
<a-button :disabled="currentIndex >= total - 1" @click="goNext">下一题</a-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.tms-online-exam-page {
|
background: #fff;
|
padding: 20px 24px;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.tms-exam-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding-bottom: 12px;
|
margin-bottom: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
flex-shrink: 0;
|
}
|
|
.tms-exam-body {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
}
|
|
.stem {
|
font-size: 15px;
|
line-height: 1.7;
|
color: rgba(0, 0, 0, 0.88);
|
}
|
|
.tms-exam-footer {
|
flex-shrink: 0;
|
display: flex;
|
gap: 12px;
|
justify-content: center;
|
padding-top: 16px;
|
border-top: 1px solid #f0f0f0;
|
}
|
</style>
|