feat(文档库): 打通模板正文真实模式并新增渲染验证

模板列表接口不返回正文,展示、编辑与发起签署前需再取版本详情获取 contentHtml,
为此新增 getTemplateContent、createTemplateWithContent、updateTemplateContent 与
deleteTemplate(归档),并在签署端按 contentSha256 缓存正文快照。

同时新增 scripts/login.cjs 与 scripts/verify-pages.cjs,通过真实登录与 CDP
渲染截图、断言关键内容并收集运行时错误,并补充 .gitignore、eslint 忽略与文档说明。
This commit is contained in:
yelan
2026-09-17 14:10:45 +08:00
parent 0d271d90be
commit 81922d774f
20 changed files with 1640 additions and 187 deletions
+89 -10
View File
@@ -17,30 +17,46 @@ import type { SigningMethod, SigningTemplate } from '@/api/workbench/types'
import { MOCK_STORAGE_KEYS, readMockState, writeMockState } from './mock-storage'
/** 与 views/management/documents/types.ts 的 DocumentStatus 保持一致 */
type LocalDocumentStatus = 'enabled' | 'draft' | 'archived' | 'review'
/**
* 演示文书的状态。
* 取值与 api/management/types.ts 的 DocumentStatus 一致(对应后端 DRAFT / PENDING_REVIEW /
* APPROVED / PUBLISHED / DISABLED / ARCHIVED 的归一结果),不再使用后端没有的 'enabled'。
*/
type LocalDocumentStatus = 'draft' | 'review' | 'published' | 'archived'
/** 与 views/management/documents/types.ts 的 DocumentCampus 保持一致 */
type LocalCampus = 'all' | '本部院区' | '东院区' | '西院区'
type LocalCampus = string
interface LocalDocument {
export interface LocalDocument {
id: string
name: string
code: string
/** 文书编码。与模板接口的 DocumentRecord.code 一样允许缺省,页面统一兜底显示 */
code?: string
version: string
status: LocalDocumentStatus
description: string
backendStatus?: string
/** 文书说明。来源同 code,允许缺省 */
description?: string
versionId?: string
departmentId?: string | null
departmentName?: string
campusId?: string | null
campusName?: string
/**
* 演示数据的更新时间。
* 演示文库的种子不带时间,页面在这一层会显示成"—"而不是伪造一个时间戳;
* 页面模型要求这个字段必填,所以在演示数据创建时写一次真实时间。
*/
updatedAt?: string
contentHtml?: string
contentSha256?: string
}
interface LocalCategory {
export interface LocalCategory {
name: string
documents: LocalDocument[]
}
interface LocalDepartment {
export interface LocalDepartment {
key: string
name: string
campus: LocalCampus
@@ -199,10 +215,62 @@ const DEMO_DOCUMENT_SPECS: DemoDocumentSpec[] = [
/** 数组顺序即文档库里的科室分组顺序,全院通用排在最前 */
const DEMO_DEPARTMENT_ORDER = ['全院通用', '放射科', '消化内科', '心内科', '健康管理中心', '医务处']
/**
* 演示院区字典。
*
* 文档库的「所属院区」下拉在真实模式下取 api/management/organization 的 getCampuses,
* 演示模式下没有接口可调,这里给一份同构的演示院区,键名与 CampusRecord 一致。
* 演示 id 带 mock- 前缀,一眼能看出不是真实 UUID,不会误当成可提交给后端的取值。
*/
export const demoCampuses = [
{ id: 'mock-campus-main', code: 'MAIN', name: '本部院区', status: 'ENABLED' as const, sortNo: 1 },
{
id: 'mock-campus-east',
code: 'EAST',
name: '东院区',
status: 'ENABLED' as const,
sortNo: 2,
},
]
/**
* 演示科室字典。
* 文档库的科室下拉在演示模式下用它,真实模式下改由 getDepartments(campusId) 提供。
* 科室名与演示文书库的分组名刻意保持一致(放射科、消化内科……),
* 否则会出现"下拉里选不到文书实际所属的科室"。
*/
export const demoDepartments = [
'医学影像科',
'医学检验科',
'临床遗传中心',
'放射科',
'消化内科',
'心内科',
'健康管理中心',
'医务处',
].map((name, index) => ({
id: `mock-department-${index + 1}`,
campusId: '',
code: `MOCK-DEPT-${index + 1}`,
name,
parentId: null,
status: 'ENABLED' as const,
sortNo: index + 1,
}))
function groupKeyOf(department: string) {
return `demo-${DEMO_DEPARTMENT_ORDER.indexOf(department)}`
}
/**
* 演示文书库种子。
*
* updatedAt 用固定的历史时间而不是"当前时间":
* 每次构建都取 now 的话,同一份种子文书在每次刷新后更新时间都会变,
* 看起来像有人在不停改动文书,反而失真。
*/
const DEMO_UPDATED_AT = '2026-08-20T09:30:00+08:00'
function buildDemoLibrary(): LocalDepartment[] {
return DEMO_DEPARTMENT_ORDER.map((department) => {
const specs = DEMO_DOCUMENT_SPECS.filter((spec) => spec.department === department)
@@ -215,11 +283,15 @@ function buildDemoLibrary(): LocalDepartment[] {
name: spec.name,
code: spec.code,
version: spec.version,
status: 'enabled',
status: 'published',
backendStatus: 'PUBLISHED',
description: spec.description,
versionId: spec.versionId,
departmentId: null,
departmentName: department,
campusId: null,
campusName: '全院通用',
updatedAt: DEMO_UPDATED_AT,
contentHtml: buildDemoDocumentHtml(spec),
})
categories.set(spec.category, documents)
@@ -253,6 +325,10 @@ export function writeDocumentLibrary(library: LocalDepartment[]): void {
/**
* 把本地保存的文档库摊平成签署模板。
* 文档库是模板的唯一来源:签署工作台能选到的模板,一定是在文档库里能维护的那几份。
*
* contentSha256 一并带出:真实模式下可用模板接口只给哈希不给正文,
* 签署端要靠这个哈希去换回正文快照;演示模式虽然不缺正文,但两边字段保持一致,
* 页面就不用按模式分叉。
*/
export function readLocalSigningTemplates(): SigningTemplate[] {
return readDocumentLibrary().flatMap((department) =>
@@ -261,7 +337,9 @@ export function readLocalSigningTemplates(): SigningTemplate[] {
id: document.id,
versionId: document.versionId ?? document.id,
name: document.name,
code: document.code,
// SigningTemplate.code 必填而演示文书可能没有编码,这里如实兜底为空串,
// 不编一个看起来像真实编码的假值
code: document.code ?? '',
version: document.version,
department: department.name,
departmentId: document.departmentId ?? null,
@@ -270,6 +348,7 @@ export function readLocalSigningTemplates(): SigningTemplate[] {
description: document.description ?? '',
supportedMethods: ['pad', 'sms'] as SigningMethod[],
contentHtml: document.contentHtml,
contentSha256: document.contentSha256,
})),
),
)