<script lang="ts" setup>
|
import type { OnlineExamQuestionItem } from './types';
|
|
import { computed, onMounted, reactive, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { Spin } from 'ant-design-vue';
|
|
import { getExamReview, getMyPaperDetail } from '#/api/x/tms/onlineExam';
|
import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
|
import { TMS_BTN } from '#/views/x/tms/shared/ui';
|
|
import '#/views/x/tms/shared/page.css';
|
|
defineOptions({ name: 'TmsOnlineExamReview' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
|
const loading = ref(false);
|
const paperName = ref('');
|
const attemptLabel = ref('');
|
const questions = ref<OnlineExamQuestionItem[]>([]);
|
const currentIndex = ref(0);
|
const answers = reactive<Record<string, string | string[]>>({});
|
|
const current = computed(() => questions.value[currentIndex.value]);
|
const total = computed(() => questions.value.length);
|
|
onMounted(async () => {
|
await loadQuestionDics();
|
await loadReview();
|
});
|
|
async function loadReview() {
|
const id = String(route.params.id || '');
|
const examId = String(route.query.examId || '');
|
if (!id && !examId) {
|
router.replace('/tms/onlineExam');
|
return;
|
}
|
loading.value = true;
|
try {
|
const detail = examId ? await getExamReview(examId) : await getMyPaperDetail(id);
|
if (!detail || detail.status !== 'submitted') {
|
createMessage.warning('交卷后才能进行考试回顾');
|
router.replace(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
|
return;
|
}
|
paperName.value = detail.paperName;
|
attemptLabel.value = detail.attemptLabel || '';
|
questions.value = detail.questions || [];
|
restoreAnswers(questions.value);
|
currentIndex.value = 0;
|
} catch (e: any) {
|
createMessage.error(e?.message || '加载考试回顾失败');
|
router.replace(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function isObjective(q: OnlineExamQuestionItem) {
|
return !(q.subjective || q.questionType === 'essay');
|
}
|
|
function restoreAnswers(list: OnlineExamQuestionItem[]) {
|
Object.keys(answers).forEach((key) => {
|
delete answers[key];
|
});
|
for (const q of list) {
|
if (q.questionType === 'multi') {
|
answers[q.id] = q.userAnswer ? q.userAnswer.split(/[,,]/).map((x) => x.trim()).filter(Boolean) : [];
|
} else {
|
answers[q.id] = q.userAnswer || '';
|
}
|
}
|
}
|
|
function stripHtml(html?: string) {
|
if (!html) return '';
|
return html.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim();
|
}
|
|
function reviewText(q?: OnlineExamQuestionItem) {
|
if (!q) return '';
|
if (!isObjective(q)) {
|
if (q.gotScore != null) return `主观题得分:${q.gotScore} / ${q.score}`;
|
return '主观题,待阅卷';
|
}
|
if (q.right) return '回答正确';
|
return `回答错误 · 正确答案:${correctText(q)}`;
|
}
|
|
function reviewClass(q?: OnlineExamQuestionItem) {
|
if (!q || !isObjective(q)) return 'text-gray-500';
|
return q.right ? 'text-green-600' : 'text-red-500';
|
}
|
function correctText(q?: OnlineExamQuestionItem) {
|
if (!q) return '-';
|
const labels = (q.correctAnswer || '')
|
.split(/[,,]/)
|
.map((x) => x.trim())
|
.filter(Boolean);
|
if (labels.length) return labels.join('、');
|
const fromOptions = (q.options || []).filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
|
return fromOptions.join('、') || '-';
|
}
|
|
function goBack() {
|
const id = String(route.params.id || '');
|
router.push(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
|
}
|
|
function goPrev() {
|
if (currentIndex.value > 0) currentIndex.value -= 1;
|
}
|
|
function goNext() {
|
if (currentIndex.value < total.value - 1) currentIndex.value += 1;
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper tms-wrong-root">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-content tms-wrong-page">
|
<Spin :spinning="loading" class="tms-wrong-spin">
|
<template v-if="!loading && !total">
|
<div class="tms-page-header">
|
<div class="tms-page-header__title">{{ paperName || '考试回顾' }}</div>
|
<div class="tms-page-header__actions">
|
<a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
|
</div>
|
</div>
|
<a-empty description="暂无答题内容" />
|
</template>
|
|
<template v-else-if="current">
|
<div class="tms-page-header">
|
<div>
|
<div class="tms-page-header__title">
|
{{ paperName }}
|
<span v-if="attemptLabel" class="ml-2 text-sm font-normal text-gray-500">({{ attemptLabel }})</span>
|
</div>
|
<div class="tms-page-header__sub">考试回顾 · 第 {{ currentIndex + 1 }} / {{ total }} 题</div>
|
</div>
|
<div class="tms-page-header__actions">
|
<a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
|
</div>
|
</div>
|
|
<div 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'"
|
:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
disabled
|
>
|
<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'"
|
:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
disabled
|
>
|
<a-checkbox v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
|
{{ opt.optionLabel }}. {{ opt.optionContent }}
|
</a-checkbox>
|
</a-checkbox-group>
|
|
<a-input
|
v-else-if="current.questionType === 'blank'"
|
:value="answers[current.id]"
|
disabled
|
placeholder="未作答"
|
/>
|
|
<a-textarea
|
v-else-if="current.questionType === 'essay'"
|
:value="answers[current.id]"
|
disabled
|
:rows="6"
|
placeholder="未作答"
|
/>
|
|
<div class="mt-4 text-sm" :class="reviewClass(current)">{{ reviewText(current) }}</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>
|
</template>
|
</Spin>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.tms-wrong-root {
|
height: 100%;
|
min-height: 0;
|
}
|
|
.tms-wrong-page {
|
height: 100%;
|
min-height: 0;
|
overflow: hidden;
|
background: #fff;
|
padding: 20px 24px;
|
box-sizing: border-box;
|
}
|
|
.tms-wrong-spin {
|
height: 100%;
|
}
|
|
.tms-wrong-spin :deep(.ant-spin-container) {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.tms-exam-body {
|
flex: 1;
|
min-height: 0;
|
overflow-y: auto !important;
|
}
|
|
.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>
|