/** * 文档库的演示数据与展示文案。 * * 状态标签放在这里而不是随演示数据一起删掉:标签是纯展示映射, * 接真实接口后照样要用,不能和"演示数据"捆在一起。 * * 演示文书库的种子与读写逻辑在 utils/mock-document-library: * 文档库页面和签署工作台必须读同一份数据,否则会出现「文档库里维护的模板,签署端选不到」, * 或者签署端混着一批文档库里看不到、也无法维护的模板。 */ import type { DocumentRecord } from '@/api/management/types' import type { LocalDocument, LocalDepartment } from '@/utils/mock-document-library' import { readDocumentLibrary, writeDocumentLibrary } from '@/utils/mock-document-library' import type { DocumentDepartment, DocumentBackendStatus, DocumentStatus } from './types' /** * 演示文库与页面模型之间的归一。 * * utils/mock-document-library 刻意只声明自己用到的字段(不反向依赖 views 的类型), * 它返回的文书缺了页面模型要求的 category / updatedBy / updatedAt,code 也可能为空。 * 两种口径不一致会在 loadLibrary / saveDocumentLibrary 处直接报类型错误, * 因此在这一层做一次显式补齐——而不是让 utils 反过来依赖 views, * 也不是把页面的必填字段放宽成可选(放宽会掩盖真实接口返回缺字段的问题)。 * * 补齐用的值都标注了是演示数据的缺省值,不冒充真实字段: * updatedAt 留空串,页面会显示成"—",不会假装有一份真实的更新时间。 */ function toDocumentRecord(document: LocalDocument, categoryName: string): DocumentRecord { return { ...document, code: document.code ?? '', category: categoryName, updatedBy: '演示数据', updatedAt: document.updatedAt ?? '', } } export function toDocumentDepartment(department: LocalDepartment): DocumentDepartment { return { ...department, categories: department.categories.map((category) => ({ ...category, documents: category.documents.map((document) => toDocumentRecord(document, category.name)), })), } } /** 读取演示文书库:优先用本地保存的,没有则用种子(调用方负责再克隆一份使用) */ export function loadDocumentLibrary(): DocumentDepartment[] { return readDocumentLibrary().map(toDocumentDepartment) } /** * 保存演示文书库。 * 落本地时要把 category 摘掉(LocalDocument 没有 category 字段, * 分类信息在 categories 这一层的 name 上),否则多存一个字段会在下次读取时对不上。 * updatedBy 同理:它是页面模型为「必须有人维护」补的展示字段,不是演示文库的数据。 */ export function saveDocumentLibrary(library: DocumentDepartment[]): void { writeDocumentLibrary( library.map((department) => ({ ...department, categories: department.categories.map((category) => ({ name: category.name, documents: category.documents.map(toLocalDocument), })), })), ) } /** * 页面模型的文书 → 演示文库存储结构。 * * 逐字段列出而不是解构剔除:解构剔除(`const { category, ...rest }`)会因为 * 被剔除的变量未使用而触发 lint,且多加一个页面专用字段时容易忘掉同步。 * 这里明确列出演示文库认识哪些字段,多出来的页面字段(category / updatedBy)自然被丢掉。 */ function toLocalDocument(document: DocumentRecord): LocalDocument { return { id: document.id, name: document.name, code: document.code, version: document.version, status: document.status, backendStatus: document.backendStatus, description: document.description ?? '', versionId: document.versionId, departmentId: document.departmentId, departmentName: document.departmentName, campusId: document.campusId, campusName: document.campusName, updatedAt: document.updatedAt, contentHtml: document.contentHtml, contentSha256: document.contentSha256, } } /** * 状态标签。键集合与后端模板状态枚举一一对应 * (见 api/management/types.ts 的 BackendTemplateStatus 与 documents.ts 的 normalizeDocumentStatus), * 不再有后端枚举里不存在的 enabled。 */ export const documentStatusLabels: Record = { draft: '草稿', review: '需复审', published: '已发布', archived: '已归档', } /** * 后端原始状态 → 中文标签。 * * 卡片与预览弹窗用它,而不是归一后的 documentStatusLabels:归一过程把 * PENDING_REVIEW(待审核)和 REJECTED(已驳回)都并成了 review, * 只看「需复审」分不清是「还等着审」还是「已经被打回」。 */ export const backendStatusLabels: Record = { DRAFT: '草稿', PENDING_REVIEW: '待审核', REJECTED: '已驳回', APPROVED: '审核通过', PUBLISHED: '已发布', DISABLED: '已停用', ARCHIVED: '已归档', } /** * 状态页签的固定展示顺序:按文书生命周期排列(草稿 → 送审 → 驳回/通过 → 发布 → 停用 → 归档)。 * 固定顺序而不是按接口返回顺序,否则同一个库里状态一多,页签会来回跳。 */ export const backendStatusOrder: DocumentBackendStatus[] = [ 'DRAFT', 'PENDING_REVIEW', 'REJECTED', 'APPROVED', 'PUBLISHED', 'DISABLED', 'ARCHIVED', ] /** 取状态标签:优先用后端原始状态(更精确),拿不到再退回归一后的状态 */ export function readStatusLabel(template: { status: DocumentStatus backendStatus?: string }): string { const backendLabel = template.backendStatus ? backendStatusLabels[template.backendStatus as DocumentBackendStatus] : undefined return backendLabel ?? documentStatusLabels[template.status] } /** * 按后端状态取标签。 * 版本历史只有后端状态,没有归一后的 status,因此单独开一个入口。 * 遇到未知状态原样显示,不显示成空白——后端新增状态时至少能看见它是什么。 */ export function readBackendStatusLabel(status: string | null | undefined): string { if (!status) { return '—' } return backendStatusLabels[status as DocumentBackendStatus] ?? status } /** * 状态色调:决定状态徽标用哪一套配色。 * * 不能直接用归一后的 DocumentStatus 当配色键——PENDING_REVIEW 与 REJECTED * 归一后都是 review,两者会共用同一个颜色,页签与文字已经区分开了、颜色却还糊在一起。 */ export type DocumentStatusTone = 'draft' | 'review' | 'rejected' | 'published' | 'archived' export function readStatusTone(template: { status: DocumentStatus backendStatus?: string }): DocumentStatusTone { switch (readBackendStatus(template)) { case 'PENDING_REVIEW': return 'review' case 'REJECTED': return 'rejected' case 'APPROVED': case 'PUBLISHED': return 'published' case 'DISABLED': case 'ARCHIVED': return 'archived' case 'DRAFT': default: return 'draft' } } /** * 取文书的后端原始状态,供状态页签归类。 * * 真实模式的列表接口会给 backendStatus;演示库与部分历史数据可能没有, * 这时按归一后的 status 反推一个等价状态,避免这类文书在页签里被漏掉。 * * 反推对 'review' 是有损的:归一过程把 PENDING_REVIEW 与 REJECTED 合并了, * 只能归到 PENDING_REVIEW。这只是缺字段时的兜底,不改变页签按真实状态过滤的口径。 */ export function readBackendStatus(template: { status: DocumentStatus backendStatus?: string }): DocumentBackendStatus { const known = backendStatusOrder.find((status) => status === template.backendStatus) if (known) { return known } switch (template.status) { case 'published': return 'PUBLISHED' case 'review': return 'PENDING_REVIEW' case 'archived': return 'ARCHIVED' case 'draft': default: return 'DRAFT' } }