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
+1 -1
View File
@@ -1,5 +1,5 @@
import App from './App' import App from './App'
var baseUrl ='https://admin.zhenyangtang.com.cn/'; var baseUrl ='https://css.zhenyangtang.com.cn/';
// #ifndef VUE3 // #ifndef VUE3
import Vue from 'vue' import Vue from 'vue'
import './uni.promisify.adaptor' import './uni.promisify.adaptor'
+154 -107
View File
@@ -222,17 +222,24 @@
<text class="elder-status" :class="todayChecked ? 'is-done' : 'is-pending'"> <text class="elder-status" :class="todayChecked ? 'is-done' : 'is-pending'">
{{ todayChecked ? '今日血糖已记录' : '今日还没记血糖' }} {{ todayChecked ? '今日血糖已记录' : '今日还没记血糖' }}
</text> </text>
<view v-if="cards.length > 1" class="elder-card-row"> <scroll-view
<view v-if="cards.length > 1"
v-for="card in cards" scroll-x
:key="card.id" class="elder-card-scroll"
class="elder-card-chip" :show-scrollbar="false"
:class="{ active: Number(card.id) === Number(diagnosisId) }" >
@click="onSelectCard(card)" <view class="elder-card-row">
> <view
<text>{{ card.patient_name || '就诊卡' }}</text> 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>
</view> </scroll-view>
</view> </view>
<view class="elder-actions"> <view class="elder-actions">
@@ -246,45 +253,12 @@
@click="ttsSpeaking ? ttsStop() : speakLatestSummary()" @click="ttsSpeaking ? ttsStop() : speakLatestSummary()"
> >
<TongjiIcon :name="ttsSpeaking ? 'pause' : 'volume'" size="md" color="#204E2B" /> <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> </view>
<!-- 血糖趋势主页面-->
<view class="elder-today-panel"> <view v-if="diagnosisId" class="elder-chart-block">
<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 class="card chart-card"> <view class="card chart-card">
<view class="card-head"> <view class="card-head">
<view class="card-head-left"> <view class="card-head-left">
@@ -292,11 +266,19 @@
<text class="card-subtitle">{{ rangeText }} · mmol/L</text> <text class="card-subtitle">{{ rangeText }} · mmol/L</text>
</view> </view>
<view class="card-legend"> <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" /> <view class="legend-dot legend-dot-fasting" />
<text>空腹</text> <text>空腹</text>
</view> </view>
<view class="legend-item"> <view
class="legend-item"
:class="{ inactive: !chartSeriesVisible.postprandial }"
@click="toggleChartSeries('postprandial')"
>
<view class="legend-dot legend-dot-postprandial" /> <view class="legend-dot legend-dot-postprandial" />
<text>餐后</text> <text>餐后</text>
</view> </view>
@@ -344,7 +326,41 @@
</cover-view> </cover-view>
</view> </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>
<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-link" @click="goMorePage">
<view class="elder-more-body"> <view class="elder-more-body">
@@ -375,7 +391,7 @@
<view v-if="inputForm.loading" class="input-modal-loading"> <view v-if="inputForm.loading" class="input-modal-loading">
<text>加载今日数据中</text> <text>加载今日数据中</text>
</view> </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"> <view class="input-modal-tip">
<text>请如实填写留空表示该项不上传当天可多次修改</text> <text>请如实填写留空表示该项不上传当天可多次修改</text>
</view> </view>
@@ -565,6 +581,8 @@ const trackingNotes = ref([])
const hoverInfo = ref(null) const hoverInfo = ref(null)
let hoverTimer = null let hoverTimer = null
const chartSeriesVisible = ref({ fasting: true, postprandial: true })
// 画布上下文相关 // 画布上下文相关
let canvasNode = null let canvasNode = null
let canvasCtx = null let canvasCtx = null
@@ -887,29 +905,13 @@ function elderMetricFromBlood(blood, field, label) {
const todayBlood = computed(() => bloodByDate.value[todayStr.value] || null) const todayBlood = computed(() => bloodByDate.value[todayStr.value] || null)
const elderFasting = computed(() => { const elderFasting = computed(() =>
const today = todayBlood.value elderMetricFromBlood(todayBlood.value, 'fasting_blood_sugar', '空腹')
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 elderPostprandial = computed(() => { const elderPostprandial = computed(() =>
const today = todayBlood.value elderMetricFromBlood(todayBlood.value, 'postprandial_blood_sugar', '餐后')
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 }
})
// 给某天某字段计算 day-over-day 趋势(用于日常记录列表的指标小标签) // 给某天某字段计算 day-over-day 趋势(用于日常记录列表的指标小标签)
function getDayTrend(date, field, precision = 1) { function getDayTrend(date, field, precision = 1) {
@@ -1800,8 +1802,8 @@ function buildSeries(field) {
}) })
} }
function computeYRange(series1, series2) { function computeYRange(...seriesList) {
const values = [...series1, ...series2].filter((v) => v !== null) const values = seriesList.flat().filter((v) => v !== null)
if (!values.length) return { yMin: 4, yMax: 12 } if (!values.length) return { yMin: 4, yMax: 12 }
let min = Math.min(...values) let min = Math.min(...values)
let max = Math.max(...values) let max = Math.max(...values)
@@ -1835,9 +1837,33 @@ function drawChart() {
const fastingSeries = buildSeries('fasting_blood_sugar') const fastingSeries = buildSeries('fasting_blood_sugar')
const postSeries = buildSeries('postprandial_blood_sugar') const postSeries = buildSeries('postprandial_blood_sugar')
const { yMin, yMax } = computeYRange(fastingSeries, postSeries)
const n = chartDates.value.length const n = chartDates.value.length
const threshold = getBloodSugarThresholds(age.value) 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)) const valueToY = (v) => padding.top + ch * (1 - (v - yMin) / (yMax - yMin))
// 1) 背景:纯净浅色 + 阈值以上一抹极淡红 // 1) 背景:纯净浅色 + 阈值以上一抹极淡红
@@ -1915,37 +1941,20 @@ function drawChart() {
} }
// 5) 绘制曲线 // 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 xAt = (i) => (n === 1 ? padding.left + cw / 2 : padding.left + (cw * i) / (n - 1))
const coordCache = [] const coordByKey = { fasting: [], postprandial: [] }
// 7 天内显示圆点,30 天以纯曲线为主 // 7 天内显示圆点,30 天以纯曲线为主
const showDots = n <= 10 const showDots = n <= 10
seriesMeta.forEach((meta) => { seriesDefs.forEach((meta) => {
const projected = meta.data.map((v, i) => ({ const projected = meta.data.map((v, i) => ({
x: xAt(i), x: xAt(i),
y: v === null ? null : valueToY(v), y: v === null ? null : valueToY(v),
value: v value: v
})) }))
coordCache.push(projected) coordByKey[meta.key] = projected
if (!chartSeriesVisible.value[meta.key]) return
const segments = [] const segments = []
let cur = [] let cur = []
@@ -2066,11 +2075,18 @@ function drawChart() {
cw, cw,
ch, ch,
n, n,
fasting: coordCache[0] || [], fasting: coordByKey.fasting,
postprandial: coordCache[1] || [] 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) { function onChartTouch(e) {
if (!chartCoords || !canvasWidth) return if (!chartCoords || !canvasWidth) return
if (!chartDates.value.length) return if (!chartDates.value.length) return
@@ -2089,8 +2105,8 @@ function onChartTouch(e) {
if (idx < 0) idx = 0 if (idx < 0) idx = 0
if (idx > n - 1) idx = n - 1 if (idx > n - 1) idx = n - 1
const date = chartDates.value[idx] const date = chartDates.value[idx]
const fp = chartCoords.fasting[idx] const fp = chartSeriesVisible.value.fasting ? chartCoords.fasting[idx] : null
const pp = chartCoords.postprandial[idx] 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 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 left = Math.max(8, Math.min(canvasWidth - 130, anchorX - 60))
@@ -2970,8 +2986,7 @@ async function onFamilyLike() {
.card { .card {
background: #ffffff; background: #ffffff;
border-radius: var(--radius-card); border-radius: var(--radius-card);
margin: 28rpx 24rpx 0; padding: 30rpx 15rpx;
padding: 30rpx 28rpx;
box-shadow: var(--shadow-premium); box-shadow: var(--shadow-premium);
animation: card-rise 0.45s ease-out both; animation: card-rise 0.45s ease-out both;
} }
@@ -3039,8 +3054,22 @@ async function onFamilyLike() {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8rpx; gap: 8rpx;
padding: 8rpx 12rpx;
margin: -8rpx -12rpx;
font-size: 22rpx; font-size: 22rpx;
color: var(--slate-600); 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 { .legend-dot {
width: 16rpx; width: 16rpx;
@@ -4003,6 +4032,7 @@ async function onFamilyLike() {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
animation: input-modal-mask-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.input-modal { .input-modal {
width: 100%; width: 100%;
@@ -4014,13 +4044,22 @@ async function onFamilyLike() {
max-height: 88vh; max-height: 88vh;
display: flex; display: flex;
flex-direction: column; 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; position: relative;
z-index: 1000; z-index: 1000;
will-change: transform, opacity;
}
@keyframes input-modal-mask-in {
from { opacity: 0; }
to { opacity: 1; }
} }
@keyframes input-modal-rise { @keyframes input-modal-rise {
from { transform: translateY(60rpx); opacity: 0.5; } from { transform: translate3d(0, 100%, 0); opacity: 0.96; }
to { transform: translateY(0); opacity: 1; } 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 { .input-modal-grip {
width: 80rpx; width: 80rpx;
@@ -4072,19 +4111,27 @@ async function onFamilyLike() {
font-weight: 700; font-weight: 700;
} }
.input-modal-loading { .input-modal-loading {
padding: 80rpx 0; min-height: 520rpx;
padding: 120rpx 0;
text-align: center; text-align: center;
color: var(--slate-600); color: var(--slate-600);
font-size: 30rpx; font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
} }
.input-modal-body { .input-modal-body {
flex: 1; flex: 1;
min-height: 520rpx;
padding: 20rpx 32rpx 28rpx; padding: 20rpx 32rpx 28rpx;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
width: 100%; 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 { .input-modal-tip {
background: var(--primary-light); background: var(--primary-light);
border: 1rpx dashed #99f6e4; border: 1rpx dashed #99f6e4;
+255 -9
View File
@@ -82,8 +82,8 @@
</view> </view>
<view class="heatmap-foot"> <view class="heatmap-foot">
<text class="heatmap-foot-text">{{ todayChecked ? '今日已完成记录' : '今日还没有记录' }}</text> <text class="heatmap-foot-text">{{ todayChecked ? '今日已完成记录' : '今日还没有记录' }}</text>
<view v-if="!todayChecked" class="heatmap-foot-cta" @click="goGlucosePage()"> <view class="heatmap-foot-cta" @click="openInputForm()">
<text>去记血糖</text> <text>{{ todayChecked ? '编辑血糖' : '去记血糖' }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -173,7 +173,7 @@
<view <view
class="record-quick-chip" class="record-quick-chip"
:class="{ done: todayChecked, pending: !todayChecked }" :class="{ done: todayChecked, pending: !todayChecked }"
@click="goGlucosePage()" @click="openInputForm()"
> >
<view class="record-quick-icon-wrap"> <view class="record-quick-icon-wrap">
<TongjiIcon name="glucose" size="md" :color="todayChecked ? '#386641' : '#204E2B'" /> <TongjiIcon name="glucose" size="md" :color="todayChecked ? '#386641' : '#204E2B'" />
@@ -501,6 +501,104 @@
<text>数据每日同步 · 阈值仅作参考请遵医嘱</text> <text>数据每日同步 · 阈值仅作参考请遵医嘱</text>
</view> </view>
<!-- 录入今日血糖 -->
<view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
<view class="input-modal" @click.stop>
<view class="input-modal-grip" />
<view class="input-modal-head">
<view class="input-modal-title-wrap">
<text class="input-modal-title">{{ inputForm.id ? '修改今日血糖' : '录入今日血糖' }}</text>
<text class="input-modal-sub">{{ todayText }} · {{ patientName || '本人' }}</text>
</view>
<view class="input-modal-close" @click="closeInputForm">
<text class="input-modal-close-icon">×</text>
</view>
</view>
<view v-if="inputForm.loading" class="input-modal-loading">
<text>加载今日数据中</text>
</view>
<scroll-view v-else scroll-y class="input-modal-body input-modal-body-ready">
<view class="input-modal-tip">
<text>请如实填写留空表示该项不上传当天可多次修改</text>
</view>
<view class="input-section-title">血糖mmol/L</view>
<view class="input-field">
<view class="input-field-label">
<view class="input-field-dot input-field-dot-fasting" />
<text>空腹血糖</text>
</view>
<view class="input-field-input">
<input
type="digit"
:value="inputForm.fasting_blood_sugar"
placeholder="如 6.5"
placeholder-class="input-placeholder"
@input="onInputField('fasting_blood_sugar', $event)"
/>
</view>
</view>
<view class="input-field">
<view class="input-field-label">
<view class="input-field-dot input-field-dot-postprandial" />
<text>餐后血糖</text>
</view>
<view class="input-field-input">
<input
type="digit"
:value="inputForm.postprandial_blood_sugar"
placeholder="如 8.0"
placeholder-class="input-placeholder"
@input="onInputField('postprandial_blood_sugar', $event)"
/>
</view>
</view>
<view class="input-field">
<view class="input-field-label">
<view class="input-field-dot input-field-dot-other" />
<text>其他血糖</text>
</view>
<view class="input-field-input">
<input
type="digit"
:value="inputForm.other_blood_sugar"
placeholder="如 7.2"
placeholder-class="input-placeholder"
@input="onInputField('other_blood_sugar', $event)"
/>
</view>
</view>
<view class="input-section-title">备注</view>
<view class="input-field input-field-textarea">
<textarea
:value="inputForm.remark"
placeholder="如:饭前空腹测量、餐后2小时测量等(选填)"
placeholder-class="input-placeholder"
:show-confirm-bar="false"
:auto-height="true"
@input="onInputField('remark', $event)"
/>
</view>
</scroll-view>
<view class="input-modal-actions">
<view
v-if="inputForm.id"
class="input-modal-btn danger"
:class="{ disabled: inputForm.submitting }"
@click="deleteInputRecord"
>
<text>删除</text>
</view>
<view
class="input-modal-btn primary"
:class="{ disabled: inputForm.submitting || inputForm.loading }"
@click="submitInputForm"
>
<text>{{ inputForm.submitting ? '提交中…' : (inputForm.id ? '保存修改' : '提交录入') }}</text>
</view>
</view>
</view>
</view>
<!-- 录入血压弹层 --> <!-- 录入血压弹层 -->
<view v-if="bpForm.visible" class="input-modal-mask" @click="closeBpForm"> <view v-if="bpForm.visible" class="input-modal-mask" @click="closeBpForm">
<view class="input-modal" @click.stop> <view class="input-modal" @click.stop>
@@ -1576,6 +1674,18 @@ const todayText = computed(() => {
return `${d.getFullYear()}-${m}-${day} ${weekdays[d.getDay()]}` return `${d.getFullYear()}-${m}-${day} ${weekdays[d.getDay()]}`
}) })
const emptyInputForm = () => ({
visible: false,
loading: false,
submitting: false,
id: 0,
fasting_blood_sugar: '',
postprandial_blood_sugar: '',
other_blood_sugar: '',
remark: ''
})
const inputForm = ref(emptyInputForm())
const emptyBpForm = () => ({ const emptyBpForm = () => ({
visible: false, visible: false,
loading: false, loading: false,
@@ -1593,6 +1703,124 @@ function prepareRecordOverlay() {
ttsStop() ttsStop()
} }
function onInputField(field, e) {
inputForm.value[field] = e?.detail?.value ?? ''
}
async function openInputForm() {
if (!diagnosisId.value) {
showUserToast('请先选择就诊卡')
return
}
const token = uni.getStorageSync('token')
if (!token) {
showUserToast('请先登录后再录入')
return
}
prepareRecordOverlay()
inputForm.value = { ...emptyInputForm(), visible: true, loading: true }
try {
const res = await proxy.apiUrl({
url: '/api/tcm/dailyTodayBloodRecord',
method: 'GET',
data: { diagnosis_id: diagnosisId.value }
}, false)
if (res && res.code === 1 && res.data && res.data.record) {
const r = res.data.record
inputForm.value.id = Number(r.id) || 0
inputForm.value.fasting_blood_sugar = numToStr(r.fasting_blood_sugar)
inputForm.value.postprandial_blood_sugar = numToStr(r.postprandial_blood_sugar)
inputForm.value.other_blood_sugar = numToStr(r.other_blood_sugar)
inputForm.value.remark = String(r.remark || '')
}
} catch (e) {
/* 静默:拉取失败时仍允许新增录入 */
} finally {
inputForm.value.loading = false
}
}
function closeInputForm() {
if (inputForm.value.submitting) return
inputForm.value.visible = false
}
async function submitInputForm() {
if (inputForm.value.submitting || inputForm.value.loading) return
const f = inputForm.value
const hasAny = [
f.fasting_blood_sugar, f.postprandial_blood_sugar, f.other_blood_sugar
].some((v) => v !== '' && v !== null && Number(v) > 0) || !!(f.remark && f.remark.trim())
if (!hasAny) {
showUserToast('请至少填写一项数据')
return
}
inputForm.value.submitting = true
try {
const res = await proxy.apiUrl({
url: '/api/tcm/dailySaveBloodRecord',
method: 'POST',
data: {
diagnosis_id: diagnosisId.value,
fasting_blood_sugar: f.fasting_blood_sugar,
postprandial_blood_sugar: f.postprandial_blood_sugar,
other_blood_sugar: f.other_blood_sugar,
remark: f.remark
}
}, false)
if (res && res.code === 1) {
showUserToast(res.msg || '已保存', { icon: 'success' })
inputForm.value.visible = false
await fetchAll()
await fetchGamifyState()
if (claimablePoints.value > 0) {
showUserToast('血糖已记录,点击浇水领取积分', { duration: 2800 })
}
} else {
showUserToast((res && res.msg) || '保存失败')
}
} catch (e) {
showUserToast('网络异常,请稍后再试')
} finally {
inputForm.value.submitting = false
}
}
function deleteInputRecord() {
if (!inputForm.value.id || inputForm.value.submitting) return
uni.showModal({
title: '删除确认',
content: '确定删除今天已录入的血糖数据吗?删除后无法恢复。',
confirmColor: '#dc2626',
success: async (m) => {
if (!m.confirm) return
inputForm.value.submitting = true
try {
const res = await proxy.apiUrl({
url: '/api/tcm/dailyDeleteBloodRecord',
method: 'POST',
data: {
diagnosis_id: diagnosisId.value,
id: inputForm.value.id
}
}, false)
if (res && res.code === 1) {
uni.showToast({ title: '已删除', icon: 'success' })
inputForm.value.visible = false
await fetchAll()
await fetchGamifyState()
} else {
showUserToast((res && res.msg) || '删除失败')
}
} catch (e) {
uni.showToast({ title: '网络异常,请稍后再试', icon: 'none' })
} finally {
inputForm.value.submitting = false
}
}
})
}
function onBpField(field, e) { function onBpField(field, e) {
const val = e?.detail?.value ?? '' const val = e?.detail?.value ?? ''
bpForm.value[field] = val bpForm.value[field] = val
@@ -1760,7 +1988,7 @@ const exerciseForm = ref(emptyExerciseForm())
/** 任一录入弹层打开时需卸载 canvas(小程序原生组件会盖住普通 view)*/ /** 任一录入弹层打开时需卸载 canvas(小程序原生组件会盖住普通 view)*/
const recordOverlayOpen = computed(() => const recordOverlayOpen = computed(() =>
bpForm.value.visible || dietForm.value.visible || exerciseForm.value.visible inputForm.value.visible || bpForm.value.visible || dietForm.value.visible || exerciseForm.value.visible
) )
function onExerciseField(field, e) { function onExerciseField(field, e) {
@@ -3082,7 +3310,7 @@ function onGamifyTaskTap(task) {
} }
if (!task.completed) { if (!task.completed) {
if (task.id === 'blood') { if (task.id === 'blood') {
goGlucosePage() openInputForm()
return return
} }
if (task.id === 'diet') { if (task.id === 'diet') {
@@ -4843,6 +5071,7 @@ function onFamilyLikeStripTap() {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
animation: input-modal-mask-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.input-modal { .input-modal {
width: 100%; width: 100%;
@@ -4854,13 +5083,22 @@ function onFamilyLikeStripTap() {
max-height: 88vh; max-height: 88vh;
display: flex; display: flex;
flex-direction: column; 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; position: relative;
z-index: 1000; z-index: 1000;
will-change: transform, opacity;
}
@keyframes input-modal-mask-in {
from { opacity: 0; }
to { opacity: 1; }
} }
@keyframes input-modal-rise { @keyframes input-modal-rise {
from { transform: translateY(60rpx); opacity: 0.5; } from { transform: translate3d(0, 100%, 0); opacity: 0.96; }
to { transform: translateY(0); opacity: 1; } 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 { .input-modal-grip {
width: 80rpx; width: 80rpx;
@@ -4912,19 +5150,27 @@ function onFamilyLikeStripTap() {
font-weight: 700; font-weight: 700;
} }
.input-modal-loading { .input-modal-loading {
padding: 80rpx 0; min-height: 520rpx;
padding: 120rpx 0;
text-align: center; text-align: center;
color: var(--slate-600); color: var(--slate-600);
font-size: 30rpx; font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
} }
.input-modal-body { .input-modal-body {
flex: 1; flex: 1;
min-height: 520rpx;
padding: 20rpx 32rpx 28rpx; padding: 20rpx 32rpx 28rpx;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
width: 100%; 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 { .input-modal-tip {
background: var(--primary-light); background: var(--primary-light);
border: 1rpx dashed #99f6e4; border: 1rpx dashed #99f6e4;
+50 -10
View File
@@ -44,6 +44,12 @@
--warning: #d97706;
--warning-light: #fff7ed;
--text-primary: #1b1c1a; --text-primary: #1b1c1a;
--text-secondary: #414941; --text-secondary: #414941;
@@ -260,7 +266,7 @@
.daily-page .legend-dot-postprandial { .daily-page .legend-dot-postprandial {
background: var(--primary-container); background: var(--warning);
} }
@@ -556,15 +562,27 @@
.daily-page .elder-card-scroll {
margin-top: 24rpx;
width: 100%;
white-space: nowrap;
}
.daily-page .elder-card-row { .daily-page .elder-card-row {
display: flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 16rpx; gap: 16rpx;
margin-top: 24rpx; padding: 2rpx 0;
} }
@@ -572,6 +590,8 @@
.daily-page .elder-card-chip { .daily-page .elder-card-chip {
flex-shrink: 0;
padding: 12rpx 28rpx; padding: 12rpx 28rpx;
border-radius: 999rpx; border-radius: 999rpx;
@@ -620,9 +640,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 20rpx; align-items: stretch;
gap: 16rpx;
} }
@@ -636,14 +658,18 @@
justify-content: center; justify-content: center;
gap: 16rpx; gap: 12rpx;
min-height: 112rpx; min-height: 112rpx;
min-width: 0;
border-radius: 24rpx; border-radius: 24rpx;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
box-sizing: border-box;
} }
@@ -660,6 +686,8 @@
.daily-page .elder-btn-primary { .daily-page .elder-btn-primary {
flex: 8;
background: var(--primary); background: var(--primary);
@@ -676,6 +704,14 @@
.daily-page .elder-btn-voice { .daily-page .elder-btn-voice {
flex: 2;
flex-direction: column;
gap: 8rpx;
padding: 12rpx 8rpx;
background: var(--surface); background: var(--surface);
border: 2rpx solid var(--border-soft); border: 2rpx solid var(--border-soft);
@@ -686,6 +722,12 @@
color: var(--primary-dark); color: var(--primary-dark);
font-size: 24rpx;
line-height: 1.25;
text-align: center;
} }
@@ -936,9 +978,7 @@
.daily-page .elder-chart-block {
margin: 0 32rpx 24rpx;
}
.daily-page .elder-chart-block .range-bar { .daily-page .elder-chart-block .range-bar {
margin-bottom: 16rpx; margin-bottom: 16rpx;