更新
This commit is contained in:
@@ -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,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' },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user