From 93c133349a5ccd7a328371fa113dce69d5611f21 Mon Sep 17 00:00:00 2001
From: liuyu <yu.liu@cqgbkj.com>
Date: 星期二, 22 九月 2026 09:25:39 +0800
Subject: [PATCH] feat(tms): 完成课程考试自测等页面与接口对接

---
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue |  192 +++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 158 insertions(+), 34 deletions(-)

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue
index 737a0f5..97fb692 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue
@@ -1,7 +1,7 @@
 <script lang="ts" setup>
-import type { OnlineExamDetail } from './types';
+import type { OnlineExamAttempt, OnlineExamDetail } from './types';
 
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
@@ -9,11 +9,22 @@
   Descriptions as ADescriptions,
   DescriptionsItem as ADescriptionsItem,
   Spin,
+  Table as ATable,
 } from 'ant-design-vue';
 
 import { getMyPaperDetail, startOnlineExam } from '#/api/x/tms/onlineExam';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
-import { colorOfMyPaperStatus, labelOfMyPaperStatus } from './constants';
+import {
+  colorOfMyPaperStatus,
+  colorOfPassFlag,
+  labelOfGradeStatus,
+  labelOfMyPaperStatus,
+  labelOfPassFlag,
+  loadOnlineExamDics,
+} from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsOnlineExamDetail' });
 
@@ -25,8 +36,22 @@
 const starting = ref(false);
 const detail = ref<OnlineExamDetail | null>(null);
 
-onMounted(() => {
-  loadDetail();
+const attemptColumns = [
+  { title: '鑰冭瘯绫诲瀷', dataIndex: 'attemptLabel', key: 'attemptLabel', width: 130 },
+  { title: '鐘舵��', dataIndex: 'status', key: 'status', width: 100 },
+  { title: '寮�濮嬫椂闂�', dataIndex: 'startTime', key: 'startTime', width: 170 },
+  { title: '浜ゅ嵎鏃堕棿', dataIndex: 'submitTime', key: 'submitTime', width: 170 },
+  { title: '寰楀垎', dataIndex: 'gotScore', key: 'gotScore', width: 90, align: 'center' as const },
+  { title: '鏄惁鍚堟牸', dataIndex: 'passFlag', key: 'passFlag', width: 100, align: 'center' as const },
+  { title: '闃呭嵎', dataIndex: 'gradeStatus', key: 'gradeStatus', width: 100 },
+  { title: '鎿嶄綔', key: 'action', width: 100, align: 'center' as const },
+];
+
+const attempts = computed(() => detail.value?.attempts || []);
+
+onMounted(async () => {
+  await loadOnlineExamDics();
+  await loadDetail();
 });
 
 async function loadDetail() {
@@ -50,16 +75,40 @@
   router.push('/tms/onlineExam');
 }
 
+function goReview(row?: OnlineExamAttempt) {
+  if (!detail.value) return;
+  const examId = row?.examId || detail.value.examId;
+  if (!examId) {
+    createMessage.warning('鏆傛棤宸蹭氦鍗风瓟鍗峰彲鍥為【');
+    return;
+  }
+  router.push({
+    path: `/tms/onlineExam/review/${detail.value.id}`,
+    query: { examId },
+  });
+}
+
 async function handleStart() {
   if (!detail.value || starting.value) return;
   starting.value = true;
   try {
     const paper = await startOnlineExam(detail.value.id);
+    if (paper?.autoSubmitted) {
+      createMessage.warning('鑰冭瘯鏃堕棿宸插埌锛屽凡鑷姩浜ゅ嵎');
+      await loadDetail();
+      return;
+    }
     sessionStorage.setItem('tms_online_exam_paper', JSON.stringify(paper));
     sessionStorage.setItem('tms_online_exam_myPaperId', detail.value.id);
     router.push('/tms/onlineExam/exam');
   } catch (e: any) {
-    createMessage.error(e?.message || '寮�濮嬭�冭瘯澶辫触');
+    const msg = e?.message || '寮�濮嬭�冭瘯澶辫触';
+    if (String(msg).includes('鑷姩浜ゅ嵎')) {
+      createMessage.warning(msg);
+      await loadDetail();
+    } else {
+      createMessage.error(msg);
+    }
   } finally {
     starting.value = false;
   }
@@ -67,49 +116,110 @@
 </script>
 
 <template>
-  <div class="jnpf-content-wrapper">
+  <div class="jnpf-content-wrapper tms-exam-detail-root">
     <div class="jnpf-content-wrapper-center">
       <div class="jnpf-content-wrapper-content tms-exam-detail-page">
         <Spin :spinning="loading">
-          <div class="tms-exam-detail-header">
-            <div class="text-base font-medium">鑰冭瘯璇︽儏</div>
-            <a-space>
-              <a-button @click="goBack">杩斿洖鍒楄〃</a-button>
+          <div class="tms-page-header">
+            <div>
+              <div class="tms-page-header__title">鑰冭瘯璇︽儏</div>
+              <div v-if="detail?.status === 'submitted'" class="tms-page-header__sub">
+                鏈�杩戝緱鍒� {{ detail.gotScore ?? '-' }} / {{ detail.totalScore }}
+                <span v-if="detail.submitTime" class="ml-3">浜ゅ嵎鏃堕棿锛歿{ detail.submitTime }}</span>
+              </div>
+            </div>
+            <div class="tms-page-header__actions">
+              <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
               <a-button
                 v-if="detail && detail.status !== 'submitted'"
                 type="primary"
                 :loading="starting"
                 @click="handleStart"
               >
-                {{ detail?.status === 'doing' ? '缁х画鑰冭瘯' : '寮�濮嬭�冭瘯' }}
+                {{ detail?.status === 'doing' ? TMS_BTN.continueExam : TMS_BTN.startExam }}
               </a-button>
-            </a-space>
+              <a-button
+                v-else-if="detail?.canRetake"
+                type="primary"
+                :loading="starting"
+                @click="handleStart"
+              >
+                {{ TMS_BTN.retake }}
+              </a-button>
+            </div>
           </div>
 
           <ADescriptions v-if="detail" bordered :column="2" size="middle">
             <ADescriptionsItem label="璇曞嵎鍚嶇О" :span="2">{{ detail.paperName }}</ADescriptionsItem>
+            <ADescriptionsItem label="鏈�杩戣�冭瘯绫诲瀷">{{ detail.attemptLabel || '棣栬��' }}</ADescriptionsItem>
             <ADescriptionsItem label="鐘舵��">
               <span :style="{ color: colorOfMyPaperStatus(detail.status) }">
                 {{ labelOfMyPaperStatus(detail.status) }}
               </span>
             </ADescriptionsItem>
             <ADescriptionsItem label="鍗烽潰鎬诲垎">{{ detail.totalScore }}</ADescriptionsItem>
+            <ADescriptionsItem label="琛ヨ�冩鏁�">
+              {{
+                detail.retakeLimit == null
+                  ? '-'
+                  : `鍓╀綑 ${detail.remainingRetakes ?? 0} / 涓婇檺 ${detail.retakeLimit}`
+              }}
+            </ADescriptionsItem>
             <ADescriptionsItem label="鑰冭瘯鏃堕棿" :span="2">{{ detail.examTimeText || '-' }}</ADescriptionsItem>
             <ADescriptionsItem label="鑰冭瘯鏃堕暱">
               {{ detail.durationMin != null ? `${detail.durationMin} 鍒嗛挓` : '-' }}
             </ADescriptionsItem>
             <ADescriptionsItem label="鍚堟牸鍒嗘暟">{{ detail.passScore ?? '-' }}</ADescriptionsItem>
-            <ADescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTime || '-' }}</ADescriptionsItem>
-            <ADescriptionsItem label="浜ゅ嵎鏃堕棿">{{ detail.submitTime || '-' }}</ADescriptionsItem>
-            <ADescriptionsItem label="寰楀垎">
-              {{ detail.gotScore != null ? detail.gotScore : '-' }}
-            </ADescriptionsItem>
-            <ADescriptionsItem label="鏄惁鍚堟牸">
-              <template v-if="detail.passFlag === '1'">鍚堟牸</template>
-              <template v-else-if="detail.passFlag === '0'">涓嶅悎鏍�</template>
-              <template v-else>-</template>
-            </ADescriptionsItem>
           </ADescriptions>
