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/selfTest/Detail.vue |  214 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 214 insertions(+), 0 deletions(-)

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue
new file mode 100644
index 0000000..38873bb
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue
@@ -0,0 +1,214 @@
+<script lang="ts" setup>
+import type { SelfTestPaper, SelfTestQuestionItem } from './types';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { useTabbarStore } from '@vben/stores';
+
+import { getSelfTestInfo } from '#/api/x/tms/selfTest';
+import { labelOfType } from '#/views/x/tms/question/constants';
+
+defineOptions({ name: 'TmsSelfTestDetail' });
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+const tabbarStore = useTabbarStore();
+
+const loading = ref(false);
+const detail = ref<SelfTestPaper | null>(null);
+/** all | wrong */
+const filterMode = ref<'all' | 'wrong'>('all');
+
+const wrongCount = computed(
+  () => detail.value?.questions?.filter((q) => q.isRight !== '1').length || 0,
+);
+
+const displayQuestions = computed(() => {
+  const list = detail.value?.questions || [];
+  if (filterMode.value === 'wrong') {
+    return list.filter((q) => q.isRight !== '1');
+  }
+  return list;
+});
+
+function stripHtml(html?: string) {
+  if (!html) return '';
+  return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+}
+
+function formatAnswer(ans?: string) {
+  if (!ans) return '鏈綔绛�';
+  return ans.split(',').filter(Boolean).join('銆�');
+}
+
+function optionClass(q: SelfTestQuestionItem, label: string) {
+  const correctSet = new Set((q.correctAnswer || '').split(',').filter(Boolean));
+  const userSet = new Set((q.userAnswer || '').split(',').filter(Boolean));
+  const isCorrectOpt = correctSet.has(label);
+  const isUserOpt = userSet.has(label);
+  if (isCorrectOpt) return 'opt-correct';
+  if (isUserOpt && !isCorrectOpt) return 'opt-wrong';
+  return '';
+}
+
+async function loadDetail() {
+  const id = String(route.params.id || '');
+  if (!id) {
+    router.replace('/tms/selfTest/records');
+    return;
+  }
+  loading.value = true;
+  try {
+    detail.value = await getSelfTestInfo(id);
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇妫�娴嬭鎯呭け璐�');
+    router.replace('/tms/selfTest/records');
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  tabbarStore.renderRouteView = true;
+  loadDetail();
+});
+
+function goBack() {
+  tabbarStore.renderRouteView = true;
+  router.push('/tms/selfTest/records');
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper">
+    <div class="jnpf-content-wrapper-center">
+      <div class="jnpf-content-wrapper-content tms-self-test-detail" v-loading="loading">
+        <div class="page-header" v-if="detail">
+          <div>
+            <div class="text-base font-medium">妫�娴嬭鎯� 路 {{ detail.bankName }}</div>
+            <div class="mt-1 text-gray-400 text-sm">
+              寰楀垎锛歿{ detail.correctCount ?? 0 }} / {{ detail.totalCount ?? detail.questions?.length ?? 0 }}
+              <span v-if="detail.scoreRate != null" class="ml-2">姝g‘鐜� {{ detail.scoreRate }}%</span>
+              <span v-if="detail.submitTime" class="ml-3">浜ゅ嵎鏃堕棿锛歿{ detail.submitTime }}</span>
+            </div>
+          </div>
+          <a-space>
+            <a-radio-group v-model:value="filterMode" button-style="solid" size="small">
+              <a-radio-button value="all">鍏ㄩ儴棰樼洰</a-radio-button>
+              <a-radio-button value="wrong">浠呴敊棰橈紙{{ wrongCount }}锛�</a-radio-button>
+            </a-radio-group>
+            <a-button @click="goBack">杩斿洖璁板綍</a-button>
+          </a-space>
+        </div>
+
+        <a-empty v-if="!loading && detail && !displayQuestions.length" description="鏆傛棤棰樼洰" />
+
+        <div v-for="(q, idx) in displayQuestions" :key="`${q.id}_${idx}`" class="question-card">
+          <div class="q-title">
+            <span class="q-index">{{ q.sortNo || idx + 1 }}.</span>
+            <span class="q-type">{{ labelOfType(q.questionType) }}</span>
+            <a-tag :color="q.isRight === '1' ? 'success' : 'error'" class="!ml-2">
+              {{ q.isRight === '1' ? '姝g‘' : '閿欒' }}
+            </a-tag>
+          </div>
+          <div class="q-stem">{{ stripHtml(q.stem) }}</div>
+          <div class="q-options">
+            <div
+              v-for="opt in q.options"
+              :key="opt.optionLabel"
+              class="q-option"
+              :class="optionClass(q, opt.optionLabel)"
+            >
+              {{ opt.optionLabel }}. {{ opt.optionContent }}
+            </div>
+          </div>
+          <div class="q-answer">
+            <span>浣犵殑绛旀锛歿{ formatAnswer(q.userAnswer) }}</span>
+            <span class="ml-4">姝g‘绛旀锛歿{ formatAnswer(q.correctAnswer) }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tms-self-test-detail {
+  background: #fff;
+  padding: 20px 24px;
+  height: 100%;
+  overflow: auto;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 16px;
+  margin-bottom: 20px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.question-card {
+  padding: 16px 0;
+  border-bottom: 1px solid #f5f5f5;
+}
+
+.q-title {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.q-index {
+  font-weight: 600;
+  margin-right: 6px;
+}
+
+.q-type {
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 13px;
+}
+
+.q-stem {
+  font-size: 15px;
+  line-height: 1.7;
+  margin-bottom: 12px;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-options {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-bottom: 10px;
+}
+
+.q-option {
+  padding: 6px 10px;
+  border-radius: 4px;
+  background: #fafafa;
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.q-option.opt-correct {
+  background: #f6ffed;
+  color: #389e0d;
+  border: 1px solid #b7eb8f;
+}
+
+.q-option.opt-wrong {
+  background: #fff2f0;
+  color: #cf1322;
+  border: 1px solid #ffccc7;
+}
+
+.q-answer {
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.55);
+}
+</style>

--
Gitblit v1.8.0