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/api/x/tms/evalMode.ts                |   37 
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Detail.vue    |  192 +
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/exam.vue      |  976 +++++++++
 apps/jnpf-web-apps-main/src/views/x/tms/record/index.vue         |  112 
 apps/jnpf-web-apps-main/src/api/x/tms/question.ts                |   51 
 apps/jnpf-web-apps-main/src/views/x/tms/record/Edit.vue          |    5 
 apps/jnpf-web-apps-main/src/api/x/tms/examScore.ts               |   32 
 apps/jnpf-web-apps-main/src/router/routes/modules/tmsSelfTest.ts |    6 
 apps/jnpf-web-apps-main/src/views/x/tms/course/index.vue         |  117 
 apps/jnpf-web-apps-main/src/views/x/tms/course/constants.ts      |   45 
 apps/jnpf-web-apps-main/src/api/x/tms/signMode.ts                |   37 
 apps/jnpf-web-apps-main/src/views/x/tms/trainMode/Form.vue       |   89 
 apps/jnpf-web-apps-main/src/api/x/tms/course.ts                  |   41 
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/types.ts      |   70 
 apps/jnpf-web-apps-main/src/views/x/tms/examGrade/index.vue      |   23 
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/mock.ts       |   82 
 apps/jnpf-web-apps-main/src/views/x/tms/question/Form.vue        |   15 
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/constants.ts  |   63 
 apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Mark.vue       |   57 
 apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue        |  825 ++++++++
 apps/jnpf-web-apps-main/src/views/x/tms/record/Detail.vue        |   45 
 apps/jnpf-web-apps-main/src/views/x/tms/evalMode/Form.vue        |   51 
 apps/jnpf-web-apps-main/src/api/x/tms/trainRecord.ts             |   25 
 apps/jnpf-web-apps-main/src/views/x/tms/question/index.vue       |  127 +
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue     |  259 ++
 apps/jnpf-web-apps-main/src/views/x/tms/examScore/index.vue      |   98 
 apps/jnpf-web-apps-main/src/views/x/tms/record/Sign.vue          |   93 
 apps/jnpf-web-apps-main/src/views/x/tms/examScore/Paper.vue      |   29 
 apps/jnpf-web-apps-main/src/views/x/tms/record/constants.ts      |   53 
 apps/jnpf-web-apps-main/src/views/x/tms/record/types.ts          |    2 
 apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue      |  214 ++
 apps/jnpf-web-apps-main/src/views/x/tms/examScore/Detail.vue     |   24 
 apps/jnpf-web-apps-main/src/api/x/tms/trainArchive.ts            |   24 
 apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Session.vue    |   42 
 apps/jnpf-web-apps-main/src/views/x/tms/trainMode/index.vue      |   77 
 apps/jnpf-web-apps-main/src/api/x/tms/record.ts                  |   35 
 apps/jnpf-web-apps-main/src/api/x/tms/examGrade.ts               |   44 
 apps/jnpf-web-apps-main/src/views/x/tms/signMode/index.vue       |   60 
 apps/jnpf-web-apps-main/src/views/x/tms/selfTest/records.vue     |  355 +++
 apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/index.vue    |    9 
 apps/jnpf-web-apps-main/src/views/x/tms/course/Form.vue          |   79 
 apps/jnpf-web-apps-main/src/views/x/tms/trainMode/mock.ts        |    1 
 apps/jnpf-web-apps-main/src/views/x/tms/shared/dic.ts            |  167 +
 apps/jnpf-web-apps-main/src/views/x/tms/trainArchive/index.vue   |   23 
 apps/jnpf-web-apps-main/src/views/x/tms/signMode/Form.vue        |   43 
 apps/jnpf-web-apps-main/src/views/x/tms/examGrade/constants.ts   |   26 
 apps/jnpf-web-apps-main/src/api/x/tms/selfTest.ts                |   49 
 apps/jnpf-web-apps-main/src/views/x/tms/evalMode/index.vue       |   64 
 apps/jnpf-web-apps-main/src/api/x/tms/onlineExam.ts              |   48 
 apps/jnpf-web-apps-main/src/views/x/tms/question/constants.ts    |   61 
 apps/jnpf-web-apps-main/src/views/x/tms/question/Detail.vue      |  254 ++
 apps/jnpf-web-apps-main/src/views/x/tms/selfTest/types.ts        |   36 
 /dev/null                                                        |   26 
 apps/jnpf-web-apps-main/src/router/routes/modules/tmsQuestion.ts |    2 
 apps/jnpf-web-apps-main/src/views/x/tms/selfTest/index.vue       |   45 
 apps/jnpf-web-apps-main/src/api/x/tms/trainMode.ts               |   37 
 apps/jnpf-web-apps-main/src/views/x/tms/shared/page.css          |   40 
 apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/Catalog.vue  |   43 
 apps/jnpf-web-apps-main/src/views/x/tms/shared/ui.ts             |   48 
 apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/index.vue     |  109 
 60 files changed, 4,853 insertions(+), 889 deletions(-)

diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/course.ts b/apps/jnpf-web-apps-main/src/api/x/tms/course.ts
index cb540e4..0fe5bc2 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/course.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/course.ts
@@ -1,49 +1,44 @@
 import type { CourseItem, CoursePageQuery, PaperOption } from '#/views/x/tms/course/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockDeleteCourse,
-  mockGetCourse,
-  mockPaperOptions,
-  mockQueryCourses,
-  mockSaveCourse,
-  mockSetCourseEnabled,
-} from '#/views/x/tms/course/mock';
 
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/course/** */
 const prefix = '/api/tms/course';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getCourseList(params: CoursePageQuery) {
-  if (USE_MOCK) return mockQueryCourses(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: CourseItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getCourseInfo(id: string) {
-  if (USE_MOCK) return mockGetCourse(id);
-  return defHttp.get<CourseItem>({ url: `${prefix}/${id}` });
+  return unwrapData<CourseItem>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function getCoursePaperOptions() {
-  if (USE_MOCK) return mockPaperOptions();
-  return defHttp.get<PaperOption[]>({ url: `${prefix}/paper-options` });
+  return unwrapData<PaperOption[]>(defHttp.get({ url: `${prefix}/paper-options` }));
 }
 
 export function createCourse(data: Partial<CourseItem>) {
-  if (USE_MOCK) return mockSaveCourse(data);
-  return defHttp.post({ url: prefix, data });
+  return unwrapData<string>(defHttp.post({ url: prefix, data }));
 }
 
 export function updateCourse(data: Partial<CourseItem> & { id: string }) {
-  if (USE_MOCK) return mockSaveCourse(data);
-  return defHttp.put({ url: `${prefix}/${data.id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${data.id}`, data }));
 }
 
 export function setCourseEnabled(id: string, enabled: '0' | '1') {
-  if (USE_MOCK) return mockSetCourseEnabled(id, enabled);
-  return defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } });
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } }));
 }
 
 export function deleteCourse(id: string) {
-  if (USE_MOCK) return mockDeleteCourse(id);
-  return defHttp.delete({ url: `${prefix}/${id}` });
+  return unwrapData(defHttp.delete({ url: `${prefix}/${id}` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/evalMode.ts b/apps/jnpf-web-apps-main/src/api/x/tms/evalMode.ts
index a559a54..395dd1b 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/evalMode.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/evalMode.ts
@@ -1,43 +1,40 @@
 import type { EvalModeItem, EvalModePageQuery } from '#/views/x/tms/evalMode/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockDeleteEvalMode,
-  mockGetEvalMode,
-  mockQueryEvalModes,
-  mockSaveEvalMode,
-  mockSetEvalModeEnabled,
-} from '#/views/x/tms/evalMode/mock';
 
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/eval-mode/** */
 const prefix = '/api/tms/eval-mode';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getEvalModeList(params: EvalModePageQuery) {
-  if (USE_MOCK) return mockQueryEvalModes(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: EvalModeItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getEvalModeInfo(id: string) {
-  if (USE_MOCK) return mockGetEvalMode(id);
-  return defHttp.get<EvalModeItem>({ url: `${prefix}/${id}` });
+  return unwrapData<EvalModeItem>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function createEvalMode(data: Partial<EvalModeItem>) {
-  if (USE_MOCK) return mockSaveEvalMode(data);
-  return defHttp.post({ url: prefix, data });
+  return unwrapData<string>(defHttp.post({ url: prefix, data }));
 }
 
 export function updateEvalMode(data: Partial<EvalModeItem> & { id: string }) {
-  if (USE_MOCK) return mockSaveEvalMode(data);
-  return defHttp.put({ url: `${prefix}/${data.id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${data.id}`, data }));
 }
 
 export function setEvalModeEnabled(id: string, enabled: '0' | '1') {
-  if (USE_MOCK) return mockSetEvalModeEnabled(id, enabled);
-  return defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } });
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } }));
 }
 
 export function deleteEvalMode(id: string) {
-  if (USE_MOCK) return mockDeleteEvalMode(id);
-  return defHttp.delete({ url: `${prefix}/${id}` });
+  return unwrapData(defHttp.delete({ url: `${prefix}/${id}` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/examGrade.ts b/apps/jnpf-web-apps-main/src/api/x/tms/examGrade.ts
index dd555c7..5e7cdfc 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/examGrade.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/examGrade.ts
@@ -1,44 +1,50 @@
 import type {
   GradeExamDetail,
+  GradeExamListItem,
   GradeSessionListItem,
   GradeSessionPageQuery,
   GradeSubmitPayload,
 } from '#/views/x/tms/examGrade/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockGetGradeExamDetail,
-  mockGetGradeSession,
-  mockQueryGradeExams,
-  mockQueryGradeSessions,
-  mockSubmitGrade,
-} from '#/views/x/tms/examGrade/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
 const prefix = '/api/tms/exam-grade';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getGradeSessionList(params: GradeSessionPageQuery) {
-  if (USE_MOCK) return mockQueryGradeSessions(params);
-  return defHttp.get({ url: `${prefix}/sessions`, params });
+  return unwrapData<{ list: GradeSessionListItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/sessions`, params }),
+  );
 }
 
 export function getGradeSessionInfo(id: string) {
-  if (USE_MOCK) return mockGetGradeSession(id);
-  return defHttp.get<GradeSessionListItem>({ url: `${prefix}/sessions/${id}` });
+  return unwrapData<GradeSessionListItem>(defHttp.get({ url: `${prefix}/sessions/${id}` }));
 }
 
 export function getGradeExamList(sessionId: string) {
-  if (USE_MOCK) return mockQueryGradeExams(sessionId);
-  return defHttp.get({ url: `${prefix}/sessions/${sessionId}/exams` });
+  return unwrapData<GradeExamListItem[]>(
+    defHttp.get({ url: `${prefix}/sessions/${sessionId}/exams` }),
+  );
 }
 
 export function getGradeExamDetail(examId: string) {
-  if (USE_MOCK) return mockGetGradeExamDetail(examId);
-  return defHttp.get<GradeExamDetail>({ url: `${prefix}/exams/${examId}` });
+  return unwrapData<GradeExamDetail>(defHttp.get({ url: `${prefix}/exams/${examId}` }));
 }
 
 export function submitGrade(data: GradeSubmitPayload) {
-  if (USE_MOCK) return mockSubmitGrade(data);
-  return defHttp.post({ url: `${prefix}/exams/${data.examId}/submit`, data });
+  return unwrapData<{
+    examId: string;
+    subjectiveScore?: number;
+    totalScore?: number;
+    passFlag?: string;
+    gradeStatus?: string;
+    nextExamId?: string;
+  }>(defHttp.post({ url: `${prefix}/exams/${data.examId}/submit`, data }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/examScore.ts b/apps/jnpf-web-apps-main/src/api/x/tms/examScore.ts
index 3b5a8da..cdcf1a5 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/examScore.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/examScore.ts
@@ -1,27 +1,31 @@
-import type { ExamPaperView, ExamScoreDetail, ExamScorePageQuery } from '#/views/x/tms/examScore/types';
+import type { ExamPaperView, ExamScoreDetail, ExamScoreDetailRow, ExamScorePageQuery, ExamScoreSummaryItem } from '#/views/x/tms/examScore/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockGetExamPaperView,
-  mockGetExamScoreDetail,
-  mockQueryExamScores,
-} from '#/views/x/tms/examScore/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
 const prefix = '/api/tms/exam-score';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getExamScoreList(params: ExamScorePageQuery) {
-  if (USE_MOCK) return mockQueryExamScores(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: ExamScoreSummaryItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getExamScoreDetail(id: string) {
-  if (USE_MOCK) return mockGetExamScoreDetail(id);
-  return defHttp.get<ExamScoreDetail>({ url: `${prefix}/${id}` });
+  return unwrapData<ExamScoreDetail>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function getExamPaperView(examId: string) {
-  if (USE_MOCK) return mockGetExamPaperView(examId);
-  return defHttp.get<ExamPaperView>({ url: `${prefix}/paper/${examId}` });
+  return unwrapData<ExamPaperView>(defHttp.get({ url: `${prefix}/paper/${examId}` }));
+}
+
+export function exportExamScores(ids: string[]) {
+  return unwrapData<ExamScoreDetailRow[]>(defHttp.post({ url: `${prefix}/export`, data: { ids } }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/onlineExam.ts b/apps/jnpf-web-apps-main/src/api/x/tms/onlineExam.ts
index 64ac990..20f15f6 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/onlineExam.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/onlineExam.ts
@@ -1,37 +1,51 @@
 import type {
+  MyPaperListItem,
   MyPaperPageQuery,
   OnlineExamDetail,
   OnlineExamPaper,
+  OnlineExamSubmitResult,
 } from '#/views/x/tms/onlineExam/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockGetMyPaperDetail,
-  mockQueryMyPapers,
-  mockStartExam,
-  mockSubmitExam,
-} from '#/views/x/tms/onlineExam/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
 const prefix = '/api/tms/online-exam';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getMyPaperList(params: MyPaperPageQuery) {
-  if (USE_MOCK) return mockQueryMyPapers(params);
-  return defHttp.get({ url: `${prefix}/my-papers`, params });
+  return unwrapData<{ list: MyPaperListItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/my-papers`, params }),
+  );
 }
 
 export function getMyPaperDetail(id: string) {
-  if (USE_MOCK) return mockGetMyPaperDetail(id);
-  return defHttp.get<OnlineExamDetail>({ url: `${prefix}/my-papers/${id}` });
+  return unwrapData<OnlineExamDetail>(defHttp.get({ url: `${prefix}/my-papers/${id}` }));
+}
+
+export function getExamReview(examId: string) {
+  return unwrapData<OnlineExamDetail>(defHttp.get({ url: `${prefix}/exams/${examId}` }));
 }
 
 export function startOnlineExam(myPaperId: string) {
-  if (USE_MOCK) return mockStartExam(myPaperId);
-  return defHttp.post<OnlineExamPaper>({ url: `${prefix}/start`, data: { myPaperId } });
+  return unwrapData<OnlineExamPaper>(
+    defHttp.post({ url: `${prefix}/start`, data: { myPaperId } }, { errorMessageMode: 'none' }),
+  );
 }
 
-export function submitOnlineExam(examId: string, data: { score: number; answers: Record<string, string | string[]> }) {
-  if (USE_MOCK) return mockSubmitExam(examId, data.score);
-  return defHttp.post({ url: `${prefix}/${examId}/submit`, data });
+export function saveOnlineExam(examId: string, answers: Record<string, string | string[]>) {
+  return unwrapData(
+    defHttp.post({ url: `${prefix}/${examId}/save`, data: { answers } }, { errorMessageMode: 'none' }),
+  );
+}
+
+export function submitOnlineExam(examId: string, answers: Record<string, string | string[]>) {
+  return unwrapData<OnlineExamSubmitResult>(
+    defHttp.post({ url: `${prefix}/${examId}/submit`, data: { answers } }, { errorMessageMode: 'none' }),
+  );
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/question.ts b/apps/jnpf-web-apps-main/src/api/x/tms/question.ts
index 10fedd7..c5e9ce9 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/question.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/question.ts
@@ -1,55 +1,46 @@
 import type { QuestionBankOption, QuestionEntity, QuestionPageQuery } from '#/views/x/tms/question/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockCreateQuestion,
-  mockDeleteQuestion,
-  mockGetAdmins,
-  mockGetBanks,
-  mockGetQuestion,
-  mockQueryQuestions,
-  mockUpdateQuestion,
-} from '#/views/x/tms/question/mock';
 
-/**
- * 鍚庣 API 鏈氨缁墠璧版湰鍦� mock锛涜仈璋� jnpf-tms 鍚庢敼涓� false銆�
- * 姝e紡璺緞绾﹀畾锛�/api/tms/question/**
- */
-const USE_MOCK = true;
-
+/** 姝e紡璺緞锛�/api/tms/question/** */
 const prefix = '/api/tms/question';
 
+/** defHttp 鎴愬姛鏃惰繑鍥炴暣鍖� ActionResult锛岄〉闈㈤渶瑕佽8 data */
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getQuestionBanks() {
-  if (USE_MOCK) return mockGetBanks();
-  return defHttp.get<QuestionBankOption[]>({ url: `${prefix}/banks` });
+  return unwrapData<QuestionBankOption[]>(defHttp.get({ url: `${prefix}/banks` }));
 }
 
 export function getQuestionAdmins() {
-  if (USE_MOCK) return mockGetAdmins();
-  return defHttp.get<{ id: string; fullName: string }[]>({ url: `${prefix}/admins` });
+  return unwrapData<{ id: string; fullName: string }[]>(defHttp.get({ url: `${prefix}/admins` }));
 }
 
 export function getQuestionList(params: QuestionPageQuery) {
-  if (USE_MOCK) return mockQueryQuestions(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: QuestionEntity[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getQuestionInfo(id: string) {
-  if (USE_MOCK) return mockGetQuestion(id);
-  return defHttp.get<QuestionEntity>({ url: `${prefix}/${id}` });
+  return unwrapData<QuestionEntity>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function createQuestion(data: QuestionEntity) {
-  if (USE_MOCK) return mockCreateQuestion(data);
-  return defHttp.post({ url: prefix, data });
+  return unwrapData<string>(defHttp.post({ url: prefix, data }));
 }
 
 export function updateQuestion(data: QuestionEntity) {
-  if (USE_MOCK) return mockUpdateQuestion(data);
-  return defHttp.put({ url: `${prefix}/${data.id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${data.id}`, data }));
 }
 
-export function deleteQuestion(id: string) {
-  if (USE_MOCK) return mockDeleteQuestion(id);
-  return defHttp.delete({ url: `${prefix}/${id}` });
+/** 搴熷純 = 閫昏緫鍒犻櫎 */
+export function invalidateQuestion(id: string) {
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}/invalidate` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/record.ts b/apps/jnpf-web-apps-main/src/api/x/tms/record.ts
index 2398a8a..79b8cce 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/record.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/record.ts
@@ -1,39 +1,36 @@
 import type { TmsRecordDetail, TmsRecordPageQuery } from '#/views/x/tms/record/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockArchiveRecords,
-  mockDeleteRecords,
-  mockGetRecordDetail,
-  mockQueryRecords,
-  mockUpdateRecord,
-} from '#/views/x/tms/record/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/record/** */
 const prefix = '/api/tms/record';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getTmsRecordList(params: TmsRecordPageQuery) {
-  if (USE_MOCK) return mockQueryRecords(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: any[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getTmsRecordDetail(id: string) {
-  if (USE_MOCK) return mockGetRecordDetail(id);
-  return defHttp.get<TmsRecordDetail>({ url: `${prefix}/${id}` });
+  return unwrapData<TmsRecordDetail>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function updateTmsRecord(id: string, data: Record<string, any>) {
-  if (USE_MOCK) return mockUpdateRecord(id, data);
-  return defHttp.put({ url: `${prefix}/${id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}`, data }));
 }
 
 export function archiveTmsRecords(ids: string[]) {
-  if (USE_MOCK) return mockArchiveRecords(ids);
-  return defHttp.post({ url: `${prefix}/archive`, data: { ids } });
+  return unwrapData<number>(defHttp.post({ url: `${prefix}/archive`, data: { ids } }));
 }
 
 export function deleteTmsRecords(ids: string[]) {
-  if (USE_MOCK) return mockDeleteRecords(ids);
-  return defHttp.delete({ url: `${prefix}`, data: { ids } });
+  return unwrapData(defHttp.delete({ url: `${prefix}`, data: { ids } }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/selfTest.ts b/apps/jnpf-web-apps-main/src/api/x/tms/selfTest.ts
index 0667daf..38a18e1 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/selfTest.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/selfTest.ts
@@ -1,19 +1,56 @@
 import type { QuestionBankOption } from '#/views/x/tms/question/types';
-import type { SelfTestPaper, SelfTestStartPayload } from '#/views/x/tms/selfTest/types';
+import type {
+  SelfTestPaper,
+  SelfTestStartPayload,
+  SelfTestSubmitPayload,
+  SelfTestSubmitResult,
+  SelfTestRecordItem,
+} from '#/views/x/tms/selfTest/types';
 
 import { defHttp } from '#/api/request';
 import { getQuestionBanks } from '#/api/x/tms/question';
-import { mockStartSelfTest } from '#/views/x/tms/selfTest/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/self-test/** */
 const prefix = '/api/tms/self-test';
+
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
 
 export function getSelfTestBanks() {
   return getQuestionBanks() as Promise<QuestionBankOption[]>;
 }
 
 export function startSelfTest(data: SelfTestStartPayload) {
-  if (USE_MOCK) return mockStartSelfTest(data);
-  return defHttp.post<SelfTestPaper>({ url: `${prefix}/start`, data });
+  return unwrapData<SelfTestPaper>(defHttp.post({ url: `${prefix}/start`, data }));
+}
+
+/** 鏆傚瓨浣滅瓟锛堜笉浜ゅ嵎锛屼究浜庣户缁�冭瘯锛� */
+export function saveSelfTest(paperId: string, data: SelfTestSubmitPayload) {
+  return unwrapData<string>(defHttp.post({ url: `${prefix}/${paperId}/save`, data }));
+}
+
+export function submitSelfTest(paperId: string, data: SelfTestSubmitPayload) {
+  return unwrapData<SelfTestSubmitResult>(defHttp.post({ url: `${prefix}/${paperId}/submit`, data }));
+}
+
+export function getSelfTestList(params?: {
+  currentPage?: number;
+  pageSize?: number;
+  bankId?: string;
+  testStatus?: string;
+  startTimeBegin?: string;
+  startTimeEnd?: string;
+}) {
+  return unwrapData<{ list: SelfTestRecordItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
+}
+
+export function getSelfTestInfo(id: string) {
+  return unwrapData<SelfTestPaper>(defHttp.get({ url: `${prefix}/${id}` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/signMode.ts b/apps/jnpf-web-apps-main/src/api/x/tms/signMode.ts
index 5543aea..f9047ce 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/signMode.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/signMode.ts
@@ -1,43 +1,40 @@
 import type { SignModeItem, SignModePageQuery } from '#/views/x/tms/signMode/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockDeleteSignMode,
-  mockGetSignMode,
-  mockQuerySignModes,
-  mockSaveSignMode,
-  mockSetSignModeEnabled,
-} from '#/views/x/tms/signMode/mock';
 
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/sign-mode/** */
 const prefix = '/api/tms/sign-mode';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getSignModeList(params: SignModePageQuery) {
-  if (USE_MOCK) return mockQuerySignModes(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: SignModeItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getSignModeInfo(id: string) {
-  if (USE_MOCK) return mockGetSignMode(id);
-  return defHttp.get<SignModeItem>({ url: `${prefix}/${id}` });
+  return unwrapData<SignModeItem>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function createSignMode(data: Partial<SignModeItem>) {
-  if (USE_MOCK) return mockSaveSignMode(data);
-  return defHttp.post({ url: prefix, data });
+  return unwrapData<string>(defHttp.post({ url: prefix, data }));
 }
 
 export function updateSignMode(data: Partial<SignModeItem> & { id: string }) {
-  if (USE_MOCK) return mockSaveSignMode(data);
-  return defHttp.put({ url: `${prefix}/${data.id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${data.id}`, data }));
 }
 
 export function setSignModeEnabled(id: string, enabled: '0' | '1') {
-  if (USE_MOCK) return mockSetSignModeEnabled(id, enabled);
-  return defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } });
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } }));
 }
 
 export function deleteSignMode(id: string) {
-  if (USE_MOCK) return mockDeleteSignMode(id);
-  return defHttp.delete({ url: `${prefix}/${id}` });
+  return unwrapData(defHttp.delete({ url: `${prefix}/${id}` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/trainArchive.ts b/apps/jnpf-web-apps-main/src/api/x/tms/trainArchive.ts
index b946e5f..47a28f7 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/trainArchive.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/trainArchive.ts
@@ -1,19 +1,19 @@
 import type { TrainArchiveDetail } from '#/views/x/tms/trainArchive/types';
 
 import { defHttp } from '#/api/request';
-import { mockGetMyTrainArchive } from '#/views/x/tms/trainArchive/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/train-archive/** */
 const prefix = '/api/tms/train-archive';
 
-export function getMyTrainArchive(user?: {
-  userId?: string;
-  userName?: string;
-  userAccount?: string;
-  organizeName?: string;
-  positionName?: string;
-}) {
-  if (USE_MOCK) return mockGetMyTrainArchive(user);
-  return defHttp.get<TrainArchiveDetail>({ url: `${prefix}/mine` });
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
+/** 褰撳墠鐧诲綍鐢ㄦ埛鐨勪釜浜哄煿璁。妗� */
+export function getMyTrainArchive() {
+  return unwrapData<TrainArchiveDetail>(defHttp.get({ url: `${prefix}/mine` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/trainMode.ts b/apps/jnpf-web-apps-main/src/api/x/tms/trainMode.ts
index 0cd19ef..43d68c5 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/trainMode.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/trainMode.ts
@@ -1,43 +1,40 @@
 import type { TrainModeItem, TrainModePageQuery } from '#/views/x/tms/trainMode/types';
 
 import { defHttp } from '#/api/request';
-import {
-  mockDeleteTrainMode,
-  mockGetTrainMode,
-  mockQueryTrainModes,
-  mockSaveTrainMode,
-  mockSetTrainModeEnabled,
-} from '#/views/x/tms/trainMode/mock';
 
-const USE_MOCK = true;
+/** 姝e紡璺緞锛�/api/tms/train-mode/** */
 const prefix = '/api/tms/train-mode';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getTrainModeList(params: TrainModePageQuery) {
-  if (USE_MOCK) return mockQueryTrainModes(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: TrainModeItem[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getTrainModeInfo(id: string) {
-  if (USE_MOCK) return mockGetTrainMode(id);
-  return defHttp.get<TrainModeItem>({ url: `${prefix}/${id}` });
+  return unwrapData<TrainModeItem>(defHttp.get({ url: `${prefix}/${id}` }));
 }
 
 export function createTrainMode(data: Partial<TrainModeItem>) {
-  if (USE_MOCK) return mockSaveTrainMode(data);
-  return defHttp.post({ url: prefix, data });
+  return unwrapData<string>(defHttp.post({ url: prefix, data }));
 }
 
 export function updateTrainMode(data: Partial<TrainModeItem> & { id: string }) {
-  if (USE_MOCK) return mockSaveTrainMode(data);
-  return defHttp.put({ url: `${prefix}/${data.id}`, data });
+  return unwrapData(defHttp.put({ url: `${prefix}/${data.id}`, data }));
 }
 
 export function setTrainModeEnabled(id: string, enabled: '0' | '1') {
-  if (USE_MOCK) return mockSetTrainModeEnabled(id, enabled);
-  return defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } });
+  return unwrapData(defHttp.put({ url: `${prefix}/${id}/enabled`, data: { enabled } }));
 }
 
 export function deleteTrainMode(id: string) {
-  if (USE_MOCK) return mockDeleteTrainMode(id);
-  return defHttp.delete({ url: `${prefix}/${id}` });
+  return unwrapData(defHttp.delete({ url: `${prefix}/${id}` }));
 }
diff --git a/apps/jnpf-web-apps-main/src/api/x/tms/trainRecord.ts b/apps/jnpf-web-apps-main/src/api/x/tms/trainRecord.ts
index ef11e81..2343cf1 100644
--- a/apps/jnpf-web-apps-main/src/api/x/tms/trainRecord.ts
+++ b/apps/jnpf-web-apps-main/src/api/x/tms/trainRecord.ts
@@ -5,18 +5,29 @@
 } from '#/views/x/tms/trainRecord/types';
 
 import { defHttp } from '#/api/request';
-import { mockGetTrainRecordCatalog, mockQueryTrainRecords } from '#/views/x/tms/trainRecord/mock';
 
-/** 鍚庣鏈氨缁墠璧� mock锛涜仈璋冨悗鏀逛负 false */
-const USE_MOCK = true;
 const prefix = '/api/tms/train-record';
 
+async function unwrapData<T>(promise: Promise<any>): Promise<T> {
+  const res = await promise;
+  if (res && typeof res === 'object' && 'data' in res && ('code' in res || 'msg' in res)) {
+    return res.data as T;
+  }
+  return res as T;
+}
+
 export function getTrainRecordList(params: TrainRecordPageQuery) {
-  if (USE_MOCK) return mockQueryTrainRecords(params);
-  return defHttp.get({ url: `${prefix}/list`, params });
+  return unwrapData<{ list: any[]; pagination: Record<string, any> }>(
+    defHttp.get({ url: `${prefix}/list`, params }),
+  );
 }
 
 export function getTrainRecordCatalog(params: TrainRecordCatalogQuery) {
-  if (USE_MOCK) return mockGetTrainRecordCatalog(params);
-  return defHttp.get<TrainRecordCatalogDetail>({ url: `${prefix}/${params.recordId}/catalog`, params });
+  const { recordId, startDate, endDate } = params;
+  return unwrapData<TrainRecordCatalogDetail>(
+    defHttp.get({
+      url: `${prefix}/${recordId}/catalog`,
+      params: { startDate, endDate },
+    }),
+  );
 }
diff --git a/apps/jnpf-web-apps-main/src/router/routes/modules/tmsQuestion.ts b/apps/jnpf-web-apps-main/src/router/routes/modules/tmsQuestion.ts
index de3ab6d..0eb8e32 100644
--- a/apps/jnpf-web-apps-main/src/router/routes/modules/tmsQuestion.ts
+++ b/apps/jnpf-web-apps-main/src/router/routes/modules/tmsQuestion.ts
@@ -2,7 +2,7 @@
 
 /**
  * 鍒楄〃鍙敱鍚庡彴鑿滃崟鎸傝浇锛歱ageAddress = x/tms/question/index锛岃矾鐢� /tms/question
- * 鍒涘缓/缂栬緫宸叉寕鍒� basicRoutes锛堜笉渚濊禆鑿滃崟锛�
+ * 鍒涘缓/缂栬緫/璇︽儏宸叉寕鍒� basicRoutes锛堜笉渚濊禆鑿滃崟锛�
  */
 const tmsQuestionRoutes: RouteRecordRaw[] = [];
 
diff --git a/apps/jnpf-web-apps-main/src/router/routes/modules/tmsSelfTest.ts b/apps/jnpf-web-apps-main/src/router/routes/modules/tmsSelfTest.ts
index 1f9e582..92c0aef 100644
--- a/apps/jnpf-web-apps-main/src/router/routes/modules/tmsSelfTest.ts
+++ b/apps/jnpf-web-apps-main/src/router/routes/modules/tmsSelfTest.ts
@@ -1,11 +1,9 @@
-import type { RouteRecordRaw } from 'vue-router';
-
 /**
  * 鑷垜妫�娴嬪彲鐢卞悗鍙拌彍鍗曟寕杞斤細
  *   璺敱鍦板潃 /tms/selfTest
  *   椤甸潰鍦板潃 x/tms/selfTest/index
- * 绛旈椤靛凡鎸� basicRoutes锛�/tms/selfTest/exam
+ * 绛旈/璁板綍/璇︽儏椤靛凡鎸� basicRoutes銆�
  */
-const tmsSelfTestRoutes: RouteRecordRaw[] = [];
+const tmsSelfTestRoutes: import('vue-router').RouteRecordRaw[] = [];
 
 export default tmsSelfTestRoutes;
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/course/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/course/Form.vue
index 2dc15ba..d9548b4 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/course/Form.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/course/Form.vue
@@ -11,19 +11,20 @@
   getCoursePaperOptions,
   updateCourse,
 } from '#/api/x/tms/course';
-
+import { useBaseStore } from '#/store';
 import {
-  CATEGORY_OPTIONS,
-  ENABLE_OPTIONS,
-  EVAL_MODE_OPTIONS,
-  TRAIN_MODE_OPTIONS,
-  evalModeNeedPaper,
-} from './constants';
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  loadTmsDic,
+} from '#/views/x/tms/shared/dic';
+
+import { evalModeNeedPaper } from './constants';
 
 defineOptions({ name: 'TmsCourseForm' });
 
 const emit = defineEmits(['register', 'reload']);
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 
 const state = reactive({ id: '' });
 const { id } = toRefs(state);
@@ -56,7 +57,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: CATEGORY_OPTIONS,
+          options: [],
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -66,7 +68,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: TRAIN_MODE_OPTIONS,
+          options: [],
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -76,7 +79,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: EVAL_MODE_OPTIONS,
+          options: [],
+          fieldNames: TMS_DIC_FIELD_NAMES,
           onChange: (val: string) => syncPaperRequired(val),
         },
       },
@@ -110,7 +114,11 @@
         label: '鍚敤鐘舵��',
         component: 'Select',
         defaultValue: '1',
-        componentProps: { placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+        componentProps: {
+          placeholder: '璇烽�夋嫨',
+          options: [],
+          fieldNames: TMS_DIC_FIELD_NAMES,
+        },
         rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
       },
       {
@@ -136,6 +144,53 @@
   }
 }
 
+async function loadDicOptions() {
+  const [category, trainMode, evalMode, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.courseCategory),
+    loadTmsDic(baseStore, TMS_DIC.trainMode),
+    loadTmsDic(baseStore, TMS_DIC.evalMode),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  updateSchema([
+    {
+      field: 'category',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: category,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'trainMode',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: trainMode,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'evalMode',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: evalMode,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+        onChange: (val: string) => syncPaperRequired(val),
+      },
+    },
+    {
+      field: 'enabled',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: enable,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+  ]);
+}
+
 async function loadPaperOptions() {
   const list = await getCoursePaperOptions();
   updateSchema({
@@ -156,7 +211,7 @@
   resetFields();
   state.id = data?.id || '';
   try {
-    await loadPaperOptions();
+    await Promise.all([loadDicOptions(), loadPaperOptions()]);
     if (state.id) {
       const info = await getCourseInfo(state.id);
       setFieldsValue(info);
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/course/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/course/constants.ts
index 0450dc5..ced3ca6 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/course/constants.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/course/constants.ts
@@ -1,48 +1,3 @@
-export const ENABLE_OPTIONS = [
-  { id: '1', fullName: '鍚敤' },
-  { id: '0', fullName: '鍋滅敤' },
-];
-
-/** tmsCourseCategory */
-export const CATEGORY_OPTIONS = [
-  { id: 'gmp', fullName: 'GMP' },
-  { id: 'sop', fullName: 'SOP' },
-  { id: 'safety', fullName: '瀹夊叏' },
-  { id: 'skill', fullName: '鎶�鑳�' },
-  { id: 'other', fullName: '鍏朵粬' },
-];
-
-/** 涓庡煿璁柟寮忛厤缃紪鐮佸榻� */
-export const TRAIN_MODE_OPTIONS = [
-  { id: 'onsite', fullName: '闆嗕腑鎺堣' },
-  { id: 'practice', fullName: '鎿嶄綔鎺堣' },
-  { id: 'online', fullName: '鍦ㄧ嚎瀛︿範' },
-];
-
-/** 涓庤�冩牳鏂瑰紡閰嶇疆缂栫爜瀵归綈 */
-export const EVAL_MODE_OPTIONS = [
-  { id: 'quiz', fullName: '鎻愰棶' },
-  { id: 'practice', fullName: '鐜板満鎿嶄綔' },
-  { id: 'exam', fullName: '鍦ㄧ嚎鑰冭瘯' },
-  { id: 'none', fullName: '鏃犻渶鑰冩牳' },
-];
-
-export function labelOfEnabled(v?: string) {
-  return ENABLE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfCategory(v?: string) {
-  return CATEGORY_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfTrainMode(v?: string) {
-  return TRAIN_MODE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfEvalMode(v?: string) {
-  return EVAL_MODE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
 /** 鍦ㄧ嚎鑰冭瘯鏃跺叧鑱旇瘯鍗峰缓璁繀濉� */
 export function evalModeNeedPaper(evalMode?: string) {
   return evalMode === 'exam';
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/course/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/course/index.vue
index 53d819e..639fda1 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/course/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/course/index.vue
@@ -3,28 +3,34 @@
 
 import type { CourseItem } from './types';
 
+import { onMounted, ref } from 'vue';
+
 import { useMessage } from '@jnpf/hooks';
 import { useModal } from '@jnpf/ui/modal';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { deleteCourse, getCourseList, setCourseEnabled } from '#/api/x/tms/course';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
 
 import Form from './Form.vue';
-import {
-  CATEGORY_OPTIONS,
-  ENABLE_OPTIONS,
-  EVAL_MODE_OPTIONS,
-  TRAIN_MODE_OPTIONS,
-  labelOfCategory,
-  labelOfEnabled,
-  labelOfEvalMode,
-  labelOfTrainMode,
-} from './constants';
 
 defineOptions({ name: 'TmsCourse' });
 
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 const [registerForm, { openModal: openFormModal }] = useModal();
+
+const categoryOpts = ref<TmsDicOpt[]>([]);
+const trainModeOpts = ref<TmsDicOpt[]>([]);
+const evalModeOpts = ref<TmsDicOpt[]>([]);
+const enableOpts = ref<TmsDicOpt[]>([]);
 
 const columns: BasicColumn[] = [
   { title: '璇剧▼缂栧彿', dataIndex: 'courseNo', width: 140 },
@@ -33,19 +39,19 @@
     title: '璇剧▼鍒嗙被',
     dataIndex: 'category',
     width: 100,
-    customRender: ({ record }) => labelOfCategory((record as CourseItem).category),
+    customRender: ({ record }) => labelOfDic(categoryOpts.value, (record as CourseItem).category),
   },
   {
     title: '鍩硅鏂瑰紡',
     dataIndex: 'trainMode',
     width: 110,
-    customRender: ({ record }) => labelOfTrainMode((record as CourseItem).trainMode),
+    customRender: ({ record }) => labelOfDic(trainModeOpts.value, (record as CourseItem).trainMode),
   },
   {
     title: '鑰冩牳鏂瑰紡',
     dataIndex: 'evalMode',
     width: 110,
-    customRender: ({ record }) => labelOfEvalMode((record as CourseItem).evalMode),
+    customRender: ({ record }) => labelOfDic(evalModeOpts.value, (record as CourseItem).evalMode),
   },
   {
     title: '鍏宠仈鑰冩牳璇曞嵎',
@@ -73,10 +79,10 @@
   { title: '澶囨敞', dataIndex: 'remark', minWidth: 140 },
 ];
 
-const [registerTable, { reload }] = useVxeTable({
+const [registerTable, { reload, getForm }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
@@ -96,7 +102,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: CATEGORY_OPTIONS,
+          options: categoryOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -106,7 +113,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: TRAIN_MODE_OPTIONS,
+          options: trainModeOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -116,7 +124,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: EVAL_MODE_OPTIONS,
+          options: evalModeOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -126,7 +135,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: ENABLE_OPTIONS,
+          options: enableOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
     ],
@@ -137,6 +147,58 @@
     dataIndex: 'action',
     fixed: 'right',
   },
+});
+
+onMounted(async () => {
+  const [category, trainMode, evalMode, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.courseCategory),
+    loadTmsDic(baseStore, TMS_DIC.trainMode),
+    loadTmsDic(baseStore, TMS_DIC.evalMode),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  categoryOpts.value = category;
+  trainModeOpts.value = trainMode;
+  evalModeOpts.value = evalMode;
+  enableOpts.value = enable;
+  getForm()?.updateSchema?.([
+    {
+      field: 'category',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: category,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'trainMode',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: trainMode,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'evalMode',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: evalMode,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'enabled',
+      componentProps: {
+        allowClear: true,
+        placeholder: '璇烽�夋嫨',
+        options: enable,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+  ]);
+  reload();
 });
 
 async function fetchList(params: Record<string, any>) {
@@ -164,6 +226,10 @@
 }
 
 async function handleDelete(record: CourseItem) {
+  if (record.enabled !== '0') {
+    createMessage.warning('璇峰厛鍋滅敤鍚庡啀鍒犻櫎');
+    return;
+  }
   try {
     await deleteCourse(record.id);
     createMessage.success('鍒犻櫎鎴愬姛');
@@ -175,7 +241,7 @@
 
 function getTableActions(record: CourseItem): ActionItem[] {
   const enableLabel = record.enabled === '1' ? '鍋滅敤' : '鍚敤';
-  return [
+  const actions: ActionItem[] = [
     { label: '缂栬緫', onClick: handleEdit.bind(null, record) },
     {
       label: enableLabel,
@@ -184,15 +250,18 @@
         onOk: handleToggleEnabled.bind(null, record),
       },
     },
-    {
+  ];
+  if (record.enabled === '0') {
+    actions.push({
       label: '鍒犻櫎',
       color: 'error',
       modelConfirm: {
         content: `纭畾鍒犻櫎鍩硅璇剧▼銆�${record.courseName}銆嶅悧锛焋,
         onOk: handleDelete.bind(null, record),
       },
-    },
-  ];
+    });
+  }
+  return actions;
 }
 </script>
 
