feat(sign): 补齐医护端与患者端签署流程

This commit is contained in:
xy
2026-09-28 19:57:57 +08:00
parent a25716c861
commit 8024075439
22 changed files with 1082 additions and 163 deletions
@@ -1,11 +1,8 @@
import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { computed, reactive, ref, toValue, watch, type MaybeRefOrGetter } from 'vue'
import { ElMessage } from 'element-plus'
import { sendSigningSms } from '@/api/workbench/deliveries'
import {
buildLinkedPrescription,
type LinkedPrescription,
} from '@/api/workbench/prescriptions'
import { buildLinkedPrescription, type LinkedPrescription } from '@/api/workbench/prescriptions'
import { collectFieldDefs } from '@/utils/signing-fields'
import {
createSigningTask,
@@ -18,6 +15,7 @@ import type {
PatientProfile,
PatientVisit,
SigningMethod,
SigningFormField,
SigningTaskRecord,
SigningTemplate,
WorkbenchCampus,
@@ -54,6 +52,14 @@ export interface DocumentListRow {
export type { LinkedPrescription }
export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
const newIdempotencyKey = () =>
`clinical-web-${
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
? crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2)}`
}`
let createTaskIdempotencyKey = newIdempotencyKey()
let sendSmsIdempotencyKey = newIdempotencyKey()
const patientId = ref('')
const patient = ref<PatientProfile | null>(null)
const visits = ref<PatientVisit[]>([])
@@ -63,6 +69,10 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
const selectedVisitId = ref('')
const method = ref<SigningMethod>('pad')
const smsDestination = ref('')
const staffAnswers = reactive<Record<string, string>>({})
const signerType = ref<'SELF' | 'FAMILY'>('SELF')
const signerName = ref('')
const signerRelation = ref('')
const loadingTemplates = ref(false)
const locating = ref(false)
const submitting = ref(false)
@@ -81,6 +91,8 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
visit: '',
document: '',
sms: '',
staffAnswers: '',
signer: '',
})
/** 演示候选患者(真实模式为空数组,候选下拉自然不出现) */
@@ -94,6 +106,26 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
() => templates.value.find((template) => template.id === selectedTemplateId.value) ?? null,
)
const manualFormFields = computed(() => {
const systemFields = new Set([
'patient',
'sex',
'age',
'visit',
'phone',
'doctor',
'signedDate',
])
return (selectedTemplate.value?.formFields ?? []).filter(
(field) => field.kind !== 'signature' && !systemFields.has(field.id),
)
})
watch(selectedTemplateId, () => {
Object.keys(staffAnswers).forEach((key) => delete staffAnswers[key])
errors.staffAnswers = ''
})
/** 关联处方:随选中就诊带出(仅演示数据) */
const prescription = computed<LinkedPrescription | null>(() =>
isSigningMockEnabled ? buildLinkedPrescription(selectedVisit.value) : null,
@@ -302,6 +334,8 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
errors.visit = ''
errors.document = ''
errors.sms = ''
errors.staffAnswers = ''
errors.signer = ''
}
function resetForm() {
@@ -315,12 +349,18 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
selectedVisitId.value = ''
method.value = initialTemplate?.supportedMethods[0] ?? 'pad'
smsDestination.value = ''
Object.keys(staffAnswers).forEach((key) => delete staffAnswers[key])
signerType.value = 'SELF'
signerName.value = ''
signerRelation.value = ''
phonePlain.value = false
suggestionOpen.value = false
selectedDepartment.value = ''
selectedCategory.value = ''
expandedDepartments.value = []
clearErrors()
createTaskIdempotencyKey = newIdempotencyKey()
sendSmsIdempotencyKey = newIdempotencyKey()
}
function resetForNextTask() {
@@ -330,8 +370,14 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
selectedTemplateId.value = initialTemplate?.id ?? ''
method.value = initialTemplate?.supportedMethods[0] ?? method.value
smsDestination.value = ''
Object.keys(staffAnswers).forEach((key) => delete staffAnswers[key])
signerType.value = 'SELF'
signerName.value = ''
signerRelation.value = ''
errors.document = ''
errors.sms = ''
createTaskIdempotencyKey = newIdempotencyKey()
sendSmsIdempotencyKey = newIdempotencyKey()
initDocumentTree()
}
@@ -481,6 +527,9 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
* 演示模板一律声明全部四种;真实模式由文档库配置决定。
*/
function isMethodAllowed(nextMethod: SigningMethod) {
if (!isSigningMockEnabled && (nextMethod === 'wx' || nextMethod === 'ipad')) {
return false
}
const template = selectedTemplate.value
if (!template) {
@@ -511,6 +560,18 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
return `${value.slice(0, 3)}****${value.slice(-4)}`
}
function toggleStaffAnswerOption(fieldId: string, option: string, checked: boolean) {
const selected = (staffAnswers[fieldId] ?? '').split('、').filter(Boolean)
const next = checked
? [...new Set([...selected, option])]
: selected.filter((item) => item !== option)
staffAnswers[fieldId] = next.join('、')
}
function staffAnswerHasOption(fieldId: string, option: string) {
return (staffAnswers[fieldId] ?? '').split('、').includes(option)
}
function validate() {
clearErrors()
let valid = true
@@ -542,6 +603,22 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
}
}
const missingField = manualFormFields.value.find(
(field: SigningFormField) => field.required && !staffAnswers[field.id]?.trim(),
)
if (missingField) {
errors.staffAnswers = `请填写必填项:${missingField.label}`
valid = false
}
if (
signerType.value === 'FAMILY' &&
(!signerName.value.trim() || !signerRelation.value.trim())
) {
errors.signer = '家属代签必须填写签署人姓名和与患者关系'
valid = false
}
return valid
}
@@ -567,13 +644,29 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
documentContentHtml: selectedTemplate.value.contentHtml,
// 需科室确认由文书模板标记带出(原型 V6.9 起弹窗里不再让人选)
needConfirm: selectedTemplate.value.needConfirm ?? false,
staffAnswers: { ...staffAnswers },
signerType: signerType.value,
signerName: signerType.value === 'FAMILY' ? signerName.value.trim() : patient.value.name,
signerRelation: signerType.value === 'FAMILY' ? signerRelation.value.trim() : '本人',
idempotencyKey: createTaskIdempotencyKey,
})
if (!isSigningMockEnabled && method.value === 'sms') {
await sendSigningSms(task.id, {
destination: smsDestination.value.trim().replace(/\s+/g, ''),
templateCode: 'SIGN_LINK',
})
try {
await sendSigningSms(
task.id,
{
destination: smsDestination.value.trim().replace(/\s+/g, ''),
templateCode: 'SIGN_LINK',
},
sendSmsIdempotencyKey,
)
} catch {
throw new Error(
`签署任务 ${task.id} 已创建,但短信投递失败。请在工作台任务详情中重试发送。`,
)
}
sendSmsIdempotencyKey = newIdempotencyKey()
}
return task
@@ -612,6 +705,13 @@ export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
selectedVisit,
selectedVisitId,
smsDestination,
staffAnswers,
manualFormFields,
signerType,
signerName,
signerRelation,
staffAnswerHasOption,
toggleStaffAnswerOption,
submit,
submitting,
suggestionOpen,