feat(workbench): 扩展签署任务管理功能

This commit is contained in:
yelan
2026-08-28 10:02:51 +08:00
parent b533ab2a58
commit 2de75ed993
14 changed files with 4287 additions and 37 deletions
+500 -18
View File
@@ -1,48 +1,334 @@
import { request } from '@/utils/request'
import type { SigningTaskListResponse, SigningTaskQuery, SigningTaskRecord } from './types'
import type {
CreateSigningTaskRequest,
PatientProfile,
SigningMethod,
SigningTaskListResponse,
SigningTaskQuery,
SigningTaskRecord,
SigningTaskStatus,
SigningTemplate,
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',
name: '住院患者知情同意书',
code: 'DOC-HOS-001',
department: '全院通用',
category: '住院告知',
description: '住院期间诊疗事项、风险与患者权利告知。',
supportedMethods: ['pad', 'sms'],
},
{
id: 'tpl-002',
name: '住院须知及告知书',
code: 'DOC-HOS-002',
department: '全院通用',
category: '住院告知',
description: '住院流程、陪护要求和患者须知确认。',
supportedMethods: ['pad', 'sms'],
},
{
id: 'tpl-003',
name: '内镜检查知情同意书',
code: 'DOC-DIG-001',
department: '消化内科',
category: '检查告知',
description: '内镜检查适应证、风险及替代方案告知。',
supportedMethods: ['pad', 'sms'],
},
{
id: 'tpl-004',
name: 'MRI 磁共振检查知情同意书',
code: 'DOC-RAD-001',
department: '放射科',
category: '检查告知',
description: '磁共振检查注意事项、禁忌证及风险告知。',
supportedMethods: ['pad', 'sms'],
},
{
id: 'tpl-005',
name: '心脏介入手术知情同意书',
code: 'DOC-CARD-001',
department: '心内科',
category: '手术告知',
description: '介入治疗方案、围术期风险及替代方案告知。',
supportedMethods: ['pad'],
},
{
id: 'tpl-006',
name: '体检报告发放及知情同意书',
code: 'DOC-PE-001',
department: '健康管理中心',
category: '体检告知',
description: '体检报告领取方式、隐私保护和异常结果随访告知。',
supportedMethods: ['pad', 'sms'],
},
{
id: 'tpl-007',
name: '患者授权委托书',
code: 'DOC-GEN-003',
department: '医务处',
category: '授权文书',
description: '患者授权家属或代理人办理相关医疗事项。',
supportedMethods: ['pad', 'sms'],
},
]
const mockRecords: SigningTaskRecord[] = [
{
id: 'task-001',
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-28 09:30',
expiresAt: '2026-08-30 09:30',
updatedAt: '2026-08-27 09:30',
},
{
id: 'task-002',
campus: '东院区',
patientId: 'P202610002',
patientName: '王某某',
sex: '女',
age: 37,
visitNo: 'JZ20260827018',
documentName: '急诊留观知情同意书',
department: '儿科急诊',
visitType: '门诊',
visitDate: '2026-08-27',
documentId: 'tpl-004',
documentName: 'MRI 磁共振检查知情同意书',
department: '放射科',
status: 'signed',
signerName: '王某某家属',
method: 'sms',
source: '工作台发起',
expiresAt: '2026-08-27 18:00',
expiresAt: '2026-08-29 18:00',
updatedAt: '2026-08-27 09:12',
signedAt: '2026-08-27 09:12',
},
{
id: 'task-003',
campus: '本部院区',
patientId: 'P202610003',
patientName: '赵某某',
sex: '男',
age: 52,
visitNo: 'ZY20260826027',
visitType: '住院',
visitDate: '2026-08-26',
documentId: 'tpl-003',
documentName: '内镜检查知情同意书',
department: '消化内科',
status: 'rejected',
status: 'expired',
signerName: '赵某某家属',
method: 'sms',
source: '电子病历',
expiresAt: '2026-08-27 16:00',
updatedAt: '2026-08-27 08:45',
},
{
id: 'task-004',
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',
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 function getSigningTasks(query: SigningTaskQuery): Promise<SigningTaskListResponse> {
if (!useMockData) {
return request.get<SigningTaskListResponse>('/workbench/signing/tasks', {
@@ -51,23 +337,49 @@ export function getSigningTasks(query: SigningTaskQuery): Promise<SigningTaskLis
}
const keyword = query.keyword?.trim().toLowerCase()
const filteredRecords = mockRecords.filter((record) => {
const matchesKeyword =
!keyword ||
[record.patientName, record.visitNo, record.documentName]
.join(' ')
.toLowerCase()
.includes(keyword)
const matchesStatus = !query.status || query.status === 'all' || record.status === query.status
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(),
)
return matchesKeyword && matchesStatus
})
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),
records: filteredRecords.slice(start, start + pageSize).map(cloneTask),
total: filteredRecords.length,
page,
pageSize,
@@ -79,5 +391,175 @@ export function getSigningTaskDetail(id: string): Promise<SigningTaskRecord | nu
return request.get<SigningTaskRecord>('/workbench/signing/tasks/' + id)
}
return Promise.resolve(mockRecords.find((record) => record.id === id) ?? null)
return Promise.resolve(mockRecords.find((record) => record.id === id) ?? null).then((task) =>
task ? cloneTask(task) : null,
)
}
export function getPatientProfile(keyword: string): Promise<PatientProfile | null> {
if (!useMockData) {
return request.get<PatientProfile>('/patients/' + encodeURIComponent(keyword))
}
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 function getSigningTemplates(): Promise<SigningTemplate[]> {
if (!useMockData) {
return request.get<SigningTemplate[]>('/workbench/signing/templates')
}
return Promise.resolve(mockTemplates.map((template) => ({ ...template })))
}
export function createSigningTask(payload: CreateSigningTaskRequest): Promise<SigningTaskRecord> {
if (!useMockData) {
return request.post<SigningTaskRecord>('/workbench/signing/tasks', payload)
}
const patient = mockPatients.find((item) => item.id === payload.patientId)
const task: SigningTaskRecord = {
id: `task-${Date.now()}`,
campus: payload.campus,
patientId: payload.patientId,
patientName: patient?.name ?? '演示患者',
sex: patient?.sex ?? '男',
age: patient?.age ?? 40,
visitNo: payload.visitNo,
visitType: payload.visitType,
visitDate:
patient?.visits.find((visit) => visit.id === payload.visitId)?.visitDate ?? '2026-08-27',
documentId: payload.documentId,
documentName: payload.documentName,
department: payload.department,
signerName: patient?.name ?? '演示患者',
method: payload.method,
status: 'pending',
source: '工作台发起',
expiresAt: addHours('2026-08-27 12:00', 48),
updatedAt: formatMockDate(),
}
mockRecords.unshift(task)
return Promise.resolve(cloneTask(task))
}
export function updateSigningTaskMethod(
id: string,
method: SigningMethod,
): Promise<SigningTaskRecord | null> {
if (!useMockData) {
return request.put<SigningTaskRecord>('/workbench/signing/tasks/' + id + '/method', { method })
}
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<SigningTaskRecord | null> {
if (!useMockData) {
return request.post<SigningTaskRecord>('/workbench/signing/tasks/' + id + '/complete', {
signatureDataUrl,
})
}
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 function reopenSigningTask(id: string): Promise<SigningTaskRecord | null> {
if (!useMockData) {
return request.post<SigningTaskRecord>('/workbench/signing/tasks/' + id + '/reopen')
}
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 function voidSigningTask(id: string, reason: string): Promise<SigningTaskRecord | null> {
if (!useMockData) {
return request.post<SigningTaskRecord>('/workbench/signing/tasks/' + id + '/void', { reason })
}
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 function resendSigningSms(id: string): Promise<SigningTaskRecord | null> {
if (!useMockData) {
return request.post<SigningTaskRecord>('/workbench/signing/tasks/' + id + '/resend-sms')
}
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 function getSigningStatusLabel(status: SigningTaskStatus) {
const labels: Record<SigningTaskStatus, string> = {
pending: '待签署',
signing: '签署中',
signed: '已签署',
rejected: '已拒签',
expired: '已超时',
void: '已作废',
}
return labels[status]
}
export function getVisitTypeLabel(type: VisitType) {
return type
}
+64 -1
View File
@@ -9,7 +9,38 @@ export interface WorkbenchOverviewQuery {
rankingPeriod: HomeRankingPeriod
}
export type SigningTaskStatus = 'pending' | 'signing' | 'signed' | 'rejected' | 'expired'
export type SigningTaskStatus = 'pending' | 'signing' | 'signed' | 'rejected' | 'expired' | 'void'
export type VisitType = '门诊' | '住院' | '体检'
export interface PatientVisit {
id: string
type: VisitType
visitNo: string
department: string
visitDate: string
isCurrent?: boolean
}
export interface PatientProfile {
id: string
name: string
sex: '男' | '女'
age: number
idCard: string
phone: string
visits: PatientVisit[]
}
export interface SigningTemplate {
id: string
name: string
code: string
department: string
category: string
description: string
supportedMethods: SigningMethod[]
}
export interface WorkbenchSummary {
todaySigned: number
@@ -66,12 +97,44 @@ export interface WorkbenchOverviewResponse {
export interface SigningTaskQuery extends PageQuery {
keyword?: string
status?: SigningTaskStatus | 'all'
dateRange?: SigningDateRange
campus?: WorkbenchCampus | 'all'
method?: SigningMethod | 'all'
department?: string
documentId?: string
visitType?: VisitType | 'all'
}
export type SigningDateRange = 'today' | 'yesterday' | '3d' | '7d' | 'all'
export interface SigningTaskRecord extends WorkbenchTask {
campus: WorkbenchCampus
patientId: string
sex: '男' | '女'
age: number
documentId: string
signerName: string
method: SigningMethod
source: string
expiresAt: string
visitType: VisitType
visitDate: string
signatureDataUrl?: string
signedAt?: string
voidReason?: string
}
export type SigningTaskListResponse = PageResult<SigningTaskRecord>
export interface CreateSigningTaskRequest {
campus: WorkbenchCampus
patientId: string
visitId: string
documentId: string
documentName: string
method: SigningMethod
visitType: VisitType
visitNo: string
department: string
phone?: string
}