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'),
+)
@@ -83,7 +102,7 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
{{ template.name }}
- {{ status }}
+ {{ statusLabel }}
@@ -133,13 +152,16 @@ const togglePermission = computed(() => workflowPermission(toggleAction.value))
+
+
+
+ 上次生成失败;重新签署会创建新的签署会话。
+
+
diff --git a/clinical-web/src/views/workbench/signing/index.vue b/clinical-web/src/views/workbench/signing/index.vue
index 3d8833f..5989c13 100644
--- a/clinical-web/src/views/workbench/signing/index.vue
+++ b/clinical-web/src/views/workbench/signing/index.vue
@@ -163,8 +163,10 @@ const canConfirmTask = computed(() => canWrite('sign:task:confirm'))
/** 催办:提醒科室签署员尽快复核 */
const canRemindTask = computed(() => canWrite('sign:task:remind'))
const canViewEvents = computed(() => canRead('sign:task:event:query'))
-const canViewArtifacts = computed(() => canRead('sign:artifact:query'))
-const canDownloadArtifacts = computed(() => canRead('sign:artifact:download'))
+// Keep these permission codes aligned with SignArtifactController: artifact:view
+// lists the index, while task:download authorizes the PNG/PDF download.
+const canViewArtifacts = computed(() => canRead('sign:artifact:view'))
+const canDownloadArtifacts = computed(() => canRead('sign:task:download'))
/** 需要执行签署权限的任务动作,对应后端 `sign:task:sign`。 */
const SIGNING_ACTIONS: SigningTaskAction[] = [
diff --git a/clinical-web/vite.config.ts b/clinical-web/vite.config.ts
index 5ac8403..c7237be 100644
--- a/clinical-web/vite.config.ts
+++ b/clinical-web/vite.config.ts
@@ -1,7 +1,7 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
function resolveBuildId() {
const configured = process.env.BUILD_ID?.trim().replace(/[^a-zA-Z0-9_-]/g, '')
@@ -9,9 +9,13 @@ function resolveBuildId() {
}
// https://vite.dev/config/
-export default defineConfig(() => {
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), '')
const buildId = resolveBuildId()
- const apiProxyTarget = process.env.VITE_API_PROXY_TARGET?.trim() || 'https://ipad.shenynet.com'
+ const apiProxyTarget =
+ process.env.VITE_API_PROXY_TARGET?.trim() ||
+ env.VITE_API_PROXY_TARGET?.trim() ||
+ 'http://127.0.0.1:18080'
return {
plugins: [vue()],
@@ -25,6 +29,9 @@ export default defineConfig(() => {
},
},
server: {
+ // The public HTTPS virtual host terminates TLS in Nginx and proxies to this dev server.
+ // Permit only that configured hostname; keep Vite's DNS-rebinding protection enabled.
+ allowedHosts: ['fn.xianyi.de'],
proxy: {
'/api': {
target: apiProxyTarget,