更新
This commit is contained in:
@@ -1,42 +1,65 @@
|
||||
<template>
|
||||
<!-- 页面内嵌模块(如 weekly 统计页主内容区) -->
|
||||
<view v-if="visible && props.inline" class="dev-entry-inline">
|
||||
<view class="inline-head">
|
||||
<view class="inline-head-text">
|
||||
<text class="inline-title">居家耗糖训练</text>
|
||||
<text class="inline-sub">低强度动起来,配合血糖管理</text>
|
||||
<view
|
||||
v-if="visible && props.inline"
|
||||
class="dev-entry-inline"
|
||||
:class="{ 'dev-entry-inline--weekly': props.tone === 'weekly' }"
|
||||
>
|
||||
<view class="inline-hero">
|
||||
<view class="inline-hero-icon" aria-hidden="true">
|
||||
<view class="inline-hero-wave" />
|
||||
</view>
|
||||
<view class="inline-wave" aria-hidden="true" />
|
||||
</view>
|
||||
<view class="inline-grid">
|
||||
<view
|
||||
v-for="item in trainingItems"
|
||||
:key="item.url"
|
||||
class="inline-item"
|
||||
@click="goto(item.url)"
|
||||
>
|
||||
<view class="menu-icon" :class="item.iconClass">
|
||||
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
|
||||
<view class="dumbbell-plate dumbbell-plate-left" />
|
||||
<view class="dumbbell-bar" />
|
||||
<view class="dumbbell-plate dumbbell-plate-right" />
|
||||
</view>
|
||||
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
|
||||
<view class="pedal-bar pedal-bar-left" />
|
||||
<view class="pedal-bar pedal-bar-right" />
|
||||
</view>
|
||||
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
|
||||
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
|
||||
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
|
||||
<view class="metro-bar metro-bar-1" />
|
||||
<view class="metro-bar metro-bar-2" />
|
||||
<view class="metro-bar metro-bar-3" />
|
||||
</view>
|
||||
<view class="inline-hero-text">
|
||||
<view class="inline-hero-title-row">
|
||||
<text class="inline-title">居家耗糖训练</text>
|
||||
<text v-if="props.tone === 'weekly'" class="inline-pill">体验</text>
|
||||
</view>
|
||||
<text class="inline-item-title">{{ item.title }}</text>
|
||||
<text class="inline-item-sub">{{ item.sub }}</text>
|
||||
<text class="inline-sub">低强度动起来,帮助身体消耗糖分</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
scroll-x
|
||||
class="inline-scroll"
|
||||
:show-scrollbar="false"
|
||||
:enable-flex="true"
|
||||
>
|
||||
<view class="inline-scroll-track">
|
||||
<view
|
||||
v-for="item in inlineItems"
|
||||
:key="item.url"
|
||||
class="inline-card"
|
||||
@click="goto(item.url)"
|
||||
>
|
||||
<view v-if="item.featured" class="inline-card-badge">
|
||||
<text>推荐</text>
|
||||
</view>
|
||||
<view class="menu-icon" :class="item.iconClass">
|
||||
<view v-if="item.icon === 'dumbbell'" class="dumbbell-shape">
|
||||
<view class="dumbbell-plate dumbbell-plate-left" />
|
||||
<view class="dumbbell-bar" />
|
||||
<view class="dumbbell-plate dumbbell-plate-right" />
|
||||
</view>
|
||||
<view v-else-if="item.icon === 'pedal'" class="foot-pedal-shape">
|
||||
<view class="pedal-bar pedal-bar-left" />
|
||||
<view class="pedal-bar pedal-bar-right" />
|
||||
</view>
|
||||
<view v-else-if="item.icon === 'pilates'" class="pilates-ring-shape" />
|
||||
<view v-else-if="item.icon === 'grip'" class="grip-ring-shape" />
|
||||
<view v-else-if="item.icon === 'metro'" class="menu-icon--metro-inner">
|
||||
<view class="metro-bar metro-bar-1" />
|
||||
<view class="metro-bar metro-bar-2" />
|
||||
<view class="metro-bar metro-bar-3" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="inline-card-title">{{ item.title }}</text>
|
||||
<text class="inline-card-sub">{{ item.sub }}</text>
|
||||
<text class="inline-card-go" aria-hidden="true">进入</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<text v-if="props.tone === 'weekly'" class="inline-scroll-hint">左右滑动,选择适合您的训练</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
|
||||
@@ -124,15 +147,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const trainingItems = [
|
||||
type TrainingItem = {
|
||||
url: string
|
||||
title: string
|
||||
sub: string
|
||||
icon: 'dumbbell' | 'pedal' | 'pilates' | 'grip' | 'metro'
|
||||
iconClass: string
|
||||
featured?: boolean
|
||||
}
|
||||
|
||||
const trainingItems: TrainingItem[] = [
|
||||
{ url: '/training/pages/dumbbell', title: '哑铃', sub: '力量塑形', icon: 'dumbbell', iconClass: 'menu-icon--dumbbell' },
|
||||
{ url: '/training/pages/foot-pedal', title: '脚蹬器', sub: '下肢有氧', icon: 'pedal', iconClass: 'menu-icon--pedal' },
|
||||
{ url: '/training/pages/pilates-ring', title: '瑜伽环', sub: '核心塑形', icon: 'pilates', iconClass: 'menu-icon--pilates' },
|
||||
{ url: '/training/pages/grip-ring', title: '握力环', sub: '握力训练', icon: 'grip', iconClass: 'menu-icon--grip' },
|
||||
{ url: '/training/pages/metronome', title: '耗糖节拍器', sub: '健走配速', icon: 'metro', iconClass: 'menu-icon--metro' },
|
||||
] as const
|
||||
{ url: '/training/pages/metronome', title: '耗糖节拍器', sub: '健走配速', icon: 'metro', iconClass: 'menu-icon--metro', featured: true },
|
||||
]
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -140,10 +172,20 @@ const props = withDefaults(
|
||||
bottom?: number
|
||||
/** 内嵌到页面主内容区,非 fixed 悬浮 */
|
||||
inline?: boolean
|
||||
/** weekly 页:与统计页 Vital Mint 视觉一致 */
|
||||
tone?: 'default' | 'weekly'
|
||||
}>(),
|
||||
{ bottom: 200, inline: false },
|
||||
{ bottom: 200, inline: false, tone: 'default' },
|
||||
)
|
||||
|
||||
/** weekly 将健走节拍器置前并标推荐 */
|
||||
const inlineItems = computed(() => {
|
||||
if (props.tone !== 'weekly') return trainingItems
|
||||
const metro = trainingItems.find((i) => i.icon === 'metro')
|
||||
const rest = trainingItems.filter((i) => i.icon !== 'metro')
|
||||
return metro ? [metro, ...rest] : trainingItems
|
||||
})
|
||||
|
||||
const isDevMode = (): boolean => {
|
||||
/* HBuilderX 工程:发行(release)模式下 NODE_ENV === 'production' */
|
||||
try {
|
||||
@@ -515,28 +557,59 @@ $brand-light: #34d399;
|
||||
* ============================================================ */
|
||||
.dev-entry-inline {
|
||||
width: 100%;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
border-radius: 48rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.12);
|
||||
box-shadow: 0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08), 0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inline-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
.dev-entry-inline--weekly {
|
||||
padding: 32rpx 28rpx 24rpx;
|
||||
border-radius: 48rpx;
|
||||
background: var(--surface-container-lowest, #fff);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.12);
|
||||
box-shadow:
|
||||
0 24rpx 60rpx -20rpx rgba(0, 108, 73, 0.08),
|
||||
0 8rpx 20rpx -8rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.inline-head-text {
|
||||
.inline-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.inline-hero-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(0, 108, 73, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inline-hero-wave {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23006c49' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><polyline points='2,12 7,12 9.5,7 12,17 14.5,9 16.5,12 22,12'/></svg>");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.inline-hero-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.inline-hero-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.inline-title {
|
||||
@@ -546,75 +619,119 @@ $brand-light: #34d399;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.inline-pill {
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #006c49;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 108, 73, 0.1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.inline-sub {
|
||||
font-size: 24rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
line-height: 1.35;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.inline-wave {
|
||||
width: 120rpx;
|
||||
height: 48rpx;
|
||||
flex-shrink: 0;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 40' fill='none'><path d='M0 20 H18 L26 8 L34 32 L42 14 L50 26 L58 20 H120' stroke='%23006c49' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' opacity='0.85'/></svg>");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right;
|
||||
.inline-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 五行并列,一屏展示完整(不横向滑动) */
|
||||
.inline-grid {
|
||||
display: flex;
|
||||
.inline-scroll-track {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx;
|
||||
width: 100%;
|
||||
gap: 16rpx;
|
||||
padding: 4rpx 2rpx 8rpx;
|
||||
}
|
||||
|
||||
.inline-item {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
.inline-card {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 14rpx 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #f0fdf4;
|
||||
align-items: flex-start;
|
||||
width: 212rpx;
|
||||
min-height: 220rpx;
|
||||
padding: 20rpx 18rpx 18rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #f4f7f5;
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease, background 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
background: #e8f3ee;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin-bottom: 8rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
margin-bottom: 14rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-item-title {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
.inline-card-badge {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
padding: 4rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #006c49;
|
||||
|
||||
text {
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-item-sub {
|
||||
margin-top: 2rpx;
|
||||
font-size: 18rpx;
|
||||
.inline-card-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.25;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-card-sub {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
line-height: 1.35;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-card-go {
|
||||
margin-top: auto;
|
||||
padding-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #006c49;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.inline-scroll-hint {
|
||||
display: block;
|
||||
margin-top: 14rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.inline-card:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import App from './App'
|
||||
var baseUrl ='https://admin.zhenyangtang.com.cn/';
|
||||
var baseUrl ='https://css.zhenyangtang.com.cn/';
|
||||
|
||||
function joinApiUrl(base, path) {
|
||||
const b = String(base || '').replace(/\/+$/, '')
|
||||
|
||||
@@ -5,6 +5,7 @@ const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill'
|
||||
|
||||
const emptyDietAiRecommend = () => ({
|
||||
breakfast: '',
|
||||
drinks: '',
|
||||
lunch: '',
|
||||
dinner: '',
|
||||
tips: '',
|
||||
@@ -31,20 +32,20 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
||||
|
||||
const hasDietAiMealContent = computed(() => {
|
||||
const r = dietAiRecommend.value
|
||||
return !!(r.breakfast || r.lunch || r.dinner || r.tips)
|
||||
return !!(r.breakfast || r.drinks || r.lunch || r.dinner || r.tips)
|
||||
})
|
||||
|
||||
function detectDietStreamField(text) {
|
||||
const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
const tail = String(text || '').match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
if (!tail) return 'breakfast'
|
||||
const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||
const map = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' }
|
||||
return map[tail[1]] || 'breakfast'
|
||||
}
|
||||
|
||||
function mergePartialDietFromStream() {
|
||||
const partial = parsePartialDietPlainText(dietStreamBuffer)
|
||||
const next = { ...dietAiRecommend.value }
|
||||
for (const key of ['breakfast', 'lunch', 'dinner', 'tips']) {
|
||||
for (const key of ['breakfast', 'drinks', 'lunch', 'dinner', 'tips']) {
|
||||
if (partial[key] != null) next[key] = partial[key]
|
||||
}
|
||||
if (Array.isArray(partial.avoid)) next.avoid = partial.avoid
|
||||
@@ -80,6 +81,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
||||
...emptyDietAiRecommend(),
|
||||
...payload,
|
||||
breakfast: streamed.breakfast,
|
||||
drinks: streamed.drinks || payload.drinks || '',
|
||||
lunch: streamed.lunch || payload.lunch || '',
|
||||
dinner: streamed.dinner || payload.dinner || '',
|
||||
tips: streamed.tips || payload.tips || '',
|
||||
@@ -259,6 +261,7 @@ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage
|
||||
try {
|
||||
uni.setStorageSync(DIET_AI_PREFILL_KEY, {
|
||||
breakfast: r.breakfast,
|
||||
drinks: r.drinks || '',
|
||||
lunch: r.lunch,
|
||||
dinner: r.dinner,
|
||||
tips: r.tips || ''
|
||||
|
||||
@@ -67,11 +67,31 @@
|
||||
<text class="gg-food-card-tip">{{ foodCard.tip }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 消除高糖:红色警示弹窗(短暂强调后进入结算) -->
|
||||
<view v-if="highClearAlertVisible" class="gg-high-alert-overlay" @tap.stop>
|
||||
<view class="gg-high-alert-card">
|
||||
<view class="gg-high-alert-icon-wrap" aria-hidden="true">
|
||||
<TongjiIcon name="alert-triangle" size="lg" color="#ffffff" />
|
||||
</view>
|
||||
<view class="gg-high-alert-badge-row">
|
||||
<text class="gg-high-alert-badge">高糖</text>
|
||||
<text class="gg-high-alert-badge-sub">红色标识 · 升糖快</text>
|
||||
</view>
|
||||
<text class="gg-high-alert-title">已消除高糖食物</text>
|
||||
<text class="gg-high-alert-name">{{ clearedHighFoodName }}</text>
|
||||
<text class="gg-high-alert-tip">请记住:这类食物要少吃</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学习结果 -->
|
||||
<view v-if="gamePhase !== 'playing'" class="gg-result-overlay" @tap.stop>
|
||||
<view class="gg-result-card">
|
||||
<text class="gg-result-title">{{ gamePhase === 'won' ? '学会啦!' : '再认一认' }}</text>
|
||||
<view v-if="gamePhase === 'won'" class="gg-result-stars">
|
||||
<view class="gg-result-card" :class="{ 'is-high-clear': winReason === 'high_cleared' }">
|
||||
<view v-if="winReason === 'high_cleared'" class="gg-result-high-head">
|
||||
<text class="gg-result-high-badge">高糖</text>
|
||||
<text class="gg-result-high-tag">已消除</text>
|
||||
</view>
|
||||
<text class="gg-result-title">{{ resultTitle }}</text>
|
||||
<view v-if="gamePhase === 'won' && winReason !== 'high_cleared'" class="gg-result-stars">
|
||||
<text
|
||||
v-for="i in 3"
|
||||
:key="'star-' + i"
|
||||
@@ -80,16 +100,20 @@
|
||||
>★</text>
|
||||
</view>
|
||||
<text class="gg-result-sub">{{ resultMessage }}</text>
|
||||
<view v-if="resultStats.length" class="gg-result-stats">
|
||||
<view v-if="resultStats.length" class="gg-result-stats" :class="{ 'is-high-clear': winReason === 'high_cleared' }">
|
||||
<view v-for="(row, idx) in resultStats" :key="'rs-' + idx" class="gg-result-stat">
|
||||
<text class="gg-result-stat-label">{{ row.label }}</text>
|
||||
<text class="gg-result-stat-val">{{ row.value }}</text>
|
||||
<text class="gg-result-stat-val" :class="{ 'is-high-val': winReason === 'high_cleared' && idx === 0 }">
|
||||
<text v-if="winReason === 'high_cleared' && idx === 0" class="gg-result-inline-badge">高糖</text>
|
||||
{{ row.value }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gg-result-actions">
|
||||
<view
|
||||
v-if="gamePhase === 'won'"
|
||||
class="gg-result-btn is-primary"
|
||||
:class="{ 'is-high-primary': winReason === 'high_cleared' }"
|
||||
@tap="goNextLevel"
|
||||
>认下一组</view>
|
||||
<view class="gg-result-btn" :class="{ 'is-primary': gamePhase !== 'won' }" @tap="restartLevel">
|
||||
@@ -128,104 +152,49 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心数据面板:三栏网格布局 -->
|
||||
<view class="gg-hud-dashboard">
|
||||
<!-- 左侧:血糖监控指示器 -->
|
||||
<view class="gg-dash-glucose">
|
||||
<view class="gg-glucose-ring" :class="meterFillClass" :style="glucoseRingStyle">
|
||||
<view class="gg-glucose-ring-inner">
|
||||
<TongjiIcon name="glucose" size="sm" :color="glucoseAccentColor" />
|
||||
<text class="gg-glucose-value">{{ glucoseLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gg-glucose-detail">
|
||||
<view class="gg-glucose-detail-head">
|
||||
<text class="gg-glucose-title">血糖</text>
|
||||
<text class="gg-glucose-status" :class="statusLabelClass">{{ glucoseShortStatus }}</text>
|
||||
</view>
|
||||
<view class="gg-meter-bar">
|
||||
<view class="gg-meter-track">
|
||||
<view class="gg-meter-zone is-low" />
|
||||
<view class="gg-meter-zone is-ok" />
|
||||
<view class="gg-meter-zone is-high" />
|
||||
</view>
|
||||
<view class="gg-meter-fill" :class="meterFillClass" :style="{ width: glucoseLevel + '%' }" />
|
||||
<view class="gg-meter-pointer" :class="meterFillClass" :style="{ left: glucoseLevel + '%' }" />
|
||||
</view>
|
||||
<view class="gg-meter-legend">
|
||||
<text class="is-low-text">偏低</text>
|
||||
<text class="is-ok-text">稳定</text>
|
||||
<text class="is-high-text">偏高</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 垂直分割线 1 -->
|
||||
<view class="gg-dash-divider" />
|
||||
|
||||
<!-- 中间:步数看板(仅展示,无失败惩罚) -->
|
||||
<view class="gg-dash-moves">
|
||||
<text class="gg-moves-label">步数</text>
|
||||
<text class="gg-moves-val">{{ movesLeft }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 垂直分割线 2 -->
|
||||
<view class="gg-dash-divider" />
|
||||
|
||||
<!-- 右侧:关卡任务目标 -->
|
||||
<view class="gg-dash-mission">
|
||||
<view class="gg-mission-head">
|
||||
<view class="gg-mission-title-wrap">
|
||||
<text class="gg-mission-title">认识这些食物</text>
|
||||
<text class="gg-mission-pct">{{ goalProgressPct }}%</text>
|
||||
</view>
|
||||
<view v-if="dailyStreak >= 2" class="gg-mission-streak">
|
||||
<TongjiIcon name="flame" size="sm" color="#059669" />
|
||||
<text>{{ dailyStreak }} 天</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="gg-mission-goals">
|
||||
<view
|
||||
v-for="g in goalList"
|
||||
:key="g.key"
|
||||
class="gg-goal-chip"
|
||||
:class="{ 'is-done': g.done }"
|
||||
>
|
||||
<view class="gg-goal-icon-wrap">
|
||||
<FoodTileIcon :food="g.food" size="goal" />
|
||||
</view>
|
||||
<view class="gg-goal-chip-body">
|
||||
<view class="gg-goal-chip-bar">
|
||||
<view
|
||||
class="gg-goal-chip-fill"
|
||||
:style="{ width: Math.min(100, Math.round((g.current / g.target) * 100)) + '%' }"
|
||||
/>
|
||||
</view>
|
||||
<text class="gg-goal-chip-text">{{ g.current }}/{{ g.target }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部棋盘说明(认知导向,无倒计时) -->
|
||||
<view v-if="gamePhase === 'playing'" class="gg-board-hint-bar">
|
||||
<view class="gg-board-hint-left">
|
||||
<view class="gg-hint-tag">认糖</view>
|
||||
<text class="gg-board-hint-text">点一下食物,看它含糖高不高</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 棋盘 -->
|
||||
<!-- 主区:棋盘为主,辅助信息弱化 -->
|
||||
<view class="gg-main" :style="mainStyle">
|
||||
<view class="gg-board-stack" :style="boardStackStyle">
|
||||
|
||||
<!-- 次:仅展示上次消除的食物与含糖量 -->
|
||||
<view v-if="gamePhase === 'playing'" class="gg-hud-last-food">
|
||||
<text class="gg-hud-last-food-kicker">上次消除</text>
|
||||
<view
|
||||
v-if="lastClearedFood"
|
||||
class="gg-hud-last-food-card"
|
||||
:class="'is-' + lastClearedFood.gi"
|
||||
>
|
||||
<view class="gg-hud-last-food-icon">
|
||||
<FoodTileIcon :food="lastClearedFood.food" size="goal" />
|
||||
</view>
|
||||
<view class="gg-hud-last-food-body">
|
||||
<text class="gg-hud-last-food-name">{{ lastClearedFood.name }}</text>
|
||||
<view class="gg-hud-last-food-sugar-row">
|
||||
<text class="gg-hud-last-food-sugar-label">含糖量</text>
|
||||
<text
|
||||
class="gg-hud-last-food-gi"
|
||||
:class="'is-' + lastClearedFood.gi"
|
||||
:style="{ backgroundColor: lastClearedFood.color }"
|
||||
>{{ lastClearedFood.giShort }}</text>
|
||||
<text class="gg-hud-last-food-sugar-hint">{{ lastClearedFood.eatHint }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="gg-hud-last-food-placeholder">
|
||||
<text>三连消除后,这里显示食物名称与含糖量</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主:棋盘 -->
|
||||
<view class="gg-board-hero">
|
||||
<view v-if="gamePhase === 'playing'" class="gg-board-hint-inline">
|
||||
<text class="gg-hint-tag">认糖</text>
|
||||
<text class="gg-board-hint-text">点食物看含糖 · 消掉红色「高」即过关</text>
|
||||
</view>
|
||||
<view class="gg-board-stack" :style="boardStackStyle">
|
||||
<view class="gg-board-zone">
|
||||
<view v-if="hintTipVisible" class="gg-no-move-tip">
|
||||
<TongjiIcon name="alert-circle" size="sm" color="#ea580c" />
|
||||
<TongjiIcon name="alert-triangle" size="sm" color="#ea580c" />
|
||||
<text class="gg-no-move-tip-text">{{ hintTipText }}</text>
|
||||
</view>
|
||||
<view class="gg-board-wrap" :style="boardWrapStyle">
|
||||
@@ -284,11 +253,12 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部道具 -->
|
||||
<view class="gg-footer gg-glass" :style="footerStyle">
|
||||
<!-- 辅:道具栏(弱化) -->
|
||||
<view class="gg-footer gg-glass gg-footer--sub" :style="footerStyle">
|
||||
<view class="gg-boosters">
|
||||
<view
|
||||
v-for="booster in boosters"
|
||||
@@ -314,7 +284,15 @@ import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref } f
|
||||
import { onLoad, onReady } from '@dcloudio/uni-app'
|
||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||
import FoodTileIcon from '../components/FoodTileIcon.vue'
|
||||
import { FOOD_LIBRARY, cloneFoodType, getClearVal, getFoodTip, GI_SHORT, GI_COLOR } from '../data/gameFoodLibrary.js'
|
||||
import {
|
||||
FOOD_LIBRARY,
|
||||
cloneFoodType,
|
||||
getClearVal,
|
||||
getFoodTip,
|
||||
formatEatLabel,
|
||||
GI_SHORT,
|
||||
GI_COLOR
|
||||
} from '../data/gameFoodLibrary.js'
|
||||
import { warmUpFoodImgs } from '../data/gameFoodImg.js'
|
||||
import { useGameSfx } from '../composables/useGameSfx.js'
|
||||
import { useGameFx } from '../composables/useGameFx.js'
|
||||
@@ -341,11 +319,11 @@ const sessionMaxCombo = ref(0)
|
||||
const menuHeight = ref(32)
|
||||
const menuTop = ref(0)
|
||||
|
||||
const ROWS = 6
|
||||
const ROWS = 5
|
||||
const COLS = 4
|
||||
const ROW_AXES = Array.from({ length: ROWS }, (_, i) => i)
|
||||
const COL_AXES = Array.from({ length: COLS }, (_, i) => i)
|
||||
/** 6 行 × 4 列棋盘:单格像素由可用区域反算 */
|
||||
/** 5 行 × 4 列棋盘:单格像素由可用区域反算 */
|
||||
const BOARD_INSET_RPX = 4
|
||||
/** 棋盘与底部道具栏之间的安全间隙,避免最后一行被遮挡 */
|
||||
const BOARD_FOOTER_GAP_RPX = 28
|
||||
@@ -366,9 +344,11 @@ const dragPreview = ref(null)
|
||||
let tileTouchStart = null
|
||||
let touchHandledInteraction = false
|
||||
|
||||
// 每局选中的食材子集(保证消除可玩,并让每次进入素材不同)
|
||||
const SESSION_FOODS = { low: 3, mid: 2, high: 2 }
|
||||
// 每局选中的食材子集;高糖仅 1 种(消除该种即通关)
|
||||
const SESSION_FOODS = { low: 3, mid: 2, high: 1 }
|
||||
const activeFoods = ref([])
|
||||
/** 本局唯一的高糖食物类型 */
|
||||
const sessionHighFood = ref(null)
|
||||
const goalDefs = ref([])
|
||||
|
||||
function pickRandom(pool, n) {
|
||||
@@ -382,17 +362,20 @@ function pickRandom(pool, n) {
|
||||
|
||||
/**
|
||||
* 随机挑选本局食材与收集目标。每次调用结果不同:
|
||||
* 低/中/高 各取若干,组成 7 种棋子;再从中挑 2 种作为收集目标。
|
||||
* 低/中各若干 + 仅 1 种高糖;再从中挑 2 种作为收集目标。
|
||||
*/
|
||||
function selectSessionFoods() {
|
||||
const lows = FOOD_LIBRARY.filter((f) => f.gi === 'low')
|
||||
const mids = FOOD_LIBRARY.filter((f) => f.gi === 'mid')
|
||||
const highs = FOOD_LIBRARY.filter((f) => f.gi === 'high')
|
||||
|
||||
const highPick = pickRandom(highs, 1)
|
||||
sessionHighFood.value = highPick[0] ? cloneFoodType(highPick[0]) : null
|
||||
|
||||
const selected = [
|
||||
...pickRandom(lows, SESSION_FOODS.low),
|
||||
...pickRandom(mids, SESSION_FOODS.mid),
|
||||
...pickRandom(highs, SESSION_FOODS.high)
|
||||
...highPick
|
||||
]
|
||||
activeFoods.value = selected
|
||||
warmUpFoodImgs(selected)
|
||||
@@ -507,8 +490,21 @@ const selectedUid = ref(null)
|
||||
const matchChain = ref(0)
|
||||
const movesLeft = ref(25)
|
||||
const gamePhase = ref('playing')
|
||||
/** goals | high_cleared */
|
||||
const winReason = ref('')
|
||||
const loseReason = ref('')
|
||||
/** 消除高糖时的红色警示弹窗 */
|
||||
const highClearAlertVisible = ref(false)
|
||||
const clearedHighFoodName = ref('')
|
||||
|
||||
const resultTitle = computed(() => {
|
||||
if (gamePhase.value !== 'won') return '再认一认'
|
||||
if (winReason.value === 'high_cleared') return '高糖已消除!'
|
||||
return '学会啦!'
|
||||
})
|
||||
const resultMessage = ref('')
|
||||
/** 上一次三连消除的食物(摘要条展示) */
|
||||
const lastClearedFood = ref(null)
|
||||
const foodCard = ref(null)
|
||||
let foodCardTimer = null
|
||||
const floatingVals = ref([])
|
||||
@@ -666,14 +662,15 @@ const glucoseShortStatus = computed(() => {
|
||||
const glucoseAccentColor = computed(() => {
|
||||
if (glucoseLevel.value < 30) return '#3b82f6'
|
||||
if (glucoseLevel.value > 70) return '#dc2626'
|
||||
return '#006c49'
|
||||
return '#ea580c'
|
||||
})
|
||||
|
||||
const glucoseRingStyle = computed(() => {
|
||||
const pct = glucoseLevel.value
|
||||
const color = glucoseAccentColor.value
|
||||
const track = glucoseLevel.value < 30 || glucoseLevel.value > 70 ? '#e2ece6' : '#ffedd5'
|
||||
return {
|
||||
background: `conic-gradient(${color} 0 ${pct}%, #e2ece6 ${pct}% 100%)`
|
||||
background: `conic-gradient(${color} 0 ${pct}%, ${track} ${pct}% 100%)`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -723,6 +720,17 @@ function randomFood() {
|
||||
return cloneFoodType(pick)
|
||||
}
|
||||
|
||||
/** 本局棋盘上是否还存在高糖食物 */
|
||||
function boardHasHighGiFood() {
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const tile = board.value[r]?.[c]
|
||||
if (tile && !tile.isRemoved && tile.type.gi === 'high') return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function wouldCreateInitialMatch(r, c, type, currentBoard, currentRow) {
|
||||
const key = type.key
|
||||
if (r >= 2 && currentBoard[r - 1][c]?.type.key === key && currentBoard[r - 2][c]?.type.key === key) {
|
||||
@@ -833,7 +841,11 @@ function setupLevel(levelId) {
|
||||
const cfg = getLevelConfig(levelId)
|
||||
applyLevelConfig(cfg)
|
||||
sessionMaxCombo.value = 0
|
||||
winReason.value = ''
|
||||
loseReason.value = ''
|
||||
highClearAlertVisible.value = false
|
||||
clearedHighFoodName.value = ''
|
||||
lastClearedFood.value = null
|
||||
tileTouchStart = null
|
||||
clearDragPreview()
|
||||
selectSessionFoods()
|
||||
@@ -869,6 +881,10 @@ function recomputeGlucoseFromBoard() {
|
||||
|
||||
function restartLevel() {
|
||||
gamePhase.value = 'playing'
|
||||
winReason.value = ''
|
||||
highClearAlertVisible.value = false
|
||||
clearedHighFoodName.value = ''
|
||||
lastClearedFood.value = null
|
||||
resultMessage.value = ''
|
||||
resultStars.value = 0
|
||||
resultStats.value = []
|
||||
@@ -884,6 +900,9 @@ function restartLevel() {
|
||||
|
||||
function goNextLevel() {
|
||||
gamePhase.value = 'playing'
|
||||
winReason.value = ''
|
||||
highClearAlertVisible.value = false
|
||||
clearedHighFoodName.value = ''
|
||||
resultMessage.value = ''
|
||||
resultStars.value = 0
|
||||
resultStats.value = []
|
||||
@@ -1407,6 +1426,24 @@ function updateGoal(key) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录本批消除中代表性食物(取最大匹配组) */
|
||||
function recordLastClearedFood(groups) {
|
||||
if (!Array.isArray(groups) || !groups.length) return
|
||||
const main = groups.reduce((a, b) => ((b.size || 0) > (a.size || 0) ? b : a))
|
||||
const rep = main?.tiles?.[Math.floor((main.tiles.length || 1) / 2)]
|
||||
const t = rep?.type
|
||||
if (!t) return
|
||||
const gi = t.gi || 'low'
|
||||
lastClearedFood.value = {
|
||||
name: t.name || '食物',
|
||||
gi,
|
||||
giShort: GI_SHORT[gi] || '低糖',
|
||||
eatHint: formatEatLabel(t),
|
||||
color: GI_COLOR[gi] || '#64748b',
|
||||
food: cloneFoodType(t)
|
||||
}
|
||||
}
|
||||
|
||||
function updateScore(pts) {
|
||||
const bonus = Math.round(pts)
|
||||
score.value += bonus
|
||||
@@ -1448,10 +1485,28 @@ function showLearnFx(groups) {
|
||||
|
||||
function checkGameEnd() {
|
||||
if (gamePhase.value !== 'playing') return
|
||||
// 无失败惩罚:仅在认全目标食物时通关,步数耗尽不判负
|
||||
// 无失败惩罚:认全目标或已消除场上全部高糖食物即通关
|
||||
if (allGoalsDone.value) {
|
||||
endGame('won')
|
||||
endGame('won', { reason: 'goals' })
|
||||
return
|
||||
}
|
||||
if (sessionHighFood.value && !boardHasHighGiFood()) {
|
||||
endGame('won', {
|
||||
reason: 'high_cleared',
|
||||
foodName: sessionHighFood.value.name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 三连消除含高糖:先红色警示弹窗,再进入结算 */
|
||||
async function finishHighSugarClear(foodType) {
|
||||
const name = foodType?.name || sessionHighFood.value?.name || '高糖食物'
|
||||
clearedHighFoodName.value = name
|
||||
highClearAlertVisible.value = true
|
||||
triggerFlash('rgba(220, 38, 38, 0.22)', 0.35)
|
||||
await delay(2000)
|
||||
highClearAlertVisible.value = false
|
||||
endGame('won', { reason: 'high_cleared', foodName: name })
|
||||
}
|
||||
|
||||
function buildResultStats() {
|
||||
@@ -1463,11 +1518,12 @@ function buildResultStats() {
|
||||
]
|
||||
}
|
||||
|
||||
function endGame(phase) {
|
||||
function endGame(phase, opts = {}) {
|
||||
gamePhase.value = phase
|
||||
clearHints()
|
||||
|
||||
if (phase === 'won') {
|
||||
winReason.value = opts.reason || 'goals'
|
||||
const { stars, newBest } = recordWin({
|
||||
levelId: levelConfig.value.id,
|
||||
movesLeft: movesLeft.value,
|
||||
@@ -1477,14 +1533,25 @@ function endGame(phase) {
|
||||
maxCombo: sessionMaxCombo.value
|
||||
})
|
||||
resultStars.value = stars
|
||||
const learnedNames = goalList.value.map((g) => g.food?.name).filter(Boolean).join('、')
|
||||
resultMessage.value = learnedNames
|
||||
? `这一组您认识了:${learnedNames}。记住它们的含糖高低啦!`
|
||||
: '又认识了几种食物的含糖高低,真棒!'
|
||||
if (newBest && gameProgress.value.maxUnlocked > levelConfig.value.id) {
|
||||
resultMessage.value += ` 已解锁第 ${levelConfig.value.id + 1} 组。`
|
||||
if (winReason.value === 'high_cleared') {
|
||||
const name = opts.foodName || sessionHighFood.value?.name || '高糖食物'
|
||||
resultMessage.value =
|
||||
`您已消除高糖食物「${name}」!这类食物升糖快,日常要少吃。`
|
||||
resultStats.value = [
|
||||
{ label: '本局高糖食物', value: name },
|
||||
{ label: '消除练习', value: '达标' },
|
||||
{ label: '得分', value: scoreText.value }
|
||||
]
|
||||
} else {
|
||||
const learnedNames = goalList.value.map((g) => g.food?.name).filter(Boolean).join('、')
|
||||
resultMessage.value = learnedNames
|
||||
? `这一组您认识了:${learnedNames}。记住它们的含糖高低啦!`
|
||||
: '又认识了几种食物的含糖高低,真棒!'
|
||||
if (newBest && gameProgress.value.maxUnlocked > levelConfig.value.id) {
|
||||
resultMessage.value += ` 已解锁第 ${levelConfig.value.id + 1} 组。`
|
||||
}
|
||||
resultStats.value = buildResultStats()
|
||||
}
|
||||
resultStats.value = buildResultStats()
|
||||
playSfxLayer('win', 'winStinger', 120, 0.7)
|
||||
} else {
|
||||
recordLoss()
|
||||
@@ -1545,7 +1612,11 @@ async function processSwap(t1, t2, opts = {}) {
|
||||
const matches = findMatches()
|
||||
if (matches.length > 0) {
|
||||
movesLeft.value = Math.max(0, movesLeft.value - 1)
|
||||
await clearAndRefill()
|
||||
const endedByHigh = await clearAndRefill()
|
||||
if (endedByHigh) {
|
||||
isProcessing.value = false
|
||||
return
|
||||
}
|
||||
// 所有连消结束后,检查棋盘是否死锁并自动重排
|
||||
await reshuffleIfStuck()
|
||||
} else {
|
||||
@@ -1561,7 +1632,10 @@ async function processSwap(t1, t2, opts = {}) {
|
||||
|
||||
async function clearAndRefill() {
|
||||
const { groups, tiles } = findMatchGroups()
|
||||
if (!tiles.length) return
|
||||
if (!tiles.length) return false
|
||||
|
||||
const clearedHighTiles = tiles.filter((t) => t.type.gi === 'high')
|
||||
recordLastClearedFood(groups)
|
||||
|
||||
let totalVal = 0
|
||||
let maxSize = 3
|
||||
@@ -1600,14 +1674,22 @@ async function clearAndRefill() {
|
||||
await delay(MATCH_MS)
|
||||
|
||||
removeTilesFromBoard(tiles)
|
||||
|
||||
if (clearedHighTiles.length > 0) {
|
||||
await finishHighSugarClear(clearedHighTiles[0].type)
|
||||
return true
|
||||
}
|
||||
|
||||
await applyGravity()
|
||||
|
||||
const next = findMatchGroups()
|
||||
if (next.tiles.length > 0) {
|
||||
matchChain.value += 1
|
||||
await delay(200)
|
||||
await clearAndRefill()
|
||||
const ended = await clearAndRefill()
|
||||
if (ended) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function applyGravity() {
|
||||
@@ -1771,7 +1853,11 @@ async function onBooster(booster) {
|
||||
|
||||
const matches = findMatches()
|
||||
if (matches.length > 0) {
|
||||
await clearAndRefill()
|
||||
const endedByHigh = await clearAndRefill()
|
||||
if (endedByHigh) {
|
||||
isProcessing.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
// fiber 改变了棋盘,保险检查是否死锁
|
||||
await reshuffleIfStuck()
|
||||
@@ -1937,7 +2023,7 @@ function initLayout() {
|
||||
paddingTop: `${paddingTop}px`,
|
||||
paddingLeft: '32rpx',
|
||||
paddingRight: '32rpx',
|
||||
paddingBottom: '16rpx'
|
||||
paddingBottom: '10rpx'
|
||||
}
|
||||
navStyle.value = {
|
||||
paddingRight: `${Math.max(0, headerPaddingRight - uni.upx2px(32))}px`
|
||||
@@ -1971,7 +2057,7 @@ function recalcBoardSize() {
|
||||
// 预留底部安全间隙,棋盘不贴到道具栏
|
||||
const zoneH = Math.max(0, footerTop - headerBottom - footerGap)
|
||||
|
||||
// 6 行 × 4 列:宽高分别约束,取较小单格像素
|
||||
// 5 行 × 4 列:宽高分别约束,取较小单格像素
|
||||
cellSizePx.value = calcMaxCellSize(zoneW - inset * 2, zoneH - inset * 2)
|
||||
const boardDim = calcBoardDimensions(cellSizePx.value)
|
||||
boardWidthPx.value = boardDim.width
|
||||
|
||||
@@ -419,7 +419,7 @@
|
||||
|
||||
<view v-else class="ai-diet-body" :class="{ 'is-streaming': dietAiStreaming }">
|
||||
<view class="ai-diet-section-head">
|
||||
<text class="ai-diet-section-title">今日三餐</text>
|
||||
<text class="ai-diet-section-title">今日饮食</text>
|
||||
<text v-if="dietAiStreaming" class="ai-diet-section-tag">生成中…</text>
|
||||
</view>
|
||||
<view class="ai-diet-meal-list">
|
||||
@@ -434,6 +434,20 @@
|
||||
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
|
||||
class="ai-diet-meal-card meal-drinks"
|
||||
>
|
||||
<view class="ai-diet-meal-head">
|
||||
<view class="ai-diet-meal-icon" aria-hidden="true">
|
||||
<TongjiIcon name="droplet" size="sm" color="#ffffff" />
|
||||
</view>
|
||||
<text class="ai-diet-meal-label">喝的</text>
|
||||
</view>
|
||||
<text class="ai-diet-meal-text">
|
||||
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="ai-diet-meal-card meal-lunch">
|
||||
<view class="ai-diet-meal-head">
|
||||
<view class="ai-diet-meal-icon" aria-hidden="true">
|
||||
@@ -2657,16 +2671,13 @@ async function bootstrap(options) {
|
||||
if (!target) target = list[0]
|
||||
await applyCard(target)
|
||||
} else {
|
||||
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
|
||||
if (passedPatientId) patientId.value = passedPatientId
|
||||
if (passedPatientName) patientName.value = passedPatientName
|
||||
if (passedAge) age.value = passedAge
|
||||
if (passedGender) gender.value = passedGender
|
||||
if (!diagnosisId.value || !patientId.value) {
|
||||
readUserContext()
|
||||
}
|
||||
diagnosisId.value = 0
|
||||
patientId.value = 0
|
||||
patientName.value = ''
|
||||
age.value = 0
|
||||
gender.value = 0
|
||||
readUserMobile()
|
||||
if (!diagnosisId.value && userMobile.value) {
|
||||
if (userMobile.value) {
|
||||
await ensureDailyContextByPhone()
|
||||
}
|
||||
}
|
||||
@@ -6558,6 +6569,12 @@ async function onFamilyLike() {
|
||||
.ai-diet-meal-card.meal-breakfast::before {
|
||||
background: linear-gradient(180deg, #fbbf24 0%, #f59e0b 100%);
|
||||
}
|
||||
.ai-diet-meal-card.meal-drinks {
|
||||
background: linear-gradient(120deg, rgba(14, 165, 233, 0.14) 0%, rgba(255, 255, 255, 0) 45%);
|
||||
}
|
||||
.ai-diet-meal-card.meal-drinks::before {
|
||||
background: linear-gradient(180deg, #38bdf8 0%, #0ea5e9 100%);
|
||||
}
|
||||
.ai-diet-meal-card.meal-lunch {
|
||||
background: linear-gradient(120deg, rgba(16, 185, 129, 0.16) 0%, rgba(255, 255, 255, 0) 45%);
|
||||
}
|
||||
@@ -6588,6 +6605,9 @@ async function onFamilyLike() {
|
||||
.meal-breakfast .ai-diet-meal-icon {
|
||||
background: linear-gradient(150deg, #fde68a 0%, #fbbf24 100%);
|
||||
}
|
||||
.meal-drinks .ai-diet-meal-icon {
|
||||
background: linear-gradient(150deg, #7dd3fc 0%, #0ea5e9 100%);
|
||||
}
|
||||
.meal-lunch .ai-diet-meal-icon {
|
||||
background: linear-gradient(150deg, #6ee7b7 0%, #10b981 100%);
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@
|
||||
<scroll-view v-else scroll-y class="input-modal-body">
|
||||
<view v-if="dietAiPrefill && dietAiPrefill.breakfast" class="input-modal-ai-tip">
|
||||
<text class="input-modal-ai-tip-label">AI 推荐</text>
|
||||
<text class="input-modal-ai-tip-text">早 {{ dietAiPrefill.breakfast }} · 午 {{ dietAiPrefill.lunch }} · 晚 {{ dietAiPrefill.dinner }}</text>
|
||||
<text class="input-modal-ai-tip-text">早 {{ dietAiPrefill.breakfast }}<text v-if="dietAiPrefill.drinks"> · 喝 {{ dietAiPrefill.drinks }}</text> · 午 {{ dietAiPrefill.lunch }} · 晚 {{ dietAiPrefill.dinner }}</text>
|
||||
<view class="input-modal-ai-tip-btn" @click="fillDietFormFromAi">
|
||||
<text>一键填入</text>
|
||||
</view>
|
||||
@@ -1291,7 +1291,7 @@ async function ensureDailyContextByPhone() {
|
||||
age: res.data.age || 0,
|
||||
gender: res.data.gender || 0
|
||||
})
|
||||
if (res.data.created) {
|
||||
if (res.data.created || res.data.linked_existing) {
|
||||
await fetchCardList()
|
||||
}
|
||||
return true
|
||||
@@ -2795,14 +2795,13 @@ async function bootstrap(options) {
|
||||
if (!target) target = list[0]
|
||||
await applyCard(target)
|
||||
} else {
|
||||
diagnosisId.value = passedDiagnosisId || 0
|
||||
patientId.value = passedPatientId || 0
|
||||
if (passedPatientName) patientName.value = passedPatientName
|
||||
if (passedAge) age.value = passedAge
|
||||
if (passedGender) gender.value = passedGender
|
||||
if (!patientName.value) readUserContext()
|
||||
diagnosisId.value = 0
|
||||
patientId.value = 0
|
||||
patientName.value = ''
|
||||
age.value = 0
|
||||
gender.value = 0
|
||||
|
||||
if (!diagnosisId.value && readUserMobile()) {
|
||||
if (readUserMobile()) {
|
||||
await ensureDailyContextByPhone()
|
||||
}
|
||||
if (!diagnosisId.value) {
|
||||
|
||||
@@ -48,13 +48,15 @@
|
||||
|
||||
|
||||
<view class="st-main">
|
||||
<!-- 1. 录入今日血糖 -->
|
||||
|
||||
<!-- 快捷建档失败提示(toast 字数有限,完整文案展示在页面) -->
|
||||
<view v-if="dailyContextError && !diagnosisId" class="st-daily-context-error" role="alert">
|
||||
<text class="st-daily-context-error-text">{{ dailyContextError }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 2. 血糖趋势(无就诊卡或录入不足 7 天显示示例曲线) -->
|
||||
<!-- 2. 血糖趋势(无就诊卡或累计录入不足 7 天显示示例曲线) -->
|
||||
<view class="st-chart-card">
|
||||
<view class="st-chart-head">
|
||||
<view>
|
||||
<view style="padding: 0 20rpx;">
|
||||
<text class="st-chart-title">血糖趋势</text>
|
||||
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
|
||||
|
||||
@@ -94,9 +96,9 @@
|
||||
</view>
|
||||
</view>
|
||||
<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 class="st-game-entry st-game-entry--in-card" @click="goGamePage">
|
||||
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
|
||||
<view class="st-game-entry-icon">
|
||||
<TongjiIcon name="activity" size="sm" color="#006c49" />
|
||||
</view>
|
||||
@@ -107,9 +109,9 @@
|
||||
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="st-training-module">
|
||||
<dev-training-entry inline />
|
||||
</view>
|
||||
<view class="st-training-section">
|
||||
<dev-training-entry inline tone="weekly" />
|
||||
</view>
|
||||
<!-- 3. AI 饮食建议 -->
|
||||
<view v-if="diagnosisId" class="st-ai-section">
|
||||
<view class="st-ai-deco" aria-hidden="true" />
|
||||
@@ -129,7 +131,7 @@
|
||||
<text>{{ dietAiLoading ? '生成中' : '换一换' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="st-ai-sub">读您近 7 天 · 30 天血糖,定制今日三餐</text>
|
||||
<text class="st-ai-sub">读您近 7 天 · 30 天血糖,定制早餐(含驼奶粉)、喝的与三餐</text>
|
||||
|
||||
<view v-if="dietAiLoading && !hasDietAiMealContent && !dietAiStreaming && !dietAiReplacing" class="st-skeleton">
|
||||
<view v-for="n in 2" :key="'sk-' + n" class="st-skeleton-line" />
|
||||
@@ -145,6 +147,18 @@
|
||||
{{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
|
||||
class="st-meal-glass st-meal-glass--drinks"
|
||||
>
|
||||
<view class="st-meal-head">
|
||||
<TongjiIcon name="droplet" size="sm" color="#0ea5e9" />
|
||||
<text class="st-meal-label">喝的</text>
|
||||
</view>
|
||||
<text class="st-meal-text">
|
||||
{{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor">▍</text>
|
||||
</text>
|
||||
</view>
|
||||
<view class="st-meal-glass">
|
||||
<view class="st-meal-head">
|
||||
<TongjiIcon name="leaf" size="sm" color="#006c49" />
|
||||
@@ -248,6 +262,9 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="phone-gate-body">
|
||||
<view v-if="dailyContextError" class="phone-gate-error" role="alert">
|
||||
<text>{{ dailyContextError }}</text>
|
||||
</view>
|
||||
<text class="phone-gate-tip">微信仅提供手机号,请选择性别以便写入就诊档案。</text>
|
||||
<view class="phone-gate-gender">
|
||||
<text class="phone-gate-gender-label">性别</text>
|
||||
@@ -432,11 +449,42 @@ function showUserToast(title, options = {}) {
|
||||
uni.showToast({
|
||||
title: text,
|
||||
icon: options.icon || 'none',
|
||||
duration: options.duration
|
||||
duration: options.duration || 2800
|
||||
})
|
||||
}
|
||||
|
||||
/** 解析接口错误文案(兼容 msg / message / data.error) */
|
||||
function extractApiError(res, fallback = '') {
|
||||
if (!res) return fallback
|
||||
const raw = res.msg ?? res.message ?? res.data?.error ?? res.data?.msg ?? res.errMsg
|
||||
return formatUserMessage(raw, fallback)
|
||||
}
|
||||
|
||||
const diagnosisId = ref(0)
|
||||
/** 手机号快捷建档失败:页面常驻提示(避免 toast 被 7 字截断) */
|
||||
const dailyContextError = ref('')
|
||||
|
||||
function clearDailyContextError() {
|
||||
dailyContextError.value = ''
|
||||
}
|
||||
|
||||
function reportDailyContextError(res, fallback = '无法开始录入', options = {}) {
|
||||
const text = extractApiError(res, fallback)
|
||||
if (!text) return
|
||||
dailyContextError.value = text
|
||||
if (options.alert) {
|
||||
uni.showModal({
|
||||
title: '无法录入血糖',
|
||||
content: text,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
} else {
|
||||
const short = text.length > 14 ? `${text.slice(0, 14)}…` : text
|
||||
showUserToast(short, { duration: 3200 })
|
||||
}
|
||||
}
|
||||
|
||||
const patientId = ref(0)
|
||||
const patientName = ref('')
|
||||
const displayPatientName = computed(() => {
|
||||
@@ -632,6 +680,19 @@ const currentRange = computed(() => {
|
||||
return { start: formatDate(start), end: formatDate(end) }
|
||||
})
|
||||
|
||||
/** 拉取血糖时向前多看几天,用于判断「累计满 7 天」是否达标(图表仍只画 currentRange) */
|
||||
const BLOOD_LOOKBACK_DAYS = 30
|
||||
|
||||
const fetchRange = computed(() => {
|
||||
const displayDays = rangeMode.value === '30' ? 30 : 7
|
||||
const lookback = Math.max(BLOOD_LOOKBACK_DAYS, displayDays)
|
||||
const end = new Date()
|
||||
end.setHours(0, 0, 0, 0)
|
||||
const start = new Date(end)
|
||||
start.setDate(start.getDate() - (lookback - 1))
|
||||
return { start: formatDate(start), end: formatDate(end) }
|
||||
})
|
||||
|
||||
const visibleDates = computed(() => {
|
||||
const { start, end } = currentRange.value
|
||||
if (!start || !end) return []
|
||||
@@ -794,27 +855,37 @@ const recordDays = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
function bloodDayHasSugar(b) {
|
||||
if (!b) return false
|
||||
return toNumber(b.fasting_blood_sugar) !== null
|
||||
|| toNumber(b.postprandial_blood_sugar) !== null
|
||||
|| toNumber(b.other_blood_sugar) !== null
|
||||
}
|
||||
|
||||
const bloodHasData = computed(() => {
|
||||
return chartDates.value.some((date) => {
|
||||
const b = bloodByDate.value[date]
|
||||
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
|
||||
})
|
||||
return chartDates.value.some((date) => bloodDayHasSugar(bloodByDate.value[date]))
|
||||
})
|
||||
|
||||
/** 当前时间范围内有真实录入的天数 */
|
||||
/** 当前图表时间范围内有真实录入的天数(用于小结统计) */
|
||||
const bloodRecordDayCount = computed(() => {
|
||||
return chartDates.value.filter((date) => {
|
||||
const b = bloodByDate.value[date]
|
||||
return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
|
||||
}).length
|
||||
return chartDates.value.filter((date) => bloodDayHasSugar(bloodByDate.value[date])).length
|
||||
})
|
||||
|
||||
/** 拉取窗口内累计有血糖录入的天数(不要求连续) */
|
||||
const bloodCumulativeDayCount = computed(() => {
|
||||
let n = 0
|
||||
for (const date of Object.keys(bloodByDate.value)) {
|
||||
if (bloodDayHasSugar(bloodByDate.value[date])) n++
|
||||
}
|
||||
return n
|
||||
})
|
||||
|
||||
const REAL_BLOOD_DAYS_MIN = 7
|
||||
|
||||
/** 无就诊卡或真实录入不足 7 天时,图表用示例数据 */
|
||||
/** 无就诊卡或累计录入不足 7 天时,图表用示例数据 */
|
||||
const chartUseMockData = computed(() => {
|
||||
if (!diagnosisId.value) return true
|
||||
return bloodRecordDayCount.value < REAL_BLOOD_DAYS_MIN
|
||||
return bloodCumulativeDayCount.value < REAL_BLOOD_DAYS_MIN
|
||||
})
|
||||
|
||||
/** 示例曲线:7 天内有波动的空腹/餐后,便于老人理解趋势图形态 */
|
||||
@@ -1154,8 +1225,14 @@ function syncPhoneGateSexFromUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPhoneGate() {
|
||||
async function openPhoneGate() {
|
||||
syncPhoneGateSexFromUser()
|
||||
readUserMobile()
|
||||
if (userMobile.value) {
|
||||
phoneGateVisible.value = false
|
||||
await resolveDailyPhoneContext(false)
|
||||
return
|
||||
}
|
||||
phoneGateVisible.value = true
|
||||
}
|
||||
|
||||
@@ -1172,8 +1249,11 @@ async function refreshUserMobile() {
|
||||
return userMobile.value
|
||||
}
|
||||
|
||||
async function ensureDailyContextByPhone(sex) {
|
||||
if (diagnosisId.value) return true
|
||||
async function ensureDailyContextByPhone(sex, options = {}) {
|
||||
if (diagnosisId.value) {
|
||||
clearDailyContextError()
|
||||
return true
|
||||
}
|
||||
|
||||
const payload = {}
|
||||
const sexVal = sex ?? phoneGateSex.value
|
||||
@@ -1188,6 +1268,7 @@ async function ensureDailyContextByPhone(sex) {
|
||||
}, false)
|
||||
|
||||
if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
|
||||
clearDailyContextError()
|
||||
await applyCard({
|
||||
id: res.data.diagnosis_id,
|
||||
patient_id: res.data.patient_id,
|
||||
@@ -1195,17 +1276,32 @@ async function ensureDailyContextByPhone(sex) {
|
||||
age: res.data.age || 0,
|
||||
gender: res.data.gender || 0
|
||||
})
|
||||
if (res.data.created) {
|
||||
if (res.data.created || res.data.linked_existing) {
|
||||
await fetchCardList()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (res && res.data && res.data.need_mobile) {
|
||||
clearDailyContextError()
|
||||
return 'need_mobile'
|
||||
}
|
||||
|
||||
showUserToast(formatUserMessage(res?.msg, '无法开始录入'))
|
||||
reportDailyContextError(res, '无法开始录入', options)
|
||||
return false
|
||||
}
|
||||
|
||||
/** 已绑定手机号时直接关联档案,不再弹出授权建档 */
|
||||
async function resolveDailyPhoneContext(openFormAfter = false) {
|
||||
const ok = await ensureDailyContextByPhone(phoneGateSex.value, { alert: true })
|
||||
if (ok === true) {
|
||||
if (openFormAfter) await openInputForm(true)
|
||||
return true
|
||||
}
|
||||
if (ok === 'need_mobile') {
|
||||
syncPhoneGateSexFromUser()
|
||||
phoneGateVisible.value = true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1218,7 +1314,7 @@ async function ensureCanRecordGlucose() {
|
||||
return false
|
||||
}
|
||||
|
||||
const ok = await ensureDailyContextByPhone()
|
||||
const ok = await ensureDailyContextByPhone(undefined, { alert: true })
|
||||
if (ok === 'need_mobile') {
|
||||
openPhoneGate()
|
||||
return false
|
||||
@@ -1251,6 +1347,13 @@ async function onPhoneGateAuthorize(e) {
|
||||
|
||||
phoneBinding.value = true
|
||||
try {
|
||||
readUserMobile()
|
||||
if (userMobile.value) {
|
||||
phoneGateVisible.value = false
|
||||
await resolveDailyPhoneContext(true)
|
||||
return
|
||||
}
|
||||
|
||||
const res = await proxy.apiUrl({
|
||||
url: '/api/user/getMobileByMnp',
|
||||
method: 'POST',
|
||||
@@ -1262,12 +1365,7 @@ async function onPhoneGateAuthorize(e) {
|
||||
}
|
||||
await refreshUserMobile()
|
||||
phoneGateVisible.value = false
|
||||
const ok = await ensureDailyContextByPhone(phoneGateSex.value)
|
||||
if (ok === true) {
|
||||
await openInputForm(true)
|
||||
} else if (ok === 'need_mobile') {
|
||||
openPhoneGate()
|
||||
}
|
||||
await resolveDailyPhoneContext(true)
|
||||
} catch (err) {
|
||||
showUserToast('网络异常,请稍后再试')
|
||||
} finally {
|
||||
@@ -1290,7 +1388,7 @@ async function onSelectCard(card) {
|
||||
async function fetchAll() {
|
||||
if (!hasAuthToken() || !diagnosisId.value) return
|
||||
loading.value = true
|
||||
const { start, end } = currentRange.value
|
||||
const { start, end } = fetchRange.value
|
||||
try {
|
||||
const [winRes, noteRes] = await Promise.all([
|
||||
proxy.apiUrl({
|
||||
@@ -2490,15 +2588,15 @@ async function bootstrap(options) {
|
||||
if (!target) target = list[0]
|
||||
await applyCard(target)
|
||||
} else {
|
||||
diagnosisId.value = passedDiagnosisId || 0
|
||||
patientId.value = passedPatientId || 0
|
||||
if (passedPatientName) patientName.value = passedPatientName
|
||||
if (passedAge) age.value = passedAge
|
||||
if (passedGender) gender.value = passedGender
|
||||
if (!patientName.value) readUserContext()
|
||||
// 无可见就诊卡:勿用 URL/缓存里的隐藏诊单 ID
|
||||
diagnosisId.value = 0
|
||||
patientId.value = 0
|
||||
patientName.value = ''
|
||||
age.value = 0
|
||||
gender.value = 0
|
||||
readUserMobile()
|
||||
if (!diagnosisId.value && userMobile.value) {
|
||||
await ensureDailyContextByPhone()
|
||||
if (userMobile.value) {
|
||||
await ensureDailyContextByPhone(undefined, { alert: false })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
border-radius: 0 0 32rpx 32rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 108, 73, 0.04);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-bottom: 1.5rpx solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 0 0 24rpx 24rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 108, 73, 0.04);
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-bottom: 1rpx solid rgba(0, 108, 73, 0.06);
|
||||
}
|
||||
|
||||
.game-page .gg-hud-nav {
|
||||
@@ -50,7 +50,7 @@
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: var(--menu-height, 64rpx);
|
||||
margin-bottom: 14rpx;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-control-group {
|
||||
@@ -107,7 +107,7 @@
|
||||
}
|
||||
|
||||
.game-page .gg-hud-score-val {
|
||||
font-size: 42rpx;
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #006c49;
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -124,29 +124,155 @@
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* 核心三栏面板 */
|
||||
.game-page .gg-hud-dashboard {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border-radius: 24rpx;
|
||||
border: 1.5rpx solid rgba(0, 108, 73, 0.08);
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 108, 73, 0.03), inset 0 2rpx 8rpx rgba(255, 255, 255, 0.8);
|
||||
padding: 16rpx;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
align-items: stretch;
|
||||
/* ===== 次:上次消除(名称 + 含糖量) ===== */
|
||||
.game-page .gg-hud-last-food {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
margin: 0 auto 14rpx;
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 22rpx;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.1);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 108, 73, 0.06);
|
||||
}
|
||||
|
||||
/* 左侧:血糖监控 */
|
||||
.game-page .gg-dash-glucose {
|
||||
flex: 1.2;
|
||||
.game-page .gg-hud-last-food-kicker {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
letter-spacing: 0.04em;
|
||||
margin-bottom: 12rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 14rpx 16rpx;
|
||||
border-radius: 18rpx;
|
||||
background: #f8fafc;
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.08);
|
||||
animation: gg-last-food-in 0.22s ease-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.game-page .gg-hud-last-food-card {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-card.is-high {
|
||||
background: rgba(254, 242, 242, 0.9);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-card.is-mid {
|
||||
background: rgba(255, 251, 235, 0.95);
|
||||
border-color: rgba(234, 88, 12, 0.18);
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-card.is-low {
|
||||
background: rgba(240, 253, 244, 0.95);
|
||||
border-color: rgba(22, 163, 74, 0.15);
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 16rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-icon .food-tile-icon--goal {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
line-height: 1.25;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-sugar-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8rpx 12rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-sugar-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-gi {
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-sugar-hint {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-card.is-high .gg-hud-last-food-sugar-hint {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-placeholder {
|
||||
padding: 12rpx 4rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-hud-last-food-placeholder text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@keyframes gg-last-food-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 血糖圆环(主区摘要用 mini) */
|
||||
.game-page .gg-glucose-ring {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
padding: 6rpx;
|
||||
flex-shrink: 0;
|
||||
@@ -168,42 +294,37 @@
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-value {
|
||||
font-size: 34rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
color: #0f172a;
|
||||
line-height: 1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-detail {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.game-page .gg-glucose-brief {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-detail-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx;
|
||||
align-items: flex-start;
|
||||
gap: 10rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-title {
|
||||
font-size: 28rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #334155;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-status {
|
||||
font-size: 26rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #006c49;
|
||||
padding: 2rpx 12rpx;
|
||||
color: #ea580c;
|
||||
padding: 6rpx 18rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 108, 73, 0.08);
|
||||
background: rgba(234, 88, 12, 0.1);
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-status.is-warn-low {
|
||||
@@ -217,294 +338,74 @@
|
||||
animation: gg-alert-pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-ring--mini {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
padding: 4rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-ring--mini .gg-glucose-value {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-glucose-status--mini {
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
}
|
||||
|
||||
@keyframes gg-alert-pulse {
|
||||
0% { opacity: 0.8; }
|
||||
50% { opacity: 1; box-shadow: 0 0 8rpx rgba(239, 68, 68, 0.3); }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.game-page .gg-meter-bar {
|
||||
position: relative;
|
||||
height: 12rpx;
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-zone {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-zone.is-low { background: #3b82f6; }
|
||||
.game-page .gg-meter-zone.is-ok { background: #10b981; }
|
||||
.game-page .gg-meter-zone.is-high { background: #ef4444; }
|
||||
|
||||
.game-page .gg-meter-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
border-radius: 999rpx;
|
||||
transition: width 0.6s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
z-index: 2;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-fill.is-stable {
|
||||
background: linear-gradient(to right, #34d399, #006c49);
|
||||
}
|
||||
|
||||
.game-page .gg-meter-fill.is-low {
|
||||
background: linear-gradient(to right, #93c5fd, #3b82f6);
|
||||
}
|
||||
|
||||
.game-page .gg-meter-fill.is-high {
|
||||
background: linear-gradient(to right, #f87171, #dc2626);
|
||||
}
|
||||
|
||||
.game-page .gg-meter-pointer {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
background: #ffffff;
|
||||
border: 4rpx solid #006c49;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.12);
|
||||
transition: left 0.6s cubic-bezier(0.25, 1, 0.5, 1), border-color 0.4s;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-pointer.is-low { border-color: #3b82f6; }
|
||||
.game-page .gg-meter-pointer.is-high { border-color: #dc2626; }
|
||||
.game-page .gg-meter-pointer.is-stable { border-color: #006c49; }
|
||||
|
||||
.game-page .gg-meter-legend {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 2rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-legend text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.game-page .gg-meter-legend .is-low-text { color: #3b82f6; }
|
||||
.game-page .gg-meter-legend .is-ok-text { color: #006c49; }
|
||||
.game-page .gg-meter-legend .is-high-text { color: #dc2626; }
|
||||
|
||||
/* 垂直分割线 */
|
||||
.game-page .gg-dash-divider {
|
||||
width: 1rpx;
|
||||
align-self: stretch;
|
||||
background: linear-gradient(to bottom, transparent, rgba(0, 108, 73, 0.08), transparent);
|
||||
}
|
||||
|
||||
/* 中间:步数看板 */
|
||||
.game-page .gg-dash-moves {
|
||||
flex: 0.8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 108, 73, 0.03);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.08);
|
||||
padding: 8rpx 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.game-page .gg-moves-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #475569;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-moves-val {
|
||||
font-size: 48rpx;
|
||||
font-weight: 900;
|
||||
color: #006c49;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* 右侧:关卡目标 */
|
||||
.game-page .gg-dash-mission {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-title-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-pct {
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #006c49;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-streak {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 108, 73, 0.06);
|
||||
font-size: 16rpx;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.game-page .gg-mission-goals {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 6rpx 10rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(0, 108, 73, 0.02);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.06);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip.is-done {
|
||||
background: linear-gradient(135deg, #ecfdf5, #d1fae5);
|
||||
border-color: rgba(16, 185, 129, 0.35);
|
||||
}
|
||||
|
||||
.game-page .gg-goal-icon-wrap {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip-bar {
|
||||
height: 6rpx;
|
||||
background: rgba(0, 108, 73, 0.06);
|
||||
border-radius: 999rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #34d399, #006c49);
|
||||
border-radius: 999rpx;
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip.is-done .gg-goal-chip-fill {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 800;
|
||||
color: #334155;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.game-page .gg-goal-chip.is-done .gg-goal-chip-text {
|
||||
color: #006c49;
|
||||
}
|
||||
|
||||
@keyframes gg-pop-score {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.12); color: #059669; }
|
||||
50% { transform: scale(1.12); color: #ea580c; }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* ===== Board hints ===== */
|
||||
.game-page .gg-board-hint-bar {
|
||||
/* ===== 主:棋盘区 ===== */
|
||||
.game-page .gg-board-hero {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-page .gg-board-hint-inline {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
padding: 10rpx 16rpx;
|
||||
margin-top: 14rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.06);
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 0 8rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.game-page .gg-board-hint-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-hint-tag {
|
||||
font-size: 24rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
background: #006c49;
|
||||
padding: 4rpx 14rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.game-page .gg-board-hint-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
line-height: 1.35;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-page .gg-board-stack {
|
||||
@@ -524,17 +425,17 @@
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--board-area-top, 200px);
|
||||
top: var(--board-area-top, 120px);
|
||||
bottom: var(--board-area-bottom, 180px);
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
padding: 8rpx 16rpx 0;
|
||||
}
|
||||
|
||||
/* gg-hint-banner 已移除,提示直接显示在棋盘食物上 */
|
||||
@@ -559,22 +460,24 @@
|
||||
}
|
||||
|
||||
.game-page .gg-board-wrap {
|
||||
/* 7 行 × 6 列矩形棋盘宽高由 JS boardWrapStyle 注入 */
|
||||
/* 5 行 × 4 列棋盘宽高由 JS boardWrapStyle 注入 */
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.game-page .game-grid {
|
||||
/* 7 行 flex,每行 6 列,单格宽高由 JS cellStyle 注入 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 30rpx 80rpx rgba(0, 108, 73, 0.08), inset 0 2rpx 10rpx rgba(255, 255, 255, 0.6);
|
||||
border: 1.5rpx solid rgba(0, 108, 73, 0.08);
|
||||
border-radius: 36rpx;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
box-shadow:
|
||||
0 20rpx 56rpx rgba(0, 108, 73, 0.14),
|
||||
0 4rpx 16rpx rgba(15, 23, 42, 0.06),
|
||||
inset 0 2rpx 12rpx rgba(255, 255, 255, 0.85);
|
||||
border: 2rpx solid rgba(0, 108, 73, 0.12);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
@@ -870,7 +773,11 @@
|
||||
|
||||
.game-page .gg-tile-gi.is-low { background: #16a34a; }
|
||||
.game-page .gg-tile-gi.is-mid { background: #ea580c; }
|
||||
.game-page .gg-tile-gi.is-high { background: #dc2626; }
|
||||
.game-page .gg-tile-gi.is-high {
|
||||
background: #dc2626;
|
||||
box-shadow: 0 2rpx 10rpx rgba(220, 38, 38, 0.45);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* ===== 点击食物的大字科普卡 ===== */
|
||||
.game-page .gg-food-card {
|
||||
@@ -955,19 +862,34 @@
|
||||
100% { transform: translate(-50%, -60px) scale(1); opacity: 0; }
|
||||
}
|
||||
|
||||
/* ===== Footer boosters ===== */
|
||||
/* ===== Footer boosters(辅,弱化) ===== */
|
||||
.game-page .gg-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 40;
|
||||
border-radius: 80rpx 80rpx 0 0;
|
||||
border-top: 1.5rpx solid rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 -10rpx 40rpx rgba(0, 108, 73, 0.05);
|
||||
padding-top: 16rpx;
|
||||
padding-left: 32rpx;
|
||||
padding-right: 32rpx;
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
border-top: 1rpx solid rgba(0, 108, 73, 0.06);
|
||||
box-shadow: 0 -4rpx 24rpx rgba(0, 108, 73, 0.04);
|
||||
padding-top: 12rpx;
|
||||
padding-left: 24rpx;
|
||||
padding-right: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.game-page .gg-footer--sub .gg-booster-btn {
|
||||
padding: 16rpx 0;
|
||||
width: 152rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border-color: rgba(0, 108, 73, 0.08);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.04);
|
||||
}
|
||||
|
||||
.game-page .gg-footer--sub .gg-booster-label {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.game-page .gg-boosters {
|
||||
@@ -1040,6 +962,121 @@
|
||||
color: #2b3a32;
|
||||
}
|
||||
|
||||
/* ===== 高糖消除红色警示弹窗 ===== */
|
||||
.game-page .gg-high-alert-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 210;
|
||||
background: rgba(127, 29, 29, 0.45);
|
||||
backdrop-filter: blur(16rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
animation: gg-high-alert-fade-in 0.28s ease-out;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-card {
|
||||
width: 100%;
|
||||
max-width: 560rpx;
|
||||
padding: 48rpx 40rpx 44rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(165deg, #fff5f5 0%, #ffffff 42%);
|
||||
border: 4rpx solid #dc2626;
|
||||
box-shadow:
|
||||
0 0 0 8rpx rgba(220, 38, 38, 0.12),
|
||||
0 24rpx 64rpx rgba(185, 28, 28, 0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
animation: gg-high-alert-pop 0.42s cubic-bezier(0.34, 1.45, 0.64, 1);
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-icon-wrap {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #ef4444, #b91c1c);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(220, 38, 38, 0.45);
|
||||
animation: gg-high-badge-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-badge-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-badge {
|
||||
font-size: 32rpx;
|
||||
font-weight: 900;
|
||||
color: #ffffff;
|
||||
background: #dc2626;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
letter-spacing: 4rpx;
|
||||
box-shadow: 0 6rpx 16rpx rgba(220, 38, 38, 0.4);
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-badge-sub {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 900;
|
||||
color: #991b1b;
|
||||
line-height: 1.25;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-name {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
color: #dc2626;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-high-alert-tip {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #7f1d1d;
|
||||
line-height: 1.45;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@keyframes gg-high-alert-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes gg-high-alert-pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.82) translateY(24rpx);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gg-high-badge-pulse {
|
||||
0%, 100% { transform: scale(1); box-shadow: 0 12rpx 32rpx rgba(220, 38, 38, 0.45); }
|
||||
50% { transform: scale(1.06); box-shadow: 0 16rpx 40rpx rgba(220, 38, 38, 0.55); }
|
||||
}
|
||||
|
||||
/* ===== Result overlay ===== */
|
||||
.game-page .gg-result-overlay {
|
||||
position: fixed;
|
||||
@@ -1067,6 +1104,80 @@
|
||||
box-shadow: 0 30rpx 80rpx rgba(22, 29, 25, 0.18);
|
||||
}
|
||||
|
||||
.game-page .gg-result-card.is-high-clear {
|
||||
border: 4rpx solid #dc2626;
|
||||
background: linear-gradient(180deg, #fff5f5 0%, #ffffff 36%);
|
||||
box-shadow:
|
||||
0 0 0 6rpx rgba(220, 38, 38, 0.1),
|
||||
0 30rpx 80rpx rgba(185, 28, 28, 0.22);
|
||||
}
|
||||
|
||||
.game-page .gg-result-high-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.game-page .gg-result-high-badge {
|
||||
font-size: 30rpx;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
background: #dc2626;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
letter-spacing: 3rpx;
|
||||
box-shadow: 0 4rpx 14rpx rgba(220, 38, 38, 0.35);
|
||||
}
|
||||
|
||||
.game-page .gg-result-high-tag {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #b91c1c;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(254, 226, 226, 0.9);
|
||||
border: 2rpx solid rgba(220, 38, 38, 0.35);
|
||||
}
|
||||
|
||||
.game-page .gg-result-card.is-high-clear .gg-result-title {
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.game-page .gg-result-card.is-high-clear .gg-result-sub {
|
||||
color: #7f1d1d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.game-page .gg-result-stats.is-high-clear {
|
||||
background: #fef2f2;
|
||||
border: 2rpx solid rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.game-page .gg-result-stat-val.is-high-val {
|
||||
color: #dc2626;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.game-page .gg-result-inline-badge {
|
||||
font-size: 22rpx;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
background: #dc2626;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.game-page .gg-result-btn.is-high-primary {
|
||||
background: linear-gradient(135deg, #ef4444, #b91c1c);
|
||||
box-shadow: 0 10rpx 28rpx rgba(220, 38, 38, 0.35);
|
||||
}
|
||||
|
||||
.game-page .gg-result-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 900;
|
||||
|
||||
@@ -89,6 +89,22 @@
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.weekly-page .phone-gate-error {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #fef2f2;
|
||||
border: 1rpx solid rgba(220, 38, 38, 0.2);
|
||||
box-sizing: border-box;
|
||||
|
||||
text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #991b1b;
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.weekly-page .phone-gate-tip {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
|
||||
@@ -108,10 +108,25 @@
|
||||
color: var(--on-primary);
|
||||
}
|
||||
|
||||
/* ===== 居家训练(DEV 内嵌,与 user 页同源入口) ===== */
|
||||
.weekly-page .st-training-module {
|
||||
padding: 10rpx var(--st-container-margin);
|
||||
margin: -10rpx -30rpx 10rpx -30rpx;
|
||||
/* ===== 居家训练(DEV 内嵌) ===== */
|
||||
.weekly-page .st-training-section {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weekly-page .st-training-section .dev-entry-inline--weekly {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.weekly-page .st-training-section .inline-scroll {
|
||||
margin-left: -4rpx;
|
||||
margin-right: -4rpx;
|
||||
width: calc(100% + 8rpx);
|
||||
}
|
||||
|
||||
.weekly-page .st-training-section .inline-card:first-child {
|
||||
border-color: rgba(0, 108, 73, 0.22);
|
||||
background: linear-gradient(160deg, rgba(0, 108, 73, 0.08) 0%, #f4f7f5 55%);
|
||||
}
|
||||
|
||||
/* ===== Main ===== */
|
||||
@@ -123,6 +138,21 @@
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.weekly-page .st-daily-context-error {
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #fef2f2;
|
||||
border: 1rpx solid rgba(220, 38, 38, 0.22);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weekly-page .st-daily-context-error-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #991b1b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.weekly-page .st-main-spacer {
|
||||
height: 32rpx;
|
||||
}
|
||||
@@ -149,7 +179,7 @@
|
||||
|
||||
/* 糖分突袭游戏入口 */
|
||||
.weekly-page .st-game-entry {
|
||||
width: 100%;
|
||||
|
||||
margin-top: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
display: flex;
|
||||
@@ -626,6 +656,11 @@
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.weekly-page .st-meal-glass--drinks {
|
||||
background: rgba(224, 242, 254, 0.65);
|
||||
border-color: rgba(14, 165, 233, 0.15);
|
||||
}
|
||||
|
||||
.weekly-page .st-meal-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -64,10 +64,10 @@ export function parsePartialDietPlainText(text) {
|
||||
const out = {}
|
||||
if (!text) return out
|
||||
|
||||
const lineRe = /^(早餐|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
||||
const lineRe = /^(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*(.*)$/gm
|
||||
let match
|
||||
while ((match = lineRe.exec(text)) !== null) {
|
||||
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
|
||||
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[match[1]]
|
||||
const val = (match[2] || '').trim()
|
||||
if (key === 'avoid') {
|
||||
out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||
@@ -76,9 +76,9 @@ export function parsePartialDietPlainText(text) {
|
||||
}
|
||||
}
|
||||
|
||||
const tail = text.match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
const tail = text.match(/(?:^|\n)(早餐|喝的|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/)
|
||||
if (tail) {
|
||||
const key = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
||||
const key = { '早餐': 'breakfast', '喝的': 'drinks', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'avoid' }[tail[1]]
|
||||
const val = (tail[2] || '').trim()
|
||||
if (key === 'avoid') {
|
||||
if (val) out.avoid = val.split(/[、,,;;\s]+/).map((s) => s.trim()).filter(Boolean)
|
||||
|
||||
@@ -210,6 +210,10 @@ class DiagnosisLogic extends BaseLogic
|
||||
$params['diagnosis_date'] = strtotime($params['diagnosis_date']);
|
||||
}
|
||||
|
||||
if (array_key_exists('show_card', $params)) {
|
||||
$params['show_card'] = (int) $params['show_card'] === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Diagnosis::update($params);
|
||||
|
||||
// 如果是编辑,也确保患者有 TRTC 账号
|
||||
|
||||
@@ -33,6 +33,7 @@ class DiagnosisValidate extends BaseValidate
|
||||
'age' => 'require|number|between:0,150',
|
||||
'diagnosis_type' => 'require',
|
||||
'status' => 'in:0,1',
|
||||
'show_card' => 'in:0,1',
|
||||
'create_source' => 'max:32',
|
||||
'current_medications' => 'max:2000',
|
||||
'tongue_images' => 'array',
|
||||
@@ -78,7 +79,7 @@ class DiagnosisValidate extends BaseValidate
|
||||
|
||||
public function sceneEdit()
|
||||
{
|
||||
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status', 'create_source']);
|
||||
return $this->only(['id', 'patient_name', 'id_card', 'phone', 'gender', 'age', 'diagnosis_date', 'diagnosis_type', 'syndrome_type', 'marital_status', 'height', 'weight', 'region', 'systolic_pressure', 'diastolic_pressure', 'fasting_blood_sugar', 'diabetes_discovery_year', 'local_hospital_diagnosis', 'local_hospital_name', 'past_history', 'symptoms', 'tongue_coating', 'pulse', 'treatment_principle', 'prescription', 'doctor_advice', 'remark', 'current_medications', 'status', 'show_card', 'create_source']);
|
||||
}
|
||||
|
||||
public function sceneId()
|
||||
|
||||
@@ -457,6 +457,10 @@ class TcmController extends BaseApiController
|
||||
return ['ok' => false, 'error' => '诊单不存在'];
|
||||
}
|
||||
|
||||
if ((int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return ['ok' => false, 'error' => '该就诊卡已在统计端隐藏'];
|
||||
}
|
||||
|
||||
// 通过 diagnosis_view_records 校验当前小程序用户与该诊单的归属关系
|
||||
// 该表是「就诊卡列表」的来源(参考 DiagnosisLogic::getCardList)
|
||||
$owned = \think\facade\Db::name('diagnosis_view_records')
|
||||
|
||||
@@ -499,8 +499,10 @@ class DailyDietAiLogic
|
||||
3. 严禁:白馒头、油条、粘豆包、糯米饭、西瓜、含糖饮料、白稀饭(杂面窝头、玉米碴粥除外)。
|
||||
4. 低 GI 优先;农村家常;禁止轻食/沙拉/藜麦等词。
|
||||
5. lunch 字段写法:蛋白菜放前面,淀粉(如有)放最后且注明「小半碗/一个」。
|
||||
6. 只输出 JSON:
|
||||
{"breakfast":"...","lunch":"...","dinner":"...","tips":"...","avoid":["...","..."]}
|
||||
6. 早餐必须包含「驼奶粉」(温水冲服,适量),可与粥、蛋、菜并列写。
|
||||
7. drinks 单独写全天喝什么:温开水为主;可写驼奶粉冲饮时间;严禁含糖饮料、果汁、酒。
|
||||
8. 只输出 JSON:
|
||||
{"breakfast":"...","drinks":"...","lunch":"...","dinner":"...","tips":"...","avoid":["...","..."]}
|
||||
SYS;
|
||||
|
||||
$user = sprintf(
|
||||
@@ -544,9 +546,11 @@ SYS;
|
||||
2. 淀粉(粥/饭/面/饼/窝头/薯/南瓜)每餐最多 1 种,早中晚互不相同。
|
||||
3. 严禁白馒头、油条、粘豆包、糯米饭、西瓜、含糖饮料。
|
||||
4. 农村家常饭,禁止轻食/沙拉/藜麦。
|
||||
5. 早餐必须含驼奶粉(温水冲服);喝的单独一行。
|
||||
|
||||
严格按下面 5 行格式输出,不要 JSON、不要 markdown、不要多余解释:
|
||||
早餐:(具体食物)
|
||||
严格按下面 6 行格式输出,不要 JSON、不要 markdown、不要多余解释:
|
||||
早餐:(具体食物,须含驼奶粉)
|
||||
喝的:(温开水、驼奶粉冲饮安排等,忌甜饮)
|
||||
午餐:(具体食物,蛋白放前)
|
||||
晚餐:(具体食物)
|
||||
提示:(一句土话提醒)
|
||||
@@ -593,6 +597,7 @@ SYS;
|
||||
|
||||
$map = [
|
||||
'breakfast' => null,
|
||||
'drinks' => null,
|
||||
'lunch' => null,
|
||||
'dinner' => null,
|
||||
'tips' => null,
|
||||
@@ -602,6 +607,9 @@ SYS;
|
||||
if (preg_match('/早餐[::]\s*(.+)$/mu', $text, $m)) {
|
||||
$map['breakfast'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
|
||||
}
|
||||
if (preg_match('/喝的[::]\s*(.+)$/mu', $text, $m)) {
|
||||
$map['drinks'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
|
||||
}
|
||||
if (preg_match('/午餐[::]\s*(.+)$/mu', $text, $m)) {
|
||||
$map['lunch'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]);
|
||||
}
|
||||
@@ -623,6 +631,7 @@ SYS;
|
||||
|
||||
$out = [
|
||||
'breakfast' => (string) $map['breakfast'],
|
||||
'drinks' => (string) ($map['drinks'] ?? ''),
|
||||
'lunch' => (string) ($map['lunch'] ?? ''),
|
||||
'dinner' => (string) ($map['dinner'] ?? ''),
|
||||
'tips' => (string) ($map['tips'] ?? ''),
|
||||
@@ -632,6 +641,29 @@ SYS;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品要求:早餐含驼奶粉,并单独给出「喝的」
|
||||
*
|
||||
* @param array<string, mixed> $plan
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function applyDietProductDefaults(array $plan): array
|
||||
{
|
||||
$breakfast = trim((string) ($plan['breakfast'] ?? ''));
|
||||
if ($breakfast !== '' && mb_strpos($breakfast, '驼奶粉') === false && mb_strpos($breakfast, '驼奶') === false) {
|
||||
$plan['breakfast'] = '驼奶粉(温水冲服)、' . $breakfast;
|
||||
}
|
||||
|
||||
$drinks = trim((string) ($plan['drinks'] ?? ''));
|
||||
if ($drinks === '') {
|
||||
$plan['drinks'] = '白天多喝温开水;早餐按量冲驼奶粉,别喝甜饮料、果汁。';
|
||||
} elseif (mb_strpos($drinks, '驼奶') === false) {
|
||||
$plan['drinks'] = $drinks . ';早餐已安排驼奶粉';
|
||||
}
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 AI 三餐方案;校验未过也保留 AI 文案(避免流式展示后被规则模板覆盖)
|
||||
*
|
||||
@@ -640,6 +672,7 @@ SYS;
|
||||
*/
|
||||
private static function finalizeAiRecommendPlan(array $parsed): array
|
||||
{
|
||||
$parsed = self::applyDietProductDefaults($parsed);
|
||||
$checked = DietMealValidator::validateAndNormalize($parsed);
|
||||
$data = $checked['plan'];
|
||||
$data['source'] = 'ai';
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace app\api\logic\tcm;
|
||||
|
||||
use app\common\model\tcm\DailyFamilyLike;
|
||||
use app\common\model\tcm\DailyShareInvite;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
|
||||
/**
|
||||
* 家人点赞(邀请观看页 → 患者日常页展示)
|
||||
@@ -204,6 +205,11 @@ class DailyFamilyLikeLogic
|
||||
return self::setError('无权查看该诊单') ? false : false;
|
||||
}
|
||||
|
||||
$diagnosis = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->field('show_card')->find();
|
||||
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
||||
}
|
||||
|
||||
$likeDate = date('Y-m-d');
|
||||
$count = self::countToday($diagnosisId, $likeDate);
|
||||
|
||||
|
||||
@@ -49,6 +49,13 @@ class DailyGamifyLogic
|
||||
if (!$owned) {
|
||||
return self::setError('无权操作该诊单') ? false : false;
|
||||
}
|
||||
$diagnosis = \app\common\model\tcm\Diagnosis::where('id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->field('show_card')
|
||||
->find();
|
||||
if (!$diagnosis || (int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace app\api\logic\tcm;
|
||||
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\common\model\DiagnosisViewRecord;
|
||||
use app\common\model\tcm\Diagnosis;
|
||||
use app\common\model\user\User;
|
||||
|
||||
/**
|
||||
@@ -74,19 +76,38 @@ class DailyPhoneLogic
|
||||
|
||||
if (!empty($cardList)) {
|
||||
$card = $cardList[0];
|
||||
return self::buildContextOk($card, $mobile, false, false);
|
||||
}
|
||||
|
||||
// 该手机号已在诊单库建档:关联查看记录,禁止重复创建轻量档案
|
||||
$existing = Diagnosis::where('phone', $mobile)
|
||||
->where('delete_time', null)
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
if ($existing && (int) ($existing['show_card'] ?? 1) !== 1) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'ok' => false,
|
||||
'need_mobile' => false,
|
||||
'created' => false,
|
||||
'diagnosis_id' => (int) $card['id'],
|
||||
'patient_id' => (int) $card['patient_id'],
|
||||
'patient_name' => (string) ($card['patient_name'] ?? ''),
|
||||
'gender' => (int) ($card['gender'] ?? 0),
|
||||
'age' => (int) ($card['age'] ?? 0),
|
||||
'mobile' => $mobile,
|
||||
'create_source' => (string) ($card['create_source'] ?? ''),
|
||||
'error' => '该手机号对应就诊卡因隐私设置已隐藏,请联系医生',
|
||||
];
|
||||
}
|
||||
if ($existing) {
|
||||
if (!self::ensureUserViewRecord($userId, (int) $existing['id'], (int) $existing['patient_id'])) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'need_mobile' => false,
|
||||
'error' => '该手机号已建档,暂无法关联到当前账号',
|
||||
];
|
||||
}
|
||||
return self::buildContextOk([
|
||||
'id' => (int) $existing['id'],
|
||||
'patient_id' => (int) $existing['patient_id'],
|
||||
'patient_name' => (string) ($existing['patient_name'] ?? ''),
|
||||
'gender' => (int) ($existing['gender'] ?? 0),
|
||||
'age' => (int) ($existing['age'] ?? 0),
|
||||
'create_source' => (string) ($existing['create_source'] ?? ''),
|
||||
], $mobile, false, true);
|
||||
}
|
||||
|
||||
$displayName = trim((string) (($user['real_name'] ?? '') ?: ($user['nickname'] ?? '')));
|
||||
if ($displayName === '') {
|
||||
@@ -114,17 +135,66 @@ class DailyPhoneLogic
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'need_mobile' => false,
|
||||
'created' => true,
|
||||
'diagnosis_id' => (int) $result['id'],
|
||||
return self::buildContextOk([
|
||||
'id' => (int) $result['id'],
|
||||
'patient_id' => (int) $result['patient_id'],
|
||||
'patient_name' => $displayName,
|
||||
'gender' => $gender,
|
||||
'age' => max(0, (int) ($user['age'] ?? 0)),
|
||||
'mobile' => $mobile,
|
||||
'create_source' => self::CREATE_SOURCE_MNP_DAILY,
|
||||
], $mobile, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{id:int,patient_id:int,patient_name?:string,gender?:int,age?:int,create_source?:string} $card
|
||||
*/
|
||||
private static function buildContextOk(array $card, string $mobile, bool $created, bool $linkedExisting): array
|
||||
{
|
||||
return [
|
||||
'ok' => true,
|
||||
'need_mobile' => false,
|
||||
'created' => $created,
|
||||
'linked_existing' => $linkedExisting,
|
||||
'diagnosis_id' => (int) $card['id'],
|
||||
'patient_id' => (int) $card['patient_id'],
|
||||
'patient_name' => (string) ($card['patient_name'] ?? ''),
|
||||
'gender' => (int) ($card['gender'] ?? 0),
|
||||
'age' => (int) ($card['age'] ?? 0),
|
||||
'mobile' => $mobile,
|
||||
'create_source' => (string) ($card['create_source'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/** 将已有诊单挂到当前小程序用户(diagnosis_view_records) */
|
||||
private static function ensureUserViewRecord(int $userId, int $diagnosisId, int $patientId): bool
|
||||
{
|
||||
if ($userId <= 0 || $diagnosisId <= 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
$exists = DiagnosisViewRecord::where('user_id', $userId)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('delete_time', null)
|
||||
->find();
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
$now = time();
|
||||
DiagnosisViewRecord::create([
|
||||
'user_id' => $userId,
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'patient_id' => $patientId,
|
||||
'share_user_id' => 0,
|
||||
'view_count' => 1,
|
||||
'first_view_time' => $now,
|
||||
'last_view_time' => $now,
|
||||
'is_confirmed' => 0,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ class DailyShareLogic
|
||||
if (!$diagnosis) {
|
||||
return self::setError('诊单不存在') ? false : false;
|
||||
}
|
||||
if ((int) ($diagnosis['show_card'] ?? 1) !== 1) {
|
||||
return self::setError('该就诊卡已在统计端隐藏') ? false : false;
|
||||
}
|
||||
|
||||
$inviteDate = date('Y-m-d');
|
||||
$code = self::generateUniqueCode();
|
||||
|
||||
@@ -114,6 +114,7 @@ class DietMealValidator
|
||||
|
||||
$normalized = [
|
||||
'breakfast' => $breakfast,
|
||||
'drinks' => trim((string) ($plan['drinks'] ?? '')),
|
||||
'lunch' => $lunch,
|
||||
'dinner' => $dinner,
|
||||
'tips' => $tips,
|
||||
|
||||
@@ -18,7 +18,7 @@ class GiKnowledge
|
||||
'蔬菜类' => ['魔芋', '粟米', '大白菜', '黄瓜', '芹菜', '茄子', '青椒', '海带', '金针菇', '香菇', '菠菜', '番茄', '豆芽', '芦笋', '花椰菜', '洋葱', '生菜'],
|
||||
'豆类' => ['黄豆', '眉豆', '鸡心豆', '豆腐', '豆角', '绿豆', '扁豆', '四季豆'],
|
||||
'水果类' => ['苹果', '水梨', '橙子', '桃', '提子', '沙田柚', '雪梨', '车厘子', '柚子', '草莓', '樱桃', '金桔', '葡萄'],
|
||||
'奶类' => ['牛奶', '低脂奶', '脱脂奶', '低脂乳酪', '红茶', '优格', '无糖豆浆'],
|
||||
'奶类' => ['牛奶', '低脂奶', '脱脂奶', '驼奶粉', '驼奶', '低脂乳酪', '红茶', '优格', '无糖豆浆'],
|
||||
],
|
||||
self::LEVEL_MEDIUM => [
|
||||
'主食类' => ['红米饭', '糙米饭', '西米', '乌冬面', '面包', '麦片', '番薯', '芋头'],
|
||||
@@ -36,40 +36,46 @@ class GiKnowledge
|
||||
],
|
||||
];
|
||||
|
||||
/** @var list<array{breakfast:string,lunch:string,dinner:string,tips:string}> */
|
||||
/** @var list<array{breakfast:string,drinks:string,lunch:string,dinner:string,tips:string}> */
|
||||
private static array $mealTemplates = [
|
||||
[
|
||||
'breakfast' => '玉米碴子粥、煮鸡蛋、拌黄瓜',
|
||||
'breakfast' => '驼奶粉(温水冲服)、玉米碴子粥、煮鸡蛋、拌黄瓜',
|
||||
'drinks' => '白天多喝温开水;早餐冲驼奶粉,别喝甜饮料、果汁。',
|
||||
'lunch' => '清炖鲤鱼、豆腐炖白菜、清炒豆角、米饭小半碗',
|
||||
'dinner' => '玉米面饼一个、瘦肉丝、凉拌茄子',
|
||||
'tips' => '中午鱼豆肉吃足;早中晚淀粉别重复,一顿一种主食就够。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '小米粥、煮鸡蛋、小碟咸菜(少盐)',
|
||||
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、小碟咸菜(少盐)',
|
||||
'drinks' => '温开水为主;驼奶粉跟早餐一起吃,下午也记得喝水。',
|
||||
'lunch' => '去皮炖鸡肉、芹菜炒豆干、木耳炒鸡蛋',
|
||||
'dinner' => '蒸红薯(小半块)、清炒菠菜、豆腐汤',
|
||||
'tips' => '午餐最讲究蛋白;晚上才吃薯,别跟中午再配大米饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '豆腐脑(少卤)、鸡蛋、一个苹果',
|
||||
'breakfast' => '驼奶粉(温水冲服)、豆腐脑(少卤)、煮鸡蛋',
|
||||
'drinks' => '早餐饮驼奶粉;白天喝白开水,别碰含糖饮料。',
|
||||
'lunch' => '清炖鸡肉、炖芸豆、番茄炒蛋、贴饼子一个',
|
||||
'dinner' => '挂面一小碗、清蒸小鱼、拌海带丝',
|
||||
'tips' => '中午饼配肉豆,蛋白管够;面条放晚上,别三顿都是粥饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '高粱米粥、茶叶蛋、拌萝卜丝',
|
||||
'breakfast' => '驼奶粉(温水冲服)、高粱米粥、茶叶蛋、拌萝卜丝',
|
||||
'drinks' => '温开水;早餐驼奶粉按说明冲,别加糖。',
|
||||
'lunch' => '炖牛肉(瘦)、豆腐炒韭菜、清炒油菜、杂豆饭小半碗',
|
||||
'dinner' => '蒸南瓜(小半碗)、鸡蛋羹、拍黄瓜',
|
||||
'tips' => '中午肉蛋豆要多吃;南瓜算晚饭主食,别再加馒头稀饭。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '棒子面粥、水煮蛋、大拌菜(少油)',
|
||||
'breakfast' => '驼奶粉(温水冲服)、棒子面粥、水煮蛋、大拌菜(少油)',
|
||||
'drinks' => '多喝温开水;驼奶粉放早餐,忌可乐汽水果汁。',
|
||||
'lunch' => '清炖鲫鱼、番茄炒蛋、炖扁豆、糙米饭小半碗',
|
||||
'dinner' => '玉米面饼一个、清炒豆芽、豆腐脑',
|
||||
'tips' => '午饭鱼蛋豆齐上;玉米面放晚上,跟中午米饭分开。',
|
||||
],
|
||||
[
|
||||
'breakfast' => '小米粥、煮鸡蛋、拌生菜',
|
||||
'breakfast' => '驼奶粉(温水冲服)、小米粥、煮鸡蛋、拌生菜',
|
||||
'drinks' => '白天小口多次喝温水;早餐冲好驼奶粉再吃饭。',
|
||||
'lunch' => '白切鸡(去皮)、炖豆腐、清炒豆角、二米饭小半碗',
|
||||
'dinner' => '杂面馒头一个、瘦肉炒芹菜、凉拌黄瓜',
|
||||
'tips' => '中午蛋白打主力;三顿别都喝粥吃面,一顿一种淀粉。',
|
||||
@@ -92,7 +98,8 @@ class GiKnowledge
|
||||
private static array $aliases = [
|
||||
'棒子面' => '荞麦面', '玉米碴' => '粟米', '地瓜' => '番薯', '红薯' => '番薯',
|
||||
'洋芋' => '芋头', '土豆' => '芋头', '窝头' => '通心粉', '贴饼子' => '通心粉',
|
||||
'二米饭' => '红米饭', '稀饭' => '白饭', '白稀饭' => '白饭', '糖糕' => '麦芽糖',
|
||||
'二米饭' => '红米饭', '稀饭' => '白饭', '白稀饭' => '白饭', '糖糕' => '麦芽糖',
|
||||
'驼奶' => '驼奶粉',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -132,6 +139,8 @@ class GiKnowledge
|
||||
public static function ruralMealHints(): string
|
||||
{
|
||||
return implode("\n", [
|
||||
'【早餐】须安排驼奶粉温水冲服(低 GI 奶类,稳血糖),可与粥、蛋、菜同餐。',
|
||||
'【喝的】单独说明:温开水为主;早餐驼奶粉;严禁含糖饮料、果汁、酒。',
|
||||
'【午餐】蛋白质要最高:至少 2 样高蛋白(鱼/鸡鸭/瘦肉/蛋/豆腐/豆干),肉蛋豆是午饭主角。',
|
||||
'【淀粉不重复】粥/饭/面/饼/窝头/红薯/土豆/玉米/南瓜等算淀粉;早中晚各最多 1 种,且三顿不能同一种、不要顿顿大主食。',
|
||||
'推荐蛋白:鸡蛋、豆腐、豆干、自家鸡鸭蛋、瘦肉、鲫鱼鲤鱼;',
|
||||
@@ -177,6 +186,7 @@ class GiKnowledge
|
||||
|
||||
$plan = [
|
||||
'breakfast' => $meal['breakfast'],
|
||||
'drinks' => $meal['drinks'] ?? '白天多喝温开水;早餐冲驼奶粉,别喝甜饮。',
|
||||
'lunch' => $meal['lunch'],
|
||||
'dinner' => $meal['dinner'],
|
||||
'tips' => $meal['tips'],
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"brandkit": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brandkit/SKILL.md",
|
||||
"computedHash": "735007879f5110aeae1e981a53fe74a12afde70347f9773dacd50e4697a091bf"
|
||||
},
|
||||
"design-taste-frontend": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill/SKILL.md",
|
||||
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
|
||||
},
|
||||
"design-taste-frontend-v1": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill-v1/SKILL.md",
|
||||
"computedHash": "8d59c96751feacf88f341a43151eae6f4fd748978884c4552293b8841573d776"
|
||||
},
|
||||
"full-output-enforcement": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/output-skill/SKILL.md",
|
||||
"computedHash": "524f7c7950c3fde726101613f630140411c7d080e63be718687f4b38f919be81"
|
||||
},
|
||||
"gpt-taste": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/gpt-tasteskill/SKILL.md",
|
||||
"computedHash": "7bd1bfdda0e51016ca222693401a65fc0dd9bf9487e0bd5b1740ba510f6f963a"
|
||||
},
|
||||
"high-end-visual-design": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/soft-skill/SKILL.md",
|
||||
"computedHash": "f730e4132775f13eea19e3dc39afc6bb453cfc0498872b127ad8f0d47cfd802d"
|
||||
},
|
||||
"image-to-code": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/image-to-code-skill/SKILL.md",
|
||||
"computedHash": "3d2ac14d956fdafc5500d2108b90895e134d847f9f199e7f1d18bf1f814cf7c8"
|
||||
},
|
||||
"imagegen-frontend-mobile": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/imagegen-frontend-mobile/SKILL.md",
|
||||
"computedHash": "7c1d1dc59cd0df4063b8621af543bf5dea1d576b261507e17b0323c69680b048"
|
||||
},
|
||||
"imagegen-frontend-web": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/imagegen-frontend-web/SKILL.md",
|
||||
"computedHash": "27255c7c50181e58add156b2aa2bd5325285b85f04027e4c8b73742d98d7be0a"
|
||||
},
|
||||
"industrial-brutalist-ui": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brutalist-skill/SKILL.md",
|
||||
"computedHash": "e66e5a2a8b73cef79ab6546459c665e8b983645689c5f3772f7eed8b9f1e8e0e"
|
||||
},
|
||||
"minimalist-ui": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/minimalist-skill/SKILL.md",
|
||||
"computedHash": "7e88897e0371184e97a82b8d9ef080323c13f21eb067b58ef71eb05173a08ac2"
|
||||
},
|
||||
"redesign-existing-projects": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/redesign-skill/SKILL.md",
|
||||
"computedHash": "47eced3e960a2c961d3cbee11fa1216264f589344b586a5cbd03967cd6b9c54b"
|
||||
},
|
||||
"stitch-design-taste": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/stitch-skill/SKILL.md",
|
||||
"computedHash": "acf2921a7e50c06e637f072ba52e36a994cbb44ecbe6e31435bc113b894f5499"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user