This commit is contained in:
Your Name
2026-07-02 14:35:20 +08:00
parent b6cdd20c56
commit efd058bce5
312 changed files with 1376 additions and 400 deletions
+261 -56
View File
@@ -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() {