+
+          <div class="history-block">
+            <div class="history-title">鍘嗗彶绛斿嵎</div>
+            <ATable
+              size="middle"
+              :columns="attemptColumns"
+              :data-source="attempts"
+              :pagination="false"
+              row-key="examId"
+              bordered
+            >
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'status'">
+                  <span :style="{ color: colorOfMyPaperStatus(record.status) }">
+                    {{ labelOfMyPaperStatus(record.status) }}
+                  </span>
+                </template>
+                <template v-else-if="column.key === 'gotScore'">
+                  {{ record.gotScore != null ? record.gotScore : '-' }}
+                </template>
+                <template v-else-if="column.key === 'passFlag'">
+                  <span :style="{ color: colorOfPassFlag(record.passFlag) }">
+                    {{ labelOfPassFlag(record.passFlag) }}
+                  </span>
+                </template>
+                <template v-else-if="column.key === 'gradeStatus'">
+                  {{ labelOfGradeStatus(record.gradeStatus) }}
+                </template>
+                <template v-else-if="column.key === 'action'">
+                  <a
+                    v-if="record.status === 'submitted'"
+                    @click.prevent="goReview(record as OnlineExamAttempt)"
+                  >
+                    鍥為【
+                  </a>
+                  <span v-else class="text-gray-400">-</span>
+                </template>
+              </template>
+            </ATable>
+            <div v-if="!attempts.length" class="history-empty">鏆傛棤绛斿嵎璁板綍</div>
+          </div>
+
+          <a-alert
+            v-if="detail && detail.status !== 'submitted' && !attempts.some((a) => a.status === 'submitted')"
+            class="mt-4"
+            type="info"
+            show-icon
+            :message="detail.status === 'doing' ? '鑰冭瘯杩涜涓紝浜ゅ嵎鍚庡彲鍦ㄥ巻鍙茬瓟鍗蜂腑鍥為【銆�' : '灏氭湭寮�鑰冿紝鏆傛棤绛旈鍐呭銆�'"
+          />
         </Spin>
       </div>
     </div>
@@ -117,19 +227,33 @@
 </template>
 
 <style scoped>
-.tms-exam-detail-page {
-  background: #fff;
-  padding: 20px 24px;
+.tms-exam-detail-root {
   height: 100%;
-  overflow: auto;
+  min-height: 0;
 }
 
-.tms-exam-detail-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20px;
-  padding-bottom: 12px;
-  border-bottom: 1px solid #f0f0f0;
+.tms-exam-detail-page {
+  height: 100%;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto !important;
+  background: #fff;
+  padding: 20px 24px;
+  box-sizing: border-box;
+}
+
+.history-block {
+  margin-top: 20px;
+}
+
+.history-title {
+  font-weight: 600;
+  margin-bottom: 12px;
+}
+
+.history-empty {
+  margin-top: 12px;
+  color: #8c8c8c;
+  text-align: center;
 }
 </style>

--
Gitblit v1.8.0