Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df65344d8f |
@@ -56,27 +56,55 @@
|
|||||||
<!-- 2. 血糖趋势(无就诊卡或累计录入不足 7 天显示示例曲线) -->
|
<!-- 2. 血糖趋势(无就诊卡或累计录入不足 7 天显示示例曲线) -->
|
||||||
<view class="st-chart-card">
|
<view class="st-chart-card">
|
||||||
<view class="st-chart-head">
|
<view class="st-chart-head">
|
||||||
<view style="padding: 0 20rpx;">
|
<view class="st-chart-head-text">
|
||||||
<text class="st-chart-title">血糖趋势</text>
|
<text class="st-chart-title">血糖趋势</text>
|
||||||
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
|
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
<view class="st-chart-legend">
|
<view class="st-chart-toolbar">
|
||||||
<view
|
<view class="st-chart-legend">
|
||||||
class="st-legend-item"
|
<view
|
||||||
:class="{ inactive: !chartSeriesVisible.fasting }"
|
class="st-legend-item"
|
||||||
@click="toggleChartSeries('fasting')"
|
:class="{ inactive: !chartSeriesVisible.fasting }"
|
||||||
>
|
@click="toggleChartSeries('fasting')"
|
||||||
<view class="st-dot st-dot-fasting" />
|
>
|
||||||
<text>空腹</text>
|
<view class="st-dot st-dot-fasting" />
|
||||||
|
<text>空腹</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="st-legend-item"
|
||||||
|
:class="{ inactive: !chartSeriesVisible.postprandial }"
|
||||||
|
@click="toggleChartSeries('postprandial')"
|
||||||
|
>
|
||||||
|
<view class="st-dot st-dot-post" />
|
||||||
|
<text>餐后</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="st-legend-item"
|
v-if="bloodCareEligible"
|
||||||
:class="{ inactive: !chartSeriesVisible.postprandial }"
|
class="st-chart-ai-chip"
|
||||||
@click="toggleChartSeries('postprandial')"
|
:class="{
|
||||||
|
active: bloodCarePanelOpen,
|
||||||
|
loading: bloodCareLoading,
|
||||||
|
hint: !bloodCarePanelOpen && !bloodCareLoading
|
||||||
|
}"
|
||||||
|
role="button"
|
||||||
|
aria-label="AI 照护建议"
|
||||||
|
@click.stop="toggleBloodCareFromChart"
|
||||||
>
|
>
|
||||||
<view class="st-dot st-dot-post" />
|
<view class="st-chart-ai-chip-inner">
|
||||||
<text>餐后</text>
|
<view class="st-chart-ai-chip-icon">
|
||||||
|
<TongjiIcon
|
||||||
|
name="sparkles"
|
||||||
|
size="sm"
|
||||||
|
:color="bloodCarePanelOpen ? '#ffffff' : '#006c49'"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<text class="st-chart-ai-chip-label">{{
|
||||||
|
bloodCareLoading
|
||||||
|
? '生成中'
|
||||||
|
: (bloodCarePanelOpen ? '收起建议' : '照护建议')
|
||||||
|
}}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -98,6 +126,88 @@
|
|||||||
<view v-if="chartUseMockData" class="st-chart-foot">
|
<view v-if="chartUseMockData" class="st-chart-foot">
|
||||||
<text class="st-chart-mock-hint">示例数据,累计记录满 7 天后显示您的真实趋势</text>
|
<text class="st-chart-mock-hint">示例数据,累计记录满 7 天后显示您的真实趋势</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 趋势卡内:点击标题栏 AI 按钮后展开 -->
|
||||||
|
<view v-if="bloodCareEligible && bloodCarePanelOpen" class="st-chart-care-panel">
|
||||||
|
<view class="st-chart-care-head">
|
||||||
|
<text class="st-chart-care-title">AI 照护建议</text>
|
||||||
|
<view class="st-chart-care-actions">
|
||||||
|
<view
|
||||||
|
class="st-chart-care-refresh"
|
||||||
|
:class="{ disabled: bloodCareLoading }"
|
||||||
|
@click.stop="refreshBloodCareAi"
|
||||||
|
>
|
||||||
|
<TongjiIcon name="refresh" size="sm" color="#006c49" />
|
||||||
|
<text>{{ bloodCareLoading ? '生成中' : '换一换' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="st-chart-care-close" @click.stop="closeBloodCarePanel">
|
||||||
|
<text>收起</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="st-chart-care-sub">基于您累计 7 天血糖 · 监测与中医调理方向(非处方)</text>
|
||||||
|
|
||||||
|
<view v-if="bloodCareControlLabel" class="st-care-badge st-care-badge--in-chart">
|
||||||
|
<text>{{ bloodCareControlLabel }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="bloodCareLoading && !hasBloodCareContent" class="st-skeleton st-skeleton--in-chart">
|
||||||
|
<view class="st-skeleton-line" />
|
||||||
|
<view class="st-skeleton-line short" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="hasBloodCareContent" class="st-care-cards st-care-cards--in-chart">
|
||||||
|
<view class="st-meal-glass st-care-block">
|
||||||
|
<view class="st-meal-head">
|
||||||
|
<text class="st-meal-label">总评</text>
|
||||||
|
</view>
|
||||||
|
<text class="st-meal-text">
|
||||||
|
{{ bloodCarePlan.summary || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'summary'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="st-meal-glass st-care-block">
|
||||||
|
<view class="st-meal-head">
|
||||||
|
<text class="st-meal-label">血糖管理</text>
|
||||||
|
</view>
|
||||||
|
<text class="st-meal-text">
|
||||||
|
{{ bloodCarePlan.blood_advice || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'blood_advice'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="st-meal-glass st-care-block st-care-block--tcm">
|
||||||
|
<view class="st-meal-head">
|
||||||
|
<text class="st-meal-label">中医调理</text>
|
||||||
|
</view>
|
||||||
|
<text class="st-meal-text">
|
||||||
|
{{ bloodCarePlan.tcm_plan || '…' }}<text v-if="bloodCareLoading && bloodCareStreamField === 'tcm_plan'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="bloodCarePlan.watch_points && bloodCarePlan.watch_points.length" class="st-care-watch">
|
||||||
|
<text class="st-care-watch-title">留意</text>
|
||||||
|
<view v-for="(item, wi) in bloodCarePlan.watch_points" :key="wi" class="st-care-watch-item">
|
||||||
|
<text class="st-care-watch-dot">·</text>
|
||||||
|
<text class="st-care-watch-text">{{ item }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="bloodCarePlan.next_steps" class="st-meal-glass st-care-block st-care-block--next">
|
||||||
|
<view class="st-meal-head">
|
||||||
|
<text class="st-meal-label">复诊提醒</text>
|
||||||
|
</view>
|
||||||
|
<text class="st-meal-text">
|
||||||
|
{{ bloodCarePlan.next_steps }}<text v-if="bloodCareLoading && bloodCareStreamField === 'next_steps'" class="ai-stream-cursor">▍</text>
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="bloodCareAnalysis.length" class="st-care-stats st-care-stats--in-chart">
|
||||||
|
<view v-for="(s, si) in bloodCareAnalysis" :key="si" class="st-care-stat-chip">
|
||||||
|
<text class="st-care-stat-label">{{ s.label }}</text>
|
||||||
|
<text class="st-care-stat-val">记录 {{ s.record_days }} 天 · 达标 {{ s.compliance }}%</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text v-if="bloodCareDisclaimer" class="st-care-disclaimer st-care-disclaimer--in-chart">{{ bloodCareDisclaimer }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
|
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
|
||||||
<view class="st-game-entry-icon">
|
<view class="st-game-entry-icon">
|
||||||
<TongjiIcon name="activity" size="sm" color="#006c49" />
|
<TongjiIcon name="activity" size="sm" color="#006c49" />
|
||||||
@@ -109,6 +219,7 @@
|
|||||||
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="st-training-section">
|
<view class="st-training-section">
|
||||||
<dev-training-entry inline tone="weekly" />
|
<dev-training-entry inline tone="weekly" />
|
||||||
</view>
|
</view>
|
||||||
@@ -414,6 +525,7 @@ import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage
|
|||||||
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
||||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||||
import { useDietAi } from '../composables/useDietAi.js'
|
import { useDietAi } from '../composables/useDietAi.js'
|
||||||
|
import { useBloodCareAi } from '../composables/useBloodCareAi.js'
|
||||||
import { useSpeechToText } from '../composables/useSpeechToText.js'
|
import { useSpeechToText } from '../composables/useSpeechToText.js'
|
||||||
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
|
||||||
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
|
||||||
@@ -924,6 +1036,27 @@ const chartHasDisplayData = computed(() => {
|
|||||||
return bloodHasData.value
|
return bloodHasData.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
bloodCarePlan,
|
||||||
|
bloodCareLoading,
|
||||||
|
bloodCareStreamField,
|
||||||
|
bloodCareEligible,
|
||||||
|
bloodCarePanelOpen,
|
||||||
|
hasBloodCareContent,
|
||||||
|
bloodCareAnalysis,
|
||||||
|
bloodCareControlLabel,
|
||||||
|
bloodCareDisclaimer,
|
||||||
|
toggleBloodCareFromChart,
|
||||||
|
closeBloodCarePanel,
|
||||||
|
refreshBloodCareAi,
|
||||||
|
resetBloodCarePlan
|
||||||
|
} = useBloodCareAi(proxy, {
|
||||||
|
diagnosisId,
|
||||||
|
chartUseMockData,
|
||||||
|
showUserToast,
|
||||||
|
formatUserMessage
|
||||||
|
})
|
||||||
|
|
||||||
const bloodHighDayCount = computed(() => {
|
const bloodHighDayCount = computed(() => {
|
||||||
return chartDates.value.filter((date) => {
|
return chartDates.value.filter((date) => {
|
||||||
const b = bloodByDate.value[date]
|
const b = bloodByDate.value[date]
|
||||||
@@ -1380,6 +1513,7 @@ async function onSelectCard(card) {
|
|||||||
exerciseRecords.value = []
|
exerciseRecords.value = []
|
||||||
trackingNotes.value = []
|
trackingNotes.value = []
|
||||||
hoverInfo.value = null
|
hoverInfo.value = null
|
||||||
|
resetBloodCarePlan()
|
||||||
await applyCard(card)
|
await applyCard(card)
|
||||||
fetchAll()
|
fetchAll()
|
||||||
}
|
}
|
||||||
@@ -1436,6 +1570,9 @@ async function fetchAll() {
|
|||||||
trackingNotes.value = []
|
trackingNotes.value = []
|
||||||
}
|
}
|
||||||
await fetchDietAiRecommend(false)
|
await fetchDietAiRecommend(false)
|
||||||
|
if (!bloodCareEligible.value) {
|
||||||
|
resetBloodCarePlan()
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' })
|
uni.showToast({ title: '加载失败,请稍后再试', icon: 'none' })
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -244,11 +244,110 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-head {
|
.weekly-page .st-chart-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-head-text {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图例 + AI 胶囊同一行,避免大块图标压过标题 */
|
||||||
|
.weekly-page .st-chart-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 12rpx;
|
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
min-height: 64rpx;
|
||||||
|
padding: 10rpx 22rpx 10rpx 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.96) 0%, rgba(236, 253, 245, 0.92) 100%);
|
||||||
|
border: 1rpx solid rgba(0, 108, 73, 0.14);
|
||||||
|
box-shadow:
|
||||||
|
0 2rpx 6rpx rgba(15, 23, 42, 0.04),
|
||||||
|
inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
|
||||||
|
transition:
|
||||||
|
background 0.22s ease,
|
||||||
|
border-color 0.22s ease,
|
||||||
|
box-shadow 0.22s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip-icon {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 108, 73, 0.08);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f3d2e;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip.hint .st-chart-ai-chip-inner {
|
||||||
|
border-color: rgba(0, 108, 73, 0.28);
|
||||||
|
box-shadow:
|
||||||
|
0 2rpx 8rpx rgba(0, 108, 73, 0.1),
|
||||||
|
0 0 0 2rpx rgba(78, 222, 163, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-inner {
|
||||||
|
background: linear-gradient(135deg, #1a8f5c 0%, #006c49 100%);
|
||||||
|
border-color: rgba(0, 108, 73, 0.35);
|
||||||
|
box-shadow: 0 6rpx 18rpx rgba(0, 108, 73, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-icon {
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip.active .st-chart-ai-chip-label {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip.loading .st-chart-ai-chip-inner {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner {
|
||||||
|
transform: scale(0.98);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.weekly-page .st-chart-ai-chip-inner {
|
||||||
|
transition: background 0.01s, border-color 0.01s, opacity 0.01s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-ai-chip:active .st-chart-ai-chip-inner {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-chart-title {
|
.weekly-page .st-chart-title {
|
||||||
@@ -285,7 +384,107 @@
|
|||||||
|
|
||||||
.weekly-page .st-chart-legend {
|
.weekly-page .st-chart-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
gap: var(--st-inline-gap);
|
gap: var(--st-inline-gap);
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 趋势卡内展开的 AI 建议 */
|
||||||
|
.weekly-page .st-chart-care-panel {
|
||||||
|
margin: 12rpx 20rpx 0;
|
||||||
|
padding: 28rpx 24rpx 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
border: 1rpx solid rgba(221, 228, 221, 0.65);
|
||||||
|
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-refresh {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-refresh.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-close {
|
||||||
|
min-height: 64rpx;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-close:active {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-chart-care-sub {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-badge--in-chart {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-skeleton--in-chart {
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-cards--in-chart {
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-cards--in-chart .st-meal-glass {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-stats--in-chart {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-disclaimer--in-chart {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.weekly-page .st-legend-item {
|
.weekly-page .st-legend-item {
|
||||||
@@ -693,6 +892,114 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-skeleton-line.short {
|
||||||
|
width: 55%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== AI 血糖照护(嵌在趋势卡内) ===== */
|
||||||
|
.weekly-page .st-care-badge {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.65);
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-cards {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-block--tcm {
|
||||||
|
background: rgba(254, 243, 199, 0.55);
|
||||||
|
border-color: rgba(217, 119, 6, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-block--next {
|
||||||
|
background: rgba(224, 242, 254, 0.55);
|
||||||
|
border-color: rgba(14, 165, 233, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-watch {
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
border-radius: var(--st-radius-2xl);
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-watch-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--on-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-watch-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-watch-dot {
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-watch-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.43;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-stats {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-stat-chip {
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-stat-val {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekly-page .st-care-disclaimer {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: block;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 底部浮动输入 ===== */
|
/* ===== 底部浮动输入 ===== */
|
||||||
.weekly-page .st-float-ask {
|
.weekly-page .st-float-ask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -90,6 +90,49 @@ export function parsePartialDietPlainText(text) {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从流式纯文本中提取血糖照护建议(支持未写完的最后一行) */
|
||||||
|
export function parsePartialBloodCarePlainText(text) {
|
||||||
|
const out = {}
|
||||||
|
if (!text) return out
|
||||||
|
|
||||||
|
const lineRe = /^(总评|血糖|中医|留意|复诊)[::]\s*(.*)$/gm
|
||||||
|
let match
|
||||||
|
while ((match = lineRe.exec(text)) !== null) {
|
||||||
|
const key = {
|
||||||
|
'总评': 'summary',
|
||||||
|
'血糖': 'blood_advice',
|
||||||
|
'中医': 'tcm_plan',
|
||||||
|
'留意': 'watch_points',
|
||||||
|
'复诊': 'next_steps'
|
||||||
|
}[match[1]]
|
||||||
|
const val = (match[2] || '').trim()
|
||||||
|
if (key === 'watch_points') {
|
||||||
|
out.watch_points = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||||
|
} else {
|
||||||
|
out[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tail = text.match(/(?:^|\n)(总评|血糖|中医|留意|复诊)[::]\s*([^\n]*)$/)
|
||||||
|
if (tail) {
|
||||||
|
const key = {
|
||||||
|
'总评': 'summary',
|
||||||
|
'血糖': 'blood_advice',
|
||||||
|
'中医': 'tcm_plan',
|
||||||
|
'留意': 'watch_points',
|
||||||
|
'复诊': 'next_steps'
|
||||||
|
}[tail[1]]
|
||||||
|
const val = (tail[2] || '').trim()
|
||||||
|
if (key === 'watch_points') {
|
||||||
|
if (val) out.watch_points = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||||
|
} else {
|
||||||
|
out[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated 使用 parsePartialDietPlainText */
|
/** @deprecated 使用 parsePartialDietPlainText */
|
||||||
export function parsePartialDietJson(text) {
|
export function parsePartialDietJson(text) {
|
||||||
return parsePartialDietPlainText(text)
|
return parsePartialDietPlainText(text)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use app\api\logic\tcm\DailyFamilyLikeLogic;
|
|||||||
use app\api\logic\tcm\DailyShareLogic;
|
use app\api\logic\tcm\DailyShareLogic;
|
||||||
use app\api\logic\tcm\DailyPhoneLogic;
|
use app\api\logic\tcm\DailyPhoneLogic;
|
||||||
use app\api\logic\tcm\DailyDietAiLogic;
|
use app\api\logic\tcm\DailyDietAiLogic;
|
||||||
|
use app\api\logic\tcm\DailyBloodCareAiLogic;
|
||||||
use app\adminapi\logic\ConfigLogic;
|
use app\adminapi\logic\ConfigLogic;
|
||||||
use app\common\model\tcm\Diagnosis;
|
use app\common\model\tcm\Diagnosis;
|
||||||
use app\common\model\tcm\BloodRecord;
|
use app\common\model\tcm\BloodRecord;
|
||||||
@@ -929,6 +930,47 @@ class TcmController extends BaseApiController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 日常记录-AI 血糖照护建议(累计 7 天有记录)
|
||||||
|
*
|
||||||
|
* 路由:GET /api/tcm/dailyBloodCareAiRecommend?diagnosis_id=:id&refresh=0|1
|
||||||
|
*/
|
||||||
|
public function dailyBloodCareAiRecommend()
|
||||||
|
{
|
||||||
|
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||||
|
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||||
|
if (!$check['ok']) {
|
||||||
|
return $this->fail($check['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$refresh = (int) $this->request->get('refresh', 0) === 1;
|
||||||
|
$result = DailyBloodCareAiLogic::getDailyCarePlan($diagnosisId, $refresh);
|
||||||
|
if (empty($result['ok'])) {
|
||||||
|
return $this->fail($result['error'] ?? '获取照护建议失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data($result['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notes 日常记录-AI 血糖照护建议(SSE 流式)
|
||||||
|
*
|
||||||
|
* 路由:GET /api/tcm/dailyBloodCareAiRecommendStream?diagnosis_id=:id&refresh=0|1
|
||||||
|
*/
|
||||||
|
public function dailyBloodCareAiRecommendStream()
|
||||||
|
{
|
||||||
|
$diagnosisId = (int) $this->request->get('diagnosis_id', 0);
|
||||||
|
$check = $this->ensurePatientOwnsDiagnosis($diagnosisId);
|
||||||
|
if (!$check['ok']) {
|
||||||
|
return $this->fail($check['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$refresh = (int) $this->request->get('refresh', 0) === 1;
|
||||||
|
$this->runDailyDietSse(static function (callable $emit) use ($diagnosisId, $refresh): void {
|
||||||
|
DailyBloodCareAiLogic::streamDailyCarePlan($diagnosisId, $refresh, $emit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callable(callable(string, array<string, mixed>):void):void $runner
|
* @param callable(callable(string, array<string, mixed>):void):void $runner
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -273,6 +273,16 @@ class DailyDietAiLogic
|
|||||||
return $idx;
|
return $idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供血糖照护 AI 等模块复用:患者档案 + 近 7/30 天血糖统计
|
||||||
|
*
|
||||||
|
* @return array{ok:bool,error?:string,data?:array}
|
||||||
|
*/
|
||||||
|
public static function getPatientContext(int $diagnosisId): array
|
||||||
|
{
|
||||||
|
return self::buildPatientContext($diagnosisId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{ok:bool,error?:string,data?:array}
|
* @return array{ok:bool,error?:string,data?:array}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -320,4 +320,68 @@ class GiKnowledge
|
|||||||
$q = trim($q);
|
$q = trim($q);
|
||||||
return $q !== '' ? $q : $question;
|
return $q !== '' ? $q : $question;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 血糖照护 AI 关闭时的规则化方案(监测 + 中医调理方向)
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $stats7
|
||||||
|
* @param array<string,mixed> $stats30
|
||||||
|
* @param array<string,mixed> $extra syndrome_type, symptoms 等
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function fallbackBloodCarePlan(array $stats7 = [], array $stats30 = [], array $extra = []): array
|
||||||
|
{
|
||||||
|
$exercise = self::exercisePlan($stats7, $stats30);
|
||||||
|
$level = (string) ($exercise['level'] ?? 'unknown');
|
||||||
|
$compliance = (int) (($stats30['record_days'] ?? 0) > 0 ? ($stats30['compliance'] ?? 0) : ($stats7['compliance'] ?? 0));
|
||||||
|
$syndrome = trim((string) ($extra['syndrome_type'] ?? ''));
|
||||||
|
$symptoms = trim((string) ($extra['symptoms'] ?? ''));
|
||||||
|
|
||||||
|
$summary = match ($level) {
|
||||||
|
'high' => '近阶段血糖偏高天数偏多,要把监测、饮食和运动一起抓紧,别只靠感觉。',
|
||||||
|
'medium' => '血糖大体可控,但还有波动,坚持记录和餐后活动,能更稳。',
|
||||||
|
'good' => '近期血糖控制不错,保持现在的节奏,定期复查别松懈。',
|
||||||
|
default => '先坚持记血糖、看趋势,再按下面几条慢慢调整生活方式。',
|
||||||
|
};
|
||||||
|
|
||||||
|
$bloodAdvice = match ($level) {
|
||||||
|
'high' => '空腹、餐后都测,偏高那天记下吃了啥、动了没。主食换杂粮、每餐先吃菜和蛋白,甜饮糕点先停。三餐后按上面运动建议动起来,头晕心慌马上停、吃点糖。',
|
||||||
|
'medium' => '每周至少 4 天空腹+餐后配对记录。主食定量、晚餐别过饱,少喝粥配咸菜。餐后散步 20 分钟,睡眠规律,情绪紧张也会抬糖。',
|
||||||
|
'good' => '保持每周 3–4 次监测即可,注意节假日和串门聚餐别放松。继续低 GI 家常饭,运动别停,体重稳一点更好。',
|
||||||
|
default => '先把血糖记满 7 天再看趋势。吃饭定时定量,多喝白开水,少甜少油,饭后多走动。',
|
||||||
|
};
|
||||||
|
|
||||||
|
$tcmBase = '消渴病多与肺脾肾气虚、阴虚燥热或痰湿瘀血相关,需结合面诊脉象。';
|
||||||
|
if ($syndrome !== '') {
|
||||||
|
$tcmBase = '就诊辨证倾向「' . $syndrome . '」。';
|
||||||
|
} elseif ($symptoms !== '') {
|
||||||
|
$tcmBase = '结合您登记的症状(' . mb_substr($symptoms, 0, 40) . '…),宜辨证论治。';
|
||||||
|
}
|
||||||
|
|
||||||
|
$tcmPlan = match ($level) {
|
||||||
|
'high' => $tcmBase . '调理思路:益气养阴、清热生津为主,可配合健脾化湿;食疗如山药、麦冬、桑叶代茶饮方向;艾灸可选足三里、胰俞等穴(需医师指导)。勿自行加大西药或乱用偏方,务必复诊调方。',
|
||||||
|
'medium' => $tcmBase . '调理思路:健脾益气、滋阴润燥,少食肥甘,宜清淡易消化;可配合八段锦、太极拳;中药汤剂需医师面诊后开具,此处不给具体方名剂量。',
|
||||||
|
'good' => $tcmBase . '调理思路:顾护脾胃、养阴防燥,季节更替注意保暖;维持规律作息与情志舒畅,按医嘱定期复诊即可。',
|
||||||
|
default => $tcmBase . '建议到中医科面诊,明确证型后再定汤药、针灸方案;平时可饮食清淡、作息规律,配合记录血糖供医师参考。',
|
||||||
|
};
|
||||||
|
|
||||||
|
$watchPoints = match ($level) {
|
||||||
|
'high' => ['空腹≥7 或餐后明显升高要联系村医', '脚麻脚痛、视力模糊及时就诊', '用药时间饭点别乱改'],
|
||||||
|
'medium' => ['外出走亲也别忘了测糖', '感冒发烧会让血糖乱,多监测', '睡前别加餐太多'],
|
||||||
|
'good' => ['每 3–6 个月复查糖化血红蛋白', '血压血脂一并管', '家人提醒别擅自停药'],
|
||||||
|
default => ['坚持每日或隔日记录', '记录与饮食运动一起给大夫看', '不适及时线下就诊'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'summary' => $summary,
|
||||||
|
'blood_advice' => $bloodAdvice,
|
||||||
|
'tcm_plan' => $tcmPlan,
|
||||||
|
'watch_points' => $watchPoints,
|
||||||
|
'next_steps' => '下周继续记血糖;饮食可参考本页「AI 饮食建议」;若连续 3 天偏高或出现胸闷脚肿,请尽快线下就诊或视频问诊。',
|
||||||
|
'control_level' => $level,
|
||||||
|
'control_label' => (string) ($exercise['level_label'] ?? ''),
|
||||||
|
'disclaimer' => '本建议供日常参考,不能替代医师面诊、开方与调药。',
|
||||||
|
'source' => 'rule',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user