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

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/question/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/question/Detail.vue
new file mode 100644
index 0000000..ef7c121
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/question/Detail.vue
@@ -0,0 +1,254 @@
+<script lang="ts" setup>
+import type { QuestionEntity, QuestionOption } from './types';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import {
+  Descriptions as ADescriptions,
+  DescriptionsItem as ADescriptionsItem,
+} from 'ant-design-vue';
+
+import { getQuestionInfo } from '#/api/x/tms/question';
+
+import {
+  labelOfDifficulty,
+  labelOfSource,
+  labelOfStatus,
+  labelOfType,
+  loadQuestionDics,
+} from './constants';
+
+defineOptions({ name: 'TmsQuestionDetail' });
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+
+const loading = ref(false);
+const detail = ref<QuestionEntity | null>(null);
+
+const answerText = computed(() => formatAnswer(detail.value));
+
+onMounted(async () => {
+  await loadQuestionDics();
+  await loadData();
+});
+
+async function loadData() {
+  const id = String(route.params.id || '');
+  if (!id) {
+    router.replace('/tms/question');
+    return;
+  }
+  loading.value = true;
+  try {
+    detail.value = await getQuestionInfo(id);
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇璇曢澶辫触');
+    router.replace('/tms/question');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function goBack() {
+  router.push('/tms/question');
+}
+
+function goEdit() {
+  if (!detail.value?.id) return;
+  router.push(`/tms/question/edit/${detail.value.id}`);
+}
+
+function formatAnswer(row?: QuestionEntity | null) {
+  if (!row) return '-';
+  const opts = row.options || [];
+  switch (row.questionType) {
+    case 'single':
+    case 'multi': {
+      const correct = opts.filter((o) => o.isCorrect === '1');
+      if (!correct.length) return '-';
+      return correct.map((o) => `${o.optionLabel}. ${o.optionContent || ''}`).join('锛�');
+    }
+    case 'judge': {
+      const correct = opts.find((o) => o.isCorrect === '1');
+      return correct?.optionContent || correct?.optionLabel || '-';
+    }
+    case 'blank':
+      return opts.map((o, i) => `濉┖${i + 1}锛�${o.optionContent || '-'}`).join('锛�') || '-';
+    case 'essay':
+      return opts[0]?.optionContent || '-';
+    default:
+      return '-';
+  }
+}
+
+function optionClass(opt: QuestionOption) {
+  return opt.isCorrect === '1' ? 'opt-correct' : '';
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper tms-question-detail-page">
+    <div class="jnpf-content-wrapper-center tms-question-detail-center">
+      <div class="jnpf-content-wrapper-content tms-question-detail-wrap">
+        <a-spin :spinning="loading" class="detail-spin">
+          <div v-if="detail" class="detail-inner">
+            <div class="detail-header">
+              <div>
+                <div class="text-base font-medium">璇曢璇︽儏</div>
+                <div class="mt-1 text-gray-400 text-sm">缂栧彿 {{ detail.questionNo || '-' }}</div>
+              </div>
+              <a-space>
+                <a-button @click="goBack">杩斿洖</a-button>
+                <a-button type="primary" @click="goEdit">缂栬緫</a-button>
+              </a-space>
+            </div>
+
+            <ADescriptions bordered :column="2" size="middle" class="detail-desc">
+              <ADescriptionsItem label="璇曢缂栧彿">{{ detail.questionNo || '-' }}</ADescriptionsItem>
+              <ADescriptionsItem label="璇曢绫诲瀷">{{ labelOfType(detail.questionType) }}</ADescriptionsItem>
+              <ADescriptionsItem label="鎵�灞為搴�">{{ detail.bankName || '-' }}</ADescriptionsItem>
+              <ADescriptionsItem label="璇曢闅惧害">{{ labelOfDifficulty(detail.difficulty) }}</ADescriptionsItem>
+              <ADescriptionsItem label="璇曢鏉ユ簮">{{ labelOfSource(detail.sourceType) }}</ADescriptionsItem>
+              <ADescriptionsItem label="璇曢鐘舵��">{{ labelOfStatus(detail.bizStatus) }}</ADescriptionsItem>
+              <ADescriptionsItem label="绠$悊鍛�">{{ detail.adminUserName || '-' }}</ADescriptionsItem>
+              <ADescriptionsItem label="鍒涘缓鏃堕棿">{{ detail.creatorTime || '-' }}</ADescriptionsItem>
+              <ADescriptionsItem label="棰樺共鍐呭" :span="2">
+                <div class="stem-html" v-html="detail.stem || '-'" />
+              </ADescriptionsItem>
+              <ADescriptionsItem
+                v-if="detail.questionType === 'single' || detail.questionType === 'multi' || detail.questionType === 'judge'"
+                label="閫夐」"
+                :span="2"
+              >
+                <div class="option-list">
+                  <div
+                    v-for="(opt, idx) in detail.options || []"
+                    :key="opt.id || idx"
+                    class="option-item"
+                    :class="optionClass(opt)"
+                  >
+                    <span class="opt-label">{{ opt.optionLabel }}.</span>
+                    <span>{{ opt.optionContent }}</span>
+                    <span v-if="opt.isCorrect === '1'" class="opt-tag">姝g‘绛旀</span>
+                  </div>
+                  <div v-if="!(detail.options && detail.options.length)" class="text-gray-400">-</div>
+                </div>
+              </ADescriptionsItem>
+              <ADescriptionsItem label="鍙傝�冪瓟妗�" :span="2">
+                <div class="pre-line">{{ answerText }}</div>
+              </ADescriptionsItem>
+              <ADescriptionsItem label="璇曢瑙f瀽" :span="2">
+                <div class="pre-line">{{ detail.analysis || '-' }}</div>
+              </ADescriptionsItem>
+            </ADescriptions>
+          </div>
+        </a-spin>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tms-question-detail-page {
+  height: 100%;
+  min-height: 0;
+}
+
+.tms-question-detail-center {
+  height: 100% !important;
+  min-height: 0 !important;
+  display: flex;
+  flex-direction: column;
+}
+
+.tms-question-detail-wrap {
+  display: flex !important;
+  flex-direction: column;
+  flex: 1 1 0 !important;
+  min-height: 0 !important;
+  height: 100%;
+  overflow: auto !important;
+  background: #fff;
+  padding: 0;
+}
+
+.detail-spin {
+  display: block;
+  min-height: 100%;
+  width: 100%;
+}
+
+.detail-spin :deep(.ant-spin-container) {
+  min-height: 100%;
+  width: 100%;
+}
+
+.detail-inner {
+  padding: 16px 20px 24px;
+  box-sizing: border-box;
+}
+
+.detail-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.detail-desc {
+  max-width: 1100px;
+  margin-bottom: 16px;
+}
+
+.stem-html :deep(p) {
+  margin-bottom: 0.5em;
+}
+
+.stem-html :deep(img) {
+  max-width: 100%;
+}
+
+.pre-line {
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+.option-list {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.option-item {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  line-height: 1.6;
+}
+
+.opt-label {
+  flex-shrink: 0;
+  font-weight: 500;
+}
+
+.opt-correct {
+  color: #52c41a;
+}
+
+.opt-tag {
+  flex-shrink: 0;
+  margin-left: 4px;
+  padding: 0 6px;
+  font-size: 12px;
+  color: #52c41a;
+  background: #f6ffed;
+  border: 1px solid #b7eb8f;
+  border-radius: 2px;
+}
+</style>

--
Gitblit v1.8.0