diff --git a/clinical-web/README.md b/clinical-web/README.md index 954f740..d01bfa7 100644 --- a/clinical-web/README.md +++ b/clinical-web/README.md @@ -86,6 +86,8 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适 已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台第一版 Mock 和文档库管理页面。登录、首页统计、签署工作台的患者/就诊/模板/任务/审计、签署文件下载、短信重发、模板创建/版本工作流、用户组织查询与用户创建编辑,以及按模板维护文档权限已经接入 MEDISIGN 后端;系统设置仍保留 Mock,真实签字板/线上签名回调和打印服务仍需设备或后端能力。 +自测与演示:`VITE_USE_MOCK=true` 时,文档库和签署任务的演示数据会镜像到 localStorage(见 `utils/mock-storage.ts`),刷新页面后仍可继续,不必每次重建文书、重发签署;文档库页头的「重置演示数据」可清空这些本地数据回到初始状态。持久化只针对演示数据,不涉及登录态与真实患者信息。 + ## 当前路由结构 ```text diff --git a/clinical-web/src/api/workbench/signing.ts b/clinical-web/src/api/workbench/signing.ts index dc91318..ac9bd65 100644 --- a/clinical-web/src/api/workbench/signing.ts +++ b/clinical-web/src/api/workbench/signing.ts @@ -1,6 +1,8 @@ import axios from 'axios' import { unwrapApiResponse } from '@/utils/api-response' +import { readLocalSigningTemplates } from '@/utils/mock-document-library' +import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from '@/utils/mock-storage' import { request } from '@/utils/request' import { resendSigningSms as resendSigningSmsDelivery } from './deliveries' @@ -215,7 +217,7 @@ const mockTemplates: SigningTemplate[] = [ }, ] -const mockRecords: SigningTaskRecord[] = [ +const defaultMockRecords: SigningTaskRecord[] = [ { id: 'task-001', templateVersionId: 'tpl-001-v2', @@ -325,6 +327,17 @@ const mockRecords: SigningTaskRecord[] = [ }, ] +/** + * 演示任务记录:优先用本地保存的,刷新后仍保留(任务固化的文书 HTML、签名图都在里面)。 + * 所有写操作都要调用 persistMockRecords 落盘,否则刷新会回到切换前的状态。 + */ +const mockRecords: SigningTaskRecord[] = + readMockState(MOCK_STORAGE_KEYS.signingTasks) ?? defaultMockRecords + +function persistMockRecords() { + writeMockState(MOCK_STORAGE_KEYS.signingTasks, mockRecords) +} + function clonePatient(patient: PatientProfile): PatientProfile { return { ...patient, @@ -371,6 +384,11 @@ function addHours(value: string, hours: number) { export const isSigningMockEnabled = useMockData +/** 演示患者清单(仅 Mock 模式):给签署弹窗做“点一下就能定位”的输入提示 */ +export function getMockPatientHints(): { id: string; name: string }[] { + return useMockData ? mockPatients.map((item) => ({ id: item.id, name: item.name })) : [] +} + function isNotFoundError(error: unknown) { return axios.isAxiosError(error) && error.response?.status === 404 } @@ -852,6 +870,8 @@ export async function getPatientProfile(keyword: string): Promise item.id === normalizedKeyword || + // 演示数据里记不住患者 ID 是常态,允许直接按姓名定位 + item.name === normalizedKeyword || item.visits.some((visit) => visit.visitNo === normalizedKeyword), ) @@ -876,7 +896,12 @@ export async function getSigningTemplates( return records.map(mapAvailableTemplate) } - return Promise.resolve(mockTemplates.map((template) => ({ ...template }))) + // 文档库本地新建的文书只存在 localStorage,必须一起返回; + // 否则文档库刚建好的文书在签署侧选不到(内置 Mock 模板是写死的演示数据)。 + return Promise.resolve([ + ...readLocalSigningTemplates(), + ...mockTemplates.map((template) => ({ ...template })), + ]) } export async function prepareSigningTask( @@ -950,6 +975,7 @@ export async function createSigningTask(input: CreateSigningTaskInput): Promise< } mockRecords.unshift(task) + persistMockRecords() return Promise.resolve(cloneTask(task)) } @@ -969,6 +995,7 @@ export function updateSigningTaskMethod( task.method = method task.updatedAt = formatMockDate() + persistMockRecords() return Promise.resolve(cloneTask(task)) } @@ -990,6 +1017,7 @@ export function completeSigningTask( task.signatureDataUrl = signatureDataUrl task.signedAt = formatMockDate() task.updatedAt = task.signedAt + persistMockRecords() return Promise.resolve(cloneTask(task)) } @@ -1015,6 +1043,7 @@ export async function reopenSigningTask( task.signatureDataUrl = undefined task.signedAt = undefined task.updatedAt = formatMockDate() + persistMockRecords() return Promise.resolve(cloneTask(task)) } @@ -1041,6 +1070,7 @@ export async function voidSigningTask( task.voidReason = reason task.signatureDataUrl = undefined task.updatedAt = formatMockDate() + persistMockRecords() return Promise.resolve(cloneTask(task)) } @@ -1063,6 +1093,7 @@ export async function resendSigningSms( task.status = 'pending' task.expiresAt = addHours('2026-08-27 12:00', 48) task.updatedAt = formatMockDate() + persistMockRecords() return Promise.resolve(cloneTask(task)) } diff --git a/clinical-web/src/components/signing/NewSigningTaskDialog.vue b/clinical-web/src/components/signing/NewSigningTaskDialog.vue index 6964e14..536306b 100644 --- a/clinical-web/src/components/signing/NewSigningTaskDialog.vue +++ b/clinical-web/src/components/signing/NewSigningTaskDialog.vue @@ -2,7 +2,7 @@ import { computed, nextTick, ref, watch } from 'vue' import { ElMessage } from 'element-plus' -import { isSigningMockEnabled } from '@/api/workbench/signing' +import { getMockPatientHints, isSigningMockEnabled } from '@/api/workbench/signing' import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types' import { useSigningTaskForm } from '@/composables/useSigningTaskForm' import { useAppStore } from '@/stores/app' @@ -20,6 +20,14 @@ const emit = defineEmits<{ const appStore = useAppStore() const patientInput = ref(null) const initialTemplate = computed(() => props.initialTemplate ?? null) +// 演示模式下列出可用的 Mock 患者,点一下直接定位,省得记患者 ID(真实模式为空数组) +const patientHints = getMockPatientHints() + +function usePatientHint(id: string) { + patientId.value = id + errors.patient = '' + void locatePatient() +} const { documentSearch, @@ -135,6 +143,18 @@ watch( {{ locating ? '定位中…' : '定位患者' }} +
+ 演示患者 + +
{{ patient.name }} @@ -962,4 +982,29 @@ watch( width: 100%; } } +.demo-patients { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin-top: 8px; +} + +.demo-patients__label { + color: var(--mut); + font-size: 11.5px; +} + +.demo-patients__chip { + padding: 3px 8px; + color: var(--brand); + font-size: 11.5px; + background: var(--brand-l); + border: none; + border-radius: 10px; +} + +.demo-patients__chip:hover { + filter: brightness(0.95); +} diff --git a/clinical-web/src/utils/mock-document-library.ts b/clinical-web/src/utils/mock-document-library.ts new file mode 100644 index 0000000..fc2511e --- /dev/null +++ b/clinical-web/src/utils/mock-document-library.ts @@ -0,0 +1,62 @@ +/** + * 本地演示文书库的读取与转换(仅 Mock 模式)。 + * + * 管理端文档库的数据只存在前端:科室在 Word 编辑器里建好文书后写进 localStorage。 + * 文档库页面和签署侧的模板列表都要读这份数据,共享同一份读取逻辑, + * 否则会出现「文档库刚建好文书,签署工作台却选不到」。 + * + * 这里只声明实际用到的字段,不绑定文档库页面的类型,避免 api 层反向依赖 views。 + */ +import type { SigningMethod, SigningTemplate } from '@/api/workbench/types' + +import { MOCK_STORAGE_KEYS, readMockState } from './mock-storage' + +interface LocalDocument { + id: string + name: string + code: string + version: string + description?: string + versionId?: string + departmentId?: string | null + campusId?: string | null + contentHtml?: string +} + +interface LocalCategory { + name: string + documents: LocalDocument[] +} + +interface LocalDepartment { + key: string + name: string + categories: LocalCategory[] +} + +/** + * 把本地保存的文档库摊平成签署模板。 + * 科室刚建好的文书因此能直接在签署工作台、文档库「发起」里选到。 + */ +export function readLocalSigningTemplates(): SigningTemplate[] { + const departments = readMockState(MOCK_STORAGE_KEYS.documentLibrary) ?? [] + + return departments.flatMap((department) => + (department.categories ?? []).flatMap((category) => + (category.documents ?? []).map((document) => ({ + id: document.id, + versionId: document.versionId ?? document.id, + name: document.name, + code: document.code, + version: document.version, + department: department.name, + departmentId: document.departmentId ?? null, + campusId: document.campusId ?? undefined, + category: category.name, + description: document.description ?? '', + supportedMethods: ['pad', 'sms'] as SigningMethod[], + contentHtml: document.contentHtml, + })), + ), + ) +} diff --git a/clinical-web/src/utils/mock-storage.ts b/clinical-web/src/utils/mock-storage.ts new file mode 100644 index 0000000..149787d --- /dev/null +++ b/clinical-web/src/utils/mock-storage.ts @@ -0,0 +1,46 @@ +/** + * 演示数据的本地持久化(仅 Mock 模式使用)。 + * + * 文档库与签署任务的演示数据原本只存在模块内存里,刷新页面就回到初始状态, + * 自测「文档库建文书 → 发起签署 → 签署 → 查看留痕」每次都要重来一遍。 + * 这里把这份数据镜像到 localStorage,刷新后可以接着测。 + * + * 只存演示数据,不涉及登录态、权限或真实患者信息;写入失败(容量超限、 + * 浏览器禁用存储)时静默降级为纯内存数据,不打断正在进行的操作。 + */ + +const STORAGE_PREFIX = 'medical-sign:mock:' + +/** 演示数据键名:清空演示数据时按这里的清单统一清理 */ +export const MOCK_STORAGE_KEYS = { + documentLibrary: `${STORAGE_PREFIX}document-library`, + signingTasks: `${STORAGE_PREFIX}signing-tasks`, +} as const + +export function readMockState(key: string): T | null { + try { + const raw = window.localStorage.getItem(key) + + return raw ? (JSON.parse(raw) as T) : null + } catch { + // 存储被禁用,或存量数据与当前结构不兼容:按「没有本地数据」处理,页面照常初始化 + return null + } +} + +export function writeMockState(key: string, value: unknown): void { + try { + window.localStorage.setItem(key, JSON.stringify(value)) + } catch { + // 容量超限或存储被禁用:退化为纯内存演示数据,不打扰使用者 + } +} + +/** 清空全部演示数据,调用方负责重新加载数据或刷新页面 */ +export function clearMockState(): void { + try { + Object.values(MOCK_STORAGE_KEYS).forEach((key) => window.localStorage.removeItem(key)) + } catch { + // 存储不可用时本来就无需清理 + } +} diff --git a/clinical-web/src/views/management/documents/components/DocumentLibraryHeader.vue b/clinical-web/src/views/management/documents/components/DocumentLibraryHeader.vue index 5138e52..e7e0173 100644 --- a/clinical-web/src/views/management/documents/components/DocumentLibraryHeader.vue +++ b/clinical-web/src/views/management/documents/components/DocumentLibraryHeader.vue @@ -8,6 +8,7 @@ const emit = defineEmits<{ 'update:modelValue': [value: string] add: [] export: [] + reset: [] }>() function handleInput(event: Event) { @@ -41,6 +42,14 @@ function handleInput(event: Event) { +
@@ -135,6 +144,17 @@ function handleInput(event: Event) { background: var(--brand-l); } +.tool-button.ghost { + color: var(--mut); + background: #fff; + border: 1px solid var(--line); +} + +.tool-button.ghost:hover { + color: var(--ink); + border-color: var(--mut); +} + @media (max-width: 760px) { .library-tools { width: 100%; diff --git a/clinical-web/src/views/management/documents/index.vue b/clinical-web/src/views/management/documents/index.vue index 810ad1c..6446f71 100644 --- a/clinical-web/src/views/management/documents/index.vue +++ b/clinical-web/src/views/management/documents/index.vue @@ -1,17 +1,19 @@