feat(clinical-web): 新增签署任务弹窗与文档库管理页面

This commit is contained in:
yelan
2026-08-28 15:35:20 +08:00
parent 2de75ed993
commit 1e2ff79b0c
5 changed files with 395 additions and 245 deletions
@@ -0,0 +1,307 @@
import { computed, reactive, ref, toValue, type MaybeRefOrGetter } from 'vue'
import { ElMessage } from 'element-plus'
import { createSigningTask, getPatientProfile, getSigningTemplates } from '@/api/workbench/signing'
import type {
PatientProfile,
PatientVisit,
SigningMethod,
SigningTaskRecord,
SigningTemplate,
WorkbenchCampus,
} from '@/api/workbench/types'
export interface UseSigningTaskFormOptions {
campus: MaybeRefOrGetter<WorkbenchCampus>
initialTemplate?: MaybeRefOrGetter<SigningTemplate | null | undefined>
}
interface TemplateCategoryGroup {
name: string
templates: SigningTemplate[]
}
interface TemplateDepartmentGroup {
name: string
categories: TemplateCategoryGroup[]
}
export function useSigningTaskForm(options: UseSigningTaskFormOptions) {
const patientId = ref('')
const patient = ref<PatientProfile | null>(null)
const visits = ref<PatientVisit[]>([])
const templates = ref<SigningTemplate[]>([])
const documentSearch = ref('')
const selectedTemplateId = ref('')
const selectedVisitId = ref('')
const method = ref<SigningMethod>('pad')
const smsPhone = ref('')
const patientPhoneRevealed = ref(false)
const loadingTemplates = ref(false)
const locating = ref(false)
const submitting = ref(false)
const errors = reactive({
patient: '',
visit: '',
document: '',
phone: '',
})
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<TemplateDepartmentGroup[]>(() => {
const keyword = documentSearch.value.trim().toLowerCase()
const grouped = new Map<string, Map<string, SigningTemplate[]>>()
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<string, SigningTemplate[]>()
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.phone = ''
}
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'
smsPhone.value = ''
patientPhoneRevealed.value = false
clearErrors()
}
function resetForNextTask() {
const initialTemplate = getInitialTemplate()
documentSearch.value = ''
selectedTemplateId.value = initialTemplate?.id ?? ''
method.value = initialTemplate?.supportedMethods[0] ?? method.value
errors.document = ''
}
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 = '未找到该患者,请核对输入内容'
smsPhone.value = ''
return
}
patient.value = profile
visits.value = profile.visits
selectedVisitId.value =
profile.visits.find((visit) => visit.isCurrent)?.id ?? profile.visits[0]?.id ?? ''
smsPhone.value = profile.phone
} 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
errors.phone = ''
}
function maskPhone(value: string) {
if (value.length < 7) {
return value
}
return `${value.slice(0, 3)}****${value.slice(-4)}`
}
function togglePhone() {
patientPhoneRevealed.value = !patientPhoneRevealed.value
ElMessage.info('明文查看已记录审计日志')
}
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 (method.value === 'sms' && !smsPhone.value.trim()) {
errors.phone = '请确认短信接收手机号'
valid = false
}
return valid
}
async function submit(): Promise<SigningTaskRecord | null> {
if (!validate() || !patient.value || !selectedVisit.value || !selectedTemplate.value) {
return null
}
submitting.value = true
try {
return await createSigningTask({
campus: toValue(options.campus),
patientId: patient.value.id,
visitId: selectedVisit.value.id,
documentId: selectedTemplate.value.id,
documentName: selectedTemplate.value.name,
method: method.value,
visitType: selectedVisit.value.type,
visitNo: selectedVisit.value.visitNo,
department: selectedVisit.value.department,
phone: method.value === 'sms' ? smsPhone.value.trim() : undefined,
})
} finally {
submitting.value = false
}
}
return {
documentSearch,
errors,
filteredTemplateGroups,
loadingTemplates,
loadTemplates,
locatePatient,
locating,
maskPhone,
method,
patient,
patientId,
patientPhoneRevealed,
resetForNextTask,
resetForm,
selectMethod,
selectTemplate,
selectVisit,
selectedTemplate,
selectedTemplateId,
selectedVisit,
selectedVisitId,
smsPhone,
submit,
submitting,
templates,
togglePhone,
visits,
}
}