@@ -206,7 +275,7 @@
           </template>
           <template #enabled="{ record }">
             <a-tag :color="record.enabled === '1' ? 'success' : 'default'">
-              {{ labelOfEnabled(record.enabled) }}
+              {{ labelOfDic(enableOpts, record.enabled) }}
             </a-tag>
           </template>
           <template #action="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/course/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/course/mock.ts
deleted file mode 100644
index 69de8c8..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/course/mock.ts
+++ /dev/null
@@ -1,202 +0,0 @@
-import type { CourseItem, CoursePageQuery, PaperOption } from './types';
-
-function delay<T>(data: T, ms = 180): Promise<T> {
-  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
-}
-
-const papers: PaperOption[] = [
-  { id: 'p4', fullName: '2026-鑽墿璀︽垝骞村害鍩硅璇曢' },
-  { id: 'p5', fullName: 'GMP鍩虹鐭ヨ瘑鍩硅锛圦C銆佸垎鏋愶級' },
-  { id: 'p6', fullName: '鍐嶇‘璁ゆ祴璇�' },
-  { id: 'p7', fullName: 'SOP-鍋忓樊涓庡彉鏇寸患鍚堣�冩牳' },
-  { id: 'p8', fullName: '瀹夊叏鐢熶骇鏈堜笓椤硅�冭瘯' },
-];
-
-const store: CourseItem[] = [
-  {
-    id: 'course_001',
-    courseNo: 'KC2026030001',
-    courseName: 'GMP鍩虹鐭ヨ瘑鍩硅',
-    category: 'gmp',
-    trainMode: 'onsite',
-    evalMode: 'exam',
-    paperId: 'p5',
-    paperName: 'GMP鍩虹鐭ヨ瘑鍩硅锛圦C銆佸垎鏋愶級',
-    hours: 4,
-    enabled: '1',
-    remark: '鏂颁汉鍏ヨ亴蹇呬慨',
-  },
-  {
-    id: 'course_002',
-    courseNo: 'KC2026030002',
-    courseName: '鍋忓樊涓庡彉鏇寸鐞� SOP',
-    category: 'sop',
-    trainMode: 'onsite',
-    evalMode: 'quiz',
-    paperId: undefined,
-    paperName: undefined,
-    hours: 2,
-    enabled: '1',
-    remark: '',
-  },
-  {
-    id: 'course_003',
-    courseNo: 'KC2026030003',
-    courseName: '璁惧鎿嶄綔瀹炴搷鍩硅',
-    category: 'skill',
-    trainMode: 'practice',
-    evalMode: 'practice',
-    hours: 8,
-    enabled: '1',
-    remark: '闇�鐜板満瀹炴搷璇勫垎',
-  },
-  {
-    id: 'course_004',
-    courseNo: 'KC2026030004',
-    courseName: '鑽墿璀︽垝骞村害鍩硅',
-    category: 'gmp',
-    trainMode: 'online',
-    evalMode: 'exam',
-    paperId: 'p4',
-    paperName: '2026-鑽墿璀︽垝骞村害鍩硅璇曢',
-    hours: 3,
-    enabled: '1',
-    remark: '',
-  },
-  {
-    id: 'course_005',
-    courseNo: 'KC2026030005',
-    courseName: '瀹夊叏鐢熶骇鏈堝璐�',
-    category: 'safety',
-    trainMode: 'onsite',
-    evalMode: 'none',
-    hours: 1.5,
-    enabled: '1',
-    remark: '鏃犻渶鑰冩牳',
-  },
-  {
-    id: 'course_006',
-    courseNo: 'KC2026020001',
-    courseName: '鏃х増鏂囦欢鍩硅锛堝仠鐢級',
-    category: 'other',
-    trainMode: 'online',
-    evalMode: 'exam',
-    paperId: 'p6',
-    paperName: '鍐嶇‘璁ゆ祴璇�',
-    hours: 2,
-    enabled: '0',
-    remark: '宸插仠鐢紝鍘嗗彶寮曠敤淇濈暀',
-  },
-];
-
-function nextCourseNo() {
-  const now = new Date();
-  const ym = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`;
-  const prefix = `KC${ym}`;
-  const seqs = store
-    .filter((x) => x.courseNo.startsWith(prefix))
-    .map((x) => Number(x.courseNo.slice(prefix.length)) || 0);
-  const next = (seqs.length ? Math.max(...seqs) : 0) + 1;
-  return `${prefix}${String(next).padStart(4, '0')}`;
-}
-
-function resolvePaperName(paperId?: string) {
-  if (!paperId) return undefined;
-  return papers.find((p) => p.id === paperId)?.fullName;
-}
-
-export function mockPaperOptions() {
-  return delay([...papers]);
-}
-
-export function mockQueryCourses(params: CoursePageQuery = {}) {
-  const {
-    currentPage = 1,
-    pageSize = 20,
-    keyword = '',
-    category,
-    trainMode,
-    evalMode,
-    enabled,
-  } = params;
-  let list = [...store];
-  if (category) list = list.filter((x) => x.category === category);
-  if (trainMode) list = list.filter((x) => x.trainMode === trainMode);
-  if (evalMode) list = list.filter((x) => x.evalMode === evalMode);
-  if (enabled) list = list.filter((x) => x.enabled === enabled);
-  if (keyword) {
-    const k = keyword.trim().toLowerCase();
-    list = list.filter(
-      (x) =>
-        x.courseNo.toLowerCase().includes(k) ||
-        x.courseName.toLowerCase().includes(k) ||
-        (x.paperName || '').toLowerCase().includes(k),
-    );
-  }
-  list.sort((a, b) => b.courseNo.localeCompare(a.courseNo));
-  const start = (currentPage - 1) * pageSize;
-  return delay({
-    list: list.slice(start, start + pageSize),
-    pagination: { total: list.length, currentPage, pageSize },
-  });
-}
-
-export async function mockGetCourse(id: string) {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('鍩硅璇剧▼涓嶅瓨鍦�');
-  return { ...row };
-}
-
-export async function mockSaveCourse(data: Partial<CourseItem> & { id?: string }) {
-  await delay(null);
-  if (data.evalMode === 'exam' && !data.paperId) {
-    throw new Error('鍦ㄧ嚎鑰冭瘯椤诲叧鑱旇�冩牳璇曞嵎');
-  }
-  const paperName = resolvePaperName(data.paperId);
-  if (data.id) {
-    const row = store.find((x) => x.id === data.id);
-    if (!row) throw new Error('鍩硅璇剧▼涓嶅瓨鍦�');
-    Object.assign(row, {
-      ...data,
-      paperName: data.paperId ? paperName : undefined,
-    });
-    if (!data.paperId) {
-      row.paperId = undefined;
-      row.paperName = undefined;
-    }
-    return row;
-  }
-  if (!data.courseName?.trim()) throw new Error('璇峰~鍐欒绋嬪悕绉�');
-  const row: CourseItem = {
-    id: `course_${Date.now()}`,
-    courseNo: nextCourseNo(),
-    courseName: data.courseName.trim(),
-    category: data.category,
-    trainMode: data.trainMode,
-    evalMode: data.evalMode,
-    paperId: data.paperId,
-    paperName: data.paperId ? paperName : undefined,
-    hours: data.hours ?? null,
-    enabled: (data.enabled as any) || '1',
-    remark: data.remark || '',
-  };
-  store.unshift(row);
-  return row;
-}
-
-export async function mockSetCourseEnabled(id: string, enabled: '0' | '1') {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('鍩硅璇剧▼涓嶅瓨鍦�');
-  row.enabled = enabled;
-  return row;
-}
-
-export async function mockDeleteCourse(id: string) {
-  await delay(null);
-  const idx = store.findIndex((x) => x.id === id);
-  if (idx < 0) throw new Error('鍩硅璇剧▼涓嶅瓨鍦�');
-  store.splice(idx, 1);
-  return true;
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/Form.vue
index 4250257..b125528 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/Form.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/Form.vue
@@ -6,13 +6,18 @@
 import { BasicModal, useModalInner } from '@jnpf/ui/modal';
 
 import { createEvalMode, getEvalModeInfo, updateEvalMode } from '#/api/x/tms/evalMode';
-
-import { ENABLE_OPTIONS, YES_NO_OPTIONS } from './constants';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  loadTmsDic,
+} from '#/views/x/tms/shared/dic';
 
 defineOptions({ name: 'TmsEvalModeForm' });
 
 const emit = defineEmits(['register', 'reload']);
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 
 const state = reactive({ id: '' });
 const { id } = toRefs(state);
@@ -40,7 +45,11 @@
       label: '闇�瑕佽瘯鍗�',
       component: 'Select',
       defaultValue: '0',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -48,7 +57,11 @@
       label: '闇�瑕佹彁闂璁�',
       component: 'Select',
       defaultValue: '0',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -56,7 +69,11 @@
       label: '闇�瑕佸疄鎿嶈瘎鍒�',
       component: 'Select',
       defaultValue: '0',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -64,7 +81,11 @@
       label: '鍚敤鐘舵��',
       component: 'Select',
       defaultValue: '1',
-      componentProps: { placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -77,6 +98,23 @@
 });
 
 const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner(init);
+
+async function loadDicOptions() {
+  const [yesNo, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.yesNo),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  updateSchema(
+    ['needPaper', 'needQuiz', 'needPractice'].map((field) => ({
+      field,
+      componentProps: { placeholder: '璇烽�夋嫨', options: yesNo, fieldNames: TMS_DIC_FIELD_NAMES },
+    })),
+  );
+  updateSchema({
+    field: 'enabled',
+    componentProps: { placeholder: '璇烽�夋嫨', options: enable, fieldNames: TMS_DIC_FIELD_NAMES },
+  });
+}
 
 async function init(data: { id?: string }) {
   changeLoading(true);
@@ -92,6 +130,7 @@
     },
   });
   try {
+    await loadDicOptions();
     if (state.id) setFieldsValue(await getEvalModeInfo(state.id));
   } finally {
     changeLoading(false);
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/constants.ts
deleted file mode 100644
index 64da652..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/constants.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export const YES_NO_OPTIONS = [
-  { id: '1', fullName: '鏄�' },
-  { id: '0', fullName: '鍚�' },
-];
-
-export const ENABLE_OPTIONS = [
-  { id: '1', fullName: '鍚敤' },
-  { id: '0', fullName: '鍋滅敤' },
-];
-
-export function labelOfYesNo(v?: string) {
-  return YES_NO_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfEnabled(v?: string) {
-  return ENABLE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/index.vue
index be2a3fc..2d53595 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/index.vue
@@ -3,19 +3,32 @@
 
 import type { EvalModeItem } from './types';
 
+import { onMounted, ref } from 'vue';
+
 import { useMessage } from '@jnpf/hooks';
 import { useModal } from '@jnpf/ui/modal';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { deleteEvalMode, getEvalModeList, setEvalModeEnabled } from '#/api/x/tms/evalMode';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
 
 import Form from './Form.vue';
-import { ENABLE_OPTIONS, labelOfEnabled, labelOfYesNo } from './constants';
 
 defineOptions({ name: 'TmsEvalMode' });
 
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 const [registerForm, { openModal: openFormModal }] = useModal();
+
+const yesNoOpts = ref<TmsDicOpt[]>([]);
+const enableOpts = ref<TmsDicOpt[]>([]);
 
 const columns: BasicColumn[] = [
   { title: '鏂瑰紡缂栫爜', dataIndex: 'modeCode', width: 120 },
@@ -25,21 +38,21 @@
     dataIndex: 'needPaper',
     width: 100,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as EvalModeItem).needPaper),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as EvalModeItem).needPaper),
   },
   {
     title: '闇�瑕佹彁闂璁�',
     dataIndex: 'needQuiz',
     width: 120,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as EvalModeItem).needQuiz),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as EvalModeItem).needQuiz),
   },
   {
     title: '闇�瑕佸疄鎿嶈瘎鍒�',
     dataIndex: 'needPractice',
     width: 120,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as EvalModeItem).needPractice),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as EvalModeItem).needPractice),
   },
   {
     title: '鐘舵��',
@@ -51,10 +64,10 @@
   { title: '澶囨敞', dataIndex: 'remark', minWidth: 200 },
 ];
 
-const [registerTable, { reload }] = useVxeTable({
+const [registerTable, { reload, getForm }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
@@ -71,7 +84,12 @@
         field: 'enabled',
         label: '鐘舵��',
         component: 'Select',
-        componentProps: { allowClear: true, placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇烽�夋嫨',
+          options: enableOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
+        },
       },
     ],
   },
@@ -81,6 +99,21 @@
     dataIndex: 'action',
     fixed: 'right',
   },
+});
+
+onMounted(async () => {
+  yesNoOpts.value = await loadTmsDic(baseStore, TMS_DIC.yesNo);
+  enableOpts.value = await loadTmsDic(baseStore, TMS_DIC.enableStatus);
+  getForm()?.updateSchema?.({
+    field: 'enabled',
+    componentProps: {
+      allowClear: true,
+      placeholder: '璇烽�夋嫨',
+      options: enableOpts.value,
+      fieldNames: TMS_DIC_FIELD_NAMES,
+    },
+  });
+  reload();
 });
 
 async function fetchList(params: Record<string, any>) {
@@ -108,6 +141,10 @@
 }
 
 async function handleDelete(record: EvalModeItem) {
+  if (record.enabled !== '0') {
+    createMessage.warning('璇峰厛鍋滅敤鍚庡啀鍒犻櫎');
+    return;
+  }
   try {
     await deleteEvalMode(record.id);
     createMessage.success('鍒犻櫎鎴愬姛');
@@ -119,7 +156,7 @@
 
 function getTableActions(record: EvalModeItem): ActionItem[] {
   const enableLabel = record.enabled === '1' ? '鍋滅敤' : '鍚敤';
-  return [
+  const actions: ActionItem[] = [
     { label: '缂栬緫', onClick: handleEdit.bind(null, record) },
     {
       label: enableLabel,
@@ -128,15 +165,18 @@
         onOk: handleToggleEnabled.bind(null, record),
       },
     },
-    {
+  ];
+  if (record.enabled === '0') {
+    actions.push({
       label: '鍒犻櫎',
       color: 'error',
       modelConfirm: {
         content: `纭畾鍒犻櫎鑰冩牳鏂瑰紡銆�${record.modeName}銆嶅悧锛焋,
         onOk: handleDelete.bind(null, record),
       },
-    },
-  ];
+    });
+  }
+  return actions;
 }
 </script>
 
@@ -150,7 +190,7 @@
           </template>
           <template #enabled="{ record }">
             <a-tag :color="record.enabled === '1' ? 'success' : 'default'">
-              {{ labelOfEnabled(record.enabled) }}
+              {{ labelOfDic(enableOpts, record.enabled) }}
             </a-tag>
           </template>
           <template #action="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/mock.ts
deleted file mode 100644
index 115641c..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/evalMode/mock.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import type { EvalModeItem, EvalModePageQuery } from './types';
-
-function delay<T>(data: T, ms = 180): Promise<T> {
-  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
-}
-
-const store: EvalModeItem[] = [
-  {
-    id: 'tms_eval_quiz',
-    modeCode: 'quiz',
-    modeName: '鎻愰棶',
-    needPaper: '0',
-    needQuiz: '1',
-    needPractice: '0',
-    enabled: '1',
-    remark: '鐜板満鎻愰棶鑰冩牳',
-  },
-  {
-    id: 'tms_eval_practice',
-    modeCode: 'practice',
-    modeName: '鐜板満鎿嶄綔',
-    needPaper: '0',
-    needQuiz: '0',
-    needPractice: '1',
-    enabled: '1',
-    remark: '瀹炴搷璇勫垎',
-  },
-  {
-    id: 'tms_eval_exam',
-    modeCode: 'exam',
-    modeName: '鍦ㄧ嚎鑰冭瘯',
-    needPaper: '1',
-    needQuiz: '0',
-    needPractice: '0',
-    enabled: '1',
-    remark: '鍙戝竷浠诲姟鏃剁敓鎴愬湪绾胯�冭瘯寰呰�冭',
-  },
-  {
-    id: 'tms_eval_none',
-    modeCode: 'none',
-    modeName: '鏃犻渶鑰冩牳',
-    needPaper: '0',
-    needQuiz: '0',
-    needPractice: '0',
-    enabled: '1',
-    remark: '',
-  },
-];
-
-export function mockQueryEvalModes(params: EvalModePageQuery = {}) {
-  const { currentPage = 1, pageSize = 20, keyword = '', enabled } = params;
-  let list = [...store];
-  if (enabled) list = list.filter((x) => x.enabled === enabled);
-  if (keyword) {
-    const k = keyword.trim().toLowerCase();
-    list = list.filter(
-      (x) => x.modeCode.toLowerCase().includes(k) || x.modeName.toLowerCase().includes(k),
-    );
-  }
-  const start = (currentPage - 1) * pageSize;
-  return delay({
-    list: list.slice(start, start + pageSize),
-    pagination: { total: list.length, currentPage, pageSize },
-  });
-}
-
-export async function mockGetEvalMode(id: string) {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('鑰冩牳鏂瑰紡涓嶅瓨鍦�');
-  return { ...row };
-}
-
-export async function mockSaveEvalMode(data: Partial<EvalModeItem> & { id?: string }) {
-  await delay(null);
-  if (data.id) {
-    const row = store.find((x) => x.id === data.id);
-    if (!row) throw new Error('鑰冩牳鏂瑰紡涓嶅瓨鍦�');
-    if (data.modeCode && data.modeCode !== row.modeCode) {
-      if (store.some((x) => x.modeCode === data.modeCode && x.id !== data.id)) {
-        throw new Error('鏂瑰紡缂栫爜宸插瓨鍦�');
-      }
-    }
-    Object.assign(row, data);
-    return row;
-  }
-  if (!data.modeCode) throw new Error('璇峰~鍐欐柟寮忕紪鐮�');
-  if (store.some((x) => x.modeCode === data.modeCode)) throw new Error('鏂瑰紡缂栫爜宸插瓨鍦�');
-  const row: EvalModeItem = {
-    id: `em_${Date.now()}`,
-    modeCode: data.modeCode,
-    modeName: data.modeName || data.modeCode,
-    needPaper: (data.needPaper as any) || '0',
-    needQuiz: (data.needQuiz as any) || '0',
-    needPractice: (data.needPractice as any) || '0',
-    enabled: (data.enabled as any) || '1',
-    remark: data.remark || '',
-  };
-  store.push(row);
-  return row;
-}
-
-export async function mockSetEvalModeEnabled(id: string, enabled: '0' | '1') {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('鑰冩牳鏂瑰紡涓嶅瓨鍦�');
-  row.enabled = enabled;
-  return row;
-}
-
-export async function mockDeleteEvalMode(id: string) {
-  await delay(null);
-  const idx = store.findIndex((x) => x.id === id);
-  if (idx < 0) throw new Error('鑰冩牳鏂瑰紡涓嶅瓨鍦�');
-  store.splice(idx, 1);
-  return true;
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Mark.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Mark.vue
index 9c30b38..86e1738 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Mark.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Mark.vue
@@ -1,15 +1,19 @@
 <script lang="ts" setup>
 import type { GradeExamDetail, GradeExamItem } from './types';
 
-import { computed, onMounted, reactive, ref } from 'vue';
+import { computed, onMounted, reactive, ref, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
+import { Modal } from 'ant-design-vue';
 
 import { getGradeExamDetail, submitGrade } from '#/api/x/tms/examGrade';
-import { labelOfType } from '#/views/x/tms/question/constants';
+import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
-import { colorOfGradeStatus, labelOfGradeStatus } from './constants';
+import { colorOfGradeStatus, labelOfGradeStatus, loadExamGradeDics } from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamGradeMark' });
 
@@ -36,9 +40,21 @@
   return Number(detail.value.objectiveScore || 0) + subjectiveTotal.value;
 });
 
-onMounted(() => {
-  loadDetail();
+onMounted(async () => {
+  await Promise.all([loadExamGradeDics(), loadQuestionDics()]);
+  await loadDetail();
 });
+
+watch(
+  () => String(route.params.id || ''),
+  async (id, prev) => {
+    if (!id || id === prev) return;
+    Object.keys(scoreMap).forEach((key) => {
+      delete scoreMap[key];
+    });
+    await loadDetail();
+  },
+);
 
 async function loadDetail() {
   const id = String(route.params.id || '');
@@ -91,19 +107,30 @@
 }
 
 async function handleSubmit() {
-  if (!detail.value || readonly.value) return;
+  if (!detail.value || readonly.value || saving.value) return;
   const err = validateScores();
   if (err) {
     createMessage.warning(err);
     return;
   }
+  Modal.confirm({
+    title: '纭鎻愪氦',
+    content: '鎻愪氦鍚庝笉鍙慨鏀癸紝纭畾鎻愪氦鍚楋紵',
+    okText: '纭',
+    cancelText: '鍙栨秷',
+    onOk: () => doSubmit(),
+  });
+}
+
+async function doSubmit() {
+  if (!detail.value || readonly.value || saving.value) return;
   saving.value = true;
   try {
     const items = detail.value.items
       .filter((x) => x.isSubjective === '1')
       .map((x) => ({ id: x.id, gotScore: Number(scoreMap[x.id] || 0) }));
-    await submitGrade({ examId: detail.value.examId, items });
-    createMessage.success(`闃呭嵎瀹屾垚锛屾�诲垎 ${previewTotal.value}`);
+    const result = await submitGrade({ examId: detail.value.examId, items });
+    createMessage.success(`闃呭嵎瀹屾垚锛屾�诲垎 ${result?.totalScore ?? previewTotal.value}`);
     goBack();
   } catch (e: any) {
     createMessage.error(e?.message || '鎻愪氦澶辫触');
@@ -122,12 +149,10 @@
     <div class="jnpf-content-wrapper-center tms-grade-center">
       <div class="jnpf-content-wrapper-content tms-grade-mark-wrap">
         <div class="mark-top">
-          <div class="mark-header">
+          <div class="mark-header tms-page-header">
             <div>
-              <div class="text-base font-medium">
-                {{ detail?.paperName || '闃呭嵎' }} 路 闃呭嵎
-              </div>
-              <div v-if="detail" class="mt-1 text-gray-400 text-sm">
+              <div class="tms-page-header__title">{{ detail?.paperName || '闃呭嵎' }} 路 闃呭嵎</div>
+              <div v-if="detail" class="tms-page-header__sub">
                 鑰冪敓锛歿{ detail.userName }}
                 <span v-if="detail.deptName">锛坽{ detail.deptName }}锛�</span>
                 <span class="ml-3">浜ゅ嵎锛歿{ detail.submitTime || '-' }}</span>
@@ -136,12 +161,12 @@
                 </span>
               </div>
             </div>
-            <a-space>
-              <a-button @click="goBack">杩斿洖</a-button>
+            <div class="tms-page-header__actions">
+              <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
               <a-button v-if="detail && !readonly" type="primary" :loading="saving" @click="handleSubmit">
                 鎻愪氦闃呭嵎
               </a-button>
-            </a-space>
+            </div>
           </div>
 
           <div v-if="detail" class="score-bar">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Session.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Session.vue
index 09c3022..8875fa3 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Session.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/Session.vue
@@ -3,15 +3,18 @@
 
 import type { GradeExamListItem, GradeSessionListItem } from './types';
 
-import { onMounted, ref } from 'vue';
+import { onActivated, onMounted, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { getGradeExamList, getGradeSessionInfo } from '#/api/x/tms/examGrade';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
-import { colorOfGradeStatus, labelOfGradeStatus } from './constants';
+import { colorOfGradeStatus, labelOfGradeStatus, loadExamGradeDics } from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamGradeSession' });
 
@@ -77,16 +80,27 @@
     return;
   }
   try {
-    session.value = await getGradeSessionInfo(sessionId);
-    reload();
+    await loadExamGradeDics();
+    await refreshSession();
   } catch (e: any) {
     createMessage.error(e?.message || '鍔犺浇澶辫触');
     router.replace('/tms/examGrade');
   }
 });
 
