This commit is contained in:
Your Name
2026-06-02 18:33:25 +08:00
parent 62f74888f3
commit 15e9c486f9
11 changed files with 731 additions and 674 deletions
@@ -1,5 +1,45 @@
<template>
<view v-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
<!-- 页面内嵌模块 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>
<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>
<text class="inline-item-title">{{ item.title }}</text>
<text class="inline-item-sub">{{ item.sub }}</text>
</view>
</view>
</view>
<view v-else-if="visible" class="dev-entry-wrap" :style="{ bottom: props.bottom + 'rpx' }">
<!-- 展开后的菜单卡片(从下往上动画) -->
<view v-if="expanded" class="menu-list" @click.stop>
<view class="menu-item" @click="goto('/training/pages/dumbbell')">
@@ -86,11 +126,22 @@
<script setup lang="ts">
import { ref } from 'vue'
const trainingItems = [
{ 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
const props = withDefaults(
defineProps<{
/** 悬浮按钮距底部 rpx(仅非 inline */
bottom?: number
/** 内嵌到页面主内容区,非 fixed 悬浮 */
inline?: boolean
}>(),
{ bottom: 200 },
{ bottom: 200, inline: false },
)
const isDevMode = (): boolean => {
@@ -449,4 +500,121 @@ $brand-light: #34d399;
line-height: 1;
margin-left: 4rpx;
}
.menu-icon--metro-inner {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4rpx;
width: 100%;
height: 100%;
}
/* ============================================================
* 内嵌模块(weekly 等)
* ============================================================ */
.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;
}
.inline-head-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.inline-title {
font-size: 32rpx;
font-weight: 700;
color: #0f172a;
line-height: 1.25;
}
.inline-sub {
font-size: 24rpx;
font-weight: 500;
color: #64748b;
line-height: 1.35;
}
.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-grid {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: space-between;
gap: 8rpx;
width: 100%;
}
.inline-item {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
padding: 14rpx 4rpx 12rpx;
border-radius: 20rpx;
background: #f0fdf4;
border: 1rpx solid rgba(0, 108, 73, 0.1);
box-sizing: border-box;
&:active {
transform: scale(0.97);
opacity: 0.9;
}
.menu-icon {
width: 56rpx;
height: 56rpx;
margin-bottom: 8rpx;
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-item-sub {
margin-top: 2rpx;
font-size: 18rpx;
color: #94a3b8;
text-align: center;
line-height: 1.2;
width: 100%;
word-break: break-all;
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
import App from './App'
var baseUrl ='https://css.zhenyangtang.com.cn/';
var baseUrl ='https://admin.zhenyangtang.com.cn/';
function joinApiUrl(base, path) {
const b = String(base || '').replace(/\/+$/, '')
@@ -58,7 +58,7 @@ function onImageError() {
top: 0;
width: 100%;
height: 100%;
font-size: calc(var(--tile-emoji-size, 48px) * 0.76);
font-size: calc(var(--tile-emoji-size, 48px) * 0.6);
}
.food-tile-icon--tile .food-tile-icon__emoji,
@@ -66,8 +66,8 @@ function onImageError() {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 70%;
height: 70%;
width: 58%;
height: 58%;
z-index: 2;
}
@@ -8,12 +8,12 @@
mode="aspectFit"
@error="onImageError"
/>
<text
<!-- <text
v-else
class="tj-icon-fallback"
:class="[`tj-icon-fallback--${size}`]"
:style="{ color }"
>{{ fallbackGlyph }}</text>
>{{ fallbackGlyph }}</text> -->
</view>
</template>
@@ -165,6 +165,10 @@ function onImageError() {
width: 100%;
height: 100%;
}
/* Lucide send 路径重心偏右上,微调使圆形按钮内视觉居中 */
.tj-icon--send {
transform: translate(-10%, 10%);
}
.tj-icon-fallback {
display: block;
line-height: 1;
@@ -28,6 +28,54 @@ export function formatEatLabel(food) {
return `若食用 +${getEatVal(food)}`
}
/** 含糖等级简称:低糖 / 中糖 / 高糖 */
export const GI_SHORT = { low: '低糖', mid: '中糖', high: '高糖' }
/** 含糖等级对应的提示主色(绿 / 橙 / 红) */
export const GI_COLOR = { low: '#16a34a', mid: '#ea580c', high: '#dc2626' }
/** 部分代表食物的专属科普文案(一句话,便于老人记忆) */
const FOOD_TIP_OVERRIDE = {
whiteRice: '精制主食,升糖快,建议小份',
stickyRice: '糯米黏软,升糖很快,要少吃',
whiteBread: '精制面食,升糖快',
mantou: '白面馒头,升糖快,配菜一起吃更稳',
youtiao: '油炸又高糖,少吃为好',
sugar: '纯糖,升糖最快,尽量不吃',
maltose: '糖分很高,升糖快',
soda: '含糖饮料,升糖很快,不建议喝',
orangeJuice: '果汁含糖高,吃整果更好',
watermelon: '很甜,升糖快,一次别吃太多',
donut: '又甜又油,升糖高',
banana: '偏甜水果,适量吃',
sweetPotato: '粗粮但有糖,可代替部分主食',
corn: '粗粮主食,适量吃',
honey: '本质是糖,别当健康品多吃',
broccoli: '新鲜蔬菜,升糖很慢,可多吃',
cucumber: '清爽蔬菜,几乎不升糖',
lettuce: '绿叶菜,升糖很低,可多吃',
celery: '高纤维蔬菜,升糖低',
apple: '带皮整果,升糖较慢',
milk: '低糖,含蛋白,适量喝好',
fish: '优质蛋白,升糖低',
beef: '蛋白为主,升糖低'
}
/** 含糖等级通用科普文案 */
const GI_TIP_DEFAULT = {
low: '升糖慢,相对放心,可常吃',
mid: '升糖中等,注意分量',
high: '含糖高、升糖快,要少吃'
}
/** 点击食物时展示的一句话科普(认知教育用) */
export function getFoodTip(food) {
if (!food) return ''
if (food.tip) return food.tip
if (FOOD_TIP_OVERRIDE[food.key]) return FOOD_TIP_OVERRIDE[food.key]
return GI_TIP_DEFAULT[food.gi] || ''
}
const RAW_FOODS = [
// 低 GI
{ key: 'lettuce', icon: '🥬', name: '生菜', gi: 'low', val: -3, color: '#8BC34A' },
+16 -15
View File
@@ -1,5 +1,6 @@
/**
* 关卡配置:难度随关卡递增,驱动“再闯一关”动力
* 关卡配置:面向 45+ 的认知练习,节奏放慢、无失败惩罚。
* 每组目标只比上一组略增,步数充裕(仅展示,不会因耗尽判负)。
*/
export function getLevelConfig(levelId) {
const lv = Math.max(1, Math.min(999, Number(levelId) || 1))
@@ -7,26 +8,26 @@ export function getLevelConfig(levelId) {
return {
id: lv,
moves: Math.max(14, 26 - tier * 2 - (lv % 3)),
goalTargets: [8 + tier * 2 + (lv % 2), 10 + tier * 2 + Math.floor(lv / 4)],
scoreTarget: 2800 + lv * 450,
// 步数充裕:仅作展示,认全目标即可通关
moves: Math.max(40, 60 - tier * 2),
// 认识目标平缓增长,避免给老人压力
goalTargets: [6 + tier, 8 + tier],
scoreTarget: 2000 + lv * 300,
startGlucose: 50,
/** 餐后消化倒计时(秒):归零时按棋盘上剩余食物升糖 */
digestIntervalSec: Math.max(5, 10 - tier),
digestMultiplier: 1 + tier * 0.12,
boosters: {
insulin: Math.max(1, 3 - Math.floor(tier / 2)),
fiber: tier >= 3 ? 0 : 1,
meal: Math.max(2, 5 - tier)
insulin: 3,
fiber: 1,
meal: 5
}
}
}
/** 根据本局表现计算 1~3 星 */
export function calcLevelStars({ movesLeft, glucoseLevel, score, levelConfig }) {
/** 根据本局表现计算 1~3 星:完成即得星,步数越省星越多(不依赖血糖) */
export function calcLevelStars({ movesLeft, levelConfig }) {
const totalMoves = levelConfig?.moves || 1
const ratio = totalMoves > 0 ? movesLeft / totalMoves : 0
let stars = 1
const stable = glucoseLevel >= 30 && glucoseLevel <= 70
if (stable && movesLeft >= 2) stars = 2
if (stars >= 2 && score >= (levelConfig?.scoreTarget ?? 3000) && movesLeft >= 4) stars = 3
if (ratio >= 0.3) stars = 2
if (ratio >= 0.55) stars = 3
return stars
}
+118 -261
View File
@@ -42,15 +42,6 @@
>
<text v-if="p.char">{{ p.char }}</text>
</view>
<view
v-for="tt in foodTooltips"
:key="tt.id"
class="food-tooltip"
:style="{ left: tt.x + 'px', top: tt.y + 'px' }"
>
<text>{{ tt.name }}</text>
<text class="gi-badge" :style="{ backgroundColor: tt.color }">{{ tt.giLabel }}</text>
</view>
<view
v-for="ft in floatingVals"
:key="ft.id"
@@ -67,15 +58,19 @@
</view>
</view>
<!-- 心理激励提示目标/步数等非升糖类 -->
<view v-if="urgeToast.show" class="gg-urge-toast" :class="'is-' + urgeToast.type">
<text>{{ urgeToast.text }}</text>
<!-- 点击食物的大字科普卡名称 + 含糖等级 + 一句话说明 -->
<view v-if="foodCard" class="gg-food-card" :class="'is-' + foodCard.gi">
<view class="gg-food-card-head">
<text class="gg-food-card-name">{{ foodCard.name }}</text>
<text class="gg-food-card-gi" :style="{ backgroundColor: foodCard.color }">{{ foodCard.giShort }}</text>
</view>
<text class="gg-food-card-tip">{{ foodCard.tip }}</text>
</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>
<text class="gg-result-title">{{ gamePhase === 'won' ? '学会啦' : '再认一认' }}</text>
<view v-if="gamePhase === 'won'" class="gg-result-stars">
<text
v-for="i in 3"
@@ -96,9 +91,9 @@
v-if="gamePhase === 'won'"
class="gg-result-btn is-primary"
@tap="goNextLevel"
>下一</view>
>下一</view>
<view class="gg-result-btn" :class="{ 'is-primary': gamePhase !== 'won' }" @tap="restartLevel">
{{ gamePhase === 'won' ? '再玩本关' : '再试一次' }}
再认一遍
</view>
<view class="gg-result-btn" @tap="goBack">返回</view>
</view>
@@ -120,21 +115,16 @@
hover-class="gg-hud-btn--pressed"
@tap="onSoundToggle"
>
<TongjiIcon name="volume" size="md" :color="sfxEnabled ? '#006c49' : '#cbd5e1'" />
</view>
<view v-if="winStreak >= 2" class="gg-hud-streak" hover-class="gg-hud-btn--pressed">
<TongjiIcon name="flame" size="sm" color="#ea580c" />
<text>{{ winStreak }}</text>
<TongjiIcon name="volume" size="md" :color="sfxEnabled ? '#006c49' : '#94a3b8'" />
</view>
</view>
<!-- 绝对居中的关卡与得分 -->
<view class="gg-hud-level-center">
<text class="gg-hud-level-text"> {{ levelConfig.id }} </text>
<text class="gg-hud-level-text"> {{ levelConfig.id }} </text>
<view class="gg-hud-score" :class="{ 'is-pop': scoreAnimating }">
<text class="gg-hud-score-val">{{ scoreText }}</text>
<text class="gg-hud-score-unit"></text>
<text v-if="scoreMultiplier > 1" class="gg-hud-mult">×{{ scoreMultiplier }}</text>
</view>
</view>
</view>
@@ -174,8 +164,8 @@
<!-- 垂直分割线 1 -->
<view class="gg-dash-divider" />
<!-- 中间步数看板 -->
<view class="gg-dash-moves" :class="{ 'is-warn': movesLeft <= 3 && gamePhase === 'playing' }">
<!-- 中间步数看板仅展示无失败惩罚 -->
<view class="gg-dash-moves">
<text class="gg-moves-label">步数</text>
<text class="gg-moves-val">{{ movesLeft }}</text>
</view>
@@ -187,7 +177,7 @@
<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-title">认识这些食物</text>
<text class="gg-mission-pct">{{ goalProgressPct }}%</text>
</view>
<view v-if="dailyStreak >= 2" class="gg-mission-streak">
@@ -220,19 +210,11 @@
</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
class="gg-digest-pill"
:class="{ 'is-warn': digestCountdown <= 3 }"
>
<TongjiIcon name="info" size="sm" color="#ffffff" />
<text class="gg-digest-num">{{ digestCountdown }}</text>
<text class="gg-digest-label">秒后升糖</text>
<view class="gg-hint-tag"></view>
<text class="gg-board-hint-text">点一下食物看它含糖高不高</text>
</view>
</view>
</view>
@@ -242,14 +224,6 @@
<view class="gg-board-stack" :style="boardStackStyle">
<view class="gg-board-zone">
<view
v-if="gamePhase === 'playing'"
class="gg-digest-urge-bar"
:class="{ 'is-show': digestUrgent }"
>
<TongjiIcon name="alert-circle" size="sm" color="#ffffff" />
<text class="gg-digest-urge-text">餐后升糖倒计时快消除棋盘运动降糖</text>
</view>
<view v-if="hintTipVisible" class="gg-no-move-tip">
<TongjiIcon name="alert-circle" size="sm" color="#ea580c" />
<text class="gg-no-move-tip-text">{{ hintTipText }}</text>
@@ -303,6 +277,7 @@
@touchcancel.stop="onTileTouchCancel"
>
<FoodTileIcon :food="cell.tile.type" size="tile" />
<view class="gg-tile-gi" :class="'is-' + cell.tile.type.gi">{{ giChar(cell.tile.type.gi) }}</view>
</view>
</view>
</view>
@@ -339,7 +314,7 @@ 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, formatEatLabel, getClearVal } from '../data/gameFoodLibrary.js'
import { FOOD_LIBRARY, cloneFoodType, getClearVal, getFoodTip, 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'
@@ -356,25 +331,24 @@ const {
warmUp: warmUpSfx,
toggleEnabled: toggleSfx
} = useGameSfx()
const { burstParticles, ripples, flash, comboBanner, spawnBurst, spawnRipple, triggerShake, triggerFlash, showComboBanner } = useGameFx()
const { burstParticles, ripples, flash, comboBanner, spawnBurst, spawnRipple, triggerFlash, showComboBanner } = useGameFx()
const { progress: gameProgress, touchDailyPlay, recordWin, recordLoss } = useGameProgress()
const levelConfig = ref(getLevelConfig(1))
const resultStars = ref(0)
const resultStats = ref([])
const sessionMaxCombo = ref(0)
const urgeToast = ref({ show: false, text: '', type: 'warm' })
const menuHeight = ref(32)
const menuTop = ref(0)
let urgeTimer = null
let lastUrgeKey = ''
const ROWS = 7
const COLS = 6
const ROWS = 6
const COLS = 4
const ROW_AXES = Array.from({ length: ROWS }, (_, i) => i)
const COL_AXES = Array.from({ length: COLS }, (_, i) => i)
/** 7× 6 列棋盘:单格像素由可用区域反算 */
/** 6× 4 列棋盘:单格像素由可用区域反算 */
const BOARD_INSET_RPX = 4
/** 棋盘与底部道具栏之间的安全间隙,避免最后一行被遮挡 */
const BOARD_FOOTER_GAP_RPX = 28
const GRID_PAD_RPX = 8
const GRID_GAP_RPX = 8
const SWAP_MS = 280
@@ -449,6 +423,11 @@ function tileStyle() {
return { background: 'transparent' }
}
/** 棋子角标用:低 / 中 / 高(含糖等级) */
function giChar(gi) {
return gi === 'high' ? '高' : gi === 'mid' ? '中' : '低'
}
function gridPadGapPx() {
return {
padPx: Math.round(uni.upx2px(GRID_PAD_RPX)),
@@ -527,16 +506,15 @@ const isProcessing = ref(false)
const selectedUid = ref(null)
const matchChain = ref(0)
const movesLeft = ref(25)
const digestCountdown = ref(8)
const gamePhase = ref('playing')
const loseReason = ref('')
const resultMessage = ref('')
const foodTooltips = ref([])
const foodCard = ref(null)
let foodCardTimer = null
const floatingVals = ref([])
const hintUids = ref(new Set())
const hintMove = ref(null) // { t1, t2, isHorizontal }
let hintTimer = null
let digestTimer = null
let startedSound = false
// 目标列表在 selectSessionFoods() 中按本局食材动态生成
@@ -634,16 +612,8 @@ const gridRows = computed(() => {
const scoreText = computed(() => score.value.toLocaleString('zh-CN'))
const winStreak = computed(() => gameProgress.value.winStreak || 0)
const dailyStreak = computed(() => gameProgress.value.dailyStreak || 0)
/** 连胜加成:2 连胜起每多 1 场 +10% 得分 */
const scoreMultiplier = computed(() => {
const s = winStreak.value
if (s < 2) return 1
return 1 + (s - 1) * 0.1
})
const goalProgressPct = computed(() => {
const list = goalList.value
if (!list.length) return 0
@@ -665,11 +635,6 @@ const goalList = computed(() =>
})
)
/** 升糖倒计时 ≤3 秒时在棋盘上方显示告急条 */
const digestUrgent = computed(() => (
gamePhase.value === 'playing' && digestCountdown.value > 0 && digestCountdown.value <= 3
))
const allGoalsDone = computed(() =>
goalList.value.length > 0 && goalList.value.every((g) => g.done)
)
@@ -868,108 +833,38 @@ function setupLevel(levelId) {
const cfg = getLevelConfig(levelId)
applyLevelConfig(cfg)
sessionMaxCombo.value = 0
lastUrgeKey = ''
loseReason.value = ''
tileTouchStart = null
clearDragPreview()
selectSessionFoods()
initBoard()
startDigestTimer()
recomputeGlucoseFromBoard()
}
/** 统计棋盘上未消化食物量(高 GI 权重更大) */
function calcBoardDigestRise() {
/** 统计棋盘上当前含糖负荷(高 GI 权重更大),用于血糖表信息性反算 */
function calcBoardSugarLoad() {
const loads = { high: 0, mid: 0, low: 0 }
let total = 0
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const tile = board.value[r]?.[c]
if (tile && !tile.isRemoved) {
loads[tile.type.gi] = (loads[tile.type.gi] || 0) + 1
total++
}
}
}
const mult = levelConfig.value.digestMultiplier ?? 1
const raw = loads.high * 2.5 + loads.mid * 0.9 + loads.low * 0.15
return Math.min(14, Math.max(1, Math.round(raw * 0.07 * mult)))
if (total <= 0) return 50
// 高糖占比越大,血糖表越高;纯信息展示,不判负、不随时间变化
const weighted = loads.high * 1 + loads.mid * 0.5 + loads.low * 0.1
const ratio = weighted / total
return Math.max(5, Math.min(95, Math.round(ratio * 100)))
}
function triggerDigestTick() {
if (gamePhase.value !== 'playing' || isProcessing.value) return
const rise = calcBoardDigestRise()
if (rise <= 0) return
updateMeter(rise)
showMeterDeltaFx(rise, '消化')
playSfx('meterWarn', 0.65, 1.05)
if (glucoseLevel.value >= 95) {
loseReason.value = 'glucose'
endGame('lost')
return
}
}
function resetDigestCountdown() {
digestCountdown.value = levelConfig.value.digestIntervalSec ?? 8
}
function startDigestTimer() {
stopDigestTimer()
resetDigestCountdown()
digestTimer = setInterval(() => {
if (gamePhase.value !== 'playing' || isProcessing.value) return
digestCountdown.value = Math.max(0, digestCountdown.value - 1)
if (digestCountdown.value <= 0) {
triggerDigestTick()
resetDigestCountdown()
}
}, 1000)
}
function stopDigestTimer() {
if (digestTimer) {
clearInterval(digestTimer)
digestTimer = null
}
}
function showUrge(text, type = 'warm') {
if (gamePhase.value !== 'playing') return
if (urgeTimer) clearTimeout(urgeTimer)
urgeToast.value = { show: true, text, type }
urgeTimer = setTimeout(() => {
urgeToast.value = { ...urgeToast.value, show: false }
}, 2200)
}
/** 心理激励:接近胜利 / 步数告急时给正反馈,促继续玩 */
function checkUrge() {
if (gamePhase.value !== 'playing') return
const pct = goalProgressPct.value
let key = ''
let text = ''
let type = 'warm'
if (pct >= 100) return
if (pct >= 88) {
key = 'almost'
text = '就差一点点!再消一组就过关 🎯'
type = 'hot'
} else if (pct >= 65) {
key = 'close'
text = `目标已完成 ${pct}%,继续保持!`
type = 'warm'
} else if (movesLeft.value <= 3 && movesLeft.value > 0) {
key = 'moves'
text = `仅剩 ${movesLeft.value} 步,试试提示箭头交换!`
type = 'warn'
} else if (winStreak.value >= 3) {
key = 'streak'
text = `${winStreak.value} 连胜中!得分 ×${scoreMultiplier.value.toFixed(1)}`
type = 'hot'
}
if (!key || key === lastUrgeKey) return
lastUrgeKey = key
showUrge(text, type)
/** 棋盘稳定后,把血糖表同步为当前含糖负荷(消除高糖食物后会变好) */
function recomputeGlucoseFromBoard() {
const target = calcBoardSugarLoad()
glucoseLevel.value = target
}
function restartLevel() {
@@ -981,9 +876,8 @@ function restartLevel() {
selectedUid.value = null
matchChain.value = 0
screenShaking.value = false
foodTooltips.value = []
foodCard.value = null
floatingVals.value = []
lastUrgeKey = ''
setupLevel(levelConfig.value.id)
nextTick(() => recalcBoardSize())
}
@@ -997,9 +891,8 @@ function goNextLevel() {
selectedUid.value = null
matchChain.value = 0
screenShaking.value = false
foodTooltips.value = []
foodCard.value = null
floatingVals.value = []
lastUrgeKey = ''
setupLevel(gameProgress.value.currentLevel || levelConfig.value.id + 1)
nextTick(() => recalcBoardSize())
}
@@ -1428,10 +1321,10 @@ async function reshuffleIfStuck() {
}
/**
* 消除组视觉特效(开心消消乐风格):
* 3 连:柔光圈 + 少量星屑(轻量“噗”)
* 4 连:糖果 confetti彩虹星屑+ 双层光环 + 轻闪屏 + 轻震屏
* 5 连+:彩虹大爆炸 + 多重金/白光环 + 闪屏 + 重震屏
* 消除组视觉特效(适老化:柔和为主,去掉震屏与强闪屏):
* 3 连:柔光圈 + 少量星屑
* 4 连:彩虹星屑 + 双层光环 + 轻闪屏
* 5 连+:彩虹光环 + 闪屏(不震屏
* 支持对每一组同时触发(并行查询中心坐标)。
*/
async function spawnGroupFx(groups, maxSize) {
@@ -1450,7 +1343,7 @@ async function spawnGroupFx(groups, maxSize) {
const main = centers.reduce((a, b) => (b.size > a.size ? b : a))
if (maxSize >= 5) {
// 5 连+:彩虹大爆炸
// 5 连+:彩虹光环(柔和,不震屏)
centers.forEach(({ center, color }) => {
spawnRipple(center.x, center.y, color, 4)
spawnBurst(center.x, center.y, color, 16, { rainbow: true, distMax: 130, sizeMax: 18 })
@@ -1458,18 +1351,16 @@ async function spawnGroupFx(groups, maxSize) {
spawnRipple(main.center.x, main.center.y, '#FFD54A', 6)
spawnRipple(main.center.x, main.center.y, '#ffffff', 2, { life: 520 })
spawnBurst(main.center.x, main.center.y, '#FFD54A', 18, { rainbow: true, sizeMax: 22, distMax: 160 })
triggerFlash('#fff7d6', 0.5)
triggerShake(2)
triggerFlash('#fff7d6', 0.18)
} else if (maxSize >= 4) {
// 4 连:糖果 confetti + 双层光环
// 4 连:彩虹星屑 + 双层光环(不震屏)
centers.forEach(({ center, color }) => {
spawnRipple(center.x, center.y, color, 3)
spawnBurst(center.x, center.y, color, 12, { rainbow: true })
})
spawnRipple(main.center.x, main.center.y, '#ffffff', 1, { life: 480 })
spawnBurst(main.center.x, main.center.y, '#FFE06B', 6, { sizeMax: 18 })
triggerFlash('#ffffff', 0.28)
triggerShake(1)
triggerFlash('#ffffff', 0.12)
} else {
// 3 连:轻量星屑 + 柔光圈
centers.forEach(({ center, color }) => {
@@ -1492,36 +1383,20 @@ async function queryTileCenter(tile) {
})
}
/** 点击食物:弹出大字科普卡,强化「这是高/中/低糖食物」的认知 */
function showFoodTooltip(tile) {
queryTileCenter(tile).then((center) => {
if (!center) return
const id = `tt-${Date.now()}`
foodTooltips.value.push({
id,
name: tile.type.name,
giLabel: tile.type.giLabel,
color: tile.type.color,
x: center.x,
y: center.y
})
setTimeout(() => {
foodTooltips.value = foodTooltips.value.filter((t) => t.id !== id)
}, 1500)
const valId = `fv-${Date.now()}`
const valText = formatEatLabel(tile.type)
const valColor = tile.type.gi === 'high' ? '#ef5350' : tile.type.gi === 'mid' ? '#fb8c00' : '#43a047'
floatingVals.value.push({
id: valId,
text: valText,
color: valColor,
x: center.x,
y: center.y
})
setTimeout(() => {
floatingVals.value = floatingVals.value.filter((t) => t.id !== valId)
}, 1000)
})
const t = tile.type
if (foodCardTimer) clearTimeout(foodCardTimer)
foodCard.value = {
name: t.name,
gi: t.gi,
giShort: GI_SHORT[t.gi] || '',
color: GI_COLOR[t.gi] || '#16a34a',
tip: getFoodTip(t)
}
foodCardTimer = setTimeout(() => {
foodCard.value = null
}, 3200)
}
function updateGoal(key) {
@@ -1529,12 +1404,11 @@ function updateGoal(key) {
if (g && g.current < g.target) {
g.current += 1
playSfx('goalTick', 0.9)
nextTick(() => checkUrge())
}
}
function updateScore(pts) {
const bonus = Math.round(pts * scoreMultiplier.value)
const bonus = Math.round(pts)
score.value += bonus
if (bonus >= 40) playSfx('score', Math.min(1.15, 0.85 + bonus / 200))
scoreAnimating.value = false
@@ -1545,64 +1419,52 @@ function updateScore(pts) {
}
function updateMeter(totalVal) {
const prev = glucoseLevel.value
glucoseLevel.value = Math.max(5, Math.min(95, glucoseLevel.value + totalVal))
playMeterSfx(totalVal, glucoseLevel.value)
if (prev <= 70 && glucoseLevel.value > 70) {
screenShaking.value = true
setTimeout(() => { screenShaking.value = false }, 500)
}
}
function showMeterDeltaFx(delta, label = '血糖') {
const query = uni.createSelectorQuery().in(instance?.proxy)
query.select('.game-grid').boundingClientRect((rect) => {
if (!rect || rect.width <= 0) return
const cx = rect.left + rect.width / 2
const cy = rect.top + rect.height / 2
const sign = delta > 0 ? '+' : ''
const valId = `fv-meter-${Date.now()}`
/** 消除时给认知正反馈:在最大一组中心浮出「认识了 X·高糖」 */
function showLearnFx(groups) {
if (!groups || !groups.length) return
const g = groups.reduce((a, b) => (b.size > a.size ? b : a))
const rep = g.tiles[Math.floor(g.tiles.length / 2)]
if (!rep) return
const t = rep.type
queryTileCenter(rep).then((center) => {
if (!center) return
const id = `learn-${Date.now()}`
floatingVals.value.push({
id: valId,
text: `${label} ${sign}${delta}`,
color: delta < 0 ? '#2ecc71' : '#ef5350',
x: cx,
y: cy - 24
id,
text: `认识了 ${t.name}·${GI_SHORT[t.gi] || ''}`,
color: GI_COLOR[t.gi] || '#16a34a',
x: center.x,
y: center.y - 30
})
setTimeout(() => {
floatingVals.value = floatingVals.value.filter((t) => t.id !== valId)
}, 1100)
}).exec()
floatingVals.value = floatingVals.value.filter((v) => v.id !== id)
}, 1400)
})
}
function checkGameEnd() {
if (gamePhase.value !== 'playing') return
// 无失败惩罚:仅在认全目标食物时通关,步数耗尽不判负
if (allGoalsDone.value) {
endGame('won')
return
}
if (movesLeft.value <= 0) {
endGame('lost')
} else {
checkUrge()
}
}
function buildResultStats() {
const rows = [
{ label: '得分', value: scoreText.value },
{ label: '剩余步数', value: String(movesLeft.value) },
{ label: '最高连消', value: `${sessionMaxCombo.value} ` }
const learned = goalList.value.reduce((s, g) => s + g.current, 0)
return [
{ label: '本组认识食物', value: `${goalList.value.length}` },
{ label: '消除练习', value: `${learned} ` },
{ label: '得分', value: scoreText.value }
]
if (winStreak.value >= 1) {
rows.push({ label: '连胜', value: `${winStreak.value}` })
}
return rows
}
function endGame(phase) {
gamePhase.value = phase
stopDigestTimer()
clearHints()
if (phase === 'won') {
@@ -1615,25 +1477,19 @@ function endGame(phase) {
maxCombo: sessionMaxCombo.value
})
resultStars.value = stars
const starTip = stars >= 3 ? '完美控糖!' : stars >= 2 ? '表现不错!' : '再接再厉,争取更多星!'
resultMessage.value = newBest
? `${starTip} 新纪录 ${stars} 星,已解锁第 ${levelConfig.value.id + 1}`
: `${starTip} 获得 ${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} 组。`
}
resultStats.value = buildResultStats()
playSfxLayer('win', 'winStinger', 120, 0.7)
} else {
recordLoss()
const pct = goalProgressPct.value
resultStars.value = 0
if (loseReason.value === 'glucose') {
resultMessage.value = '血糖过高!多消除棋盘上的食物,相当于运动降糖'
} else if (pct >= 70) {
resultMessage.value = `目标已完成 ${pct}%,只差一步!再试一次很可能过关`
} else if (pct >= 40) {
resultMessage.value = `已完成 ${pct}%,换种交换顺序试试`
} else {
resultMessage.value = '步数用尽,调整策略再挑战'
}
resultMessage.value = '再认一认这些食物,下次更熟练。'
resultStats.value = buildResultStats()
playSfx('lose')
}
@@ -1738,11 +1594,7 @@ async function clearAndRefill() {
updateScore(tiles.length * 20 * (matchChain.value + 1))
updateMeter(totalVal)
if (totalVal !== 0) {
showMeterDeltaFx(totalVal, totalVal < 0 ? '运动' : '血糖')
}
// 消除食物 = 运动,升糖倒计时恢复初始
resetDigestCountdown()
showLearnFx(groups)
bumpLayout()
await delay(MATCH_MS)
@@ -2110,27 +1962,31 @@ function recalcBoardSize() {
const sys = uni.getSystemInfoSync()
const header = res?.[0]
const footer = res?.[1]
const headerBottom = Math.ceil(header?.bottom ?? header?.height ?? uni.upx2px(200))
const footerTop = Math.floor(footer?.top ?? (sys.windowHeight - uni.upx2px(180)))
const headerBottom = Math.ceil(header?.bottom ?? header?.height ?? uni.upx2px(360))
// 底部道具栏较高,测量失败时用更保守的兜底高度,避免最后一行被遮挡
const footerTop = Math.floor(footer?.top ?? (sys.windowHeight - uni.upx2px(300)))
const inset = uni.upx2px(BOARD_INSET_RPX)
const footerGap = uni.upx2px(BOARD_FOOTER_GAP_RPX)
const zoneW = sys.windowWidth
const zoneH = Math.max(0, footerTop - headerBottom)
// 预留底部安全间隙,棋盘不贴到道具栏
const zoneH = Math.max(0, footerTop - headerBottom - footerGap)
// 7× 6 列:宽高分别约束,取较小单格像素
// 6× 4 列:宽高分别约束,取较小单格像素
cellSizePx.value = calcMaxCellSize(zoneW - inset * 2, zoneH - inset * 2)
const boardDim = calcBoardDimensions(cellSizePx.value)
boardWidthPx.value = boardDim.width
boardHeightPx.value = boardDim.height
const areaBottom = Math.max(0, sys.windowHeight - footerTop + footerGap)
mainStyle.value = {
top: `${headerBottom}px`,
bottom: `${Math.max(0, sys.windowHeight - footerTop)}px`
bottom: `${areaBottom}px`
}
pageStyle.value = {
...pageStyle.value,
'--board-area-top': `${headerBottom}px`,
'--board-area-bottom': `${Math.max(0, sys.windowHeight - footerTop)}px`,
'--board-area-bottom': `${areaBottom}px`,
'--cell-size': `${cellSizePx.value}px`,
'--tile-cell-px': `${cellSizePx.value}px`,
'--tile-emoji-size': `${Math.floor(cellSizePx.value * 0.82)}px`
@@ -2157,6 +2013,8 @@ onLoad(() => {
onReady(() => {
recalcBoardSize()
// 等头/底栏(含安全区、道具栏)完全布局后再量一次,确保最后一行不被遮挡
setTimeout(() => recalcBoardSize(), 180)
})
onMounted(() => {
@@ -2165,8 +2023,7 @@ onMounted(() => {
onUnmounted(() => {
clearHints()
stopDigestTimer()
if (urgeTimer) clearTimeout(urgeTimer)
if (foodCardTimer) clearTimeout(foodCardTimer)
})
</script>
+63 -12
View File
@@ -1264,6 +1264,47 @@ async function applyCard(card) {
await fetchGamifyState()
}
function readUserMobile() {
try {
const userData = uni.getStorageSync('userData') || {}
return String(userData.mobile || userData.phone || '').trim()
} catch (e) {
return ''
}
}
/** 无就诊卡时:已绑定手机号则快捷建档(与 weekly 一致) */
async function ensureDailyContextByPhone() {
if (diagnosisId.value) return true
const res = await proxy.apiUrl({
url: '/api/tcm/dailyEnsurePhoneContext',
method: 'POST',
data: {}
}, false)
if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
await applyCard({
id: res.data.diagnosis_id,
patient_id: res.data.patient_id,
patient_name: res.data.patient_name || '',
age: res.data.age || 0,
gender: res.data.gender || 0
})
if (res.data.created) {
await fetchCardList()
}
return true
}
if (res && res.data && res.data.need_mobile) {
return 'need_mobile'
}
showUserToast(formatUserMessage(res?.msg, '无法加载记录'))
return false
}
async function onSelectCard(card) {
if (!card || Number(card.id) === Number(diagnosisId.value)) return
bloodRecords.value = []
@@ -2680,13 +2721,12 @@ function onChartTouch(e) {
function readUserContext() {
try {
const userData = uni.getStorageSync('userData')
if (userData && userData.diagnosis) {
if (!patientId.value) patientId.value = Number(userData.diagnosis.patient_id) || 0
if (!diagnosisId.value) diagnosisId.value = Number(userData.diagnosis.id) || 0
if (!patientName.value) patientName.value = userData.diagnosis.patient_name || ''
if (!age.value) age.value = Number(userData.diagnosis.age) || 0
if (!gender.value) gender.value = Number(userData.diagnosis.gender) || 0
}
if (!userData) return
const name = String(userData.real_name || userData.nickname || '').trim()
if (!patientName.value && name) patientName.value = name
const sex = Number(userData.sex)
if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
if (!age.value && userData.age) age.value = Number(userData.age) || 0
} catch (e) {}
}
@@ -2755,17 +2795,28 @@ async function bootstrap(options) {
if (!target) target = list[0]
await applyCard(target)
} else {
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
if (passedPatientId) patientId.value = passedPatientId
diagnosisId.value = passedDiagnosisId || 0
patientId.value = passedPatientId || 0
if (passedPatientName) patientName.value = passedPatientName
if (passedAge) age.value = passedAge
if (passedGender) gender.value = passedGender
if (!diagnosisId.value || !patientId.value) {
readUserContext()
if (!patientName.value) readUserContext()
if (!diagnosisId.value && readUserMobile()) {
await ensureDailyContextByPhone()
}
if (!diagnosisId.value) {
authChecking.value = false
redirectToCardEntry()
if (readUserMobile()) {
showUserToast('暂时无法加载记录,请稍后重试')
} else {
showUserToast('请先在统计页授权手机号后再查看记录')
}
setTimeout(() => {
const pages = getCurrentPages()
if (pages.length > 1) uni.navigateBack()
else uni.redirectTo({ url: '/tongji/pages/weekly' })
}, 1600)
return
}
}
+105 -140
View File
@@ -45,20 +45,19 @@
</view>
</scroll-view>
<view class="st-main">
<!-- 1. 录入今日血糖 -->
<!-- 2. 血糖趋势 -->
<view v-if="diagnosisId" class="st-chart-card">
<!-- 2. 血糖趋势无就诊卡或录入不足 7 天显示示例曲线 -->
<view class="st-chart-card">
<view class="st-chart-head">
<view>
<text class="st-chart-title">血糖趋势</text>
<text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
<view class="st-chart-more" @click="goRecordsPage">
<text class="st-chart-more-text">查看更多记录</text>
<TongjiIcon name="chevron-right" size="sm" color="#006c49" />
</view>
</view>
<view class="st-chart-legend">
<view
@@ -79,97 +78,39 @@
</view>
</view>
</view>
<view v-if="bloodHasData" class="st-chart-summary">
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-high">{{ bloodHighDayCount }}</text>
<text class="st-chart-summary-label">天偏高</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num is-good">{{ bloodNormalDayCount }}</text>
<text class="st-chart-summary-label">天正常</text>
</view>
<view class="st-chart-summary-item">
<text class="st-chart-summary-num">{{ bloodRecordDayCount }}</text>
<text class="st-chart-summary-label">天有记录</text>
</view>
</view>
<view class="st-chart-area">
<view class="st-chart-area" @click="onChartTap">
<canvas
v-if="!recordOverlayOpen"
id="bloodWaveChart"
type="2d"
class="st-chart-canvas"
@touchstart="onChartTouch"
@touchmove="onChartTouch"
@click.stop="onChartTap"
/>
<view v-if="recordOverlayOpen" class="st-chart-placeholder">
<text>图表已暂停显示</text>
</view>
<view v-if="!bloodHasData && !loading && !recordOverlayOpen" class="st-chart-empty">
<view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
<text>当前时间范围暂无血糖数据</text>
</view>
<cover-view v-if="hoverInfo && !recordOverlayOpen" class="chart-tip" :style="{ left: hoverInfo.left + 'px', top: hoverInfo.top + 'px' }">
<cover-view class="chart-tip-date">{{ hoverInfo.date }}</cover-view>
<cover-view v-if="hoverInfo.fasting != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-fasting" />
<cover-view>空腹 {{ hoverInfo.fasting }}</cover-view>
</cover-view>
<cover-view v-if="hoverInfo.postprandial != null" class="chart-tip-row">
<cover-view class="st-dot st-dot-post" />
<cover-view>餐后 {{ hoverInfo.postprandial }}</cover-view>
</cover-view>
</cover-view>
</view>
<view class="st-cta" @click="openInputForm()">
<TongjiIcon name="plus" size="md" color="#FFFFFF" />
<text>录入今日血糖</text>
</view>
<view class="st-game-entry" @click="goGamePage">
<view class="st-game-entry-icon">
<TongjiIcon name="activity" size="md" color="#006c49" />
<view v-if="chartUseMockData" class="st-chart-foot">
<text class="st-chart-mock-hint">示例数据连续记录满 7 天后显示您的真实趋势</text>
</view>
<view class="st-game-entry-body">
<text class="st-game-entry-title">糖分突袭</text>
<text class="st-game-entry-sub">消除棋盘食物边玩边学控糖</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view>
</view>
<!-- 3. 最新测量 & 概览 -->
<view class="st-latest-section">
<view
v-if="latestMeasurement.hasValue"
class="st-latest-hero"
:class="{ 'is-high': latestMeasurement.isHigh }"
>
<view class="st-latest-hero-pattern" aria-hidden="true" />
<text class="st-latest-label">最新测量 · {{ latestMeasurement.typeLabel }}</text>
<view class="st-latest-value-row">
<text class="st-latest-value">{{ latestMeasurement.value }}</text>
<text class="st-latest-unit">mmol/L</text>
<view class="st-game-entry st-game-entry--in-card" @click="goGamePage">
<view class="st-game-entry-icon">
<TongjiIcon name="activity" size="sm" color="#006c49" />
</view>
<view v-if="latestMeasurement.isHigh" class="st-latest-flag">
<TongjiIcon name="alert-triangle" size="sm" color="#93000a" />
<text>偏高请遵医嘱</text>
</view>
</view>
<view class="st-summary-grid">
<view class="st-summary-card">
<text class="st-summary-label">空腹</text>
<text class="st-summary-value" :class="{ 'is-high': elderFasting.isHigh }">{{ elderFasting.value }}</text>
<text v-if="elderFasting.isHigh" class="st-summary-tag">偏高</text>
</view>
<view class="st-summary-card">
<text class="st-summary-label">餐后</text>
<text class="st-summary-value" :class="{ 'is-high': elderPostprandial.isHigh }">{{ elderPostprandial.value }}</text>
<text v-if="elderPostprandial.isHigh" class="st-summary-tag">偏高</text>
<view class="st-game-entry-body">
<text class="st-game-entry-title">识糖小课堂</text>
<text class="st-game-entry-sub">认一认哪些食物含糖高</text>
</view>
<TongjiIcon name="chevron-right" size="sm" color="#64748b" />
</view>
</view>
<!-- 4. AI 饮食建议 -->
<view class="st-training-module">
<dev-training-entry inline />
</view>
<!-- 3. AI 饮食建议 -->
<view v-if="diagnosisId" class="st-ai-section">
<view class="st-ai-deco" aria-hidden="true" />
<view class="st-ai-head">
@@ -227,7 +168,7 @@
<view class="st-main-spacer" />
</view>
<!-- 收起后的重新唤起按钮 -->
<view v-if="diagnosisId && floatAskClosed" class="st-float-reopen" @click="floatAskClosed = false">
<TongjiIcon name="sparkles" size="sm" color="#ffffff" />
@@ -458,6 +399,7 @@ import TongjiIcon from '../components/TongjiIcon.vue'
import { useDietAi } from '../composables/useDietAi.js'
import { useSpeechToText } from '../composables/useSpeechToText.js'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'
const { proxy } = getCurrentInstance()
@@ -859,6 +801,7 @@ const bloodHasData = computed(() => {
})
})
/** 当前时间范围内有真实录入的天数 */
const bloodRecordDayCount = computed(() => {
return chartDates.value.filter((date) => {
const b = bloodByDate.value[date]
@@ -866,6 +809,50 @@ const bloodRecordDayCount = computed(() => {
}).length
})
const REAL_BLOOD_DAYS_MIN = 7
/** 无就诊卡或真实录入不足 7 天时,图表用示例数据 */
const chartUseMockData = computed(() => {
if (!diagnosisId.value) return true
return bloodRecordDayCount.value < REAL_BLOOD_DAYS_MIN
})
/** 示例曲线:7 天内有波动的空腹/餐后,便于老人理解趋势图形态 */
const mockBloodByDate = computed(() => {
const map = {}
const dates = chartDates.value
dates.forEach((date, i) => {
const t = dates.length > 1 ? i / (dates.length - 1) : 0
const fasting = Number((5.2 + Math.sin(t * Math.PI) * 0.5 + (i % 2) * 0.15).toFixed(1))
const post = Number((7.6 + Math.cos(t * Math.PI) * 1.1 + (i % 3) * 0.25).toFixed(1))
const row = {
record_date: date,
fasting_blood_sugar: fasting,
postprandial_blood_sugar: post,
other_blood_sugar: '',
systolic_pressure: '',
diastolic_pressure: ''
}
row.isHighFasting = isHighFasting(row.fasting_blood_sugar)
row.isHighPostprandial = isHighPostprandial(row.postprandial_blood_sugar)
row.isHighOther = false
row.isHighBp = false
row.bpText = '—'
map[date] = row
})
return map
})
/** 图表绘制用数据源:示例或真实 */
const chartBloodByDate = computed(() => (
chartUseMockData.value ? mockBloodByDate.value : bloodByDate.value
))
const chartHasDisplayData = computed(() => {
if (chartUseMockData.value) return chartDates.value.length > 0
return bloodHasData.value
})
const bloodHighDayCount = computed(() => {
return chartDates.value.filter((date) => {
const b = bloodByDate.value[date]
@@ -1377,16 +1364,26 @@ function onRefresh() {
fetchAll()
}
async function goRecordsPage() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
function buildMorePageUrl() {
if (!diagnosisId.value) return '/tongji/pages/more'
const q = [
`diagnosis_id=${diagnosisId.value}`,
patientId.value ? `patient_id=${patientId.value}` : ''
].filter(Boolean).join('&')
uni.navigateTo({ url: `/tongji/pages/more?${q}` })
return `/tongji/pages/more?${q}`
}
/** 点击血糖趋势图:进入记录页 more(无就诊卡时先手机号快捷建档) */
async function onChartTap() {
if (!diagnosisId.value) {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: buildMorePageUrl() })
}
async function goRecordsPage() {
onChartTap()
}
async function goMorePage() {
@@ -1394,7 +1391,7 @@ async function goMorePage() {
const ok = await ensureCanRecordGlucose()
if (!ok || !diagnosisId.value) return
}
uni.navigateTo({ url: '/tongji/pages/more' })
uni.navigateTo({ url: buildMorePageUrl() })
}
function goGamePage() {
@@ -2102,7 +2099,7 @@ function scheduleChartRedraw() {
function buildSeries(field) {
return chartDates.value.map((date) => {
const b = bloodByDate.value[date]
const b = chartBloodByDate.value[date]
return b ? toNumber(b[field]) : null
})
}
@@ -2136,7 +2133,7 @@ function drawChart() {
ctx.clearRect(0, 0, w, h)
const padding = { left: 40, right: 18, top: 26, bottom: 36 }
const padding = { left: 40, right: 18, top: 12, bottom: 28 }
const cw = w - padding.left - padding.right
const ch = h - padding.top - padding.bottom
@@ -2392,59 +2389,25 @@ function toggleChartSeries(key) {
scheduleChartRedraw()
}
function onChartTouch(e) {
if (!chartCoords || !canvasWidth) return
if (!chartDates.value.length) return
let touchX = 0
let touchY = 0
if (e && e.touches && e.touches[0]) {
touchX = e.touches[0].x ?? e.touches[0].clientX ?? 0
touchY = e.touches[0].y ?? e.touches[0].clientY ?? 0
}
const { padding, cw, n } = chartCoords
if (touchX < padding.left || touchX > padding.left + cw) {
return
}
const ratio = n <= 1 ? 0 : (touchX - padding.left) / cw
let idx = Math.round(ratio * (n - 1))
if (idx < 0) idx = 0
if (idx > n - 1) idx = n - 1
const date = chartDates.value[idx]
const fp = chartSeriesVisible.value.fasting ? chartCoords.fasting[idx] : null
const pp = chartSeriesVisible.value.postprandial ? chartCoords.postprandial[idx] : null
const anchorX = fp?.x ?? pp?.x ?? padding.left + cw * (idx / Math.max(1, n - 1))
const left = Math.max(8, Math.min(canvasWidth - 130, anchorX - 60))
const top = Math.max(4, touchY - 70)
hoverInfo.value = {
date: date.slice(5),
fasting: fp?.value ?? null,
postprandial: pp?.value ?? null,
left,
top
}
if (hoverTimer) clearTimeout(hoverTimer)
hoverTimer = setTimeout(() => {
hoverInfo.value = null
}, 2500)
}
watch(rangeMode, () => {
scheduleChartRedraw()
})
watch([chartUseMockData, bloodRecords], () => {
scheduleChartRedraw()
})
// ============ 生命周期 ============
function readUserContext() {
try {
const userData = uni.getStorageSync('userData')
if (userData && userData.diagnosis) {
if (!patientId.value) patientId.value = Number(userData.diagnosis.patient_id) || 0
if (!diagnosisId.value) diagnosisId.value = Number(userData.diagnosis.id) || 0
if (!patientName.value) patientName.value = userData.diagnosis.patient_name || ''
if (!age.value) age.value = Number(userData.diagnosis.age) || 0
if (!gender.value) gender.value = Number(userData.diagnosis.gender) || 0
}
if (!userData) return
// 勿用问诊会话里的 diagnosis 作为日常血糖诊单(无 view_records 会报无权)
const name = String(userData.real_name || userData.nickname || '').trim()
if (!patientName.value && name) patientName.value = name
const sex = Number(userData.sex)
if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
if (!age.value && userData.age) age.value = Number(userData.age) || 0
} catch (e) {}
}
@@ -2527,14 +2490,12 @@ async function bootstrap(options) {
if (!target) target = list[0]
await applyCard(target)
} else {
if (passedDiagnosisId) diagnosisId.value = passedDiagnosisId
if (passedPatientId) patientId.value = passedPatientId
diagnosisId.value = passedDiagnosisId || 0
patientId.value = passedPatientId || 0
if (passedPatientName) patientName.value = passedPatientName
if (passedAge) age.value = passedAge
if (passedGender) gender.value = passedGender
if (!diagnosisId.value || !patientId.value) {
readUserContext()
}
if (!patientName.value) readUserContext()
readUserMobile()
if (!diagnosisId.value && userMobile.value) {
await ensureDailyContextByPhone()
@@ -2544,6 +2505,10 @@ async function bootstrap(options) {
authChecking.value = false
if (diagnosisId.value) {
await fetchAll()
} else {
bloodRecords.value = []
await nextTick()
scheduleChartRedraw()
}
}
+136 -230
View File
@@ -93,11 +93,10 @@
}
.game-page .gg-hud-level-text {
font-size: 24rpx;
font-size: 30rpx;
font-weight: 800;
color: #64748b;
color: #475569;
line-height: 1.1;
text-transform: uppercase;
letter-spacing: 1rpx;
}
@@ -108,7 +107,7 @@
}
.game-page .gg-hud-score-val {
font-size: 30rpx;
font-size: 42rpx;
font-weight: 900;
color: #006c49;
font-variant-numeric: tabular-nums;
@@ -120,34 +119,9 @@
}
.game-page .gg-hud-score-unit {
font-size: 18rpx;
font-size: 26rpx;
font-weight: 700;
color: #94a3b8;
}
.game-page .gg-hud-mult {
font-size: 16rpx;
font-weight: 900;
color: #fff;
background: linear-gradient(135deg, #f59e0b, #ea580c);
padding: 1rpx 8rpx;
border-radius: 999rpx;
line-height: 1.2;
margin-left: 4rpx;
}
.game-page .gg-hud-streak {
display: flex;
align-items: center;
gap: 4rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(254, 243, 199, 0.8);
border: 1rpx solid rgba(245, 158, 11, 0.15);
font-size: 20rpx;
font-weight: 800;
color: #ea580c;
transition: all 0.2s ease;
color: #64748b;
}
/* 核心三栏面板 */
@@ -194,7 +168,7 @@
}
.game-page .gg-glucose-value {
font-size: 28rpx;
font-size: 34rpx;
font-weight: 900;
color: #0f172a;
line-height: 1;
@@ -217,13 +191,13 @@
}
.game-page .gg-glucose-title {
font-size: 22rpx;
font-size: 28rpx;
font-weight: 800;
color: #475569;
color: #334155;
}
.game-page .gg-glucose-status {
font-size: 20rpx;
font-size: 26rpx;
font-weight: 800;
color: #006c49;
padding: 2rpx 12rpx;
@@ -322,7 +296,7 @@
}
.game-page .gg-meter-legend text {
font-size: 16rpx;
font-size: 22rpx;
font-weight: 700;
}
@@ -351,42 +325,21 @@
transition: all 0.3s ease;
}
.game-page .gg-dash-moves.is-warn {
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(185, 28, 28, 0.15));
border-color: rgba(239, 68, 68, 0.3);
animation: gg-moves-pulse 1.2s ease-in-out infinite alternate;
}
.game-page .gg-moves-label {
font-size: 20rpx;
font-size: 26rpx;
font-weight: 800;
color: #64748b;
color: #475569;
margin-bottom: 2rpx;
}
.game-page .gg-moves-val {
font-size: 40rpx;
font-size: 48rpx;
font-weight: 900;
color: #006c49;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.game-page .gg-dash-moves.is-warn .gg-moves-val {
color: #dc2626;
}
@keyframes gg-moves-pulse {
from {
transform: scale(1);
box-shadow: 0 0 8rpx rgba(239, 68, 68, 0.2);
}
to {
transform: scale(1.04);
box-shadow: 0 0 16rpx rgba(239, 68, 68, 0.45);
}
}
/* 右侧:关卡目标 */
.game-page .gg-dash-mission {
flex: 1.2;
@@ -410,13 +363,13 @@
}
.game-page .gg-mission-title {
font-size: 22rpx;
font-size: 28rpx;
font-weight: 800;
color: #475569;
color: #334155;
}
.game-page .gg-mission-pct {
font-size: 20rpx;
font-size: 26rpx;
font-weight: 800;
color: #006c49;
font-variant-numeric: tabular-nums;
@@ -459,8 +412,8 @@
}
.game-page .gg-goal-icon-wrap {
width: 32rpx;
height: 32rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
@@ -494,9 +447,9 @@
}
.game-page .gg-goal-chip-text {
font-size: 18rpx;
font-size: 26rpx;
font-weight: 800;
color: #475569;
color: #334155;
font-variant-numeric: tabular-nums;
line-height: 1;
}
@@ -534,51 +487,26 @@
}
.game-page .gg-hint-tag {
font-size: 16rpx;
font-size: 24rpx;
font-weight: 800;
color: #ffffff;
background: #006c49;
padding: 2rpx 8rpx;
border-radius: 6rpx;
padding: 4rpx 14rpx;
border-radius: 8rpx;
flex-shrink: 0;
line-height: 1.2;
line-height: 1.3;
}
.game-page .gg-board-hint-text {
font-size: 20rpx;
font-size: 28rpx;
font-weight: 700;
color: #475569;
line-height: 1.2;
color: #334155;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.game-page .gg-board-hint-bar .gg-digest-pill {
flex-shrink: 0;
background: linear-gradient(135deg, #ea580c, #c2410c);
border-radius: 999rpx;
padding: 6rpx 14rpx;
display: flex;
align-items: center;
gap: 6rpx;
box-shadow: 0 4rpx 10rpx rgba(234, 88, 12, 0.15);
}
.game-page .gg-board-hint-bar .gg-digest-num {
font-size: 24rpx;
font-weight: 900;
color: #ffffff;
line-height: 1;
}
.game-page .gg-board-hint-bar .gg-digest-label {
font-size: 16rpx;
font-weight: 700;
color: rgba(255, 255, 255, 0.9);
}
/* 棋盘上方升糖告急条 — 浮层,不占文档流 */
.game-page .gg-board-stack {
display: flex;
flex-direction: column;
@@ -591,47 +519,6 @@
width: 100%;
}
.game-page .gg-digest-urge-bar {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 10rpx;
padding: 14rpx 20rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #ef4444, #b91c1c);
box-shadow: 0 6rpx 20rpx rgba(185, 28, 28, 0.35);
box-sizing: border-box;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(calc(-100% - 10rpx));
transition: opacity 0.22s ease, visibility 0.22s ease;
}
.game-page .gg-digest-urge-bar.is-show {
opacity: 1;
visibility: visible;
animation: gg-digest-urge-fade 1.1s ease-in-out infinite alternate;
}
.game-page .gg-digest-urge-text {
flex: 1;
min-width: 0;
font-size: 22rpx;
font-weight: 700;
color: #fff;
line-height: 1.35;
}
@keyframes gg-digest-urge-fade {
from { opacity: 0.82; }
to { opacity: 1; }
}
/* ===== Main / Board ===== */
.game-page .gg-main {
position: fixed;
@@ -666,9 +553,9 @@
}
.game-page .gg-no-move-tip-text {
font-size: 22rpx;
font-size: 28rpx;
font-weight: 600;
color: #ea580c;
color: #c2410c;
}
.game-page .gg-board-wrap {
@@ -959,6 +846,92 @@
pointer-events: none;
}
/* ===== 棋子含糖等级常驻角标(认知核心:一眼看出高/中/低糖) ===== */
.game-page .gg-tile-gi {
position: absolute;
top: 2rpx;
left: 2rpx;
z-index: 3;
min-width: 30rpx;
height: 30rpx;
padding: 0 6rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
font-weight: 900;
color: #ffffff;
line-height: 1;
pointer-events: none;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.25);
border: 2rpx solid rgba(255, 255, 255, 0.9);
}
.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-food-card {
position: fixed;
left: 50%;
top: 30%;
transform: translateX(-50%);
z-index: 150;
width: 560rpx;
max-width: 86vw;
padding: 32rpx 36rpx;
border-radius: 32rpx;
background: #ffffff;
border: 6rpx solid #16a34a;
box-shadow: 0 20rpx 60rpx rgba(22, 29, 25, 0.22);
pointer-events: none;
animation: gg-food-card-in 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.game-page .gg-food-card.is-mid { border-color: #ea580c; }
.game-page .gg-food-card.is-high { border-color: #dc2626; }
.game-page .gg-food-card-head {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 16rpx;
}
.game-page .gg-food-card-name {
flex: 1;
min-width: 0;
font-size: 46rpx;
font-weight: 900;
color: #161d19;
line-height: 1.2;
}
.game-page .gg-food-card-gi {
flex-shrink: 0;
font-size: 32rpx;
font-weight: 900;
color: #ffffff;
padding: 8rpx 24rpx;
border-radius: 999rpx;
line-height: 1.2;
}
.game-page .gg-food-card-tip {
display: block;
font-size: 34rpx;
font-weight: 600;
color: #3c4a42;
line-height: 1.5;
}
@keyframes gg-food-card-in {
from { transform: translateX(-50%) scale(0.86); opacity: 0; }
to { transform: translateX(-50%) scale(1); opacity: 1; }
}
/* ===== FX Layer ===== */
.game-page .gg-fx-layer {
position: fixed;
@@ -967,43 +940,10 @@
z-index: 100;
}
.game-page .food-tooltip {
position: absolute;
font-weight: 700;
font-size: 26rpx;
padding: 12rpx 24rpx;
border-radius: 24rpx;
background: #fff;
color: #161c28;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
border: 1rpx solid #f1f3ff;
display: flex;
align-items: center;
gap: 12rpx;
white-space: nowrap;
transform: translateX(-50%);
animation: gg-tooltip-in 1.5s ease-out forwards;
}
.game-page .gi-badge {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
color: #fff;
font-weight: 800;
}
@keyframes gg-tooltip-in {
0% { transform: translate(-50%, 0) scale(0.8); opacity: 0; }
15% { transform: translate(-50%, -35px) scale(1.05); opacity: 1; }
85% { transform: translate(-50%, -40px) scale(1); opacity: 1; }
100% { transform: translate(-50%, -50px) scale(0.9); opacity: 0; }
}
.game-page .gg-floating-val {
position: absolute;
font-weight: 800;
font-size: 36rpx;
font-weight: 900;
font-size: 44rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
transform: translateX(-50%);
animation: gg-float-up 1s ease-out forwards;
@@ -1043,8 +983,8 @@
display: flex;
flex-direction: column;
align-items: center;
padding: 16rpx 0;
width: 152rpx;
padding: 22rpx 0;
width: 176rpx;
background: rgba(255, 255, 255, 0.85);
border: 1.5rpx solid rgba(0, 108, 73, 0.12);
border-radius: 40rpx;
@@ -1067,11 +1007,11 @@
position: absolute;
top: -10rpx;
right: -10rpx;
width: 48rpx;
height: 48rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
color: #fff;
font-size: 24rpx;
font-size: 30rpx;
font-weight: 900;
display: flex;
align-items: center;
@@ -1095,9 +1035,9 @@
}
.game-page .gg-booster-label {
font-size: 24rpx;
font-size: 30rpx;
font-weight: 700;
color: #3c4a42;
color: #2b3a32;
}
/* ===== Result overlay ===== */
@@ -1135,10 +1075,10 @@
}
.game-page .gg-result-sub {
font-size: 30rpx;
color: #3c4a42;
font-size: 34rpx;
color: #2b3a32;
margin-bottom: 24rpx;
line-height: 1.5;
line-height: 1.6;
}
.game-page .gg-combo-banner {
@@ -1177,41 +1117,6 @@
100% { transform: translate(-50%, -60%) scale(0.85); opacity: 0; }
}
.game-page .gg-urge-toast {
position: fixed;
left: 50%;
bottom: 280rpx;
transform: translateX(-50%);
z-index: 90;
padding: 20rpx 36rpx;
border-radius: 999rpx;
font-size: 28rpx;
font-weight: 800;
color: #fff;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
animation: gg-urge-in 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
max-width: 86vw;
text-align: center;
}
.game-page .gg-urge-toast.is-warm {
background: linear-gradient(135deg, #10b981, #006c49);
}
.game-page .gg-urge-toast.is-hot {
background: linear-gradient(135deg, #f59e0b, #ea580c);
}
.game-page .gg-urge-toast.is-warn {
background: linear-gradient(135deg, #ef4444, #b91c1c);
}
@keyframes gg-urge-in {
from { transform: translateX(-50%) translateY(20rpx); opacity: 0; }
to { transform: translateX(-50%) translateY(0); opacity: 1; }
}
.game-page .gg-result-stars {
display: flex;
gap: 16rpx;
@@ -1244,12 +1149,12 @@
}
.game-page .gg-result-stat-label {
font-size: 26rpx;
color: #64748b;
font-size: 32rpx;
color: #475569;
}
.game-page .gg-result-stat-val {
font-size: 26rpx;
font-size: 32rpx;
font-weight: 800;
color: #161d19;
}
@@ -1263,14 +1168,14 @@
.game-page .gg-result-btn {
flex: 1 1 40%;
min-width: 160rpx;
padding: 28rpx 0;
min-width: 180rpx;
padding: 32rpx 0;
border-radius: 999rpx;
font-size: 30rpx;
font-size: 36rpx;
font-weight: 800;
text-align: center;
background: #eef6ee;
color: #3c4a42;
color: #2b3a32;
transition: all 0.2s;
}
@@ -1287,7 +1192,8 @@
@media (prefers-reduced-motion: reduce) {
.game-page.is-screen-shake,
.game-page .game-tile.is-hint,
.game-page .food-tooltip,
.game-page .gg-food-card,
.game-page .gg-combo-banner,
.game-page .gg-floating-val {
animation: none !important;
}
@@ -108,6 +108,12 @@
color: var(--on-primary);
}
/* ===== 居家训练(DEV 内嵌,与 user 页同源入口) ===== */
.weekly-page .st-training-module {
padding: 10rpx var(--st-container-margin);
margin: -10rpx -30rpx 10rpx -30rpx;
}
/* ===== Main ===== */
.weekly-page .st-main {
padding: 0 var(--st-container-margin) 32rpx;
@@ -196,23 +202,28 @@
/* ===== Chart card — rounded-3xl ===== */
.weekly-page .st-chart-card {
padding: 40rpx;
padding: 40rpx 10rpx 40rpx 10rpx;
border-radius: 48rpx;
background: var(--surface-container-lowest);
border: 1rpx solid rgba(221, 228, 221, 0.3);
box-shadow: var(--st-shadow-ambient);
display: flex;
flex-direction: column;
gap: 0;
overflow: hidden;
}
.weekly-page .st-chart-head {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
margin-bottom: 48rpx;
margin-bottom: 12rpx;
gap: 16rpx;
}
.weekly-page .st-chart-title {
display: block;
font-size: 40rpx;
font-size: 30rpx;
font-weight: 700;
color: var(--on-surface);
}
@@ -220,7 +231,7 @@
.weekly-page .st-chart-sub {
display: block;
margin-top: 4rpx;
font-size: 24rpx;
font-size: 22rpx;
font-weight: 600;
color: var(--on-surface-variant);
}
@@ -229,11 +240,11 @@
display: inline-flex;
align-items: center;
gap: 4rpx;
margin-top: 12rpx;
margin-top: 6rpx;
}
.weekly-page .st-chart-more-text {
font-size: 26rpx;
font-size: 22rpx;
font-weight: 600;
color: var(--primary);
}
@@ -315,10 +326,29 @@
color: var(--on-surface-variant);
}
.weekly-page .st-chart-foot {
padding: 20rpx 0 20rpx;
}
.weekly-page .st-chart-mock-hint {
display: block;
margin: 0;
font-size: 30rpx;
font-weight: 600;
color: var(--outline);
line-height: 1.35;
text-align: center;
}
.weekly-page .st-chart-area {
position: relative;
height: 360rpx;
border-bottom: 1rpx dashed var(--surface-container-highest);
overflow: hidden;
cursor: pointer;
}
.weekly-page .st-chart-area:active {
opacity: 0.92;
}
.weekly-page .st-chart-canvas {
@@ -327,6 +357,31 @@
display: block;
}
/* 识糖入口嵌在趋势卡内:轻量分隔,避免大块留白 */
.weekly-page .st-chart-card .st-game-entry--in-card {
margin-top: 12rpx;
padding: 16rpx 20rpx;
gap: 16rpx;
border-radius: 24rpx;
background: var(--surface-container-low);
border: 1rpx solid rgba(0, 108, 73, 0.08);
box-shadow: none;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-icon {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-title {
font-size: 28rpx;
}
.weekly-page .st-chart-card .st-game-entry--in-card .st-game-entry-sub {
font-size: 22rpx;
}
.weekly-page .st-chart-empty,
.weekly-page .st-chart-placeholder {
position: absolute;
@@ -790,6 +845,8 @@
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.2);
line-height: 0;
overflow: hidden;
}
.weekly-page .st-float-send.disabled {