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

diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/records.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/records.vue
new file mode 100644
index 0000000..f8d26ec
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/records.vue
@@ -0,0 +1,355 @@
+<script lang="ts" setup>
+import type { QuestionBankOption } from '#/views/x/tms/question/types';
+import type { SelfTestPaper, SelfTestRecordItem } from './types';
+
+import { onMounted, reactive, ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { useTabbarStore } from '@vben/stores';
+import dayjs from 'dayjs';
+
+import { getSelfTestBanks, getSelfTestInfo, getSelfTestList } from '#/api/x/tms/selfTest';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
+
+defineOptions({ name: 'TmsSelfTestRecords' });
+
+const baseStore = useBaseStore();
+const statusOpts = ref<TmsDicOpt[]>([]);
+
+const router = useRouter();
+const { createMessage } = useMessage();
+const tabbarStore = useTabbarStore();
+
+const banks = ref<QuestionBankOption[]>([]);
+const records = ref<SelfTestRecordItem[]>([]);
+const loading = ref(false);
+const continueLoadingId = ref('');
+const pagination = ref({ currentPage: 1, pageSize: 10, total: 0 });
+
+const queryForm = reactive<{
+  bankId?: string;
+  testStatus?: string;
+  /** jnpf-date-range 鍊间负鏃堕棿鎴虫暟缁� */
+  timeRange?: number[];
+}>({
+  bankId: undefined,
+  testStatus: undefined,
+  timeRange: undefined,
+});
+
+async function loadBanks() {
+  try {
+    banks.value = (await getSelfTestBanks()) || [];
+  } catch {
+    banks.value = [];
+  }
+}
+
+async function loadRecords() {
+  loading.value = true;
+  try {
+    const range = queryForm.timeRange;
+    const begin = range?.[0] != null ? dayjs(range[0]).format('YYYY-MM-DD') : undefined;
+    const end = range?.[1] != null ? dayjs(range[1]).format('YYYY-MM-DD') : undefined;
+    const res = await getSelfTestList({
+      currentPage: pagination.value.currentPage,
+      pageSize: pagination.value.pageSize,
+      bankId: queryForm.bankId || undefined,
+      testStatus: queryForm.testStatus || undefined,
+      startTimeBegin: begin ? `${begin} 00:00:00` : undefined,
+      startTimeEnd: end ? `${end} 23:59:59` : undefined,
+    });
+    records.value = res?.list || [];
+    const p = res?.pagination || {};
+    pagination.value.total = Number(p.total || p.totalCount || records.value.length);
+  } catch (e: any) {
+    records.value = [];
+    createMessage.error(e?.message || '鍔犺浇妫�娴嬭褰曞け璐�');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleSearch() {
+  pagination.value.currentPage = 1;
+  loadRecords();
+}
+
+function handleReset() {
+  queryForm.bankId = undefined;
+  queryForm.testStatus = undefined;
+  queryForm.timeRange = undefined;
+  pagination.value.currentPage = 1;
+  loadRecords();
+}
+
+onMounted(async () => {
+  tabbarStore.renderRouteView = true;
+  const examStatus = await loadTmsDic(baseStore, TMS_DIC.examStatus);
+  statusOpts.value = examStatus.filter(
+    (x) => x.enCode === 'submitted' || x.enCode === 'doing',
+  );
+  await loadBanks();
+  loadRecords();
+});
+
+function statusLabel(status?: string) {
+  return labelOfDic(statusOpts.value, status);
+}
+
+function goBack() {
+  tabbarStore.renderRouteView = true;
+  router.push('/tms/selfTest');
+}
+
+function handleView(record: SelfTestRecordItem) {
+  if (record.testStatus !== 'submitted') {
+    createMessage.warning('璇ユ娴嬪皻鏈氦鍗凤紝鏆傛棤绛旈缁撴灉鍙煡鐪�');
+    return;
+  }
+  router.push(`/tms/selfTest/detail/${record.id}`);
+}
+
+/** 缁х画鏈氦鍗风殑妫�娴� */
+async function handleContinue(record: SelfTestRecordItem) {
+  if (record.testStatus !== 'doing') return;
+  continueLoadingId.value = record.id;
+  try {
+    const detail = await getSelfTestInfo(record.id);
+    if (detail.testStatus === 'submitted') {
+      createMessage.warning('璇ユ娴嬪凡浜ゅ嵎锛岃鐩存帴鏌ョ湅璇︽儏');
+      loadRecords();
+      return;
+    }
+    if (!detail.questions?.length) {
+      createMessage.warning('鏈壘鍒板彲缁х画鐨勮瘯棰�');
+      return;
+    }
+    const paper: SelfTestPaper = {
+      paperId: detail.paperId,
+      bankId: detail.bankId,
+      bankName: detail.bankName,
+      testStatus: detail.testStatus,
+      questions: detail.questions,
+    };
+    // 鎶婂凡浣滅瓟鍐呭涓�骞跺甫鍏ョ瓟棰橀〉
+    const savedAnswers: Record<string, string | string[]> = {};
+    detail.questions.forEach((q) => {
+      if (!q.userAnswer) return;
+      if (q.questionType === 'multi') {
+        savedAnswers[q.id] = q.userAnswer.split(',').filter(Boolean);
+      } else {
+        savedAnswers[q.id] = q.userAnswer;
+      }
+    });
+    sessionStorage.setItem('tms_self_test_paper', JSON.stringify(paper));
+    sessionStorage.setItem('tms_self_test_answers', JSON.stringify(savedAnswers));
+    router.push('/tms/selfTest/exam');
+  } catch (e: any) {
+    createMessage.error(e?.message || '缁х画鑰冭瘯澶辫触');
+  } finally {
+    continueLoadingId.value = '';
+  }
+}
+
+function onPageChange(page: number, pageSize: number) {
+  pagination.value.currentPage = page;
+  pagination.value.pageSize = pageSize;
+  loadRecords();
+}
+
+/** 寮瑰嚭灞傛寕鍒� body锛岄伩鍏嶈 jnpf-content-wrapper overflow:hidden 瑁佸垏 */
+function popupContainer() {
+  return document.body;
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper tms-self-test-records-page">
+    <div class="jnpf-content-wrapper-center tms-self-test-records-center">
+      <div class="jnpf-content-wrapper-content tms-self-test-records">
+        <div class="records-top">
+          <div class="page-header">
+            <div>
+              <div class="text-base font-medium">鎴戠殑妫�娴嬭褰�</div>
+              <div class="mt-1 text-gray-400 text-sm">鏌ョ湅鍘嗗彶鑷垜妫�娴嬫垚缁╀笌閿欓銆�</div>
+            </div>
+            <a-button @click="goBack">杩斿洖鑷垜妫�娴�</a-button>
+          </div>
+
+          <div class="search-bar">
+            <a-form layout="inline" class="search-fields" :model="queryForm" @finish="handleSearch">
+              <a-form-item label="棰樺簱">
+                <jnpf-select
+                  v-model:value="queryForm.bankId"
+                  :options="banks"
+                  allow-clear
+                  show-search
+                  placeholder="璇烽�夋嫨棰樺簱"
+                  class="!w-[200px]"
+                />
+              </a-form-item>
+              <a-form-item label="鐘舵��">
+                <jnpf-select
+                  v-model:value="queryForm.testStatus"
+                  :options="statusOpts"
+                  :field-names="TMS_DIC_FIELD_NAMES"
+                  allow-clear
+                  placeholder="璇烽�夋嫨鐘舵��"
+                  class="!w-[140px]"
+                />
+              </a-form-item>
+              <a-form-item label="鏃堕棿鑼冨洿">
+                <jnpf-date-range
+                  v-model:value="queryForm.timeRange"
+                  allow-clear
+                  format="YYYY-MM-DD"
+                  class="!w-[260px]"
+                  :placeholder="['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡']"
+                  :get-popup-container="popupContainer"
+                />
+              </a-form-item>
+            </a-form>
+            <a-space class="search-actions">
+              <a-button type="primary" :loading="loading" @click="handleSearch">鏌ヨ</a-button>
+              <a-button @click="handleReset">閲嶇疆</a-button>
+            </a-space>
+          </div>
+        </div>
+
+        <div class="records-body">
+          <a-table
+            :data-source="records"
+            :loading="loading"
+            row-key="id"
+            size="middle"
+            :pagination="{
+              current: pagination.currentPage,
+              pageSize: pagination.pageSize,
+              total: pagination.total,
+              showSizeChanger: true,
+              showTotal: (t: number) => `鍏� ${t} 鏉,
+              onChange: onPageChange,
+            }"
+            :columns="[
+              { title: '棰樺簱', dataIndex: 'bankName', key: 'bankName', ellipsis: true },
+              { title: '棰橀噺', dataIndex: 'totalCount', key: 'totalCount', width: 80 },
+              { title: '姝g‘', dataIndex: 'correctCount', key: 'correctCount', width: 80 },
+              { title: '姝g‘鐜�', key: 'scoreRate', width: 100 },
+              { title: '鐘舵��', key: 'testStatus', width: 100 },
+              { title: '寮�濮嬫椂闂�', dataIndex: 'startTime', key: 'startTime', width: 170 },
+              { title: '浜ゅ嵎鏃堕棿', dataIndex: 'submitTime', key: 'submitTime', width: 170 },
+              { title: '鎿嶄綔', key: 'action', width: 120, fixed: 'right' },
+            ]"
+          >
+            <template #bodyCell="{ column, record }">
+              <template v-if="column.key === 'scoreRate'">
+                {{ record.scoreRate != null ? `${record.scoreRate}%` : '-' }}
+              </template>
+              <template v-else-if="column.key === 'testStatus'">
+                {{ statusLabel(record.testStatus) }}
+              </template>
+              <template v-else-if="column.key === 'correctCount'">
+                {{ record.correctCount ?? '-' }}
+              </template>
+              <template v-else-if="column.key === 'action'">
+                <a-button
+                  v-if="record.testStatus === 'doing'"
+                  type="link"
+                  size="small"
+                  :loading="continueLoadingId === record.id"
+                  @click="handleContinue(record)"
+                >
+                  缁х画鑰冭瘯
+                </a-button>
+                <a-button
+                  v-else
+                  type="link"
+                  size="small"
+                  @click="handleView(record)"
+                >
+                  鏌ョ湅
+                </a-button>
+              </template>
+            </template>
+          </a-table>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* 鍏ㄥ眬 jnpf-content-wrapper* 涓� overflow:hidden 涓旀棤 min-height:0锛屾粴鍔ㄥ繀椤昏惤鍦ㄥ唴灞� body */
+.tms-self-test-records-page {
+  min-height: 0;
+}
+
+.tms-self-test-records-center {
+  min-height: 0 !important;
+}
+
+.tms-self-test-records {
+  display: flex !important;
+  flex-direction: column;
+  flex: 1 1 0 !important;
+  min-height: 0 !important;
+  height: auto !important;
+  overflow: hidden !important;
+  background: #fff;
+  padding: 0;
+}
+
+.records-top {
+  flex-shrink: 0;
+  padding: 20px 24px 0;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.search-bar {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 16px;
+  margin-bottom: 12px;
+}
+
+.search-fields {
+  flex: 1;
+  row-gap: 12px;
+}
+
+.search-actions {
+  flex-shrink: 0;
+  padding-top: 4px;
+}
+
+.records-body {
+  flex: 1 1 0;
+  min-height: 0;
+  overflow-y: auto !important;
+  overflow-x: hidden;
+  padding: 0 24px 24px;
+  -webkit-overflow-scrolling: touch;
+}
+
+.records-body :deep(.ant-table-wrapper) {
+  overflow: visible !important;
+}
+</style>

--
Gitblit v1.8.0