+onActivated(() => {
+  if (!sessionId) return;
+  void refreshSession();
+});
+
+async function refreshSession() {
+  session.value = await getGradeSessionInfo(sessionId);
+  reload();
+}
+
 async function fetchList() {
-  return { data: await getGradeExamList(sessionId) };
+  const list = await getGradeExamList(sessionId);
+  return { data: Array.isArray(list) ? list : [] };
 }
 
 function goBack() {
@@ -99,9 +113,9 @@
 
 function getTableActions(record: GradeExamListItem): ActionItem[] {
   if (record.gradeStatus === 'pending') {
-    return [{ label: '闃呭嵎', onClick: handleMark.bind(null, record) }];
+    return [{ label: TMS_BTN.grade, onClick: handleMark.bind(null, record) }];
   }
-  return [{ label: '鏌ョ湅', onClick: handleMark.bind(null, record) }];
+  return [{ label: TMS_BTN.detail, onClick: handleMark.bind(null, record) }];
 }
 </script>
 
@@ -111,15 +125,13 @@
       <div class="jnpf-content-wrapper-content">
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
-            <div class="flex items-center gap-3">
-              <a-button @click="goBack">杩斿洖</a-button>
+            <div class="tms-page-header" style="margin-bottom: 0">
               <div>
-                <div class="text-base font-medium">
-                  {{ session?.paperName || '绛斿嵎鍒楄〃' }}
-                </div>
-                <div class="mt-1 text-gray-400 text-sm">
-                  {{ session?.taskNo }} 路 {{ session?.taskSubject }}
-                </div>
+                <div class="tms-page-header__title">{{ session?.paperName || '绛斿嵎鍒楄〃' }}</div>
+                <div class="tms-page-header__sub">{{ session?.taskNo }} 路 {{ session?.taskSubject }}</div>
+              </div>
+              <div class="tms-page-header__actions">
+                <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
               </div>
             </div>
           </template>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/constants.ts
index 715308e..7fb8f54 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/constants.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/constants.ts
@@ -1,15 +1,25 @@
-import type { GradeStatus } from './types';
+import { ref } from 'vue';
 
-export const GRADE_STATUS_OPTIONS: { id: GradeStatus; fullName: string; color?: string }[] = [
-  { id: 'auto', fullName: '鏃犻渶闃呭嵎', color: '#8c8c8c' },
-  { id: 'pending', fullName: '寰呮壒鏀�', color: '#fa8c16' },
-  { id: 'graded', fullName: '宸叉壒鏀�', color: '#52c41a' },
-];
+import { useBaseStore } from '#/store';
+import { TMS_DIC, labelOfDic, loadTmsDic, type TmsDicOpt } from '#/views/x/tms/shared/dic';
+
+const GRADE_STATUS_COLOR: Record<string, string> = {
+  auto: '#8c8c8c',
+  pending: '#fa8c16',
+  graded: '#52c41a',
+};
+
+export const GRADE_STATUS_OPTIONS = ref<TmsDicOpt[]>([]);
+
+export async function loadExamGradeDics() {
+  const baseStore = useBaseStore();
+  GRADE_STATUS_OPTIONS.value = await loadTmsDic(baseStore, TMS_DIC.gradeStatus);
+}
 
 export function labelOfGradeStatus(v?: string) {
-  return GRADE_STATUS_OPTIONS.find((x) => x.id === v)?.fullName ?? v ?? '-';
+  return labelOfDic(GRADE_STATUS_OPTIONS.value, v);
 }
 
 export function colorOfGradeStatus(v?: string) {
-  return GRADE_STATUS_OPTIONS.find((x) => x.id === v)?.color;
+  return (v && GRADE_STATUS_COLOR[v]) || undefined;
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/index.vue
index f5b4455..c2f95ab 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examGrade/index.vue
@@ -8,13 +8,16 @@
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { getGradeSessionList } from '#/api/x/tms/examGrade';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamGrade' });
 
 const router = useRouter();
 
 const columns: BasicColumn[] = [
-  { title: '鍩硅缂栧彿', dataIndex: 'taskNo', width: 140 },
+  { title: '璇曞嵎缂栧彿', dataIndex: 'taskNo', width: 140 },
   { title: '鍩硅涓婚', dataIndex: 'taskSubject', minWidth: 260 },
   { title: '璇曞嵎鍚嶇О', dataIndex: 'paperName', minWidth: 200 },
   {
@@ -51,9 +54,9 @@
     schemas: [
       {
         field: 'taskNo',
-        label: '鍩硅缂栧彿',
+        label: '璇曞嵎缂栧彿',
         component: 'Input',
-        componentProps: { placeholder: '璇疯緭鍏ュ煿璁紪鍙�', submitOnPressEnter: true },
+        componentProps: { placeholder: '璇疯緭鍏ヨ瘯鍗风紪鍙�', submitOnPressEnter: true },
       },
       {
         field: 'keyword',
@@ -72,7 +75,13 @@
 });
 
 async function fetchList(params: Record<string, any>) {
-  return { data: await getGradeSessionList(params) };
+  const page = await getGradeSessionList(params);
+  return {
+    data: {
+      list: Array.isArray(page?.list) ? page.list : [],
+      pagination: page?.pagination || { total: 0 },
+    },
+  };
 }
 
 function handleEnter(record: GradeSessionListItem) {
@@ -80,7 +89,7 @@
 }
 
 function getTableActions(record: GradeSessionListItem): ActionItem[] {
-  return [{ label: '杩涘叆', onClick: handleEnter.bind(null, record) }];
+  return [{ label: TMS_BTN.grade, onClick: handleEnter.bind(null, record) }];
 }
 </script>
 
@@ -91,8 +100,8 @@
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
             <div>
-              <div class="text-base font-medium">鑰冭瘯闃呭嵎</div>
-              <div class="mt-1 text-gray-400 text-sm">闃呭嵎鍒楄〃锛岄�夋嫨璇曞嵎杩涜闃呭嵎锛屾垨鏌ョ湅鑰冭瘯璇︽儏銆�</div>
+              <div class="tms-page-header__title">鑰冭瘯闃呭嵎</div>
+              <div class="tms-page-header__sub">閫夋嫨鍦烘杩涘叆绛斿嵎鍒楄〃锛屽寰呴槄璇曞嵎璇勫垎銆�</div>
             </div>
           </template>
           <template #examTime="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Detail.vue
index a3605d4..02e76de 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Detail.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Detail.vue
@@ -10,6 +10,9 @@
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { getExamScoreDetail } from '#/api/x/tms/examScore';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamScoreDetail' });
 
@@ -22,10 +25,10 @@
 const loaded = ref(false);
 
 const columns: BasicColumn[] = [
-  { title: '鍩硅涓婚', dataIndex: 'taskSubject', minWidth: 220 },
-  { title: '鍩硅浠诲姟缂栧彿', dataIndex: 'taskNo', width: 140 },
+  { title: '璇曞嵎鍚嶇О', dataIndex: 'taskSubject', minWidth: 220 },
+  { title: '璇曞嵎缂栧彿', dataIndex: 'taskNo', width: 180 },
   {
-    title: '鐢ㄦ埛ID',
+    title: '鑰冪敓',
     dataIndex: 'userName',
     width: 120,
     customRender: ({ record }) => {
@@ -43,7 +46,6 @@
     align: 'center',
     slots: { default: 'passFlag' },
   },
-  { title: '鏉ユ簮IP鍦板潃', dataIndex: 'sourceIp', width: 140 },
 ];
 
 const [registerTable, { getSelectRows, reload }] = useVxeTable({
@@ -146,7 +148,7 @@
 }
 
 function getTableActions(record: ExamScoreDetailRow): ActionItem[] {
-  return [{ label: '鏌ョ湅璇曞嵎', onClick: viewPaper.bind(null, record) }];
+  return [{ label: TMS_BTN.viewPaper, onClick: viewPaper.bind(null, record) }];
 }
 </script>
 
@@ -157,15 +159,19 @@
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
             <a-space>
-              <a-button @click="goBack">杩斿洖</a-button>
-              <a-button type="primary" @click="handleViewSelected">鏌ョ湅璇曞嵎</a-button>
-              <span class="text-gray-400 text-sm">{{ titleText }}</span>
+              <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+              <a-button type="primary" @click="handleViewSelected">{{ TMS_BTN.viewPaper }}</a-button>
+              <span class="tms-toolbar-hint">{{ titleText }}</span>
             </a-space>
           </template>
           <template #passFlag="{ record }">
-            <span :class="record.passFlag === '1' ? 'text-green-600' : 'text-red-500'">
+            <span
+              v-if="record.passFlag === '1' || record.passFlag === '0'"
+              :class="record.passFlag === '1' ? 'text-green-600' : 'text-red-500'"
+            >
               {{ record.passFlag === '1' ? '鏄�' : '鍚�' }}
             </span>
+            <span v-else>-</span>
           </template>
           <template #action="{ record }">
             <TableAction :actions="getTableActions(record)" />
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Paper.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Paper.vue
index 655ec4f..8b666c4 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Paper.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/Paper.vue
@@ -11,7 +11,10 @@
 } from 'ant-design-vue';
 
 import { getExamPaperView } from '#/api/x/tms/examScore';
-import { labelOfType } from '#/views/x/tms/question/constants';
+import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamScorePaper' });
 
@@ -22,7 +25,8 @@
 const loading = ref(false);
 const paper = ref<ExamPaperView | null>(null);
 
-onMounted(() => {
+onMounted(async () => {
+  await loadQuestionDics();
   loadPaper();
 });
 
@@ -62,30 +66,35 @@
     <div class="jnpf-content-wrapper-center tms-paper-center">
       <div class="jnpf-content-wrapper-content tms-paper-wrap">
         <div class="paper-top">
-          <div class="paper-header">
+          <div class="paper-header tms-page-header">
             <div>
-              <div class="text-base font-medium">鑰冪敓璇曞嵎璇︽儏</div>
-              <div v-if="paper" class="mt-1 text-gray-400 text-sm">
+              <div class="tms-page-header__title">鑰冪敓璇曞嵎璇︽儏</div>
+              <div v-if="paper" class="tms-page-header__sub">
                 {{ paper.userName }}锛坽{ paper.userId }}锛� 路 {{ paper.paperName }}
               </div>
             </div>
-            <a-button @click="goBack">杩斿洖</a-button>
+            <div class="tms-page-header__actions">
+              <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+            </div>
           </div>
 
           <ADescriptions v-if="paper" bordered :column="3" size="small" class="mb-3">
-            <ADescriptionsItem label="鍩硅涓婚" :span="2">{{ paper.taskSubject }}</ADescriptionsItem>
-            <ADescriptionsItem label="鍩硅缂栧彿">{{ paper.taskNo }}</ADescriptionsItem>
+            <ADescriptionsItem label="璇曞嵎鍚嶇О" :span="2">{{ paper.taskSubject }}</ADescriptionsItem>
+            <ADescriptionsItem label="璇曞嵎缂栧彿">{{ paper.taskNo }}</ADescriptionsItem>
             <ADescriptionsItem label="鑰冭瘯寮�濮�">{{ paper.examStart || '-' }}</ADescriptionsItem>
             <ADescriptionsItem label="鑰冭瘯缁撴潫">{{ paper.examEnd || '-' }}</ADescriptionsItem>
-            <ADescriptionsItem label="鏉ユ簮IP">{{ paper.sourceIp || '-' }}</ADescriptionsItem>
             <ADescriptionsItem label="鎴愮哗">
               <b class="text-primary">{{ paper.score }}</b> / {{ paper.totalScore }}
             </ADescriptionsItem>
             <ADescriptionsItem label="鍙婃牸鍒�">{{ paper.passScore }}</ADescriptionsItem>
             <ADescriptionsItem label="鏄惁鍚堟牸">
-              <span :class="paper.passFlag === '1' ? 'text-green-600' : 'text-red-500'">
+              <span
+                v-if="paper.passFlag === '1' || paper.passFlag === '0'"
+                :class="paper.passFlag === '1' ? 'text-green-600' : 'text-red-500'"
+              >
                 {{ paper.passFlag === '1' ? '鏄�' : '鍚�' }}
               </span>
+              <span v-else>-</span>
             </ADescriptionsItem>
           </ADescriptions>
         </div>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/index.vue
index 341ed2c..bca3273 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/examScore/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/examScore/index.vue
@@ -1,23 +1,28 @@
 <script lang="ts" setup>
 import type { ActionItem, BasicColumn } from '@jnpf/ui/vxeTable';
 
-import type { ExamScoreSummaryItem } from './types';
+import type { ExamScoreDetailRow, ExamScoreSummaryItem } from './types';
 
+import { ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
-import { getExamScoreList } from '#/api/x/tms/examScore';
+import { exportExamScores, getExamScoreList } from '#/api/x/tms/examScore';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsExamScore' });
 
 const router = useRouter();
 const { createMessage } = useMessage();
+const exporting = ref(false);
 
 const columns: BasicColumn[] = [
-  { title: '鍩硅涓婚', dataIndex: 'taskSubject', minWidth: 240 },
-  { title: '鍩硅缂栧彿', dataIndex: 'taskNo', width: 140 },
+  { title: '璇曞嵎鍚嶇О', dataIndex: 'taskSubject', minWidth: 240 },
+  { title: '璇曞嵎缂栧彿', dataIndex: 'taskNo', width: 180 },
   {
     title: '璇曞嵎鍚嶇О',
     dataIndex: 'paperName',
@@ -63,9 +68,9 @@
       },
       {
         field: 'taskNo',
-        label: '鍩硅缂栧彿',
+        label: '璇曞嵎缂栧彿',
         component: 'Input',
-        componentProps: { placeholder: '璇疯緭鍏ュ煿璁紪鍙�', submitOnPressEnter: true },
+        componentProps: { placeholder: '璇疯緭鍏ヨ瘯鍗风紪鍙�', submitOnPressEnter: true },
       },
     ],
   },
@@ -78,24 +83,90 @@
 });
 
 async function fetchList(params: Record<string, any>) {
-  return { data: await getExamScoreList(params) };
+  const page = await getExamScoreList(params);
+  return {
+    data: {
+      list: Array.isArray(page?.list) ? page.list : [],
+      pagination: page?.pagination || { total: 0 },
+    },
+  };
 }
 
 function handleDetail(record: ExamScoreSummaryItem) {
   router.push(`/tms/examScore/detail/${record.id}`);
 }
 
-function handleExport() {
-  const rows = getSelectRows?.() || [];
-  if (!rows.length) {
+async function handleExport() {
+  const selected = (getSelectRows?.() || []) as ExamScoreSummaryItem[];
+  if (!selected.length) {
     createMessage.warning('璇峰厛鍕鹃�夎瀵煎嚭鐨勮�冭瘯璁板綍');
     return;
   }
-  createMessage.success(`宸查�夋嫨 ${rows.length} 鏉★紙瀵煎嚭鎺ュ彛鑱旇皟鍚庣敓鏁堬級`);
+  if (exporting.value) return;
+  exporting.value = true;
+  try {
+    const rows = await exportExamScores(selected.map((item) => item.id));
+    if (!rows?.length) {
+      createMessage.warning('鎵�閫夎瘯鍗锋殏鏃犲凡浜ゅ嵎鎴愮哗');
+      return;
+    }
+    downloadScoreFile(rows, selected);
+    createMessage.success(`宸插鍑� ${rows.length} 鏉¤�冪敓鎴愮哗`);
+  } catch (e: any) {
+    createMessage.error(e?.message || '瀵煎嚭澶辫触');
+  } finally {
+    exporting.value = false;
+  }
+}
+
+function passText(flag?: string) {
+  if (flag === '1') return '鍚堟牸';
+  if (flag === '0') return '涓嶅悎鏍�';
+  return '-';
+}
+
+function xmlCell(value: unknown) {
+  const text = value == null ? '' : String(value);
+  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+}
+
+function scoreFileName(selected: ExamScoreSummaryItem[]) {
+  const names = [...new Set(selected.map((item) => (item.taskSubject || '').trim()).filter(Boolean))];
+  const raw = names.length === 1 ? `鑰冭瘯鎴愮哗_${names[0]}` : '鑰冭瘯鎴愮哗';
+  return `${raw.replace(/[\\/:*?"<>|\r\n]/g, '_')}.xls`;
+}
+
+function downloadScoreFile(rows: ExamScoreDetailRow[], selected: ExamScoreSummaryItem[]) {
+  const header = ['璇曞嵎鍚嶇О', '璇曞嵎缂栧彿', '鑰冪敓', '鑰冭瘯寮�濮嬫椂闂�', '鑰冭瘯缁撴潫鏃堕棿', '鎴愮哗', '鍙婃牸鍒�', '鏄惁鍚堟牸'];
+  const body = rows.map((row) => [
+    row.taskSubject,
+    row.taskNo,
+    row.userName || row.userId,
+    row.examStart || '',
+    row.examEnd || '',
+    row.score,
+    row.passScore,
+    passText(row.passFlag),
+  ]);
+  const widths = [180, 160, 100, 150, 150, 60, 60, 80];
+  const cols = widths.map((width) => `<Column ss:Width="${width}"/>`).join('');
+  const toRow = (cells: unknown[]) =>
+    `<Row>${cells.map((cell) => `<Cell><Data ss:Type="String">${xmlCell(cell)}</Data></Cell>`).join('')}</Row>`;
+  const xml = `<?xml version="1.0" encoding="UTF-8"?>
+<?mso-application progid="Excel.Sheet"?>
+<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
+<Worksheet ss:Name="鑰冭瘯鎴愮哗"><Table>${cols}${toRow(header)}${body.map(toRow).join('')}</Table></Worksheet>
+</Workbook>`;
+  const blob = new Blob([xml], { type: 'application/vnd.ms-excel;charset=utf-8' });
+  const link = document.createElement('a');
+  link.href = URL.createObjectURL(blob);
+  link.download = scoreFileName(selected);
+  link.click();
+  URL.revokeObjectURL(link.href);
 }
 
 function getTableActions(record: ExamScoreSummaryItem): ActionItem[] {
-  return [{ label: '鑰冭瘯璇︽儏', onClick: handleDetail.bind(null, record) }];
+  return [{ label: TMS_BTN.detail, onClick: handleDetail.bind(null, record) }];
 }
 </script>
 
@@ -106,7 +177,8 @@
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
             <a-space>
-              <a-button type="primary" @click="handleExport">瀵煎嚭鑰冭瘯</a-button>
+              <a-button type="primary" :loading="exporting" @click="handleExport">{{ TMS_BTN.export }}</a-button>
+              <span class="tms-toolbar-hint">鎸夊満娆℃眹鎬绘垚缁╋紝鍙鍑烘垨杩涘叆鏄庣粏</span>
             </a-space>
           </template>
           <template #paperName="{ record }">
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>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue
new file mode 100644
index 0000000..4da1eb2
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/Wrong.vue
@@ -0,0 +1,259 @@
+<script lang="ts" setup>
+import type { OnlineExamQuestionItem } from './types';
+
+import { computed, onMounted, reactive, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { Spin } from 'ant-design-vue';
+
+import { getExamReview, getMyPaperDetail } from '#/api/x/tms/onlineExam';
+import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
+
+defineOptions({ name: 'TmsOnlineExamReview' });
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+
+const loading = ref(false);
+const paperName = ref('');
+const attemptLabel = ref('');
+const questions = ref<OnlineExamQuestionItem[]>([]);
+const currentIndex = ref(0);
+const answers = reactive<Record<string, string | string[]>>({});
+
+const current = computed(() => questions.value[currentIndex.value]);
+const total = computed(() => questions.value.length);
+
+onMounted(async () => {
+  await loadQuestionDics();
+  await loadReview();
+});
+
+async function loadReview() {
+  const id = String(route.params.id || '');
+  const examId = String(route.query.examId || '');
+  if (!id && !examId) {
+    router.replace('/tms/onlineExam');
+    return;
+  }
+  loading.value = true;
+  try {
+    const detail = examId ? await getExamReview(examId) : await getMyPaperDetail(id);
+    if (!detail || detail.status !== 'submitted') {
+      createMessage.warning('浜ゅ嵎鍚庢墠鑳借繘琛岃�冭瘯鍥為【');
+      router.replace(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
+      return;
+    }
+    paperName.value = detail.paperName;
+    attemptLabel.value = detail.attemptLabel || '';
+    questions.value = detail.questions || [];
+    restoreAnswers(questions.value);
+    currentIndex.value = 0;
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇鑰冭瘯鍥為【澶辫触');
+    router.replace(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
+  } finally {
+    loading.value = false;
+  }
+}
+
+function isObjective(q: OnlineExamQuestionItem) {
+  return !(q.subjective || q.questionType === 'essay');
+}
+
+function restoreAnswers(list: OnlineExamQuestionItem[]) {
+  Object.keys(answers).forEach((key) => {
+    delete answers[key];
+  });
+  for (const q of list) {
+    if (q.questionType === 'multi') {
+      answers[q.id] = q.userAnswer ? q.userAnswer.split(/[,锛宂/).map((x) => x.trim()).filter(Boolean) : [];
+    } else {
+      answers[q.id] = q.userAnswer || '';
+    }
+  }
+}
+
+function stripHtml(html?: string) {
+  if (!html) return '';
+  return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+}
+
+function reviewText(q?: OnlineExamQuestionItem) {
+  if (!q) return '';
+  if (!isObjective(q)) {
+    if (q.gotScore != null) return `涓昏棰樺緱鍒嗭細${q.gotScore} / ${q.score}`;
+    return '涓昏棰橈紝寰呴槄鍗�';
+  }
+  if (q.right) return '鍥炵瓟姝g‘';
+  return `鍥炵瓟閿欒 路 姝g‘绛旀锛�${correctText(q)}`;
+}
+
+function reviewClass(q?: OnlineExamQuestionItem) {
+  if (!q || !isObjective(q)) return 'text-gray-500';
+  return q.right ? 'text-green-600' : 'text-red-500';
+}
+function correctText(q?: OnlineExamQuestionItem) {
+  if (!q) return '-';
+  const labels = (q.correctAnswer || '')
+    .split(/[,锛宂/)
+    .map((x) => x.trim())
+    .filter(Boolean);
+  if (labels.length) return labels.join('銆�');
+  const fromOptions = (q.options || []).filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
+  return fromOptions.join('銆�') || '-';
+}
+
+function goBack() {
+  const id = String(route.params.id || '');
+  router.push(id ? `/tms/onlineExam/detail/${id}` : '/tms/onlineExam');
+}
+
+function goPrev() {
+  if (currentIndex.value > 0) currentIndex.value -= 1;
+}
+
+function goNext() {
+  if (currentIndex.value < total.value - 1) currentIndex.value += 1;
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper tms-wrong-root">
+    <div class="jnpf-content-wrapper-center">
+      <div class="jnpf-content-wrapper-content tms-wrong-page">
+        <Spin :spinning="loading" class="tms-wrong-spin">
+          <template v-if="!loading && !total">
+            <div class="tms-page-header">
+              <div class="tms-page-header__title">{{ paperName || '鑰冭瘯鍥為【' }}</div>
+              <div class="tms-page-header__actions">
+                <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+              </div>
+            </div>
+            <a-empty description="鏆傛棤绛旈鍐呭" />
+          </template>
+
+          <template v-else-if="current">
+            <div class="tms-page-header">
+              <div>
+                <div class="tms-page-header__title">
+                  {{ paperName }}
+                  <span v-if="attemptLabel" class="ml-2 text-sm font-normal text-gray-500">锛坽{ attemptLabel }}锛�</span>
+                </div>
+                <div class="tms-page-header__sub">鑰冭瘯鍥為【 路 绗� {{ currentIndex + 1 }} / {{ total }} 棰�</div>
+              </div>
+              <div class="tms-page-header__actions">
+                <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+              </div>
+            </div>
+
+            <div class="tms-exam-body">
+              <div class="mb-3 text-sm text-gray-500">
+                {{ labelOfType(current.questionType) }}
+                <span class="ml-2">锛坽{ current.score }} 鍒嗭級</span>
+              </div>
+              <div class="stem mb-4">{{ stripHtml(current.stem) }}</div>
+
+              <a-radio-group
+                v-if="current.questionType === 'single' || current.questionType === 'judge'"
+                :value="answers[current.id]"
+                class="!flex !flex-col gap-3"
+                disabled
+              >
+                <a-radio v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
+                  {{ opt.optionLabel }}. {{ opt.optionContent }}
+                </a-radio>
+              </a-radio-group>
+
+              <a-checkbox-group
+                v-else-if="current.questionType === 'multi'"
+                :value="answers[current.id]"
+                class="!flex !flex-col gap-3"
+                disabled
+              >
+                <a-checkbox v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
+                  {{ opt.optionLabel }}. {{ opt.optionContent }}
+                </a-checkbox>
+              </a-checkbox-group>
+
+              <a-input
+                v-else-if="current.questionType === 'blank'"
+                :value="answers[current.id]"
+                disabled
+                placeholder="鏈綔绛�"
+              />
+
+              <a-textarea
+                v-else-if="current.questionType === 'essay'"
+                :value="answers[current.id]"
+                disabled
+                :rows="6"
+                placeholder="鏈綔绛�"
+              />
+
+              <div class="mt-4 text-sm" :class="reviewClass(current)">{{ reviewText(current) }}</div>
+            </div>
+
+            <div class="tms-exam-footer">
+              <a-button :disabled="currentIndex <= 0" @click="goPrev">涓婁竴棰�</a-button>
+              <a-button :disabled="currentIndex >= total - 1" @click="goNext">涓嬩竴棰�</a-button>
+            </div>
+          </template>
+        </Spin>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tms-wrong-root {
+  height: 100%;
+  min-height: 0;
+}
+
+.tms-wrong-page {
+  height: 100%;
+  min-height: 0;
+  overflow: hidden;
+  background: #fff;
+  padding: 20px 24px;
+  box-sizing: border-box;
+}
+
+.tms-wrong-spin {
+  height: 100%;
+}
+
+.tms-wrong-spin :deep(.ant-spin-container) {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.tms-exam-body {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto !important;
+}
+
+.stem {
+  font-size: 15px;
+  line-height: 1.7;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.tms-exam-footer {
+  flex-shrink: 0;
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+  padding-top: 16px;
+  border-top: 1px solid #f0f0f0;
+}
+</style>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/constants.ts
index f2dab78..0a09640 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/constants.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/constants.ts
@@ -1,19 +1,58 @@
-import type { MyPaperStatus } from './types';
+import { ref } from 'vue';
 
-export const MY_PAPER_STATUS_OPTIONS: {
-  id: MyPaperStatus;
-  fullName: string;
-  color?: string;
-}[] = [
-  { id: 'notStarted', fullName: '鏈紑濮�', color: '#8c8c8c' },
-  { id: 'doing', fullName: '鑰冭瘯涓�', color: '#1890ff' },
-  { id: 'submitted', fullName: '宸蹭氦鍗�', color: '#52c41a' },
-];
+import { useBaseStore } from '#/store';
+import { TMS_DIC, labelOfDic, loadTmsDic, type TmsDicOpt } from '#/views/x/tms/shared/dic';
+
+/** 鎴戠殑璇曞嵎銆屾湭寮�濮嬨�嶄负鍓嶇鎬侊紝瀛楀吀 tmsExamStatus 涓嶅惈璇ラ」 */
+const NOT_STARTED: TmsDicOpt = { id: 'notStarted', enCode: 'notStarted', fullName: '鏈紑濮�' };
+
+const STATUS_COLOR: Record<string, string> = {
+  notStarted: '#8c8c8c',
+  doing: '#1890ff',
+  submitted: '#52c41a',
+  cancelled: '#ff4d4f',
+};
+
+export const MY_PAPER_STATUS_OPTIONS = ref<TmsDicOpt[]>([NOT_STARTED]);
+export const PASS_FLAG_OPTIONS = ref<TmsDicOpt[]>([]);
+
+export async function loadOnlineExamDics() {
+  const baseStore = useBaseStore();
+  const [examStatus, passFlag] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.examStatus),
+    loadTmsDic(baseStore, TMS_DIC.passFlag),
+  ]);
+  const rest = examStatus.filter((x) => (x.enCode || x.id) !== 'notStarted');
+  MY_PAPER_STATUS_OPTIONS.value = [NOT_STARTED, ...rest];
+  PASS_FLAG_OPTIONS.value = passFlag;
+}
 
 export function labelOfMyPaperStatus(v?: string) {
-  return MY_PAPER_STATUS_OPTIONS.find((x) => x.id === v)?.fullName ?? v ?? '-';
+  return labelOfDic(MY_PAPER_STATUS_OPTIONS.value, v);
 }
 
 export function colorOfMyPaperStatus(v?: string) {
-  return MY_PAPER_STATUS_OPTIONS.find((x) => x.id === v)?.color;
+  return (v && STATUS_COLOR[v]) || undefined;
+}
+
+export function labelOfPassFlag(v?: string | null) {
+  if (v == null || v === '') return '-';
+  const code = String(v);
+  if (code === '1') return '鍚堟牸';
+  if (code === '0') return '涓嶅悎鏍�';
+  return labelOfDic(PASS_FLAG_OPTIONS.value, code);
+}
+
+export function colorOfPassFlag(v?: string | null) {
+  if (v === '1') return '#52c41a';
+  if (v === '0') return '#ff4d4f';
+  return undefined;
+}
+
+export function labelOfGradeStatus(v?: string | null) {
+  if (!v) return '-';
+  if (v === 'auto') return '鑷姩闃呭嵎';
+  if (v === 'pending') return '寰呴槄鍗�';
+  if (v === 'graded') return '宸查槄鍗�';
+  return v;
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/exam.vue b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/exam.vue
index 68f3126..65bd27c 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/exam.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/exam.vue
@@ -1,14 +1,16 @@
 <script lang="ts" setup>
-import type { OnlineExamPaper, OnlineExamQuestionItem } from './types';
+import type { OnlineExamPaper, OnlineExamQuestionItem, OnlineExamSubmitResult } from './types';
 
-import { computed, onMounted, reactive, ref } from 'vue';
+import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
 import { Modal } from 'ant-design-vue';
+import { AppstoreOutlined } from '@ant-design/icons-vue';
 
-import { submitOnlineExam } from '#/api/x/tms/onlineExam';
-import { labelOfType } from '#/views/x/tms/question/constants';
+import { saveOnlineExam, submitOnlineExam } from '#/api/x/tms/onlineExam';
+import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
 defineOptions({ name: 'TmsOnlineExamTake' });
 
@@ -21,11 +23,47 @@
 const submitted = ref(false);
 const scoreText = ref('');
 const submitting = ref(false);
+const saving = ref(false);
+const remainSeconds = ref<number | null>(null);
+const autoSubmitting = ref(false);
+const resultModalOpen = ref(false);
+const submitResult = ref<OnlineExamSubmitResult | null>(null);
+const resultFilter = ref<'all' | 'wrong'>('all');
+const sheetOpen = ref(false);
+
+let timer: ReturnType<typeof setInterval> | null = null;
 
 const current = computed(() => paper.value?.questions?.[currentIndex.value]);
 const total = computed(() => paper.value?.questions?.length || 0);
+const countdownText = computed(() => formatRemain(remainSeconds.value));
+const countdownUrgent = computed(
+  () => remainSeconds.value != null && remainSeconds.value > 0 && remainSeconds.value <= 5 * 60,
+);
 
-onMounted(() => {
+const rightCount = computed(
+  () => paper.value?.questions?.filter((q) => isObjective(q) && q.right === true).length || 0,
+);
+const wrongCount = computed(
+  () => paper.value?.questions?.filter((q) => isObjective(q) && q.right === false).length || 0,
+);
+const objectiveCount = computed(() => rightCount.value + wrongCount.value);
+const scoreRate = computed(() => {
+  if (!objectiveCount.value) return 0;
+  return Math.round((rightCount.value * 10000) / objectiveCount.value) / 100;
+});
+const resultQuestions = computed(() => {
+  const list = paper.value?.questions || [];
+  if (resultFilter.value === 'wrong') {
+    return list.filter((q) => isObjective(q) && q.right === false);
+  }
+  return list;
+});
+const answeredCount = computed(
+  () => (paper.value?.questions || []).filter((q) => isAnswered(q)).length,
+);
+
+onMounted(async () => {
+  await loadQuestionDics();
   const raw = sessionStorage.getItem('tms_online_exam_paper');
   if (!raw) {
     createMessage.warning('璇峰厛浠庤瘯鍗峰垪琛ㄩ�夋嫨鑰冭瘯');
@@ -33,46 +71,161 @@
     return;
   }
   try {
-    paper.value = JSON.parse(raw);
+    const parsed = JSON.parse(raw) as OnlineExamPaper;
+    paper.value = parsed;
+    restoreAnswers(parsed);
+    initCountdown(parsed);
   } catch {
     router.replace('/tms/onlineExam');
   }
 });
+
+onUnmounted(() => {
+  stopCountdown();
+  resultModalOpen.value = false;
+});
+
+function isObjective(q: OnlineExamQuestionItem) {
+  return !(q.subjective || q.questionType === 'essay');
+}
+
+function isAnswered(q: OnlineExamQuestionItem) {
+  const val = answers[q.id];
+  if (Array.isArray(val)) return val.length > 0;
+  return String(val ?? '').trim().length > 0;
+}
+
+function restoreAnswers(data: OnlineExamPaper) {
+  Object.keys(answers).forEach((key) => {
+    delete answers[key];
+  });
+  for (const q of data.questions || []) {
+    if (q.questionType === 'multi') {
+      answers[q.id] = q.userAnswer ? q.userAnswer.split(',').filter(Boolean) : [];
+    } else {
+      answers[q.id] = q.userAnswer || '';
+    }
+  }
+}
+
+function parseStartMs(startTime?: string) {
+  if (!startTime) return NaN;
+  const normalized = startTime.includes('T') ? startTime : startTime.replace(/-/g, '/');
+  return new Date(normalized).getTime();
+}
+
+function calcRemain(data: OnlineExamPaper) {
+  if (data.durationMin == null || data.durationMin <= 0) return null;
+  const startMs = parseStartMs(data.startTime);
+  if (!Number.isNaN(startMs)) {
+    const endMs = startMs + data.durationMin * 60 * 1000;
+    return Math.max(0, Math.floor((endMs - Date.now()) / 1000));
+  }
+  if (data.remainSeconds != null) return Math.max(0, Number(data.remainSeconds));
+  return null;
+}
+
+function initCountdown(data: OnlineExamPaper) {
+  stopCountdown();
+  const remain = calcRemain(data);
+  remainSeconds.value = remain;
+  if (remain == null) return;
+  if (remain <= 0) {
+    void doSubmit({ auto: true });
+    return;
+  }
+  timer = setInterval(() => {
+    if (submitted.value || submitting.value || autoSubmitting.value) {
+      stopCountdown();
+      return;
+    }
+    const next = calcRemain(paper.value!);
+    if (next == null) {
+      remainSeconds.value = null;
+      stopCountdown();
+      return;
+    }
+    remainSeconds.value = next;
+    if (next <= 0) {
+      stopCountdown();
+      void doSubmit({ auto: true });
+    }
+  }, 1000);
+}
+
+function stopCountdown() {
+  if (timer) {
+    clearInterval(timer);
+    timer = null;
+  }
+}
+
+function formatRemain(sec: number | null) {
+  if (sec == null) return '';
+  const s = Math.max(0, sec);
+  const h = Math.floor(s / 3600);
+  const m = Math.floor((s % 3600) / 60);
+  const r = s % 60;
+  const mm = String(m).padStart(2, '0');
+  const ss = String(r).padStart(2, '0');
+  if (h > 0) return `${String(h).padStart(2, '0')}:${mm}:${ss}`;
+  return `${mm}:${ss}`;
+}
 
 function stripHtml(html?: string) {
   if (!html) return '';
   return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
 }
 
-function goPrev() {
-  if (currentIndex.value > 0) currentIndex.value -= 1;
-}
-
-function goNext() {
-  if (currentIndex.value < total.value - 1) currentIndex.value += 1;
-}
-
-function goBack() {
-  router.push('/tms/onlineExam');
-}
-
-function isCorrect(q: OnlineExamQuestionItem): boolean {
-  const ans = answers[q.id];
-  const correctLabels = q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
-  if (q.questionType === 'multi') {
-    const selected = Array.isArray(ans) ? [...ans].sort() : [];
-    return selected.join(',') === [...correctLabels].sort().join(',');
+async function persistAnswers() {
+  if (!paper.value || submitted.value || saving.value) return;
+  saving.value = true;
+  try {
+    await saveOnlineExam(paper.value.examId, { ...answers });
+  } catch (e: any) {
+    const msg = e?.message || '鏆傚瓨澶辫触';
+    if (String(msg).includes('鑷姩浜ゅ嵎')) {
+      createMessage.warning(msg);
+      stopCountdown();
+      router.replace('/tms/onlineExam');
+      return;
+    }
+    createMessage.error(msg);
+  } finally {
+    saving.value = false;
   }
-  return String(ans || '') === String(correctLabels[0] || '');
 }
 
-function calcScore(): number {
-  if (!paper.value) return 0;
-  let got = 0;
-  paper.value.questions.forEach((q) => {
-    if (isCorrect(q)) got += Number(q.score || 0);
-  });
-  return got;
+async function goPrev() {
+  if (currentIndex.value <= 0) return;
+  await persistAnswers();
+  currentIndex.value -= 1;
+}
+
+async function goNext() {
+  if (currentIndex.value >= total.value - 1) return;
+  await persistAnswers();
+  currentIndex.value += 1;
+}
+
+async function goToQuestion(index: number) {
+  if (index < 0 || index >= total.value || index === currentIndex.value) {
+    sheetOpen.value = false;
+    return;
+  }
+  if (!submitted.value) await persistAnswers();
+  currentIndex.value = index;
+  sheetOpen.value = false;
+}
+
+function openSheet() {
+  sheetOpen.value = true;
+}
+
+async function goBack() {
+  if (!submitted.value) await persistAnswers();
+  resultModalOpen.value = false;
+  router.push('/tms/onlineExam');
 }
 
 function handleSubmit() {
@@ -80,24 +233,130 @@
   Modal.confirm({
     title: '纭浜ゅ嵎',
     content: '浜ゅ嵎鍚庝笉鍙啀淇敼绛旀锛岀‘瀹氫氦鍗峰悧锛�',
-    onOk: doSubmit,
+    onOk: () => doSubmit(),
   });
 }
 
-async function doSubmit() {
+function applyGrade(result: OnlineExamSubmitResult) {
   if (!paper.value) return;
+  const byId = new Map((result.items || []).map((item) => [item.id, item]));
+  for (const q of paper.value.questions) {
+    const item = byId.get(q.id);
+    if (!item) continue;
+    q.right = item.right;
+    q.subjective = item.subjective;
+    q.correctAnswer = item.correctAnswer || '';
+    if (q.options?.length && item.correctAnswer && q.questionType !== 'blank' && q.questionType !== 'essay') {
+      const labels = new Set(item.correctAnswer.split(/[,锛宂/).map((x) => x.trim()).filter(Boolean));
+      q.options.forEach((opt) => {
+        opt.isCorrect = labels.has(opt.optionLabel) ? '1' : '0';
+      });
+    }
+  }
+}
+
+function closeResultModal() {
+  resultModalOpen.value = false;
+}
+
+async function openResultModal() {
+  resultFilter.value = 'all';
+  await nextTick();
+  resultModalOpen.value = true;
+}
+
+function correctLabelsOf(q: OnlineExamQuestionItem) {
+  if (q.questionType === 'blank' || q.questionType === 'essay') {
+    return q.correctAnswer ? [q.correctAnswer] : [];
+  }
+  const fromOpt = q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
+  if (fromOpt.length) return fromOpt;
+  return (q.correctAnswer || '')
+    .split(/[,锛宂/)
+    .map((x) => x.trim())
+    .filter(Boolean);
+}
+
+function userAnswerText(q: OnlineExamQuestionItem) {
+  const ans = answers[q.id];
+  if (q.questionType === 'multi') {
+    const selected = Array.isArray(ans) ? [...ans].sort() : [];
+    return selected.length ? selected.join('銆�') : '鏈綔绛�';
+  }
+  return String(ans || '') || '鏈綔绛�';
+}
+
+function optionClass(q: OnlineExamQuestionItem, label: string) {
+  if (!submitted.value || !isObjective(q)) return '';
+  const correctSet = new Set(correctLabelsOf(q));
+  const ans = answers[q.id];
+  const userSet = new Set(
+    q.questionType === 'multi'
+      ? Array.isArray(ans)
+        ? ans
+        : []
+      : ans
+        ? [String(ans)]
+        : [],
+  );
+  if (correctSet.has(label)) return 'opt-correct';
+  if (userSet.has(label) && !correctSet.has(label)) return 'opt-wrong';
+  return '';
+}
+
+function resultTag(q: OnlineExamQuestionItem) {
+  if (!isObjective(q)) return { color: 'default', text: '寰呴槄鍗�' };
+  return q.right ? { color: 'success', text: '鍥炵瓟姝g‘' } : { color: 'error', text: '鍥炵瓟閿欒' };
+}
+
+function passText() {
+  if (submitResult.value?.pendingGrade) return '寰呴槄鍗�';
+  if (submitResult.value?.passFlag === '1') return '鍚堟牸';
+  if (submitResult.value?.passFlag === '0') return '涓嶅悎鏍�';
+  return '-';
+}
+
+async function doSubmit(opts?: { auto?: boolean }) {
+  if (!paper.value || submitted.value || submitting.value) return;
+  if (opts?.auto) autoSubmitting.value = true;
   submitting.value = true;
+  stopCountdown();
   try {
-    const got = calcScore();
-    await submitOnlineExam(paper.value.examId, { score: got, answers: { ...answers } });
+    const result = await submitOnlineExam(paper.value.examId, { ...answers });
+    applyGrade(result);
     submitted.value = true;
-    scoreText.value = `${got} / ${paper.value.totalScore}`;
-    createMessage.success(`浜ゅ嵎鎴愬姛锛屽緱鍒� ${got} 鍒嗭紙婊″垎 ${paper.value.totalScore}锛塦);
+    remainSeconds.value = 0;
+    submitResult.value = result;
+    const got = result.pendingGrade ? result.objectiveScore : result.totalScore;
+    scoreText.value = result.pendingGrade
+      ? `瀹㈣棰� ${got ?? 0} / ${paper.value.totalScore}`
+      : `${got ?? 0} / ${paper.value.totalScore}`;
+    if (opts?.auto) {
+      createMessage.warning('鑰冭瘯鏃堕棿宸插埌锛屽凡鑷姩浜ゅ嵎');
+    }
+    await openResultModal();
   } catch (e: any) {
-    createMessage.error(e?.message || '浜ゅ嵎澶辫触');
+    const msg = e?.message || '浜ゅ嵎澶辫触';
+    if (String(msg).includes('宸蹭氦鍗�') || String(msg).includes('鑷姩浜ゅ嵎')) {
+      createMessage.warning(msg);
+      router.replace('/tms/onlineExam');
+      return;
+    }
+    createMessage.error(msg);
+    if (opts?.auto && paper.value && !submitted.value) {
+      initCountdown(paper.value);
+    }
   } finally {
     submitting.value = false;
+    autoSubmitting.value = false;
   }
+}
+
+function reviewText(q?: OnlineExamQuestionItem) {
+  if (!q || !submitted.value) return '';
+  if (!isObjective(q)) return '涓昏棰橈紝寰呴槄鍗�';
+  if (q.right) return '鍥炵瓟姝g‘';
+  return `鍥炵瓟閿欒 路 姝g‘绛旀锛�${correctLabelsOf(q).join('銆�') || '-'}`;
 }
 </script>
 
@@ -107,7 +366,12 @@
       <div v-if="paper" class="jnpf-content-wrapper-content tms-online-exam-page">
         <div class="tms-exam-header">
           <div>
-            <div class="text-base font-medium">{{ paper.paperName }}</div>
+            <div class="text-base font-medium">
+              {{ paper.paperName }}
+              <span v-if="paper.attemptLabel" class="ml-2 text-sm font-normal text-gray-500">
+                锛坽{ paper.attemptLabel }}锛�
+              </span>
+            </div>
             <div class="mt-1 text-gray-400 text-sm">
               绗� {{ currentIndex + 1 }} / {{ total }} 棰�
               <span v-if="paper.durationMin" class="ml-3">鏃堕暱 {{ paper.durationMin }} 鍒嗛挓</span>
@@ -115,17 +379,32 @@
             </div>
           </div>
           <a-space>
-            <a-button @click="goBack">杩斿洖鍒楄〃</a-button>
+            <div
+              v-if="countdownText && !submitted"
+              class="countdown"
+              :class="{ urgent: countdownUrgent, ended: remainSeconds === 0 }"
+            >
+              鍓╀綑 {{ countdownText }}
+            </div>
+            <a-button v-if="submitted" type="link" @click="openResultModal">鏌ョ湅鏈缁撴灉</a-button>
+            <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
             <a-button type="primary" :loading="submitting" :disabled="submitted" @click="handleSubmit">
-              浜ゅ嵎
+              {{ TMS_BTN.submitExam }}
             </a-button>
           </a-space>
         </div>
 
         <div v-if="current" class="tms-exam-body">
-          <div class="mb-3 text-sm text-gray-500">
-            {{ labelOfType(current.questionType) }}
-            <span class="ml-2">锛坽{ current.score }} 鍒嗭級</span>
+          <div class="q-meta mb-3">
+            <div class="text-sm text-gray-500">
+              {{ labelOfType(current.questionType) }}
+              <span class="ml-2">锛坽{ current.score }} 鍒嗭級</span>
+            </div>
+            <a-tooltip title="绛旈鍗�">
+              <button type="button" class="sheet-icon-btn" aria-label="绛旈鍗�" @click="openSheet">
+                <AppstoreOutlined />
+              </button>
+            </a-tooltip>
           </div>
           <div class="stem mb-4">{{ stripHtml(current.stem) }}</div>
 
@@ -151,20 +430,177 @@
             </a-checkbox>
           </a-checkbox-group>
 
+          <a-input
+            v-else-if="current.questionType === 'blank'"
+            v-model:value="answers[current.id]"
+            :disabled="submitted"
+            placeholder="璇疯緭鍏ョ瓟妗�"
+          />
+
+          <a-textarea
+            v-else-if="current.questionType === 'essay'"
+            v-model:value="answers[current.id]"
+            :disabled="submitted"
+            :rows="6"
+            placeholder="璇疯緭鍏ョ瓟妗�"
+          />
+
           <div
             v-if="submitted"
             class="mt-4 text-sm"
-            :class="isCorrect(current) ? 'text-green-600' : 'text-red-500'"
+            :class="
+              current.right
+                ? 'text-green-600'
+                : !isObjective(current)
+                  ? 'text-gray-500'
+                  : 'text-red-500'
+            "
           >
-            {{ isCorrect(current) ? '鍥炵瓟姝g‘' : '鍥炵瓟閿欒' }}
-            路 姝g‘绛旀锛�
-            {{ current.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel).join('銆�') }}
+            {{ reviewText(current) }}
           </div>
         </div>
 
         <div class="tms-exam-footer">
           <a-button :disabled="currentIndex <= 0" @click="goPrev">涓婁竴棰�</a-button>
           <a-button :disabled="currentIndex >= total - 1" @click="goNext">涓嬩竴棰�</a-button>
+        </div>
+      </div>
+    </div>
+
+    <div v-if="sheetOpen && paper" class="tms-sheet-overlay" @click.self="sheetOpen = false">
+      <div class="tms-sheet-dialog" role="dialog" aria-modal="true">
+        <div class="sheet-head">
+          <div>
+            <div class="sheet-title">绛旈鍗�</div>
+            <div class="sheet-sub">宸茬瓟 {{ answeredCount }} / {{ total }}锛岀偣鍑婚鍙峰彲鍒囨崲</div>
+          </div>
+          <button type="button" class="tms-result-close" aria-label="鍏抽棴" @click="sheetOpen = false">脳</button>
+        </div>
+        <div class="sheet-legend">
+          <span class="legend-chip current">褰撳墠</span>
+          <span class="legend-chip answered">宸茬瓟</span>
+          <span class="legend-chip unanswered">鏈瓟</span>
+        </div>
+        <div class="sheet-grid">
+          <button
+            v-for="(q, idx) in paper.questions"
+            :key="q.id"
+            type="button"
+            class="sheet-item"
+            :class="{
+              current: idx === currentIndex,
+              answered: isAnswered(q),
+              unanswered: !isAnswered(q),
+            }"
+            @click="goToQuestion(idx)"
+          >
+            {{ idx + 1 }}
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <div
+      v-if="resultModalOpen && submitResult && paper"
+      class="tms-result-overlay"
+      @click.self="closeResultModal"
+    >
+      <div class="tms-result-dialog" role="dialog" aria-modal="true">
+        <button type="button" class="tms-result-close" aria-label="鍏抽棴" @click="closeResultModal">脳</button>
+
+        <div class="result-hero">
+          <div class="hero-title">鏈鑰冭瘯缁撴灉</div>
+          <div class="hero-bank">
+            {{ paper.paperName }}
+            <span v-if="paper.attemptLabel">锛坽{ paper.attemptLabel }}锛�</span>
+          </div>
+          <div class="hero-stats">
+            <div class="stat-item">
+              <div class="stat-value">{{ rightCount }}</div>
+              <div class="stat-label">绛斿</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">{{ wrongCount }}</div>
+              <div class="stat-label">绛旈敊</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">{{ scoreRate }}%</div>
+              <div class="stat-label">姝g‘鐜�</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">
+                {{ submitResult.pendingGrade ? submitResult.objectiveScore : submitResult.totalScore }}/{{
+                  paper.totalScore
+                }}
+              </div>
+              <div class="stat-label">{{ submitResult.pendingGrade ? '瀹㈣鍒�' : '寰楀垎' }}</div>
+            </div>
+          </div>
+          <div class="hero-extra">
+            缁撴灉锛歿{ passText() }}
+            <span v-if="submitResult.pendingGrade" class="ml-2">锛堝惈涓昏棰橈紝寰呴槄鍗峰悗鍑烘渶缁堟垚缁╋級</span>
+          </div>
+        </div>
+
+        <div class="result-toolbar">
+          <a-radio-group v-model:value="resultFilter" button-style="solid" size="small">
+            <a-radio-button value="all">鍏ㄩ儴棰樼洰锛坽{ total }}锛�</a-radio-button>
+            <a-radio-button value="wrong">浠呴敊棰橈紙{{ wrongCount }}锛�</a-radio-button>
+          </a-radio-group>
+          <div class="legend">
+            <span class="legend-item correct">姝g‘绛旀</span>
+            <span class="legend-item wrong">浣犵殑閿欒閫夐」</span>
+          </div>
+        </div>
+
+        <div class="result-list">
+          <a-empty v-if="!resultQuestions.length" description="鏆傛棤棰樼洰" />
+          <div
+            v-for="(q, idx) in resultQuestions"
+            :key="q.id"
+            class="question-card"
+            :class="!isObjective(q) ? '' : q.right ? 'is-right' : 'is-wrong'"
+          >
+            <div class="q-head">
+              <div class="q-head-left">
+                <span class="q-index">绗� {{ idx + 1 }} 棰�</span>
+                <span class="q-type">{{ labelOfType(q.questionType) }} 路 {{ q.score }} 鍒�</span>
+              </div>
+              <a-tag :color="resultTag(q).color">{{ resultTag(q).text }}</a-tag>
+            </div>
+            <div class="q-stem">{{ stripHtml(q.stem) }}</div>
+            <div v-if="q.options?.length" class="q-options">
+              <div
+                v-for="opt in q.options"
+                :key="opt.optionLabel"
+                class="q-option"
+                :class="optionClass(q, opt.optionLabel)"
+              >
+                <span class="opt-label">{{ opt.optionLabel }}</span>
+                <span class="opt-content">{{ opt.optionContent }}</span>
+              </div>
+            </div>
+            <div class="q-answer-bar">
+              <div>
+                <span class="ans-label">浣犵殑绛旀</span>
+                <span :class="!isObjective(q) ? '' : q.right ? 'ans-ok' : 'ans-bad'">
+                  {{ userAnswerText(q) }}
+                </span>
+              </div>
+              <div v-if="isObjective(q)">
+                <span class="ans-label">姝g‘绛旀</span>
+                <span class="ans-ok">{{ correctLabelsOf(q).join('銆�') || '-' }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="result-footer">
+          <a-button @click="closeResultModal">{{ TMS_BTN.close }}</a-button>
+          <a-button type="primary" @click="goBack">{{ TMS_BTN.back }}</a-button>
         </div>
       </div>
     </div>
@@ -191,10 +627,62 @@
   flex-shrink: 0;
 }
 
+.countdown {
+  min-width: 120px;
+  padding: 4px 12px;
+  font-size: 16px;
+  font-weight: 600;
+  font-variant-numeric: tabular-nums;
+  color: #1890ff;
+  background: #e6f7ff;
+  border: 1px solid #91d5ff;
+  border-radius: 4px;
+  text-align: center;
+}
+
+.countdown.urgent {
+  color: #cf1322;
+  background: #fff1f0;
+  border-color: #ffa39e;
+}
+
+.countdown.ended {
+  color: #8c8c8c;
+  background: #fafafa;
+  border-color: #d9d9d9;
+}
+
 .tms-exam-body {
   flex: 1;
   min-height: 0;
   overflow: auto;
+}
+
+.q-meta {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.sheet-icon-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border: 1px solid #d9d9d9;
+  border-radius: 6px;
+  background: #fff;
+  color: #1677ff;
+  font-size: 16px;
+  cursor: pointer;
+  flex-shrink: 0;
+}
+
+.sheet-icon-btn:hover {
+  border-color: #1677ff;
+  background: #e6f4ff;
 }
 
 .stem {
@@ -211,4 +699,394 @@
   padding-top: 16px;
   border-top: 1px solid #f0f0f0;
 }
+
+.tms-sheet-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 1900;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  background: rgba(0, 0, 0, 0.45);
+}
+
+.tms-sheet-dialog {
+  width: min(520px, 100%);
+  max-height: min(70vh, 640px);
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+}
+
+.sheet-head {
+  position: relative;
+  flex-shrink: 0;
+  padding: 18px 48px 12px 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.sheet-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.sheet-sub {
+  margin-top: 4px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.sheet-legend {
+  flex-shrink: 0;
+  display: flex;
+  gap: 16px;
+  padding: 12px 20px 0;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.55);
+}
+
+.legend-chip::before {
+  content: '';
+  display: inline-block;
+  width: 12px;
+  height: 12px;
+  margin-right: 6px;
+  border-radius: 3px;
+  vertical-align: -2px;
+}
+
+.legend-chip.current::before {
+  background: #fff;
+  border: 2px solid #1677ff;
+  box-sizing: border-box;
+}
+
+.legend-chip.answered::before {
+  background: #1677ff;
+}
+
+.legend-chip.unanswered::before {
+  background: #f5f5f5;
+  border: 1px solid #d9d9d9;
+  box-sizing: border-box;
+}
+
+.sheet-grid {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
+  gap: 10px;
+  padding: 16px 20px 20px;
+}
+
+.sheet-item {
+  height: 40px;
+  border-radius: 8px;
+  border: 1px solid #d9d9d9;
+  background: #fafafa;
+  color: rgba(0, 0, 0, 0.65);
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.15s ease;
+}
+
+.sheet-item.answered {
+  background: #1677ff;
+  border-color: #1677ff;
+  color: #fff;
+}
+
+.sheet-item.unanswered {
+  background: #f5f5f5;
+  border-color: #d9d9d9;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.sheet-item.current {
+  box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.35);
+}
+
+.sheet-item.current.unanswered {
+  border-color: #1677ff;
+  color: #1677ff;
+  background: #e6f4ff;
+}
+
+.sheet-item:hover {
+  filter: brightness(0.97);
+}
+
+.tms-result-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 2000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  background: rgba(0, 0, 0, 0.45);
+}
+
+.tms-result-dialog {
+  position: relative;
+  width: min(720px, 100%);
+  max-height: min(80vh, 780px);
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+}
+
+.tms-result-close {
+  position: absolute;
+  top: 10px;
+  right: 12px;
+  z-index: 2;
+  width: 32px;
+  height: 32px;
+  border: none;
+  border-radius: 6px;
+  background: transparent;
+  color: rgba(0, 0, 0, 0.45);
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+  line-height: 1;
+}
+
+.tms-result-close:hover {
+  background: rgba(0, 0, 0, 0.06);
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.result-hero {
+  flex-shrink: 0;
+  padding: 24px 28px 20px;
+  background: linear-gradient(135deg, #f0f7ff 0%, #f8fbff 55%, #ffffff 100%);
+  border-bottom: 1px solid #eef2f7;
+}
+
+.hero-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+  padding-right: 36px;
+}
+
+.hero-bank {
+  margin-top: 4px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.hero-stats {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 18px;
+  padding: 14px 8px;
+  background: #fff;
+  border: 1px solid #e8eef5;
+  border-radius: 10px;
+}
+
+.hero-extra {
+  margin-top: 12px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.65);
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 22px;
+  font-weight: 700;
+  line-height: 1.2;
+  color: #1677ff;
+}
+
+.stat-label {
+  margin-top: 4px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.stat-divider {
+  width: 1px;
+  height: 28px;
+  background: #eef2f7;
+}
+
+.result-toolbar {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+  padding: 12px 28px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.legend {
+  display: flex;
+  gap: 12px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.legend-item::before {
+  content: '';
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin-right: 6px;
+  border-radius: 2px;
+  vertical-align: -1px;
+}
+
+.legend-item.correct::before {
+  background: #b7eb8f;
+}
+
+.legend-item.wrong::before {
+  background: #ffa39e;
+}
+
+.result-list {
+  flex: 1;
+  min-height: 160px;
+  overflow: auto;
+  padding: 8px 28px 4px;
+}
+
+.question-card {
+  margin-bottom: 12px;
+  padding: 14px 16px;
+  border: 1px solid #f0f0f0;
+  border-radius: 10px;
+  background: #fff;
+}
+
+.question-card.is-wrong {
+  border-color: #ffccc7;
+  background: #fffafa;
+}
+
+.question-card.is-right {
+  border-color: #d9f7be;
+  background: #fcfffb;
+}
+
+.q-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.q-head-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.q-index {
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-type {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.q-stem {
+  margin-bottom: 10px;
+  font-size: 14px;
+  line-height: 1.7;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-options {
+  display: grid;
+  gap: 6px;
+  margin-bottom: 10px;
+}
+
+.q-option {
+  display: flex;
+  gap: 8px;
+  padding: 8px 10px;
+  border-radius: 6px;
+  background: #fafafa;
+  border: 1px solid transparent;
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.q-option .opt-label {
+  min-width: 18px;
+  font-weight: 600;
+}
+
+.q-option.opt-correct {
+  background: #f6ffed;
+  color: #389e0d;
+  border-color: #b7eb8f;
+}
+
+.q-option.opt-wrong {
+  background: #fff2f0;
+  color: #cf1322;
+  border-color: #ffccc7;
+}
+
+.q-answer-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px 28px;
+  padding-top: 8px;
+  border-top: 1px dashed #f0f0f0;
+  font-size: 13px;
+}
+
+.ans-label {
+  margin-right: 8px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.ans-ok {
+  color: #389e0d;
+  font-weight: 600;
+}
+
+.ans-bad {
+  color: #cf1322;
+  font-weight: 600;
+}
+
+.result-footer {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 14px 28px 18px;
+  border-top: 1px solid #f0f0f0;
+  background: #fafafa;
+}
 </style>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/index.vue
index 8fe379c..df9fb19 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/index.vue
@@ -3,15 +3,26 @@
 
 import type { MyPaperListItem } from './types';
 
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { getMyPaperList, startOnlineExam } from '#/api/x/tms/onlineExam';
+import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
-import { MY_PAPER_STATUS_OPTIONS, colorOfMyPaperStatus, labelOfMyPaperStatus } from './constants';
+import {
+  MY_PAPER_STATUS_OPTIONS,
+  colorOfMyPaperStatus,
+  colorOfPassFlag,
+  labelOfMyPaperStatus,
+  labelOfPassFlag,
+  loadOnlineExamDics,
+} from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsOnlineExam' });
 
@@ -20,7 +31,18 @@
 const starting = ref(false);
 
 const columns: BasicColumn[] = [
-  { title: '璇曞嵎鍚嶇О', dataIndex: 'paperName', minWidth: 260 },
+  { title: '璇曞嵎鍚嶇О', dataIndex: 'paperName', minWidth: 220 },
+  {
+    title: '鑰冭瘯绫诲瀷',
+    dataIndex: 'attemptLabel',
+    width: 130,
+    align: 'center',
+    customRender: ({ record }) => {
+      const row = record as MyPaperListItem;
+      return row.attemptLabel
+        || (row.attemptNo && row.attemptNo > 1 ? `琛ヨ�冿紙绗�${row.attemptNo - 1}娆★級` : '棣栬��');
+    },
+  },
   {
     title: '鐘舵��',
     dataIndex: 'status',
@@ -31,7 +53,7 @@
   {
     title: '鑰冭瘯鏃堕棿',
     dataIndex: 'examTimeText',
-    minWidth: 280,
+    minWidth: 260,
     customRender: ({ record }) => (record as MyPaperListItem).examTimeText || '-',
   },
   {
@@ -40,12 +62,30 @@
     width: 100,
     align: 'center',
   },
+  {
+    title: '鏄惁鍚堟牸',
+    dataIndex: 'passFlag',
+    width: 100,
+    align: 'center',
+    slots: { default: 'passFlag' },
+  },
+  {
+    title: '鍓╀綑琛ヨ��',
+    dataIndex: 'remainingRetakes',
+    width: 100,
+    align: 'center',
+    customRender: ({ record }) => {
+      const row = record as MyPaperListItem;
+      if (row.retakeLimit == null) return '-';
+      return `${row.remainingRetakes ?? 0} / ${row.retakeLimit}`;
+    },
+  },
 ];
 
-const [registerTable] = useVxeTable({
+const [registerTable, { reload, getForm }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
@@ -65,21 +105,42 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨鐘舵��',
-          options: MY_PAPER_STATUS_OPTIONS,
+          options: MY_PAPER_STATUS_OPTIONS.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
     ],
   },
   actionColumn: {
-    width: 160,
+    width: 200,
     title: '鎿嶄綔',
     dataIndex: 'action',
     fixed: 'right',
   },
 });
 
+onMounted(async () => {
+  await loadOnlineExamDics();
+  getForm()?.updateSchema?.({
+    field: 'status',
+    componentProps: {
+      allowClear: true,
+      placeholder: '璇烽�夋嫨鐘舵��',
+      options: MY_PAPER_STATUS_OPTIONS.value,
+      fieldNames: TMS_DIC_FIELD_NAMES,
+    },
+  });
+  reload();
+});
+
 async function fetchList(params: Record<string, any>) {
-  return { data: await getMyPaperList(params) };
+  const page = await getMyPaperList(params);
+  return {
+    data: {
+      list: Array.isArray(page?.list) ? page.list : [],
+      pagination: page?.pagination || { total: 0 },
+    },
+  };
 }
 
 function handleDetail(record: MyPaperListItem) {
@@ -91,11 +152,22 @@
   starting.value = true;
   try {
     const paper = await startOnlineExam(record.id);
+    if (paper?.autoSubmitted) {
+      createMessage.warning('鑰冭瘯鏃堕棿宸插埌锛屽凡鑷姩浜ゅ嵎');
+      reload();
+      return;
+    }
     sessionStorage.setItem('tms_online_exam_paper', JSON.stringify(paper));
     sessionStorage.setItem('tms_online_exam_myPaperId', record.id);
     router.push('/tms/onlineExam/exam');
   } catch (e: any) {
-    createMessage.error(e?.message || '寮�濮嬭�冭瘯澶辫触');
+    const msg = e?.message || '寮�濮嬭�冭瘯澶辫触';
+    if (String(msg).includes('鑷姩浜ゅ嵎')) {
+      createMessage.warning(msg);
+      reload();
+    } else {
+      createMessage.error(msg);
+    }
   } finally {
     starting.value = false;
   }
@@ -104,11 +176,13 @@
 function getTableActions(record: MyPaperListItem): ActionItem[] {
   const actions: ActionItem[] = [];
   if (record.status === 'notStarted') {
-    actions.push({ label: '寮�濮嬭�冭瘯', onClick: handleStart.bind(null, record) });
+    actions.push({ label: TMS_BTN.startExam, onClick: handleStart.bind(null, record) });
   } else if (record.status === 'doing') {
-    actions.push({ label: '缁х画鑰冭瘯', onClick: handleStart.bind(null, record) });
+    actions.push({ label: TMS_BTN.continueExam, onClick: handleStart.bind(null, record) });
+  } else if (record.status === 'submitted' && record.canRetake) {
+    actions.push({ label: TMS_BTN.retake, onClick: handleStart.bind(null, record) });
   }
-  actions.push({ label: '鏌ョ湅璇︽儏', onClick: handleDetail.bind(null, record) });
+  actions.push({ label: TMS_BTN.detail, onClick: handleDetail.bind(null, record) });
   return actions;
 }
 </script>
@@ -120,8 +194,8 @@
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
             <div>
-              <div class="text-base font-medium">鎴戠殑璇曞嵎</div>
-              <div class="mt-1 text-gray-400 text-sm">鎴戠殑璇曞嵎锛岄�夋嫨璇曞嵎鍙傚姞鑰冭瘯锛屾垨鑰呮煡鐪嬭�冭瘯璇︽儏銆�</div>
+              <div class="tms-page-header__title">鎴戠殑璇曞嵎</div>
+              <div class="tms-page-header__sub">鍙傚姞鑰冭瘯鎴栨煡鐪嬫垚缁╋紱鍙�冩椂鎿嶄綔鍒楃洿鎺ュ紑鑰冦��</div>
             </div>
           </template>
           <template #status="{ record }">
@@ -129,6 +203,11 @@
               {{ labelOfMyPaperStatus(record.status) }}
             </span>
           </template>
+          <template #passFlag="{ record }">
+            <span :style="{ color: colorOfPassFlag(record.passFlag) }">
+              {{ labelOfPassFlag(record.passFlag) }}
+            </span>
+          </template>
           <template #action="{ record }">
             <TableAction :actions="getTableActions(record)" />
           </template>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/mock.ts
index d35d515..8a78b1e 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/mock.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/mock.ts
@@ -6,11 +6,50 @@
   OnlineExamQuestionItem,
 } from './types';
 
-import { mockGetQuestion, mockQueryQuestions } from '#/views/x/tms/question/mock';
-
 function delay<T>(data: T, ms = 220): Promise<T> {
   return new Promise((resolve) => setTimeout(() => resolve(data), ms));
 }
+
+/** 鍦ㄧ嚎鑰冭瘯 mock 鑷甫鏍烽锛堜笉鍐嶄緷璧栬瘯棰樼鐞� mock锛� */
+const DEMO_QUESTIONS: OnlineExamQuestionItem[] = [
+  {
+    id: 'demo_q1',
+    questionNo: '260001',
+    questionType: 'judge',
+    stem: '鐗圭浣滀笟浜哄憳蹇呴』鍙栧緱鏈夋晥璧勬牸璇佷功鍚庢柟鍙笂宀椾綔涓氥��',
+    score: 20,
+    options: [
+      { optionLabel: 'T', optionContent: '姝g‘', isCorrect: '1' },
+      { optionLabel: 'F', optionContent: '閿欒', isCorrect: '0' },
+    ],
+  },
+  {
+    id: 'demo_q2',
+    questionNo: '260002',
+    questionType: 'multi',
+    stem: '涓嬪垪鍝簺灞炰簬鐗圭璁惧锛�',
+    score: 20,
+    options: [
+      { optionLabel: 'A', optionContent: '鐢垫', isCorrect: '1' },
+      { optionLabel: 'B', optionContent: '鍘嬪姏瀹瑰櫒', isCorrect: '1' },
+      { optionLabel: 'C', optionContent: '鏅�氬姙鍏', isCorrect: '0' },
+      { optionLabel: 'D', optionContent: '閿呯倝', isCorrect: '1' },
+    ],
+  },
+  {
+    id: 'demo_q3',
+    questionNo: '260003',
+    questionType: 'single',
+    stem: 'GMP 鐨勫叏绉版槸锛�',
+    score: 20,
+    options: [
+      { optionLabel: 'A', optionContent: '鑽搧鐢熶骇璐ㄩ噺绠$悊瑙勮寖', isCorrect: '1' },
+      { optionLabel: 'B', optionContent: '鑽搧缁忚惀璐ㄩ噺绠$悊瑙勮寖', isCorrect: '0' },
+      { optionLabel: 'C', optionContent: '瀹為獙瀹ょ鐞嗚鑼�', isCorrect: '0' },
+      { optionLabel: 'D', optionContent: '鏂囦欢绠$悊瑙勮寖', isCorrect: '0' },
+    ],
+  },
+];
 
 const store: MyPaperListItem[] = [
   {
@@ -89,48 +128,11 @@
   });
 }
 
-async function buildQuestions(): Promise<OnlineExamQuestionItem[]> {
-  const all = await mockQueryQuestions({ pageSize: 50, currentPage: 1 });
-  const list = (all.list || []).filter((q) => ['single', 'multi', 'judge'].includes(q.questionType));
-  const picked = list.slice(0, 5);
-  const detailed: OnlineExamQuestionItem[] = [];
-  for (const q of picked) {
-    const full = await mockGetQuestion(q.id!);
-    detailed.push({
-      id: full.id!,
-      questionNo: full.questionNo,
-      questionType: full.questionType as 'single' | 'multi' | 'judge',
-      stem: full.stem,
-      score: 20,
-      options: (full.options || []).map((o) => ({
-        optionLabel: o.optionLabel,
-        optionContent: o.optionContent,
-        isCorrect: o.isCorrect,
-      })),
-    });
-  }
-  // mock 棰樹笉澶熸椂琛ョ┖棰橀伩鍏嶆棤娉曞紑鑰�
-  if (!detailed.length) {
-    detailed.push({
-      id: 'demo_q1',
-      questionType: 'judge',
-      stem: '鐗圭浣滀笟浜哄憳蹇呴』鍙栧緱鏈夋晥璧勬牸璇佷功鍚庢柟鍙笂宀椾綔涓氥��',
-      score: 100,
-      options: [
-        { optionLabel: 'T', optionContent: '姝g‘', isCorrect: '1' },
-        { optionLabel: 'F', optionContent: '閿欒', isCorrect: '0' },
-      ],
-    });
-  }
-  return detailed;
-}
-
 export async function mockStartExam(myPaperId: string): Promise<OnlineExamPaper> {
   const row = store.find((x) => x.id === myPaperId);
   if (!row) return Promise.reject(new Error('璇曞嵎涓嶅瓨鍦�'));
   if (row.status === 'submitted') return Promise.reject(new Error('璇ヨ瘯鍗峰凡浜ゅ嵎锛屾棤娉曞啀娆¤�冭瘯'));
 
-  const questions = await buildQuestions();
   const examId = row.examId || `exam_${Date.now()}`;
   row.status = 'doing';
   row.examId = examId;
@@ -142,7 +144,7 @@
     totalScore: row.totalScore,
     passScore: row.passScore,
     durationMin: row.durationMin,
-    questions,
+    questions: DEMO_QUESTIONS.map((q) => ({ ...q, options: q.options.map((o) => ({ ...o })) })),
   });
 }
 
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/types.ts b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/types.ts
index 26d2778..61ebd77 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/types.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/onlineExam/types.ts
@@ -17,7 +17,17 @@
   durationMin?: number;
   /** 宸蹭氦鍗锋椂鐨勫緱鍒� */
   gotScore?: number;
+  /** 1鍚堟牸 / 0涓嶅悎鏍� / 绌�=鏈嚭缁撴灉 */
+  passFlag?: '0' | '1' | '';
   examId?: string;
+  /** 褰撳墠/鏈�杩戜竴娆¤�冭瘯搴忓彿锛�1=棣栬�� */
+  attemptNo?: number;
+  /** 棣栬�� / 琛ヨ�冿紙绗�1娆★級鈥︼紙琛ヨ�冨簭鍙蜂笉鍚鑰冿級 */
+  attemptLabel?: string;
+  submittedCount?: number;
+  retakeLimit?: number;
+  remainingRetakes?: number;
+  canRetake?: boolean;
 }
 
 export interface MyPaperPageQuery {
@@ -35,16 +45,60 @@
   totalScore: number;
   passScore?: number;
   durationMin?: number;
+  /** 寮�鑰冩椂闂达紝鍊掕鏃舵寜姝ょ画绠� */
+  startTime?: string;
+  /** 鏈嶅姟绔墿浣欑鏁� */
+  remainSeconds?: number | null;
+  /** 寮�鑰冩帴鍙e彂鐜板凡瓒呮椂骞跺畬鎴愯嚜鍔ㄤ氦鍗� */
+  autoSubmitted?: boolean;
+  attemptNo?: number;
+  attemptLabel?: string;
   questions: OnlineExamQuestionItem[];
 }
 
+export type OnlineQuestionType = 'single' | 'multi' | 'judge' | 'blank' | 'essay';
+
 export interface OnlineExamQuestionItem {
   id: string;
+  questionId?: string;
   questionNo?: string;
-  questionType: 'single' | 'multi' | 'judge';
+  questionType: OnlineQuestionType;
   stem: string;
   score: number;
-  options: { optionLabel: string; optionContent: string; isCorrect: '0' | '1' }[];
+  gotScore?: number | null;
+  userAnswer?: string;
+  options: { optionLabel: string; optionContent: string; isCorrect?: '0' | '1' }[];
+  /** 浜ゅ嵎鍚庡洖濉� */
+  right?: boolean | null;
+  correctAnswer?: string;
+  subjective?: boolean;
+}
+
+export interface OnlineExamSubmitResult {
+  objectiveScore?: number;
+  totalScore?: number;
+  passFlag?: '0' | '1' | '';
+  gradeStatus?: string;
+  pendingGrade?: boolean;
+  items?: {
+    id: string;
+    correctAnswer?: string;
+    gotScore?: number | null;
+    right?: boolean | null;
+    subjective?: boolean;
+  }[];
+}
+
+export interface OnlineExamAttempt {
+  examId: string;
+  attemptNo?: number;
+  attemptLabel?: string;
+  status?: MyPaperStatus;
+  gradeStatus?: string;
+  startTime?: string;
+  submitTime?: string;
+  gotScore?: number | null;
+  passFlag?: '0' | '1' | '';
 }
 
 export interface OnlineExamDetail {
@@ -60,4 +114,16 @@
   startTime?: string;
   submitTime?: string;
   passFlag?: '0' | '1';
+  gradeStatus?: string;
+  examId?: string;
+  attemptNo?: number;
+  attemptLabel?: string;
+  submittedCount?: number;
+  retakeLimit?: number;
+  remainingRetakes?: number;
+  canRetake?: boolean;
+  /** 鍘嗗彶绛斿嵎 */
+  attempts?: OnlineExamAttempt[];
+  /** 宸蹭氦鍗峰洖椤� */
+  questions?: OnlineExamQuestionItem[];
 }
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>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/question/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/question/Form.vue
index 334f2aa..e57dcf3 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/question/Form.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/question/Form.vue
@@ -14,7 +14,9 @@
   QUESTION_TYPE_OPTIONS,
   SOURCE_OPTIONS,
   STATUS_OPTIONS,
+  loadQuestionDics,
 } from './constants';
+import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
 import {
   createDefaultOptions,
   hydrateFormFromEntity,
@@ -50,8 +52,12 @@
   blankMixMode: false,
 });
 
-const statusTip = computed(() => STATUS_OPTIONS.find((x) => x.id === dataForm.bizStatus)?.tip);
-const statusTipColor = computed(() => STATUS_OPTIONS.find((x) => x.id === dataForm.bizStatus)?.tipColor);
+const statusTip = computed(
+  () => STATUS_OPTIONS.value.find((x) => (x.enCode || x.id) === dataForm.bizStatus)?.tip,
+);
+const statusTipColor = computed(
+  () => STATUS_OPTIONS.value.find((x) => (x.enCode || x.id) === dataForm.bizStatus)?.tipColor,
+);
 
 const rules = {
   questionType: [{ required: true, message: '璇烽�夋嫨棰樺瀷', trigger: 'change' }],
@@ -68,6 +74,7 @@
 );
 
 onMounted(async () => {
+  await loadQuestionDics();
   banks.value = (await getQuestionBanks()) || [];
   if (isEdit.value) {
     await loadDetail(String(route.params.id));
@@ -170,6 +177,7 @@
                     <jnpf-select
                       v-model:value="dataForm.questionType"
                       :options="QUESTION_TYPE_OPTIONS"
+                      :field-names="TMS_DIC_FIELD_NAMES"
                       :allow-clear="false"
                       placeholder="璇烽�夋嫨棰樺瀷"
                     />
@@ -190,6 +198,7 @@
                     <jnpf-select
                       v-model:value="dataForm.difficulty"
                       :options="DIFFICULTY_OPTIONS"
+                      :field-names="TMS_DIC_FIELD_NAMES"
                       :allow-clear="false"
                     />
                   </a-form-item>
@@ -200,6 +209,7 @@
                       <jnpf-select
                         v-model:value="dataForm.bizStatus"
                         :options="STATUS_OPTIONS"
+                        :field-names="TMS_DIC_FIELD_NAMES"
                         :allow-clear="false"
                         class="!w-[200px]"
                       />
@@ -216,6 +226,7 @@
                     <jnpf-select
                       v-model:value="dataForm.sourceType"
                       :options="SOURCE_OPTIONS"
+                      :field-names="TMS_DIC_FIELD_NAMES"
                       :allow-clear="false"
                     />
                   </a-form-item>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/question/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/question/constants.ts
index 2b54ca1..d62eaca 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/question/constants.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/question/constants.ts
@@ -1,45 +1,52 @@
-import type { Difficulty, QuestionStatus, QuestionType, SourceType } from './types';
+import { ref } from 'vue';
 
-export const QUESTION_TYPE_OPTIONS: { id: QuestionType; fullName: string }[] = [
-  { id: 'single', fullName: '鍗曢�夐' },
-  { id: 'multi', fullName: '澶氶�夐' },
-  { id: 'judge', fullName: '鍒ゆ柇棰�' },
-  { id: 'blank', fullName: '濉┖棰�' },
-  { id: 'essay', fullName: '闂瓟棰�' },
-];
+import { useBaseStore } from '#/store';
+import { TMS_DIC, labelOfDic, loadTmsDic, type TmsDicOpt } from '#/views/x/tms/shared/dic';
 
-export const DIFFICULTY_OPTIONS: { id: Difficulty; fullName: string }[] = [
-  { id: 'easy', fullName: '绠�鍗�' },
-  { id: 'normal', fullName: '涓�鑸�' },
-  { id: 'hard', fullName: '鍥伴毦' },
-];
+type StatusOpt = TmsDicOpt & { tip?: string; tipColor?: string };
 
-export const SOURCE_OPTIONS: { id: SourceType; fullName: string }[] = [
-  { id: 'self', fullName: '鑷富鍛介' },
-  { id: 'import', fullName: '瀵煎叆' },
-  { id: 'external', fullName: '澶栬喘' },
-];
+function withStatusTip(list: TmsDicOpt[]): StatusOpt[] {
+  return list.map((x) => {
+    const code = x.enCode || x.id;
+    if (code === 'open') return { ...x, tip: '瀛︾敓鍙互妯℃嫙', tipColor: 'green' };
+    if (code === 'closed') return { ...x, tip: '瀛︾敓涓嶈兘妯℃嫙', tipColor: 'red' };
+    return x;
+  });
+}
 
-export const STATUS_OPTIONS: { id: QuestionStatus; fullName: string; tip?: string; tipColor?: string }[] = [
-  { id: 'open', fullName: '寮�鏀�', tip: '瀛︾敓鍙互妯℃嫙', tipColor: 'green' },
-  { id: 'closed', fullName: '涓嶅紑鏀�', tip: '瀛︾敓涓嶈兘妯℃嫙', tipColor: 'red' },
-  { id: 'invalid', fullName: '搴熷純' },
-];
+export const QUESTION_TYPE_OPTIONS = ref<TmsDicOpt[]>([]);
+export const DIFFICULTY_OPTIONS = ref<TmsDicOpt[]>([]);
+export const SOURCE_OPTIONS = ref<TmsDicOpt[]>([]);
+export const STATUS_OPTIONS = ref<StatusOpt[]>([]);
+
+export async function loadQuestionDics() {
+  const baseStore = useBaseStore();
+  const [questionType, difficulty, source, status] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.questionType),
+    loadTmsDic(baseStore, TMS_DIC.difficulty),
+    loadTmsDic(baseStore, TMS_DIC.questionSource),
+    loadTmsDic(baseStore, TMS_DIC.openClosedInvalid),
+  ]);
+  QUESTION_TYPE_OPTIONS.value = questionType;
+  DIFFICULTY_OPTIONS.value = difficulty;
+  SOURCE_OPTIONS.value = source;
+  STATUS_OPTIONS.value = withStatusTip(status);
+}
 
 export const OPTION_LABELS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
 
 export function labelOfType(type?: string) {
-  return QUESTION_TYPE_OPTIONS.find((x) => x.id === type)?.fullName ?? type ?? '-';
+  return labelOfDic(QUESTION_TYPE_OPTIONS.value, type);
 }
 
 export function labelOfStatus(status?: string) {
-  return STATUS_OPTIONS.find((x) => x.id === status)?.fullName ?? status ?? '-';
+  return labelOfDic(STATUS_OPTIONS.value, status);
 }
 
 export function labelOfDifficulty(v?: string) {
-  return DIFFICULTY_OPTIONS.find((x) => x.id === v)?.fullName ?? v ?? '-';
+  return labelOfDic(DIFFICULTY_OPTIONS.value, v);
 }
 
 export function labelOfSource(v?: string) {
-  return SOURCE_OPTIONS.find((x) => x.id === v)?.fullName ?? v ?? '-';
+  return labelOfDic(SOURCE_OPTIONS.value, v);
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/question/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/question/index.vue
index f098095..0c97947 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/question/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/question/index.vue
@@ -11,14 +11,16 @@
 
 import { Modal } from 'ant-design-vue';
 
-import { deleteQuestion, getQuestionBanks, getQuestionList } from '#/api/x/tms/question';
+import { getQuestionBanks, getQuestionList, invalidateQuestion } from '#/api/x/tms/question';
 
 import {
   QUESTION_TYPE_OPTIONS,
   STATUS_OPTIONS,
   labelOfStatus,
   labelOfType,
+  loadQuestionDics,
 } from './constants';
+import { TMS_DIC_FIELD_NAMES } from '#/views/x/tms/shared/dic';
 
 defineOptions({ name: 'TmsQuestionList' });
 
@@ -53,17 +55,18 @@
   },
 ];
 
-const [registerTable, { reload, getForm }] = useVxeTable({
+const [registerTable, { reload, getForm, getSelectRows }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
     baseColProps: { span: 6 },
     compact: true,
     showAdvancedButton: true,
-    autoAdvancedLine: 1,
+    alwaysShowLines: 1,
+    autoAdvancedLine: 4,
     schemas: [
       {
         field: 'bankId',
@@ -84,7 +87,8 @@
           allowClear: true,
           showSearch: true,
           placeholder: '璇烽�夋嫨棰樺瀷',
-          options: QUESTION_TYPE_OPTIONS,
+          options: QUESTION_TYPE_OPTIONS.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -95,7 +99,8 @@
           allowClear: true,
           showSearch: true,
           placeholder: '璇烽�夋嫨鐘舵��',
-          options: STATUS_OPTIONS.filter((x) => x.id !== 'invalid'),
+          options: STATUS_OPTIONS.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
       {
@@ -114,8 +119,9 @@
       },
     ],
   },
+  rowSelection: { type: 'checkbox' },
   actionColumn: {
-    width: 100,
+    width: 180,
     title: '鎿嶄綔',
     dataIndex: 'action',
     fixed: 'right',
@@ -123,20 +129,55 @@
 });
 
 onMounted(async () => {
-  banks.value = (await getQuestionBanks()) || [];
-  getForm()?.updateSchema?.({
-    field: 'bankId',
-    componentProps: {
-      allowClear: true,
-      showSearch: true,
-      placeholder: '璇烽�夋嫨棰樺簱',
-      options: banks.value,
+  await loadQuestionDics();
+  try {
+    const list = await getQuestionBanks();
+    banks.value = Array.isArray(list) ? list : [];
+  } catch {
+    banks.value = [];
+  }
+  getForm()?.updateSchema?.([
+    {
+      field: 'bankId',
+      componentProps: {
+        allowClear: true,
+        showSearch: true,
+        placeholder: '璇烽�夋嫨棰樺簱',
+        options: banks.value,
+      },
     },
-  });
+    {
+      field: 'questionType',
+      componentProps: {
+        allowClear: true,
+        showSearch: true,
+        placeholder: '璇烽�夋嫨棰樺瀷',
+        options: QUESTION_TYPE_OPTIONS.value,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'bizStatus',
+      componentProps: {
+        allowClear: true,
+        showSearch: true,
+        placeholder: '璇烽�夋嫨鐘舵��',
+        options: STATUS_OPTIONS.value,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+  ]);
+  reload();
 });
 
 async function fetchList(params: Record<string, any>) {
-  return { data: await getQuestionList(params) };
+  const page = await getQuestionList(params);
+  return {
+    data: {
+      list: Array.isArray(page?.list) ? page.list : [],
+      pagination: page?.pagination || { total: 0 },
+    },
+  };
 }
 
 function stripHtml(html?: string) {
@@ -155,31 +196,55 @@
   router.push('/tms/question/create');
 }
 
-function handleEdit(record: QuestionEntity) {
-  router.push(`/tms/question/edit/${record.id}`);
-}
-
-function handleDelete(record: QuestionEntity) {
+function handleManage() {
+  const rows = (getSelectRows?.() || []) as QuestionEntity[];
+  if (!rows.length) {
+    createMessage.warning('璇峰厛鍕鹃�夎绠$悊鐨勮瘯棰�');
+    return;
+  }
+  if (rows.length === 1) {
+    handleEdit(rows[0]!);
+    return;
+  }
   Modal.confirm({
-    title: '纭鍒犻櫎',
-    content: `纭畾鍒犻櫎璇曢銆�${stripHtml(record.stem) || record.questionNo}銆嶅悧锛焋,
+    title: '鎵归噺搴熷純',
+    content: `宸插嬀閫� ${rows.length} 閬撹瘯棰橈紝纭畾鍏ㄩ儴搴熷純鍚楋紵`,
     onOk: async () => {
-      await deleteQuestion(record.id!);
-      createMessage.success('鍒犻櫎鎴愬姛');
+      for (const row of rows) {
+        if (row.id) await invalidateQuestion(row.id);
+      }
+      createMessage.success('搴熷純鎴愬姛');
       reload();
     },
   });
 }
 
+function handleDetail(record: QuestionEntity) {
+  router.push(`/tms/question/detail/${record.id}`);
+}
+
+function handleEdit(record: QuestionEntity) {
+  router.push(`/tms/question/edit/${record.id}`);
+}
+
+async function handleInvalidate(record: QuestionEntity) {
+  await invalidateQuestion(record.id!);
+  createMessage.success('搴熷純鎴愬姛');
+  reload();
+}
+
 function getTableActions(record: QuestionEntity): ActionItem[] {
   return [
-    { icon: 'icon-ym icon-ym-btn-edit', tooltip: '缂栬緫', onClick: handleEdit.bind(null, record) },
+    { label: '缂栬緫', onClick: handleEdit.bind(null, record) },
     {
-      icon: 'icon-ym icon-ym-btn-clearn',
-      tooltip: '鍒犻櫎',
+      label: '搴熷純',
       color: 'error',
-      onClick: handleDelete.bind(null, record),
+      modelConfirm: {
+        content: `纭畾搴熷純璇曢銆�${stripHtml(record.stem) || record.questionNo}銆嶅悧锛熷簾寮冨悗灏嗕粠鍒楄〃绉婚櫎銆俙,
+        onOk: handleInvalidate.bind(null, record),
+      },
     },
+    { label: '璇︽儏', onClick: handleDetail.bind(null, record) },
   ];
 }
 </script>
@@ -192,7 +257,7 @@
           <template #tableTitle>
             <a-space>
               <a-button type="primary" pre-icon="icon-ym icon-ym-btn-add" @click="handleCreate">鍒涘缓璇曢</a-button>
-              <a-button disabled>绠$悊璇曢</a-button>
+              <a-button @click="handleManage">绠$悊璇曢</a-button>
             </a-space>
           </template>
           <template #bizStatus="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/question/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/question/mock.ts
deleted file mode 100644
index 1472202..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/question/mock.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import type { QuestionBankOption, QuestionEntity, QuestionPageQuery } from './types';
-
-import dayjs from 'dayjs';
-
-const banks: QuestionBankOption[] = [
-  { id: 'bank1', fullName: '2026骞村畨鍏ㄧ敓浜х煡璇嗗煿璁瘯棰�1' },
-  { id: 'bank2', fullName: 'GMP鍩虹鍩硅棰樺簱' },
-  { id: 'bank3', fullName: 'SOP鎿嶄綔鑰冩牳棰樺簱' },
-];
-
-let seq = 23560;
-const store: QuestionEntity[] = [
-  {
-    id: 'q1',
-    questionNo: '23561',
-    bankId: 'bank1',
-    bankName: '2026骞村畨鍏ㄧ敓浜х煡璇嗗煿璁瘯棰�1',
-    questionType: 'judge',
-    difficulty: 'normal',
-    sourceType: 'self',
-    stem: '鐗圭浣滀笟浜哄憳蹇呴』鍙栧緱鏈夋晥璧勬牸璇佷功鍚庢柟鍙笂宀椾綔涓氥��',
-    analysis: '渚濇嵁瀹夊叏鐢熶骇鐩稿叧瑙勫畾銆�',
-    adminUserId: 'u1',
-    adminUserName: '娼樺織閫�',
-    bizStatus: 'open',
-    creatorTime: '2026-09-04 11:12:00',
-    options: [
-      { sortNo: 1, optionLabel: 'T', optionContent: '姝g‘', isCorrect: '1' },
-      { sortNo: 2, optionLabel: 'F', optionContent: '閿欒', isCorrect: '0' },
-    ],
-  },
-  {
-    id: 'q2',
-    questionNo: '23562',
-    bankId: 'bank1',
-    bankName: '2026骞村畨鍏ㄧ敓浜х煡璇嗗煿璁瘯棰�1',
-    questionType: 'multi',
-    difficulty: 'normal',
-    sourceType: 'self',
-    stem: '涓嬪垪鍝簺灞炰簬鐗圭璁惧锛�',
-    adminUserId: 'u1',
-    adminUserName: '娼樺織閫�',
-    bizStatus: 'open',
-    creatorTime: '2026-09-04 11:20:00',
-    options: [
-      { sortNo: 1, optionLabel: 'A', optionContent: '鐢垫', isCorrect: '1' },
-      { sortNo: 2, optionLabel: 'B', optionContent: '鍘嬪姏瀹瑰櫒', isCorrect: '1' },
-      { sortNo: 3, optionLabel: 'C', optionContent: '鏅�氬姙鍏', isCorrect: '0' },
-      { sortNo: 4, optionLabel: 'D', optionContent: '閿呯倝', isCorrect: '1' },
-    ],
-  },
-  {
-    id: 'q3',
-    questionNo: '23563',
-    bankId: 'bank2',
-    bankName: 'GMP鍩虹鍩硅棰樺簱',
-    questionType: 'single',
-    difficulty: 'easy',
-    sourceType: 'self',
-    stem: 'GMP 鐨勫叏绉版槸锛�',
-    adminUserId: 'u1',
-    adminUserName: '娼樺織閫�',
-    bizStatus: 'closed',
-    creatorTime: '2026-09-05 09:00:00',
-    options: [
-      { sortNo: 1, optionLabel: 'A', optionContent: '鑽搧鐢熶骇璐ㄩ噺绠$悊瑙勮寖', isCorrect: '1' },
-      { sortNo: 2, optionLabel: 'B', optionContent: '鑽搧缁忚惀璐ㄩ噺绠$悊瑙勮寖', isCorrect: '0' },
-      { sortNo: 3, optionLabel: 'C', optionContent: '瀹為獙瀹ょ鐞嗚鑼�', isCorrect: '0' },
-      { sortNo: 4, optionLabel: 'D', optionContent: '鏂囦欢绠$悊瑙勮寖', isCorrect: '0' },
-    ],
-  },
-];
-
-function delay<T>(data: T, ms = 200): Promise<T> {
-  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
-}
-
-export function mockGetBanks() {
-  return delay([...banks]);
-}
-
-export function mockGetAdmins() {
-  const map = new Map<string, string>();
-  store.forEach((x) => {
-    if (x.adminUserId && x.adminUserName) map.set(x.adminUserId, x.adminUserName);
-  });
-  if (!map.size) map.set('u1', '娼樺織閫�');
-  return delay([...map.entries()].map(([id, fullName]) => ({ id, fullName })));
-}
-
-export function mockQueryQuestions(params: QuestionPageQuery) {
-  let list = [...store];
-  if (params.bankId) list = list.filter((x) => x.bankId === params.bankId);
-  if (params.questionType) list = list.filter((x) => x.questionType === params.questionType);
-  if (params.bizStatus) list = list.filter((x) => x.bizStatus === params.bizStatus);
-  if (params.adminUserId) list = list.filter((x) => x.adminUserId === params.adminUserId);
-  if (params.keyword && params.keyword !== 'null') {
-    const kw = params.keyword.trim().toLowerCase();
-    list = list.filter((x) => (x.stem || '').toLowerCase().includes(kw) || (x.questionNo || '').includes(kw));
-  }
-  const currentPage = Number(params.currentPage || 1);
-  const pageSize = Number(params.pageSize || 20);
-  const start = (currentPage - 1) * pageSize;
-  return delay({
-    list: list.slice(start, start + pageSize),
-    pagination: { total: list.length, currentPage, pageSize },
-  });
-}
-
-export function mockGetQuestion(id: string) {
-  const row = store.find((x) => x.id === id);
-  if (!row) return Promise.reject(new Error('璇曢涓嶅瓨鍦�'));
-  return delay({ ...row, options: row.options ? [...row.options] : [] });
-}
-
-export function mockCreateQuestion(data: QuestionEntity) {
-  seq += 1;
-  const bank = banks.find((b) => b.id === data.bankId);
-  const row: QuestionEntity = {
-    ...data,
-    id: `q${Date.now()}`,
-    questionNo: String(seq),
-    bankName: bank?.fullName,
-    adminUserId: 'u1',
-    adminUserName: '褰撳墠鐢ㄦ埛',
-    creatorTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-  };
-  store.unshift(row);
-  return delay({ id: row.id, msg: '鍒涘缓鎴愬姛' });
-}
-
-export function mockUpdateQuestion(data: QuestionEntity) {
-  const idx = store.findIndex((x) => x.id === data.id);
-  if (idx < 0) return Promise.reject(new Error('璇曢涓嶅瓨鍦�'));
-  const bank = banks.find((b) => b.id === data.bankId);
-  store[idx] = {
-    ...store[idx],
-    ...data,
-    bankName: bank?.fullName || store[idx].bankName,
-  };
-  return delay({ msg: '鏇存柊鎴愬姛' });
-}
-
-export function mockDeleteQuestion(id: string) {
-  const idx = store.findIndex((x) => x.id === id);
-  if (idx >= 0) store.splice(idx, 1);
-  return delay({ msg: '鍒犻櫎鎴愬姛' });
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/record/Detail.vue
index 6f12ac0..7a96afa 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/Detail.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/Detail.vue
@@ -12,6 +12,17 @@
 } from 'ant-design-vue';
 
 import { getTmsRecordDetail } from '#/api/x/tms/record';
+import { getAuthMediaUrl } from '#/utils/jnpf';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import {
+  labelOfCategory,
+  labelOfEvalMode,
+  labelOfTrainMode,
+  loadRecordDics,
+} from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsRecordDetail' });
 
@@ -37,8 +48,9 @@
   },
 ];
 
-onMounted(() => {
-  loadData();
+onMounted(async () => {
+  await loadRecordDics();
+  await loadData();
 });
 
 async function loadData() {
@@ -83,10 +95,12 @@
 
 function openAttachment(url?: string) {
   if (!url || url === '#') {
-    createMessage.info('闄勪欢棰勮鑱旇皟鍚庣敓鏁�');
+    createMessage.info('鏆傛棤鍙瑙堥檮浠�');
     return;
   }
-  window.open(url, '_blank');
+  const apiIndex = url.indexOf('/api/');
+  const path = /^https?:\/\//i.test(url) && apiIndex >= 0 ? url.slice(apiIndex) : url;
+  window.open(getAuthMediaUrl(path, false), '_blank');
 }
 </script>
 
@@ -96,9 +110,11 @@
       <div class="jnpf-content-wrapper-content tms-record-detail-wrap">
         <a-spin :spinning="loading" class="detail-spin">
           <div v-if="detail" class="detail-inner">
-            <div class="detail-header">
-              <div class="text-base font-medium">鍩硅璁板綍</div>
-              <a-button type="primary" @click="goBack">杩斿洖</a-button>
+            <div class="tms-page-header">
+              <div class="tms-page-header__title">鍩硅璁板綍</div>
+              <div class="tms-page-header__actions">
+                <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+              </div>
             </div>
 
             <ADescriptions bordered :column="2" size="middle" class="detail-desc">
@@ -113,10 +129,10 @@
               <ADescriptionsItem label="鍩硅寮�濮嬫椂闂�">{{ detail.startTime }}</ADescriptionsItem>
               <ADescriptionsItem label="鍩硅缁撴潫鏃堕棿">{{ detail.endTime || '-' }}</ADescriptionsItem>
               <ADescriptionsItem label="鍩硅鍦扮偣" :span="2">{{ detail.placeName || '-' }}</ADescriptionsItem>
-              <ADescriptionsItem label="鍩硅鍒嗙被">{{ detail.category }}</ADescriptionsItem>
-              <ADescriptionsItem label="鍩硅鏂瑰紡">{{ detail.trainMode || detail.trainType || '-' }}</ADescriptionsItem>
+              <ADescriptionsItem label="鍩硅鍒嗙被">{{ labelOfCategory(detail.category) }}</ADescriptionsItem>
+              <ADescriptionsItem label="鍩硅鏂瑰紡">{{ labelOfTrainMode(detail.trainMode || detail.trainType) }}</ADescriptionsItem>
               <ADescriptionsItem label="鍩硅甯�">{{ detail.trainerName }}</ADescriptionsItem>
-              <ADescriptionsItem label="鍩硅鑰冩牳鏂瑰紡">{{ detail.evalMode }}</ADescriptionsItem>
+              <ADescriptionsItem label="鍩硅鑰冩牳鏂瑰紡">{{ labelOfEvalMode(detail.evalMode) }}</ADescriptionsItem>
               <ADescriptionsItem label="鍩硅瀵硅薄" :span="2">{{ detail.trainees || '-' }}</ADescriptionsItem>
               <ADescriptionsItem label="闄勪欢" :span="2">
                 <div v-if="detail.attachments?.length" class="attach-list">
@@ -189,15 +205,6 @@
 .detail-inner {
   padding: 16px 20px 24px;
   box-sizing: border-box;
-}
-
-.detail-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
-  padding-bottom: 12px;
-  border-bottom: 1px solid #f0f0f0;
 }
 
 .detail-desc {
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/Edit.vue b/apps/jnpf-web-apps-main/src/views/x/tms/record/Edit.vue
index 88f735d..462f330 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/Edit.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/Edit.vue
@@ -7,6 +7,7 @@
 import { useMessage } from '@jnpf/hooks';
 
 import { getTmsRecordDetail, updateTmsRecord } from '#/api/x/tms/record';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
 defineOptions({ name: 'TmsRecordEdit' });
 
@@ -91,8 +92,8 @@
                 <div class="mt-1 text-gray-400 text-sm">缂栧彿锛歿{ detail.recordNo }}</div>
               </div>
               <a-space>
-                <a-button @click="goBack">鍙栨秷</a-button>
-                <a-button type="primary" :loading="submitting" @click="handleSubmit">淇濆瓨</a-button>
+                <a-button @click="goBack">{{ TMS_BTN.cancel }}</a-button>
+                <a-button type="primary" :loading="submitting" @click="handleSubmit">{{ TMS_BTN.save }}</a-button>
               </a-space>
             </div>
 
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/Sign.vue b/apps/jnpf-web-apps-main/src/views/x/tms/record/Sign.vue
index ad80e52..0fe1c27 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/Sign.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/Sign.vue
@@ -1,14 +1,16 @@
 <script lang="ts" setup>
 import type { TmsRecordDetail } from './types';
 
-import { onMounted, reactive, ref } from 'vue';
+import { onMounted, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
 import { Table as ATable } from 'ant-design-vue';
-import dayjs from 'dayjs';
 
 import { getTmsRecordDetail } from '#/api/x/tms/record';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsRecordSign' });
 
@@ -17,14 +19,7 @@
 const { createMessage } = useMessage();
 
 const loading = ref(false);
-const submitting = ref(false);
 const detail = ref<TmsRecordDetail | null>(null);
-
-const form = reactive({
-  userName: '',
-  deptName: '',
-  signMode: '鍦ㄧ嚎绛惧埌',
-});
 
 const signColumns = [
   { title: '搴忓彿', key: 'index', width: 70, align: 'center', customRender: ({ index }: any) => index + 1 },
@@ -59,32 +54,6 @@
   }
 }
 
-function handleSubmit() {
-  if (!form.userName.trim()) {
-    createMessage.warning('璇疯緭鍏ョ鍒颁汉濮撳悕');
-    return;
-  }
-  if (!detail.value) return;
-  submitting.value = true;
-  try {
-    detail.value.signList = [
-      {
-        id: `s-${Date.now()}`,
-        userName: form.userName.trim(),
-        deptName: form.deptName.trim() || undefined,
-        signTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
-        signMode: form.signMode,
-      },
-      ...(detail.value.signList || []),
-    ];
-    form.userName = '';
-    form.deptName = '';
-    createMessage.success('绛惧埌鎴愬姛锛堟湰鍦� mock锛岃仈璋冨悗鍐欏叆鏈嶅姟绔級');
-  } finally {
-    submitting.value = false;
-  }
-}
-
 function goBack() {
   router.push('/tms/record');
 }
@@ -96,38 +65,23 @@
       <div class="jnpf-content-wrapper-content tms-record-wrap">
         <a-spin :spinning="loading" class="record-spin">
           <div v-if="detail" class="record-inner">
-            <div class="record-header">
+            <div class="tms-page-header">
               <div>
-                <div class="text-base font-medium">鍩硅绛惧埌</div>
-                <div class="mt-1 text-gray-400 text-sm">
-                  {{ detail.recordNo }} 路 {{ detail.subject }}
-                </div>
+                <div class="tms-page-header__title">鍩硅绛惧埌</div>
+                <div class="tms-page-header__sub">{{ detail.recordNo }} 路 {{ detail.subject }}</div>
               </div>
-              <a-button @click="goBack">杩斿洖</a-button>
+              <div class="tms-page-header__actions">
+                <a-button @click="goBack">{{ TMS_BTN.back }}</a-button>
+              </div>
             </div>
 
-            <a-form layout="inline" class="sign-form mb-4">
-              <a-form-item label="濮撳悕">
-                <a-input v-model:value="form.userName" placeholder="绛惧埌浜哄鍚�" class="!w-[140px]" allow-clear />
-              </a-form-item>
-              <a-form-item label="閮ㄩ棬">
-                <a-input v-model:value="form.deptName" placeholder="閮ㄩ棬锛堝彲閫夛級" class="!w-[160px]" allow-clear />
-              </a-form-item>
-              <a-form-item label="鏂瑰紡">
-                <a-select
-                  v-model:value="form.signMode"
-                  class="!w-[130px]"
-                  :options="[
-                    { label: '鍦ㄧ嚎绛惧埌', value: '鍦ㄧ嚎绛惧埌' },
-                    { label: 'APP鎵爜', value: 'APP鎵爜' },
-                    { label: '寰俊鎵爜', value: '寰俊鎵爜' },
-                  ]"
-                />
-              </a-form-item>
-              <a-form-item>
-                <a-button type="primary" :loading="submitting" @click="handleSubmit">纭绛惧埌</a-button>
-              </a-form-item>
-            </a-form>
+            <a-alert
+              class="tms-page-tip"
+              type="info"
+              show-icon
+              message="绠$悊绔ˉ绛�/鎵爜绛惧埌鍗冲皢鎺ュ叆"
+              description="褰撳墠璇峰鍛樺湪銆屼釜浜哄煿璁换鍔°�嶄腑瀹屾垚绛惧埌銆備笅鏂逛负宸插悓姝ョ殑绛惧埌鍚嶅崟锛堝彧璇伙級銆�"
+            />
 
             <div class="section-title">宸茬鍒颁汉鍛�</div>
             <ATable
@@ -173,24 +127,11 @@
   padding: 16px 20px 24px;
 }
 
-.record-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
-}
-
 .section-title {
   font-size: 15px;
   font-weight: 600;
   margin: 8px 0 12px;
   padding-left: 10px;
   border-left: 3px solid #1677ff;
-}
-
-.sign-form {
-  padding: 12px;
-  background: #fafafa;
-  border-radius: 6px;
 }
 </style>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/record/constants.ts
index 536fc03..d827fe2 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/constants.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/constants.ts
@@ -1,11 +1,33 @@
 import type { ArchiveStatus, RecordListMode } from './types';
 
-export const ARCHIVE_STATUS_OPTIONS: { id: ArchiveStatus; fullName: string }[] = [
-  { id: 'pending', fullName: '寰呭綊妗�' },
-  { id: 'ready', fullName: '鍙綊妗�' },
-  { id: 'archived', fullName: '宸插綊妗�' },
-  { id: 'invalid', fullName: '鏃犳晥' },
-];
+import { ref } from 'vue';
+
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
+
+export const ARCHIVE_STATUS_OPTIONS = ref<TmsDicOpt[]>([]);
+export const TASK_CATEGORY_OPTIONS = ref<TmsDicOpt[]>([]);
+export const TRAIN_MODE_OPTIONS = ref<TmsDicOpt[]>([]);
+export const EVAL_MODE_OPTIONS = ref<TmsDicOpt[]>([]);
+
+export async function loadRecordDics() {
+  const baseStore = useBaseStore();
+  const [archive, category, trainMode, evalMode] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.archiveStatus),
+    loadTmsDic(baseStore, TMS_DIC.taskCategory),
+    loadTmsDic(baseStore, TMS_DIC.trainMode),
+    loadTmsDic(baseStore, TMS_DIC.evalMode),
+  ]);
+  ARCHIVE_STATUS_OPTIONS.value = archive;
+  TASK_CATEGORY_OPTIONS.value = category;
+  TRAIN_MODE_OPTIONS.value = trainMode;
+  EVAL_MODE_OPTIONS.value = evalMode;
+}
 
 export const LIST_MODE_LABEL: Record<RecordListMode, string> = {
   main: '鍩硅璁板綍',
@@ -13,18 +35,29 @@
   archived: '宸插綊妗e垪琛�',
 };
 
-/** 鍚勫垪琛ㄩ〉璺敱 */
 export const RECORD_LIST_PATH: Record<RecordListMode, string> = {
   main: '/tms/record',
   ready: '/tms/record/ready',
   archived: '/tms/record/archived',
 };
 
-export function labelOfArchiveStatus(status?: ArchiveStatus) {
-  return ARCHIVE_STATUS_OPTIONS.find((x) => x.id === status)?.fullName || status || '-';
+export function labelOfArchiveStatus(status?: ArchiveStatus | string) {
+  return labelOfDic(ARCHIVE_STATUS_OPTIONS.value, status);
 }
 
-export function colorOfArchiveStatus(status?: ArchiveStatus) {
+export function labelOfCategory(v?: string) {
+  return labelOfDic(TASK_CATEGORY_OPTIONS.value, v);
+}
+
+export function labelOfTrainMode(v?: string) {
+  return labelOfDic(TRAIN_MODE_OPTIONS.value, v);
+}
+
+export function labelOfEvalMode(v?: string) {
+  return labelOfDic(EVAL_MODE_OPTIONS.value, v);
+}
+
+export function colorOfArchiveStatus(status?: ArchiveStatus | string) {
   if (status === 'ready') return '#1677ff';
   if (status === 'archived') return '#52c41a';
   if (status === 'invalid') return '#ff4d4f';
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/record/index.vue
index cdae71f..5f7ccde 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/index.vue
@@ -3,7 +3,7 @@
 
 import type { RecordListMode, TmsRecordListItem } from './types';
 
-import { computed, watch } from 'vue';
+import { computed, onMounted, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
@@ -12,8 +12,11 @@
 import { Modal } from 'ant-design-vue';
 
 import { archiveTmsRecords, deleteTmsRecords, getTmsRecordList } from '#/api/x/tms/record';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
-import { LIST_MODE_LABEL, RECORD_LIST_PATH } from './constants';
+import { LIST_MODE_LABEL, RECORD_LIST_PATH, labelOfCategory, labelOfEvalMode, labelOfTrainMode, loadRecordDics } from './constants';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsRecord' });
 
@@ -35,7 +38,12 @@
 
 const columns: BasicColumn[] = [
   { title: '缂栧彿', dataIndex: 'recordNo', width: 140 },
-  { title: '鍩硅鍒嗙被', dataIndex: 'category', width: 120 },
+  {
+    title: '鍩硅鍒嗙被',
+    dataIndex: 'category',
+    width: 120,
+    customRender: ({ record }) => labelOfCategory((record as TmsRecordListItem).category),
+  },
   {
     title: '鍩硅涓婚',
     dataIndex: 'subject',
@@ -43,9 +51,22 @@
     slots: { default: 'subject' },
   },
   { title: '鍩硅甯�', dataIndex: 'trainerName', width: 100 },
-  { title: '鍩硅绫诲瀷', dataIndex: 'trainType', width: 110 },
+  {
+    title: '鍩硅绫诲瀷',
+    dataIndex: 'trainType',
+    width: 110,
+    customRender: ({ record }) => {
+      const row = record as TmsRecordListItem;
+      return labelOfTrainMode(row.trainMode || row.trainType);
+    },
+  },
   { title: '鍩硅寮�濮嬫椂闂�', dataIndex: 'startTime', width: 160 },
-  { title: '鑰冩牳鏂瑰紡', dataIndex: 'evalMode', width: 110 },
+  {
+    title: '鑰冩牳鏂瑰紡',
+    dataIndex: 'evalMode',
+    width: 110,
+    customRender: ({ record }) => labelOfEvalMode((record as TmsRecordListItem).evalMode),
+  },
 ];
 
 const [registerTable, { reload, getSelectRows }] = useVxeTable({
@@ -59,6 +80,7 @@
     baseColProps: { span: 6 },
     compact: true,
     showAdvancedButton: true,
+    alwaysShowLines: 1,
     autoAdvancedLine: 1,
     schemas: [
       {
@@ -100,12 +122,20 @@
   () => reload(),
 );
 
+onMounted(() => {
+  loadRecordDics();
+});
+
 async function fetchList(params: Record<string, any>) {
+  const page = await getTmsRecordList({
+    ...params,
+    listMode: listMode.value,
+  });
   return {
-    data: await getTmsRecordList({
-      ...params,
-      listMode: listMode.value,
-    }),
+    data: {
+      list: Array.isArray(page?.list) ? page.list : [],
+      pagination: page?.pagination || { total: 0 },
+    },
   };
 }
 
@@ -139,19 +169,13 @@
   router.push(RECORD_LIST_PATH[mode]);
 }
 
-function handleQrcode() {
-  const row = requireOneRow();
-  if (!row) return;
-  createMessage.success(`宸茬敓鎴愩��${row.recordNo}銆嶇鍒颁簩缁寸爜锛堟帴鍙h仈璋冨悗鐢熸晥锛塦);
-}
-
-function handleAdd() {
-  createMessage.info('鏂板鍩硅璁板綍锛堣仈璋冧换鍔″彂甯冨悗鐢辩郴缁熻嚜鍔ㄧ敓鎴愶紝鎵嬪伐鏂板寰呭悗绔帴鍙o級');
-}
-
 function handleEdit() {
   const row = requireOneRow();
   if (!row) return;
+  if (row.archiveStatus === 'archived' || isArchivedView.value) {
+    createMessage.warning('宸插綊妗h褰曚笉鍙慨鏀�');
+    return;
+  }
   router.push(`/tms/record/edit/${row.id}`);
 }
 
@@ -164,10 +188,8 @@
   });
 }
 
-function handleSign() {
-  const row = requireOneRow();
-  if (!row) return;
-  router.push(`/tms/record/sign/${row.id}`);
+function handleAdminSignUnavailable() {
+  createMessage.info('绠$悊绔ˉ绛�/鎵爜绛惧埌鍗冲皢鎺ュ叆锛岃瀛﹀憳鍦ㄣ�屼釜浜哄煿璁换鍔°�嶄腑瀹屾垚绛惧埌');
 }
 
 function handleDelete() {
@@ -207,14 +229,7 @@
 }
 
 function getTableActions(record: TmsRecordListItem): ActionItem[] {
-  const actions: ActionItem[] = [{ label: '鏌ョ湅', onClick: handleView.bind(null, record) }];
-  if (isMainView.value) {
-    actions.push({
-      label: '绛惧埌',
-      onClick: () => router.push(`/tms/record/sign/${record.id}`),
-    });
-  }
-  return actions;
+  return [{ label: TMS_BTN.detail, onClick: handleView.bind(null, record) }];
 }
 </script>
 
@@ -226,33 +241,36 @@
           <template #tableTitle>
             <!-- 1. 鍩硅璁板綍 -->
             <a-space v-if="isMainView" wrap>
-              <a-button pre-icon="icon-ym icon-ym-generator-qrcode" @click="handleQrcode">鐢熸垚浜岀淮鐮�</a-button>
-              <a-button pre-icon="icon-ym icon-ym-btn-edit" @click="handleEdit">淇敼</a-button>
-              <a-button pre-icon="icon-ym icon-ym-btn-preview" @click="handleView()">鏌ョ湅</a-button>
-              <a-button pre-icon="icon-ym icon-ym-extend-form" @click="handleSign">绛惧埌</a-button>
+              <a-button pre-icon="icon-ym icon-ym-btn-edit" @click="handleEdit">{{ TMS_BTN.edit }}</a-button>
+              <a-button pre-icon="icon-ym icon-ym-btn-preview" @click="handleView()">{{ TMS_BTN.detail }}</a-button>
+              <a-button pre-icon="icon-ym icon-ym-extend-form" @click="handleAdminSignUnavailable">
+                {{ TMS_BTN.sign }}
+              </a-button>
               <a-button pre-icon="icon-ym icon-ym-file-text" @click="goList('ready')">鍙綊妗e垪琛�</a-button>
               <a-button pre-icon="icon-ym icon-ym-extend-folder" @click="goList('archived')">宸插綊妗e垪琛�</a-button>
-              <span class="text-gray-400 text-sm">褰撳墠锛歿{ listModeTitle }}</span>
+              <span class="tms-toolbar-hint">褰撳墠锛歿{ listModeTitle }} 路 璁板綍鐢变换鍔″彂甯冭嚜鍔ㄧ敓鎴�</span>
             </a-space>
 
             <!-- 2. 鍙綊妗e垪琛� -->
             <a-space v-else-if="isReadyView" wrap>
-              <a-button type="primary" pre-icon="icon-ym icon-ym-btn-add" @click="handleAdd">鏂板</a-button>
-              <a-button pre-icon="icon-ym icon-ym-btn-edit" @click="handleEdit">淇敼</a-button>
-              <a-button danger pre-icon="icon-ym icon-ym-btn-clearn" @click="handleDelete">鍒犻櫎</a-button>
-              <a-button pre-icon="icon-ym icon-ym-extend-folder" @click="handleArchive">褰掓。</a-button>
-              <a-button @click="goList('main')">杩斿洖</a-button>
+              <a-button pre-icon="icon-ym icon-ym-btn-edit" @click="handleEdit">{{ TMS_BTN.edit }}</a-button>
+              <a-button danger pre-icon="icon-ym icon-ym-btn-clearn" @click="handleDelete">
+                {{ TMS_BTN.delete }}
+              </a-button>
+              <a-button type="primary" pre-icon="icon-ym icon-ym-extend-folder" @click="handleArchive">
+                {{ TMS_BTN.archive }}
+              </a-button>
+              <a-button @click="goList('main')">{{ TMS_BTN.back }}</a-button>
               <a-button pre-icon="icon-ym icon-ym-file-text" @click="goList('archived')">宸插綊妗e垪琛�</a-button>
-              <span class="text-gray-400 text-sm">褰撳墠锛歿{ listModeTitle }}</span>
+              <span class="tms-toolbar-hint">褰撳墠锛歿{ listModeTitle }}</span>
             </a-space>
 
             <!-- 3. 宸插綊妗e垪琛� -->
             <a-space v-else wrap>
-              <a-button @click="handleView()">鏌ョ湅</a-button>
-              <a-button @click="handleEdit">淇敼</a-button>
-              <a-button @click="goList('main')">杩斿洖</a-button>
-              <a-button type="primary" @click="goList('ready')">鍙綊妗e垪琛�</a-button>
-              <span class="text-gray-400 text-sm">褰撳墠锛歿{ listModeTitle }}</span>
+              <a-button @click="handleView()">{{ TMS_BTN.detail }}</a-button>
+              <a-button @click="goList('main')">{{ TMS_BTN.back }}</a-button>
+              <a-button type="primary" ghost @click="goList('ready')">鍙綊妗e垪琛�</a-button>
+              <span class="tms-toolbar-hint">褰撳墠锛歿{ listModeTitle }} 路 宸插綊妗d笉鍙慨鏀�</span>
             </a-space>
           </template>
           <template #subject="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/record/types.ts b/apps/jnpf-web-apps-main/src/views/x/tms/record/types.ts
index 11b95dc..2cf1446 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/record/types.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/record/types.ts
@@ -77,5 +77,5 @@
   attachments?: TmsRecordAttachment[];
   /** 鍙傚姞浜哄憳鍚嶅崟 */
   participants?: TmsRecordParticipant[];
-  signList: TmsRecordSignItem[];
+  signList?: TmsRecordSignItem[];
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue
new file mode 100644
index 0000000..38873bb
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/Detail.vue
@@ -0,0 +1,214 @@
+<script lang="ts" setup>
+import type { SelfTestPaper, SelfTestQuestionItem } from './types';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { useMessage } from '@jnpf/hooks';
+import { useTabbarStore } from '@vben/stores';
+
+import { getSelfTestInfo } from '#/api/x/tms/selfTest';
+import { labelOfType } from '#/views/x/tms/question/constants';
+
+defineOptions({ name: 'TmsSelfTestDetail' });
+
+const route = useRoute();
+const router = useRouter();
+const { createMessage } = useMessage();
+const tabbarStore = useTabbarStore();
+
+const loading = ref(false);
+const detail = ref<SelfTestPaper | null>(null);
+/** all | wrong */
+const filterMode = ref<'all' | 'wrong'>('all');
+
+const wrongCount = computed(
+  () => detail.value?.questions?.filter((q) => q.isRight !== '1').length || 0,
+);
+
+const displayQuestions = computed(() => {
+  const list = detail.value?.questions || [];
+  if (filterMode.value === 'wrong') {
+    return list.filter((q) => q.isRight !== '1');
+  }
+  return list;
+});
+
+function stripHtml(html?: string) {
+  if (!html) return '';
+  return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+}
+
+function formatAnswer(ans?: string) {
+  if (!ans) return '鏈綔绛�';
+  return ans.split(',').filter(Boolean).join('銆�');
+}
+
+function optionClass(q: SelfTestQuestionItem, label: string) {
+  const correctSet = new Set((q.correctAnswer || '').split(',').filter(Boolean));
+  const userSet = new Set((q.userAnswer || '').split(',').filter(Boolean));
+  const isCorrectOpt = correctSet.has(label);
+  const isUserOpt = userSet.has(label);
+  if (isCorrectOpt) return 'opt-correct';
+  if (isUserOpt && !isCorrectOpt) return 'opt-wrong';
+  return '';
+}
+
+async function loadDetail() {
+  const id = String(route.params.id || '');
+  if (!id) {
+    router.replace('/tms/selfTest/records');
+    return;
+  }
+  loading.value = true;
+  try {
+    detail.value = await getSelfTestInfo(id);
+  } catch (e: any) {
+    createMessage.error(e?.message || '鍔犺浇妫�娴嬭鎯呭け璐�');
+    router.replace('/tms/selfTest/records');
+  } finally {
+    loading.value = false;
+  }
+}
+
+onMounted(() => {
+  tabbarStore.renderRouteView = true;
+  loadDetail();
+});
+
+function goBack() {
+  tabbarStore.renderRouteView = true;
+  router.push('/tms/selfTest/records');
+}
+</script>
+
+<template>
+  <div class="jnpf-content-wrapper">
+    <div class="jnpf-content-wrapper-center">
+      <div class="jnpf-content-wrapper-content tms-self-test-detail" v-loading="loading">
+        <div class="page-header" v-if="detail">
+          <div>
+            <div class="text-base font-medium">妫�娴嬭鎯� 路 {{ detail.bankName }}</div>
+            <div class="mt-1 text-gray-400 text-sm">
+              寰楀垎锛歿{ detail.correctCount ?? 0 }} / {{ detail.totalCount ?? detail.questions?.length ?? 0 }}
+              <span v-if="detail.scoreRate != null" class="ml-2">姝g‘鐜� {{ detail.scoreRate }}%</span>
+              <span v-if="detail.submitTime" class="ml-3">浜ゅ嵎鏃堕棿锛歿{ detail.submitTime }}</span>
+            </div>
+          </div>
+          <a-space>
+            <a-radio-group v-model:value="filterMode" button-style="solid" size="small">
+              <a-radio-button value="all">鍏ㄩ儴棰樼洰</a-radio-button>
+              <a-radio-button value="wrong">浠呴敊棰橈紙{{ wrongCount }}锛�</a-radio-button>
+            </a-radio-group>
+            <a-button @click="goBack">杩斿洖璁板綍</a-button>
+          </a-space>
+        </div>
+
+        <a-empty v-if="!loading && detail && !displayQuestions.length" description="鏆傛棤棰樼洰" />
+
+        <div v-for="(q, idx) in displayQuestions" :key="`${q.id}_${idx}`" class="question-card">
+          <div class="q-title">
+            <span class="q-index">{{ q.sortNo || idx + 1 }}.</span>
+            <span class="q-type">{{ labelOfType(q.questionType) }}</span>
+            <a-tag :color="q.isRight === '1' ? 'success' : 'error'" class="!ml-2">
+              {{ q.isRight === '1' ? '姝g‘' : '閿欒' }}
+            </a-tag>
+          </div>
+          <div class="q-stem">{{ stripHtml(q.stem) }}</div>
+          <div class="q-options">
+            <div
+              v-for="opt in q.options"
+              :key="opt.optionLabel"
+              class="q-option"
+              :class="optionClass(q, opt.optionLabel)"
+            >
+              {{ opt.optionLabel }}. {{ opt.optionContent }}
+            </div>
+          </div>
+          <div class="q-answer">
+            <span>浣犵殑绛旀锛歿{ formatAnswer(q.userAnswer) }}</span>
+            <span class="ml-4">姝g‘绛旀锛歿{ formatAnswer(q.correctAnswer) }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.tms-self-test-detail {
+  background: #fff;
+  padding: 20px 24px;
+  height: 100%;
+  overflow: auto;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 16px;
+  margin-bottom: 20px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.question-card {
+  padding: 16px 0;
+  border-bottom: 1px solid #f5f5f5;
+}
+
+.q-title {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.q-index {
+  font-weight: 600;
+  margin-right: 6px;
+}
+
+.q-type {
+  color: rgba(0, 0, 0, 0.45);
+  font-size: 13px;
+}
+
+.q-stem {
+  font-size: 15px;
+  line-height: 1.7;
+  margin-bottom: 12px;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-options {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  margin-bottom: 10px;
+}
+
+.q-option {
+  padding: 6px 10px;
+  border-radius: 4px;
+  background: #fafafa;
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.q-option.opt-correct {
+  background: #f6ffed;
+  color: #389e0d;
+  border: 1px solid #b7eb8f;
+}
+
+.q-option.opt-wrong {
+  background: #fff2f0;
+  color: #cf1322;
+  border: 1px solid #ffccc7;
+}
+
+.q-answer {
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.55);
+}
+</style>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
index 6911352..9541d22 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
@@ -1,45 +1,138 @@
 <script lang="ts" setup>
-import type { SelfTestPaper, SelfTestQuestionItem } from './types';
+import type { SelfTestPaper, SelfTestQuestionItem, SelfTestSubmitResult } from './types';
 
-import { computed, onMounted, reactive, ref } from 'vue';
-import { useRouter } from 'vue-router';
+import {
+  computed,
+  nextTick,
+  onDeactivated,
+  onMounted,
+  onUnmounted,
+  reactive,
+  ref,
+  watch,
+} from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
+import { useTabbarStore } from '@vben/stores';
+import { AppstoreOutlined } from '@ant-design/icons-vue';
 
-import { labelOfType } from '#/views/x/tms/question/constants';
+import { saveSelfTest, submitSelfTest } from '#/api/x/tms/selfTest';
+import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
 
 defineOptions({ name: 'TmsSelfTestExam' });
 
+const route = useRoute();
 const router = useRouter();
 const { createMessage } = useMessage();
+const tabbarStore = useTabbarStore();
 
 const paper = ref<SelfTestPaper | null>(null);
 const currentIndex = ref(0);
 /** questionId -> answer: single/judge 瀛� optionLabel锛沵ulti 瀛� label[] */
 const answers = reactive<Record<string, string | string[]>>({});
 const submitted = ref(false);
+const submitting = ref(false);
+const saving = ref(false);
+const answersReady = ref(false);
 const scoreText = ref('');
+const resultModalOpen = ref(false);
+const submitResult = ref<SelfTestSubmitResult | null>(null);
+const resultFilter = ref<'all' | 'wrong'>('all');
+const sheetOpen = ref(false);
+let saveTimer: ReturnType<typeof setTimeout> | null = null;
 
 const current = computed(() => paper.value?.questions?.[currentIndex.value]);
 const total = computed(() => paper.value?.questions?.length || 0);
 
-onMounted(() => {
+const wrongCount = computed(
+  () => paper.value?.questions?.filter((q) => !isCorrect(q)).length || 0,
+);
+
+const rightCount = computed(() => submitResult.value?.correctCount ?? 0);
+
+const resultQuestions = computed(() => {
+  const list = paper.value?.questions || [];
+  if (resultFilter.value === 'wrong') {
+    return list.filter((q) => !isCorrect(q));
+  }
+  return list;
+});
+const answeredCount = computed(
+  () => (paper.value?.questions || []).filter((q) => isAnswered(q)).length,
+);
+
+onMounted(async () => {
+  // 鑻ユ鍓� refreshTab 涓柇瀵艰嚧鍏ㄥ眬鍐呭鍖轰笉娓叉煋锛岃繖閲屽己鍒舵仮澶�
+  tabbarStore.renderRouteView = true;
+  await loadQuestionDics();
+
   const raw = sessionStorage.getItem('tms_self_test_paper');
   if (!raw) {
     createMessage.warning('璇峰厛璁剧疆鎶介鏉′欢');
-    router.replace('/tms/selfTest');
+    goBack();
     return;
   }
   try {
     paper.value = JSON.parse(raw);
+    // 缁х画鑰冭瘯锛氭仮澶嶅凡淇濆瓨鐨勪綔绛�
+    const ansRaw = sessionStorage.getItem('tms_self_test_answers');
+    if (ansRaw) {
+      try {
+        const saved = JSON.parse(ansRaw) as Record<string, string | string[]>;
+        Object.keys(saved || {}).forEach((qid) => {
+          answers[qid] = saved[qid];
+        });
+      } catch {
+        // ignore
+      }
+      sessionStorage.removeItem('tms_self_test_answers');
+    }
   } catch {
-    router.replace('/tms/selfTest');
+    goBack();
+    return;
   }
+  answersReady.value = true;
 });
+
+onDeactivated(() => {
+  resultModalOpen.value = false;
+  sheetOpen.value = false;
+});
+
+onUnmounted(() => {
+  if (saveTimer) {
+    clearTimeout(saveTimer);
+    saveTimer = null;
+  }
+  resultModalOpen.value = false;
+  sheetOpen.value = false;
+  // 鍐嶆纭繚绂诲紑鏈〉鍚庡唴瀹瑰尯鍙覆鏌�
+  tabbarStore.renderRouteView = true;
+});
+
+/** 浣滅瓟鍙樻洿鍚庤嚜鍔ㄦ殏瀛橈紝鏂逛究涓�旂寮�鍐嶇户缁� */
+watch(
+  answers,
+  () => {
+    if (!answersReady.value || submitted.value || !paper.value) return;
+    if (saveTimer) clearTimeout(saveTimer);
+    saveTimer = setTimeout(() => {
+      void persistAnswers(false);
+    }, 1500);
+  },
+  { deep: true },
+);
 
 function stripHtml(html?: string) {
   if (!html) return '';
   return html.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').trim();
+}
+
+function isAnswered(q: SelfTestQuestionItem) {
+  const val = answers[q.id];
+  if (Array.isArray(val)) return val.length > 0;
+  return String(val ?? '').trim().length > 0;
 }
 
 function goPrev() {
@@ -50,13 +143,111 @@
   if (currentIndex.value < total.value - 1) currentIndex.value += 1;
 }
 
-function goBack() {
-  router.push('/tms/selfTest');
+function goToQuestion(index: number) {
+  if (index < 0 || index >= total.value) {
+    sheetOpen.value = false;
+    return;
+  }
+  currentIndex.value = index;
+  sheetOpen.value = false;
+}
+
+function openSheet() {
+  sheetOpen.value = true;
+}
+
+function getSettingsPath() {
+  return String(route.meta.currentActiveMenu || '/tms/selfTest');
+}
+
+/**
+ * 涓庡湪绾胯�冭瘯涓�鑷达細鎸� path 璺宠浆锛屼笉璋冪敤 closeCurrentTab銆�
+ * closeCurrentTab 鍐呴儴鎸� route.name 璺宠浆锛屽姩鎬佽彍鍗曞満鏅鏄撹烦鍒扮┖椤靛苟褰卞搷鍏ㄥ眬鍐呭鍖恒��
+ */
+async function goBack() {
+  if (!submitted.value && paper.value) {
+    if (saveTimer) {
+      clearTimeout(saveTimer);
+      saveTimer = null;
+    }
+    await persistAnswers(false);
+  }
+  resultModalOpen.value = false;
+  tabbarStore.renderRouteView = true;
+  router.push(getSettingsPath());
+}
+
+async function persistAnswers(showTip: boolean) {
+  if (!paper.value || submitted.value || saving.value) return;
+  const payload = { ...answers };
+  if (!Object.keys(payload).length) {
+    if (showTip) createMessage.info('鏆傛棤浣滅瓟鍙繚瀛�');
+    return;
+  }
+  saving.value = true;
+  try {
+    await saveSelfTest(paper.value.paperId, { answers: payload });
+    if (showTip) createMessage.success('宸叉殏瀛橈紝鍙◢鍚庣户缁�冭瘯');
+  } catch (e: any) {
+    if (showTip) createMessage.error(e?.message || '鏆傚瓨澶辫触');
+  } finally {
+    saving.value = false;
+  }
+}
+
+async function handleSave() {
+  if (saveTimer) {
+    clearTimeout(saveTimer);
+    saveTimer = null;
+  }
+  await persistAnswers(true);
+}
+
+function closeResultModal() {
+  resultModalOpen.value = false;
+}
+
+async function openResultModal() {
+  if (!submitResult.value && paper.value) {
+    const qs = paper.value.questions || [];
+    let correct = 0;
+    qs.forEach((q) => {
+      if (isCorrect(q)) correct += 1;
+    });
+    submitResult.value = {
+      paperId: paper.value.paperId,
+      totalCount: qs.length,
+      correctCount: correct,
+      scoreRate: qs.length ? Math.round((correct * 10000) / qs.length) / 100 : 0,
+    };
+  }
+  resultFilter.value = 'all';
+  await nextTick();
+  resultModalOpen.value = true;
+}
+
+function goRecords() {
+  resultModalOpen.value = false;
+  tabbarStore.renderRouteView = true;
+  router.push('/tms/selfTest/records');
+}
+
+function correctLabelsOf(q: SelfTestQuestionItem) {
+  return q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
+}
+
+function userAnswerText(q: SelfTestQuestionItem) {
+  const ans = answers[q.id];
+  if (q.questionType === 'multi') {
+    const selected = Array.isArray(ans) ? [...ans].sort() : [];
+    return selected.length ? selected.join('銆�') : '鏈綔绛�';
+  }
+  return String(ans || '') || '鏈綔绛�';
 }
 
 function isCorrect(q: SelfTestQuestionItem): boolean {
   const ans = answers[q.id];
-  const correctLabels = q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
+  const correctLabels = correctLabelsOf(q);
   if (q.questionType === 'multi') {
     const selected = Array.isArray(ans) ? [...ans].sort() : [];
     return selected.join(',') === [...correctLabels].sort().join(',');
@@ -64,16 +255,41 @@
   return String(ans || '') === String(correctLabels[0] || '');
 }
 
-function handleSubmit() {
-  if (!paper.value) return;
-  const qs = paper.value.questions;
-  let right = 0;
-  qs.forEach((q) => {
-    if (isCorrect(q)) right += 1;
-  });
-  submitted.value = true;
-  scoreText.value = `${right} / ${qs.length}`;
-  createMessage.success(`妫�娴嬪畬鎴愶紝姝g‘ ${right} 棰橈紝鍏� ${qs.length} 棰榒);
+function optionClass(q: SelfTestQuestionItem, label: string) {
+  const correctSet = new Set(correctLabelsOf(q));
+  const ans = answers[q.id];
+  const userSet = new Set(
+    q.questionType === 'multi'
+      ? Array.isArray(ans)
+        ? ans
+        : []
+      : ans
+        ? [String(ans)]
+        : [],
+  );
+  if (correctSet.has(label)) return 'opt-correct';
+  if (userSet.has(label) && !correctSet.has(label)) return 'opt-wrong';
+  return '';
+}
+
+async function handleSubmit() {
+  if (!paper.value || submitting.value) return;
+  if (saveTimer) {
+    clearTimeout(saveTimer);
+    saveTimer = null;
+  }
+  submitting.value = true;
+  try {
+    const result = await submitSelfTest(paper.value.paperId, { answers: { ...answers } });
+    submitted.value = true;
+    submitResult.value = result;
+    scoreText.value = `${result.correctCount} / ${result.totalCount}`;
+    await openResultModal();
+  } catch (e: any) {
+    createMessage.error(e?.message || '浜ゅ嵎澶辫触');
+  } finally {
+    submitting.value = false;
+  }
 }
 </script>
 
@@ -90,18 +306,36 @@
             </div>
           </div>
           <a-space>
+            <a-button v-if="submitted" type="link" @click="openResultModal">鏌ョ湅鏈缁撴灉</a-button>
+            <a-button v-if="submitted" type="link" @click="goRecords">妫�娴嬭褰�</a-button>
             <a-button @click="goBack">杩斿洖璁剧疆</a-button>
-            <a-button type="primary" :disabled="submitted" @click="handleSubmit">浜ゅ嵎</a-button>
+            <a-button
+              v-if="!submitted"
+              :loading="saving"
+              :disabled="submitting"
+              @click="handleSave"
+            >
+              鏆傚瓨
+            </a-button>
+            <a-button type="primary" :disabled="submitted" :loading="submitting" @click="handleSubmit">
+              浜ゅ嵎
+            </a-button>
           </a-space>
         </div>
 
         <div v-if="current" class="tms-exam-body">
-          <div class="mb-3 text-sm text-gray-500">
-            {{ labelOfType(current.questionType) }}
+          <div class="q-meta mb-3">
+            <div class="text-sm text-gray-500">
+              {{ labelOfType(current.questionType) }}
+            </div>
+            <a-tooltip title="绛旈鍗�">
+              <button type="button" class="sheet-icon-btn" aria-label="绛旈鍗�" @click="openSheet">
+                <AppstoreOutlined />
+              </button>
+            </a-tooltip>
           </div>
           <div class="stem mb-4">{{ stripHtml(current.stem) }}</div>
 
-          <!-- 鍗曢�� / 鍒ゆ柇 -->
           <a-radio-group
             v-if="current.questionType === 'single' || current.questionType === 'judge'"
             v-model:value="answers[current.id]"
@@ -113,7 +347,6 @@
             </a-radio>
           </a-radio-group>
 
-          <!-- 澶氶�� -->
           <a-checkbox-group
             v-else-if="current.questionType === 'multi'"
             v-model:value="answers[current.id]"
@@ -127,14 +360,141 @@
 
           <div v-if="submitted" class="mt-4 text-sm" :class="isCorrect(current) ? 'text-green-600' : 'text-red-500'">
             {{ isCorrect(current) ? '鍥炵瓟姝g‘' : '鍥炵瓟閿欒' }}
-            路 姝g‘绛旀锛�
-            {{ current.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel).join('銆�') }}
+            路 姝g‘绛旀锛歿{ correctLabelsOf(current).join('銆�') }}
           </div>
         </div>
 
         <div class="tms-exam-footer">
           <a-button :disabled="currentIndex <= 0" @click="goPrev">涓婁竴棰�</a-button>
           <a-button :disabled="currentIndex >= total - 1" @click="goNext">涓嬩竴棰�</a-button>
+        </div>
+      </div>
+    </div>
+
+    <div v-if="sheetOpen && paper" class="tms-sheet-overlay" @click.self="sheetOpen = false">
+      <div class="tms-sheet-dialog" role="dialog" aria-modal="true">
+        <div class="sheet-head">
+          <div>
+            <div class="sheet-title">绛旈鍗�</div>
+            <div class="sheet-sub">宸茬瓟 {{ answeredCount }} / {{ total }}锛岀偣鍑婚鍙峰彲鍒囨崲</div>
+          </div>
+          <button type="button" class="tms-result-close" aria-label="鍏抽棴" @click="sheetOpen = false">脳</button>
+        </div>
+        <div class="sheet-legend">
+          <span class="legend-chip current">褰撳墠</span>
+          <span class="legend-chip answered">宸茬瓟</span>
+          <span class="legend-chip unanswered">鏈瓟</span>
+        </div>
+        <div class="sheet-grid">
+          <button
+            v-for="(q, idx) in paper.questions"
+            :key="q.id"
+            type="button"
+            class="sheet-item"
+            :class="{
+              current: idx === currentIndex,
+              answered: isAnswered(q),
+              unanswered: !isAnswered(q),
+            }"
+            @click="goToQuestion(idx)"
+          >
+            {{ idx + 1 }}
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 涓嶇敤 Teleport 鍒� body锛岄伩鍏� KeepAlive 鍦烘櫙涓嬮伄缃╂畫鐣欏鑷村叏灞�鏃犳硶鐐瑰嚮/鐧藉睆 -->
+    <div
+      v-if="resultModalOpen && submitResult && paper"
+      class="tms-result-overlay"
+      @click.self="closeResultModal"
+    >
+      <div class="tms-result-dialog" role="dialog" aria-modal="true">
+        <button type="button" class="tms-result-close" aria-label="鍏抽棴" @click="closeResultModal">脳</button>
+
+        <div class="result-hero">
+          <div class="hero-title">鏈妫�娴嬬粨鏋�</div>
+          <div class="hero-bank">{{ paper.bankName }}</div>
+          <div class="hero-stats">
+            <div class="stat-item">
+              <div class="stat-value">{{ rightCount }}</div>
+              <div class="stat-label">绛斿</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">{{ wrongCount }}</div>
+              <div class="stat-label">绛旈敊</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">{{ submitResult.scoreRate }}%</div>
+              <div class="stat-label">姝g‘鐜�</div>
+            </div>
+            <div class="stat-divider" />
+            <div class="stat-item">
+              <div class="stat-value">{{ submitResult.correctCount }}/{{ submitResult.totalCount }}</div>
+              <div class="stat-label">寰楀垎</div>
+            </div>
+          </div>
+        </div>
+
+        <div class="result-toolbar">
+          <a-radio-group v-model:value="resultFilter" button-style="solid" size="small">
+            <a-radio-button value="all">鍏ㄩ儴棰樼洰锛坽{ total }}锛�</a-radio-button>
+            <a-radio-button value="wrong">浠呴敊棰橈紙{{ wrongCount }}锛�</a-radio-button>
+          </a-radio-group>
+          <div class="legend">
+            <span class="legend-item correct">姝g‘绛旀</span>
+            <span class="legend-item wrong">浣犵殑閿欒閫夐」</span>
+          </div>
+        </div>
+
+        <div class="result-list">
+          <a-empty v-if="!resultQuestions.length" description="鏆傛棤棰樼洰" />
+          <div
+            v-for="(q, idx) in resultQuestions"
+            :key="q.id"
+            class="question-card"
+            :class="isCorrect(q) ? 'is-right' : 'is-wrong'"
+          >
+            <div class="q-head">
+              <div class="q-head-left">
+                <span class="q-index">绗� {{ idx + 1 }} 棰�</span>
+                <span class="q-type">{{ labelOfType(q.questionType) }}</span>
+              </div>
+              <a-tag :color="isCorrect(q) ? 'success' : 'error'">
+                {{ isCorrect(q) ? '鍥炵瓟姝g‘' : '鍥炵瓟閿欒' }}
+              </a-tag>
+            </div>
+            <div class="q-stem">{{ stripHtml(q.stem) }}</div>
+            <div class="q-options">
+              <div
+                v-for="opt in q.options"
+                :key="opt.optionLabel"
+                class="q-option"
+                :class="optionClass(q, opt.optionLabel)"
+              >
+                <span class="opt-label">{{ opt.optionLabel }}</span>
+                <span class="opt-content">{{ opt.optionContent }}</span>
+              </div>
+            </div>
+            <div class="q-answer-bar">
+              <div>
+                <span class="ans-label">浣犵殑绛旀</span>
+                <span :class="isCorrect(q) ? 'ans-ok' : 'ans-bad'">{{ userAnswerText(q) }}</span>
+              </div>
+              <div>
+                <span class="ans-label">姝g‘绛旀</span>
+                <span class="ans-ok">{{ correctLabelsOf(q).join('銆�') || '-' }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="result-footer">
+          <a-button @click="closeResultModal">鍏抽棴</a-button>
+          <a-button type="primary" @click="goRecords">鎴戠殑妫�娴嬭褰�</a-button>
         </div>
       </div>
     </div>
@@ -167,6 +527,33 @@
   overflow: auto;
 }
 
+.q-meta {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+}
+
+.sheet-icon-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  border: 1px solid #d9d9d9;
+  border-radius: 6px;
+  background: #fff;
+  color: #1677ff;
+  font-size: 16px;
+  cursor: pointer;
+  flex-shrink: 0;
+}
+
+.sheet-icon-btn:hover {
+  border-color: #1677ff;
+  background: #e6f4ff;
+}
+
 .stem {
   font-size: 15px;
   line-height: 1.7;
@@ -181,4 +568,388 @@
   padding-top: 16px;
   border-top: 1px solid #f0f0f0;
 }
+
+.tms-sheet-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 1900;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  background: rgba(0, 0, 0, 0.45);
+}
+
+.tms-sheet-dialog {
+  width: min(520px, 100%);
+  max-height: min(70vh, 640px);
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+}
+
+.sheet-head {
+  position: relative;
+  flex-shrink: 0;
+  padding: 18px 48px 12px 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.sheet-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.sheet-sub {
+  margin-top: 4px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.sheet-legend {
+  flex-shrink: 0;
+  display: flex;
+  gap: 16px;
+  padding: 12px 20px 0;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.55);
+}
+
+.legend-chip::before {
+  content: '';
+  display: inline-block;
+  width: 12px;
+  height: 12px;
+  margin-right: 6px;
+  border-radius: 3px;
+  vertical-align: -2px;
+}
+
+.legend-chip.current::before {
+  background: #fff;
+  border: 2px solid #1677ff;
+  box-sizing: border-box;
+}
+
+.legend-chip.answered::before {
+  background: #1677ff;
+}
+
+.legend-chip.unanswered::before {
+  background: #f5f5f5;
+  border: 1px solid #d9d9d9;
+  box-sizing: border-box;
+}
+
+.sheet-grid {
+  flex: 1;
+  min-height: 0;
+  overflow: auto;
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
+  gap: 10px;
+  padding: 16px 20px 20px;
+}
+
+.sheet-item {
+  height: 40px;
+  border-radius: 8px;
+  border: 1px solid #d9d9d9;
+  background: #fafafa;
+  color: rgba(0, 0, 0, 0.65);
+  font-size: 14px;
+  font-weight: 600;
+  cursor: pointer;
+  transition: all 0.15s ease;
+}
+
+.sheet-item.answered {
+  background: #1677ff;
+  border-color: #1677ff;
+  color: #fff;
+}
+
+.sheet-item.unanswered {
+  background: #f5f5f5;
+  border-color: #d9d9d9;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.sheet-item.current {
+  box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.35);
+}
+
+.sheet-item.current.unanswered {
+  border-color: #1677ff;
+  color: #1677ff;
+  background: #e6f4ff;
+}
+
+.sheet-item:hover {
+  filter: brightness(0.97);
+}
+
+.tms-result-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 2000;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 24px;
+  background: rgba(0, 0, 0, 0.45);
+}
+
+.tms-result-dialog {
+  position: relative;
+  width: min(720px, 100%);
+  max-height: min(80vh, 780px);
+  display: flex;
+  flex-direction: column;
+  background: #fff;
+  border-radius: 12px;
+  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+}
+
+.tms-result-close {
+  position: absolute;
+  top: 10px;
+  right: 12px;
+  z-index: 2;
+  width: 32px;
+  height: 32px;
+  border: none;
+  border-radius: 6px;
+  background: transparent;
+  color: rgba(0, 0, 0, 0.45);
+  cursor: pointer;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22px;
+  line-height: 1;
+}
+
+.tms-result-close:hover {
+  background: rgba(0, 0, 0, 0.06);
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.result-hero {
+  flex-shrink: 0;
+  padding: 24px 28px 20px;
+  background: linear-gradient(135deg, #f0f7ff 0%, #f8fbff 55%, #ffffff 100%);
+  border-bottom: 1px solid #eef2f7;
+}
+
+.hero-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+  padding-right: 36px;
+}
+
+.hero-bank {
+  margin-top: 4px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.hero-stats {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 18px;
+  padding: 14px 8px;
+  background: #fff;
+  border: 1px solid #e8eef5;
+  border-radius: 10px;
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 22px;
+  font-weight: 700;
+  line-height: 1.2;
+  color: #1677ff;
+}
+
+.stat-label {
+  margin-top: 4px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.stat-divider {
+  width: 1px;
+  height: 28px;
+  background: #eef2f7;
+}
+
+.result-toolbar {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  flex-wrap: wrap;
+  padding: 12px 28px;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.legend {
+  display: flex;
+  gap: 12px;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.legend-item::before {
+  content: '';
+  display: inline-block;
+  width: 10px;
+  height: 10px;
+  margin-right: 6px;
+  border-radius: 2px;
+  vertical-align: -1px;
+}
+
+.legend-item.correct::before {
+  background: #b7eb8f;
+}
+
+.legend-item.wrong::before {
+  background: #ffa39e;
+}
+
+.result-list {
+  flex: 1;
+  min-height: 160px;
+  overflow: auto;
+  padding: 8px 28px 4px;
+}
+
+.question-card {
+  margin-bottom: 12px;
+  padding: 14px 16px;
+  border: 1px solid #f0f0f0;
+  border-radius: 10px;
+  background: #fff;
+}
+
+.question-card.is-wrong {
+  border-color: #ffccc7;
+  background: #fffafa;
+}
+
+.question-card.is-right {
+  border-color: #d9f7be;
+  background: #fcfffb;
+}
+
+.q-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.q-head-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.q-index {
+  font-weight: 600;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-type {
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.q-stem {
+  margin-bottom: 10px;
+  font-size: 14px;
+  line-height: 1.7;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.q-options {
+  display: grid;
+  gap: 6px;
+  margin-bottom: 10px;
+}
+
+.q-option {
+  display: flex;
+  gap: 8px;
+  padding: 8px 10px;
+  border-radius: 6px;
+  background: #fafafa;
+  border: 1px solid transparent;
+  color: rgba(0, 0, 0, 0.75);
+}
+
+.q-option .opt-label {
+  min-width: 18px;
+  font-weight: 600;
+}
+
+.q-option.opt-correct {
+  background: #f6ffed;
+  color: #389e0d;
+  border-color: #b7eb8f;
+}
+
+.q-option.opt-wrong {
+  background: #fff2f0;
+  color: #cf1322;
+  border-color: #ffccc7;
+}
+
+.q-answer-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px 28px;
+  padding-top: 8px;
+  border-top: 1px dashed #f0f0f0;
+  font-size: 13px;
+}
+
+.ans-label {
+  margin-right: 8px;
+  color: rgba(0, 0, 0, 0.45);
+}
+
+.ans-ok {
+  color: #389e0d;
+  font-weight: 600;
+}
+
+.ans-bad {
+  color: #cf1322;
+  font-weight: 600;
+}
+
+.result-footer {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 14px 28px 18px;
+  border-top: 1px solid #f0f0f0;
+  background: #fafafa;
+}
 </style>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/index.vue
index 9912477..682c37e 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/index.vue
@@ -2,10 +2,11 @@
 import type { QuestionBankOption } from '#/views/x/tms/question/types';
 import type { SelfTestFormModel } from './types';
 
-import { computed, onMounted, reactive, ref } from 'vue';
+import { computed, onActivated, onMounted, reactive, ref } from 'vue';
 import { useRouter } from 'vue-router';
 
 import { useMessage } from '@jnpf/hooks';
+import { useTabbarStore } from '@vben/stores';
 
 import { getSelfTestBanks, startSelfTest } from '#/api/x/tms/selfTest';
 
@@ -20,10 +21,12 @@
 
 const router = useRouter();
 const { createMessage } = useMessage();
+const tabbarStore = useTabbarStore();
 
 const banks = ref<QuestionBankOption[]>([]);
 const loading = ref(false);
 const formRef = ref();
+const pageReady = ref(false);
 
 function defaultSetting() {
   return { count: 0, difficulty: 'normal' as SelfTestDifficulty };
@@ -44,10 +47,28 @@
   () => Number(dataForm.single.count || 0) + Number(dataForm.multi.count || 0) + Number(dataForm.judge.count || 0),
 );
 
-onMounted(async () => {
-  banks.value = (await getSelfTestBanks()) || [];
-  if (banks.value.length === 1) {
-    dataForm.bankId = banks.value[0].id;
+async function loadBanks() {
+  try {
+    banks.value = (await getSelfTestBanks()) || [];
+    if (!dataForm.bankId && banks.value.length === 1) {
+      dataForm.bankId = banks.value[0].id;
+    }
+  } catch (e: any) {
+    banks.value = [];
+    createMessage.error(e?.message || '鍔犺浇棰樺簱澶辫触');
+  } finally {
+    pageReady.value = true;
+  }
+}
+
+onMounted(() => {
+  tabbarStore.renderRouteView = true;
+  loadBanks();
+});
+onActivated(() => {
+  tabbarStore.renderRouteView = true;
+  if (!pageReady.value || !banks.value.length) {
+    loadBanks();
   }
 });
 
@@ -92,6 +113,10 @@
     loading.value = false;
   }
 }
+
+function goRecords() {
+  router.push('/tms/selfTest/records');
+}
 </script>
 
 <template>
@@ -99,8 +124,11 @@
     <div class="jnpf-content-wrapper-center">
       <div class="jnpf-content-wrapper-content tms-self-test-page">
         <div class="tms-self-test-header">
-          <div class="text-base font-medium">鑷垜妫�娴�</div>
-          <div class="mt-1 text-gray-400 text-sm">璁剧疆鏌ヨ鏉′欢锛屼粠棰樺簱涓瓫閫夐鐩繘琛岃嚜鎴戞娴嬨��</div>
+          <div>
+            <div class="text-base font-medium">鑷垜妫�娴�</div>
+            <div class="mt-1 text-gray-400 text-sm">璁剧疆鏌ヨ鏉′欢锛屼粠棰樺簱涓瓫閫夐鐩繘琛岃嚜鎴戞娴嬨��</div>
+          </div>
+          <a-button @click="goRecords">鎴戠殑妫�娴嬭褰�</a-button>
         </div>
 
         <a-form
@@ -213,6 +241,9 @@
 }
 
 .tms-self-test-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
   margin-bottom: 24px;
   padding-bottom: 12px;
   border-bottom: 1px solid #f0f0f0;
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/mock.ts
deleted file mode 100644
index 989e2bb..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/mock.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import type { SelfTestPaper, SelfTestQuestionItem, SelfTestStartPayload } from './types';
-
-import { mockGetQuestion, mockQueryQuestions } from '#/views/x/tms/question/mock';
-
-import { SELF_TEST_DIFFICULTY_TO_QUESTION, type SelfTestDifficulty } from './constants';
-
-function delay<T>(data: T, ms = 250): Promise<T> {
-  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
-}
-
-/** 鎸夋潯浠朵粠 mock 棰樺簱鎶介锛堜笉澶熷垯鏀惧闅惧害锛� */
-export async function mockStartSelfTest(payload: SelfTestStartPayload): Promise<SelfTestPaper> {
-  const all = await mockQueryQuestions({ bankId: payload.bankId, pageSize: 500, currentPage: 1 });
-  const list = all.list || [];
-
-  const pick = async (type: 'single' | 'multi' | 'judge', count: number, difficulty: SelfTestDifficulty) => {
-    if (count <= 0) return [];
-    const mapped = SELF_TEST_DIFFICULTY_TO_QUESTION[difficulty] || [difficulty];
-    let pool = list.filter((q) => q.questionType === type);
-    if (difficulty) {
-      pool = pool.filter((q) => !q.difficulty || mapped.includes(q.difficulty));
-    }
-    // 涓嶅鍒欐斁瀹介毦搴�
-    if (pool.length < count) {
-      pool = list.filter((q) => q.questionType === type);
-    }
-    const sliced = pool.slice(0, count);
-    const detailed: SelfTestQuestionItem[] = [];
-    for (const q of sliced) {
-      const full = await mockGetQuestion(q.id!);
-      detailed.push({
-        id: full.id!,
-        questionNo: full.questionNo,
-        questionType: full.questionType as 'single' | 'multi' | 'judge',
-        difficulty: full.difficulty,
-        stem: full.stem,
-        options: (full.options || []).map((o) => ({
-          optionLabel: o.optionLabel,
-          optionContent: o.optionContent,
-          isCorrect: o.isCorrect,
-        })),
-      });
-    }
-    return detailed;
-  };
-
-  const questions = [
-    ...(await pick('single', payload.single.count, payload.single.difficulty)),
-    ...(await pick('multi', payload.multi.count, payload.multi.difficulty)),
-    ...(await pick('judge', payload.judge.count, payload.judge.difficulty)),
-  ];
-
-  return delay({
-    paperId: `self_${Date.now()}`,
-    bankId: payload.bankId,
-    bankName: payload.bankName || '',
-    questions,
-  });
-}
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>
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/types.ts b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/types.ts
index 2fce373..0f705e9 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/types.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/types.ts
@@ -22,11 +22,17 @@
   judge: SelfTestTypeSetting;
 }
 
-/** 鎶介缁撴灉锛堣�冭瘯椤碉級 */
+/** 鎶介缁撴灉 / 妫�娴嬭鎯� */
 export interface SelfTestPaper {
   paperId: string;
   bankId: string;
   bankName: string;
+  testStatus?: string;
+  totalCount?: number;
+  correctCount?: number;
+  scoreRate?: number;
+  startTime?: string;
+  submitTime?: string;
   questions: SelfTestQuestionItem[];
 }
 
@@ -37,4 +43,32 @@
   difficulty?: Difficulty;
   stem: string;
   options: { optionLabel: string; optionContent: string; isCorrect: '0' | '1' }[];
+  /** 鍥為【瀛楁 */
+  correctAnswer?: string;
+  userAnswer?: string;
+  isRight?: '0' | '1';
+  sortNo?: number;
+}
+
+export interface SelfTestSubmitPayload {
+  answers: Record<string, string | string[]>;
+}
+
+export interface SelfTestSubmitResult {
+  paperId: string;
+  totalCount: number;
+  correctCount: number;
+  scoreRate: number;
+}
+
+export interface SelfTestRecordItem {
+  id: string;
+  bankId: string;
+  bankName: string;
+  totalCount: number;
+  correctCount?: number;
+  scoreRate?: number;
+  testStatus: string;
+  startTime?: string;
+  submitTime?: string;
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/shared/dic.ts b/apps/jnpf-web-apps-main/src/views/x/tms/shared/dic.ts
new file mode 100644
index 0000000..6e58c6c
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/shared/dic.ts
@@ -0,0 +1,167 @@
+import type { useBaseStore } from '#/store';
+
+/** 涓� docs/tms/瀛楁娉ㄩ噴涓庡瓧鍏歌鑼�.md 瀵归綈 */
+export const TMS_DIC = {
+  yesNo: 'tmsYesNo',
+  enableStatus: 'tmsEnableStatus',
+  signRule: 'tmsSignRule',
+  questionType: 'tmsQuestionType',
+  difficulty: 'tmsDifficulty',
+  questionSource: 'tmsQuestionSource',
+  openClosedInvalid: 'tmsOpenClosedInvalid',
+  paperSortMode: 'tmsPaperSortMode',
+  courseCategory: 'tmsCourseCategory',
+  trainMode: 'tmsTrainMode',
+  evalMode: 'tmsEvalMode',
+  archiveStatus: 'tmsArchiveStatus',
+  gradeStatus: 'tmsGradeStatus',
+  examStatus: 'tmsExamStatus',
+  passFlag: 'tmsPassFlag',
+  taskPublishStatus: 'tmsTaskPublishStatus',
+  taskCategory: 'tmsTaskCategory',
+  taskKind: 'tmsTaskKind',
+  taskSourceType: 'tmsTaskSourceType',
+  personTaskStatus: 'tmsPersonTaskStatus',
+} as const;
+
+export type TmsDicCode = (typeof TMS_DIC)[keyof typeof TMS_DIC];
+
+export type TmsDicOpt = {
+  id: string;
+  enCode?: string;
+  fullName: string;
+  [key: string]: any;
+};
+
+/** Select 瀛樺瓧鍏� enCode锛堜笌搴撳瓧娈电紪鐮佷竴鑷达級 */
+export const TMS_DIC_FIELD_NAMES = { value: 'enCode', label: 'fullName' } as const;
+
+/** 瀛楀吀涓嶅彲鐢ㄦ椂鐨勫厹搴曪紝浠呬緵 loadTmsDic 鍐呴儴浣跨敤 */
+const FALLBACK_BY_CODE: Record<string, TmsDicOpt[]> = {
+  [TMS_DIC.yesNo]: [
+    { id: '1', enCode: '1', fullName: '鏄�' },
+    { id: '0', enCode: '0', fullName: '鍚�' },
+  ],
+  [TMS_DIC.enableStatus]: [
+    { id: '1', enCode: '1', fullName: '鍚敤' },
+    { id: '0', enCode: '0', fullName: '鍋滅敤' },
+  ],
+  [TMS_DIC.signRule]: [
+    { id: 'scan', enCode: 'scan', fullName: '浠呮壂鐮�' },
+    { id: 'both', enCode: 'both', fullName: '鎵爜+鍦ㄧ嚎' },
+  ],
+  [TMS_DIC.questionType]: [
+    { id: 'single', enCode: 'single', fullName: '鍗曢��' },
+    { id: 'multi', enCode: 'multi', fullName: '澶氶��' },
+    { id: 'judge', enCode: 'judge', fullName: '鍒ゆ柇' },
+    { id: 'blank', enCode: 'blank', fullName: '濉┖' },
+    { id: 'essay', enCode: 'essay', fullName: '闂瓟' },
+  ],
+  [TMS_DIC.difficulty]: [
+    { id: 'easy', enCode: 'easy', fullName: '绠�鍗�' },
+    { id: 'normal', enCode: 'normal', fullName: '涓�鑸�' },
+    { id: 'hard', enCode: 'hard', fullName: '鍥伴毦' },
+  ],
+  [TMS_DIC.questionSource]: [
+    { id: 'self', enCode: 'self', fullName: '鑷懡棰�' },
+    { id: 'import', enCode: 'import', fullName: '瀵煎叆' },
+    { id: 'external', enCode: 'external', fullName: '澶栬喘' },
+  ],
+  [TMS_DIC.openClosedInvalid]: [
+    { id: 'open', enCode: 'open', fullName: '寮�鏀�' },
+    { id: 'closed', enCode: 'closed', fullName: '涓嶅紑鏀�' },
+    { id: 'invalid', enCode: 'invalid', fullName: '搴熷純' },
+  ],
+  [TMS_DIC.paperSortMode]: [
+    { id: 'paper', enCode: 'paper', fullName: '璇曞嵎椤哄簭' },
+    { id: 'random', enCode: 'random', fullName: '闅忔満' },
+  ],
+  [TMS_DIC.courseCategory]: [
+    { id: 'gmp', enCode: 'gmp', fullName: 'GMP' },
+    { id: 'sop', enCode: 'sop', fullName: 'SOP' },
+    { id: 'safety', enCode: 'safety', fullName: '瀹夊叏' },
+    { id: 'skill', enCode: 'skill', fullName: '鎶�鑳�' },
+    { id: 'other', enCode: 'other', fullName: '鍏朵粬' },
+  ],
+  [TMS_DIC.trainMode]: [
+    { id: 'onsite', enCode: 'onsite', fullName: '闆嗕腑鎺堣' },
+    { id: 'practice', enCode: 'practice', fullName: '鎿嶄綔鎺堣' },
+    { id: 'online', enCode: 'online', fullName: '鍦ㄧ嚎瀛︿範' },
+  ],
+  [TMS_DIC.evalMode]: [
+    { id: 'quiz', enCode: 'quiz', fullName: '鎻愰棶' },
+    { id: 'practice', enCode: 'practice', fullName: '鐜板満鎿嶄綔' },
+    { id: 'exam', enCode: 'exam', fullName: '鍦ㄧ嚎鑰冭瘯' },
+    { id: 'none', enCode: 'none', fullName: '鏃犻渶鑰冩牳' },
+  ],
+  [TMS_DIC.archiveStatus]: [
+    { id: 'pending', enCode: 'pending', fullName: '寰呭綊妗�' },
+    { id: 'ready', enCode: 'ready', fullName: '鍙綊妗�' },
+    { id: 'archived', enCode: 'archived', fullName: '宸插綊妗�' },
+    { id: 'invalid', enCode: 'invalid', fullName: '鏃犳晥' },
+  ],
+  [TMS_DIC.gradeStatus]: [
+    { id: 'auto', enCode: 'auto', fullName: '鏃犻渶闃呭嵎' },
+    { id: 'pending', enCode: 'pending', fullName: '寰呮壒鏀�' },
+    { id: 'graded', enCode: 'graded', fullName: '宸叉壒鏀�' },
+  ],
+  [TMS_DIC.examStatus]: [
+    { id: 'doing', enCode: 'doing', fullName: '鑰冭瘯涓�' },
+    { id: 'submitted', enCode: 'submitted', fullName: '宸蹭氦鍗�' },
+    { id: 'cancelled', enCode: 'cancelled', fullName: '宸插彇娑�' },
+  ],
+  [TMS_DIC.passFlag]: [
+    { id: '1', enCode: '1', fullName: '鍚堟牸' },
+    { id: '0', enCode: '0', fullName: '涓嶅悎鏍�' },
+  ],
+  [TMS_DIC.taskPublishStatus]: [
+    { id: 'draft', enCode: 'draft', fullName: '鏈彂甯�' },
+    { id: 'published', enCode: 'published', fullName: '宸插彂甯�' },
+    { id: 'cancelled', enCode: 'cancelled', fullName: '宸插彇娑�' },
+    { id: 'done', enCode: 'done', fullName: '宸插畬鎴�' },
+  ],
+  [TMS_DIC.taskCategory]: [
+    { id: 'temp', enCode: 'temp', fullName: '涓存椂鍩硅' },
+    { id: 'post_plan', enCode: 'post_plan', fullName: '宀椾綅璁″垝' },
+    { id: 'annual_plan', enCode: 'annual_plan', fullName: '骞村害璁″垝' },
+    { id: 'file_effect', enCode: 'file_effect', fullName: '鏂囦欢鐢熸晥' },
+    { id: 'out_train', enCode: 'out_train', fullName: '澶栨淳鍩硅' },
+    { id: 'retrain', enCode: 'retrain', fullName: '鍐嶅煿璁�' },
+  ],
+  [TMS_DIC.taskKind]: [
+    { id: 'new', enCode: 'new', fullName: '鏂板' },
+    { id: 'continue', enCode: 'continue', fullName: '缁х画' },
+  ],
+  [TMS_DIC.taskSourceType]: [
+    { id: 'annual', enCode: 'annual', fullName: '骞磋鍒�' },
+    { id: 'post', enCode: 'post', fullName: '宀楄鍒�' },
+    { id: 'temp', enCode: 'temp', fullName: '涓存椂' },
+    { id: 'file', enCode: 'file', fullName: '鏂囦欢鍩硅' },
+  ],
+  [TMS_DIC.personTaskStatus]: [
+    { id: 'todo', enCode: 'todo', fullName: '鏈畬鎴�' },
+    { id: 'signed', enCode: 'signed', fullName: '宸茬鍒�' },
+    { id: 'done', enCode: 'done', fullName: '宸插畬鎴�' },
+    { id: 'expired', enCode: 'expired', fullName: '宸茶繃鏈�' },
+    { id: 'cancelled', enCode: 'cancelled', fullName: '宸插彇娑�' },
+  ],
+};
+
+export function labelOfDic(options: TmsDicOpt[], v?: string | null) {
+  if (v == null || v === '') return '-';
+  const hit = options.find((x) => x.enCode === v || x.id === v);
+  return hit?.fullName || v;
+}
+
+type BaseStore = ReturnType<typeof useBaseStore>;
+
+/** 缁熶竴瀛楀吀鍏ュ彛锛氭寜缂栫爜鎷夊彇锛屽け璐ユ椂鐢ㄥ唴缃厹搴� */
+export async function loadTmsDic(baseStore: BaseStore, code: string): Promise<TmsDicOpt[]> {
+  const fallback = FALLBACK_BY_CODE[code] ?? [];
+  try {
+    const res = await baseStore.getDictionaryData(code);
+    return Array.isArray(res) && res.length ? (res as TmsDicOpt[]) : fallback;
+  } catch {
+    return fallback;
+  }
+}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/shared/page.css b/apps/jnpf-web-apps-main/src/views/x/tms/shared/page.css
new file mode 100644
index 0000000..fc03f63
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/shared/page.css
@@ -0,0 +1,40 @@
+/* TMS 璇︽儏/宸ュ叿鏉¢�氱敤鏍峰紡锛屾寜闇�鍦ㄩ〉闈� import */
+
+.tms-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.tms-page-header__title {
+  font-size: 16px;
+  font-weight: 600;
+  line-height: 1.4;
+  color: rgba(0, 0, 0, 0.88);
+}
+
+.tms-page-header__sub {
+  margin-top: 4px;
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+  line-height: 1.4;
+}
+
+.tms-page-header__actions {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.tms-page-tip {
+  margin-bottom: 12px;
+}
+
+.tms-toolbar-hint {
+  font-size: 13px;
+  color: rgba(0, 0, 0, 0.45);
+}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/shared/ui.ts b/apps/jnpf-web-apps-main/src/views/x/tms/shared/ui.ts
new file mode 100644
index 0000000..fc8fcf0
--- /dev/null
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/shared/ui.ts
@@ -0,0 +1,48 @@
+/**
+ * TMS 椤甸潰鎸夐挳 / 鏂囨缁熶竴绾﹀畾
+ *
+ * 棰滆壊锛�
+ * - primary锛氭湰椤靛敮涓�涓绘搷浣滐紙鏂板銆佸彂甯冦�佷氦鍗枫�佺鍒般�佽繘鍏ヨ�冭瘯銆佸綊妗o級
+ * - default锛氳繑鍥炪�佹瑕併�佸鑸�
+ * - primary + ghost锛氭涓绘搷浣滐紙杩涘叆瀛︿範銆佷繚瀛樿崏绋匡級
+ * - danger锛氬垹闄ゃ�佸簾寮冦�佸彇娑堜换鍔�
+ *
+ * 鏂囨锛�
+ * - 杩斿洖璇︽儏/鍒楄〃缁熶竴鐢ㄣ�岃繑鍥炪��
+ * - 琛屽唴鏌ョ湅缁熶竴銆岃鎯呫��
+ * - 鍏抽棴寮瑰眰鐢ㄣ�屽叧闂��
+ */
+
+export const TMS_BTN = {
+  back: '杩斿洖',
+  close: '鍏抽棴',
+  detail: '璇︽儏',
+  edit: '缂栬緫',
+  add: '鏂板',
+  delete: '鍒犻櫎',
+  save: '淇濆瓨',
+  saveDraft: '淇濆瓨鑽夌',
+  savePublish: '淇濆瓨骞跺彂甯�',
+  search: '鏌ヨ',
+  reset: '閲嶇疆',
+  export: '瀵煎嚭',
+  publish: '鍙戝竷',
+  cancel: '鍙栨秷',
+  archive: '褰掓。',
+  sign: '绛惧埌',
+  learn: '瀛︿範',
+  enterLearn: '杩涘叆瀛︿範',
+  exam: '鑰冭瘯',
+  enterExam: '杩涘叆鑰冭瘯',
+  retake: '琛ヨ��',
+  continueExam: '缁х画鑰冭瘯',
+  startExam: '寮�濮嬭�冭瘯',
+  submitExam: '浜ゅ嵎',
+  grade: '闃呭嵎',
+  viewPaper: '鏌ョ湅璇曞嵎',
+  restore: '鎭㈠',
+  maintain: '缁存姢',
+} as const;
+
+/** 璇︽儏椤垫搷浣滃尯鎸夐挳椤哄簭锛氳繑鍥� 鈫� 娆¤ 鈫� 涓绘搷浣� */
+export type TmsHeaderActionTone = 'back' | 'secondary' | 'primary' | 'danger';
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/Form.vue
index 85d3092..a943bc3 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/Form.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/Form.vue
@@ -6,13 +6,18 @@
 import { BasicModal, useModalInner } from '@jnpf/ui/modal';
 
 import { createSignMode, getSignModeInfo, updateSignMode } from '#/api/x/tms/signMode';
-
-import { ENABLE_OPTIONS, YES_NO_OPTIONS } from './constants';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  loadTmsDic,
+} from '#/views/x/tms/shared/dic';
 
 defineOptions({ name: 'TmsSignModeForm' });
 
 const emit = defineEmits(['register', 'reload']);
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 
 const state = reactive({ id: '' });
 const { id } = toRefs(state);
@@ -48,7 +53,11 @@
       label: '椤诲尮閰嶅悕鍗�',
       component: 'Select',
       defaultValue: '1',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -56,7 +65,11 @@
       label: '鍚敤鐘舵��',
       component: 'Select',
       defaultValue: '1',
-      componentProps: { placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -69,6 +82,23 @@
 });
 
 const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner(init);
+
+async function loadDicOptions() {
+  const [yesNo, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.yesNo),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  updateSchema([
+    {
+      field: 'matchRoster',
+      componentProps: { placeholder: '璇烽�夋嫨', options: yesNo, fieldNames: TMS_DIC_FIELD_NAMES },
+    },
+    {
+      field: 'enabled',
+      componentProps: { placeholder: '璇烽�夋嫨', options: enable, fieldNames: TMS_DIC_FIELD_NAMES },
+    },
+  ]);
+}
 
 async function init(data: { id?: string }) {
   changeLoading(true);
@@ -84,9 +114,8 @@
     },
   });
   try {
-    if (state.id) {
-      setFieldsValue(await getSignModeInfo(state.id));
-    }
+    await loadDicOptions();
+    if (state.id) setFieldsValue(await getSignModeInfo(state.id));
   } finally {
     changeLoading(false);
   }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/constants.ts
deleted file mode 100644
index 64da652..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/constants.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export const YES_NO_OPTIONS = [
-  { id: '1', fullName: '鏄�' },
-  { id: '0', fullName: '鍚�' },
-];
-
-export const ENABLE_OPTIONS = [
-  { id: '1', fullName: '鍚敤' },
-  { id: '0', fullName: '鍋滅敤' },
-];
-
-export function labelOfYesNo(v?: string) {
-  return YES_NO_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfEnabled(v?: string) {
-  return ENABLE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/index.vue
index b532908..8d3c9ea 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/index.vue
@@ -3,19 +3,32 @@
 
 import type { SignModeItem } from './types';
 
+import { onMounted, ref } from 'vue';
+
 import { useMessage } from '@jnpf/hooks';
 import { useModal } from '@jnpf/ui/modal';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { deleteSignMode, getSignModeList, setSignModeEnabled } from '#/api/x/tms/signMode';
+import { useBaseStore } from '#/store';
+import {
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
 
 import Form from './Form.vue';
-import { ENABLE_OPTIONS, labelOfEnabled, labelOfYesNo } from './constants';
 
 defineOptions({ name: 'TmsSignMode' });
 
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 const [registerForm, { openModal: openFormModal }] = useModal();
+
+const yesNoOpts = ref<TmsDicOpt[]>([]);
+const enableOpts = ref<TmsDicOpt[]>([]);
 
 const columns: BasicColumn[] = [
   { title: '鏂瑰紡缂栫爜', dataIndex: 'modeCode', width: 140 },
@@ -32,7 +45,7 @@
     dataIndex: 'matchRoster',
     width: 120,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as SignModeItem).matchRoster),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as SignModeItem).matchRoster),
   },
   {
     title: '鐘舵��',
@@ -44,10 +57,10 @@
   { title: '澶囨敞', dataIndex: 'remark', minWidth: 220 },
 ];
 
-const [registerTable, { reload }] = useVxeTable({
+const [registerTable, { reload, getForm }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
@@ -64,7 +77,12 @@
         field: 'enabled',
         label: '鐘舵��',
         component: 'Select',
-        componentProps: { allowClear: true, placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+        componentProps: {
+          allowClear: true,
+          placeholder: '璇烽�夋嫨',
+          options: enableOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
+        },
       },
     ],
   },
@@ -74,6 +92,21 @@
     dataIndex: 'action',
     fixed: 'right',
   },
+});
+
+onMounted(async () => {
+  yesNoOpts.value = await loadTmsDic(baseStore, TMS_DIC.yesNo);
+  enableOpts.value = await loadTmsDic(baseStore, TMS_DIC.enableStatus);
+  getForm()?.updateSchema?.({
+    field: 'enabled',
+    componentProps: {
+      allowClear: true,
+      placeholder: '璇烽�夋嫨',
+      options: enableOpts.value,
+      fieldNames: TMS_DIC_FIELD_NAMES,
+    },
+  });
+  reload();
 });
 
 async function fetchList(params: Record<string, any>) {
@@ -101,6 +134,10 @@
 }
 
 async function handleDelete(record: SignModeItem) {
+  if (record.enabled !== '0') {
+    createMessage.warning('璇峰厛鍋滅敤鍚庡啀鍒犻櫎');
+    return;
+  }
   try {
     await deleteSignMode(record.id);
     createMessage.success('鍒犻櫎鎴愬姛');
@@ -112,7 +149,7 @@
 
 function getTableActions(record: SignModeItem): ActionItem[] {
   const enableLabel = record.enabled === '1' ? '鍋滅敤' : '鍚敤';
-  return [
+  const actions: ActionItem[] = [
     { label: '缂栬緫', onClick: handleEdit.bind(null, record) },
     {
       label: enableLabel,
@@ -121,15 +158,18 @@
         onOk: handleToggleEnabled.bind(null, record),
       },
     },
-    {
+  ];
+  if (record.enabled === '0') {
+    actions.push({
       label: '鍒犻櫎',
       color: 'error',
       modelConfirm: {
         content: `纭畾鍒犻櫎绛惧埌鏂瑰紡銆�${record.modeName}銆嶅悧锛焋,
         onOk: handleDelete.bind(null, record),
       },
-    },
-  ];
+    });
+  }
+  return actions;
 }
 </script>
 
@@ -143,7 +183,7 @@
           </template>
           <template #enabled="{ record }">
             <a-tag :color="record.enabled === '1' ? 'success' : 'default'">
-              {{ labelOfEnabled(record.enabled) }}
+              {{ labelOfDic(enableOpts, record.enabled) }}
             </a-tag>
           </template>
           <template #action="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/signMode/mock.ts
deleted file mode 100644
index 705ca37..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/signMode/mock.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import type { SignModeItem, SignModePageQuery } from './types';
-
-function delay<T>(data: T, ms = 180): Promise<T> {
-  return new Promise((resolve) => setTimeout(() => resolve(data), ms));
-}
-
-const store: SignModeItem[] = [
-  {
-    id: 'tms_sign_app',
-    modeCode: 'app_scan',
-    modeName: 'APP鎵爜',
-    tipSeconds: 30,
-    matchRoster: '1',
-    enabled: '1',
-    remark: '绯荤粺 APP 鎵爜锛堝浘鐗囪瘑鍒� / 鎽勫儚澶� / 闀挎寜璇嗙爜锛�',
-  },
-  {
-    id: 'tms_sign_wx',
-    modeCode: 'wx_scan',
-    modeName: '寰俊鎵爜',
-    tipSeconds: 30,
-    matchRoster: '1',
-    enabled: '1',
-    remark: '寰俊鎵爜绛惧埌',
-  },
-  {
-    id: 'tms_sign_online',
-    modeCode: 'online',
-    modeName: '鍦ㄧ嚎绛惧埌',
-    tipSeconds: 30,
-    matchRoster: '1',
-    enabled: '1',
-    remark: '鐧诲綍绯荤粺鍦ㄧ嚎绛惧埌',
-  },
-];
-
-export function mockQuerySignModes(params: SignModePageQuery = {}) {
-  const { currentPage = 1, pageSize = 20, keyword = '', enabled } = params;
-  let list = [...store];
-  if (enabled) list = list.filter((x) => x.enabled === enabled);
-  if (keyword) {
-    const k = keyword.trim().toLowerCase();
-    list = list.filter(
-      (x) => x.modeCode.toLowerCase().includes(k) || x.modeName.toLowerCase().includes(k),
-    );
-  }
-  const start = (currentPage - 1) * pageSize;
-  return delay({
-    list: list.slice(start, start + pageSize),
-    pagination: { total: list.length, currentPage, pageSize },
-  });
-}
-
-export async function mockGetSignMode(id: string) {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('绛惧埌鏂瑰紡涓嶅瓨鍦�');
-  return { ...row };
-}
-
-export async function mockSaveSignMode(data: Partial<SignModeItem> & { id?: string }) {
-  await delay(null);
-  if (data.id) {
-    const row = store.find((x) => x.id === data.id);
-    if (!row) throw new Error('绛惧埌鏂瑰紡涓嶅瓨鍦�');
-    if (data.modeCode && data.modeCode !== row.modeCode) {
-      if (store.some((x) => x.modeCode === data.modeCode && x.id !== data.id)) {
-        throw new Error('鏂瑰紡缂栫爜宸插瓨鍦�');
-      }
-    }
-    Object.assign(row, data);
-    return row;
-  }
-  if (!data.modeCode) throw new Error('璇峰~鍐欐柟寮忕紪鐮�');
-  if (store.some((x) => x.modeCode === data.modeCode)) throw new Error('鏂瑰紡缂栫爜宸插瓨鍦�');
-  const row: SignModeItem = {
-    id: `sm_${Date.now()}`,
-    modeCode: data.modeCode,
-    modeName: data.modeName || data.modeCode,
-    tipSeconds: data.tipSeconds ?? 30,
-    matchRoster: (data.matchRoster as any) || '1',
-    enabled: (data.enabled as any) || '1',
-    remark: data.remark || '',
-  };
-  store.push(row);
-  return row;
-}
-
-export async function mockSetSignModeEnabled(id: string, enabled: '0' | '1') {
-  await delay(null);
-  const row = store.find((x) => x.id === id);
-  if (!row) throw new Error('绛惧埌鏂瑰紡涓嶅瓨鍦�');
-  row.enabled = enabled;
-  return row;
-}
-
-export async function mockDeleteSignMode(id: string) {
-  await delay(null);
-  const idx = store.findIndex((x) => x.id === id);
-  if (idx < 0) throw new Error('绛惧埌鏂瑰紡涓嶅瓨鍦�');
-  store.splice(idx, 1);
-  return true;
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainArchive/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/trainArchive/index.vue
index c83a3af..a6fac89 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainArchive/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainArchive/index.vue
@@ -1,10 +1,9 @@
 <script lang="ts" setup>
 import type { TrainArchiveDetail } from './types';
 
-import { computed, onMounted, ref } from 'vue';
+import { onMounted, ref } from 'vue';
 
 import { useMessage } from '@jnpf/hooks';
-import { useUserStore } from '@vben/stores';
 import {
   Descriptions as ADescriptions,
   DescriptionsItem as ADescriptionsItem,
@@ -16,12 +15,9 @@
 defineOptions({ name: 'TmsTrainArchive' });
 
 const { createMessage } = useMessage();
-const userStore = useUserStore();
 
 const loading = ref(false);
 const detail = ref<TrainArchiveDetail | null>(null);
-
-const userInfo = computed(() => (userStore.getUserInfo || {}) as Record<string, any>);
 
 const workColumns = [
   { title: '搴忓彿', key: 'index', width: 70, align: 'center', customRender: ({ index }: any) => index + 1 },
@@ -48,25 +44,10 @@
   loadArchive();
 });
 
-function resolvePositionName() {
-  const list = userInfo.value.positionList;
-  if (Array.isArray(list) && list.length) {
-    return list.map((o: any) => o.treeName || o.fullName || o.name).filter(Boolean).join('銆�') || '鏃�';
-  }
-  return userInfo.value.positionName || '鏃�';
-}
-
 async function loadArchive() {
   loading.value = true;
   try {
-    const u = userInfo.value;
-    detail.value = await getMyTrainArchive({
-      userId: u.userId || u.id,
-      userName: u.userName,
-      userAccount: u.userAccount,
-      organizeName: u.organizeName || u.departmentName,
-      positionName: resolvePositionName(),
-    });
+    detail.value = await getMyTrainArchive();
   } catch (e: any) {
     createMessage.error(e?.message || '鍔犺浇鍩硅妗f澶辫触');
   } finally {
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/Form.vue b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/Form.vue
index 990201f..9c6ab78 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/Form.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/Form.vue
@@ -6,13 +6,14 @@
 import { BasicModal, useModalInner } from '@jnpf/ui/modal';
 
 import { createTrainMode, getTrainModeInfo, updateTrainMode } from '#/api/x/tms/trainMode';
-
-import { ENABLE_OPTIONS, SIGN_RULE_OPTIONS, YES_NO_OPTIONS } from './constants';
+import { useBaseStore } from '#/store';
+import { TMS_DIC, TMS_DIC_FIELD_NAMES, loadTmsDic } from '#/views/x/tms/shared/dic';
 
 defineOptions({ name: 'TmsTrainModeForm' });
 
 const emit = defineEmits(['register', 'reload']);
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 
 const state = reactive({ id: '' });
 const { id } = toRefs(state);
@@ -41,7 +42,11 @@
       label: '绛惧埌瑙勫垯',
       component: 'Select',
       defaultValue: 'both',
-      componentProps: { placeholder: '璇烽�夋嫨', options: SIGN_RULE_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -49,7 +54,11 @@
       label: '鍏佽鍚嶅崟澶栫鍒�',
       component: 'Select',
       defaultValue: '0',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -57,7 +66,11 @@
       label: '璧锋椤诲悓涓�澶�',
       component: 'Select',
       defaultValue: '0',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -65,7 +78,11 @@
       label: '鍦扮偣蹇呭~',
       component: 'Select',
       defaultValue: '1',
-      componentProps: { placeholder: '璇烽�夋嫨', options: YES_NO_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -80,7 +97,11 @@
       label: '鍚敤鐘舵��',
       component: 'Select',
       defaultValue: '1',
-      componentProps: { placeholder: '璇烽�夋嫨', options: ENABLE_OPTIONS },
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: [],
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
       rules: [{ required: true, message: '蹇呭~', trigger: 'change' }],
     },
     {
@@ -93,6 +114,56 @@
 });
 
 const [registerModal, { closeModal, changeLoading, changeOkLoading }] = useModalInner(init);
+
+async function loadDicOptions() {
+  const [signRule, yesNo, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.signRule),
+    loadTmsDic(baseStore, TMS_DIC.yesNo),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  updateSchema([
+    {
+      field: 'signRule',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: signRule,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'allowGuestSign',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: yesNo,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'sameDayRequired',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: yesNo,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'placeRequired',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: yesNo,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+    {
+      field: 'enabled',
+      componentProps: {
+        placeholder: '璇烽�夋嫨',
+        options: enable,
+        fieldNames: TMS_DIC_FIELD_NAMES,
+      },
+    },
+  ]);
+}
 
 async function init(data: { id?: string }) {
   changeLoading(true);
@@ -108,9 +179,9 @@
     },
   });
   try {
+    await loadDicOptions();
     if (state.id) {
-      const info = await getTrainModeInfo(state.id);
-      setFieldsValue(info);
+      setFieldsValue(await getTrainModeInfo(state.id));
     }
   } finally {
     changeLoading(false);
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/constants.ts b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/constants.ts
deleted file mode 100644
index 889fb58..0000000
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/constants.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export const YES_NO_OPTIONS = [
-  { id: '1', fullName: '鏄�' },
-  { id: '0', fullName: '鍚�' },
-];
-
-export const ENABLE_OPTIONS = [
-  { id: '1', fullName: '鍚敤' },
-  { id: '0', fullName: '鍋滅敤' },
-];
-
-export const SIGN_RULE_OPTIONS = [
-  { id: 'scan', fullName: '浠呮壂鐮�' },
-  { id: 'both', fullName: '鎵爜+鍦ㄧ嚎' },
-];
-
-export function labelOfYesNo(v?: string) {
-  return YES_NO_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfSignRule(v?: string) {
-  return SIGN_RULE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
-
-export function labelOfEnabled(v?: string) {
-  return ENABLE_OPTIONS.find((x) => x.id === v)?.fullName || v || '-';
-}
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/index.vue
index ed76a4a..86045a2 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/index.vue
@@ -3,24 +3,33 @@
 
 import type { TrainModeItem } from './types';
 
+import { onMounted, ref } from 'vue';
+
 import { useMessage } from '@jnpf/hooks';
 import { useModal } from '@jnpf/ui/modal';
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { deleteTrainMode, getTrainModeList, setTrainModeEnabled } from '#/api/x/tms/trainMode';
+import { useBaseStore } from '#/store';
 
 import Form from './Form.vue';
 import {
-  ENABLE_OPTIONS,
-  labelOfEnabled,
-  labelOfSignRule,
-  labelOfYesNo,
-} from './constants';
+  TMS_DIC,
+  TMS_DIC_FIELD_NAMES,
+  labelOfDic,
+  loadTmsDic,
+  type TmsDicOpt,
+} from '#/views/x/tms/shared/dic';
 
 defineOptions({ name: 'TmsTrainMode' });
 
 const { createMessage } = useMessage();
+const baseStore = useBaseStore();
 const [registerForm, { openModal: openFormModal }] = useModal();
+
+const signRuleOpts = ref<TmsDicOpt[]>([]);
+const yesNoOpts = ref<TmsDicOpt[]>([]);
+const enableOpts = ref<TmsDicOpt[]>([]);
 
 const columns: BasicColumn[] = [
   { title: '鏂瑰紡缂栫爜', dataIndex: 'modeCode', width: 120 },
@@ -29,28 +38,28 @@
     title: '绛惧埌瑙勫垯',
     dataIndex: 'signRule',
     width: 120,
-    customRender: ({ record }) => labelOfSignRule((record as TrainModeItem).signRule),
+    customRender: ({ record }) => labelOfDic(signRuleOpts.value, (record as TrainModeItem).signRule),
   },
   {
     title: '鍏佽鍚嶅崟澶栫鍒�',
     dataIndex: 'allowGuestSign',
     width: 130,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as TrainModeItem).allowGuestSign),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as TrainModeItem).allowGuestSign),
   },
   {
     title: '璧锋椤诲悓涓�澶�',
     dataIndex: 'sameDayRequired',
     width: 120,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as TrainModeItem).sameDayRequired),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as TrainModeItem).sameDayRequired),
   },
   {
     title: '鍦扮偣蹇呭~',
     dataIndex: 'placeRequired',
     width: 100,
     align: 'center',
-    customRender: ({ record }) => labelOfYesNo((record as TrainModeItem).placeRequired),
+    customRender: ({ record }) => labelOfDic(yesNoOpts.value, (record as TrainModeItem).placeRequired),
   },
   { title: '鎺掑簭', dataIndex: 'sortNo', width: 80, align: 'center' },
   {
@@ -63,10 +72,10 @@
   { title: '澶囨敞', dataIndex: 'remark', minWidth: 180 },
 ];
 
-const [registerTable, { reload }] = useVxeTable({
+const [registerTable, { reload, getForm }] = useVxeTable({
   api: fetchList,
   columns,
-  immediate: true,
+  immediate: false,
   rowKey: 'id',
   useSearchForm: true,
   formConfig: {
@@ -86,7 +95,8 @@
         componentProps: {
           allowClear: true,
           placeholder: '璇烽�夋嫨',
-          options: ENABLE_OPTIONS,
+          options: enableOpts.value,
+          fieldNames: TMS_DIC_FIELD_NAMES,
         },
       },
     ],
@@ -97,6 +107,32 @@
     dataIndex: 'action',
     fixed: 'right',
   },
+});
+
+async function loadDic() {
+  const [signRule, yesNo, enable] = await Promise.all([
+    loadTmsDic(baseStore, TMS_DIC.signRule),
+    loadTmsDic(baseStore, TMS_DIC.yesNo),
+    loadTmsDic(baseStore, TMS_DIC.enableStatus),
+  ]);
+  signRuleOpts.value = signRule;
+  yesNoOpts.value = yesNo;
+  enableOpts.value = enable;
+
+  getForm()?.updateSchema?.({
+    field: 'enabled',
+    componentProps: {
+      allowClear: true,
+      placeholder: '璇烽�夋嫨',
+      options: enableOpts.value,
+      fieldNames: TMS_DIC_FIELD_NAMES,
+    },
+  });
+}
+
+onMounted(async () => {
+  await loadDic();
+  reload();
 });
 
 async function fetchList(params: Record<string, any>) {
@@ -124,6 +160,10 @@
 }
 
 async function handleDelete(record: TrainModeItem) {
+  if (record.enabled !== '0') {
+    createMessage.warning('璇峰厛鍋滅敤鍚庡啀鍒犻櫎');
+    return;
+  }
   try {
     await deleteTrainMode(record.id);
     createMessage.success('鍒犻櫎鎴愬姛');
@@ -135,7 +175,7 @@
 
 function getTableActions(record: TrainModeItem): ActionItem[] {
   const enableLabel = record.enabled === '1' ? '鍋滅敤' : '鍚敤';
-  return [
+  const actions: ActionItem[] = [
     { label: '缂栬緫', onClick: handleEdit.bind(null, record) },
     {
       label: enableLabel,
@@ -144,15 +184,18 @@
         onOk: handleToggleEnabled.bind(null, record),
       },
     },
-    {
+  ];
+  if (record.enabled === '0') {
+    actions.push({
       label: '鍒犻櫎',
       color: 'error',
       modelConfirm: {
         content: `纭畾鍒犻櫎鍩硅鏂瑰紡銆�${record.modeName}銆嶅悧锛焋,
         onOk: handleDelete.bind(null, record),
       },
-    },
-  ];
+    });
+  }
+  return actions;
 }
 </script>
 
@@ -166,7 +209,7 @@
           </template>
           <template #enabled="{ record }">
             <a-tag :color="record.enabled === '1' ? 'success' : 'default'">
-              {{ labelOfEnabled(record.enabled) }}
+              {{ labelOfDic(enableOpts, record.enabled) }}
             </a-tag>
           </template>
           <template #action="{ record }">
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/mock.ts b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/mock.ts
index 13e7c0b..7728b0f 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/mock.ts
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainMode/mock.ts
@@ -111,6 +111,7 @@
   await delay(null);
   const idx = store.findIndex((x) => x.id === id);
   if (idx < 0) throw new Error('鍩硅鏂瑰紡涓嶅瓨鍦�');
+  if (store[idx]!.enabled !== '0') throw new Error('璇峰厛鍋滅敤鍚庡啀鍒犻櫎');
   store.splice(idx, 1);
   return true;
 }
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/Catalog.vue b/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/Catalog.vue
index 86d0cf8..4601102 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/Catalog.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/Catalog.vue
@@ -9,6 +9,7 @@
 import dayjs from 'dayjs';
 
 import { getTrainRecordCatalog } from '#/api/x/tms/trainRecord';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
 
 defineOptions({ name: 'TmsTrainRecordCatalog' });
 
@@ -84,7 +85,39 @@
     createMessage.warning('鏆傛棤鏁版嵁鍙鍑�');
     return;
   }
-  createMessage.success(`宸插噯澶囧鍑� ${list.length} 鏉★紙瀵煎嚭鎺ュ彛鑱旇皟鍚庣敓鏁堬級`);
+  const headers = ['鍩硅璁板綍缂栧彿', '鍩硅绫诲瀷', '鍩硅鍐呭', '鍩硅鏂瑰紡', '鍩硅甯�', '鑰冩牳鏂瑰紡', '鍩硅缁撴灉', '鏄惁鍚堟牸', '鍏蜂綋鍩硅鏃堕棿'];
+  const escape = (v: unknown) => {
+    const s = v === null || v === undefined ? '' : String(v);
+    return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
+  };
+  const passText = (v: string) => (v === '1' ? '鍚堟牸' : v === '0' ? '涓嶅悎鏍�' : '');
+  const lines = [
+    headers.join(','),
+    ...list.map((row) =>
+      [
+        row.recordNo,
+        row.trainType,
+        row.trainContent,
+        row.trainMode,
+        row.trainerName,
+        row.evalMode,
+        resultText(row),
+        passText(row.passFlag),
+        row.trainDate || '',
+      ]
+        .map(escape)
+        .join(','),
+    ),
+  ];
+  const bom = '\uFEFF';
+  const blob = new Blob([bom + lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = `鍩硅鐩綍_${detail.value?.userName || detail.value?.archiveNo || 'export'}_${dayjs().format('YYYYMMDD')}.csv`;
+  a.click();
+  URL.revokeObjectURL(url);
+  createMessage.success(`宸插鍑� ${list.length} 鏉);
 }
 
 function goBack() {
@@ -119,12 +152,12 @@
                   :placeholder="['寮�濮嬫棩鏈�', '缁撴潫鏃ユ湡']"
                   :get-popup-container="popupContainer"
                 />
-                <a-button type="primary" class="ml-3" @click="handleSearch">鏌ヨ</a-button>
-                <a-button class="ml-2" @click="handleReset">閲嶇疆</a-button>
+                <a-button type="primary" class="ml-3" @click="handleSearch">{{ TMS_BTN.search }}</a-button>
+                <a-button class="ml-2" @click="handleReset">{{ TMS_BTN.reset }}</a-button>
               </div>
               <a-space>
-                <a-button @click="handleExport">瀵煎嚭</a-button>
-                <a-button @click="goBack">鍏抽棴</a-button>
+                <a-button type="primary" ghost @click="handleExport">{{ TMS_BTN.export }}</a-button>
+                <a-button @click="goBack">{{ TMS_BTN.close }}</a-button>
               </a-space>
             </div>
 
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/index.vue b/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/index.vue
index 06f049d..31b2456 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/index.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/trainRecord/index.vue
@@ -9,6 +9,9 @@
 import { BasicVxeTable, TableAction, useVxeTable } from '@jnpf/ui/vxeTable';
 
 import { getTrainRecordList } from '#/api/x/tms/trainRecord';
+import { TMS_BTN } from '#/views/x/tms/shared/ui';
+
+import '#/views/x/tms/shared/page.css';
 
 defineOptions({ name: 'TmsTrainRecord' });
 
@@ -84,7 +87,7 @@
 }
 
 function getTableActions(record: TrainRecordListItem): ActionItem[] {
-  return [{ label: '鏌ョ湅', onClick: handleView.bind(null, record) }];
+  return [{ label: TMS_BTN.detail, onClick: handleView.bind(null, record) }];
 }
 </script>
 
@@ -95,8 +98,8 @@
         <BasicVxeTable @register="registerTable">
           <template #tableTitle>
             <a-space>
-              <a-button type="primary" @click="handleViewSelected">鏌ョ湅</a-button>
-              <span class="text-gray-400 text-sm">涓汉鍩硅璁板綍琛紝閫夋嫨浜哄憳鏌ョ湅鍩硅鐩綍銆�</span>
+              <a-button type="primary" @click="handleViewSelected">{{ TMS_BTN.detail }}</a-button>
+              <span class="tms-toolbar-hint">閫夋嫨浜哄憳鏌ョ湅鍩硅鐩綍</span>
             </a-space>
           </template>
           <template #action="{ record }">

--
Gitblit v1.8.0