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