diff --git a/clinical-web/src/api/management/documents.ts b/clinical-web/src/api/management/documents.ts index 12483be..5a0c83c 100644 --- a/clinical-web/src/api/management/documents.ts +++ b/clinical-web/src/api/management/documents.ts @@ -494,7 +494,7 @@ export async function getTemplateVersionDetail( } export type TemplateVersionAction = - 'submit-review' | 'reject' | 'approve' | 'publish' | 'disable' | 'archive' + 'submit-review' | 'reject' | 'approve' | 'publish' | 'enable' | 'disable' | 'archive' /** * 版本工作流动作。 diff --git a/clinical-web/src/api/workbench/signing.ts b/clinical-web/src/api/workbench/signing.ts index 8138863..867366f 100644 --- a/clinical-web/src/api/workbench/signing.ts +++ b/clinical-web/src/api/workbench/signing.ts @@ -155,6 +155,121 @@ const mockPatients: PatientProfile[] = [ }, ], }, + { + id: '10000000-0000-4000-8000-000000000001', + name: '测试患者001', + sex: '男', + age: 38, + idCard: '测试数据(未提供)', + phone: '未提供', + visits: [ + { + id: '20000000-0000-4000-8000-000000000001', + type: '门诊', + visitNo: 'TEST-VISIT-001', + department: '心内科', + departmentId: '00000000-0000-0000-0001-000000000003', + doctorName: '测试医生-心内科', + visitDate: '2026-09-29', + visitedAt: '2026-09-29T01:10:00Z', + campusId: '00000000-0000-0000-0000-000000000001', + sourceSystem: 'LOCAL_TEST', + isCurrent: true, + }, + ], + }, + { + id: '10000000-0000-4000-8000-000000000002', + name: '测试患者002', + sex: '女', + age: 50, + idCard: '测试数据(未提供)', + phone: '未提供', + visits: [ + { + id: '20000000-0000-4000-8000-000000000002', + type: '门诊', + visitNo: 'TEST-VISIT-002', + department: '放射科', + departmentId: '00000000-0000-0000-0001-000000000002', + doctorName: '测试医生-放射科', + visitDate: '2026-09-28', + visitedAt: '2026-09-28T02:25:00Z', + campusId: '00000000-0000-0000-0000-000000000001', + sourceSystem: 'LOCAL_TEST', + isCurrent: true, + }, + ], + }, + { + id: '10000000-0000-4000-8000-000000000003', + name: '测试患者003', + sex: '男', + age: 25, + idCard: '测试数据(未提供)', + phone: '未提供', + visits: [ + { + id: '20000000-0000-4000-8000-000000000003', + type: '门诊', + visitNo: 'TEST-VISIT-003', + department: '心内科', + departmentId: '00000000-0000-0000-0001-000000000003', + doctorName: '测试医生-心内科', + visitDate: '2026-09-27', + visitedAt: '2026-09-27T03:40:00Z', + campusId: '00000000-0000-0000-0000-000000000001', + sourceSystem: 'LOCAL_TEST', + isCurrent: true, + }, + ], + }, + { + id: '10000000-0000-4000-8000-000000000004', + name: '测试患者004', + sex: '女', + age: 34, + idCard: '测试数据(未提供)', + phone: '未提供', + visits: [ + { + id: '20000000-0000-4000-8000-000000000004', + type: '门诊', + visitNo: 'TEST-VISIT-004', + department: '放射科', + departmentId: '00000000-0000-0000-0001-000000000002', + doctorName: '测试医生-放射科', + visitDate: '2026-09-26', + visitedAt: '2026-09-26T04:55:00Z', + campusId: '00000000-0000-0000-0000-000000000001', + sourceSystem: 'LOCAL_TEST', + isCurrent: true, + }, + ], + }, + { + id: '10000000-0000-4000-8000-000000000005', + name: '测试患者005', + sex: '男', + age: 60, + idCard: '测试数据(未提供)', + phone: '未提供', + visits: [ + { + id: '20000000-0000-4000-8000-000000000005', + type: '门诊', + visitNo: 'TEST-VISIT-005', + department: '心内科', + departmentId: '00000000-0000-0000-0001-000000000003', + doctorName: '测试医生-心内科', + visitDate: '2026-09-25', + visitedAt: '2026-09-25T06:05:00Z', + campusId: '00000000-0000-0000-0000-000000000001', + sourceSystem: 'LOCAL_TEST', + isCurrent: true, + }, + ], + }, ] const defaultMockRecords: SigningTaskRecord[] = [ diff --git a/clinical-web/src/components/signing/SigningDocumentPreview.vue b/clinical-web/src/components/signing/SigningDocumentPreview.vue index bd576c8..d382147 100644 --- a/clinical-web/src/components/signing/SigningDocumentPreview.vue +++ b/clinical-web/src/components/signing/SigningDocumentPreview.vue @@ -59,7 +59,11 @@ import type { SigningTaskRecord } from '@/api/workbench/types' 患者 / 授权委托人签名:
患者签名 - {{ task.status === 'void' ? '—' : '未签署' }} + — + + 签名图片暂不可显示 + + 未签署
diff --git a/clinical-web/src/utils/request.ts b/clinical-web/src/utils/request.ts index 99700b5..dd57cc2 100644 --- a/clinical-web/src/utils/request.ts +++ b/clinical-web/src/utils/request.ts @@ -19,6 +19,15 @@ function redirectToLogin() { window.location.assign(`/login?redirect=${encodeURIComponent(redirect)}`) } +function isPublicSigningProofEndpoint(url: string | undefined) { + return Boolean( + url && + /\/v1\/sign-deliveries\/(?:token\/(?:consume|context|signature)|[^/?#]+\/signature)(?:[?#]|$)/.test( + url, + ), + ) +} + class Request { private readonly instance: AxiosInstance @@ -45,7 +54,11 @@ class Request { this.instance.interceptors.response.use( (response: AxiosResponse) => response, (error: unknown) => { - if (axios.isAxiosError(error) && error.response?.status === 401) { + if ( + axios.isAxiosError(error) && + error.response?.status === 401 && + !isPublicSigningProofEndpoint(error.config?.url) + ) { clearAuthStorage() redirectToLogin() } diff --git a/clinical-web/src/utils/signing-document.ts b/clinical-web/src/utils/signing-document.ts index 0d04e21..d65f4f3 100644 --- a/clinical-web/src/utils/signing-document.ts +++ b/clinical-web/src/utils/signing-document.ts @@ -790,6 +790,18 @@ export function buildSigningDocumentHtml( renderStructuredFields(root, answers, interactive, signatureActionable) renderTokenFields(root, answers, interactive, signatureActionable, claimed) + // 已签任务但图片产物暂时取不到时,不要把权威状态渲染成“未签署”。 + // 签名图本身仍以 SIGNATURE_IMAGE 产物为准;这里仅修正文案,避免误导查看者。 + if (data.signedAt && !answers[PRIMARY_SIGNATURE_FIELD_ID]) { + const pendingLabel = root.querySelector( + `[${FIELD_ATTR}="${PRIMARY_SIGNATURE_FIELD_ID}"] .docfld-empty`, + ) + + if (pendingLabel) { + pendingLabel.textContent = '签名图片暂不可显示' + } + } + const selected = new Set(data.selectedItems ?? []) convertCheckboxTables(root, interactive, selected) convertInlineOptions(root, interactive, selected) diff --git a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue index 90b9ef9..eb0f988 100644 --- a/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue +++ b/clinical-web/src/views/management/documents/components/DocumentTemplateCard.vue @@ -9,6 +9,8 @@ import { isGeneralOwnership, readLibraryStatus, readStatusTone, + templateWorkflowAction, + templateWorkflowLabel, } from '../library' import { DOC_TEMPLATE_PERMISSIONS, workflowPermission } from '../permissions' import type { DocumentTemplate } from '../types' @@ -27,7 +29,22 @@ const emit = defineEmits<{ }>() const status = computed(() => readLibraryStatus(props.template)) -const tone = computed(() => readStatusTone(status.value)) +const rawStatus = computed(() => props.template.backendStatus?.toUpperCase()) +const statusLabel = computed(() => { + if (rawStatus.value === 'DRAFT') { + return props.template.currentVersionId ? '有新版本待发布' : '待发布' + } + if (rawStatus.value === 'APPROVED') return '待发布' + if (rawStatus.value === 'PENDING_REVIEW') return '待审核' + if (rawStatus.value === 'REJECTED') return '已驳回' + if (rawStatus.value === 'ARCHIVED') return '已归档' + return status.value +}) +const tone = computed(() => + ['DRAFT', 'APPROVED', 'PENDING_REVIEW', 'REJECTED', 'ARCHIVED'].includes(rawStatus.value ?? '') + ? 'stopped' + : readStatusTone(status.value), +) const dotColor = computed(() => DOCUMENT_STATUS_DOT[tone.value]) const isGeneral = computed(() => isGeneralOwnership(props.template)) @@ -45,18 +62,19 @@ const ownershipLabel = computed(() => const sharedCount = computed(() => props.template.sharedDepartments?.length ?? 0) /** - * 只有「启用中」能发起签署;「停用」已下线。 - * - * 这里只看状态,不看正文——真实模式的列表接口不返回正文,按正文判断会让所有 - * 真实模板的「发起」按钮都消失。缺正文由发起那一步拦下(见 index.vue 的 toSigningTemplate)。 + * 真实模式必须存在 currentVersionId 才能发起签署;演示模式按「启用中 / 停用」判断。 + * 列表接口不返回正文,正文完整性由发起那一步检查(见 index.vue 的 toSigningTemplate)。 */ -const canInitiate = computed(() => canInitiateSigning(status.value)) +const canInitiate = computed(() => + props.template.backendStatus + ? Boolean(props.template.currentVersionId) + : canInitiateSigning(status.value), +) const initiateTitle = computed(() => - canInitiate.value ? '用此文书发起签署' : '仅「启用中」的文书可发起签署', + canInitiate.value ? '用此文书发起签署' : '该文书还没有已发布版本,无法发起签署', ) -/** 两档之间都能来回切,所以这个按钮没有「不适用」的状态 */ -const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停用')) +const toggleLabel = computed(() => templateWorkflowLabel(props.template)) /** * 检查类型 / 关联项目编码(原型 V10.2 卡片上的两条信息)。 @@ -67,14 +85,15 @@ const toggleLabel = computed(() => (status.value === '停用' ? '启用' : '停 const examType = computed(() => props.template.examType?.trim() ?? '') const projectCodes = computed(() => (props.template.projectCodes ?? []).filter(Boolean)) -/** 停用/启用走的是后端版本动作:停用 → disable,启用 → publish */ -const toggleAction = computed(() => - status.value === '停用' ? 'publish' : 'disable', +const toggleAction = computed(() => + templateWorkflowAction(props.template), ) // 按钮文案与权限码的对应关系集中在模板里,避免多处各写一份 const permissions = DOC_TEMPLATE_PERMISSIONS -const togglePermission = computed(() => workflowPermission(toggleAction.value)) +const togglePermission = computed(() => + workflowPermission(toggleAction.value ?? 'disable'), +) diff --git a/clinical-web/src/views/workbench/records/components/SigningRecordDetailDialog.vue b/clinical-web/src/views/workbench/records/components/SigningRecordDetailDialog.vue index 6da4f83..97745e6 100644 --- a/clinical-web/src/views/workbench/records/components/SigningRecordDetailDialog.vue +++ b/clinical-web/src/views/workbench/records/components/SigningRecordDetailDialog.vue @@ -125,6 +125,9 @@ function value(value: string | undefined) {
签署人签名 任务已作废,签名不保留 + + 签名图片暂不可显示 + 未采集签名
diff --git a/clinical-web/src/views/workbench/records/index.vue b/clinical-web/src/views/workbench/records/index.vue index 886fa64..26de302 100644 --- a/clinical-web/src/views/workbench/records/index.vue +++ b/clinical-web/src/views/workbench/records/index.vue @@ -3,7 +3,11 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { getCampuses, getDepartments } from '@/api/management/organization' -import { downloadSigningArtifact, getSigningArtifacts } from '@/api/workbench/artifacts' +import { + downloadSigningArtifact, + fetchSignatureDataUrl, + getSigningArtifacts, +} from '@/api/workbench/artifacts' import { mockCatalogCategories, mockCatalogDepartments } from '@/api/workbench/mock-catalog' import { getAllSigningRecords, @@ -120,7 +124,8 @@ const canView = computed(() => canRead('sign:record:view')) const canExport = computed( () => !loginStore.isReadOnly && canRead('sign:record:export'), ) -const canDownloadArtifact = computed(() => canRead('sign:artifact:download')) +const canViewArtifacts = computed(() => canRead('sign:artifact:view')) +const canDownloadArtifact = computed(() => canRead('sign:task:download')) const campusOptions = computed[]>(() => [ { label: '全部院区', value: 'all' }, @@ -276,9 +281,13 @@ async function openRecord(row: SigningRecordRow) { const requestId = ++detailRequestId - const [detailResult, eventsResult] = await Promise.allSettled([ + const isSigned = row.status === 'signed' || row.status === 'confirming' + const [detailResult, eventsResult, artifactsResult] = await Promise.allSettled([ getSigningRecordDetail(row.id, signingApiOptions.value), getSigningRecordEvents(row.id), + isSigned && canViewArtifacts.value && canDownloadArtifact.value + ? getSigningArtifacts(row.id) + : Promise.resolve([]), ]) if (requestId !== detailRequestId) { @@ -288,11 +297,37 @@ async function openRecord(row: SigningRecordRow) { if (detailResult.status === 'fulfilled' && detailResult.value) { // 任务接口不返回文档类别与正文,正文靠详情接口补回; // 类别沿用列表里带出的那一份,避免被详情里的空值覆盖 - detailRecord.value = { + let record = { ...row, ...detailResult.value, category: detailResult.value.category || row.category, } + + // 签署图片是独立的 SIGNATURE_IMAGE 产物,不在任务详情接口中。 + // 记录详情也要像签署工作台一样取回图片,否则已签任务的文书位会误显示“未签署”。 + if (isSigned && !record.signatureDataUrl && artifactsResult.status === 'fulfilled') { + const signatureArtifact = artifactsResult.value.find( + (artifact) => artifact.artifactType === 'SIGNATURE_IMAGE', + ) + + if (signatureArtifact) { + const signatureDataUrl = await fetchSignatureDataUrl(signatureArtifact) + + if (requestId !== detailRequestId) { + return + } + + if (signatureDataUrl) { + record = { + ...record, + signatureDataUrl, + fieldAnswers: { ...(record.fieldAnswers ?? {}), signature: signatureDataUrl }, + } + } + } + } + + detailRecord.value = record } if (eventsResult.status === 'fulfilled') { diff --git a/clinical-web/src/views/workbench/signing/components/SigningTaskDetail.vue b/clinical-web/src/views/workbench/signing/components/SigningTaskDetail.vue index a604892..d613897 100644 --- a/clinical-web/src/views/workbench/signing/components/SigningTaskDetail.vue +++ b/clinical-web/src/views/workbench/signing/components/SigningTaskDetail.vue @@ -270,6 +270,28 @@ defineExpose({ getDocumentElement, locateSignatureSlot }) + +