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

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/personTask/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/personTask/Detail.vue
new file mode 100644
index 0000000..d3b72ec
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/personTask/Detail.vue
@@ -0,0 +1,205 @@
+<script lang="ts" setup>
+import type { PersonTaskItem } 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 { getPersonTaskInfo, signPersonTask } from '#/api/x/tms/personTask';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import {
+  labelOfCategory,
+  labelOfEvalMode,
+  labelOfPersonStatus,
+  labelOfTrainMode,
+  loadPersonTaskDics,
+} from './constants';
+
+import '#/views/x/tms/shared/page.css';
+
+defineOptions({ name: 'TmsPersonTaskDetail' });
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+
+const loading = ref(false);
+const signing = ref(false);
+const detail = ref<PersonTaskItem | null>(null);
+
+const progressText = computed(() => {
+  const learned = detail.value?.learnedSeconds || 0;
+  const required = detail.value?.requiredSeconds || 0;
+  return `${formatDuration(learned)} / ${required > 0 ? formatDuration(required) : '鏈厤缃�'}`;
+});
+
+const isClosed = computed(() => detail.value?.bizStatus === 'cancelled' || detail.value?.bizStatus === 'expired');
+const canLearn = computed(() => Boolean(detail.value?.id) && !isClosed.value);
+const showExam = computed(() => Boolean(detail.value?.canExam && detail.value?.paperId));
+const tipMessage = computed(() => {
+  if (!detail.value) return '';
+  if (detail.value.bizStatus === 'cancelled') return '浠诲姟宸插彇娑堬紝鏃犳硶缁х画瀛︿範鎴栬�冭瘯銆�';
+  if (detail.value.bizStatus === 'expired') return '浠诲姟宸茶繃鏈燂紝鏃犳硶缁х画瀛︿範鎴栬�冭瘯銆�';
+  if (detail.value.examTip && !detail.value.canExam) return detail.value.examTip;
+  if (detail.value.signTip && detail.value.canSign) return detail.value.signTip;
+  return '';
+});
+
+onMounted(async () => {
+  await loadPersonTaskDics();
+  await loadData();
+});
+
+async function loadData() {
+  const id = String(route.params.id || '');
+  if (!id) {
+    router.replace('/tms/personTask');
+    return;
+  }
+  loading.value = true;
+  try {
+    detail.value = await getPersonTaskInfo(id);
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇澶辫触');
+    router.replace('/tms/personTask');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function goBack() {
+  router.push('/tms/personTask');
+}
+
+function goLearn() {
+  if (!detail.value?.id) return;
+  if (isClosed.value) {
+    createMessage.warning(tipMessage.value || '褰撳墠浠诲姟涓嶅彲瀛︿範');
+    return;
+  }
+  router.push(`/tms/personTask/learn/${detail.value.id}`);
+}
+
+function goExam() {
+  if (!detail.value?.id || !showExam.value) {
+    createMessage.info(detail.value?.examTip || '褰撳墠鏆備笉鍙�冭瘯');
+    return;
+  }
+  router.push(`/tms/personTask/learn/${detail.value.id}`);
+}
+
+async function handleSign() {
+  if (!detail.value?.id || !detail.value.canSign || signing.value) return;
+  signing.value = true;
+  try {
+    const result = await signPersonTask(detail.value.id);
+    detail.value.signTime = result.signTime;
+    detail.value.signed = result.signed ?? true;
+    detail.value.canSign = result.canSign ?? false;
+    detail.value.signTip = result.signTip || '宸茬鍒�';
+    detail.value.bizStatus = result.bizStatus || detail.value.bizStatus;
+    detail.value.canExam = result.canExam;
+    detail.value.examTip = result.examTip;
+    createMessage.success(result.message || '绛惧埌鎴愬姛');
+  } catch (e: any) {
+    createMessage.error(e?.message || '绛惧埌澶辫触');
+  } finally {
+    signing.value = false;
+  }
+}
+
+function formatDuration(seconds: number) {
+  const h = Math.floor(seconds / 3600);
+  const m = Math.floor((seconds % 3600) / 60);
+  const s = seconds % 60;
+  if (h > 0) return `${h}灏忔椂${m}鍒哷;
+  if (m > 0) return `${m}鍒�${s}绉抈;
+  return `${s}绉抈;
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper tms-person-task-page" v-loading="loading">
+    <div class="jnpf-content-wrapper-center">
+      <div class="jnpf-content-wrapper-content tms-person-task-wrap" v-if="detail">
+        <div class="tms-page-header">
+          <div>
+            <div class="tms-page-header__title">涓汉鍩硅浠诲姟</div>
+            <div class="tms-page-header__sub">{{ detail.taskNo || '-' }} 路 {{ detail.subject || '-' }}</div>
+          </div>
+          <div class="tms-page-header__actions">
+            <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+            <a-button
+              v-if="detail.canSign"
+              type="primary"
+              :ghost="showExam"
+              :loading="signing"
+              @click="handleSign"
+            >
+              {{ TMS_BTN.sign }}
+            </a-button>
+            <a-button v-if="canLearn" type="primary" ghost @click="goLearn">{{ TMS_BTN.enterLearn }}</a-button>
+            <a-button v-if="showExam" type="primary" @click="goExam">
+              {{ detail.passFlag === '0' ? TMS_BTN.retake : TMS_BTN.enterExam }}
+            </a-button>
+          </div>
+        </div>
+
+        <a-alert v-if="tipMessage" class="tms-page-tip" type="warning" show-icon :message="tipMessage" />
+
+        <ADescriptions bordered :column="2" size="small">
+          <ADescriptionsItem label="鍩硅缂栧彿">{{ detail.taskNo || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鐘舵��">{{ labelOfPersonStatus(detail.bizStatus) }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍩硅涓婚" :span="2">{{ detail.subject || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍩硅鍒嗙被">{{ labelOfCategory(detail.category) }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍩硅鏂瑰紡">{{ labelOfTrainMode(detail.trainMode) }}</ADescriptionsItem>
+          <ADescriptionsItem label="鑰冩牳鏂瑰紡">{{ labelOfEvalMode(detail.evalMode) }}</ADescriptionsItem>
+          <ADescriptionsItem label="绛惧埌鐘舵��">
+            {{
+              detail.signed || detail.signTime
+                ? `宸茬鍒�${detail.signTime ? `锛�${detail.signTime}锛塦 : ''}`
+                : detail.signTip || '鏈鍒�'
+            }}
+          </ADescriptionsItem>
+          <ADescriptionsItem label="鏄惁鍙��">
+            {{ detail.canExam ? '鏄�' : '鍚�' }}
+            <span v-if="!detail.canExam && detail.examTip" class="tip-inline">锛坽{ detail.examTip }}锛�</span>
+          </ADescriptionsItem>
+          <ADescriptionsItem label="瀛︿範杩涘害">{{ detail.learnStatus || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="瑕佹眰璇炬椂">{{ detail.requiredHours ?? '-' }} 灏忔椂</ADescriptionsItem>
+          <ADescriptionsItem label="宸插/闇�瀛�">{{ progressText }}</ADescriptionsItem>
+          <ADescriptionsItem label="寮�濮嬫椂闂�">{{ detail.startTime || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="缁撴潫鏃堕棿">{{ detail.endTime || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍏抽棴鏃堕棿">{{ detail.closeTime || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍦扮偣">{{ detail.placeName || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="璇曞嵎">{{ detail.paperName || '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鎴愮哗">{{ detail.examScore ?? '-' }}</ADescriptionsItem>
+          <ADescriptionsItem label="鍩硅瑕佺偣" :span="2">{{ detail.keyPoints || '-' }}</ADescriptionsItem>
+        </ADescriptions>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tms-person-task-page,
+.tms-person-task-wrap {
+  height: 100%;
+  min-height: 0;
+}
+
+.tms-person-task-wrap {
+  overflow: auto;
+  background: #fff;
+  padding: 16px 20px 24px;
+  box-sizing: border-box;
+}
+
+.tip-inline {
+  color: #fa8c16;
+  margin-left: 4px;
+}
+</style>

--
Gitblit v1.8.0