更新
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
<template>
|
||||
<view class="food-tile-icon" :class="[`food-tile-icon--${size}`]">
|
||||
<text class="food-tile-icon__emoji">{{ fallbackIcon }}</text>
|
||||
<view
|
||||
class="food-tile-icon"
|
||||
:class="[`food-tile-icon--${size}`, { 'is-custom-img': hasCustomImg }]"
|
||||
>
|
||||
<text v-if="showEmojiFallback" class="food-tile-icon__emoji">{{ fallbackIcon }}</text>
|
||||
<image
|
||||
v-if="src && !imageFailed"
|
||||
class="food-tile-icon__img"
|
||||
:src="src"
|
||||
mode="aspectFit"
|
||||
:mode="imageMode"
|
||||
@error="onImageError"
|
||||
/>
|
||||
</view>
|
||||
@@ -27,12 +30,20 @@ const fallbackIcon = computed(() => {
|
||||
return '🍽'
|
||||
})
|
||||
|
||||
const hasCustomImg = computed(() => typeof props.food === 'object' && !!props.food?.img)
|
||||
|
||||
const src = computed(() => {
|
||||
if (typeof props.food === 'object' && props.food?.img) return props.food.img
|
||||
if (typeof props.food === 'object' && props.food?.icon) return getFoodImgUrl(props.food.icon)
|
||||
return ''
|
||||
})
|
||||
|
||||
/** 有可用图片时不渲染 emoji,避免 PNG 透明区域露出底层符号 */
|
||||
const showEmojiFallback = computed(() => !src.value || imageFailed.value)
|
||||
|
||||
/** 自定义素材用 aspectFill 铺满,减少边缘露底 */
|
||||
const imageMode = computed(() => (hasCustomImg.value ? 'aspectFill' : 'aspectFit'))
|
||||
|
||||
watch(
|
||||
() => src.value,
|
||||
() => {
|
||||
@@ -71,6 +82,11 @@ function onImageError() {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.food-tile-icon--tile.is-custom-img .food-tile-icon__img {
|
||||
width: 82%;
|
||||
height: 82%;
|
||||
}
|
||||
|
||||
/* 玻璃格上的食材图片:投影增强立体感(参考稿) */
|
||||
.food-tile-icon--tile .food-tile-icon__img {
|
||||
filter: drop-shadow(0 6rpx 10rpx rgba(0, 0, 0, 0.16));
|
||||
@@ -109,6 +125,11 @@ function onImageError() {
|
||||
font-size: 34rpx;
|
||||
}
|
||||
|
||||
.food-tile-icon--goal.is-custom-img .food-tile-icon__img,
|
||||
.food-tile-icon--tooltip.is-custom-img .food-tile-icon__img {
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.food-tile-icon__img {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
@@ -63,7 +63,15 @@ const FOOD_TIP_OVERRIDE = {
|
||||
grainMantou: '杂粮做的,比白馒头稳,仍要控量',
|
||||
riceNoodleSoup: '米粉升糖快,汤粉要少吃',
|
||||
friedNoodles: '油多又是精面,升糖快',
|
||||
centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝'
|
||||
centuryEggCongee: '白粥熬得软烂,升糖很快,糖友要少喝',
|
||||
sandwich: '夹心面包精制碳多,升糖快',
|
||||
eightTreasureCongee: '八宝粥甜糯,升糖快,要少喝',
|
||||
milletCongee: '小米粥熬软升糖快,糖友控量',
|
||||
shandongPancake: '煎饼加油面,升糖中等偏快',
|
||||
cake: '蛋糕又甜又油,升糖快',
|
||||
biscuit: '饼干又甜又碎,升糖快',
|
||||
wonton: '馄饨面皮精白,升糖中等,别多吃',
|
||||
blackCoffee: '不加糖的黑咖啡,升糖很低'
|
||||
}
|
||||
|
||||
/** 含糖等级通用科普文案 */
|
||||
@@ -109,7 +117,7 @@ const RAW_FOODS = [
|
||||
|
||||
// 中 GI
|
||||
{ key: 'brownRice', icon: '🍙', name: '糙米饭', gi: 'mid', val: 4, color: '#C9A26B' },
|
||||
{ key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39' },
|
||||
{ key: 'sweetPotato', icon: '🍠', name: '番薯', gi: 'mid', val: 5, color: '#C75B39', img: `${GAMES_IMG_BASE}/%E7%95%AA%E8%96%AF.png` },
|
||||
{ key: 'taro', icon: '🥔', name: '芋头', gi: 'mid', val: 5, color: '#B39DDB' },
|
||||
{ key: 'fish', icon: '🐟', name: '鱼肉', gi: 'mid', val: 2, color: '#4FA3C7' },
|
||||
{ key: 'chicken', icon: '🍗', name: '鸡肉', gi: 'mid', val: 3, color: '#D6A15A' },
|
||||
@@ -126,16 +134,24 @@ const RAW_FOODS = [
|
||||
{ key: 'wine', icon: '🍷', name: '红酒', gi: 'mid', val: 4, color: '#9B2C3B' },
|
||||
{ key: 'beer', icon: '🍺', name: '啤酒', gi: 'mid', val: 5, color: '#E0A83E' },
|
||||
{ key: 'coffee', icon: '☕', name: '咖啡', gi: 'mid', val: 2, color: '#6F4E37' },
|
||||
{ key: 'blackCoffee', icon: '☕', name: '黑咖啡', gi: 'low', val: -1, color: '#4E342E', img: `${GAMES_IMG_BASE}/%E9%BB%91%E5%92%96%E5%95%A1.png` },
|
||||
{ key: 'wonton', icon: '🥟', name: '馄饨', gi: 'mid', val: 5, color: '#E8DCC8', img: `${GAMES_IMG_BASE}/%E9%A6%84%E9%A5%A8.png` },
|
||||
{ key: 'milletCongee', icon: '🥣', name: '小米粥', gi: 'mid', val: 5, color: '#F5E6B8', img: `${GAMES_IMG_BASE}/%E5%B0%8F%E7%B1%B3%E7%B2%A5.png` },
|
||||
{ key: 'shandongPancake', icon: '🥞', name: '山东煎饼', gi: 'high', val: 16, color: '#D4A574', img: `${GAMES_IMG_BASE}/%E5%B1%B1%E4%B8%9C%E7%85%8E%E9%A5%BC.png` },
|
||||
{ key: 'corn', icon: '🌽', name: '玉米', gi: 'mid', val: 5, color: '#F9C513' },
|
||||
{ key: 'udon', icon: '🍲', name: '乌冬面', gi: 'mid', val: 5, color: '#D9B88F' },
|
||||
{ key: 'milkOats', icon: '🥣', name: '奶冲麦片', gi: 'mid', val: 6, color: '#E8D9B5', img: `${GAMES_IMG_BASE}/%E5%A5%B6%E5%86%B2%E9%BA%A6%E7%89%87.png` },
|
||||
{ key: 'grainMantou', icon: '🫓', name: '杂粮馒头', gi: 'mid', val: 5, color: '#C8A878', img: `${GAMES_IMG_BASE}/%E6%9D%82%E7%B2%AE%E9%A6%92%E5%A4%B4.png` },
|
||||
|
||||
// 高 GI
|
||||
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0' },
|
||||
{ key: 'whiteRice', icon: '🍚', name: '白米饭', gi: 'high', val: 18, color: '#E0E0E0', img: `${GAMES_IMG_BASE}/%E7%B1%B3%E9%A5%AD.png` },
|
||||
{ key: 'whiteBread', icon: '🍞', name: '白面包', gi: 'high', val: 16, color: '#D9A85C' },
|
||||
{ key: 'mantou', icon: '🥯', name: '馒头', gi: 'high', val: 17, color: '#ECE0C8' },
|
||||
{ key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F' },
|
||||
{ key: 'youtiao', icon: '🥖', name: '油条', gi: 'high', val: 20, color: '#D4943F', img: `${GAMES_IMG_BASE}/%E6%B2%B9%E6%9D%A1.png` },
|
||||
{ key: 'sandwich', icon: '🥪', name: '三明治', gi: 'high', val: 16, color: '#D9B88F', img: `${GAMES_IMG_BASE}/%E4%B8%89%E6%98%8E%E6%B2%BB.png` },
|
||||
{ key: 'biscuit', icon: '🍪', name: '饼干', gi: 'high', val: 18, color: '#C9A063', img: `${GAMES_IMG_BASE}/%E9%A5%BC%E5%B9%B2.png` },
|
||||
{ key: 'cake', icon: '🎂', name: '蛋糕', gi: 'high', val: 22, color: '#F48FB1', img: `${GAMES_IMG_BASE}/%E8%9B%8B%E7%B3%95.png` },
|
||||
{ key: 'eightTreasureCongee', icon: '🥣', name: '八宝粥', gi: 'high', val: 17, color: '#E8C4A0', img: `${GAMES_IMG_BASE}/%E5%85%AB%E5%AE%9D%E7%B2%A5.png` },
|
||||
{ key: 'donut', icon: '🍩', name: '甜甜圈', gi: 'high', val: 22, color: '#E87FB0' },
|
||||
{ key: 'popcorn', icon: '🍿', name: '爆米花', gi: 'high', val: 18, color: '#F5E6B3' },
|
||||
{ key: 'ramen', icon: '🍜', name: '拉面', gi: 'high', val: 17, color: '#E0A96D' },
|
||||
|
||||
@@ -4,10 +4,27 @@
|
||||
*/
|
||||
/**
|
||||
* 指定关卡固定食材(按 key)。未配置的关卡走随机抽取逻辑。
|
||||
* 第一关固定为这 5 种带真实图片素材的食物。
|
||||
* 第一关固定为这 16 种带真实图片素材的食物。
|
||||
*/
|
||||
const FIXED_LEVEL_FOODS = {
|
||||
1: ['milkOats', 'grainMantou', 'riceNoodleSoup', 'friedNoodles', 'centuryEggCongee']
|
||||
1: [
|
||||
'sandwich',
|
||||
'eightTreasureCongee',
|
||||
'milkOats',
|
||||
'milletCongee',
|
||||
'shandongPancake',
|
||||
'grainMantou',
|
||||
'riceNoodleSoup',
|
||||
'youtiao',
|
||||
'friedNoodles',
|
||||
'sweetPotato',
|
||||
'centuryEggCongee',
|
||||
'whiteRice',
|
||||
'cake',
|
||||
'biscuit',
|
||||
'wonton',
|
||||
'blackCoffee'
|
||||
]
|
||||
}
|
||||
|
||||
export function getLevelConfig(levelId) {
|
||||
|
||||
@@ -54,27 +54,6 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mc-quick-grid">
|
||||
<view class="mc-quick-item" @click="openInputForm()">
|
||||
<TongjiIcon name="glucose" size="md" color="#006c49" />
|
||||
<text class="mc-quick-label">血糖</text>
|
||||
</view>
|
||||
<view class="mc-quick-item" @click="openBpForm()">
|
||||
<TongjiIcon name="activity" size="md" color="#006c49" />
|
||||
<text class="mc-quick-label">血压</text>
|
||||
</view>
|
||||
<view class="mc-quick-item" @click="openDietForm()">
|
||||
<TongjiIcon name="utensils" size="md" color="#006c49" />
|
||||
<text class="mc-quick-label">饮食</text>
|
||||
</view>
|
||||
<view class="mc-quick-item" @click="openExerciseForm()">
|
||||
<TongjiIcon name="flame" size="md" color="#006c49" />
|
||||
<text class="mc-quick-label">运动</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
|
||||
<view class="mc-tasks-section">
|
||||
<text class="mc-tasks-title">今日控糖任务</text>
|
||||
<view class="mc-tasks-grid">
|
||||
@@ -117,6 +96,10 @@
|
||||
<view class="mc-tree-level-row">
|
||||
<text class="mc-tree-lv-badge">Lv.{{ treeLevel }}</text>
|
||||
<text class="mc-tree-stage-name">{{ treeLevelName }}</text>
|
||||
<view class="mc-tree-species-btn" @click.stop="openTreeSpeciesPicker">
|
||||
<text>{{ currentTreeSpecies.name }}</text>
|
||||
<TongjiIcon name="chevron-right" size="sm" color="#006c49" />
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="treeState.isMax" class="mc-tree-xp-hint">{{ treeXpUpgradeHint }}</text>
|
||||
<text v-else class="mc-tree-xp-hint">
|
||||
@@ -161,7 +144,7 @@
|
||||
<button
|
||||
class="mc-water-btn"
|
||||
:class="{ disabled: treeWaterLocked }"
|
||||
@click="waterTree"
|
||||
@click="onWaterTreeTap"
|
||||
>
|
||||
<TongjiIcon name="droplet" size="md" color="#FFFFFF" />
|
||||
<text>给小树浇水</text>
|
||||
@@ -841,6 +824,100 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今日任务 / 浇水状态 -->
|
||||
<view v-if="waterStatusVisible" class="input-modal-mask" @click="closeWaterStatusPanel">
|
||||
<view class="mc-water-panel" @click.stop>
|
||||
<view class="mc-water-panel-head">
|
||||
<text class="mc-water-panel-title">今日稳糖进度</text>
|
||||
<view class="mc-water-panel-close" @click="closeWaterStatusPanel">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mc-water-panel-summary">{{ waterStatusSummary }}</text>
|
||||
<view class="mc-water-panel-list">
|
||||
<view
|
||||
v-for="row in waterStatusRows"
|
||||
:key="row.id"
|
||||
class="mc-water-panel-row"
|
||||
:class="'is-' + row.statusKind"
|
||||
>
|
||||
<TongjiIcon :name="gamifyTaskIcon(row.id)" size="sm" color="#006c49" />
|
||||
<view class="mc-water-panel-row-main">
|
||||
<text class="mc-water-panel-row-name">{{ row.name }}</text>
|
||||
<text class="mc-water-panel-row-sub">{{ row.statusSub }}</text>
|
||||
</view>
|
||||
<text class="mc-water-panel-row-tag">{{ row.statusLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="waterStatusGrowthHint" class="mc-water-panel-growth">
|
||||
<text>{{ waterStatusGrowthHint }}</text>
|
||||
</view>
|
||||
<view class="mc-water-panel-actions">
|
||||
<button
|
||||
v-if="canWaterTree"
|
||||
class="mc-water-panel-btn primary"
|
||||
@click="waterTreeFromPanel"
|
||||
>
|
||||
立即浇水领 {{ claimablePoints }} 积分
|
||||
</button>
|
||||
<button
|
||||
v-else-if="firstPendingDisplayTask"
|
||||
class="mc-water-panel-btn"
|
||||
@click="goRecordFromWaterPanel"
|
||||
>
|
||||
去记录「{{ firstPendingDisplayTask.name }}」
|
||||
</button>
|
||||
<button v-else class="mc-water-panel-btn ghost" @click="closeWaterStatusPanel">
|
||||
知道了
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 树种选择 -->
|
||||
<view v-if="treeSpeciesPickerVisible" class="input-modal-mask" @click="closeTreeSpeciesPicker">
|
||||
<view class="mc-species-panel" @click.stop>
|
||||
<view class="mc-water-panel-head">
|
||||
<text class="mc-water-panel-title">选择你的稳糖树</text>
|
||||
<view class="mc-water-panel-close" @click="closeTreeSpeciesPicker">
|
||||
<text>×</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="mc-species-panel-tip">
|
||||
全勤打卡约 {{ speciesDaysToMax }} 天满级 · 每级 {{ TREE_XP_PER_LEVEL }} 积分 · 从小到大的 10 个阶段见下方
|
||||
</text>
|
||||
<scroll-view scroll-y class="mc-species-scroll">
|
||||
<view
|
||||
v-for="sp in TREE_SPECIES_LIST"
|
||||
:key="sp.id"
|
||||
class="mc-species-card"
|
||||
:class="{ active: treeSpeciesId === sp.id }"
|
||||
@click="selectTreeSpecies(sp.id)"
|
||||
>
|
||||
<view class="mc-species-card-head">
|
||||
<text class="mc-species-card-emoji">{{ speciesEmojiAtLevel(sp.id, treeLevel) }}</text>
|
||||
<view>
|
||||
<text class="mc-species-card-name">{{ sp.name }}</text>
|
||||
<text class="mc-species-card-tagline">{{ sp.tagline }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mc-species-stages">
|
||||
<view
|
||||
v-for="stage in speciesGrowthRoadmap(sp.id)"
|
||||
:key="stage.level"
|
||||
class="mc-species-stage"
|
||||
:class="{ reached: treeLevel >= stage.level, current: treeLevel === stage.level }"
|
||||
>
|
||||
<text class="mc-species-stage-emoji">{{ stage.emoji }}</text>
|
||||
<text class="mc-species-stage-lv">Lv.{{ stage.level }}</text>
|
||||
<text class="mc-species-stage-name">{{ stage.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -852,7 +929,15 @@ import SugarTreeGraphic from '../components/SugarTreeGraphic.vue'
|
||||
import TongjiIcon from '../components/TongjiIcon.vue'
|
||||
import CelebrateBurst from '../components/CelebrateBurst.vue'
|
||||
import VoiceSpeakMascot from '../components/VoiceSpeakMascot.vue'
|
||||
import { TREE_LEVELS, TREE_MAX_LEVEL, calcTreeFromPoints, pickTreeWhisper } from '../utils/treeLevels.js'
|
||||
import { TREE_LEVELS, TREE_MAX_LEVEL, TREE_XP_PER_LEVEL, calcTreeFromPoints, pickTreeWhisper } from '../utils/treeLevels.js'
|
||||
import {
|
||||
TREE_SPECIES_LIST,
|
||||
DAILY_MAX_TASK_POINTS,
|
||||
getTreeSpecies,
|
||||
speciesEmojiAtLevel,
|
||||
speciesGrowthRoadmap,
|
||||
estimateDaysToMaxLevel
|
||||
} from '../utils/treeSpecies.js'
|
||||
import { consumeDietAiPrefill } from '../composables/useDietAi.js'
|
||||
import { useSpeechToText, isBenignSttError } from '../composables/useSpeechToText.js'
|
||||
import { parseGlucose, parseBloodPressure, parseDiet, parseExercise, summarizeParsed, normalizeNumbers } from '../utils/voiceParse.js'
|
||||
@@ -1461,6 +1546,7 @@ async function applyCard(card) {
|
||||
recent: []
|
||||
}
|
||||
gamifyStatusTip.value = ''
|
||||
loadTreeSpecies()
|
||||
await fetchGamifyState()
|
||||
}
|
||||
|
||||
@@ -1878,6 +1964,9 @@ async function openInputForm() {
|
||||
inputForm.value.postprandial_blood_sugar = numToStr(r.postprandial_blood_sugar)
|
||||
inputForm.value.other_blood_sugar = numToStr(r.other_blood_sugar)
|
||||
inputForm.value.remark = String(r.remark || '')
|
||||
if (todayHasGlucose.value) {
|
||||
showUserToast('今日血糖已录入,如需修改可在下方调整', { duration: 2600 })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/* 静默:拉取失败时仍允许新增录入 */
|
||||
@@ -1918,7 +2007,7 @@ async function submitInputForm() {
|
||||
}
|
||||
}, false)
|
||||
if (res && res.code === 1) {
|
||||
showUserToast(res.msg || '已保存', { icon: 'success' })
|
||||
showUserToast(res.msg || '今日血糖已录入完成', { icon: 'success' })
|
||||
inputForm.value.visible = false
|
||||
await fetchAll()
|
||||
await fetchGamifyState()
|
||||
@@ -2004,6 +2093,9 @@ async function openBpForm() {
|
||||
bpForm.value.western_medicine = String(r.western_medicine || '')
|
||||
bpForm.value.insulin = String(r.insulin || '')
|
||||
bpForm.value.remark = String(r.remark || '')
|
||||
if (todayHasBpPressure.value) {
|
||||
showUserToast('今日血压已录入,如需修改可在下方调整', { duration: 2600 })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默:拉取失败时仍允许新增录入
|
||||
@@ -2084,6 +2176,9 @@ async function openDietForm(options = {}) {
|
||||
dietForm.value.lunch_foods = String(r.lunch_foods || '')
|
||||
dietForm.value.dinner_foods = String(r.dinner_foods || '')
|
||||
dietForm.value.note = String(r.note || '')
|
||||
if (todayHasDiet.value) {
|
||||
showUserToast('今日饮食已录入,如需修改可在下方调整', { duration: 2600 })
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
finally {
|
||||
@@ -2123,7 +2218,7 @@ async function submitDietForm() {
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1) {
|
||||
showUserToast(res.msg || '已保存', { icon: 'success' })
|
||||
showUserToast(res.msg || '今日饮食已录入完成', { icon: 'success' })
|
||||
dietForm.value.visible = false
|
||||
await fetchAll()
|
||||
await fetchGamifyState()
|
||||
@@ -2822,6 +2917,9 @@ async function openExerciseForm() {
|
||||
exerciseForm.value.duration = numToStr(r.duration)
|
||||
exerciseForm.value.intensity = Number(r.intensity) || 2
|
||||
exerciseForm.value.note = String(r.note || '')
|
||||
if (todayHasExercise.value) {
|
||||
showUserToast('今日运动已录入,如需修改可在下方调整', { duration: 2600 })
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
finally {
|
||||
@@ -2857,7 +2955,7 @@ async function submitExerciseForm() {
|
||||
}
|
||||
}, false)
|
||||
if (res?.code === 1) {
|
||||
showUserToast(res.msg || '已保存', { icon: 'success' })
|
||||
showUserToast(res.msg || '今日运动已录入完成', { icon: 'success' })
|
||||
exerciseForm.value.visible = false
|
||||
await fetchAll()
|
||||
await fetchGamifyState()
|
||||
@@ -2901,7 +2999,7 @@ async function submitBpForm() {
|
||||
}
|
||||
}, false)
|
||||
if (res && res.code === 1) {
|
||||
showUserToast(res.msg || '已保存', { icon: 'success' })
|
||||
showUserToast(res.msg || '今日血压已录入完成', { icon: 'success' })
|
||||
bpForm.value.visible = false
|
||||
await fetchAll()
|
||||
await fetchGamifyState()
|
||||
@@ -3951,16 +4049,16 @@ const firstPendingDisplayTask = computed(() => {
|
||||
|
||||
const waterHintText = computed(() => {
|
||||
if (canWaterTree.value) {
|
||||
return `有 ${claimablePoints.value} 积分待领,点击「给小树浇水」`
|
||||
return `有 ${claimablePoints.value} 积分待领,点击浇水查看进度并领取`
|
||||
}
|
||||
if (displayTasksAllCleared.value) {
|
||||
return '今日任务已全部完成,小树喝饱啦 🌱'
|
||||
}
|
||||
const pending = firstPendingDisplayTask.value
|
||||
if (pending) {
|
||||
return `先完成「${pending.name}」,再给小树浇水领积分`
|
||||
return `还差「${pending.name}」等项,点击浇水查看今日进度`
|
||||
}
|
||||
return '先完成今日任务,再给小树浇水领积分'
|
||||
return '点击浇水查看今日任务进度'
|
||||
})
|
||||
|
||||
const allTasksCleared = computed(() => {
|
||||
@@ -4145,7 +4243,7 @@ function onInviteTreeWhisper() {
|
||||
function promptWaterTreeGuide(taskLabel = '', delayMs = 900) {
|
||||
setTimeout(() => {
|
||||
if (claimablePoints.value > 0) {
|
||||
const prefix = taskLabel ? `${taskLabel}已记录,` : ''
|
||||
const prefix = taskLabel ? `今日${taskLabel}已录入完成,` : '今日已录入完成,'
|
||||
showUserToast(
|
||||
`${prefix}点击「给小树浇水」领取 ${claimablePoints.value} 积分`,
|
||||
{ duration: 3200 }
|
||||
@@ -4154,7 +4252,7 @@ function promptWaterTreeGuide(taskLabel = '', delayMs = 900) {
|
||||
}
|
||||
const pending = firstPendingDisplayTask.value
|
||||
if (pending) {
|
||||
const prefix = taskLabel ? `${taskLabel}已记录。` : ''
|
||||
const prefix = taskLabel ? `今日${taskLabel}已录入完成。` : '今日该项已录入完成。'
|
||||
showUserToast(
|
||||
`${prefix}继续完成「${pending.name}」,再给小树浇水领积分`,
|
||||
{ duration: 3200 }
|
||||
@@ -4170,23 +4268,7 @@ function promptWaterTreeGuide(taskLabel = '', delayMs = 900) {
|
||||
async function waterTree() {
|
||||
if (!diagnosisId.value) return
|
||||
if (treeWaterLocked.value) return
|
||||
|
||||
// 有可领取积分 → 正常浇水
|
||||
if (!canWaterTree.value) {
|
||||
if (displayTasksAllCleared.value) {
|
||||
showUserToast('今日控糖任务已全部完成,小树喝饱啦,明天继续加油 🌱', { duration: 2800 })
|
||||
hapticLight()
|
||||
return
|
||||
}
|
||||
const pending = firstPendingDisplayTask.value
|
||||
if (pending) {
|
||||
showUserToast(`先完成「${pending.name}」,再给小树浇水领积分`, { duration: 2800 })
|
||||
onGamifyTaskTap(pending, { skipGuide: true })
|
||||
return
|
||||
}
|
||||
showUserToast('暂无可领取积分,请先完成任务再给小树浇水', { duration: 2800 })
|
||||
return
|
||||
}
|
||||
if (!canWaterTree.value) return
|
||||
|
||||
const oldLevel = treeLevel.value
|
||||
treeWaterLocked.value = true
|
||||
@@ -4218,6 +4300,7 @@ async function waterTree() {
|
||||
if (addedPoints > 0) {
|
||||
showFloatReward(addedPoints)
|
||||
hapticLight()
|
||||
closeWaterStatusPanel()
|
||||
triggerCelebrate('浇水成功', tip || `+${addedPoints} 稳糖积分,小树咕咚喝饱啦`)
|
||||
const claimedIds = Array.isArray(data.claimed_tasks) ? data.claimed_tasks : []
|
||||
claimedIds.forEach((id) => popTaskDone(id))
|
||||
@@ -4274,8 +4357,126 @@ const treeState = computed(() => calcTreeFromPoints(gamifyPoints.value))
|
||||
const treeLevel = computed(() => treeState.value.level)
|
||||
const treeProgress = computed(() => treeState.value.progress)
|
||||
const treeLevelName = computed(() => treeState.value.name)
|
||||
const treeEmoji = computed(() => treeState.value.emoji)
|
||||
const treeEmoji = computed(() => speciesEmojiAtLevel(treeSpeciesId.value, treeLevel.value))
|
||||
const treeMood = computed(() => treeState.value.mood)
|
||||
|
||||
// ===== 树种选择 =====
|
||||
const treeSpeciesId = ref('classic')
|
||||
const treeSpeciesPickerVisible = ref(false)
|
||||
const speciesDaysToMax = estimateDaysToMaxLevel()
|
||||
|
||||
const currentTreeSpecies = computed(() => getTreeSpecies(treeSpeciesId.value))
|
||||
|
||||
function treeSpeciesStorageKey(dId = diagnosisId.value) {
|
||||
return `tongji_tree_species_v1_${Number(dId) || 0}`
|
||||
}
|
||||
|
||||
function loadTreeSpecies(dId = diagnosisId.value) {
|
||||
if (!dId) {
|
||||
treeSpeciesId.value = 'classic'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const saved = uni.getStorageSync(treeSpeciesStorageKey(dId))
|
||||
if (saved && getTreeSpecies(saved)) {
|
||||
treeSpeciesId.value = saved
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
treeSpeciesId.value = 'classic'
|
||||
}
|
||||
|
||||
function selectTreeSpecies(id) {
|
||||
if (!getTreeSpecies(id)) return
|
||||
treeSpeciesId.value = id
|
||||
try {
|
||||
uni.setStorageSync(treeSpeciesStorageKey(), id)
|
||||
} catch (e) {}
|
||||
treeSpeciesPickerVisible.value = false
|
||||
showUserToast(`已切换为「${getTreeSpecies(id).name}」`, { duration: 2200 })
|
||||
hapticLight()
|
||||
}
|
||||
|
||||
function openTreeSpeciesPicker() {
|
||||
treeSpeciesPickerVisible.value = true
|
||||
}
|
||||
|
||||
function closeTreeSpeciesPicker() {
|
||||
treeSpeciesPickerVisible.value = false
|
||||
}
|
||||
|
||||
// ===== 浇水 / 今日进度面板 =====
|
||||
const waterStatusVisible = ref(false)
|
||||
|
||||
const waterStatusRows = computed(() => {
|
||||
return displayTasksForUi.value.map((t) => {
|
||||
let statusLabel = '未记录'
|
||||
let statusKind = 'pending'
|
||||
let statusSub = '今日尚未打开此项'
|
||||
if (t.claimed) {
|
||||
statusLabel = '已完成'
|
||||
statusKind = 'claimed'
|
||||
statusSub = '今日已录入,积分已领取'
|
||||
} else if (t.completed) {
|
||||
statusLabel = '待浇水'
|
||||
statusKind = 'water'
|
||||
statusSub = `今日已录入,浇水可领 +${t.points} 积分`
|
||||
}
|
||||
return { ...t, statusLabel, statusKind, statusSub }
|
||||
})
|
||||
})
|
||||
|
||||
const waterStatusSummary = computed(() => {
|
||||
if (displayTasksAllCleared.value) {
|
||||
return '今日四项任务已全部完成并领取,小树喝饱啦,明天继续加油 🌱'
|
||||
}
|
||||
const pending = displayTasksForUi.value.filter((t) => !t.completed)
|
||||
const toWater = displayTasksForUi.value.filter((t) => t.completed && !t.claimed)
|
||||
if (canWaterTree.value) {
|
||||
return `今日已打开 ${toWater.length} 项记录,浇水可领 ${claimablePoints.value} 积分`
|
||||
}
|
||||
if (pending.length) {
|
||||
return `今日还差 ${pending.length} 项未打开:${pending.map((t) => t.name).join('、')}`
|
||||
}
|
||||
return '先完成今日记录,再给小树浇水领积分'
|
||||
})
|
||||
|
||||
const waterStatusGrowthHint = computed(() => {
|
||||
const sp = currentTreeSpecies.value
|
||||
if (treeState.value.isMax) {
|
||||
return `${sp.name} 已满级 · 继续打卡巩固习惯`
|
||||
}
|
||||
return `${sp.name} · Lv.${treeLevel.value} ${treeLevelName.value} · 再积 ${treeState.value.pointsToNext} 分升级「${treeState.value.nextName}」`
|
||||
})
|
||||
|
||||
function onWaterTreeTap() {
|
||||
if (treeWaterLocked.value) return
|
||||
openWaterStatusPanel()
|
||||
hapticLight()
|
||||
}
|
||||
|
||||
function openWaterStatusPanel() {
|
||||
waterStatusVisible.value = true
|
||||
}
|
||||
|
||||
function closeWaterStatusPanel() {
|
||||
waterStatusVisible.value = false
|
||||
}
|
||||
|
||||
function waterTreeFromPanel() {
|
||||
closeWaterStatusPanel()
|
||||
waterTree()
|
||||
}
|
||||
|
||||
function goRecordFromWaterPanel() {
|
||||
const pending = firstPendingDisplayTask.value
|
||||
if (!pending) {
|
||||
closeWaterStatusPanel()
|
||||
return
|
||||
}
|
||||
closeWaterStatusPanel()
|
||||
onGamifyTaskTap(pending, { skipGuide: true })
|
||||
}
|
||||
const treeProgressLabel = computed(() => {
|
||||
if (treeState.value.isMax) return '已满级 · 圆满树'
|
||||
return `${treeState.value.xpInLevel}/${treeState.value.xpNeed} XP`
|
||||
@@ -4451,6 +4652,14 @@ function onGamifyTaskTap(task, options = {}) {
|
||||
if (!task) return
|
||||
const skipGuide = !!options.skipGuide
|
||||
if (!task.completed) {
|
||||
if (resolveGamifyTaskCompleted(task.id, task.completed)) {
|
||||
showUserToast(
|
||||
`今日「${task.name}」已录入${task.claimed ? ',积分已领取' : ',请去浇水领积分'}`,
|
||||
{ duration: 2800 }
|
||||
)
|
||||
if (!task.claimed) openWaterStatusPanel()
|
||||
return
|
||||
}
|
||||
if (!diagnosisId.value) {
|
||||
showUserToast('请先选择就诊卡')
|
||||
return
|
||||
@@ -4487,14 +4696,10 @@ function onGamifyTaskTap(task, options = {}) {
|
||||
return
|
||||
}
|
||||
if (task.claimed) {
|
||||
showUserToast('该任务积分已领取')
|
||||
showUserToast(`今日「${task.name}」已录入,积分已领取`)
|
||||
return
|
||||
}
|
||||
if (!canWaterTree.value) {
|
||||
showUserToast('暂无可领取积分,请先完成其他任务', { duration: 2800 })
|
||||
return
|
||||
}
|
||||
waterTree()
|
||||
openWaterStatusPanel()
|
||||
}
|
||||
|
||||
function shareInviteCacheKey() {
|
||||
|
||||
@@ -639,7 +639,8 @@
|
||||
}
|
||||
|
||||
.more-page .mc-tasks-section {
|
||||
margin-top: 40rpx;
|
||||
margin-top: 0;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-tasks-grid {
|
||||
@@ -1167,3 +1168,259 @@
|
||||
font-size: 22rpx;
|
||||
color: rgba(108, 122, 113, 0.6);
|
||||
}
|
||||
|
||||
.more-page .mc-tree-species-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
margin-left: 8rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(0, 108, 73, 0.08);
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel,
|
||||
.more-page .mc-species-panel {
|
||||
width: calc(100% - 48rpx);
|
||||
max-width: 680rpx;
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-height: 78vh;
|
||||
background: var(--surface);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 32rpx 28rpx calc(32rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -12rpx 40rpx rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-close {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-container-low);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
color: var(--outline);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-summary {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.5;
|
||||
color: var(--on-surface-variant);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 18rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background: var(--surface-container-low);
|
||||
border: 1rpx solid rgba(0, 108, 73, 0.08);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row.is-water {
|
||||
border-color: rgba(0, 108, 73, 0.22);
|
||||
background: rgba(0, 108, 73, 0.06);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row.is-claimed {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row-sub {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--outline);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row-tag {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-row.is-pending .mc-water-panel-row-tag {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-growth {
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(0, 108, 73, 0.06);
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
background: var(--on-surface);
|
||||
color: var(--surface);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-btn.primary {
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
}
|
||||
|
||||
.more-page .mc-water-panel-btn.ghost {
|
||||
background: var(--surface-container-low);
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.more-page .mc-species-panel-tip {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
color: var(--outline);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-species-scroll {
|
||||
max-height: 56vh;
|
||||
}
|
||||
|
||||
.more-page .mc-species-card {
|
||||
padding: 20rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(0, 108, 73, 0.1);
|
||||
background: var(--surface-container-low);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-species-card.active {
|
||||
border-color: var(--primary);
|
||||
background: rgba(0, 108, 73, 0.06);
|
||||
}
|
||||
|
||||
.more-page .mc-species-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-species-card-emoji {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.more-page .mc-species-card-name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.more-page .mc-species-card-tagline {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--outline);
|
||||
}
|
||||
|
||||
.more-page .mc-species-stages {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage {
|
||||
width: calc(20% - 8rpx);
|
||||
min-width: 96rpx;
|
||||
padding: 10rpx 6rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
text-align: center;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage.reached {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage.current {
|
||||
box-shadow: 0 0 0 2rpx var(--primary);
|
||||
background: rgba(0, 108, 73, 0.1);
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage-emoji {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage-lv {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.more-page .mc-species-stage-name {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: var(--outline);
|
||||
margin-top: 2rpx;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { TREE_LEVELS, TREE_MAX_LEVEL, TREE_XP_PER_LEVEL } from './treeLevels.js'
|
||||
|
||||
/** 可选树种:每级 emoji 与 treeLevels.js Lv.0~9 一一对应 */
|
||||
export const TREE_SPECIES_LIST = [
|
||||
{
|
||||
id: 'classic',
|
||||
name: '稳糖灵树',
|
||||
tagline: '经典路线,从种子到圆满',
|
||||
stages: TREE_LEVELS.map((l) => l.emoji)
|
||||
},
|
||||
{
|
||||
id: 'bonsai',
|
||||
name: '雅韵盆景',
|
||||
tagline: '小巧精致,书桌旁的温柔陪伴',
|
||||
stages: ['🫘', '🌱', '🪴', '🎋', '🪴', '🌳', '🎍', '🌸', '🏵️', '🏆']
|
||||
},
|
||||
{
|
||||
id: 'pine',
|
||||
name: '苍劲青松',
|
||||
tagline: '挺拔向上,越记越稳',
|
||||
stages: ['🫘', '🌱', '🌿', '🌲', '🌲', '🌲', '⛰️', '🌲', '🏔️', '🏆']
|
||||
},
|
||||
{
|
||||
id: 'sakura',
|
||||
name: '樱花小树',
|
||||
tagline: '坚持打卡,枝头渐开',
|
||||
stages: ['🫘', '🌱', '🌿', '🌳', '🌸', '🌸', '🌸', '🌸', '🌺', '🏆']
|
||||
},
|
||||
{
|
||||
id: 'ginkgo',
|
||||
name: '金秋银杏',
|
||||
tagline: '叶色渐变,见证每一天',
|
||||
stages: ['🫘', '🌱', '🍃', '🌳', '🍂', '🍂', '🌳', '🍁', '🌟', '🏆']
|
||||
}
|
||||
]
|
||||
|
||||
export const TREE_SPECIES_STORAGE_KEY = 'tongji_tree_species_v1'
|
||||
|
||||
/** 四项任务全勤时每日最多可领积分(与后端 taskDefs 一致) */
|
||||
export const DAILY_MAX_TASK_POINTS = 40
|
||||
|
||||
export function getTreeSpecies(id) {
|
||||
return TREE_SPECIES_LIST.find((s) => s.id === id) || TREE_SPECIES_LIST[0]
|
||||
}
|
||||
|
||||
export function speciesEmojiAtLevel(speciesId, level) {
|
||||
const sp = getTreeSpecies(speciesId)
|
||||
const lv = Math.min(TREE_MAX_LEVEL, Math.max(0, Number(level) || 0))
|
||||
return sp.stages[lv] || sp.stages[0] || '🌱'
|
||||
}
|
||||
|
||||
/** 当前树种成长路线图(供选择面板展示) */
|
||||
export function speciesGrowthRoadmap(speciesId) {
|
||||
const sp = getTreeSpecies(speciesId)
|
||||
return TREE_LEVELS.map((meta, i) => {
|
||||
const xpTotal = i * TREE_XP_PER_LEVEL
|
||||
const daysHint = i === 0
|
||||
? 0
|
||||
: Math.max(1, Math.ceil(xpTotal / DAILY_MAX_TASK_POINTS))
|
||||
return {
|
||||
level: i,
|
||||
name: meta.name,
|
||||
emoji: sp.stages[i] || meta.emoji,
|
||||
desc: meta.desc,
|
||||
xpTotal,
|
||||
daysHint
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 全勤打卡大约多少天满级 */
|
||||
export function estimateDaysToMaxLevel() {
|
||||
const totalXp = TREE_MAX_LEVEL * TREE_XP_PER_LEVEL
|
||||
return Math.max(1, Math.ceil(totalXp / DAILY_MAX_TASK_POINTS))
|
||||
}
|
||||
Reference in New Issue
Block a user