feat(clinical-web): 新增签署任务弹窗与文档库管理页面
This commit is contained in:
@@ -84,7 +84,7 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页和医护端整体布局。当前所有业务 View 暂时保留文字占位,登录使用演示认证逻辑,尚未接入真实后端。
|
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页、首页、签署工作台第一版 Mock 和文档库管理页面。报表、用户权限、文档权限和系统设置仍保留页面骨架;登录和签署流程使用演示数据,尚未接入真实后端。
|
||||||
|
|
||||||
## 当前路由结构
|
## 当前路由结构
|
||||||
|
|
||||||
@@ -108,6 +108,11 @@ ClinicalLayout
|
|||||||
|
|
||||||
页面目录按业务域组织:`auth` 保留登录页;工作台页面位于 `views/workbench`;管理页面位于 `views/management`。每个具体页面目录都有 `index.vue` 入口和 `components` 目录,用于继续拆分当前页面组件。
|
页面目录按业务域组织:`auth` 保留登录页;工作台页面位于 `views/workbench`;管理页面位于 `views/management`。每个具体页面目录都有 `index.vue` 入口和 `components` 目录,用于继续拆分当前页面组件。
|
||||||
|
|
||||||
|
跨页面复用的业务组件和 composable 不放在具体 View 目录中:
|
||||||
|
|
||||||
|
- `components/signing/NewSigningTaskDialog.vue`:新增签署任务公共弹窗,供签署工作台和文档库管理复用;
|
||||||
|
- `composables/useSigningTaskForm.ts`:封装患者定位、就诊选择、模板选择、签署方式、校验和任务提交状态。
|
||||||
|
|
||||||
## API 与类型约定
|
## API 与类型约定
|
||||||
|
|
||||||
API 模块与页面按业务域对应。工作台页面使用 `api/workbench` 下的页面文件,管理页面使用 `api/management` 下的直接文件:
|
API 模块与页面按业务域对应。工作台页面使用 `api/workbench` 下的页面文件,管理页面使用 `api/management` 下的直接文件:
|
||||||
|
|||||||
@@ -1,29 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import { createSigningTask, getPatientProfile, getSigningTemplates } from '@/api/workbench/signing'
|
import type { SigningTaskRecord, SigningTemplate } from '@/api/workbench/types'
|
||||||
import type {
|
import { useSigningTaskForm } from '@/composables/useSigningTaskForm'
|
||||||
PatientProfile,
|
|
||||||
PatientVisit,
|
|
||||||
SigningMethod,
|
|
||||||
SigningTaskRecord,
|
|
||||||
SigningTemplate,
|
|
||||||
} from '@/api/workbench/types'
|
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
interface TemplateCategoryGroup {
|
|
||||||
name: string
|
|
||||||
templates: SigningTemplate[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TemplateDepartmentGroup {
|
|
||||||
name: string
|
|
||||||
categories: TemplateCategoryGroup[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
visible: boolean
|
visible: boolean
|
||||||
|
initialTemplate?: SigningTemplate | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -33,231 +18,53 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const patientInput = ref<HTMLInputElement | null>(null)
|
const patientInput = ref<HTMLInputElement | null>(null)
|
||||||
const patientId = ref('')
|
const initialTemplate = computed(() => props.initialTemplate ?? null)
|
||||||
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({
|
const {
|
||||||
patient: '',
|
documentSearch,
|
||||||
visit: '',
|
errors,
|
||||||
document: '',
|
filteredTemplateGroups,
|
||||||
phone: '',
|
loadingTemplates,
|
||||||
|
loadTemplates,
|
||||||
|
locatePatient,
|
||||||
|
locating,
|
||||||
|
maskPhone,
|
||||||
|
method,
|
||||||
|
patient,
|
||||||
|
patientId,
|
||||||
|
patientPhoneRevealed,
|
||||||
|
resetForNextTask,
|
||||||
|
resetForm,
|
||||||
|
selectMethod,
|
||||||
|
selectTemplate,
|
||||||
|
selectVisit,
|
||||||
|
selectedTemplate,
|
||||||
|
selectedTemplateId,
|
||||||
|
selectedVisit,
|
||||||
|
selectedVisitId,
|
||||||
|
smsPhone,
|
||||||
|
submit: submitForm,
|
||||||
|
submitting,
|
||||||
|
togglePhone,
|
||||||
|
visits,
|
||||||
|
} = useSigningTaskForm({
|
||||||
|
campus: computed(() => appStore.selectedCampus),
|
||||||
|
initialTemplate,
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedVisit = computed(
|
async function submitTask(keepOpen: boolean) {
|
||||||
() => visits.value.find((visit) => visit.id === selectedVisitId.value) ?? null,
|
try {
|
||||||
)
|
const task = await submitForm()
|
||||||
|
|
||||||
const selectedTemplate = computed(
|
if (!task) {
|
||||||
() => 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
|
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 clearErrors() {
|
|
||||||
errors.patient = ''
|
|
||||||
errors.visit = ''
|
|
||||||
errors.document = ''
|
|
||||||
errors.phone = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
patientId.value = ''
|
|
||||||
patient.value = null
|
|
||||||
visits.value = []
|
|
||||||
documentSearch.value = ''
|
|
||||||
selectedTemplateId.value = ''
|
|
||||||
selectedVisitId.value = ''
|
|
||||||
method.value = 'pad'
|
|
||||||
smsPhone.value = ''
|
|
||||||
patientPhoneRevealed.value = false
|
|
||||||
clearErrors()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTemplates() {
|
|
||||||
if (templates.value.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loadingTemplates.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
templates.value = await getSigningTemplates()
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('文档模板加载失败,请稍后重试')
|
|
||||||
} finally {
|
|
||||||
loadingTemplates.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function locatePatient() {
|
|
||||||
const keyword = patientId.value.trim()
|
|
||||||
errors.patient = ''
|
|
||||||
errors.visit = ''
|
|
||||||
patient.value = null
|
|
||||||
visits.value = []
|
|
||||||
selectedVisitId.value = ''
|
|
||||||
selectedTemplateId.value = ''
|
|
||||||
|
|
||||||
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(keepOpen: boolean) {
|
|
||||||
if (!validate() || !patient.value || !selectedVisit.value || !selectedTemplate.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const task = await createSigningTask({
|
|
||||||
campus: appStore.selectedCampus,
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
emit('created', task)
|
emit('created', task)
|
||||||
ElMessage.success(`已创建 1 个签署任务${method.value === 'sms' ? ',短信已发送' : ''}`)
|
ElMessage.success(`已创建 1 个签署任务${task.method === 'sms' ? ',短信已发送' : ''}`)
|
||||||
|
|
||||||
if (keepOpen) {
|
if (keepOpen) {
|
||||||
selectedTemplateId.value = ''
|
resetForNextTask()
|
||||||
documentSearch.value = ''
|
|
||||||
errors.document = ''
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
patientInput.value?.focus()
|
patientInput.value?.focus()
|
||||||
return
|
return
|
||||||
@@ -266,8 +73,6 @@ async function submit(keepOpen: boolean) {
|
|||||||
emit('close')
|
emit('close')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('签署任务创建失败,请稍后重试')
|
ElMessage.error('签署任务创建失败,请稍后重试')
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,11 +314,16 @@ watch(
|
|||||||
type="button"
|
type="button"
|
||||||
class="action-button action-button--ghost"
|
class="action-button action-button--ghost"
|
||||||
:disabled="submitting"
|
:disabled="submitting"
|
||||||
@click="submit(true)"
|
@click="submitTask(true)"
|
||||||
>
|
>
|
||||||
提交并继续新增
|
提交并继续新增
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="action-button" :disabled="submitting" @click="submit(false)">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="action-button"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="submitTask(false)"
|
||||||
|
>
|
||||||
{{ submitting ? '提交中…' : '提交任务' }}
|
{{ submitting ? '提交中…' : '提交任务' }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
307
clinical-web/src/composables/useSigningTaskForm.ts
Normal file
307
clinical-web/src/composables/useSigningTaskForm.ts
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
||||||
|
import type { SigningTemplate } from '@/api/workbench/types'
|
||||||
|
|
||||||
import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
|
import DocumentFilterTabs from './components/DocumentFilterTabs.vue'
|
||||||
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
|
import DocumentLibraryHeader from './components/DocumentLibraryHeader.vue'
|
||||||
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
|
import DocumentPreviewDialog from './components/DocumentPreviewDialog.vue'
|
||||||
@@ -18,6 +21,8 @@ import type {
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const library = ref<DocumentDepartment[]>([])
|
const library = ref<DocumentDepartment[]>([])
|
||||||
const selectedTemplate = ref<DocumentTemplate | null>(null)
|
const selectedTemplate = ref<DocumentTemplate | null>(null)
|
||||||
|
const signingTemplate = ref<SigningTemplate | null>(null)
|
||||||
|
const signingDialogVisible = ref(false)
|
||||||
|
|
||||||
const filters = reactive<DocumentFilterForm>({
|
const filters = reactive<DocumentFilterForm>({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@@ -96,8 +101,27 @@ function showEditMessage(template: DocumentTemplate) {
|
|||||||
ElMessage.info('原型演示:编辑模板《' + template.name + '》')
|
ElMessage.info('原型演示:编辑模板《' + template.name + '》')
|
||||||
}
|
}
|
||||||
|
|
||||||
function showInitiateMessage(template: DocumentTemplate) {
|
function toSigningTemplate(template: DocumentTemplate): SigningTemplate {
|
||||||
ElMessage.info('已选择《' + template.name + '》,签署工作台暂未实现')
|
return {
|
||||||
|
id: template.id,
|
||||||
|
name: template.name,
|
||||||
|
code: template.code,
|
||||||
|
department: template.department,
|
||||||
|
category: template.category,
|
||||||
|
description: template.description,
|
||||||
|
supportedMethods: ['pad', 'sms'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSigningDialog(template: DocumentTemplate) {
|
||||||
|
closePreview()
|
||||||
|
signingTemplate.value = toSigningTemplate(template)
|
||||||
|
signingDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSigningDialog() {
|
||||||
|
signingDialogVisible.value = false
|
||||||
|
signingTemplate.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
@@ -106,8 +130,7 @@ function clearFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useTemplate(template: DocumentTemplate) {
|
function useTemplate(template: DocumentTemplate) {
|
||||||
closePreview()
|
openSigningDialog(template)
|
||||||
ElMessage.info('原型演示:从模板《' + template.name + '》发起签署')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadLibrary)
|
onMounted(loadLibrary)
|
||||||
@@ -146,7 +169,7 @@ onMounted(loadLibrary)
|
|||||||
:template="template"
|
:template="template"
|
||||||
@preview="handlePreview"
|
@preview="handlePreview"
|
||||||
@edit="showEditMessage"
|
@edit="showEditMessage"
|
||||||
@initiate="showInitiateMessage"
|
@initiate="openSigningDialog"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,6 +192,11 @@ onMounted(loadLibrary)
|
|||||||
@close="closePreview"
|
@close="closePreview"
|
||||||
@use="useTemplate"
|
@use="useTemplate"
|
||||||
/>
|
/>
|
||||||
|
<NewSigningTaskDialog
|
||||||
|
:visible="signingDialogVisible"
|
||||||
|
:initial-template="signingTemplate"
|
||||||
|
@close="closeSigningDialog"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import type {
|
|||||||
VisitType,
|
VisitType,
|
||||||
WorkbenchCampus,
|
WorkbenchCampus,
|
||||||
} from '@/api/workbench/types'
|
} from '@/api/workbench/types'
|
||||||
|
import NewSigningTaskDialog from '@/components/signing/NewSigningTaskDialog.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
import NewSigningTaskDialog from './components/NewSigningTaskDialog.vue'
|
|
||||||
import OnlineSigningDialog from './components/OnlineSigningDialog.vue'
|
import OnlineSigningDialog from './components/OnlineSigningDialog.vue'
|
||||||
import SignaturePadDialog from './components/SignaturePadDialog.vue'
|
import SignaturePadDialog from './components/SignaturePadDialog.vue'
|
||||||
import SigningFilterBar from './components/SigningFilterBar.vue'
|
import SigningFilterBar from './components/SigningFilterBar.vue'
|
||||||
|
|||||||
Reference in New Issue
Block a user