This commit is contained in:
2026-05-05 15:02:04 +08:00
parent 0da27ed745
commit 19bfeef98f
19 changed files with 1771 additions and 272 deletions
+14 -262
View File
@@ -111,126 +111,10 @@
<template v-else-if="detail">
<!-- 患者信息 -->
<div class="card patient-card">
<div class="card-title">患者信息</div>
<div class="patient-hero">
<div class="patient-name">{{ apt.patient_name || '—' }}</div>
<div class="patient-meta">
<div>{{ maskPhone(apt.patient_phone) }} · {{ formatGender(diag.gender) }} · {{ diag.age != null ? diag.age + '岁' : '—' }}</div>
<div>{{ diag.height ? diag.height + 'cm' : '—' }} / {{ diag.weight ? diag.weight + 'kg' : '—' }} · {{ diag.region || '—' }}</div>
<div>预约{{ apt.appointment_date }} {{ apt.appointment_time }} · {{ formatPeriod(apt.period) }}</div>
<div>医生{{ apt.doctor_name || '—' }} 医助{{ apt.assistant_name || '—' }}</div>
<div>状态{{ apt.status_desc || '—' }} · {{ apt.has_prescription ? '已开方' : '未开方' }}</div>
<div v-if="apt.remark">备注{{ apt.remark }}</div>
</div>
</div>
</div>
<PatientInfoCard :apt="apt" :diag="diag" />
<!-- 患者病例完整字段与 tcm/diagnosis/edit.vue 保持同步只读展示 -->
<div class="card case-card">
<div class="card-title">
患者病例
<span class="case-sub">{{ caseTypeLabel }} · 诊断日期 {{ diag.diagnosis_date || apt.appointment_date || '—' }}</span>
</div>
<!-- 基本信息 -->
<div class="grid-block">
<div class="grid-title">基本信息</div>
<div class="kv-grid four">
<KVItem label="诊单ID" :value="diag.patient_id" />
<KVItem label="姓名" :value="diag.patient_name" />
<KVItem label="身份证号" :value="diag.id_card" />
<KVItem label="手机号" :value="maskPhone(diag.phone || apt.patient_phone)" />
<KVItem label="性别" :value="textOf('gender')" />
<KVItem label="年龄" :value="diag.age != null ? diag.age + '岁' : ''" />
<KVItem label="婚姻状态" :value="textOf('marital_status')" />
<KVItem label="地区" :value="diag.region" />
</div>
</div>
<!-- 生命体征 -->
<div class="grid-block">
<div class="grid-title">生命体征</div>
<div class="kv-grid four">
<KVItem label="身高" :value="diag.height ? diag.height + ' cm' : ''" />
<KVItem label="体重" :value="diag.weight ? diag.weight + ' kg' : ''" />
<MetricKVItem label="高压" :value="diag.systolic_pressure" unit=" mmHg" :high="isHighSystolicPressure(diag.systolic_pressure)" />
<MetricKVItem label="低压" :value="diag.diastolic_pressure" unit=" mmHg" :high="isHighDiastolicPressure(diag.diastolic_pressure)" />
<KVItem label="诊断类型" :value="textOf('diagnosis_type')" />
<MetricKVItem label="空腹血糖" :value="diag.fasting_blood_sugar" unit=" mmol/L" :high="isHighFastingBloodSugar(diag.fasting_blood_sugar, diag.age)" />
<KVItem label="在用药物" :value="diag.current_medications" multiline />
</div>
</div>
<!-- 主诉 -->
<div class="grid-block">
<div class="grid-title">主诉</div>
<div class="kv-grid four">
<KVItem label="当地医院诊断日期" :value="diag.diagnosis_date" />
<KVItem label="发现糖尿病病史" :value="formatDiabetesDiscoveryDisplay ? formatDiabetesDiscoveryDisplay(diag.diabetes_discovery_year) : diag.diabetes_discovery_year" />
<KVItem label="当地就诊医院" :value="diag.local_hospital_name" />
<KVItem label="当地医院诊断结果" :value="joinArray(diag.local_hospital_diagnosis)" />
</div>
</div>
<!-- 现病史后端已翻译 *_text -->
<div class="grid-block">
<div class="grid-title">现病史</div>
<div class="kv-grid three">
<KVItem label="口腔感觉" :value="textOf('appetite')" />
<KVItem label="每日饮水量" :value="textOf('water_intake')" />
<KVItem label="近月体重变化" :value="textOf('weight_change')" />
<KVItem label="脂肪肝程度" :value="textOf('fatty_liver_degree')" />
<KVItem label="饮食情况" :value="textOf('diet_condition')" span="2" />
<KVItem label="肢体感觉" :value="textOf('body_feeling')" />
<KVItem label="睡眠情况" :value="textOf('sleep_condition')" />
<KVItem label="眼睛情况" :value="textOf('eye_condition')" />
<KVItem label="头部感觉" :value="textOf('head_feeling')" />
<KVItem label="出汗情况" :value="textOf('sweat_condition')" />
<KVItem label="皮肤情况" :value="textOf('skin_condition')" />
<KVItem label="小便情况" :value="textOf('urine_condition')" />
<KVItem label="大便情况" :value="textOf('stool_condition')" />
<KVItem label="腰肾情况" :value="textOf('kidney_condition')" />
<KVItem label="其他补充" :value="diag.symptoms" span="3" multiline />
</div>
</div>
<!-- 既往史 -->
<div class="grid-block">
<div class="grid-title">既往史</div>
<div class="kv-grid three">
<KVItem label="既往病史" :value="textOf('past_history')" span="3" />
</div>
</div>
<!-- 其他病史 -->
<div class="grid-block">
<div class="grid-title">其他病史</div>
<div class="kv-grid three">
<KVItem label="外伤史" :value="textOf('trauma_history')" />
<KVItem label="手术史" :value="textOf('surgery_history')" />
<KVItem label="过敏史" :value="textOf('allergy_history')" />
<KVItem label="家族病史" :value="textOf('family_history')" />
<KVItem label="妊娠哺乳史" :value="textOf('pregnancy_history')" />
</div>
</div>
<!-- 病史补充 -->
<div class="grid-block" v-if="diag.remark">
<div class="grid-title">病史补充</div>
<div class="kv-grid one">
<KVItem label="" :value="diag.remark" multiline />
</div>
</div>
<!-- 处方意见 -->
<div class="grid-block">
<div class="grid-title">处方意见</div>
<div class="rx-note">
{{ apt.has_prescription ? '已开方。' : '当前未开方。' }}
</div>
</div>
</div>
<!-- 患者病例 tcm/diagnosis/readonly 共用同一组件确保两处字段一致 -->
<PatientCaseCard :apt="apt" :diag="diag" />
<!-- 医生备注 & 舌苔照片 -->
<div class="card note-card">
@@ -353,13 +237,23 @@
</template>
<script setup lang="ts">
import { defineAsyncComponent, onMounted, onUnmounted, h } from 'vue'
import { defineAsyncComponent, onMounted, onUnmounted } from 'vue'
import { receptionQueue, receptionDetail, notifyAssistant, addDoctorNote } from '@/api/patient'
import NoteTimeline from './components/NoteTimeline.vue'
import PatientInfoCard from '@/views/tcm/diagnosis/components/PatientInfoCard.vue'
import PatientCaseCard from '@/views/tcm/diagnosis/components/PatientCaseCard.vue'
import { completeAppointment } from '@/api/doctor'
import { getCallSignature } from '@/api/tcm'
import feedback from '@/utils/feedback'
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
import { formatGender } from '@/utils/diag-display'
import {
isHighFastingBloodSugar,
isHighPostprandialBloodSugar,
isHighOtherBloodSugar,
isHighBloodPressure,
formatBp
} from '@/utils/blood-thresholds'
import {
Phone,
Search,
@@ -370,55 +264,6 @@ import {
const ChatDialog = defineAsyncComponent(() => import('@/components/chat-dialog/index.vue'))
const DiagnosisEdit = defineAsyncComponent(() => import('@/views/tcm/diagnosis/edit.vue'))
/**
* 局部渲染组件:左侧 label + 右侧 value,支持 span / 换行
*/
const KVItem = (props: { label: string; value: any; span?: string | number; multiline?: boolean }) => {
const val = props.value
const empty = val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0)
return h(
'div',
{
class: ['kv-item', props.multiline ? 'multiline' : '', empty ? 'empty' : ''],
style: props.span ? `grid-column: span ${props.span}` : ''
},
[
h('span', { class: 'kv-label' }, props.label),
h('span', { class: 'kv-value' }, empty ? '—' : String(val))
]
)
}
const MetricKVItem = (props: {
label: string
value: any
unit?: string
span?: string | number
high?: boolean
}) => {
const empty = props.value === undefined || props.value === null || props.value === ''
return h(
'div',
{
class: ['kv-item', 'metric-kv-item', empty ? 'empty' : ''],
style: props.span ? `grid-column: span ${props.span}` : ''
},
[
h('span', { class: 'kv-label' }, props.label),
h(
'span',
{ class: ['kv-value', 'metric-kv-value', props.high ? 'high' : ''] },
empty
? '—'
: [
`${props.value}${props.unit || ''}`,
props.high ? h('span', { class: 'metric-up' }, '↑') : null
]
)
]
)
}
const POLL_MS = 5_000
const PAGE_SIZE = 15
@@ -489,38 +334,12 @@ const todayMeta = computed(() => {
})
const caseTypeLabel = computed(() => {
if (!diag.value) return '初诊'
return diag.value.consultation_type === 'follow_up' ? '复诊' : '初诊'
})
/**
* 诊单字典字段由后端 AppointmentLogic::enrichDiagnosisLabels 预翻译,
* 前端直接读 `<field>_text`;找不到时回退原值。
*/
const textOf = (field: string): string => {
const d: any = diag.value || {}
const t = d[field + '_text']
if (t !== undefined && t !== null && t !== '') return String(t)
const raw = d[field]
if (raw === undefined || raw === null || raw === '') return ''
return Array.isArray(raw) ? raw.join('、') : String(raw)
}
const todayStr = () => {
const d = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
const maskPhone = (phone?: string) => {
if (!phone) return '—'
if (phone.length < 11) return phone
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
const formatGender = (g?: number) => (g === 1 ? '男' : g === 0 ? '女' : '—')
const formatGenderAge = (row: QueueRow) => {
const parts = [formatGender(row.gender), row.age != null ? row.age + '岁' : '']
return parts.filter((p) => p && p !== '—').join(' ')
@@ -531,73 +350,6 @@ const formatTimeHM = (time?: string) => {
return time.replace(/^(\d{2}:\d{2})(:\d{2})?$/, '$1')
}
const formatPeriod = (period?: string) => {
if (period === 'morning') return '上午'
if (period === 'afternoon') return '下午'
return '全天'
}
const formatBp = (row: any) => {
const sys = row?.systolic_pressure
const dia = row?.diastolic_pressure
if (sys == null && dia == null) return '—'
return `${sys ?? '—'} / ${dia ?? '—'}`
}
const toNumeric = (value: any) => {
if (value === null || value === undefined || value === '') return null
const num = Number(value)
return Number.isFinite(num) ? num : null
}
const getBloodSugarThresholds = (ageValue: any) => {
const age = toNumeric(ageValue)
if (age === null) return null
if (age < 50) return { fasting: 7, postprandial: 9 }
if (age >= 50) return { fasting: 8, postprandial: 10 }
return null
}
const isHighFastingBloodSugar = (value: any, ageValue: any) => {
const num = toNumeric(value)
const thresholds = getBloodSugarThresholds(ageValue)
return num !== null && thresholds !== null && num >= thresholds.fasting
}
const isHighPostprandialBloodSugar = (value: any, ageValue: any) => {
const num = toNumeric(value)
const thresholds = getBloodSugarThresholds(ageValue)
return num !== null && thresholds !== null && num >= thresholds.postprandial
}
const isHighOtherBloodSugar = (value: any, ageValue: any) => {
const num = toNumeric(value)
const thresholds = getBloodSugarThresholds(ageValue)
return num !== null && thresholds !== null && num >= thresholds.postprandial
}
const isHighBloodPressure = (row: any) => {
const sys = toNumeric(row?.systolic_pressure)
const dia = toNumeric(row?.diastolic_pressure)
return (sys !== null && sys > 140) || (dia !== null && dia > 90)
}
const isHighSystolicPressure = (value: any) => {
const num = toNumeric(value)
return num !== null && num > 140
}
const isHighDiastolicPressure = (value: any) => {
const num = toNumeric(value)
return num !== null && num > 90
}
const joinArray = (arr: any): string => {
if (!arr) return ''
if (Array.isArray(arr)) return arr.filter(Boolean).join('、')
return String(arr)
}
const fetchQueuePage = async (pageNo: number, status?: number, patientName?: string) => {
const today = todayStr()
const res: any = await receptionQueue({