import axios from 'axios' import { unwrapApiResponse } from '@/utils/api-response' import { request } from '@/utils/request' import { resendSigningSms as resendSigningSmsDelivery } from './deliveries' import type { ApiPageResponse, ApiResponseOf, AvailableTemplateVersionResponseDto, BackendSigningMethod, BackendSigningTaskStatus, CreateSigningTaskInput, CreateSigningTaskRequest, PatientProfile, PatientResponseDto, PatientSex, SignTaskActionRequest, SignTaskEventResponseDto, SignTaskResponseDto, SignTaskSendPreparationResponseDto, SigningApiOptions, SigningMethod, SigningTaskEvent, SigningTaskActionOptions, SigningTaskListResponse, SigningTaskQuery, SigningTaskRecord, SigningTaskStatus, SigningTemplate, VisitResponseDto, VisitType, } from './types' const useMockData = import.meta.env.VITE_USE_MOCK !== 'false' const mockPatients: PatientProfile[] = [ { id: 'P202610001', name: '李某某', sex: '男', age: 46, idCard: '310***********1234', phone: '13800005678', visits: [ { id: 'visit-001', type: '住院', visitNo: 'ZY20260827001', department: '消化内科', visitDate: '2026-08-27', isCurrent: true, }, { id: 'visit-002', type: '门诊', visitNo: 'MZ20260812018', department: '消化内科', visitDate: '2026-08-12', }, ], }, { id: 'P202610002', name: '王某某', sex: '女', age: 37, idCard: '310***********6721', phone: '13900006789', visits: [ { id: 'visit-003', type: '门诊', visitNo: 'JZ20260827018', department: '儿科急诊', visitDate: '2026-08-27', isCurrent: true, }, ], }, { id: 'P202610003', name: '赵某某', sex: '男', age: 52, idCard: '310***********2846', phone: '13700007890', visits: [ { id: 'visit-004', type: '住院', visitNo: 'ZY20260826027', department: '消化内科', visitDate: '2026-08-26', isCurrent: true, }, ], }, { id: 'P202610004', name: '周雅琴', sex: '女', age: 29, idCard: '310***********9152', phone: '13600008901', visits: [ { id: 'visit-005', type: '体检', visitNo: 'TJ20260825006', department: '健康管理中心', visitDate: '2026-08-25', isCurrent: true, }, ], }, { id: 'P202610005', name: '陈志强', sex: '男', age: 61, idCard: '310***********3488', phone: '13500009012', visits: [ { id: 'visit-006', type: '住院', visitNo: 'ZY20260826011', department: '普外科', visitDate: '2026-08-26', isCurrent: true, }, ], }, ] const mockTemplates: SigningTemplate[] = [ { id: 'tpl-001', versionId: 'tpl-001-v2', name: '住院患者知情同意书', code: 'DOC-HOS-001', version: 'V2.0', department: '全院通用', category: '住院告知', description: '住院期间诊疗事项、风险与患者权利告知。', supportedMethods: ['pad', 'sms'], }, { id: 'tpl-002', versionId: 'tpl-002-v1', name: '住院须知及告知书', code: 'DOC-HOS-002', version: 'V1.0', department: '全院通用', category: '住院告知', description: '住院流程、陪护要求和患者须知确认。', supportedMethods: ['pad', 'sms'], }, { id: 'tpl-003', versionId: 'tpl-003-v3', name: '内镜检查知情同意书', code: 'DOC-DIG-001', version: 'V3.0', department: '消化内科', category: '检查告知', description: '内镜检查适应证、风险及替代方案告知。', supportedMethods: ['pad', 'sms'], }, { id: 'tpl-004', versionId: 'tpl-004-v1', name: 'MRI 磁共振检查知情同意书', code: 'DOC-RAD-001', version: 'V1.0', department: '放射科', category: '检查告知', description: '磁共振检查注意事项、禁忌证及风险告知。', supportedMethods: ['pad', 'sms'], }, { id: 'tpl-005', versionId: 'tpl-005-v2', name: '心脏介入手术知情同意书', code: 'DOC-CARD-001', version: 'V2.0', department: '心内科', category: '手术告知', description: '介入治疗方案、围术期风险及替代方案告知。', supportedMethods: ['pad'], }, { id: 'tpl-006', versionId: 'tpl-006-v1', name: '体检报告发放及知情同意书', code: 'DOC-PE-001', version: 'V1.0', department: '健康管理中心', category: '体检告知', description: '体检报告领取方式、隐私保护和异常结果随访告知。', supportedMethods: ['pad', 'sms'], }, { id: 'tpl-007', versionId: 'tpl-007-v1', name: '患者授权委托书', code: 'DOC-GEN-003', version: 'V1.0', department: '医务处', category: '授权文书', description: '患者授权家属或代理人办理相关医疗事项。', supportedMethods: ['pad', 'sms'], }, ] const mockRecords: SigningTaskRecord[] = [ { id: 'task-001', templateVersionId: 'tpl-001-v2', campus: '本部院区', patientId: 'P202610001', patientName: '李某某', sex: '男', age: 46, visitNo: 'ZY20260827001', visitType: '住院', visitDate: '2026-08-27', documentId: 'tpl-001', documentName: '住院患者知情同意书', department: '消化内科', status: 'pending', signerName: '李某某', method: 'pad', source: '电子病历', expiresAt: '2026-08-30 09:30', updatedAt: '2026-08-27 09:30', }, { id: 'task-002', templateVersionId: 'tpl-004-v1', campus: '东院区', patientId: 'P202610002', patientName: '王某某', sex: '女', age: 37, visitNo: 'JZ20260827018', visitType: '门诊', visitDate: '2026-08-27', documentId: 'tpl-004', documentName: 'MRI 磁共振检查知情同意书', department: '放射科', status: 'signed', signerName: '王某某家属', method: 'sms', source: '工作台发起', expiresAt: '2026-08-29 18:00', updatedAt: '2026-08-27 09:12', signedAt: '2026-08-27 09:12', }, { id: 'task-003', templateVersionId: 'tpl-003-v3', campus: '本部院区', patientId: 'P202610003', patientName: '赵某某', sex: '男', age: 52, visitNo: 'ZY20260826027', visitType: '住院', visitDate: '2026-08-26', documentId: 'tpl-003', documentName: '内镜检查知情同意书', department: '消化内科', status: 'expired', signerName: '赵某某家属', method: 'sms', source: '电子病历', expiresAt: '2026-08-27 16:00', updatedAt: '2026-08-27 08:45', }, { id: 'task-004', templateVersionId: 'tpl-002-v1', campus: '本部院区', patientId: 'P202610005', patientName: '陈志强', sex: '男', age: 61, visitNo: 'ZY20260826011', visitType: '住院', visitDate: '2026-08-26', documentId: 'tpl-002', documentName: '住院须知及告知书', department: '普外科', status: 'void', signerName: '陈志强', method: 'pad', source: '电子病历', expiresAt: '2026-08-29 11:00', updatedAt: '2026-08-26 11:20', voidReason: '文档选择错误', }, { id: 'task-005', templateVersionId: 'tpl-006-v1', campus: '东院区', patientId: 'P202610004', patientName: '周雅琴', sex: '女', age: 29, visitNo: 'TJ20260825006', visitType: '体检', visitDate: '2026-08-25', documentId: 'tpl-006', documentName: '体检报告发放及知情同意书', department: '健康管理中心', status: 'signing', signerName: '周雅琴', method: 'pad', source: '工作台发起', expiresAt: '2026-08-30 14:20', updatedAt: '2026-08-27 08:20', }, ] function clonePatient(patient: PatientProfile): PatientProfile { return { ...patient, visits: patient.visits.map((visit) => ({ ...visit })), } } function cloneTask(task: SigningTaskRecord): SigningTaskRecord { return { ...task } } function getRangeStart(range: SigningTaskQuery['dateRange']) { if (!range || range === 'all') { return null } const date = new Date('2026-08-27T00:00:00') if (range === 'yesterday') { date.setDate(date.getDate() - 1) } else if (range === '3d') { date.setDate(date.getDate() - 2) } else if (range === '7d') { date.setDate(date.getDate() - 6) } return date } function parseMockDate(value: string) { return new Date(value.replace(' ', 'T')) } function formatMockDate(date = new Date('2026-08-27T12:00:00')) { const pad = (value: number) => String(value).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}` } function addHours(value: string, hours: number) { const date = parseMockDate(value) date.setHours(date.getHours() + hours) return formatMockDate(date) } export const isSigningMockEnabled = useMockData function isNotFoundError(error: unknown) { return axios.isAxiosError(error) && error.response?.status === 404 } function formatApiDateTime(value: string | null | undefined) { if (!value) { return '' } const date = new Date(value) if (Number.isNaN(date.getTime())) { return value .replace('T', ' ') .replace(/\.\d+Z$/, '') .replace(/Z$/, '') } const pad = (part: number) => String(part).padStart(2, '0') return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad( date.getHours(), )}:${pad(date.getMinutes())}` } function formatApiDate(value: string | null | undefined) { return formatApiDateTime(value).slice(0, 10) } function normalizePatientSex(value: string): PatientSex { if (value === 'MALE' || value === '男') { return '男' } if (value === 'FEMALE' || value === '女') { return '女' } return '未知' } function normalizeVisitType(value: string): VisitType { if (value === 'OUTPATIENT' || value === '门诊') { return '门诊' } if (value === 'INPATIENT' || value === '住院') { return '住院' } if (value === 'CHECKUP' || value === '体检') { return '体检' } return '其他' } function normalizeSigningMethod(value: BackendSigningMethod): SigningMethod { return value === 'SMS' ? 'sms' : 'pad' } function normalizeTaskStatus(value: BackendSigningTaskStatus): SigningTaskStatus { switch (value) { case 'SIGNED': return 'signed' case 'EXPIRED': return 'expired' case 'VOIDED': return 'void' case 'GENERATING': return 'signing' case 'FAILED': return 'failed' case 'CREATED': case 'WAITING_SIGN': default: return 'pending' } } function mapPatient(dto: PatientResponseDto, visits: PatientProfile['visits']): PatientProfile { return { id: dto.id, name: dto.name, sex: normalizePatientSex(dto.sex), age: dto.age, idCard: dto.idCardMasked ?? '', phone: dto.phoneMasked ?? '', visits, } } function mapVisit(dto: VisitResponseDto): PatientProfile['visits'][number] { return { id: dto.id, type: normalizeVisitType(dto.visitType), visitNo: dto.visitNo, department: dto.departmentName || '未指定科室', departmentId: dto.departmentId, doctorName: dto.doctorName ?? undefined, visitDate: formatApiDate(dto.visitedAt), visitedAt: dto.visitedAt, campusId: dto.campusId, sourceSystem: dto.sourceSystem, } } function mapAvailableTemplate(dto: AvailableTemplateVersionResponseDto): SigningTemplate { return { id: dto.templateId, versionId: dto.templateVersionId, name: dto.templateName, code: dto.templateCode, version: dto.versionNo, department: dto.departmentId ? '指定科室' : '全院通用', departmentId: dto.departmentId, campusId: dto.campusId, category: dto.category || '未分类', description: `已发布版本 ${dto.versionNo}`, // 可用模板接口没有返回通道 ACL;具体通道仍由后端在创建时校验。 supportedMethods: ['pad', 'sms'], } } function findTemplate(templates: SigningTemplate[] | undefined, versionId: string) { return templates?.find((template) => template.versionId === versionId) } function mapSigningTask( dto: SignTaskResponseDto, options: SigningApiOptions = {}, ): SigningTaskRecord { const template = findTemplate(options.templates, dto.templateVersionId) return { id: dto.id, templateVersionId: dto.templateVersionId, campus: options.campusNames?.[dto.campusId] ?? options.campus ?? '本部院区', campusId: dto.campusId, patientId: dto.patientSnapshot.patientId, patientName: dto.patientSnapshot.name, sex: normalizePatientSex(dto.patientSnapshot.sex), age: dto.patientSnapshot.age, visitNo: dto.visitSnapshot.visitNo, visitType: normalizeVisitType(dto.visitSnapshot.visitType), visitDate: formatApiDate(dto.visitSnapshot.visitedAt), documentId: template?.id ?? dto.templateVersionId, documentName: template?.name ?? `模板版本 ${dto.templateVersionNumber}`, department: dto.visitSnapshot.departmentName || '未指定科室', departmentId: dto.departmentId, signerName: dto.patientSnapshot.name, method: normalizeSigningMethod(dto.signMethod), status: normalizeTaskStatus(dto.status), source: dto.visitSnapshot.sourceSystem || dto.patientSnapshot.sourceSystem || '—', expiresAt: formatApiDateTime(dto.expiredAt), createdAt: formatApiDateTime(dto.createdAt), updatedAt: formatApiDateTime(dto.updatedAt), rowVersion: dto.rowVersion, backendStatus: dto.status, signedAt: formatApiDateTime(dto.signedAt) || undefined, voidReason: dto.voidReason ?? undefined, } } function createTemplateFromInput(input: CreateSigningTaskInput): SigningTemplate { return { id: input.documentId, versionId: input.templateVersionId, name: input.documentName, code: input.documentId, version: '—', department: input.department, category: '—', description: '', supportedMethods: ['pad', 'sms'], } } function toBackendTaskQuery(query: SigningTaskQuery, options: SigningApiOptions = {}) { const params: Record = { page: Math.max(query.page, 1), size: Math.min(Math.max(query.pageSize, 1), 200), } if (query.keyword?.trim()) { params.keyword = query.keyword.trim() } if (query.status && query.status !== 'all') { const statusMap: Partial> = { pending: 'WAITING_SIGN', signing: 'GENERATING', signed: 'SIGNED', rejected: 'FAILED', expired: 'EXPIRED', void: 'VOIDED', failed: 'FAILED', } const status = statusMap[query.status] if (status) { params.status = status } } if (query.method && query.method !== 'all') { params.signMethod = query.method === 'sms' ? 'SMS' : 'PAD' } if (query.documentId) { params.templateVersionId = findTemplate(options.templates, query.documentId)?.versionId ?? query.documentId } const now = new Date() const start = new Date(now) const end = new Date(now) start.setHours(0, 0, 0, 0) end.setHours(23, 59, 59, 999) if (query.dateRange === 'yesterday') { start.setDate(start.getDate() - 1) end.setDate(end.getDate() - 1) } else if (query.dateRange === '3d') { start.setDate(start.getDate() - 2) } else if (query.dateRange === '7d') { start.setDate(start.getDate() - 6) } if (query.dateRange && query.dateRange !== 'all') { params.createdFrom = start.toISOString() params.createdTo = end.toISOString() } if (query.campus && query.campus !== 'all' && options.campusId) { params.campusId = options.campusId } if (query.department && options.departmentIds?.[query.department]) { params.departmentId = options.departmentIds[query.department] } return params } function createIdempotencyKey() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return `clinical-web-${crypto.randomUUID()}` } return `clinical-web-${Date.now()}-${Math.random().toString(36).slice(2)}` } function getTaskActionBody(expectedRowVersion?: number): SignTaskActionRequest { return expectedRowVersion === undefined ? {} : { expectedRowVersion } } function mapEventText(event: SignTaskEventResponseDto) { const actor = event.actorSubject || '系统' const channel = event.channel === 'SMS' ? '短信' : event.channel === 'PAD' ? '手写板' : '' switch (event.eventType) { case 'TASK_CREATED': return `${actor}创建签署任务${channel ? `(${channel})` : ''}` case 'SEND_PREPARED': return `${actor}准备${channel || '签署'}发送` case 'VOIDED': return `任务已作废${event.reason ? `,原因:${event.reason}` : ''}` case 'RESENT': case 'RESEND_PREPARED': return `${actor}重新发送${channel || '签署'}请求` case 'REOPENED': return `${actor}重新开启签署任务` case 'SIGNED': return `${actor}完成签名` default: return `${actor}:${event.eventType}` } } function mapSigningEvent(event: SignTaskEventResponseDto): SigningTaskEvent { return { id: event.id, time: formatApiDateTime(event.eventTime), text: mapEventText(event), eventType: event.eventType, actorSubject: event.actorSubject ?? undefined, reason: event.reason ?? undefined, channel: event.channel ?? undefined, attemptNo: event.attemptNo ?? undefined, } } export async function getSigningTasks( query: SigningTaskQuery, options: SigningApiOptions = {}, ): Promise { if (!useMockData) { const response = await request.get>>( '/v1/sign-tasks', { params: toBackendTaskQuery(query, options) }, ) const page = unwrapApiResponse(response) return { records: page.records.map((task) => mapSigningTask(task, options)), total: page.total, page: page.page, pageSize: page.size, } } const keyword = query.keyword?.trim().toLowerCase() const rangeStart = getRangeStart(query.dateRange) const filteredRecords = mockRecords .filter((record) => { const matchesKeyword = !keyword || [record.patientName, record.patientId, record.visitNo, record.documentName, record.id] .join(' ') .toLowerCase() .includes(keyword) const matchesStatus = !query.status || query.status === 'all' || record.status === query.status const matchesCampus = !query.campus || query.campus === 'all' || record.campus === query.campus const matchesMethod = !query.method || query.method === 'all' || record.method === query.method const matchesDepartment = !query.department || record.department === query.department const matchesDocument = !query.documentId || record.documentId === query.documentId const matchesVisitType = !query.visitType || query.visitType === 'all' || record.visitType === query.visitType const matchesDate = !rangeStart || parseMockDate(record.updatedAt) >= rangeStart return ( matchesKeyword && matchesStatus && matchesCampus && matchesMethod && matchesDepartment && matchesDocument && matchesVisitType && matchesDate ) }) .sort( (left, right) => parseMockDate(right.updatedAt).getTime() - parseMockDate(left.updatedAt).getTime(), ) const page = Math.max(query.page, 1) const pageSize = Math.max(query.pageSize, 1) const start = (page - 1) * pageSize return Promise.resolve({ records: filteredRecords.slice(start, start + pageSize).map(cloneTask), total: filteredRecords.length, page, pageSize, }) } export async function getSigningTaskDetail( id: string, options: SigningApiOptions = {}, ): Promise { if (!useMockData) { try { const response = await request.get>( `/v1/sign-tasks/${encodeURIComponent(id)}`, ) return mapSigningTask(unwrapApiResponse(response), options) } catch (error) { if (isNotFoundError(error)) { return null } throw error } } return Promise.resolve(mockRecords.find((record) => record.id === id) ?? null).then((task) => task ? cloneTask(task) : null, ) } async function getPatientVisits(patientId: string): Promise { const response = await request.get>>( `/v1/patients/${encodeURIComponent(patientId)}/visits`, { params: { page: 1, size: 200 } }, ) const page = unwrapApiResponse(response) return page.records.map(mapVisit) } async function getPatientById(id: string): Promise { try { const [patientResponse, visits] = await Promise.all([ request.get>(`/v1/patients/${encodeURIComponent(id)}`), getPatientVisits(id), ]) return mapPatient(unwrapApiResponse(patientResponse), visits) } catch (error) { if (isNotFoundError(error)) { return null } throw error } } async function getPatientByVisitNo(visitNo: string): Promise { const response = await request.get>>( '/v1/visits', { params: { page: 1, size: 20, visitNo }, }, ) const page = unwrapApiResponse(response) const visit = page.records[0] return visit ? getPatientById(visit.patientId) : null } function isUuid(value: string) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) } export async function getPatientProfile(keyword: string): Promise { if (!useMockData) { const normalizedKeyword = keyword.trim() if (isUuid(normalizedKeyword)) { return getPatientById(normalizedKeyword) } const patientResponse = await request.get>>( '/v1/patients', { params: { page: 1, size: 20, keyword: normalizedKeyword }, }, ) const patients = unwrapApiResponse(patientResponse).records const matchedPatient = patients.find( (patient) => patient.externalPatientId === normalizedKeyword || patient.name === normalizedKeyword, ) if (matchedPatient) { return getPatientById(matchedPatient.id) } if (patients.length) { return getPatientById(patients[0].id) } return getPatientByVisitNo(normalizedKeyword) } const normalizedKeyword = keyword.trim() const patient = mockPatients.find( (item) => item.id === normalizedKeyword || item.visits.some((visit) => visit.visitNo === normalizedKeyword), ) return Promise.resolve(patient ? clonePatient(patient) : null) } export async function getSigningTemplates( query: { campusId?: string departmentId?: string } = {}, ): Promise { if (!useMockData) { const response = await request.get< ApiResponseOf< AvailableTemplateVersionResponseDto[] | ApiPageResponse > >('/v1/templates/available-versions', { params: query }) const data = unwrapApiResponse(response) const records = Array.isArray(data) ? data : data.records return records.map(mapAvailableTemplate) } return Promise.resolve(mockTemplates.map((template) => ({ ...template }))) } export async function prepareSigningTask( id: string, expectedRowVersion?: number, ): Promise { const response = await request.post>( `/v1/sign-tasks/${encodeURIComponent(id)}/prepare-send`, getTaskActionBody(expectedRowVersion), ) return unwrapApiResponse(response) } export async function createSigningTask(input: CreateSigningTaskInput): Promise { if (!useMockData) { const payload: CreateSigningTaskRequest = { patientId: input.patientId, visitId: input.visitId, templateVersionId: input.templateVersionId, signMethod: input.method === 'sms' ? 'SMS' : 'PAD', idempotencyKey: createIdempotencyKey(), } const response = await request.post>( '/v1/sign-tasks', payload, ) const createdTask = unwrapApiResponse(response) const options: SigningApiOptions = { campus: input.campus, templates: [createTemplateFromInput(input)], } if (createdTask.status === 'CREATED') { await prepareSigningTask(createdTask.id, createdTask.rowVersion) const preparedTask = await getSigningTaskDetail(createdTask.id, options) if (preparedTask) { return preparedTask } } return mapSigningTask(createdTask, options) } const patient = mockPatients.find((item) => item.id === input.patientId) const task: SigningTaskRecord = { id: `task-${Date.now()}`, templateVersionId: input.templateVersionId, campus: input.campus, patientId: input.patientId, patientName: patient?.name ?? '演示患者', sex: patient?.sex ?? '男', age: patient?.age ?? 40, visitNo: input.visitNo, visitType: input.visitType, visitDate: patient?.visits.find((visit) => visit.id === input.visitId)?.visitDate ?? '2026-08-27', documentId: input.documentId, documentName: input.documentName, department: input.department, signerName: patient?.name ?? '演示患者', method: input.method, status: 'pending', source: '工作台发起', expiresAt: addHours('2026-08-27 12:00', 48), updatedAt: formatMockDate(), rowVersion: 1, backendStatus: 'WAITING_SIGN', } mockRecords.unshift(task) return Promise.resolve(cloneTask(task)) } export function updateSigningTaskMethod( id: string, method: SigningMethod, ): Promise { if (!useMockData) { throw new Error('当前后端未提供切换签署方式接口,请作废后重新创建任务') } const task = mockRecords.find((record) => record.id === id) if (!task) { return Promise.resolve(null) } task.method = method task.updatedAt = formatMockDate() return Promise.resolve(cloneTask(task)) } export function completeSigningTask( id: string, signatureDataUrl: string, ): Promise { if (!useMockData) { throw new Error('真实签署需要通过签署投递与文件上传接口完成') } const task = mockRecords.find((record) => record.id === id) if (!task) { return Promise.resolve(null) } task.status = 'signed' task.signatureDataUrl = signatureDataUrl task.signedAt = formatMockDate() task.updatedAt = task.signedAt return Promise.resolve(cloneTask(task)) } export async function reopenSigningTask( id: string, options: SigningTaskActionOptions = {}, ): Promise { if (!useMockData) { const response = await request.post>( `/v1/sign-tasks/${encodeURIComponent(id)}/reopen`, getTaskActionBody(options.expectedRowVersion), ) return mapSigningTask(unwrapApiResponse(response), options) } const task = mockRecords.find((record) => record.id === id) if (!task) { return Promise.resolve(null) } task.status = 'pending' task.signatureDataUrl = undefined task.signedAt = undefined task.updatedAt = formatMockDate() return Promise.resolve(cloneTask(task)) } export async function voidSigningTask( id: string, reason: string, options: SigningTaskActionOptions = {}, ): Promise { if (!useMockData) { const response = await request.post>( `/v1/sign-tasks/${encodeURIComponent(id)}/void`, { ...getTaskActionBody(options.expectedRowVersion), reason }, ) return mapSigningTask(unwrapApiResponse(response), options) } const task = mockRecords.find((record) => record.id === id) if (!task) { return Promise.resolve(null) } task.status = 'void' task.voidReason = reason task.signatureDataUrl = undefined task.updatedAt = formatMockDate() return Promise.resolve(cloneTask(task)) } export async function resendSigningSms( id: string, options: SigningTaskActionOptions = {}, ): Promise { if (!useMockData) { await resendSigningSmsDelivery(id, options.expectedRowVersion) return getSigningTaskDetail(id, options) } const task = mockRecords.find((record) => record.id === id) if (!task) { return Promise.resolve(null) } task.method = 'sms' task.status = 'pending' task.expiresAt = addHours('2026-08-27 12:00', 48) task.updatedAt = formatMockDate() return Promise.resolve(cloneTask(task)) } export async function getSigningTaskEvents(id: string): Promise { if (!useMockData) { const response = await request.get>( `/v1/sign-tasks/${encodeURIComponent(id)}/events`, ) return unwrapApiResponse(response).map(mapSigningEvent) } const task = mockRecords.find((record) => record.id === id) if (!task) { return [] } const events: SigningTaskEvent[] = [ { id: `${task.id}-created`, time: task.updatedAt, text: `张文静发起签署任务(${task.method === 'pad' ? '手写板' : '线上短信'})`, eventType: 'TASK_CREATED', }, ] if (task.status === 'signing') { events.push({ id: `${task.id}-signing`, time: task.updatedAt, text: '已打开签名采集,等待患者完成签名', eventType: 'SIGNING_STARTED', }) } if (task.signedAt) { events.push({ id: `${task.id}-signed`, time: task.signedAt, text: `患者 ${task.patientName} 完成签名,系统自动合成并双留存`, eventType: 'SIGNED', }) } if (task.status === 'void') { events.push({ id: `${task.id}-voided`, time: task.updatedAt, text: `任务已作废${task.voidReason ? `,原因:${task.voidReason}` : ''},全程可审计`, eventType: 'VOIDED', }) } return events } export function getSigningStatusLabel(status: SigningTaskStatus) { const labels: Record = { pending: '待签署', signing: '签署中', signed: '已签署', rejected: '已拒签', expired: '已超时', void: '已作废', failed: '处理失败', } return labels[status] } export function getVisitTypeLabel(type: VisitType) { return type }