import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue' import { ElMessage } from 'element-plus' import { sendSigningSms } from '@/api/workbench/deliveries' import { createSigningTask, getPatientProfile, getSigningTemplates, isSigningMockEnabled, } from '@/api/workbench/signing' import type { PatientProfile, PatientVisit, SigningMethod, SigningTaskRecord, SigningTemplate, WorkbenchCampus, } from '@/api/workbench/types' export interface UseSigningTaskFormOptions { campus: MaybeRefOrGetter initialTemplate?: MaybeRefOrGetter } interface TemplateCategoryGroup { name: string templates: SigningTemplate[] } interface TemplateDepartmentGroup { name: string categories: TemplateCategoryGroup[] } export function useSigningTaskForm(options: UseSigningTaskFormOptions) { const patientId = ref('') const patient = ref(null) const visits = ref([]) const templates = ref([]) const documentSearch = ref('') const selectedTemplateId = ref('') const selectedVisitId = ref('') const method = ref('pad') const smsDestination = ref('') const loadingTemplates = ref(false) const locating = ref(false) const submitting = ref(false) const errors = reactive({ patient: '', visit: '', document: '', sms: '', }) const selectedVisit = computed( () => visits.value.find((visit) => visit.id === selectedVisitId.value) ?? null, ) const selectedTemplate = computed( () => templates.value.find((template) => template.id === selectedTemplateId.value) ?? null, ) const filteredTemplateGroups = computed(() => { const keyword = documentSearch.value.trim().toLowerCase() const grouped = new Map>() templates.value.forEach((template) => { const matches = !keyword || [template.name, template.code, template.department, template.category] .join(' ') .toLowerCase() .includes(keyword) if (!matches) { return } const categories = grouped.get(template.department) ?? new Map() const categoryTemplates = categories.get(template.category) ?? [] categoryTemplates.push(template) categories.set(template.category, categoryTemplates) grouped.set(template.department, categories) }) return [...grouped.entries()].map(([department, categories]) => ({ name: department, categories: [...categories.entries()].map(([category, categoryTemplates]) => ({ name: category, templates: categoryTemplates, })), })) }) function getInitialTemplate() { return options.initialTemplate ? (toValue(options.initialTemplate) ?? null) : null } function includeInitialTemplate() { const initialTemplate = getInitialTemplate() if ( initialTemplate && !templates.value.some((template) => template.id === initialTemplate.id) ) { templates.value = [initialTemplate, ...templates.value] } } function clearErrors() { errors.patient = '' errors.visit = '' errors.document = '' errors.sms = '' } function resetForm() { const initialTemplate = getInitialTemplate() patientId.value = '' patient.value = null visits.value = [] documentSearch.value = '' selectedTemplateId.value = initialTemplate?.id ?? '' selectedVisitId.value = '' method.value = initialTemplate?.supportedMethods[0] ?? 'pad' smsDestination.value = '' clearErrors() } function resetForNextTask() { const initialTemplate = getInitialTemplate() documentSearch.value = '' selectedTemplateId.value = initialTemplate?.id ?? '' method.value = initialTemplate?.supportedMethods[0] ?? method.value smsDestination.value = '' errors.document = '' errors.sms = '' } async function loadTemplates() { if (!templates.value.length) { loadingTemplates.value = true try { templates.value = await getSigningTemplates() } catch { ElMessage.error('文档模板加载失败,请稍后重试') } finally { loadingTemplates.value = false } } includeInitialTemplate() } async function locatePatient() { const keyword = patientId.value.trim() errors.patient = '' errors.visit = '' patient.value = null visits.value = [] selectedVisitId.value = '' selectedTemplateId.value = getInitialTemplate()?.id ?? '' if (!keyword) { errors.patient = '请输入患者 ID、门诊号或住院号' return } locating.value = true try { const profile = await getPatientProfile(keyword) if (!profile) { errors.patient = '未找到该患者,请核对输入内容' return } patient.value = profile visits.value = profile.visits selectedVisitId.value = profile.visits.find((visit) => visit.isCurrent)?.id ?? profile.visits[0]?.id ?? '' } catch { errors.patient = '患者信息查询失败,请稍后重试' } finally { locating.value = false } } function selectVisit(visit: PatientVisit) { selectedVisitId.value = visit.id errors.visit = '' } function selectTemplate(template: SigningTemplate) { selectedTemplateId.value = template.id errors.document = '' if (!template.supportedMethods.includes(method.value)) { method.value = template.supportedMethods[0] ?? 'pad' } } function selectMethod(nextMethod: SigningMethod) { if (selectedTemplate.value && !selectedTemplate.value.supportedMethods.includes(nextMethod)) { return } method.value = nextMethod } function maskPhone(value: string) { if (value.length < 7) { return value } return `${value.slice(0, 3)}****${value.slice(-4)}` } function validate() { clearErrors() let valid = true if (!patient.value) { errors.patient = '请先定位患者' valid = false } if (!selectedVisit.value) { errors.visit = '请选择关联就诊' valid = false } if (!selectedTemplate.value) { errors.document = '请至少选择一份文档' valid = false } if (!isSigningMockEnabled && method.value === 'sms') { const destination = smsDestination.value.trim().replace(/\s+/g, '') if (!/^\+?[1-9]\d{6,14}$/.test(destination)) { errors.sms = '请输入完整的手机号或 E.164 地址,才能发送短信' valid = false } } return valid } async function submit(): Promise { if (!validate() || !patient.value || !selectedVisit.value || !selectedTemplate.value) { return null } submitting.value = true try { const task = await createSigningTask({ campus: toValue(options.campus), patientId: patient.value.id, visitId: selectedVisit.value.id, documentId: selectedTemplate.value.id, documentName: selectedTemplate.value.name, templateVersionId: selectedTemplate.value.versionId, method: method.value, visitType: selectedVisit.value.type, visitNo: selectedVisit.value.visitNo, department: selectedVisit.value.department, }) if (!isSigningMockEnabled && method.value === 'sms') { await sendSigningSms(task.id, { destination: smsDestination.value.trim().replace(/\s+/g, ''), templateCode: 'SIGN_LINK', }) } return task } finally { submitting.value = false } } return { documentSearch, errors, filteredTemplateGroups, loadingTemplates, loadTemplates, locatePatient, locating, maskPhone, method, patient, patientId, resetForNextTask, resetForm, selectMethod, selectTemplate, selectVisit, selectedTemplate, selectedTemplateId, selectedVisit, selectedVisitId, smsDestination, submit, submitting, templates, visits, } }