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(/&nbsp;/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