1
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="tracking-matrix">
|
||||
<div class="toolbar">
|
||||
<el-radio-group v-model="windowDays" @change="onWindowChange">
|
||||
<el-radio-button :value="7">最近 7 天</el-radio-button>
|
||||
<el-radio-button :value="30">最近 30 天</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span class="hint">同一天血糖三段挤一格;超阈值红字↑(年龄分段:<50:空7/餐9,≥50:空8/餐10)</span>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="visibleDates.length === 0"
|
||||
description="近期无任何打卡记录"
|
||||
:image-size="80"
|
||||
/>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="rows"
|
||||
border
|
||||
stripe
|
||||
class="matrix-table"
|
||||
empty-text="暂无数据"
|
||||
row-key="date"
|
||||
max-height="640"
|
||||
>
|
||||
<el-table-column prop="dateLabel" label="日期" width="120" fixed="left" />
|
||||
<el-table-column label="血糖 (mmol/L)" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.bloodAny" class="cell-blood">
|
||||
<span :class="{ 'cell-high': row.fastingHigh }">
|
||||
空
|
||||
<span class="value">{{ row.fasting ?? '—' }}</span>
|
||||
<span v-if="row.fastingHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span :class="{ 'cell-high': row.postprandialHigh }">
|
||||
餐
|
||||
<span class="value">{{ row.postprandial ?? '—' }}</span>
|
||||
<span v-if="row.postprandialHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span :class="{ 'cell-high': row.otherHigh }">
|
||||
其他
|
||||
<span class="value">{{ row.other ?? '—' }}</span>
|
||||
<span v-if="row.otherHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
</div>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="血压 (mmHg)" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.bp" :class="{ 'cell-high': row.bpHigh }">
|
||||
{{ row.bp }}
|
||||
<span v-if="row.bpHigh" class="cell-up">↑</span>
|
||||
</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="西药" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.westernMedicine">{{ row.westernMedicine }}</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="胰岛素" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.insulin">{{ row.insulin }}</span>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="饮食打卡" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="row.diet"
|
||||
placement="top"
|
||||
:disabled="!row.diet"
|
||||
effect="light"
|
||||
>
|
||||
<template #content>
|
||||
<div class="diet-tip">
|
||||
<div>早:{{ row.diet.breakfast || '—' }}</div>
|
||||
<div>午:{{ row.diet.lunch || '—' }}</div>
|
||||
<div>晚:{{ row.diet.dinner || '—' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="cell-checked">已打卡</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运动打卡" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.exercise" placement="top" effect="light">
|
||||
<template #content>
|
||||
<div class="exercise-tip">
|
||||
<div>强度:{{ row.exercise.intensityText || '—' }}</div>
|
||||
<div>时长:{{ row.exercise.duration ? row.exercise.duration + ' min' : '—' }}</div>
|
||||
<div v-if="row.exercise.remark">备注:{{ row.exercise.remark }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="cell-checked">
|
||||
已打卡
|
||||
<span v-if="row.exercise.duration" class="meta">· {{ row.exercise.duration }}min</span>
|
||||
<span v-if="row.exercise.intensityText" class="meta">· {{ row.exercise.intensityText }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span v-else class="cell-empty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
isHighFastingBloodSugar,
|
||||
isHighPostprandialBloodSugar,
|
||||
isHighOtherBloodSugar,
|
||||
isHighBloodPressure,
|
||||
formatBp
|
||||
} from '@/utils/blood-thresholds'
|
||||
|
||||
interface BloodRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
fasting_blood_sugar?: any
|
||||
postprandial_blood_sugar?: any
|
||||
other_blood_sugar?: any
|
||||
systolic_pressure?: any
|
||||
diastolic_pressure?: any
|
||||
western_medicine?: string
|
||||
insulin?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface DietRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
breakfast?: string
|
||||
lunch?: string
|
||||
dinner?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface ExerciseRecord {
|
||||
record_date?: string
|
||||
record_date_ts?: number
|
||||
duration?: number
|
||||
intensity?: number
|
||||
intensity_text?: string
|
||||
remark?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props {
|
||||
diagnosisId?: number
|
||||
patientId?: number | null
|
||||
age?: number | null
|
||||
bloodRecords: BloodRecord[]
|
||||
dietRecords: DietRecord[]
|
||||
exerciseRecords: ExerciseRecord[]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
diagnosisId: 0,
|
||||
patientId: null,
|
||||
age: null,
|
||||
bloodRecords: () => [],
|
||||
dietRecords: () => [],
|
||||
exerciseRecords: () => []
|
||||
})
|
||||
|
||||
const windowDays = ref<7 | 30>(30)
|
||||
|
||||
const onWindowChange = () => {
|
||||
// 触发 computed 重算;预留扩展点:未来可在此处通知父组件重拉
|
||||
}
|
||||
|
||||
/**
|
||||
* 把同一天可能多条的血糖记录聚合:每段取第一条非空值(与 BloodRecordLogic 趋势图一致)
|
||||
*/
|
||||
const bloodByDate = computed(() => {
|
||||
const map = new Map<string, BloodRecord>()
|
||||
for (const r of props.bloodRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) {
|
||||
map.set(d, {
|
||||
record_date: d,
|
||||
record_date_ts: r.record_date_ts
|
||||
})
|
||||
}
|
||||
const acc = map.get(d)!
|
||||
const fields: Array<keyof BloodRecord> = [
|
||||
'fasting_blood_sugar',
|
||||
'postprandial_blood_sugar',
|
||||
'other_blood_sugar',
|
||||
'systolic_pressure',
|
||||
'diastolic_pressure',
|
||||
'western_medicine',
|
||||
'insulin'
|
||||
]
|
||||
for (const f of fields) {
|
||||
if ((acc[f] === undefined || acc[f] === null || acc[f] === '') && r[f] !== undefined && r[f] !== null && r[f] !== '') {
|
||||
;(acc as any)[f] = r[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const dietByDate = computed(() => {
|
||||
const map = new Map<string, DietRecord>()
|
||||
for (const r of props.dietRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
const exerciseByDate = computed(() => {
|
||||
const map = new Map<string, ExerciseRecord>()
|
||||
for (const r of props.exerciseRecords || []) {
|
||||
const d = String(r.record_date || '')
|
||||
if (!d) continue
|
||||
if (!map.has(d)) map.set(d, r)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
/**
|
||||
* 取三类记录的日期并集,按 desc 排序并去重,限制最大 windowDays 条
|
||||
*/
|
||||
const visibleDates = computed<string[]>(() => {
|
||||
const set = new Set<string>()
|
||||
for (const r of props.bloodRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
for (const r of props.dietRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
for (const r of props.exerciseRecords || []) if (r.record_date) set.add(String(r.record_date))
|
||||
const list = Array.from(set).filter((d) => d.length === 10)
|
||||
list.sort((a, b) => (a < b ? 1 : a > b ? -1 : 0))
|
||||
return list.slice(0, windowDays.value)
|
||||
})
|
||||
|
||||
const weekText = (d: string) => {
|
||||
const ts = Date.parse(d + 'T00:00:00')
|
||||
if (!Number.isFinite(ts)) return ''
|
||||
const w = new Date(ts).getDay()
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][w]
|
||||
}
|
||||
|
||||
const rows = computed(() =>
|
||||
visibleDates.value.map((date) => {
|
||||
const blood = bloodByDate.value.get(date)
|
||||
const diet = dietByDate.value.get(date)
|
||||
const exercise = exerciseByDate.value.get(date)
|
||||
|
||||
const fasting = blood?.fasting_blood_sugar ?? null
|
||||
const postprandial = blood?.postprandial_blood_sugar ?? null
|
||||
const other = blood?.other_blood_sugar ?? null
|
||||
const sys = blood?.systolic_pressure ?? null
|
||||
const dia = blood?.diastolic_pressure ?? null
|
||||
|
||||
const bloodAny =
|
||||
fasting !== null && fasting !== '' ||
|
||||
postprandial !== null && postprandial !== '' ||
|
||||
other !== null && other !== ''
|
||||
|
||||
const bp = sys || dia ? formatBp({ systolic_pressure: sys, diastolic_pressure: dia }) : ''
|
||||
|
||||
return {
|
||||
date,
|
||||
dateLabel: `${date.slice(5)}(周${weekText(date)})`,
|
||||
// 血糖
|
||||
bloodAny,
|
||||
fasting: fasting ?? '—',
|
||||
postprandial: postprandial ?? '—',
|
||||
other: other ?? '—',
|
||||
fastingHigh: isHighFastingBloodSugar(fasting, props.age),
|
||||
postprandialHigh: isHighPostprandialBloodSugar(postprandial, props.age),
|
||||
otherHigh: isHighOtherBloodSugar(other, props.age),
|
||||
// 血压
|
||||
bp,
|
||||
bpHigh: isHighBloodPressure({ systolic_pressure: sys, diastolic_pressure: dia }),
|
||||
// 用药
|
||||
westernMedicine: blood?.western_medicine || '',
|
||||
insulin: blood?.insulin || '',
|
||||
// 饮食
|
||||
diet: diet
|
||||
? {
|
||||
breakfast: diet.breakfast || '',
|
||||
lunch: diet.lunch || '',
|
||||
dinner: diet.dinner || ''
|
||||
}
|
||||
: null,
|
||||
// 运动
|
||||
exercise: exercise
|
||||
? {
|
||||
duration: exercise.duration || 0,
|
||||
intensityText: exercise.intensity_text || '',
|
||||
remark: exercise.remark || ''
|
||||
}
|
||||
: null
|
||||
}
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tracking-matrix {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.matrix-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cell-blood {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
|
||||
> span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cell-high {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cell-up {
|
||||
font-size: 13px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.cell-empty {
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
|
||||
.cell-checked {
|
||||
color: #2563eb;
|
||||
font-weight: 500;
|
||||
|
||||
.meta {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 400;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diet-tip,
|
||||
.exercise-tip {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
|
||||
> div {
|
||||
margin-bottom: 2px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user