370 lines
11 KiB
Vue
370 lines
11 KiB
Vue
<template>
|
||
<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>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, h } from 'vue'
|
||
import {
|
||
isHighSystolicPressure,
|
||
isHighDiastolicPressure,
|
||
isHighFastingBloodSugar
|
||
} from '@/utils/blood-thresholds'
|
||
import { maskPhone, joinArray, makeTextOf } from '@/utils/diag-display'
|
||
import { formatDiabetesDiscoveryDisplay } from '@/utils/diabetes-discovery-display'
|
||
|
||
interface Props {
|
||
apt?: Record<string, any> | null
|
||
diag?: Record<string, any> | null
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
apt: () => ({}),
|
||
diag: () => ({})
|
||
})
|
||
|
||
const caseTypeLabel = computed(() => {
|
||
const d: any = props.diag || {}
|
||
return d.consultation_type === 'follow_up' ? '复诊' : '复诊'
|
||
})
|
||
|
||
const textOf = makeTextOf(() => props.diag || {})
|
||
|
||
// ========== 本地函数式子组件 ==========
|
||
|
||
/**
|
||
* 局部渲染组件:左侧 label + 右侧 value,支持 span / 换行(与抽离前 reception 内联实现一致)
|
||
*/
|
||
const KVItem = (kvProps: { label: string; value: any; span?: string | number; multiline?: boolean }) => {
|
||
const val = kvProps.value
|
||
const empty =
|
||
val === undefined || val === null || val === '' || (Array.isArray(val) && val.length === 0)
|
||
return h(
|
||
'div',
|
||
{
|
||
class: ['kv-item', kvProps.multiline ? 'multiline' : '', empty ? 'empty' : ''],
|
||
style: kvProps.span ? `grid-column: span ${kvProps.span}` : ''
|
||
},
|
||
[
|
||
h('span', { class: 'kv-label' }, kvProps.label),
|
||
h('span', { class: 'kv-value' }, empty ? '—' : String(val))
|
||
]
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 数值型 KV:偏高时红色 + ↑ 箭头(与抽离前 reception 内联实现一致)
|
||
*/
|
||
const MetricKVItem = (mkvProps: {
|
||
label: string
|
||
value: any
|
||
unit?: string
|
||
span?: string | number
|
||
high?: boolean
|
||
}) => {
|
||
const empty = mkvProps.value === undefined || mkvProps.value === null || mkvProps.value === ''
|
||
return h(
|
||
'div',
|
||
{
|
||
class: ['kv-item', 'metric-kv-item', empty ? 'empty' : ''],
|
||
style: mkvProps.span ? `grid-column: span ${mkvProps.span}` : ''
|
||
},
|
||
[
|
||
h('span', { class: 'kv-label' }, mkvProps.label),
|
||
h(
|
||
'span',
|
||
{ class: ['kv-value', 'metric-kv-value', mkvProps.high ? 'high' : ''] },
|
||
empty
|
||
? '—'
|
||
: [
|
||
`${mkvProps.value}${mkvProps.unit || ''}`,
|
||
mkvProps.high ? h('span', { class: 'metric-up' }, '↑') : null
|
||
]
|
||
)
|
||
]
|
||
)
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.card {
|
||
background: var(--rx-surface);
|
||
border: 1px solid var(--rx-line);
|
||
border-radius: 14px;
|
||
padding: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
box-shadow: var(--rx-shadow-sm);
|
||
transition: box-shadow 0.2s ease;
|
||
|
||
&:hover {
|
||
box-shadow: var(--rx-shadow);
|
||
}
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--rx-ink);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding-left: 10px;
|
||
position: relative;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 3px;
|
||
height: 16px;
|
||
border-radius: 2px;
|
||
background: linear-gradient(180deg, #4f8cff 0%, #2563eb 100%);
|
||
}
|
||
}
|
||
|
||
.case-sub {
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: var(--rx-soft);
|
||
font-family: var(--rx-font-data);
|
||
}
|
||
|
||
.grid-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed var(--rx-line);
|
||
|
||
&:first-of-type {
|
||
border-top: none;
|
||
padding-top: 0;
|
||
}
|
||
}
|
||
|
||
.grid-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--rx-accent-strong);
|
||
letter-spacing: 0.02em;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 4px;
|
||
height: 4px;
|
||
border-radius: 50%;
|
||
background: var(--rx-accent);
|
||
}
|
||
}
|
||
|
||
.kv-grid {
|
||
display: grid;
|
||
gap: 8px 16px;
|
||
|
||
&.one {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
&.two {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
&.three {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
&.four {
|
||
grid-template-columns: repeat(4, 1fr);
|
||
}
|
||
}
|
||
|
||
:deep(.kv-item) {
|
||
display: flex;
|
||
gap: 6px;
|
||
font-size: 12.5px;
|
||
line-height: 1.55;
|
||
min-width: 0;
|
||
|
||
&.multiline {
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.kv-label {
|
||
color: var(--rx-soft);
|
||
flex-shrink: 0;
|
||
min-width: 58px;
|
||
}
|
||
|
||
.kv-value {
|
||
color: var(--rx-ink);
|
||
word-break: break-word;
|
||
flex: 1;
|
||
font-weight: 500;
|
||
}
|
||
|
||
&.empty .kv-value {
|
||
color: #c0c4cc;
|
||
font-weight: 400;
|
||
}
|
||
|
||
&.multiline .kv-value {
|
||
white-space: pre-wrap;
|
||
font-weight: 400;
|
||
}
|
||
}
|
||
|
||
:deep(.metric-kv-item .metric-kv-value) {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
&.high {
|
||
color: #dc2626;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
:deep(.metric-up) {
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
color: #dc2626;
|
||
}
|
||
|
||
.rx-note {
|
||
font-size: 13px;
|
||
color: var(--rx-muted);
|
||
line-height: 1.6;
|
||
padding: 12px 14px;
|
||
background: linear-gradient(135deg, #fafbfd 0%, #f5f7fb 100%);
|
||
border-radius: 8px;
|
||
border-left: 3px solid var(--rx-accent);
|
||
}
|
||
</style>
|