feat: add reviewed follow-up audio patient enrichment
This commit is contained in:
@@ -52,7 +52,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<el-tabs v-model="activeTab" class="diagnosis-tabs tcm-diagnosis-tabs">
|
||||
<el-tabs v-model="activeTab" :before-leave="beforeTabLeave" class="diagnosis-tabs tcm-diagnosis-tabs">
|
||||
<!-- 基本信息标签页 -->
|
||||
<el-tab-pane label="病历" name="basic">
|
||||
<el-form
|
||||
@@ -660,6 +660,7 @@
|
||||
lazy
|
||||
>
|
||||
<daily-matrix
|
||||
ref="dailyMatrixRef"
|
||||
v-if="formData.id"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
:patient-id="Number(formData.patient_id)"
|
||||
@@ -670,6 +671,21 @@
|
||||
<el-empty v-else description="请先保存诊单后查看" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane v-if="followupCapabilities?.enabled && formData.id" label="回访录音" name="followupAudio" lazy>
|
||||
<FollowupAudioPanel
|
||||
v-if="visible && formData.id && followupCapabilities"
|
||||
:key="Number(formData.id)"
|
||||
ref="followupAudioRef"
|
||||
:diagnosis-id="Number(formData.id)"
|
||||
:view-only="viewOnly"
|
||||
:active="activeTab === 'followupAudio'"
|
||||
:capabilities="followupCapabilities"
|
||||
:basic-dirty="basicDirty"
|
||||
:before-apply="canApplyFollowup"
|
||||
@applied="handleFollowupApplied"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 病历记录标签页(开方后自动显示) -->
|
||||
<el-tab-pane label="处方" name="bingli" :disabled="!formData.id" v-if="hasPermission(['tcm.diagnosis/chufang'])">
|
||||
<div v-if="formData.id" class="bingli-tab-content">
|
||||
@@ -785,6 +801,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { tcmDiagnosisAdd, tcmDiagnosisEdit, tcmDiagnosisDetail, checkPhone, checkIdCard, diagnosisTrackingNotes } from '@/api/tcm'
|
||||
import { getDictData } from '@/api/app'
|
||||
import feedback from '@/utils/feedback'
|
||||
@@ -794,6 +811,8 @@ import useAppStore from '@/stores/modules/app'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import DailyMatrix from './components/DailyMatrix.vue'
|
||||
import FollowupAudioPanel from './components/FollowupAudioPanel.vue'
|
||||
import { followupAudioCapabilities, type FollowupCapabilities } from '@/api/followupAudio'
|
||||
import CaseRecordList from './components/CaseRecordList.vue'
|
||||
import CallRecordPanel from './components/CallRecordPanel.vue'
|
||||
import ImChatRecordPanel from './components/ImChatRecordPanel.vue'
|
||||
@@ -811,6 +830,12 @@ const getImageUrl = (url: string) => (url?.indexOf?.('http') === 0 ? url : appSt
|
||||
const caseRecordListRef = ref()
|
||||
const callRecordPanelRef = ref<{ refresh?: () => void } | null>(null)
|
||||
const prescriptionRef = ref()
|
||||
const dailyMatrixRef = ref<{ refresh?: () => Promise<void> | void } | null>(null)
|
||||
const followupAudioRef = ref<{ confirmLeave: () => Promise<boolean> } | null>(null)
|
||||
const followupCapabilities = ref<FollowupCapabilities | null>(null)
|
||||
const basicSnapshot = ref('')
|
||||
let diagnosisGeneration = 0
|
||||
let capabilitiesRequest: AbortController | undefined
|
||||
|
||||
const visible = ref(false)
|
||||
/** 只读查看:与 consumers 处方等处「查看诊单」一致,全部 Tab 仅展示不可保存 */
|
||||
@@ -970,6 +995,45 @@ const formData = ref({
|
||||
external_userid: ''
|
||||
})
|
||||
|
||||
// Include unmasked identity values so merely revealing a number is not an unsaved edit.
|
||||
const basicFingerprint = () => JSON.stringify({ ...formData.value,
|
||||
phone: originalPhone.value || formData.value.phone, id_card: originalIdCard.value || formData.value.id_card })
|
||||
const basicDirty = computed(() => !viewOnly.value && !!basicSnapshot.value && basicFingerprint() !== basicSnapshot.value)
|
||||
const canApplyFollowup = () => !basicDirty.value && visible.value && !viewOnly.value
|
||||
async function beforeTabLeave(next: string | number, previous: string | number): Promise<boolean> {
|
||||
if (next === 'followupAudio' && basicDirty.value) {
|
||||
feedback.msgWarning('病历有未保存修改,请先保存后再进入回访录音审核,当前输入已保留')
|
||||
return false
|
||||
}
|
||||
if (previous === 'followupAudio') return await followupAudioRef.value?.confirmLeave() ?? true
|
||||
return true
|
||||
}
|
||||
async function handleFollowupApplied(diagnosisId: number) {
|
||||
if (!visible.value || Number(formData.value.id) !== diagnosisId) return
|
||||
const generation = diagnosisGeneration
|
||||
try {
|
||||
// The response itself is also guarded, including edits made while it is in flight.
|
||||
await loadDiagnosisDetailIntoForm(diagnosisId, true)
|
||||
if (generation !== diagnosisGeneration || !visible.value || Number(formData.value.id) !== diagnosisId) return
|
||||
await dailyMatrixRef.value?.refresh?.()
|
||||
emit('success')
|
||||
} catch {
|
||||
if (generation === diagnosisGeneration) feedback.msgWarning('录音已采纳,但病历或日常记录刷新失败,请手动刷新查看')
|
||||
}
|
||||
}
|
||||
watch([visible, () => formData.value.id], async ([isVisible, id]) => {
|
||||
capabilitiesRequest?.abort()
|
||||
followupCapabilities.value = null
|
||||
if (!isVisible || !id) return
|
||||
const request = new AbortController()
|
||||
capabilitiesRequest = request
|
||||
try {
|
||||
const result = await followupAudioCapabilities(Number(id), request.signal)
|
||||
if (!request.signal.aborted && visible.value && String(formData.value.id) === String(id)) followupCapabilities.value = result
|
||||
} catch { /* Fail closed: an unavailable capability endpoint must not expose this feature. */ }
|
||||
})
|
||||
onBeforeUnmount(() => { diagnosisGeneration++; capabilitiesRequest?.abort() })
|
||||
|
||||
/** 就诊卡创建来源(与 zyt_tcm_diagnosis.create_source 一致) */
|
||||
const createSourceOptions = [
|
||||
{ value: '', label: '未设置' },
|
||||
@@ -1260,8 +1324,14 @@ const getDictOptions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
|
||||
async function loadDiagnosisDetailIntoForm(id: number, preserveDirty = false): Promise<void> {
|
||||
const generation = diagnosisGeneration
|
||||
const data = await tcmDiagnosisDetail({ id })
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
if (preserveDirty && basicDirty.value) {
|
||||
feedback.msgWarning('录音已采纳,病历仍保留你的未保存修改;请核对后再保存,未用刷新覆盖输入')
|
||||
return
|
||||
}
|
||||
|
||||
originalPhone.value = data.phone || ''
|
||||
originalIdCard.value = data.id_card || ''
|
||||
@@ -1277,6 +1347,7 @@ async function loadDiagnosisDetailIntoForm(id: number): Promise<void> {
|
||||
formData.value.show_card = Number(data.show_card) === 0 ? 0 : 1
|
||||
const y = data.diabetes_discovery_year
|
||||
formData.value.diabetes_discovery_year = y == null || y === '' ? '' : String(y)
|
||||
basicSnapshot.value = basicFingerprint()
|
||||
}
|
||||
|
||||
const resetPatientBasicLockState = () => {
|
||||
@@ -1286,6 +1357,11 @@ const resetPatientBasicLockState = () => {
|
||||
}
|
||||
|
||||
const open = async (type: string, id?: number) => {
|
||||
const generation = ++diagnosisGeneration
|
||||
submitting.value = false
|
||||
basicSnapshot.value = ''
|
||||
followupCapabilities.value = null
|
||||
formData.value.id = ''
|
||||
viewOnly.value = false
|
||||
mode.value = type
|
||||
visible.value = true
|
||||
@@ -1296,6 +1372,7 @@ const open = async (type: string, id?: number) => {
|
||||
|
||||
// 加载字典数据
|
||||
await getDictOptions()
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
|
||||
if (type === 'edit' && id) {
|
||||
await loadDiagnosisDetailIntoForm(id)
|
||||
@@ -1310,6 +1387,11 @@ const open = async (type: string, id?: number) => {
|
||||
/** 只读打开:与编辑同一套界面与全部 Tab,不可保存 */
|
||||
const openViewOnly = async (id: number) => {
|
||||
if (!id) return
|
||||
const generation = ++diagnosisGeneration
|
||||
submitting.value = false
|
||||
basicSnapshot.value = ''
|
||||
followupCapabilities.value = null
|
||||
formData.value.id = ''
|
||||
viewOnly.value = true
|
||||
mode.value = 'edit'
|
||||
visible.value = true
|
||||
@@ -1318,13 +1400,17 @@ const openViewOnly = async (id: number) => {
|
||||
idCardRevealUnlocked.value = false
|
||||
resetPatientBasicLockState()
|
||||
await getDictOptions()
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
await loadDiagnosisDetailIntoForm(id)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (viewOnly.value) return
|
||||
if (viewOnly.value || submitting.value) return
|
||||
const generation = diagnosisGeneration
|
||||
await formRef.value?.validate()
|
||||
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
// A user can keep typing while the request is pending. Only mark submitted bytes saved.
|
||||
const submittedForm = JSON.parse(basicFingerprint()) as typeof formData.value
|
||||
submitting.value = true
|
||||
try {
|
||||
// 准备提交数据,使用原始完整数据
|
||||
@@ -1336,6 +1422,7 @@ const handleSubmit = async () => {
|
||||
|
||||
if (mode.value === 'add') {
|
||||
const result = await tcmDiagnosisAdd(submitData)
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
|
||||
// 如果是新增,保存成功后切换到编辑模式,这样可以添加血糖血压记录
|
||||
if (result && result.id) {
|
||||
@@ -1343,11 +1430,14 @@ const handleSubmit = async () => {
|
||||
phoneRevealUnlocked.value = false
|
||||
idCardRevealUnlocked.value = false
|
||||
formData.value.id = result.id
|
||||
submittedForm.id = result.id
|
||||
|
||||
// 重新获取详情,确保patient_id等字段正确
|
||||
try {
|
||||
const detail = await tcmDiagnosisDetail({ id: result.id })
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
formData.value.patient_id = detail.patient_id
|
||||
submittedForm.patient_id = detail.patient_id
|
||||
|
||||
// 更新原始数据
|
||||
originalPhone.value = detail.phone || ''
|
||||
@@ -1367,11 +1457,13 @@ const handleSubmit = async () => {
|
||||
await tcmDiagnosisEdit(submitData)
|
||||
}
|
||||
|
||||
if (generation !== diagnosisGeneration || !visible.value) return
|
||||
basicSnapshot.value = JSON.stringify(submittedForm)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
if (generation === diagnosisGeneration) submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1395,7 +1487,12 @@ const handleOpenPrescription = () => {
|
||||
prescriptionRef.value?.open(data)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
const handleClose = async () => {
|
||||
if (followupAudioRef.value && !await followupAudioRef.value.confirmLeave()) return
|
||||
diagnosisGeneration++
|
||||
capabilitiesRequest?.abort()
|
||||
followupCapabilities.value = null
|
||||
basicSnapshot.value = ''
|
||||
formRef.value?.resetFields()
|
||||
formData.value = {
|
||||
id: '',
|
||||
|
||||
Reference in New Issue
Block a user