Files
xh-medical-sign-web/clinical-web/src/api/workbench/records.ts
T
yelan 8c70f01e2e 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 错。
2026-09-26 20:09:00 +08:00

572 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 签署记录查询(工作台 · 签署记录查询)。
*
* 这里回答的是「历史签署记录」,与签署工作台那份「当前待办」不是同一个视角:
* 工作台按当天操作组织,记录页按条件检索全量历史,因此两边各有一份演示数据 ——
* 工作台的种子固定在某一天,直接拿它当历史记录,会让「今日 / 近 3 天」这些筛选
* 在演示里长期空着,看不出筛选到底有没有生效。两份种子的字段口径完全一致
* (都是 SigningRecord),真实模式下读的也是同一个接口,所以不存在两套业务语义。
*
* 真实模式复用已有的签署任务列表接口(GET /v1/sign-tasks):签署记录就是签署任务的
* 历史视图,后端目前没有单独的记录检索接口,前端不凭空造一个不存在的地址。
* 已知差异:后端该接口不支持按「文档类别」筛选,记录页会在真实模式下禁用这一项
* (见 SigningRecordFilterBar 的 categoryEnabled),不做静默忽略。
*/
import { DEMO_SIGNATURE_DATA_URL } from '@/utils/demo-signature'
import { mockFlags } from '@/utils/mock-flags'
import { buildFieldSpanHtml } from '@/utils/signing-fields'
import { formatSigningMethod, toBackendSigningMethod } from '@/utils/signing-method'
import {
campusOfDepartment,
mockDocumentById,
mockDocumentCatalog,
type MockDocumentDef,
} from './mock-catalog'
import { getSigningTaskDetail, getSigningTaskEvents, getSigningTasks } from './signing'
import type {
PatientSex,
SigningApiOptions,
SigningFieldKind,
SigningMethod,
SigningRecord,
SigningRecordListResponse,
SigningRecordQuery,
SigningTaskEvent,
SigningTaskQuery,
SigningTaskRecord,
SigningTaskStatus,
VisitType,
} from './types'
const useMockData = mockFlags.signRecords
export const isSigningRecordsMockEnabled = useMockData
/** 签署记录的演示数据量上限:导出时逐页取回,避免一条筛选条件意外拉回整个库 */
const MAX_PAGES = 25
/** 导出/批量取回时单页条数 */
const BULK_PAGE_SIZE = 200
/**
* 演示正文。
*
* 记录页的「查看」要能打开当时的文书,所以演示记录必须带正文快照。
* 正文由文书定义现场生成(含结构化占位符,签名位与签署日期都能按记录取值回显),
* 不是渲染时兜底拼出来的假纸样 —— 那种兜底只在正文真的取不到时才允许出现。
*
* 惰性构建:buildFieldSpanHtml 依赖 DOM,模块在 Node 侧被 import 时不该直接抛错。
*/
let mockDocumentHtmlCache: Map<string, string> | null = null
function buildMockDocumentHtml(document: MockDocumentDef): string {
const field = (id: string, label: string, kind: SigningFieldKind = 'text', required = false) =>
buildFieldSpanHtml({ id, label, kind, required, options: [] })
return [
`<h2>${document.name}</h2>`,
'<h3>一、患者与就诊信息</h3>',
'<table><tbody>',
`<tr><td>患者姓名</td><td>${field('patient', '患者姓名', 'text', true)}</td>`,
`<td>性别</td><td>${field('sex', '性别')}</td></tr>`,
`<tr><td>年龄</td><td>${field('age', '年龄')}</td>`,
`<td>住院号/门诊号</td><td>${field('visit', '就诊号')}</td></tr>`,
'</tbody></table>',
'<h3>二、风险告知与注意事项</h3>',
`<ol>${document.items.map((item) => `<li>${item}</li>`).join('')}</ol>`,
'<p>医务人员已就上述诊疗方案、医疗风险、替代方案及可能出现的不良后果向患者本人(或授权委托人)充分说明,患者已完全理解并表示同意。</p>',
'<h3>三、签字确认</h3>',
'<table><tbody>',
`<tr><td>患者(或授权委托人)签名:${field('signature', '患者签名', 'signature', true)}</td>`,
`<td>签署日期:${field('signedDate', '签署日期', 'date', true)}</td></tr>`,
`<tr><td>经治医师签名:${field('doctor', '医师签名', 'signature')}</td><td></td></tr>`,
'</tbody></table>',
'<p>注:本知情同意书采用电子方式签署,签名图片与文书原件一并留存,可随时查验。</p>',
].join('')
}
function getMockDocumentHtml(documentId: string): string {
mockDocumentHtmlCache ??= new Map(
mockDocumentCatalog.map((document) => [document.id, buildMockDocumentHtml(document)]),
)
return mockDocumentHtmlCache.get(documentId) ?? ''
}
/* =========================================================================
演示数据:记录种子
========================================================================= */
/**
* 一条演示记录的种子。
*
* 只写「人 + 事」:患者 ID、就诊号、院区、失效时间这些都能由这几项派生出来
* (见 buildMockRecord),逐条手写既冗长又容易写错。
* `days` 是距今天数(0 = 今天),演示数据因此始终落在「今日 / 近 3 天 / 近 7 天」窗口内,
* 不会像写死日期那样,过几天就全部落到筛选范围之外。
*/
interface MockRecordSeed {
patient: string
sex: PatientSex
age: number
visitType: VisitType
/** mockDocumentCatalog 的 id */
doc: string
status: SigningTaskStatus
method: SigningMethod
days: number
/** 发起时刻 HH:mm */
at: string
/** 签署用时(分钟):只有已签署的记录才有 */
minutes?: number
source?: string
voidReason?: string
/** 患者签署后是否需要科室复核签章(原型 needConfirm) */
needConfirm?: boolean
}
const MOCK_RECORD_SEEDS: MockRecordSeed[] = [
// 今天
{ patient: '赵美英', sex: '女', age: 58, visitType: '住院', doc: 'mock-doc-cardiac', status: 'signed', method: 'pad', days: 0, at: '09:12', minutes: 7 },
{ patient: '李淑芬', sex: '女', age: 67, visitType: '门诊', doc: 'mock-doc-mri', status: 'signed', method: 'sms', days: 0, at: '10:05', minutes: 4 },
{ 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 },
{ patient: '陈志强', sex: '男', age: 61, visitType: '住院', doc: 'mock-doc-admission', status: 'void', method: 'pad', days: 1, at: '16:02', source: '工作台发起', voidReason: '文书选择错误,已重新发起' },
{ patient: '罗文静', sex: '女', age: 44, visitType: '住院', doc: 'mock-doc-mri', status: 'signed', method: 'ipad', days: 1, at: '11:35', minutes: 5 },
// 近 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 天
{ patient: '徐丽华', sex: '女', age: 50, visitType: '住院', doc: 'mock-doc-blood', status: 'signed', method: 'pad', days: 4, at: '11:40', minutes: 6 },
{ patient: '马晓东', sex: '男', age: 33, visitType: '其他', doc: 'mock-doc-emergency', status: 'failed', method: 'pad', days: 5, at: '20:18', source: '工作台发起' },
{ patient: '何秀英', sex: '女', age: 79, visitType: '住院', doc: 'mock-doc-surgery', status: 'signed', method: 'sms', days: 6, at: '09:05', minutes: 12 },
{ patient: '林俊杰', sex: '男', age: 41, visitType: '门诊', doc: 'mock-doc-narcotic', status: 'signing', method: 'pad', days: 6, at: '14:52' },
{ patient: '梁俊豪', sex: '男', age: 35, visitType: '住院', doc: 'mock-doc-surgery', status: 'signed', method: 'wx', days: 6, at: '10:28', minutes: 4 },
// 近 30 天
{ patient: '罗美玲', sex: '女', age: 62, visitType: '门诊', doc: 'mock-doc-admission', status: 'expired', method: 'sms', days: 11, at: '17:30' },
{ patient: '高文斌', sex: '男', age: 57, visitType: '住院', doc: 'mock-doc-cardiac', status: 'signed', method: 'pad', days: 14, at: '10:10', minutes: 7 },
{ patient: '邓小燕', sex: '女', age: 36, visitType: '门诊', doc: 'mock-doc-mri', status: 'void', method: 'sms', days: 19, at: '08:55', voidReason: '患者拒绝签署,任务作废' },
{ patient: '苏国华', sex: '男', age: 68, visitType: '住院', doc: 'mock-doc-endoscopy', status: 'signed', method: 'pad', days: 26, at: '15:25', minutes: 6 },
]
function pad(value: number) {
return String(value).padStart(2, '0')
}
function formatDateTime(date: Date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
date.getHours(),
)}:${pad(date.getMinutes())}`
}
/** 演示数据的就诊号前缀:与医院里门诊 / 住院 / 体检的编号习惯一致 */
const VISIT_NO_PREFIX: Record<VisitType, string> = {
门诊: 'MZ',
住院: 'ZY',
体检: 'TJ',
其他: 'JZ',
}
function buildMockRecord(seed: MockRecordSeed, index: number): SigningRecord {
const document = mockDocumentById.get(seed.doc)
if (!document) {
throw new Error(`演示签署记录引用了不存在的文书:${seed.doc}`)
}
const createdAtDate = new Date()
createdAtDate.setDate(createdAtDate.getDate() - seed.days)
const [hour, minute] = seed.at.split(':').map(Number)
createdAtDate.setHours(hour, minute, 0, 0)
const createdAt = formatDateTime(createdAtDate)
const signedDate = new Date(createdAtDate)
signedDate.setMinutes(signedDate.getMinutes() + (seed.minutes ?? 0))
// 待科室确认的记录同样已经签过了,签名时间与签名图都要有
const hasSigned = seed.status === 'signed' || seed.status === 'confirming'
const signedAt = hasSigned ? formatDateTime(signedDate) : undefined
// 已超时的记录按 24 小时失效,其余按 48 小时;失效时间只做展示,不参与筛选
const expireHours = seed.status === 'expired' ? 24 : 48
const expiresAtDate = new Date(createdAtDate)
expiresAtDate.setHours(expiresAtDate.getHours() + expireHours)
const dateKey = `${createdAtDate.getFullYear()}${pad(createdAtDate.getMonth() + 1)}${pad(
createdAtDate.getDate(),
)}`
return {
id: `SR${dateKey}${pad(index + 1)}`,
templateVersionId: `${seed.doc}-v1`,
campus: campusOfDepartment(document.department),
patientId: `P2026${10001 + index}`,
patientName: seed.patient,
sex: seed.sex,
age: seed.age,
visitNo: `${VISIT_NO_PREFIX[seed.visitType]}${dateKey}${pad(index + 1)}`,
visitType: seed.visitType,
visitDate: `${createdAtDate.getFullYear()}-${pad(createdAtDate.getMonth() + 1)}-${pad(
createdAtDate.getDate(),
)}`,
documentId: document.id,
documentName: document.name,
category: document.category,
department: document.department,
signerName: seed.patient,
method: seed.method,
status: seed.status,
needConfirm: seed.needConfirm ?? false,
source: seed.source ?? '电子病历',
createdAt,
updatedAt: signedAt ?? createdAt,
signedAt,
expiresAt: formatDateTime(expiresAtDate),
voidReason: seed.voidReason,
// 已签署的记录带上签名图,否则「列表说已签署、打开没有签名」自相矛盾
signatureDataUrl: hasSigned ? DEMO_SIGNATURE_DATA_URL : undefined,
documentContentHtml: getMockDocumentHtml(document.id),
}
}
/** 演示记录:种子派生一次,进程内复用(只读,不需要落盘) */
let mockRecordsCache: SigningRecord[] | null = null
function getMockRecords(): SigningRecord[] {
mockRecordsCache ??= MOCK_RECORD_SEEDS.map(buildMockRecord)
return mockRecordsCache
}
function cloneRecord(record: SigningRecord): SigningRecord {
return {
...record,
selectedItems: record.selectedItems ? [...record.selectedItems] : record.selectedItems,
fieldAnswers: record.fieldAnswers ? { ...record.fieldAnswers } : record.fieldAnswers,
}
}
/* =========================================================================
查询
========================================================================= */
function parseRecordTime(value: string | undefined) {
if (!value) {
return null
}
const date = new Date(value.replace(' ', 'T'))
return Number.isNaN(date.getTime()) ? null : date
}
/** 时间窗口:`to` 为开区间上界(不含),只有「今日 / 昨日」需要 */
function resolveDateWindow(range: SigningRecordQuery['dateRange']): {
from: Date
to: Date
} | null {
const todayStart = new Date()
todayStart.setHours(0, 0, 0, 0)
const tomorrowStart = new Date(todayStart)
tomorrowStart.setDate(tomorrowStart.getDate() + 1)
const daysBack = (days: number) => {
const from = new Date(todayStart)
from.setDate(from.getDate() - days)
return from
}
switch (range) {
case 'today':
return { from: todayStart, to: tomorrowStart }
case 'yesterday':
return { from: daysBack(1), to: todayStart }
case '3d':
return { from: daysBack(2), to: tomorrowStart }
case '7d':
return { from: daysBack(6), to: tomorrowStart }
case '30d':
return { from: daysBack(29), to: tomorrowStart }
default:
return null
}
}
/**
* 记录查询条件 → 签署任务查询条件。
* 签署记录就是签署任务的历史视图,真实模式直接复用工作台的列表接口,
* 因此这里只做字段映射;后端不支持的条件(文档类别)在页面层已禁用,不会走到这里。
*/
function toTaskQuery(query: SigningRecordQuery): SigningTaskQuery {
return {
page: query.page,
pageSize: query.pageSize,
keyword: query.keyword,
status: query.status,
dateRange: query.dateRange,
campus: query.campus,
method: query.method,
department: query.department,
}
}
function toRecord(task: SigningTaskRecord): SigningRecord {
// 任务接口不返回文档类别,这里如实留空,由页面沿用列表里带出的那一份
return { ...task, category: '' }
}
export async function getSigningRecords(
query: SigningRecordQuery,
options: SigningApiOptions = {},
): Promise<SigningRecordListResponse> {
if (!useMockData) {
const page = await getSigningTasks(toTaskQuery(query), options)
return {
records: page.records.map(toRecord),
total: page.total,
page: page.page,
pageSize: page.pageSize,
}
}
const window = resolveDateWindow(query.dateRange)
const keyword = query.keyword?.trim().toLowerCase()
const filtered = getMockRecords().filter((record) => {
if (window) {
const createdAt = parseRecordTime(record.createdAt ?? record.updatedAt)
if (!createdAt || createdAt < window.from || createdAt >= window.to) {
return false
}
}
if (query.status && query.status !== 'all' && record.status !== query.status) {
return false
}
if (query.campus && query.campus !== 'all' && record.campus !== query.campus) {
return false
}
if (query.method && query.method !== 'all' && record.method !== query.method) {
return false
}
if (query.department && record.department !== query.department) {
return false
}
if (query.category && record.category !== query.category) {
return false
}
if (keyword) {
const haystack = [
record.id,
record.patientName,
record.patientId,
record.visitNo,
record.documentName,
]
.join(' ')
.toLowerCase()
if (!haystack.includes(keyword)) {
return false
}
}
return true
})
const sorted = filtered.sort(
(left, right) =>
(parseRecordTime(right.createdAt)?.getTime() ?? 0) -
(parseRecordTime(left.createdAt)?.getTime() ?? 0),
)
const page = Math.max(query.page, 1)
const pageSize = Math.max(query.pageSize, 1)
const start = (page - 1) * pageSize
return {
records: sorted.slice(start, start + pageSize).map(cloneRecord),
total: sorted.length,
page,
pageSize,
}
}
/**
* 逐页取回当前筛选条件下的全部记录,供导出使用。
* 单次查询上限见 MAX_PAGES:宁可在演示/导出时截断并提示,也不要一条筛选条件
* 意外把整个库拉进浏览器内存。
*/
export async function getAllSigningRecords(
query: SigningRecordQuery,
options: SigningApiOptions = {},
): Promise<SigningRecord[]> {
const firstPage = await getSigningRecords(
{ ...query, page: 1, pageSize: BULK_PAGE_SIZE },
options,
)
const pageCount = Math.min(
Math.max(Math.ceil(firstPage.total / firstPage.pageSize), 1),
MAX_PAGES,
)
if (pageCount === 1) {
return firstPage.records
}
const rest = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getSigningRecords({ ...query, page: index + 2, pageSize: BULK_PAGE_SIZE }, options),
),
)
return [firstPage, ...rest].flatMap((page) => page.records)
}
/** 记录详情:真实模式按任务接口取回正文快照与固化信息 */
export async function getSigningRecordDetail(
id: string,
options: SigningApiOptions = {},
): Promise<SigningRecord | null> {
if (!useMockData) {
const task = await getSigningTaskDetail(id, options)
return task ? toRecord(task) : null
}
const record = getMockRecords().find((item) => item.id === id)
return record ? cloneRecord(record) : null
}
/* =========================================================================
操作留痕
========================================================================= */
/**
* 演示留痕:按记录状态推导一条时间线。
*
* 与签署工作台的演示留痕同口径(创建 → 发送 → 签署 / 作废 → 结束),
* 但时间点一律取记录自身固化的时间,不掺"现在"—— 历史记录的操作时间
* 如果每次刷新都在变,就没法用来核对签署链路了。
*/
function buildMockEvents(record: SigningRecord): SigningTaskEvent[] {
const backendChannel = toBackendSigningMethod(record.method)
// 线上方式(短信 / 公众号)才有一条「已发送」留痕,现场方式是设备上直接采集
const isOnline = record.method === 'sms' || record.method === 'wx'
const createdAt = record.createdAt ?? record.updatedAt
const events: SigningTaskEvent[] = [
{
id: `${record.id}-created`,
time: createdAt,
text: `张文静创建签署任务(${formatSigningMethod(record.method)})`,
eventType: 'TASK_CREATED',
channel: backendChannel,
},
]
if (isOnline) {
events.push({
id: `${record.id}-sent`,
time: createdAt,
text:
record.method === 'wx'
? `已向 ${record.patientName} 推送公众号签署通知`
: `已向 ${record.patientName} 预留手机号发送签署短信`,
eventType: 'SEND_PREPARED',
channel: backendChannel,
attemptNo: 1,
})
}
if (record.status === 'signing') {
events.push({
id: `${record.id}-signing`,
time: record.updatedAt,
text: '已打开签名采集,等待患者完成签名',
eventType: 'SIGNING_STARTED',
channel: backendChannel,
})
}
if (record.signedAt) {
events.push({
id: `${record.id}-signed`,
time: record.signedAt,
text: `患者 ${record.signerName} 完成签名,系统已合成文书并双留存`,
eventType: 'SIGNED',
channel: backendChannel,
})
}
if (record.status === 'expired') {
events.push({
id: `${record.id}-expired`,
time: record.expiresAt,
text: '签署链接已过期,任务自动结束(可重新发起)',
eventType: 'EXPIRED',
})
}
// 需科室确认的记录:签完先入科室确认队列,科室签章后才归档(与工作台留痕同一口径)
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`,
time: record.updatedAt,
text: '签名合成失败,请核对签名图与文书模板后重新发起',
eventType: 'FAILED',
reason: '签名合成失败',
})
}
if (record.status === 'void') {
events.push({
id: `${record.id}-voided`,
time: record.updatedAt,
text: `任务已作废${record.voidReason ? `,原因:${record.voidReason}` : ''}`,
eventType: 'VOIDED',
reason: record.voidReason,
})
}
return events
}
export async function getSigningRecordEvents(id: string): Promise<SigningTaskEvent[]> {
if (!useMockData) {
return getSigningTaskEvents(id)
}
const record = getMockRecords().find((item) => item.id === id)
return record ? buildMockEvents(record) : []
}