This commit is contained in:
Your Name
2026-05-28 14:41:50 +08:00
parent 33f8f669ad
commit 2b1ce61e72
4 changed files with 460 additions and 127 deletions
+154 -107
View File
@@ -222,17 +222,24 @@
<text class="elder-status" :class="todayChecked ? 'is-done' : 'is-pending'">
{{ todayChecked ? '今日血糖已记录' : '今日还没记血糖' }}
</text>
<view v-if="cards.length > 1" class="elder-card-row">
<view
v-for="card in cards"
:key="card.id"
class="elder-card-chip"
:class="{ active: Number(card.id) === Number(diagnosisId) }"
@click="onSelectCard(card)"
>
<text>{{ card.patient_name || '就诊卡' }}</text>
<scroll-view
v-if="cards.length > 1"
scroll-x
class="elder-card-scroll"
:show-scrollbar="false"
>
<view class="elder-card-row">
<view
v-for="card in cards"
:key="card.id"
class="elder-card-chip"
:class="{ active: Number(card.id) === Number(diagnosisId) }"
@click="onSelectCard(card)"
>
<text>{{ card.patient_name || '就诊卡' }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="elder-actions">
@@ -246,45 +253,12 @@
@click="ttsSpeaking ? ttsStop() : speakLatestSummary()"
>
<TongjiIcon :name="ttsSpeaking ? 'pause' : 'volume'" size="md" color="#204E2B" />
<text class="elder-btn-text">{{ ttsSpeaking ? '停止朗读' : '朗读最新血糖' }}</text>
<text class="elder-btn-text">{{ ttsSpeaking ? '停止朗读' : '朗读血糖' }}</text>
</view>
</view>
<view class="elder-today-panel">
<text class="elder-panel-title">今日血糖</text>
<view class="elder-today-grid">
<view class="elder-metric" :class="{ 'is-high': elderFasting.isHigh }">
<text class="elder-metric-label">{{ elderFasting.label }}</text>
<view class="elder-metric-value-row">
<text class="elder-metric-value">{{ elderFasting.value }}</text>
<text v-if="elderFasting.value !== '—'" class="elder-metric-unit">mmol/L</text>
</view>
<text v-if="elderFasting.isHigh" class="elder-metric-flag">偏高请遵医嘱</text>
</view>
<view class="elder-metric" :class="{ 'is-high': elderPostprandial.isHigh }">
<text class="elder-metric-label">{{ elderPostprandial.label }}</text>
<view class="elder-metric-value-row">
<text class="elder-metric-value">{{ elderPostprandial.value }}</text>
<text v-if="elderPostprandial.value !== '—'" class="elder-metric-unit">mmol/L</text>
</view>
<text v-if="elderPostprandial.isHigh" class="elder-metric-flag">偏高请遵医嘱</text>
</view>
</view>
</view>
<!-- 血糖趋势主页面-->
<view v-if="diagnosisId" class="elder-chart-block">
<view class="range-bar">
<view
v-for="opt in rangeOptions"
:key="opt.value"
class="range-item"
:class="{ active: rangeMode === opt.value }"
@click="onChangeRange(opt.value)"
>
<text>{{ opt.label }}</text>
</view>
</view>
<!-- 血糖趋势主页面-->
<view v-if="diagnosisId" class="elder-chart-block">
<view class="card chart-card">
<view class="card-head">
<view class="card-head-left">
@@ -292,11 +266,19 @@
<text class="card-subtitle">{{ rangeText }} · mmol/L</text>
</view>
<view class="card-legend">
<view class="legend-item">
<view
class="legend-item"
:class="{ inactive: !chartSeriesVisible.fasting }"
@click="toggleChartSeries('fasting')"
>
<view class="legend-dot legend-dot-fasting" />
<text>空腹</text>
</view>
<view class="legend-item">
<view
class="legend-item"
:class="{ inactive: !chartSeriesVisible.postprandial }"
@click="toggleChartSeries('postprandial')"
>
<view class="legend-dot legend-dot-postprandial" />
<text>餐后</text>
</view>
@@ -344,7 +326,41 @@
</cover-view>
</view>
</view>
<view class="range-bar">
<view
v-for="opt in rangeOptions"
:key="opt.value"
class="range-item"
:class="{ active: rangeMode === opt.value }"
@click="onChangeRange(opt.value)"
>
<text>{{ opt.label }}</text>
</view>
</view>
</view>
<view class="elder-today-panel">
<text class="elder-panel-title">今日血糖</text>
<view class="elder-today-grid">
<view class="elder-metric" :class="{ 'is-high': elderFasting.isHigh }">
<text class="elder-metric-label">{{ elderFasting.label }}</text>
<view class="elder-metric-value-row">
<text class="elder-metric-value">{{ elderFasting.value }}</text>
<text v-if="elderFasting.value !== '—'" class="elder-metric-unit">mmol/L</text>
</view>
<text v-if="elderFasting.isHigh" class="elder-metric-flag">偏高请遵医嘱</text>
</view>
<view class="elder-metric" :class="{ 'is-high': elderPostprandial.isHigh }">
<text class="elder-metric-label">{{ elderPostprandial.label }}</text>
<view class="elder-metric-value-row">
<text class="elder-metric-value">{{ elderPostprandial.value }}</text>
<text v-if="elderPostprandial.value !== '—'" class="elder-metric-unit">mmol/L</text>
</view>
<text v-if="elderPostprandial.isHigh" class="elder-metric-flag">偏高请遵医嘱</text>
</view>
</view>
</view>
<view class="elder-more-link" @click="goMorePage">
<view class="elder-more-body">
@@ -375,7 +391,7 @@
<view v-if="inputForm.loading" class="input-modal-loading">
<text>加载今日数据中</text>
</view>
<scroll-view v-else scroll-y class="input-modal-body">
<scroll-view v-else scroll-y class="input-modal-body input-modal-body-ready">
<view class="input-modal-tip">
<text>请如实填写留空表示该项不上传当天可多次修改</text>
</view>
@@ -565,6 +581,8 @@ const trackingNotes = ref([])
const hoverInfo = ref(null)
let hoverTimer = null
const chartSeriesVisible = ref({ fasting: true, postprandial: true })
// 画布上下文相关
let canvasNode = null
let canvasCtx = null
@@ -887,29 +905,13 @@ function elderMetricFromBlood(blood, field, label) {
const todayBlood = computed(() => bloodByDate.value[todayStr.value] || null)
const elderFasting = computed(() => {
const today = todayBlood.value
if (today && toNumber(today.fasting_blood_sugar) !== null) {
return elderMetricFromBlood(today, 'fasting_blood_sugar', '空腹')
}
const latest = latestStat.value.fasting
if (latest.value !== '—') {
return { label: '空腹(最近)', value: latest.value, isHigh: latest.isHigh }
}
return { label: '空腹', value: '—', isHigh: false }
})
const elderFasting = computed(() =>
elderMetricFromBlood(todayBlood.value, 'fasting_blood_sugar', '空腹')
)
const elderPostprandial = computed(() => {
const today = todayBlood.value
if (today && toNumber(today.postprandial_blood_sugar) !== null) {
return elderMetricFromBlood(today, 'postprandial_blood_sugar', '餐后')
}
const latest = latestStat.value.postprandial
if (latest.value !== '—') {
return { label: '餐后(最近)', value: latest.value, isHigh: latest.isHigh }
}
return { label: '餐后', value: '—', isHigh: false }
})
const elderPostprandial = computed(() =>
elderMetricFromBlood(todayBlood.value, 'postprandial_blood_sugar', '餐后')
)
// 给某天某字段计算 day-over-day 趋势(用于日常记录列表的指标小标签)
function getDayTrend(date, field, precision = 1) {
@@ -1800,8 +1802,8 @@ function buildSeries(field) {
})
}
function computeYRange(series1, series2) {
const values = [...series1, ...series2].filter((v) => v !== null)
function computeYRange(...seriesList) {
const values = seriesList.flat().filter((v) => v !== null)
if (!values.length) return { yMin: 4, yMax: 12 }
let min = Math.min(...values)
let max = Math.max(...values)
@@ -1835,9 +1837,33 @@ function drawChart() {
const fastingSeries = buildSeries('fasting_blood_sugar')
const postSeries = buildSeries('postprandial_blood_sugar')
const { yMin, yMax } = computeYRange(fastingSeries, postSeries)
const n = chartDates.value.length
const threshold = getBloodSugarThresholds(age.value)
const seriesDefs = [
{
key: 'fasting',
data: fastingSeries,
color: '#204E2B',
fillTop: 'rgba(32,78,43,0.18)',
fillBottom: 'rgba(32,78,43,0)',
label: '空腹',
highThreshold: threshold ? threshold.fasting : null
},
{
key: 'postprandial',
data: postSeries,
color: '#D97706',
fillTop: 'rgba(217,119,6,0.18)',
fillBottom: 'rgba(217,119,6,0)',
label: '餐后',
highThreshold: threshold ? threshold.postprandial : null
}
]
const visibleDefs = seriesDefs.filter((item) => chartSeriesVisible.value[item.key])
const rangeSources = visibleDefs.length
? visibleDefs.map((item) => item.data)
: [fastingSeries, postSeries]
const { yMin, yMax } = computeYRange(...rangeSources)
const valueToY = (v) => padding.top + ch * (1 - (v - yMin) / (yMax - yMin))
// 1) 背景:纯净浅色 + 阈值以上一抹极淡红
@@ -1915,37 +1941,20 @@ function drawChart() {
}
// 5) 绘制曲线
const seriesMeta = [
{
data: fastingSeries,
color: '#204E2B',
fillTop: 'rgba(32,78,43,0.18)',
fillBottom: 'rgba(32,78,43,0)',
label: '空腹',
highThreshold: threshold ? threshold.fasting : null
},
{
data: postSeries,
color: '#386641',
fillTop: 'rgba(56,102,65,0.14)',
fillBottom: 'rgba(56,102,65,0)',
label: '餐后',
highThreshold: threshold ? threshold.postprandial : null
}
]
const xAt = (i) => (n === 1 ? padding.left + cw / 2 : padding.left + (cw * i) / (n - 1))
const coordCache = []
const coordByKey = { fasting: [], postprandial: [] }
// 7 天内显示圆点,30 天以纯曲线为主
const showDots = n <= 10
seriesMeta.forEach((meta) => {
seriesDefs.forEach((meta) => {
const projected = meta.data.map((v, i) => ({
x: xAt(i),
y: v === null ? null : valueToY(v),
value: v
}))
coordCache.push(projected)
coordByKey[meta.key] = projected
if (!chartSeriesVisible.value[meta.key]) return
const segments = []
let cur = []
@@ -2066,11 +2075,18 @@ function drawChart() {
cw,
ch,
n,
fasting: coordCache[0] || [],
postprandial: coordCache[1] || []
fasting: coordByKey.fasting,
postprandial: coordByKey.postprandial
}
}
function toggleChartSeries(key) {
if (key !== 'fasting' && key !== 'postprandial') return
chartSeriesVisible.value[key] = !chartSeriesVisible.value[key]
hoverInfo.value = null
scheduleChartRedraw()
}
function onChartTouch(e) {
if (!chartCoords || !canvasWidth) return
if (!chartDates.value.length) return
@@ -2089,8 +2105,8 @@ function onChartTouch(e) {
if (idx < 0) idx = 0
if (idx > n - 1) idx = n - 1
const date = chartDates.value[idx]
const fp = chartCoords.fasting[idx]
const pp = chartCoords.postprandial[idx]
const fp = chartSeriesVisible.value.fasting ? chartCoords.fasting[idx] : null
const pp = chartSeriesVisible.value.postprandial ? chartCoords.postprandial[idx] : null
const anchorX = fp?.x ?? pp?.x ?? padding.left + cw * (idx / Math.max(1, n - 1))
const left = Math.max(8, Math.min(canvasWidth - 130, anchorX - 60))
@@ -2970,8 +2986,7 @@ async function onFamilyLike() {
.card {
background: #ffffff;
border-radius: var(--radius-card);
margin: 28rpx 24rpx 0;
padding: 30rpx 28rpx;
padding: 30rpx 15rpx;
box-shadow: var(--shadow-premium);
animation: card-rise 0.45s ease-out both;
}
@@ -3039,8 +3054,22 @@ async function onFamilyLike() {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 12rpx;
margin: -8rpx -12rpx;
font-size: 22rpx;
color: var(--slate-600);
border-radius: 999rpx;
transition: opacity 0.2s ease;
&.inactive {
opacity: 0.42;
.legend-dot-fasting,
.legend-dot-postprandial {
background: #cbd5e1;
}
}
&:active {
opacity: 0.72;
}
}
.legend-dot {
width: 16rpx;
@@ -4003,6 +4032,7 @@ async function onFamilyLike() {
display: flex;
align-items: flex-end;
justify-content: center;
animation: input-modal-mask-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.input-modal {
width: 100%;
@@ -4014,13 +4044,22 @@ async function onFamilyLike() {
max-height: 88vh;
display: flex;
flex-direction: column;
animation: input-modal-rise 0.28s cubic-bezier(0.16, 1, 0.3, 1);
animation: input-modal-rise 0.46s cubic-bezier(0.22, 1, 0.36, 1) both;
position: relative;
z-index: 1000;
will-change: transform, opacity;
}
@keyframes input-modal-mask-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes input-modal-rise {
from { transform: translateY(60rpx); opacity: 0.5; }
to { transform: translateY(0); opacity: 1; }
from { transform: translate3d(0, 100%, 0); opacity: 0.96; }
to { transform: translate3d(0, 0, 0); opacity: 1; }
}
@keyframes input-modal-content-in {
from { opacity: 0; transform: translate3d(0, 24rpx, 0); }
to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.input-modal-grip {
width: 80rpx;
@@ -4072,19 +4111,27 @@ async function onFamilyLike() {
font-weight: 700;
}
.input-modal-loading {
padding: 80rpx 0;
min-height: 520rpx;
padding: 120rpx 0;
text-align: center;
color: var(--slate-600);
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.input-modal-body {
flex: 1;
min-height: 520rpx;
padding: 20rpx 32rpx 28rpx;
overflow-y: auto;
overflow-x: hidden;
box-sizing: border-box;
width: 100%;
}
.input-modal-body-ready {
animation: input-modal-content-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.input-modal-tip {
background: var(--primary-light);
border: 1rpx dashed #99f6e4;