diff --git a/clinical-web/.env.example b/clinical-web/.env.example index 70e5114..27edbbf 100644 --- a/clinical-web/.env.example +++ b/clinical-web/.env.example @@ -3,7 +3,7 @@ VITE_API_BASE_URL=/api # Mock 开关:按业务模块(对齐侧边栏菜单)独立控制,取值只认 true / false。 # 留空或注释掉即使用该模块的缺省值,缺省值以 src/utils/mock-flags.ts 为准, -# 目前是「签署工作台 + 文档库走演示数据,其余模块走真实接口」。 +# 目前只有签署工作台缺省走演示数据;文档库及其余已接入模块缺省走真实接口。 # # 建议在本地 .env.*.local 里显式声明这两个开关,而不是依赖缺省值: # 缺省值只保证「不改代码时的默认行为」,缺省值一旦调整, @@ -11,7 +11,7 @@ VITE_API_BASE_URL=/api # 「接口报错」或「数据不对」,排查成本很高。 # VITE_MOCK_WORKBENCH_HOME=false # VITE_MOCK_SIGNING=true -# VITE_MOCK_DOCUMENTS=true +# VITE_MOCK_DOCUMENTS=false # VITE_MOCK_DOCUMENT_PERMISSIONS=false # VITE_MOCK_REPORTS=false # VITE_MOCK_USERS=false diff --git a/clinical-web/README.md b/clinical-web/README.md index 61d8b3a..c2911ab 100644 --- a/clinical-web/README.md +++ b/clinical-web/README.md @@ -90,11 +90,11 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适 - 列表读 `GET /api/v1/templates`(自动翻完所有页),再把扁平的模板列表组织成页面需要的「科室 → 分类 → 文书」树; - 正文不在列表接口里(`TemplateResponseDto` 只给 `templateContentSha256`,不给正文),预览、编辑、发起签署前都要再取一次版本详情 `GET /api/v1/templates/{id}/versions/{versionId}` 拿 `contentHtml`。取不到正文时页面如实报错,不展示空壳模板; -- 新建走「建模板 + 建首版」两步,编辑走「建新版本 + 更新模板」;模板没有 DELETE 接口,页面上的「删除」实际是归档动作(`POST .../versions/{versionId}/archive`),保留历史留痕链。 +- 新建走「建模板 + 建首版」两步,编辑走「建新版本 + 更新模板」;版本号由后端按并发安全序号生成。列表和详情分别使用最新维护版本与当前发布版本指针,审核编辑不会误操作旧发布版,发起签署也不会误用待审核草稿。模板没有 DELETE 接口,页面上的「删除」实际是归档动作(`POST .../versions/{versionId}/archive`),保留历史留痕链。 签署工作台的真实模式同样依赖正文:`GET /api/v1/templates/available-versions` 只返回 `contentSha256`,签署端用这个哈希当缓存键去换回正文快照(同一哈希命中同一份正文,版本更新后哈希变化即自动失效),避免版本更新后仍按旧正文签署。取不到正文时任务详情如实提示并给出占位版式,不退化成「看起来像真文书」的假纸样。 -自测与演示:开启 `VITE_MOCK_DOCUMENTS` / `VITE_MOCK_SIGNING`(两者缺省即为开启)时,文档库和签署任务的演示数据会镜像到 localStorage(见 `utils/mock-storage.ts`),刷新页面后仍可继续,不必每次重建文书、重发签署;文档库页头的「重置演示数据」可清空这些本地数据回到初始状态(该按钮只在演示模式下出现)。持久化只针对演示数据,不涉及登录态与真实患者信息。本地建议在 `.env.development.local` 里显式写出这两个开关,不要依赖缺省值。 +自测与演示:显式开启 `VITE_MOCK_DOCUMENTS=true` 或 `VITE_MOCK_SIGNING=true` 时,文档库和签署任务的演示数据会镜像到 localStorage(见 `utils/mock-storage.ts`),刷新页面后仍可继续,不必每次重建文书、重发签署;文档库页头的「重置演示数据」可清空这些本地数据回到初始状态(该按钮只在演示模式下出现)。文档库缺省使用真实接口,签署工作台暂时缺省使用演示数据;持久化只针对演示数据,不涉及登录态与真实患者信息。本地建议在 `.env.development.local` 里显式写出开关,不要依赖缺省值。 ## 当前路由结构 diff --git a/clinical-web/src/api/management/documents.ts b/clinical-web/src/api/management/documents.ts index b12883c..44d2514 100644 --- a/clinical-web/src/api/management/documents.ts +++ b/clinical-web/src/api/management/documents.ts @@ -17,6 +17,7 @@ import type { DocumentRecord, TemplateResponseDto, TemplateVersionResponseDto, + UpdateTemplateRequest, } from './types' const useMockData = mockFlags.documents @@ -89,8 +90,9 @@ function normalizeDocumentStatus(status: BackendTemplateStatus | null | undefine } function mapDocument(dto: TemplateResponseDto): DocumentRecord { - const versionId = dto.currentVersionId ?? undefined - const version = dto.versionSequence ? `v${dto.versionSequence}` : '—' + const versionId = dto.latestVersionId ?? dto.currentVersionId ?? undefined + const version = dto.latestVersionNo ?? (dto.versionSequence ? `v${dto.versionSequence}` : '—') + const backendStatus = dto.latestVersionStatus ?? dto.status return { id: dto.id, @@ -98,11 +100,12 @@ function mapDocument(dto: TemplateResponseDto): DocumentRecord { code: dto.templateCode, category: dto.category ?? '未分类', version: String(version), - status: normalizeDocumentStatus(dto.status), - backendStatus: dto.status ?? undefined, + status: normalizeDocumentStatus(backendStatus), + backendStatus: backendStatus ?? undefined, updatedBy: dto.updatedBy ?? dto.createdBy ?? '—', updatedAt: formatTemplateDateTime(dto.updatedAt ?? dto.createdAt), versionId, + currentVersionId: dto.currentVersionId ?? undefined, departmentId: dto.departmentId, departmentName: dto.departmentId ? undefined : '全院通用', campusId: dto.campusId, @@ -143,21 +146,21 @@ export function buildSignatureFieldsPayload(contentHtml: string): unknown[] { /** * 读取模板当前版本的正文。 - * 模板列表接口只给 currentVersionId 与哈希,不给正文, + * 模板列表接口只给版本指针与哈希,不给正文, * 因此展示/编辑正文必须再取一次版本详情——这是文档库能否接入的关键一步。 */ -export async function getTemplateContent(templateId: string): Promise { +export async function getTemplateContent(templateId: string, versionId?: string): Promise { if (useMockData) { return '' } - const template = await getDocumentDetail(templateId) + const resolvedVersionId = versionId ?? (await getDocumentDetail(templateId))?.versionId - if (!template?.versionId) { + if (!resolvedVersionId) { return '' } - const version = await getTemplateVersionDetail(templateId, template.versionId) + const version = await getTemplateVersionDetail(templateId, resolvedVersionId) return readVersionContentHtml(version) } @@ -235,7 +238,10 @@ async function findContentByVersionId(templateVersionId: string): Promise page.records) - const matched = candidates.find((template) => template.versionId === templateVersionId) + const matched = candidates.find( + (template) => + template.versionId === templateVersionId || template.currentVersionId === templateVersionId, + ) if (!matched) { return '' @@ -376,7 +382,7 @@ export async function createTemplate(payload: CreateTemplateRequest): Promise, + payload: UpdateTemplateRequest, ): Promise { if (useMockData) { throw new Error('演示模式下请在文档库页面直接编辑文书') @@ -506,7 +512,7 @@ export async function createTemplateWithContent( signatureFields: buildSignatureFieldsPayload(version.contentHtml), }) - // 版本创建后模板的 currentVersionId/versionSequence 才更新,重新取一次拿到准确版本号 + // 版本创建后模板的 latestVersionId/versionSequence 才更新,重新取一次拿到准确版本号 const detail = await getDocumentDetail(template.id) return detail ?? template } diff --git a/clinical-web/src/api/management/types.ts b/clinical-web/src/api/management/types.ts index 00c7448..0683151 100644 --- a/clinical-web/src/api/management/types.ts +++ b/clinical-web/src/api/management/types.ts @@ -27,6 +27,8 @@ export interface DocumentRecord { description?: string contentHtml?: string contentSha256?: string + /** 当前已发布版本;发起签署必须使用它,不能误用最新草稿版本。 */ + currentVersionId?: string } export type DocumentListResponse = PageResult @@ -51,6 +53,9 @@ export interface TemplateResponseDto { category?: string | null status?: BackendTemplateStatus | null currentVersionId?: string | null + latestVersionId?: string | null + latestVersionNo?: string | null + latestVersionStatus?: BackendTemplateStatus | null versionSequence?: number | null createdAt?: string | null createdBy?: string | null @@ -93,6 +98,12 @@ export interface CreateTemplateRequest { category?: string } +export interface UpdateTemplateRequest { + name: string + description?: string + category?: string +} + export interface CreateTemplateVersionRequest { versionNo?: string contentHtml: string diff --git a/clinical-web/src/utils/mock-flags.ts b/clinical-web/src/utils/mock-flags.ts index ab7af41..26d8964 100644 --- a/clinical-web/src/utils/mock-flags.ts +++ b/clinical-web/src/utils/mock-flags.ts @@ -32,7 +32,7 @@ export const mockFlags = { /** 签署工作台:签署任务、签署产物,以及仅用于签署留痕的审计日志 */ signing: resolveMockFlag(import.meta.env.VITE_MOCK_SIGNING, true), /** 文档库管理:文书库与文书模板接口 */ - documents: resolveMockFlag(import.meta.env.VITE_MOCK_DOCUMENTS, true), + documents: resolveMockFlag(import.meta.env.VITE_MOCK_DOCUMENTS, false), /** 文档权限:模板权限配置与权限矩阵 */ documentPermissions: resolveMockFlag(import.meta.env.VITE_MOCK_DOCUMENT_PERMISSIONS, false), /** 报表分析 */ diff --git a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue index b66aa69..8e96480 100644 --- a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue +++ b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue @@ -10,6 +10,7 @@ const props = defineProps<{ template: DocumentTemplate canEdit: boolean canInitiate: boolean + canWorkflow: (action: TemplateVersionAction) => boolean canRemove: boolean /** 演示模式下「删除」是真删;真实模式下语义是下架(模板无 DELETE 接口,走归档) */ isMock: boolean @@ -31,9 +32,7 @@ const statusTone = computed(() => readStatusTone(props.template)) /** 只有已发布 / 待复审的模板可以发起签署;草稿与已归档不可用 */ const canInitiateTemplate = computed( - () => - props.canInitiate && - (props.template.status === 'published' || props.template.status === 'review'), + () => props.canInitiate && Boolean(props.template.currentVersionId || props.isMock), ) const initiateTitle = computed(() => @@ -64,6 +63,8 @@ const primaryAction = computed<{ key: TemplateVersionAction; label: string } | n return { key: 'submit-review', label: '重新提交' } case 'DISABLED': return { key: 'publish', label: '重新启用' } + case 'APPROVED': + return { key: 'publish', label: '发布' } default: return null } @@ -75,21 +76,21 @@ const primaryAction = computed<{ key: TemplateVersionAction; label: string } | n * 「驳回」不能只做成主动作的反面:审核通过和驳回是审核人的一对选择, * 必须同时可见,否则待审核的文书就只能通过、不能打回。 */ -const secondaryActions = computed< - { key: TemplateVersionAction; label: string; title: string }[] ->(() => { - if (props.template.backendStatus === 'PENDING_REVIEW') { - return [ - { - key: 'reject', - label: '驳回', - title: '驳回后需填写原因,科室按意见修改并重新提交', - }, - ] - } +const secondaryActions = computed<{ key: TemplateVersionAction; label: string; title: string }[]>( + () => { + if (props.template.backendStatus === 'PENDING_REVIEW' && props.canWorkflow('reject')) { + return [ + { + key: 'reject', + label: '驳回', + title: '驳回后需填写原因,科室按意见修改并重新提交', + }, + ] + } - return [] -}) + return [] + }, +) /** * 本次交付暂不包含「版本历史」与「删除 / 下架」入口,先隐藏对应按钮。 @@ -136,10 +137,9 @@ const removeTitle = computed(() => (props.isMock ? '删除此文书' : '下架 版本