feat(签署任务): 新增「待科室确认」节点整条链路
原型 V10.2 的 needConfirm:检验 / 基因检测 / 自费类文书要求患者签完之后 由科室再复核签章,避免漏签补签。此前仓库完全没有这个节点,现在补全。 状态模型 - SigningTaskStatus 增加 confirming(待科室确认),BackendSigningTaskStatus 增加 WAITING_CONFIRM;文档侧 DocumentRecord / TemplateResponseDto / SigningTemplate / CreateSigningTaskInput 增加 needConfirm。 - normalizeTaskStatus、mapSigningTask、mapDocument、toBackendTaskQuery 的 statusMap、 mapAvailableTemplate、readLocalSigningTemplates 同步;needConfirm 取不到一律按 false, 不凭空给任务多插一道人工节点。 流转 - completeSigningTask 按 needConfirm 决定落 confirming 还是 signed; 新增 confirmDepartmentSigning(科室复核签章 → 已签署)。 真实模式暂未接入(后端要重做),如实提示"待接入",不前端自己改状态。 - 任务事件与记录页事件补「已推送至 X 确认队列」「科室复核签章完成,文档归档」两条留痕。 工作台 - 列表徽标走 .task-status--confirming(原型 tg-cfm 橙),状态筛选加「待科室确认」。 - 详情动作按原型给:🏛 科室复核签章(确认完成)+ 催办 + 在线预览 + 下载已签 PDF; 该状态下不给「作废」(原型只在待签署/已超时给)。 - 新增「科室确认节点」卡(待患者签署后 / 待科室确认 / ✓ 已确认三分支)—— 原型放在文书签名区那一格,这里做成紧跟文书的独立卡,真实模板与回退纸样都能显示。 - confirming 时文书只读(患者已签完,不该再改填写项)。 - 签署完成提示语按 needConfirm 分流:需科室确认的说「已流转至科室确认节点」, 否则说「签署完成,文档已归档」—— 一律说"完成"会让人以为可以关页面了。 - 新增权限码 sign:task:confirm / sign:task:remind。 记录查询与报表 - 记录查询状态筛选与演示数据加该状态(2 条样本)。 - 报表 STATUS_LABELS / STATUS_COLUMNS / ReportStatusCounts / ReportGroupRow 加这一栏, 单独成列不并进「已签署」——并进去会让已签署虚高,科室复核本来就是这类文书的卡点。 - 时效样本按「有签署用时」取,待科室确认同样计入;总览统计块加一块。 演示数据 - 工作台补 task-009(confirming + needConfirm),否则该状态的徽标、动作、 筛选、报表栏在演示环境里一次都走不到。 - 注意:演示文库的麻精同意书**没有** needConfirm(原型里也确实没有), 所以「新建任务 → 签署 → 落待科室确认」这条流转靠探针夹具验证,没有改产品种子。 验证:probe-dept-confirm.cjs(新建)36/36 PASS,含真实落笔签署后落待科室确认; 回归 probe-new-task 104/104、probe-method-domain 14/14、probe-workbench-lib 42/42、 probe-report-analytics 92/92(断言按七状态更新);vue-tsc 与 eslint 0 错。
This commit is contained in:
@@ -114,6 +114,9 @@ function mapDocument(dto: TemplateResponseDto): DocumentRecord {
|
||||
// 现在取不到就是 undefined,卡片不渲染对应标签,不伪造取值
|
||||
examType: dto.examType ?? undefined,
|
||||
projectCodes: dto.projectCodes ?? undefined,
|
||||
// 需科室确认:后端没给就是 false(走原路直接归档),
|
||||
// 不能默认成 true —— 那会给所有文书凭空多插一道人工节点
|
||||
needConfirm: dto.needConfirm ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ export interface DocumentRecord {
|
||||
* 同样只在后端提供时才有值,缺省即不显示。
|
||||
*/
|
||||
projectCodes?: string[]
|
||||
/**
|
||||
* 患者签署后是否需要「科室复核签章」节点(原型 V10.2 的 needConfirm)。
|
||||
*
|
||||
* 检验 / 基因检测 / 自费类文书要科室再确认一次,避免漏签补签;
|
||||
* 发起签署时它随文书带到任务上,决定签完落「待科室确认」还是「已签署」。
|
||||
*/
|
||||
needConfirm?: boolean
|
||||
contentHtml?: string
|
||||
contentSha256?: string
|
||||
/** 当前已发布版本;发起签署必须使用它,不能误用最新草稿版本。 */
|
||||
@@ -83,6 +90,8 @@ export interface TemplateResponseDto {
|
||||
*/
|
||||
examType?: string | null
|
||||
projectCodes?: string[] | null
|
||||
/** 患者签署后是否需要科室复核签章;取不到即按 false(直接归档)。 */
|
||||
needConfirm?: boolean | null
|
||||
status?: BackendTemplateStatus | null
|
||||
currentVersionId?: string | null
|
||||
latestVersionId?: string | null
|
||||
@@ -331,7 +340,15 @@ export interface OrganizationResponseDto {
|
||||
}
|
||||
|
||||
export type ReportBackendTaskStatus =
|
||||
'CREATED' | 'WAITING_SIGN' | 'SIGNED' | 'EXPIRED' | 'VOIDED' | 'GENERATING' | 'FAILED'
|
||||
| 'CREATED'
|
||||
| 'WAITING_SIGN'
|
||||
| 'SIGNED'
|
||||
| 'EXPIRED'
|
||||
| 'VOIDED'
|
||||
| 'GENERATING'
|
||||
| 'FAILED'
|
||||
/** 患者已签、等科室复核签章(与 workbench 的 WAITING_CONFIRM 同口径) */
|
||||
| 'WAITING_CONFIRM'
|
||||
|
||||
/** 与 workbench 的 BackendSigningMethod 同口径:四种方式平铺,无通道归并 */
|
||||
export type ReportBackendSignMethod = 'PAD' | 'SMS' | 'IPAD' | 'WX'
|
||||
|
||||
@@ -122,6 +122,8 @@ interface MockRecordSeed {
|
||||
minutes?: number
|
||||
source?: string
|
||||
voidReason?: string
|
||||
/** 患者签署后是否需要科室复核签章(原型 needConfirm) */
|
||||
needConfirm?: boolean
|
||||
}
|
||||
|
||||
const MOCK_RECORD_SEEDS: MockRecordSeed[] = [
|
||||
@@ -131,6 +133,8 @@ const MOCK_RECORD_SEEDS: MockRecordSeed[] = [
|
||||
{ patient: '周雅琴', sex: '女', age: 29, visitType: '门诊', doc: 'mock-doc-endoscopy', status: 'pending', method: 'sms', days: 0, at: '11:20' },
|
||||
{ patient: '赵某某', sex: '男', age: 52, visitType: '住院', doc: 'mock-doc-admission', status: 'pending', method: 'ipad', days: 0, at: '09:05', source: '工作台发起' },
|
||||
{ patient: '孙浩然', sex: '男', age: 54, visitType: '住院', doc: 'mock-doc-endoscopy', status: 'pending', method: 'wx', days: 0, at: '08:50' },
|
||||
// 「待科室确认」样本:患者已签完,等科室复核签章(原型 T0818006 那种)
|
||||
{ patient: '王秀兰', sex: '女', age: 63, visitType: '住院', doc: 'mock-doc-endoscopy', status: 'confirming', method: 'sms', days: 0, at: '10:40', minutes: 5, needConfirm: true },
|
||||
// 昨天
|
||||
{ patient: '孙浩然', sex: '男', age: 54, visitType: '住院', doc: 'mock-doc-narcotic', status: 'signed', method: 'pad', days: 1, at: '08:40', minutes: 6, source: '工作台发起' },
|
||||
{ patient: '王建国', sex: '男', age: 71, visitType: '住院', doc: 'mock-doc-blood', status: 'signed', method: 'sms', days: 1, at: '14:15', minutes: 9 },
|
||||
@@ -139,6 +143,7 @@ const MOCK_RECORD_SEEDS: MockRecordSeed[] = [
|
||||
// 近 3 天
|
||||
{ patient: '吴桂芳', sex: '女', age: 73, visitType: '住院', doc: 'mock-doc-surgery', status: 'signed', method: 'pad', days: 2, at: '09:35', minutes: 5 },
|
||||
{ patient: '刘伟', sex: '男', age: 45, visitType: '门诊', doc: 'mock-doc-cardiac', status: 'signed', method: 'sms', days: 2, at: '15:48', minutes: 3, source: '工作台发起' },
|
||||
{ patient: '张海涛', sex: '男', age: 47, visitType: '门诊', doc: 'mock-doc-blood', status: 'confirming', method: 'pad', days: 2, at: '16:20', minutes: 6, needConfirm: true },
|
||||
{ patient: '郑海燕', sex: '女', age: 38, visitType: '门诊', doc: 'mock-doc-endoscopy', status: 'expired', method: 'sms', days: 3, at: '10:22' },
|
||||
{ patient: '黄志明', sex: '男', age: 66, visitType: '住院', doc: 'mock-doc-mri', status: 'signed', method: 'pad', days: 3, at: '13:07', minutes: 8 },
|
||||
// 近 7 天
|
||||
@@ -187,7 +192,9 @@ function buildMockRecord(seed: MockRecordSeed, index: number): SigningRecord {
|
||||
const createdAt = formatDateTime(createdAtDate)
|
||||
const signedDate = new Date(createdAtDate)
|
||||
signedDate.setMinutes(signedDate.getMinutes() + (seed.minutes ?? 0))
|
||||
const signedAt = seed.status === 'signed' ? formatDateTime(signedDate) : undefined
|
||||
// 待科室确认的记录同样已经签过了,签名时间与签名图都要有
|
||||
const hasSigned = seed.status === 'signed' || seed.status === 'confirming'
|
||||
const signedAt = hasSigned ? formatDateTime(signedDate) : undefined
|
||||
|
||||
// 已超时的记录按 24 小时失效,其余按 48 小时;失效时间只做展示,不参与筛选
|
||||
const expireHours = seed.status === 'expired' ? 24 : 48
|
||||
@@ -218,6 +225,7 @@ function buildMockRecord(seed: MockRecordSeed, index: number): SigningRecord {
|
||||
signerName: seed.patient,
|
||||
method: seed.method,
|
||||
status: seed.status,
|
||||
needConfirm: seed.needConfirm ?? false,
|
||||
source: seed.source ?? '电子病历',
|
||||
createdAt,
|
||||
updatedAt: signedAt ?? createdAt,
|
||||
@@ -225,7 +233,7 @@ function buildMockRecord(seed: MockRecordSeed, index: number): SigningRecord {
|
||||
expiresAt: formatDateTime(expiresAtDate),
|
||||
voidReason: seed.voidReason,
|
||||
// 已签署的记录带上签名图,否则「列表说已签署、打开没有签名」自相矛盾
|
||||
signatureDataUrl: seed.status === 'signed' ? DEMO_SIGNATURE_DATA_URL : undefined,
|
||||
signatureDataUrl: hasSigned ? DEMO_SIGNATURE_DATA_URL : undefined,
|
||||
documentContentHtml: getMockDocumentHtml(document.id),
|
||||
}
|
||||
}
|
||||
@@ -509,6 +517,27 @@ function buildMockEvents(record: SigningRecord): SigningTaskEvent[] {
|
||||
})
|
||||
}
|
||||
|
||||
// 需科室确认的记录:签完先入科室确认队列,科室签章后才归档(与工作台留痕同一口径)
|
||||
if (record.needConfirm && record.signedAt) {
|
||||
events.push({
|
||||
id: `${record.id}-confirm-queued`,
|
||||
time: record.signedAt,
|
||||
text: `已推送至${record.department}确认队列,等待科室复核签章`,
|
||||
eventType: 'CONFIRM_QUEUED',
|
||||
channel: backendChannel,
|
||||
})
|
||||
|
||||
if (record.status === 'signed') {
|
||||
events.push({
|
||||
id: `${record.id}-confirmed`,
|
||||
time: record.updatedAt,
|
||||
text: '科室复核签章完成,文档归档至本次诊疗记录',
|
||||
eventType: 'CONFIRMED',
|
||||
channel: backendChannel,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (record.status === 'failed') {
|
||||
events.push({
|
||||
id: `${record.id}-failed`,
|
||||
|
||||
@@ -156,9 +156,10 @@ const DEMO_PATIENTS: DemoPatient[] = [
|
||||
演示任务生成
|
||||
========================================================================= */
|
||||
|
||||
/** 状态分布:已签署占多数,其余各留一点,六种状态在总览统计块里都看得到 */
|
||||
/** 状态分布:已签署占多数,其余各留一点,七种状态在总览统计块里都看得到 */
|
||||
const STATUS_WEIGHTS: { status: SigningTaskStatus; weight: number }[] = [
|
||||
{ status: 'signed', weight: 68 },
|
||||
{ status: 'signed', weight: 62 },
|
||||
{ status: 'confirming', weight: 6 },
|
||||
{ status: 'pending', weight: 9 },
|
||||
{ status: 'signing', weight: 5 },
|
||||
{ status: 'expired', weight: 8 },
|
||||
@@ -259,7 +260,9 @@ function buildTasks(): ReportTaskRow[] {
|
||||
createdAt.setDate(createdAt.getDate() - daysAgo)
|
||||
createdAt.setHours(intBetween(random, 8, 19), intBetween(random, 0, 59), 0, 0)
|
||||
|
||||
const isSigned = status === 'signed'
|
||||
// 待科室确认的任务患者也已经签完了,签署时间与用时都要有 ——
|
||||
// 否则「签署时效分析」会把它们当成"还没签",24 小时内完成率被算低
|
||||
const isSigned = status === 'signed' || status === 'confirming'
|
||||
const signMinutes = isSigned ? buildSignMinutes(random) : undefined
|
||||
|
||||
const row: ReportTaskRow = {
|
||||
|
||||
@@ -336,6 +336,35 @@ const defaultMockRecords: SigningTaskRecord[] = [
|
||||
expiresAt: '2026-08-31 18:00',
|
||||
updatedAt: '2026-08-27 09:50',
|
||||
},
|
||||
{
|
||||
// 「待科室确认」节点:患者已经签完,只差科室复核签章。
|
||||
// 四种状态徽标之外还必须有一条这个状态的样本,否则节点卡、复核签章动作、
|
||||
// 状态筛选在演示环境里一次都走不到。
|
||||
id: 'task-009',
|
||||
templateVersionId: 'tpl-004-v1',
|
||||
campus: '西院区',
|
||||
patientId: 'P202610005',
|
||||
patientName: '陈志强',
|
||||
sex: '男',
|
||||
age: 61,
|
||||
visitNo: 'ZY20260826011',
|
||||
visitType: '住院',
|
||||
visitDate: '2026-08-26',
|
||||
documentId: 'tpl-004',
|
||||
documentName: '内镜检查知情同意书',
|
||||
department: '普外科',
|
||||
status: 'confirming',
|
||||
signerName: '陈志强',
|
||||
signerType: '本人',
|
||||
// 需科室确认:检验 / 基因检测 / 自费类文书的典型场景,由模板标记带出
|
||||
needConfirm: true,
|
||||
method: 'sms',
|
||||
smsMobileMasked: '135****9012',
|
||||
source: '电子病历',
|
||||
expiresAt: '2026-08-31 12:00',
|
||||
signedAt: '2026-08-27 10:05',
|
||||
updatedAt: '2026-08-27 10:05',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -364,7 +393,9 @@ function hydrateTaskDocuments(records: SigningTaskRecord[]): SigningTaskRecord[]
|
||||
return records.map((record, index) => {
|
||||
let next = record
|
||||
|
||||
if (next.status === 'signed' && !next.signatureDataUrl) {
|
||||
// 待科室确认的任务患者已经签过了,同样要有签名图 ——
|
||||
// 否则详情里文书签名位是空的,看着像"患者没签却进了科室确认队列"
|
||||
if ((next.status === 'signed' || next.status === 'confirming') && !next.signatureDataUrl) {
|
||||
next = { ...next, signatureDataUrl: DEMO_SIGNATURE_DATA_URL }
|
||||
}
|
||||
|
||||
@@ -573,6 +604,8 @@ function normalizeTaskStatus(value: BackendSigningTaskStatus): SigningTaskStatus
|
||||
switch (value) {
|
||||
case 'SIGNED':
|
||||
return 'signed'
|
||||
case 'WAITING_CONFIRM':
|
||||
return 'confirming'
|
||||
case 'EXPIRED':
|
||||
return 'expired'
|
||||
case 'VOIDED':
|
||||
@@ -631,6 +664,8 @@ function mapAvailableTemplate(dto: AvailableTemplateVersionResponseDto): Signing
|
||||
description: `已发布版本 ${dto.versionNo}`,
|
||||
// 可用模板接口不返回方式 ACL;四种方式都放行,实际允许范围由后端在创建时校验。
|
||||
supportedMethods: [...ALL_SIGNING_METHODS],
|
||||
// 需科室确认随模板带出;后端没给就是 false(签完直接归档)
|
||||
needConfirm: dto.needConfirm ?? false,
|
||||
// 接口不返回正文,只能带出哈希;正文由 hydrateTemplateContents 按需补回
|
||||
contentSha256: dto.contentSha256,
|
||||
contentHtml: cachedContent || undefined,
|
||||
@@ -726,6 +761,8 @@ function mapSigningTask(
|
||||
// 签署人类型 / 关系:后端补齐后自动带出,取不到即 undefined,列表不显示代签标签
|
||||
signerType: dto.signerType ?? undefined,
|
||||
signerRelation: dto.signerRelation ?? undefined,
|
||||
// 需科室确认:后端没给就是 false,不凭空给任务多插一道人工节点
|
||||
needConfirm: dto.needConfirm ?? false,
|
||||
method: normalizeSigningMethod(dto.signMethod),
|
||||
smsMobileMasked: dto.smsMobileMasked ?? undefined,
|
||||
status: normalizeTaskStatus(dto.status),
|
||||
@@ -780,6 +817,7 @@ function createTemplateFromInput(input: CreateSigningTaskInput): SigningTemplate
|
||||
category: '—',
|
||||
description: '',
|
||||
supportedMethods: [...ALL_SIGNING_METHODS],
|
||||
needConfirm: input.needConfirm ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +835,7 @@ function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions
|
||||
const statusMap: Partial<Record<SigningTaskStatus, BackendSigningTaskStatus>> = {
|
||||
pending: 'WAITING_SIGN',
|
||||
signing: 'GENERATING',
|
||||
confirming: 'WAITING_CONFIRM',
|
||||
signed: 'SIGNED',
|
||||
expired: 'EXPIRED',
|
||||
void: 'VOIDED',
|
||||
@@ -1187,6 +1226,8 @@ export async function createSigningTask(input: CreateSigningTaskInput): Promise<
|
||||
signerName: patient?.name ?? '演示患者',
|
||||
// 原型新建任务固定落「本人」(发起弹窗没有代签人选择,代签是签署现场才知道的事)
|
||||
signerType: '本人',
|
||||
// 需科室确认由文书模板标记带出(原型 ntDocNeedConfirm),不是弹窗里选的
|
||||
needConfirm: input.needConfirm ?? false,
|
||||
method: input.method,
|
||||
status: 'pending',
|
||||
source: '工作台发起',
|
||||
@@ -1238,7 +1279,9 @@ export function completeSigningTask(
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
task.status = 'signed'
|
||||
// 患者签完之后落哪个状态由文书的「需科室确认」标记决定:
|
||||
// 需要科室复核的先停在 confirming,等科室签章才算归档(原型 padTask.status 同款写法)
|
||||
task.status = task.needConfirm ? 'confirming' : 'signed'
|
||||
task.signatureDataUrl = signatureDataUrl
|
||||
task.signedAt = formatMockDate()
|
||||
task.updatedAt = task.signedAt
|
||||
@@ -1250,6 +1293,34 @@ export function completeSigningTask(
|
||||
return Promise.resolve(cloneTask(task))
|
||||
}
|
||||
|
||||
/**
|
||||
* 科室复核签章(原型 confirmDept)。
|
||||
*
|
||||
* 「待科室确认」是个**节点**不是终态:科室确认完才转「已签署」、文档才归档。
|
||||
* 真实模式暂未接入(后端要重做),按项目既有口径如实提示"待接入",
|
||||
* 不前端自己把状态改成已签署 —— 那是要留痕的正式动作。
|
||||
*/
|
||||
export async function confirmDepartmentSigning(id: string): Promise<SigningTaskRecord | null> {
|
||||
if (!useMockData) {
|
||||
throw new Error('当前后端未提供科室复核签章接口')
|
||||
}
|
||||
|
||||
const task = mockRecords.find((record) => record.id === id)
|
||||
|
||||
if (!task) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (task.status !== 'confirming') {
|
||||
throw new Error('只有「待科室确认」的任务需要科室复核签章')
|
||||
}
|
||||
|
||||
task.status = 'signed'
|
||||
task.updatedAt = formatMockDate()
|
||||
persistMockRecords()
|
||||
return Promise.resolve(cloneTask(task))
|
||||
}
|
||||
|
||||
export async function reopenSigningTask(
|
||||
id: string,
|
||||
options: SigningTaskActionOptions = {},
|
||||
@@ -1371,6 +1442,25 @@ export async function getSigningTaskEvents(id: string): Promise<SigningTaskEvent
|
||||
})
|
||||
}
|
||||
|
||||
// 需科室确认的任务:签完先入科室确认队列,科室签章后才归档(原型操作留痕同款两条)
|
||||
if (task.needConfirm && task.signedAt) {
|
||||
events.push({
|
||||
id: `${task.id}-confirm-queued`,
|
||||
time: task.signedAt,
|
||||
text: `已推送至${task.department}确认队列,等待科室复核签章`,
|
||||
eventType: 'CONFIRM_QUEUED',
|
||||
})
|
||||
|
||||
if (task.status === 'signed') {
|
||||
events.push({
|
||||
id: `${task.id}-confirmed`,
|
||||
time: task.updatedAt,
|
||||
text: '科室复核签章完成,文档归档至本次诊疗记录',
|
||||
eventType: 'CONFIRMED',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (task.status === 'void') {
|
||||
events.push({
|
||||
id: `${task.id}-voided`,
|
||||
@@ -1387,6 +1477,7 @@ export function getSigningStatusLabel(status: SigningTaskStatus) {
|
||||
const labels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
|
||||
@@ -18,7 +18,22 @@ export interface WorkbenchOverviewRequestDto {
|
||||
rankingLimit?: number
|
||||
}
|
||||
|
||||
export type SigningTaskStatus = 'pending' | 'signing' | 'signed' | 'expired' | 'void' | 'failed'
|
||||
/**
|
||||
* 签署任务状态(原型 V10.2 的六种 + 「待科室确认」)。
|
||||
*
|
||||
* `confirming` 是**节点状态**,不是失败态:检验 / 基因检测 / 自费等场景要求
|
||||
* 患者签完之后由科室再复核签章(原型 `needConfirm`),此时文书已经签好、只差科室那一道章。
|
||||
* 它与 `signed` 的区别是「能不能算最终归档」——`signed` 才能下载归档原件,
|
||||
* `confirming` 只能下载患者已签版本。
|
||||
*/
|
||||
export type SigningTaskStatus =
|
||||
| 'pending'
|
||||
| 'signing'
|
||||
| 'confirming'
|
||||
| 'signed'
|
||||
| 'expired'
|
||||
| 'void'
|
||||
| 'failed'
|
||||
|
||||
export type PatientSex = '男' | '女' | '未知'
|
||||
|
||||
@@ -67,6 +82,11 @@ export interface SigningTemplate {
|
||||
category: string
|
||||
description: string
|
||||
supportedMethods: SigningMethod[]
|
||||
/**
|
||||
* 患者签署后是否需要科室复核签章(原型 needConfirm)。
|
||||
* 发起签署时随文书带到任务上,决定签完落「待科室确认」还是「已签署」。
|
||||
*/
|
||||
needConfirm?: boolean
|
||||
/** 原型:文书定稿 HTML 随模板携带,发起签署时进入任务快照。 */
|
||||
contentHtml?: string
|
||||
/**
|
||||
@@ -258,6 +278,13 @@ export interface SigningTaskRecord extends WorkbenchTask {
|
||||
*/
|
||||
signerType?: string | null
|
||||
signerRelation?: string | null
|
||||
/**
|
||||
* 是否需要在患者签署后增加「科室复核签章」节点(原型 needConfirm,由文书模板标记带出)。
|
||||
*
|
||||
* 为 true 时患者签完落 `confirming` 而不是 `signed`,要等科室确认完才算归档。
|
||||
* 取不到即按 false 处理(走原路直接归档),不凭空多插一道人工节点。
|
||||
*/
|
||||
needConfirm?: boolean
|
||||
method: SigningMethod
|
||||
/**
|
||||
* 短信发送到的号码,后端只返回脱敏值(如 `138****5678`)。
|
||||
@@ -337,6 +364,8 @@ export interface CreateSigningTaskInput {
|
||||
department: string
|
||||
/** 页面快照字段:仅 Mock 模式进入任务记录;真实接口不提交(后端按 templateVersionId 取版本快照)。 */
|
||||
documentContentHtml?: string
|
||||
/** 由文书模板带出的「需科室确认」标记;真实接口不提交(后端按 templateVersionId 取)。 */
|
||||
needConfirm?: boolean
|
||||
}
|
||||
|
||||
/** MEDISIGN 创建签署任务请求 DTO。不要向后端提交页面快照字段。 */
|
||||
@@ -362,7 +391,15 @@ export interface SigningTaskActionOptions extends SigningApiOptions {
|
||||
}
|
||||
|
||||
export type BackendSigningTaskStatus =
|
||||
'CREATED' | 'WAITING_SIGN' | 'SIGNED' | 'EXPIRED' | 'VOIDED' | 'GENERATING' | 'FAILED'
|
||||
| 'CREATED'
|
||||
| 'WAITING_SIGN'
|
||||
| 'SIGNED'
|
||||
| 'EXPIRED'
|
||||
| 'VOIDED'
|
||||
| 'GENERATING'
|
||||
| 'FAILED'
|
||||
/** 患者已签、等科室复核签章(对应前端 confirming)。后端重做时按这个取值对齐。 */
|
||||
| 'WAITING_CONFIRM'
|
||||
|
||||
/**
|
||||
* 后端签署方式,与前端 SigningMethod 一一对应(大小写之差)。
|
||||
@@ -421,6 +458,8 @@ export interface AvailableTemplateVersionResponseDto {
|
||||
versionNo: string
|
||||
contentSha256: string
|
||||
effectiveAt: string
|
||||
/** 患者签署后是否需要科室复核签章;后端补齐后前端无需改动即可带出。 */
|
||||
needConfirm?: boolean | null
|
||||
}
|
||||
|
||||
export interface SignTaskPatientSnapshotDto {
|
||||
@@ -474,6 +513,8 @@ export interface SignTaskResponseDto {
|
||||
*/
|
||||
signerType?: string | null
|
||||
signerRelation?: string | null
|
||||
/** 患者签署后是否需要科室复核签章(由文书模板标记带出)。 */
|
||||
needConfirm?: boolean | null
|
||||
signedAt: string | null
|
||||
expiredAt: string | null
|
||||
voidedAt: string | null
|
||||
|
||||
@@ -565,6 +565,8 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
|
||||
visitNo: selectedVisit.value.visitNo,
|
||||
department: selectedVisit.value.department,
|
||||
documentContentHtml: selectedTemplate.value.contentHtml,
|
||||
// 需科室确认由文书模板标记带出(原型 V6.9 起弹窗里不再让人选)
|
||||
needConfirm: selectedTemplate.value.needConfirm ?? false,
|
||||
})
|
||||
|
||||
if (!isSigningMockEnabled && method.value === 'sms') {
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface LocalDocument {
|
||||
*/
|
||||
examType?: string | null
|
||||
projectCodes?: string[]
|
||||
/** 患者签署后是否需要科室复核签章(原型 needConfirm),发起签署时随文书带到任务上 */
|
||||
needConfirm?: boolean
|
||||
/**
|
||||
* 演示数据的更新时间。
|
||||
* 演示文库的种子不带时间,页面在这一层会显示成"—"而不是伪造一个时间戳;
|
||||
@@ -323,6 +325,7 @@ export function readLocalSigningTemplates(): SigningTemplate[] {
|
||||
category: category.name,
|
||||
description: document.description ?? '',
|
||||
supportedMethods: [...ALL_SIGNING_METHODS],
|
||||
needConfirm: document.needConfirm ?? false,
|
||||
contentHtml: document.contentHtml,
|
||||
contentSha256: document.contentSha256,
|
||||
})),
|
||||
|
||||
@@ -998,6 +998,7 @@ async function toSigningTemplate(template: DocumentTemplate): Promise<SigningTem
|
||||
category: template.category,
|
||||
description: template.description ?? '',
|
||||
supportedMethods: [...ALL_SIGNING_METHODS],
|
||||
needConfirm: template.needConfirm ?? false,
|
||||
contentHtml,
|
||||
contentSha256,
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ function toLocalDocument(document: DocumentRecord): LocalDocument {
|
||||
// 否则刷新一次卡片上的标签就没了
|
||||
examType: document.examType,
|
||||
projectCodes: document.projectCodes,
|
||||
needConfirm: document.needConfirm,
|
||||
updatedAt: document.updatedAt,
|
||||
contentHtml: document.contentHtml,
|
||||
contentSha256: document.contentSha256,
|
||||
|
||||
@@ -23,6 +23,7 @@ const emit = defineEmits<{
|
||||
|
||||
const statusLabels: Record<ReportTaskStatus, string> = {
|
||||
signed: '已签署',
|
||||
confirming: '待科室确认',
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
expired: '已超时',
|
||||
|
||||
@@ -814,6 +814,7 @@ function mapReportTaskStatus(status: ReportTaskResponseDto['status']): ReportTas
|
||||
CREATED: 'pending',
|
||||
WAITING_SIGN: 'pending',
|
||||
SIGNED: 'signed',
|
||||
WAITING_CONFIRM: 'confirming',
|
||||
EXPIRED: 'expired',
|
||||
VOIDED: 'void',
|
||||
GENERATING: 'signing',
|
||||
@@ -1018,6 +1019,7 @@ function handleView(row: ReportTask) {
|
||||
function getReportStatusLabel(status: ReportTaskStatus) {
|
||||
const labels: Record<ReportTaskStatus, string> = {
|
||||
signed: '已签署',
|
||||
confirming: '待科室确认',
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
expired: '已超时',
|
||||
|
||||
@@ -60,6 +60,23 @@ export const reportTasks: ReportTask[] = [
|
||||
expiredAt: daysAgo(-1, 9, 12),
|
||||
durationMinutes: 7,
|
||||
},
|
||||
{
|
||||
id: 'T0818009',
|
||||
patientName: '王秀兰',
|
||||
patientId: 'P202610007',
|
||||
documentName: '内镜检查知情同意书',
|
||||
department: '消化内科',
|
||||
campus: '本部院区',
|
||||
category: 'consent',
|
||||
method: 'sms',
|
||||
// 「待科室确认」样本:患者已签完,等科室复核签章 ——
|
||||
// 没有这条样本,报表里的这一栏永远是 0,看不出它到底有没有接上
|
||||
status: 'confirming',
|
||||
createdAt: daysAgo(0, 10, 40),
|
||||
signedAt: daysAgo(0, 10, 45),
|
||||
expiredAt: daysAgo(-1, 10, 40),
|
||||
durationMinutes: 5,
|
||||
},
|
||||
{
|
||||
id: 'T0818002',
|
||||
patientName: '李淑芬',
|
||||
|
||||
@@ -30,7 +30,14 @@ export interface ReportMetricCard {
|
||||
tone: ReportMetricTone
|
||||
}
|
||||
|
||||
export type ReportTaskStatus = 'signed' | 'pending' | 'signing' | 'expired' | 'void' | 'failed'
|
||||
export type ReportTaskStatus =
|
||||
| 'signed'
|
||||
| 'confirming'
|
||||
| 'pending'
|
||||
| 'signing'
|
||||
| 'expired'
|
||||
| 'void'
|
||||
| 'failed'
|
||||
|
||||
export type ReportSigningMethod = SigningMethod
|
||||
|
||||
|
||||
@@ -49,12 +49,16 @@ export const METHOD_LABELS: Record<SigningMethod, string> = Object.fromEntries(
|
||||
) as Record<SigningMethod, string>
|
||||
|
||||
/**
|
||||
* 状态文案。六种状态与后端枚举一一对应,不映射成原型里的「待科室确认」——
|
||||
* 那个状态在后端枚举里不存在,硬映射会让报表与签署记录页对同一个任务给出不同说法。
|
||||
* 状态文案。七种状态与后端枚举一一对应。
|
||||
*
|
||||
* `confirming`(待科室确认)是**节点状态**:患者已经签完、只差科室复核签章。
|
||||
* 它在报表里单列一栏而不是并进「已签署」—— 并进去会让「已签署」虚高,
|
||||
* 而科室复核本来就是这类文书的关键卡点,混掉就看不见卡在哪了。
|
||||
*/
|
||||
export const STATUS_LABELS: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
@@ -64,6 +68,7 @@ export const STATUS_LABELS: Record<SigningTaskStatus, string> = {
|
||||
/** 汇总矩阵与统计块共用的列顺序:先「好」后「坏」,读起来符合预期 */
|
||||
export const STATUS_COLUMNS: SigningTaskStatus[] = [
|
||||
'signed',
|
||||
'confirming',
|
||||
'pending',
|
||||
'signing',
|
||||
'expired',
|
||||
@@ -106,9 +111,18 @@ export function axisLabelOf(dateKey: string): string {
|
||||
return `${Number(month)}/${Number(day)}`
|
||||
}
|
||||
|
||||
/** 任务状态 → 六类计数里的字段名:状态枚举与计数字段一一对应,直接索引即可 */
|
||||
/** 任务状态 → 计数里的字段名:状态枚举与计数字段一一对应,直接索引即可 */
|
||||
export function emptyCounts(): ReportStatusCounts {
|
||||
return { total: 0, signed: 0, pending: 0, signing: 0, expired: 0, void: 0, failed: 0 }
|
||||
return {
|
||||
total: 0,
|
||||
signed: 0,
|
||||
confirming: 0,
|
||||
pending: 0,
|
||||
signing: 0,
|
||||
expired: 0,
|
||||
void: 0,
|
||||
failed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function addToCounts(counts: ReportStatusCounts, status: ReportTaskRow['status']): void {
|
||||
|
||||
@@ -40,13 +40,23 @@ const totals = computed<ReportStatusCounts>(() =>
|
||||
(sum, row) => ({
|
||||
total: sum.total + row.total,
|
||||
signed: sum.signed + row.signed,
|
||||
confirming: sum.confirming + row.confirming,
|
||||
pending: sum.pending + row.pending,
|
||||
signing: sum.signing + row.signing,
|
||||
expired: sum.expired + row.expired,
|
||||
void: sum.void + row.void,
|
||||
failed: sum.failed + row.failed,
|
||||
}),
|
||||
{ total: 0, signed: 0, pending: 0, signing: 0, expired: 0, void: 0, failed: 0 },
|
||||
{
|
||||
total: 0,
|
||||
signed: 0,
|
||||
confirming: 0,
|
||||
pending: 0,
|
||||
signing: 0,
|
||||
expired: 0,
|
||||
void: 0,
|
||||
failed: 0,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +39,9 @@ const tiles = computed(() => [
|
||||
{ label: '累计发起任务', value: counts.value.total, color: '#0e6e8c' },
|
||||
{ label: '已签署', value: counts.value.signed, color: '#0f9d6c' },
|
||||
{ label: '待签署', value: counts.value.pending, color: '#3a7bd5' },
|
||||
// 待科室确认:患者已签、等科室复核签章。取色与任务徽标(原型 .tg-cfm)一致,
|
||||
// 不沿用原型的 #7a4dbb —— 那个色在本仓库已经给了「签署中」,同色两块会看串
|
||||
{ label: '待科室确认', value: counts.value.confirming, color: '#c07f00' },
|
||||
{ label: '签署中', value: counts.value.signing, color: '#7a4dbb' },
|
||||
{ label: '已超时', value: counts.value.expired, color: '#d9821f' },
|
||||
{ label: '已作废', value: counts.value.void, color: '#d0534f' },
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface ReportFilterOption<T = string> {
|
||||
export interface ReportStatusCounts {
|
||||
total: number
|
||||
signed: number
|
||||
/** 待科室确认:患者已签、等科室复核签章,不能并进 signed */
|
||||
confirming: number
|
||||
pending: number
|
||||
signing: number
|
||||
expired: number
|
||||
|
||||
@@ -28,6 +28,7 @@ const emit = defineEmits<{
|
||||
const statusLabels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
|
||||
@@ -30,6 +30,7 @@ const pageCount = computed(() => Math.max(Math.ceil(props.total / props.pageSize
|
||||
const statusLabels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
|
||||
@@ -44,6 +44,7 @@ const DEFAULT_PAGE_SIZE = 20
|
||||
const statusLabels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
@@ -128,9 +129,9 @@ const campusOptions = computed<SigningRecordFilterOption<WorkbenchCampus | 'all'
|
||||
|
||||
const statusOptions: SigningRecordFilterOption<SigningRecordFilterForm['status']>[] = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
...(['pending', 'signing', 'signed', 'expired', 'void', 'failed'] as SigningTaskStatus[]).map(
|
||||
(status) => ({ label: statusLabels[status], value: status }),
|
||||
),
|
||||
...(
|
||||
['pending', 'signing', 'confirming', 'signed', 'expired', 'void', 'failed'] as SigningTaskStatus[]
|
||||
).map((status) => ({ label: statusLabels[status], value: status })),
|
||||
]
|
||||
|
||||
/** 原型 #qMethod 只有 3 项(手写板 / 短信线上 / Pad),没有公众号 */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type {
|
||||
SigningAnswers,
|
||||
@@ -18,7 +18,7 @@ import LinkedPrescriptionCard from './LinkedPrescriptionCard.vue'
|
||||
import SigningArtifactList from './SigningArtifactList.vue'
|
||||
import type { SigningTaskAction } from '../types'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
task: SigningTaskRecord | null
|
||||
loading: boolean
|
||||
auditEvents: SigningTaskEvent[]
|
||||
@@ -33,6 +33,10 @@ defineProps<{
|
||||
canDeliveryWrite: boolean
|
||||
canReopen: boolean
|
||||
canVoid: boolean
|
||||
/** 科室复核签章(sign:task:confirm):只有「待科室确认」的任务用得上 */
|
||||
canConfirm: boolean
|
||||
/** 催办(sign:task:remind) */
|
||||
canRemind: boolean
|
||||
canViewAudit: boolean
|
||||
canViewArtifacts: boolean
|
||||
canDownloadArtifact: boolean
|
||||
@@ -50,6 +54,22 @@ const emit = defineEmits<{
|
||||
|
||||
const documentRef = ref<InstanceType<typeof SigningDocumentInteractive> | null>(null)
|
||||
|
||||
/**
|
||||
* 科室确认节点的三种形态(原型文书签名区那一格的三分支):
|
||||
* 待患者签署后 / 待科室确认 / ✓ 已确认。
|
||||
*
|
||||
* 只看任务状态,不另存一份「科室签没签」的布尔量 —— 两个来源迟早会打架。
|
||||
*/
|
||||
const confirmNodeState = computed<'idle' | 'waiting' | 'done'>(() => {
|
||||
const status = props.task?.status
|
||||
|
||||
if (status === 'signed') {
|
||||
return 'done'
|
||||
}
|
||||
|
||||
return status === 'confirming' ? 'waiting' : 'idle'
|
||||
})
|
||||
|
||||
/**
|
||||
* 原型任务详情:主按钮 = 当前方式的签署入口,另外三种方式各给一个「转为 X」的次按钮。
|
||||
* 原型的四种方式都走这一套(`待签署` 下四个 if/else 分支,主按钮与其余三个各不相同),
|
||||
@@ -168,6 +188,43 @@ defineExpose({ getDocumentElement })
|
||||
<span v-else-if="task.method === 'wx'" class="action-hint">公众号推送接口待接入</span>
|
||||
</template>
|
||||
|
||||
<!-- 待科室确认:患者已签完,此刻要动的是科室那一道章(原型 confirmDept 的动作组)。
|
||||
预览与下载放在这里,是因为科室复核时要先看患者签了什么 -->
|
||||
<template v-else-if="task.status === 'confirming'">
|
||||
<button
|
||||
v-if="canConfirm"
|
||||
type="button"
|
||||
class="action-button"
|
||||
@click="emit('action', 'confirm-dept')"
|
||||
>
|
||||
🏛 科室复核签章(确认完成)
|
||||
</button>
|
||||
<button
|
||||
v-if="canRemind"
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'remind')"
|
||||
>
|
||||
催办
|
||||
</button>
|
||||
<button
|
||||
v-if="canViewArtifacts"
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'preview-signed')"
|
||||
>
|
||||
在线预览
|
||||
</button>
|
||||
<button
|
||||
v-if="canDownloadArtifact"
|
||||
type="button"
|
||||
class="action-button action-button--ghost"
|
||||
@click="emit('action', 'download-pdf')"
|
||||
>
|
||||
下载已签 PDF
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="task.status === 'expired' && canReopen">
|
||||
<button type="button" class="action-button" @click="emit('action', 'reopen')">
|
||||
↻ 重新发起
|
||||
@@ -243,13 +300,44 @@ defineExpose({ getDocumentElement })
|
||||
v-if="task.documentContentHtml"
|
||||
ref="documentRef"
|
||||
:task="task"
|
||||
:interactive="allowLocalSigning && task.status !== 'signed' && task.status !== 'void'"
|
||||
:interactive="
|
||||
allowLocalSigning &&
|
||||
task.status !== 'signed' &&
|
||||
task.status !== 'void' &&
|
||||
task.status !== 'confirming'
|
||||
"
|
||||
:signature-injections="signatureInjections"
|
||||
@selection-change="emit('selectionChange', $event)"
|
||||
@field-change="(answers, progress) => emit('fieldChange', answers, progress)"
|
||||
@signature-request="emit('signatureRequest', $event)"
|
||||
/>
|
||||
<SigningDocumentPreview v-else :task="task" />
|
||||
<!-- 科室确认节点(原型:文书签名区里的「科室确认(签章)」那一格)。
|
||||
它跟着文书走,不跟产物走 —— 科室复核的是患者签了什么,不是下载了什么 -->
|
||||
<section
|
||||
v-if="task.needConfirm"
|
||||
class="dept-confirm"
|
||||
:class="`is-${confirmNodeState}`"
|
||||
aria-label="科室确认节点"
|
||||
>
|
||||
<span class="dept-confirm__label">科室确认(签章)</span>
|
||||
<span v-if="confirmNodeState === 'done'" class="dept-confirm__tag is-done">
|
||||
✓ {{ task.department }}已确认
|
||||
</span>
|
||||
<span v-else-if="confirmNodeState === 'waiting'" class="dept-confirm__tag is-waiting">
|
||||
待科室确认
|
||||
</span>
|
||||
<span v-else class="dept-confirm__hint">待患者签署后</span>
|
||||
<span class="dept-confirm__note">
|
||||
{{
|
||||
confirmNodeState === 'done'
|
||||
? '科室复核签章完成,文档已归档至本次诊疗记录'
|
||||
: confirmNodeState === 'waiting'
|
||||
? '患者已签署,等待科室复核签章后归档'
|
||||
: '本文书需科室复核签章,患者签署后自动流转'
|
||||
}}
|
||||
</span>
|
||||
</section>
|
||||
<!-- 顺序照原型:文书 → 签署文件(归档结果)→ 关联处方 → 操作留痕 -->
|
||||
<SigningArtifactList
|
||||
v-if="canViewArtifacts"
|
||||
@@ -365,6 +453,68 @@ defineExpose({ getDocumentElement })
|
||||
background: var(--err-l);
|
||||
}
|
||||
|
||||
/* 科室确认节点:原型文书签名区里「科室确认(签章)」那一格的独立呈现。
|
||||
三种形态用左边框颜色区分:待患者签署(灰)/ 待科室确认(橙)/ 已确认(绿) */
|
||||
.dept-confirm {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
margin-top: 14px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-left: 3px solid #c0c8cd;
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--sh);
|
||||
}
|
||||
|
||||
.dept-confirm.is-waiting {
|
||||
border-left-color: #c07f00;
|
||||
}
|
||||
|
||||
.dept-confirm.is-done {
|
||||
border-left-color: var(--ok);
|
||||
}
|
||||
|
||||
.dept-confirm__label {
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dept-confirm__tag {
|
||||
padding: 2px 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
/* 原型 .tag.tg-cfm */
|
||||
.dept-confirm__tag.is-waiting {
|
||||
color: #c07f00;
|
||||
background: #fff3e0;
|
||||
}
|
||||
|
||||
/* 原型 .tag.tg-done */
|
||||
.dept-confirm__tag.is-done {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
}
|
||||
|
||||
.dept-confirm__hint {
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dept-confirm__note {
|
||||
flex: 1;
|
||||
min-width: 180px;
|
||||
color: var(--mut);
|
||||
font-size: 11.5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.detail-empty {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
|
||||
@@ -23,6 +23,7 @@ const pageCount = computed(() => Math.max(Math.ceil(props.total / props.pageSize
|
||||
const statusLabels: Record<SigningTaskRecord['status'], string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
@@ -260,6 +261,13 @@ function getVisitClass(type: SigningTaskRecord['visitType']) {
|
||||
background: var(--info-l);
|
||||
}
|
||||
|
||||
/* 待科室确认:原型 .tag.tg-cfm(橙底 #fff3e0 / 字色 #c07f00)。
|
||||
与「已签署」的绿底刻意区分 —— 患者签完了,但还没归档 */
|
||||
.task-status--confirming {
|
||||
color: #c07f00;
|
||||
background: #fff3e0;
|
||||
}
|
||||
|
||||
.task-status--signed {
|
||||
color: var(--ok);
|
||||
background: var(--ok-l);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { createPadSigningSession } from '@/api/workbench/deliveries'
|
||||
import {
|
||||
completeSigningTask,
|
||||
confirmDepartmentSigning,
|
||||
getSigningTaskDetail,
|
||||
getSigningTaskEvents,
|
||||
getSigningTasks,
|
||||
@@ -63,6 +64,7 @@ const loginStore = useLoginStore()
|
||||
const STATUS_VALUES: SigningTaskStatus[] = [
|
||||
'pending',
|
||||
'signing',
|
||||
'confirming',
|
||||
'signed',
|
||||
'expired',
|
||||
'void',
|
||||
@@ -73,6 +75,7 @@ const DATE_RANGE_VALUES: SigningDateRange[] = ['today', 'yesterday', '3d', '7d',
|
||||
const statusLabels: Record<SigningTaskStatus, string> = {
|
||||
pending: '待签署',
|
||||
signing: '签署中',
|
||||
confirming: '待科室确认',
|
||||
signed: '已签署',
|
||||
expired: '已超时',
|
||||
void: '已作废',
|
||||
@@ -151,6 +154,10 @@ const canCreateTask = computed(() => canWrite('sign:task:create'))
|
||||
const canDeliveryWrite = computed(() => canWrite('sign:delivery:write'))
|
||||
const canReopenTask = computed(() => canWrite('sign:task:reopen'))
|
||||
const canVoidTask = computed(() => canWrite('sign:task:void'))
|
||||
/** 科室复核签章:把「待科室确认」推到「已签署」,是正式留痕动作,与作废同级 */
|
||||
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'))
|
||||
@@ -536,6 +543,8 @@ async function handleTaskAction(action: SigningTaskAction, targetMethod?: Signin
|
||||
(action === 'resend-sms' && canDeliveryWrite.value) ||
|
||||
(action === 'reopen' && canReopenTask.value) ||
|
||||
(action === 'void' && canVoidTask.value) ||
|
||||
(action === 'confirm-dept' && canConfirmTask.value) ||
|
||||
(action === 'remind' && canRemindTask.value) ||
|
||||
(action === 'preview-signed' && canViewArtifacts.value) ||
|
||||
((action === 'download-pdf' ||
|
||||
action === 'download-signature' ||
|
||||
@@ -599,6 +608,52 @@ async function handleTaskAction(action: SigningTaskAction, targetMethod?: Signin
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* 催办:提醒科室签署员尽快复核。
|
||||
*
|
||||
* 真实模式后端没有催办接口(要重做),按项目既有口径如实提示"待接入",
|
||||
* 不前端假装发了一条提醒 —— 催办是要留痕的对外动作。
|
||||
*/
|
||||
if (action === 'remind') {
|
||||
if (!isSigningMockEnabled) {
|
||||
ElMessage.info('催办提醒接口待接入')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success(`已向${task.department}签署员发送催办提醒`)
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'confirm-dept') {
|
||||
if (!isSigningMockEnabled) {
|
||||
ElMessage.info('科室复核签章接口待接入')
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = await ElMessageBox.confirm(
|
||||
`确认「${task.department}」已完成复核签章?确认后任务转为「已签署」,文档归档至本次诊疗记录。`,
|
||||
'科室复核签章',
|
||||
{ type: 'warning', confirmButtonText: '确认签章', cancelButtonText: '取消' },
|
||||
)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
if (!confirmed) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshTask(
|
||||
await confirmDepartmentSigning(task.id),
|
||||
'科室复核签章完成,文档已归档至本次诊疗记录',
|
||||
)
|
||||
} catch {
|
||||
ElMessage.error('科室复核签章失败,请稍后重试')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (action === 'reopen' && task.status !== 'expired' && task.status !== 'failed') {
|
||||
ElMessage.info('只有已超时或处理失败的任务可以重新发起')
|
||||
return
|
||||
@@ -779,15 +834,13 @@ async function confirmPadSignature(signatureDataUrl: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshTask(
|
||||
await completeSigningTask(
|
||||
task.id,
|
||||
signatureDataUrl,
|
||||
signingSelection.value,
|
||||
signingAnswers.value,
|
||||
),
|
||||
'手写签署已完成',
|
||||
const updated = await completeSigningTask(
|
||||
task.id,
|
||||
signatureDataUrl,
|
||||
signingSelection.value,
|
||||
signingAnswers.value,
|
||||
)
|
||||
await refreshTask(updated, signingCompletedMessage(updated))
|
||||
signatureDialogVisible.value = false
|
||||
} catch {
|
||||
ElMessage.error('手写签署失败,请稍后重试')
|
||||
@@ -802,21 +855,32 @@ async function completeOnlineSignature(signatureDataUrl: string) {
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshTask(
|
||||
await completeSigningTask(
|
||||
task.id,
|
||||
signatureDataUrl,
|
||||
signingSelection.value,
|
||||
signingAnswers.value,
|
||||
),
|
||||
'线上签署已完成',
|
||||
const updated = await completeSigningTask(
|
||||
task.id,
|
||||
signatureDataUrl,
|
||||
signingSelection.value,
|
||||
signingAnswers.value,
|
||||
)
|
||||
await refreshTask(updated, signingCompletedMessage(updated))
|
||||
onlineDialogVisible.value = false
|
||||
} catch {
|
||||
ElMessage.error('线上签署失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 签署完成后的提示语。
|
||||
*
|
||||
* 需科室确认的任务签完**并没有结束**(原型同样区分这两种说法):
|
||||
* 如果这里一律说「签署已完成」,操作人会以为可以关页面了,
|
||||
* 而实际上文档还停在科室确认队列里没归档。
|
||||
*/
|
||||
function signingCompletedMessage(updated: SigningTaskRecord | null) {
|
||||
return updated?.status === 'confirming'
|
||||
? '患者已签署,已流转至科室确认节点,等待科室复核签章'
|
||||
: '签署完成,文档已归档'
|
||||
}
|
||||
|
||||
async function confirmVoidTask(reason: string) {
|
||||
const task = selectedTask.value
|
||||
|
||||
@@ -933,6 +997,8 @@ onMounted(() => {
|
||||
:can-delivery-write="canDeliveryWrite"
|
||||
:can-reopen="canReopenTask"
|
||||
:can-void="canVoidTask"
|
||||
:can-confirm="canConfirmTask"
|
||||
:can-remind="canRemindTask"
|
||||
:can-view-audit="canViewEvents"
|
||||
:can-view-artifacts="canViewArtifacts"
|
||||
:can-download-artifact="canDownloadArtifacts"
|
||||
|
||||
@@ -47,6 +47,10 @@ export type SigningTaskAction =
|
||||
| 'switch-method'
|
||||
| 'reopen'
|
||||
| 'void'
|
||||
/** 科室复核签章:把「待科室确认」的任务推到「已签署」(原型 confirmDept) */
|
||||
| 'confirm-dept'
|
||||
/** 催办:提醒科室签署员尽快复核(原型 sign:task:remind) */
|
||||
| 'remind'
|
||||
| 'download-pdf'
|
||||
| 'download-signature'
|
||||
/** 在线预览已签署文书(原型 openPdfView 的弹窗预览) */
|
||||
|
||||
Reference in New Issue
Block a user