From 8534025c45b4736975730678b9ccf23570a75f95 Mon Sep 17 00:00:00 2001
From: liuyu <yu.liu@cqgbkj.com>
Date: 星期二, 22 九月 2026 09:25:48 +0800
Subject: [PATCH] feat(tms): 新增试卷、培训任务、个人任务页面
---
apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 259 insertions(+), 0 deletions(-)
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue
new file mode 100644
index 0000000..4da1eb2
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue
@@ -0,0 +1,259 @@
+<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 '鍥炵瓟姝g‘';
+ return `鍥炵瓟閿欒 路 姝g‘绛旀锛�${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>
--
Gitblit v1.8.0