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