This commit is contained in:
Your Name
2026-06-02 18:33:25 +08:00
parent 62f74888f3
commit 15e9c486f9
11 changed files with 731 additions and 674 deletions
+105 -140
View File
@@ -45,20 +45,19 @@
</view>
</scroll-view>
<view class="st-main">
<!-- 1. 录入今日血糖 -->
<!-- 2. 血糖趋势 -->
<view v-if="diagnosisId" class="st-chart-card">
<!-- 2. 血糖趋势无就诊卡或录入不足 7 天显示示例曲线 -->
<view class="st-chart-card">
<view class="st-chart-head">
<view>
<text class="st-chart-title">血糖趋势</text>
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
<view class="st-chart-more" @click="goRecordsPage">
<text class="st-chart-more-text">查看更多记录</text>
<TongjiIcon name="chevron-right" size="sm" color="#006c49" />
</view>
</view>
<view class="st-chart-legend">
<view
@@ -79,97 +78,39 @@
</view>
</view>
</view>
<view v-if="bloodHasData" class="st-chart-summary">
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-high">{{ bloodHighDayCount }}</text>
<text class="st-chart-summary-label">天偏高</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-good">{{ bloodNormalDayCount }}</text>
<text class="st-chart-summary-label">天正常</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num">{{ bloodRecordDayCount }}</text>
<text class="st-chart-summary-label">天有记录</text>
</view>
</view>
<view class="st-chart-area">
<view class="st-chart-area" @click="onChartTap">
<canvas
v-if="!recordOverlayOpen"
id="bloodWaveChart"
type="2d"
class="st-chart-canvas"
@touchstart="onChartTouch"
@touchmove="onChartTouch"
@click.stop="onChartTap"
/>
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
<text>图表已暂停显示</text>
</view>
<view v-if="!bloodHasData && !loading && !recordOverlayOpen" class="st-chart-empty">
<view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
<text>当前时间范围暂无血糖数据</text>
</view>
<cover-view v-if="hoverInfo && !recordOverlayOpen" class="chart-tip" :style="{ left: hoverInfo.left + 'px', top: hoverInfo.top + 'px' }">
<cover-view class="chart-tip-date">{{ hoverInfo.date }}</cover-view>
<cover-view v-if="hoverInfo.fasting != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-fasting" />
<cover-view>空腹 {{ hoverInfo.fasting }}</cover-view>
</cover-view>
<cover-view v-if="hoverInfo.postprandial != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-post" />
<cover-view>餐后 {{ hoverInfo.postprandial }}</cover-view>
</cover-view>
</cover-view>
</view>
<view class="st-cta" @click="openInputForm()">
<TongjiIcon name="plus" size="md" color="#FFFFFF" />
<text>录入今日血糖</text>
</view>
<view class="st-game-entry" @click="goGamePage">
<view class="st-game-entry-icon">
<TongjiIcon name="activity" size="md" color="#006c49" />
<view v-if="chartUseMockData" class="st-chart-foot">
<text class="st-chart-mock-hint">示例数据连续记录满 7 天后显示您的真实趋势</text>
</view>
<view class="st-game-entry-body">
<text class="st-game-entry-title">糖分突袭</text>
<text class="st-game-entry-sub">消除棋盘食物边玩边学控糖</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view>
</view>
<!-- 3. 最新测量 & 概览 -->
<view class="st-latest-section">
<view
v-if="latestMeasurement.hasValue"
class="st-latest-hero"
:class="{ 'is-high': latestMeasurement.isHigh }"
>
<view class="st-latest-hero-pattern" aria-hidden="true" />
<text class="st-latest-label">最新测量 · {{ latestMeasurement.typeLabel }}</text>
<view class="st-latest-value-row">
<text class="st-latest-value">{{ latestMeasurement.value }}</text>
<text class="st-latest-unit">mmol/L</text>
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage">
<view class="st-game-entry-icon">
<TongjiIcon name="activity" size="sm" color="#006c49" />
</view>
<view v-if="latestMeasurement.isHigh" class="st-latest-flag">
<TongjiIcon name="alert-triangle" size="sm" color="#93000a" />
<text>偏高请遵医嘱</text>
</view>
</view>
<view class="st-summary-grid">
<view class="st-summary-card">
<text class="st-summary-label">空腹</text>
<text class="st-summary-value" :class="{ 'is-high': elderFasting.isHigh }">{{ elderFasting.value }}</text>
<text v-if="elderFasting.isHigh" class="st-summary-tag">偏高</text>
</view>
<view class="st-summary-card">
<text class="st-summary-label">餐后</text>
<text class="st-summary-value" :class="{ 'is-high': elderPostprandial.isHigh }">{{ elderPostprandial.value }}</text>
<text v-if="elderPostprandial.isHigh" class="st-summary-tag">偏高</text>
<view class="st-game-entry-body">
<text class="st-game-entry-title">识糖小课堂</text>
<text class="st-game-entry-sub">认一认哪些食物含糖高</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view>
</view>
<!-- 4. AI 饮食建议 -->
<view class="st-training-module">
<dev-training-entry inline />
</view>
<!-- 3. AI 饮食建议 -->
<view v-if="diagnosisId" class="st-ai-section">
<view class="st-ai-deco" aria-hidden="true" />
<view class="st-ai-head">
@@ -227,7 +168,7 @@
<view class="st-main-spacer" />
</view>
<!-- 收起后的重新唤起按钮 -->
<view v-if="diagnosisId && floatAskClosed" class="st-float-reopen" @click="floatAskClosed = false">
<TongjiIcon name="sparkles" size="sm" color="#ffffff" />
@@ -458,6 +399,7 @@ import TongjiIcon from '../components/TongjiIcon.vue'
import { useDietAi } from '../composables/useDietAi.js'
import { useSpeechToText } from '../composables/useSpeechToText.js'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
const { proxy } = getCurrentInstance()
@@ -859,6 +801,7 @@ const bloodHasData = computed(() => {
})
})
/** 当前时间范围内有真实录入的天数 */
const bloodRecordDayCount = computed(() => {
return chartDates.value.filter((date) => {
const b = bloodByDate.value[date]
@@ -866,6 +809,50 @@ const bloodRecordDayCount = computed(() => {
}).length
})
const REAL_BLOOD_DAYS_MIN = 7
/** 无就诊卡或真实录入不足 7 天时,图表用示例数据 */
const chartUseMockData = computed(() => {
if (!diagnosisId.value) return true
return bloodRecordDayCount.value < REAL_BLOOD_DAYS_MIN
})
/** 示例曲线:7 天内有波动的空腹/餐后,便于老人理解趋势图形态 */
const mockBloodByDate = computed(() => {
const map = {}
const dates = chartDates.value
dates.forEach((date, i) => {
const t = dates.length > 1 ? i / (dates.length - 1) : 0
const fasting = Number((5.2 + Math.sin(t * Math.PI) * 0.5 + (i % 2) * 0.15).toFixed(1))
const post = Number((7.6 + Math.cos(t * Math.PI) * 1.1 + (i % 3) * 0.25).toFixed(1))
const row = {
record_date: date,
fasting_blood_sugar: fasting,
postprandial_blood_sugar: post,
other_blood_sugar: '',
systolic_pressure: '',
diastolic_pressure: ''
}
row.isHighFasting = isHighFasting(row.fasting_blood_sugar)
row.isHighPostprandial = isHighPostprandial(row.postprandial_blood_sugar)
row.isHighOther = false
row.isHighBp = false
row.bpText = '—'
map[date] = row
})
return map
})
/** 图表绘制用数据源:示例或真实 */
const chartBloodByDate = computed(() => (
chartUseMockData.value ? mockBloodByDate.value : bloodByDate.value
))
const chartHasDisplayData = computed(() => {
if (chartUseMockData.value) return chartDates.value.length > 0
return bloodHasData.value
})
const bloodHighDayCount = computed(() => {
return chartDates.value.filter((date) => {
const b = bloodByDate.value[date]
@@ -1377,16 +1364,26 @@ function onRefresh() {
fetchAll()
}
async function goRecordsPage() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
function buildMorePageUrl() {
if (!diagnosisId.value) return '/tongji/pages/more'
const q = [
`diagnosis_id=${diagnosisId.value}`,
patientId.value ? `patient_id=${patientId.value}` : ''
].filter(Boolean).join('&')
uni.navigateTo({ url: `/tongji/pages/more?${q}` })
return `/tongji/pages/more?${q}`
}
/** 点击血糖趋势图:进入记录页 more(无就诊卡时先手机号快捷建档) */
async function onChartTap() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: buildMorePageUrl() })
}
async function goRecordsPage() {
onChartTap()
}
async function goMorePage() {
@@ -1394,7 +1391,7 @@ async function goMorePage() {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: '/tongji/pages/more' })
uni.navigateTo({ url: buildMorePageUrl() })
}
function goGamePage() {
@@ -2102,7 +2099,7 @@ function scheduleChartRedraw() {
function buildSeries(field) {
return chartDates.value.map((date) => {
const b = bloodByDate.value[date]
const b = chartBloodByDate.value[date]
return b ? toNumber(b[field]) : null
})
}
@@ -2136,7 +2133,7 @@ function drawChart() {
ctx.clearRect(0, 0, w, h)
const padding = { left: 40, right: 18, top: 26, bottom: 36 }
const padding = { left: 40, right: 18, top: 12, bottom: 28 }
const cw = w - padding.left - padding.right
const ch = h - padding.top - padding.bottom
@@ -2392,59 +2389,25 @@ function toggleChartSeries(key) {
scheduleChartRedraw()
}
function onChartTouch(e) {
if (!chartCoords || !canvasWidth) return
if (!chartDates.value.length) return
let touchX = 0
let touchY = 0
if (e && e.touches && e.touches[0]) {
touchX = e.touches[0].x ?? e.touches[0].clientX ?? 0
touchY = e.touches[0].y ?? e.touches[0].clientY ?? 0
}
const { padding, cw, n } = chartCoords
if (touchX < padding.left || touchX > padding.left + cw) {
return
}
const ratio = n <= 1 ? 0 : (touchX - padding.left) / cw
let idx = Math.round(ratio * (n - 1))
if (idx < 0) idx = 0
if (idx > n - 1) idx = n - 1
const date = chartDates.value[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))
const top = Math.max(4, touchY - 70)
hoverInfo.value = {
date: date.slice(5),
fasting: fp?.value ?? null,
postprandial: pp?.value ?? null,
left,
top
}
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = setTimeout(() => {
hoverInfo.value = null
}, 2500)
}
watch(rangeMode, () => {
scheduleChartRedraw()
})
watch([chartUseMockData, bloodRecords], () => {
scheduleChartRedraw()
})
// ============ 生命周期 ============
function readUserContext() {
try {
const userData = uni.getStorageSync('userData')
if (userData && userData.diagnosis) {
if (!patientId.value) patientId.value = Number(userData.diagnosis.patient_id) || 0
if (!diagnosisId.value) diagnosisId.value = Number(userData.diagnosis.id) || 0
if (!patientName.value) patientName.value = userData.diagnosis.patient_name || ''
if (!age.value) age.value = Number(userData.diagnosis.age) || 0
if (!gender.value) gender.value = Number(userData.diagnosis.gender) || 0
}
if (!userData) return
// 勿用问诊会话里的 diagnosis 作为日常血糖诊单(无 view_records 会报无权)
const name = String(userData.real_name || userData.nickname || '').trim()
if (!patientName.value && name) patientName.value = name
const sex = Number(userData.sex)
if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
if (!age.value && userData.age) age.value = Number(userData.age) || 0
} catch (e) {}
}
@@ -2527,14 +2490,12 @@ async function bootstrap(options) {
if (!target) target = list[0]
await applyCard(target)
} else {
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
if (passedPatientId) patientId.value = passedPatientId
diagnosisId.value = passedDiagnosisId || 0
patientId.value = passedPatientId || 0
if (passedPatientName) patientName.value = passedPatientName
if (passedAge) age.value = passedAge
if (passedGender) gender.value = passedGender
if (!diagnosisId.value || !patientId.value) {
readUserContext()
}
if (!patientName.value) readUserContext()
readUserMobile()
if (!diagnosisId.value && userMobile.value) {
await ensureDailyContextByPhone()
@@ -2544,6 +2505,10 @@ async function bootstrap(options) {
authChecking.value = false
if (diagnosisId.value) {
await fetchAll()
} else {
bloodRecords.value = []
await nextTick()
scheduleChartRedraw()
}
}