<template>
    <view class="more-page stitch-vitalmint" @touchend="onPageVoiceTouchEnd" @touchcancel="onPageVoiceTouchEnd">
        <CelebrateBurst :show="celebrateShow" :title="celebrateTitle" :subtitle="celebrateSub" />

        <!-- 浇水降糖粒子雨（全屏 fixed，对应 code.html sugar-particle） -->
        <view v-if="sugarParticles.length" class="mc-sugar-particles" aria-hidden="true">
            <text
                v-for="p in sugarParticles"
                :key="p.key"
                class="mc-sugar-particle"
                :class="{ 'is-south': p.kind === 'south' }"
                :style="{ left: p.left + 'vw', fontSize: p.size + 'rpx', animationDelay: p.delay + 's' }"
            >{{ p.kind === 'south' ? '↓' : '▼' }}</text>
        </view>
        <view v-if="authChecking" class="page-gate-loading">
            <text class="page-gate-loading-text">正在登录…</text>
        </view>

        <block v-if="!authChecking">
        <!-- Top App Bar（code.html） -->
        <view class="mc-header" :style="headerStyle">
            <view class="mc-header-inner" :style="headerInnerStyle">
                <view class="mc-header-left">
                    <view class="mc-header-back" @click="goBack">
                        <TongjiIcon name="chevron-left" size="md" color="#ffffff" />
                    </view>
                    <view class="mc-header-text">
                     
                        <text class="mc-header-name">{{ patientName || '健康档案' }}</text>
                    </view>
                </view>
                <view class="mc-header-actions">
                    <view class="mc-header-pill" @click="goGlucosePage">
                        <TongjiIcon name="glucose" size="sm" color="#ffffff" />
                        <text>血糖</text>
                    </view>
                    <view class="mc-header-icon-btn" @click="onRefresh">
                        <TongjiIcon name="refresh" size="md" color="#ffffff" />
                    </view>
                </view>
            </view>
        </view>
        
        <view class="mc-main" :style="mainPadStyle">
            <view class="mc-card mc-paradise-card">
                    <view class="mc-paradise-head">
                        <view>
                            <text class="mc-card-title">稳糖乐园</text>
                            <text class="mc-card-sub">用健康打卡浇灌你的稳糖树</text>
                        </view>
                        <view class="mc-points-badge">
                            <text class="mc-points-num">{{ gamifyPoints }}</text>
                            <text class="mc-points-label">稳糖积分</text>
                        </view>
                    </view>

                    <view class="mc-tasks-section">
                        <text class="mc-tasks-title">今日控糖任务</text>
                        <view class="mc-tasks-grid">
                            <view
                                v-for="task in displayTasksForUi"
                                :key="task.id"
                                class="mc-task-block"
                                :class="{ 'is-done': task.completed }"
                                @click="onGamifyTaskTap(task)"
                            >
                                <view class="mc-task-block-top">
                                    <view class="mc-task-check" :class="{ pending: !task.completed }">
                                        <TongjiIcon
                                            v-if="task.completed"
                                            name="check"
                                            size="sm"
                                            color="#006c49"
                                        />
                                        <view v-else class="mc-task-dot" />
                                    </view>
                                    <text class="mc-task-block-points">+{{ task.points }}积分</text>
                                </view>
                                <view class="mc-task-block-body">
                                    <TongjiIcon :name="gamifyTaskIcon(task.id)" size="sm" color="#006c49" />
                                    <text class="mc-task-block-name">{{ task.name }}</text>
                                </view>
                                <text v-if="task.completed && task.claimed" class="mc-task-block-status">已完成</text>
                                <view v-else-if="task.completed" class="mc-task-block-action is-water">
                                    <text>去浇水</text>
                                </view>
                                <view v-else class="mc-task-block-action">
                                    <text>去记录</text>
                                </view>
                            </view>
                        </view>
                    </view>
                    <view class="mc-tree-zone" style="margin-top: 10rpx;">
                        <view class="mc-tree-level-head">
                            <view>
                                <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">
                                    还差 <text class="mc-tree-xp-bold">{{ treeState.pointsToNext }} XP</text> 升级至 Lv.{{ treeLevel + 1 }}
                                </text>
                            </view>
                            <text class="mc-tree-xp-text">{{ treeProgressLabel }}</text>
                        </view>
                        <view class="mc-tree-progress-track">
                            <view class="mc-tree-progress-bar" :style="{ width: treeProgress + '%' }" />
                        </view>
                        <view class="mc-tree-display" @click="onTreeWhisper">
                            <text class="mc-tree-display-bg" aria-hidden="true">🌿</text>
                            <!-- 浇水水滴雨 -->
                            <view class="mc-rain" aria-hidden="true">
                                <view
                                    v-for="d in waterDroplets"
                                    :key="d.key"
                                    class="mc-droplet"
                                    :style="{ left: d.left + '%', animationDelay: d.delay + 's' }"
                                />
                            </view>
                            <view class="mc-tree-emoji-wrap" :class="{ 'is-watering': treePulseActive }">
                                <text class="mc-tree-emoji">{{ treeEmoji }}</text>
                            </view>
                            <!-- 闪光星（对应 code.html shimmer-particle / sparkles） -->
                            <text
                                v-for="s in sparkles"
                                :key="s.key"
                                class="mc-sparkle"
                                :style="{ left: s.left + '%', top: s.top + '%', animationDuration: s.dur + 's' }"
                            >✨</text>
                            <!-- 降糖飘字（对应 code.html sugar-decrease-text，在植物上方） -->
                            <view v-if="sugarDecrease" :key="sugarDecrease.key" class="mc-sugar-decrease-float">
                                <text>-0.5 mmol/L</text>
                            </view>
                            <view v-if="floatReward" :key="floatReward.key" class="mc-float-reward">
                                <text>+{{ floatReward.text }}</text>
                            </view>
                        </view>
                        <view class="mc-water-action">
                            <button
                                class="mc-water-btn"
                                :class="{ disabled: treeWaterLocked }"
                                @click="onWaterTreeTap"
                            >
                                <TongjiIcon name="droplet" size="md" color="#FFFFFF" />
                                <text>给小树浇水</text>
                            </button>
                            <text class="mc-water-hint">{{ waterHintText }}</text>
                        </view>
                    </view>
                </view>
            <template v-if="diagnosisId">
                <!-- 7 / 30 天切换 -->
                <view class="mc-range-toggle">
                    <view
                        v-for="opt in rangeOptions"
                        :key="opt.value"
                        class="mc-range-opt"
                        :class="{ active: rangeMode === opt.value }"
                        @click="onChangeRange(opt.value)"
                    >
                        <text>{{ opt.label }}</text>
                    </view>
                </view>

                <!-- 健康日历 -->
                <view v-if="heatmapWeeks.length > 0" class="mc-card">
                    <view class="mc-card-head">
                        <view>
                            <text class="mc-card-title">健康日历</text>
                            <text class="mc-card-sub">{{ heatmapMonthLabel }} · 已记录 {{ heatmapTotalRecords }} / {{ heatmapInRangeDays }} 天</text>
                        </view>
                        <TongjiIcon name="chevron-right" size="md" color="#006c49" />
                    </view>
                    <view class="mc-cal-grid mc-cal-grid--full">
                        <text class="mc-cal-weekday">一</text>
                        <text class="mc-cal-weekday">二</text>
                        <text class="mc-cal-weekday">三</text>
                        <text class="mc-cal-weekday">四</text>
                        <text class="mc-cal-weekday">五</text>
                        <text class="mc-cal-weekday">六</text>
                        <text class="mc-cal-weekday is-weekend">日</text>
                        <view
                            v-for="cell in heatmapCells"
                            :key="cell.date"
                            class="mc-cal-cell"
                            :class="{
                                off: !cell.inRange,
                                recorded: cell.inRange && cell.level > 0 && !cell.isHigh,
                                'recorded-high': cell.inRange && cell.isHigh,
                                'today-empty': cell.isToday && cell.inRange && cell.level === 0,
                                'in-range': cell.inRange && cell.level === 0 && !cell.isToday,
                                'future-off': !cell.inRange && cell.level < 0
                            }"
                            @click="cell.inRange && cell.level > 0 ? jumpToDayRecord(cell.date) : null"
                        >
                            <text>{{ cell.day }}</text>
                            <view
                                v-if="cell.inRange && cell.level > 0"
                                class="mc-cal-dot"
                                :class="{ 'is-high': cell.isHigh }"
                            />
                        </view>
                    </view>
                    <view class="mc-cal-foot">
                        <text class="mc-cal-foot-text">{{ todayChecked ? '今日已完成记录' : '今日还没有记录' }}</text>
                        <view class="mc-cal-foot-btn" @click="openInputForm()">
                            <text>去记血糖</text>
                        </view>
                    </view>
                </view>

                <!-- 血糖明细 -->
                <view class="mc-blood-section">
                    <view class="mc-section-head">
                        <text class="mc-card-title">血糖明细</text>
                        <text class="mc-section-count">共 {{ bloodRecordDays.length }} 天</text>
                    </view>
                    <view v-if="loading" class="mc-card mc-state-card">
                        <text>加载中…</text>
                    </view>
                    <view v-else-if="bloodRecordDays.length === 0" class="mc-card mc-state-card">
                        <text>该时段暂无血糖记录</text>
                    </view>
                    <view v-else class="mc-blood-list">
                        <view
                            v-for="day in bloodRecordDays"
                            :key="day.date"
                            :id="`day-${day.date.replace(/-/g, '')}`"
                            class="mc-card mc-day-card"
                        >
                            <view class="mc-day-head">
                                <text class="mc-day-date">{{ day.dateLabel }}</text>
                                <text class="mc-day-weekday">{{ day.weekday }}</text>
                            </view>
                            <view v-if="day.blood" class="mc-metric-stack">
                                <view
                                    class="mc-metric-row"
                                    :class="{
                                        'is-abnormal-high': day.blood.isHighFasting,
                                        'is-abnormal-low': day.blood.isLowFasting
                                    }"
                                >
                                    <text class="mc-metric-label">空腹</text>
                                    <view class="mc-metric-value-wrap">
                                        <text v-if="day.blood.isHighFasting" class="mc-metric-badge is-high">偏高</text>
                                        <text v-else-if="day.blood.isLowFasting" class="mc-metric-badge is-low">偏低</text>
                                        <text
                                            class="mc-metric-value"
                                            :class="{ 'is-high': day.blood.isHighFasting, 'is-low': day.blood.isLowFasting }"
                                        >{{ formatBloodMetric(day.blood.fasting_blood_sugar) }}</text>
                                        <text class="mc-metric-unit">mmol/L</text>
                                    </view>
                                </view>
                                <view
                                    class="mc-metric-row"
                                    :class="{
                                        'is-abnormal-high': day.blood.isHighPostprandial,
                                        'is-abnormal-low': day.blood.isLowPostprandial
                                    }"
                                >
                                    <text class="mc-metric-label">餐后</text>
                                    <view v-if="formatBloodMetric(day.blood.postprandial_blood_sugar) !== '——'" class="mc-metric-value-wrap">
                                        <text v-if="day.blood.isHighPostprandial" class="mc-metric-badge is-high">偏高</text>
                                        <text v-else-if="day.blood.isLowPostprandial" class="mc-metric-badge is-low">偏低</text>
                                        <text
                                            class="mc-metric-value"
                                            :class="{ 'is-high': day.blood.isHighPostprandial, 'is-low': day.blood.isLowPostprandial }"
                                        >{{ formatBloodMetric(day.blood.postprandial_blood_sugar) }}</text>
                                        <text class="mc-metric-unit">mmol/L</text>
                                    </view>
                                    <view v-else class="mc-metric-empty">
                                        <text class="mc-metric-empty-val">—— </text>
                                        <text class="mc-metric-empty-unit">mmol/L</text>
                                    </view>
                                </view>
                            </view>
                        </view>
                    </view>
                </view>

                <!-- 稳糖乐园 -->
               

                <!-- 邀请家人点赞 -->
                <view
                    class="mc-card mc-social-card"
                    @click="onFamilyLikeStripTap"
                >
                    <view class="mc-social-left">
                        <view class="mc-social-icon">
                            <TongjiIcon name="heart" size="md" color="#a43a3a" />
                        </view>
                        <view>
                            <text class="mc-social-title">邀请家人点赞</text>
                            <text class="mc-social-sub">分享战报给家人，邀请他们为您点赞鼓励</text>
                        </view>
                    </view>
                    <TongjiIcon name="chevron-right" size="md" color="#6c7a71" />
                </view>
            </template>

            <!-- 每日鼓励（code.html Status Tip） -->
            <view v-if="diagnosisId" class="mc-encourage" @click="onEncourageTap">
                <view class="mc-encourage-icon-wrap">
                    <text class="mc-encourage-icon">💬</text>
                </view>
                <view class="mc-encourage-body">
                    <text class="mc-encourage-text">
                        <text>{{ encourageParts.prefix }}</text><text v-if="encourageParts.highlight" class="mc-encourage-highlight">{{ encourageParts.highlight }}</text><text>{{ encourageParts.suffix }}</text>
                    </text>
                   
                </view>
            </view>

            <!-- 就诊卡（Care List Horizontal） -->
            <scroll-view v-if="cards.length > 0" scroll-x class="mc-card-scroll" :show-scrollbar="false">
                <view class="mc-card-row">
                    <view class="mc-card-chip mc-card-chip-count-only">
                        <text class="mc-card-chip-count">就诊卡</text>
                        <text class="mc-card-chip-num">{{ cards.length }} 张</text>
                    </view>
                    <view
                        v-for="card in cards"
                        :key="card.id"
                        class="mc-card-chip"
                        :class="{ active: Number(card.id) === Number(diagnosisId) }"
                        @click="onSelectCard(card)"
                    >
                        <text>{{ card.patient_name || '未填' }}</text>
                        <text v-if="card.age" class="mc-card-chip-meta">{{ card.age }}岁</text>
                    </view>
                </view>
            </scroll-view>
        </view>

        <view class="mc-footer-tip">
            <text>数据每日同步 · 阈值仅作参考，请遵医嘱</text>
        </view>

        <!-- 录入今日血糖 -->
        <view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
            <view class="input-modal" @click.stop>
                <view class="input-modal-grip" />
                <view class="input-modal-head">
                    <view class="input-modal-title-wrap">
                        <text class="input-modal-title">{{ inputForm.id ? '修改今日血糖' : '录入今日血糖' }}</text>
                        <text class="input-modal-sub">{{ todayText }} · {{ patientName || '本人' }}</text>
                    </view>
                    <view class="input-modal-close" @click="closeInputForm">
                        <text class="input-modal-close-icon">×</text>
                    </view>
                </view>
                <view v-if="inputForm.loading" class="input-modal-loading">
                    <text>加载今日数据中…</text>
                </view>
                <scroll-view
                    v-else
                    class="input-modal-body input-modal-body-ready"
                    :scroll-y="!manualVoicePressing"
                    @touchend="onPageVoiceTouchEnd"
                    @touchcancel="onPageVoiceTouchEnd"
                >
                    <view class="input-modal-tip">
                        <text>请如实填写，留空表示该项不上传。当天可多次修改。</text>
                    </view>

                    <!-- 语音录入区：长按为主、一问一答为辅 -->
                    <view v-if="sttSupported" class="voice-entry-section">
                        <view class="voice-entry-head">
                            <text class="voice-entry-title">语音录入</text>
                            <text class="voice-entry-desc">选一种方式，自动填入下方字段</text>
                        </view>

                        <view
                            class="voice-input-bar voice-input-bar--primary"
                            :class="{
                                active: isVoiceBarLive('glucose'),
                                muted: voiceDialog.active
                            }"
                            @touchstart.stop="startVoiceInput('glucose')"
                            @touchend="endVoiceInput"
                            @touchcancel="endVoiceInput"
                            @touchmove.stop.prevent="blockVoiceTouchMove"
                        >
                            <view class="voice-input-accent" />
                            <view class="voice-input-badge">推荐</view>
                            <view class="voice-input-icon">
                                <VoiceSpeakMascot
                                    :active="isVoiceBarLive('glucose')"
                                    :on-dark="isVoiceBarLive('glucose')"
                                />
                            </view>
                            <view class="voice-input-text">
                                <text class="voice-input-title">{{ isVoiceBarLive('glucose') ? '正在聆听…松手结束' : '长按说话，自动填写' }}</text>
                                <text class="voice-input-hint">例：空腹6.5，餐后8.2</text>
                            </view>
                            <!-- <view v-if="!isManualVoiceActive('glucose')" class="voice-input-hold-pill">
                                <text>按住说话</text>
                            </view> -->
                        </view>

                        <!-- <view class="voice-entry-or">
                            <view class="voice-entry-or-line" />
                            <text class="voice-entry-or-text">或</text>
                            <view class="voice-entry-or-line" />
                        </view> -->

                        <!-- <view class="voice-dialog voice-dialog--secondary" :class="{ active: voiceDialog.active }">
                            <view class="voice-dialog-head">
                                <view class="voice-dialog-title-wrap">
                                    <view class="voice-dialog-orb" :class="{ listening: voiceDialog.phase === 'listening', speaking: voiceDialog.phase === 'asking' }">
                                        <TongjiIcon name="volume" size="sm" :color="voiceDialog.active ? '#ffffff' : '#006c49'" />
                                    </view>
                                    <view class="voice-dialog-title-text">
                                        <view class="voice-dialog-title-row">
                                            <text class="voice-dialog-title">语音问答录入</text>
                                            <text v-if="!voiceDialog.active" class="voice-dialog-tag">可选</text>
                                        </view>
                                        <text class="voice-dialog-tip">{{ voiceDialog.active ? (voiceDialog.phase === 'listening' ? '按住下方作答区说话，松手结束' : '跟着提问回答数字即可自动填写') : '逐项提问，适合不方便一次性说完' }}</text>
                                    </view>
                                </view>
                                <view v-if="voiceDialog.active" class="voice-dialog-stop" @click.stop="stopVoiceDialog()">
                                    <text>结束</text>
                                </view>
                                <view v-else class="voice-dialog-start" @click="startGlucoseVoiceDialog">
                                    <text>开始问答</text>
                                </view>
                            </view>

                            <view v-if="voiceDialog.active" class="voice-dialog-body">
                                <view class="voice-dialog-bubble bot">
                                    <text>{{ voiceDialog.botText }}</text>
                                </view>
                                <view v-if="voiceDialog.heardText" class="voice-dialog-bubble user">
                                    <text>{{ voiceDialog.heardText }}</text>
                                </view>
                                <view
                                    v-if="voiceDialog.phase === 'listening'"
                                    class="voice-dialog-hold-pad"
                                    :class="{ active: sttListening || sttHolding }"
                                    @touchstart.stop="onDialogAnswerTouchStart"
                                    @touchend="onDialogAnswerTouchEnd"
                                    @touchcancel="onDialogAnswerTouchEnd"
                                    @touchmove.stop.prevent="blockVoiceTouchMove"
                                >
                                    <view class="voice-dialog-hold-icon">
                                        <TongjiIcon name="mic" size="md" :color="(sttListening || sttHolding) ? '#ffffff' : '#006c49'" />
                                    </view>
                                    <text class="voice-dialog-hold-title">{{ (sttListening || sttHolding) ? '正在聆听…松手结束' : '按住这里回答' }}</text>
                                    <text v-if="voiceDialog.heardText" class="voice-dialog-hold-heard">{{ voiceDialog.heardText }}</text>
                                </view>
                                <view v-else class="voice-dialog-status">
                                    <text class="voice-dialog-status-text">{{ voiceDialogStatusText }}</text>
                                </view>
                                <view class="voice-dialog-actions">
                                    <view class="voice-dialog-btn" @click.stop="replayCurrentQuestion">
                                        <text>{{ voiceDialog.phase === 'listening' ? '重说' : '重听' }}</text>
                                    </view>
                                    <view
                                        v-if="voiceDialog.stage !== 'confirm'"
                                        class="voice-dialog-btn"
                                        @click.stop="skipCurrentStepManually"
                                    >
                                        <text>跳过本项</text>
                                    </view>
                                </view>
                            </view>
                        </view> -->
                    </view>
                    <view class="input-section-title">血糖（mmol/L）</view>
                    <view class="input-field">
                        <view class="input-field-label">
                            <view class="input-field-dot input-field-dot-fasting" />
                            <text>空腹血糖</text>
                        </view>
                        <view class="input-field-input">
                            <input
                                type="digit"
                                :value="inputForm.fasting_blood_sugar"
                                placeholder="如 6.5"
                                placeholder-class="input-placeholder"
                                @input="onInputField('fasting_blood_sugar', $event)"
                            />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label">
                            <view class="input-field-dot input-field-dot-postprandial" />
                            <text>餐后血糖</text>
                        </view>
                        <view class="input-field-input">
                            <input
                                type="digit"
                                :value="inputForm.postprandial_blood_sugar"
                                placeholder="如 8.0"
                                placeholder-class="input-placeholder"
                                @input="onInputField('postprandial_blood_sugar', $event)"
                            />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label">
                            <view class="input-field-dot input-field-dot-other" />
                            <text>其他血糖</text>
                        </view>
                        <view class="input-field-input">
                            <input
                                type="digit"
                                :value="inputForm.other_blood_sugar"
                                placeholder="如 7.2"
                                placeholder-class="input-placeholder"
                                @input="onInputField('other_blood_sugar', $event)"
                            />
                        </view>
                    </view>
                    <view class="input-section-title">备注</view>
                    <view class="input-field input-field-textarea">
                        <textarea
                            :value="inputForm.remark"
                            placeholder="如：饭前空腹测量、餐后2小时测量等（选填）"
                            placeholder-class="input-placeholder"
                            :show-confirm-bar="false"
                            :auto-height="true"
                            @input="onInputField('remark', $event)"
                        />
                    </view>
                </scroll-view>
                <view class="input-modal-actions">
                    <view
                        v-if="inputForm.id"
                        class="input-modal-btn danger"
                        :class="{ disabled: inputForm.submitting }"
                        @click="deleteInputRecord"
                    >
                        <text>删除</text>
                    </view>
                    <view
                        class="input-modal-btn primary"
                        :class="{ disabled: inputForm.submitting || inputForm.loading }"
                        @click="submitInputForm"
                    >
                        <text>{{ inputForm.submitting ? '提交中…' : (inputForm.id ? '保存修改' : '提交录入') }}</text>
                    </view>
                </view>
            </view>
        </view>

        <!-- 录入血压弹层 -->
        <view v-if="bpForm.visible" class="input-modal-mask" @click="closeBpForm">
            <view class="input-modal" @click.stop>
                <view class="input-modal-grip" />
                <view class="input-modal-head">
                    <view class="input-modal-title-wrap">
                        <text class="input-modal-title">{{ bpForm.id ? '修改今日血压' : '录入今日血压' }}</text>
                        <text class="input-modal-sub">{{ todayText }} · {{ patientName || '本人' }}</text>
                    </view>
                    <view class="input-modal-close" @click="closeBpForm"><text class="input-modal-close-icon">×</text></view>
                </view>
                <view v-if="bpForm.loading" class="input-modal-loading"><text>加载中…</text></view>
                <scroll-view
                    v-else
                    class="input-modal-body"
                    :scroll-y="!manualVoicePressing"
                    @touchend="onPageVoiceTouchEnd"
                    @touchcancel="onPageVoiceTouchEnd"
                >
                    <view
                        class="voice-input-bar"
                        :class="{ active: isVoiceBarLive('bp') }"
                        @touchstart.stop="startVoiceInput('bp')"
                        @touchend="endVoiceInput"
                        @touchcancel="endVoiceInput"
                        @touchmove.stop.prevent="blockVoiceTouchMove"
                    >
                        <view class="voice-input-icon">
                            <VoiceSpeakMascot
                                :active="isVoiceBarLive('bp')"
                                :on-dark="isVoiceBarLive('bp')"
                            />
                        </view>
                        <view class="voice-input-text">
                            <text class="voice-input-title">{{ isVoiceBarLive('bp') ? '正在聆听…松手结束' : '长按说话，自动填写' }}</text>
                            <text class="voice-input-hint">例：高压120，低压80</text>
                        </view>
                    </view>
                    <view class="input-section-title">血压（mmHg）</view>
                    <view class="input-field-row">
                        <view class="input-field input-field-half">
                            <view class="input-field-label"><text>高压</text></view>
                            <view class="input-field-input">
                                <input type="number" :value="bpForm.systolic_pressure" placeholder="如 120" placeholder-class="input-placeholder" @input="onBpField('systolic_pressure', $event)" />
                            </view>
                        </view>
                        <view class="input-field input-field-half">
                            <view class="input-field-label"><text>低压</text></view>
                            <view class="input-field-input">
                                <input type="number" :value="bpForm.diastolic_pressure" placeholder="如 80" placeholder-class="input-placeholder" @input="onBpField('diastolic_pressure', $event)" />
                            </view>
                        </view>
                    </view>
                    <view class="input-section-title">用药（选填）</view>
                    <view class="input-field">
                        <view class="input-field-label"><text>西药</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="bpForm.western_medicine" placeholder="如：二甲双胍" placeholder-class="input-placeholder" @input="onBpField('western_medicine', $event)" />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>胰岛素</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="bpForm.insulin" placeholder="如：门冬 8U" placeholder-class="input-placeholder" @input="onBpField('insulin', $event)" />
                        </view>
                    </view>
                </scroll-view>
                <view class="input-modal-actions">
                    <view v-if="bpForm.id" class="input-modal-btn danger" :class="{ disabled: bpForm.submitting }" @click="deleteBpRecord"><text>删除</text></view>
                    <view class="input-modal-btn primary" :class="{ disabled: bpForm.submitting || bpForm.loading }" @click="submitBpForm"><text>{{ bpForm.submitting ? '提交中…' : '保存' }}</text></view>
                </view>
            </view>
        </view>

        <!-- 录入今日饮食 -->
        <view v-if="dietForm.visible" class="input-modal-mask" @click="closeDietForm">
            <view class="input-modal" @click.stop>
                <view class="input-modal-grip" />
                <view class="input-modal-head">
                    <view class="input-modal-title-wrap">
                        <text class="input-modal-title">记今日饮食</text>
                        <text class="input-modal-sub">{{ todayText }}</text>
                    </view>
                    <view class="input-modal-close" @click="closeDietForm"><text class="input-modal-close-icon">×</text></view>
                </view>
                <view v-if="dietForm.loading" class="input-modal-loading"><text>加载中…</text></view>
                <scroll-view
                    v-else
                    class="input-modal-body"
                    :scroll-y="!manualVoicePressing"
                    @touchend="onPageVoiceTouchEnd"
                    @touchcancel="onPageVoiceTouchEnd"
                >
                    <view v-if="dietAiPrefill && dietAiPrefill.breakfast" class="input-modal-ai-tip">
                        <text class="input-modal-ai-tip-label">AI 推荐</text>
                        <text class="input-modal-ai-tip-text">早 {{ dietAiPrefill.breakfast }}<text v-if="dietAiPrefill.drinks"> · 喝 {{ dietAiPrefill.drinks }}</text> · 午 {{ dietAiPrefill.lunch }} · 晚 {{ dietAiPrefill.dinner }}</text>
                        <view class="input-modal-ai-tip-btn" @click="fillDietFormFromAi">
                            <text>一键填入</text>
                        </view>
                    </view>
                    <view class="input-modal-tip"><text>至少填写一餐，完成后可浇水领取积分。</text></view>
                    <view
                        class="voice-input-bar"
                        :class="{ active: isVoiceBarLive('diet') }"
                        @touchstart.stop="startVoiceInput('diet')"
                        @touchend="endVoiceInput"
                        @touchcancel="endVoiceInput"
                        @touchmove.stop.prevent="blockVoiceTouchMove"
                    >
                        <view class="voice-input-icon">
                            <VoiceSpeakMascot
                                :active="isVoiceBarLive('diet')"
                                :on-dark="isVoiceBarLive('diet')"
                            />
                        </view>
                        <view class="voice-input-text">
                            <text class="voice-input-title">{{ isVoiceBarLive('diet') ? '正在聆听…松手结束' : '长按说话，自动填写' }}</text>
                            <text class="voice-input-hint">例：早餐鸡蛋粥，午餐米饭炒青菜</text>
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>早餐</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="dietForm.breakfast_foods" placeholder="如：玉米碴粥、鸡蛋" placeholder-class="input-placeholder" @input="onDietField('breakfast_foods', $event)" />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>午餐</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="dietForm.lunch_foods" placeholder="如：二米饭、炖白菜豆腐" placeholder-class="input-placeholder" @input="onDietField('lunch_foods', $event)" />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>晚餐</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="dietForm.dinner_foods" placeholder="如：杂面窝头、炒茄子" placeholder-class="input-placeholder" @input="onDietField('dinner_foods', $event)" />
                        </view>
                    </view>
                    <view class="input-field input-field-textarea">
                        <textarea :value="dietForm.note" placeholder="饮食备注（选填）" placeholder-class="input-placeholder" :show-confirm-bar="false" :auto-height="true" @input="onDietField('note', $event)" />
                    </view>
                </scroll-view>
                <view class="input-modal-actions">
                    <view class="input-modal-btn primary" :class="{ disabled: dietForm.submitting }" @click="submitDietForm">
                        <text>{{ dietForm.submitting ? '提交中…' : '保存饮食' }}</text>
                    </view>
                </view>
            </view>
        </view>

        <!-- 录入今日运动 -->
        <view v-if="exerciseForm.visible" class="input-modal-mask" @click="closeExerciseForm">
            <view class="input-modal" @click.stop>
                <view class="input-modal-grip" />
                <view class="input-modal-head">
                    <view class="input-modal-title-wrap">
                        <text class="input-modal-title">记今日运动</text>
                        <text class="input-modal-sub">{{ todayText }}</text>
                    </view>
                    <view class="input-modal-close" @click="closeExerciseForm"><text class="input-modal-close-icon">×</text></view>
                </view>
                <view v-if="exerciseForm.loading" class="input-modal-loading"><text>加载中…</text></view>
                <scroll-view
                    v-else
                    class="input-modal-body"
                    :scroll-y="!manualVoicePressing"
                    @touchend="onPageVoiceTouchEnd"
                    @touchcancel="onPageVoiceTouchEnd"
                >
                    <view class="input-modal-tip"><text>填写运动类型或时长，完成后可浇水领取积分。</text></view>
                    <view
                        class="voice-input-bar"
                        :class="{ active: isVoiceBarLive('exercise') }"
                        @touchstart.stop="startVoiceInput('exercise')"
                        @touchend="endVoiceInput"
                        @touchcancel="endVoiceInput"
                        @touchmove.stop.prevent="blockVoiceTouchMove"
                    >
                        <view class="voice-input-icon">
                            <VoiceSpeakMascot
                                :active="isVoiceBarLive('exercise')"
                                :on-dark="isVoiceBarLive('exercise')"
                            />
                        </view>
                        <view class="voice-input-text">
                            <text class="voice-input-title">{{ isVoiceBarLive('exercise') ? '正在聆听…松手结束' : '长按说话，自动填写' }}</text>
                            <text class="voice-input-hint">例：散步30分钟，中强度</text>
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>运动类型</text></view>
                        <view class="input-field-input">
                            <input type="text" :value="exerciseForm.exercise_type" placeholder="如：散步、太极拳" placeholder-class="input-placeholder" @input="onExerciseField('exercise_type', $event)" />
                        </view>
                    </view>
                    <view class="input-field">
                        <view class="input-field-label"><text>时长（分钟）</text></view>
                        <view class="input-field-input">
                            <input type="number" :value="exerciseForm.duration" placeholder="如：30" placeholder-class="input-placeholder" @input="onExerciseField('duration', $event)" />
                        </view>
                    </view>
                    <view class="input-section-title">运动强度</view>
                    <view class="intensity-row">
                        <view
                            v-for="opt in exerciseIntensityOptions"
                            :key="opt.value"
                            class="intensity-chip"
                            :class="{ active: exerciseForm.intensity === opt.value }"
                            @click="exerciseForm.intensity = opt.value"
                        >
                            <text>{{ opt.label }}</text>
                        </view>
                    </view>
                </scroll-view>
                <view class="input-modal-actions">
                    <view class="input-modal-btn primary" :class="{ disabled: exerciseForm.submitting }" @click="submitExerciseForm">
                        <text>{{ exerciseForm.submitting ? '提交中…' : '保存运动' }}</text>
                    </view>
                </view>
            </view>
        </view>

        <!-- 语音录音浮层：仅非弹窗场景使用；录入弹窗内由卡片自身状态提示 -->
        <view v-if="(sttHolding || sttListening) && !recordOverlayOpen && !voiceDialog.active" class="voice-rec-overlay">
            <view class="voice-rec-card">
                <view class="voice-rec-icon" :class="{ active: sttListening }">
                    <TongjiIcon name="mic" size="lg" color="#ffffff" />
                </view>
                <text class="voice-rec-title">{{ sttListening ? '正在聆听…' : '准备录音…' }}</text>
                <text class="voice-rec-sub">松开手指结束并识别</text>
            </view>
        </view>

        <!-- 本周战报（脱敏，可分享）-->
        <view v-if="weeklyReportVisible" class="report-mask" @click="closeWeeklyReport">
            <view class="report-panel" @click.stop>
                <view class="report-panel-head">
                    <text class="report-panel-title">本周稳糖战报</text>
                    <text class="report-panel-close" @click="closeWeeklyReport">×</text>
                </view>
                <view class="report-poster">
                    <view class="report-poster-top">
                        <SugarTreeGraphic :level="reportTreeLevel" size="md" />
                        <text class="report-poster-name">{{ weeklyReportData.patientLabel }}</text>
                    </view>
                    <text class="report-poster-week">{{ weeklyReportData.weekLabel }}</text>
                    <view class="report-stat-grid">
                        <view class="report-stat-item">
                            <text class="report-stat-num">{{ weeklyReportData.recordDays }}</text>
                            <text class="report-stat-label">天有记录</text>
                        </view>
                        <view class="report-stat-item">
                            <text class="report-stat-num">{{ weeklyReportData.completeDays }}</text>
                            <text class="report-stat-label">完整一日</text>
                        </view>
                        <view class="report-stat-item">
                            <text class="report-stat-num">{{ weeklyReportData.streakDays }}</text>
                            <text class="report-stat-label">连续天数</text>
                        </view>
                    </view>
                    <text class="report-poster-quote">{{ weeklyReportData.quote }}</text>
                    <text class="report-poster-disclaimer">习惯参考，不替代医嘱 · 家人打开分享可查看近7日记录</text>
                </view>
                <view class="report-actions">
                    <!-- #ifdef MP-WEIXIN -->
                    <button class="report-share-btn" open-type="share" @click="prepareShareWeekly">分享给家人</button>
                    <!-- #endif -->
                    <!-- #ifndef MP-WEIXIN -->
                    <view class="report-share-btn" @click="shareWeeklyFallback"><text>分享给家人</text></view>
                    <!-- #endif -->
                    <view class="report-secondary-btn" @click="closeWeeklyReport"><text>关闭</text></view>
                </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>

<script setup>
import { ref, reactive, computed, getCurrentInstance, nextTick } from 'vue'
import { onLoad, onShow, onHide, onUnload, onPullDownRefresh, onShareAppMessage } from '@dcloudio/uni-app'
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, 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'

const { proxy } = getCurrentInstance()

/** 将接口 msg / 异常对象转为可展示的字符串，避免 [object Object] */
function formatUserMessage(msg, fallback = '') {
    if (msg == null || msg === '') return fallback
    if (typeof msg === 'string') return msg
    if (typeof msg === 'number' || typeof msg === 'boolean') return String(msg)
    if (Array.isArray(msg)) {
        const parts = msg.map((item) => {
            if (item == null) return ''
            if (typeof item === 'string') return item
            if (typeof item === 'number' || typeof item === 'boolean') return String(item)
            if (typeof item === 'object') {
                return item.msg || item.message || item.error || item.title || item.label || ''
            }
            return String(item)
        }).filter(Boolean)
        return parts.length ? parts.join('；') : fallback
    }
    if (typeof msg === 'object') {
        return msg.msg || msg.message || msg.error || msg.title || msg.label || fallback
    }
    return String(msg)
}

function showUserToast(title, options = {}) {
    const text = formatUserMessage(title, '')
    if (!text) return
    uni.showToast({
        title: text,
        icon: options.icon || 'none',
        duration: typeof options.duration === 'number' ? options.duration : 2000
    })
}

const diagnosisId = ref(0)
const patientId = ref(0)
const patientName = ref('')
const headerStyle = ref({})
const headerInnerStyle = ref({})
const mainPadStyle = ref({})
const age = ref(0)
const gender = ref(0)

/** 登录 / 就诊卡门禁校验中（避免未登录时闪屏） */
const authChecking = ref(false)
let gateRedirected = false

const loading = ref(false)
const cardsLoading = ref(false)
const cards = ref([])
const rangeMode = ref('7')
const rangeOptions = [
    { value: '7', label: '最近 7 天' },
    { value: '30', label: '最近 30 天' }
]

const diagnosisTypeMap = {
    first_visit: '初诊',
    follow_up: '复诊',
    consultation: '会诊'
}

const syndromeTypeMap = {
    qi_deficiency: '气虚',
    blood_deficiency: '血虚',
    yin_deficiency: '阴虚',
    yang_deficiency: '阳虚',
    qi_stagnation: '气滞',
    blood_stasis: '血瘀',
    phlegm_dampness: '痰湿',
    damp_heat: '湿热',
    cold_dampness: '寒湿',
    wind_cold: '风寒',
    wind_heat: '风热'
}

function getCardSubtitle(card) {
    if (!card) return ''
    const parts = []
    if (card.gender_desc) parts.push(card.gender_desc)
    else if (Number(card.gender) === 1) parts.push('男')
    else if (Number(card.gender) === 2 || Number(card.gender) === 0) parts.push('女')
    if (card.age) parts.push(`${card.age}岁`)
    const dt = diagnosisTypeMap[card.diagnosis_type] || card.diagnosis_type
    if (dt) parts.push(dt)
    const st = syndromeTypeMap[card.syndrome_type] || card.syndrome_type
    if (st) parts.push(st)
    return parts.join(' · ')
}

const bloodRecords = ref([])
const dietRecords = ref([])
const exerciseRecords = ref([])
const trackingNotes = ref([])

const hoverInfo = ref(null)
let hoverTimer = null


const ageText = computed(() => (age.value > 0 ? `${age.value} 岁` : ''))
const genderText = computed(() => {
    const map = { 1: '男', 2: '女' }
    return map[Number(gender.value)] || ''
})
const rangeText = computed(() => {
    const opt = rangeOptions.find((o) => o.value === rangeMode.value)
    return opt ? opt.label : ''
})

// ============ 时间区间 ============
function formatDate(date) {
    const y = date.getFullYear()
    const m = String(date.getMonth() + 1).padStart(2, '0')
    const d = String(date.getDate()).padStart(2, '0')
    return `${y}-${m}-${d}`
}

function parseDate(str) {
    if (!str) return new Date()
    const [y, m, d] = str.split('-').map(Number)
    return new Date(y, (m || 1) - 1, d || 1)
}

const currentRange = computed(() => {
    const days = rangeMode.value === '30' ? 30 : 7
    const end = new Date()
    end.setHours(0, 0, 0, 0)
    const start = new Date(end)
    start.setDate(start.getDate() - (days - 1))
    return { start: formatDate(start), end: formatDate(end) }
})

const visibleDates = computed(() => {
    const { start, end } = currentRange.value
    if (!start || !end) return []
    const dates = []
    const cursor = parseDate(end)
    const lower = parseDate(start).getTime()
    while (cursor.getTime() >= lower) {
        dates.push(formatDate(cursor))
        cursor.setDate(cursor.getDate() - 1)
    }
    return dates
})

const chartDates = computed(() => [...visibleDates.value].reverse())

// ============ 数据聚合 ============
function toNumber(v) {
    if (v === null || v === undefined || v === '') return null
    const n = Number(v)
    return Number.isFinite(n) && n !== 0 ? n : null
}

const BLOOD_LOW_THRESHOLD = 3.9

function getBloodSugarThresholds(ageValue) {
    const a = Number(ageValue)
    // 年龄未知时用通用糖尿病控制目标兜底，避免高血糖不被标记
    if (!Number.isFinite(a) || a <= 0) return { fasting: 7, postprandial: 10 }
    if (a < 50) return { fasting: 7, postprandial: 9 }
    return { fasting: 8, postprandial: 10 }
}

function isLowGlucose(v) {
    const n = toNumber(v)
    return n !== null && n < BLOOD_LOW_THRESHOLD
}

function isHighFasting(v) {
    const n = toNumber(v)
    const t = getBloodSugarThresholds(age.value)
    return n !== null && t !== null && n >= t.fasting
}

function isHighPostprandial(v) {
    const n = toNumber(v)
    const t = getBloodSugarThresholds(age.value)
    return n !== null && t !== null && n >= t.postprandial
}

function isLowFasting(v) {
    return isLowGlucose(v) && !isHighFasting(v)
}

function isLowPostprandial(v) {
    return isLowGlucose(v) && !isHighPostprandial(v)
}

function isHighBp(row) {
    if (!row) return false
    const s = toNumber(row.systolic_pressure)
    const d = toNumber(row.diastolic_pressure)
    return (s !== null && s > 140) || (d !== null && d > 90)
}

function formatBp(row) {
    const s = row?.systolic_pressure
    const d = row?.diastolic_pressure
    if ((s === null || s === undefined || s === '' || s === 0) && (d === null || d === undefined || d === '' || d === 0)) return '—'
    return `${s || '—'} / ${d || '—'}`
}

function formatValue(v) {
    const n = toNumber(v)
    return n === null ? '—' : String(n)
}

function formatBloodMetric(v) {
    const n = toNumber(v)
    return n === null ? '——' : String(n)
}

const intensityTextMap = { 1: '低强度', 2: '中强度', 3: '高强度' }

const bloodByDate = computed(() => {
    const map = {}
    for (const r of bloodRecords.value) {
        const date = String(r.record_date || '')
        if (!date) continue
        if (!map[date]) {
            map[date] = {
                record_date: date,
                record_time: r.record_time || '',
                fasting_blood_sugar: '',
                postprandial_blood_sugar: '',
                other_blood_sugar: '',
                systolic_pressure: '',
                diastolic_pressure: '',
                western_medicine: '',
                insulin: '',
                remark: ''
            }
        }
        const merged = map[date]
        const fields = ['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar', 'systolic_pressure', 'diastolic_pressure', 'western_medicine', 'insulin', 'remark']
        for (const f of fields) {
            if ((merged[f] === '' || merged[f] === null || merged[f] === undefined) && r[f] !== '' && r[f] !== null && r[f] !== undefined) {
                merged[f] = r[f]
            }
        }
    }
    for (const date in map) {
        const m = map[date]
        m.isHighFasting = isHighFasting(m.fasting_blood_sugar)
        m.isHighPostprandial = isHighPostprandial(m.postprandial_blood_sugar)
        m.isHighOther = isHighPostprandial(m.other_blood_sugar)
        m.isLowFasting = isLowFasting(m.fasting_blood_sugar)
        m.isLowPostprandial = isLowPostprandial(m.postprandial_blood_sugar)
        m.isLowOther = isLowPostprandial(m.other_blood_sugar)
        m.isHighBp = isHighBp(m)
        m.bpText = formatBp(m)
        m.hasBpOrMed = m.bpText !== '—' || !!(m.western_medicine || m.insulin || m.remark)
    }
    return map
})

const dietByDate = computed(() => {
    const map = {}
    for (const r of dietRecords.value) {
        const date = String(r.record_date || '')
        if (!date || map[date]) continue
        map[date] = r
    }
    return map
})

const exerciseByDate = computed(() => {
    const map = {}
    for (const r of exerciseRecords.value) {
        const date = String(r.record_date || '')
        if (!date || map[date]) continue
        const item = { ...r }
        item.intensityText = intensityTextMap[Number(r.intensity)] || ''
        map[date] = item
    }
    return map
})

const trackingByDate = computed(() => {
    const map = {}
    for (const r of trackingNotes.value) {
        const date = String(r.note_date || '')
        if (!date || map[date]) continue
        map[date] = r
    }
    return map
})

const weekdayNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']

function getWeekday(dateStr) {
    return weekdayNames[parseDate(dateStr).getDay()] || ''
}

const todayStr = computed(() => formatDate(new Date()))

const recordDays = computed(() => {
    const result = []
    for (const date of visibleDates.value) {
        const bloodRaw = bloodByDate.value[date]
        const blood = bloodRaw && bloodRaw.hasBpOrMed ? bloodRaw : null
        const diet = dietByDate.value[date]
        const exercise = exerciseByDate.value[date]
        const tracking = trackingByDate.value[date]
        const hasAny = blood || diet || exercise || (tracking && tracking.content)
        if (!hasAny) continue
        const [y, m, d] = date.split('-')
        result.push({
            date,
            dateLabel: `${m}-${d}`,
            weekday: getWeekday(date),
            isToday: date === todayStr.value,
            blood,
            diet,
            exercise,
            tracking
        })
    }
    return result
})

const bloodRecordDays = computed(() => {
    const result = []
    for (const date of visibleDates.value) {
        const blood = bloodByDate.value[date]
        if (!blood) continue
        const hasSugar = toNumber(blood.fasting_blood_sugar) !== null
            || toNumber(blood.postprandial_blood_sugar) !== null
            || toNumber(blood.other_blood_sugar) !== null
        if (!hasSugar) continue
        const [y, m, d] = date.split('-')
        result.push({
            date,
            dateLabel: `${m}-${d}`,
            weekday: getWeekday(date),
            isToday: date === todayStr.value,
            blood
        })
    }
    return result
})

const bloodHasData = computed(() => {
    return chartDates.value.some((date) => {
        const b = bloodByDate.value[date]
        return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
    })
})

const bloodRecordDayCount = computed(() => {
    return chartDates.value.filter((date) => {
        const b = bloodByDate.value[date]
        return b && (toNumber(b.fasting_blood_sugar) !== null || toNumber(b.postprandial_blood_sugar) !== null)
    }).length
})

const bloodHighDayCount = computed(() => {
    return chartDates.value.filter((date) => {
        const b = bloodByDate.value[date]
        return b && (b.isHighFasting || b.isHighPostprandial)
    }).length
})

const bloodNormalDayCount = computed(() => bloodRecordDayCount.value - bloodHighDayCount.value)

// 趋势比较：dir = up/down/flat, delta = 与前一次的绝对差
// 注意：对于血糖/血压，「up = 升高 = 风险」，所以语义颜色按健康角度而非数学方向
function makeTrend(curr, prev, precision = 1) {
    if (curr == null || prev == null) return null
    const diff = Number(curr) - Number(prev)
    if (!Number.isFinite(diff)) return null
    const abs = Math.abs(diff)
    const dir = abs < 0.01 ? 'flat' : diff > 0 ? 'up' : 'down'
    const sign = dir === 'up' ? '+' : dir === 'down' ? '-' : ''
    const deltaText = dir === 'flat' ? '持平' : `${sign}${abs.toFixed(precision)}`
    return { dir, delta: abs, deltaText, prev }
}

// 找某字段在指定日期之前最近一条非空记录
function findPrev(field, beforeDate) {
    const dates = chartDates.value
    const idx = dates.indexOf(beforeDate)
    if (idx <= 0) return null
    for (let i = idx - 1; i >= 0; i--) {
        const b = bloodByDate.value[dates[i]]
        if (!b) continue
        const v = toNumber(b[field])
        if (v !== null) return { date: dates[i], value: v }
    }
    return null
}

function findPrevBp(beforeDate) {
    const dates = chartDates.value
    const idx = dates.indexOf(beforeDate)
    if (idx <= 0) return null
    for (let i = idx - 1; i >= 0; i--) {
        const b = bloodByDate.value[dates[i]]
        if (!b) continue
        const s = toNumber(b.systolic_pressure)
        const d = toNumber(b.diastolic_pressure)
        if (s !== null || d !== null) return { date: dates[i], systolic: s, diastolic: d }
    }
    return null
}

const latestStat = computed(() => {
    const dates = [...chartDates.value].reverse()
    const dummy = { value: '—', date: '暂无记录', isHigh: false, trend: null }
    const result = { fasting: { ...dummy }, postprandial: { ...dummy }, other: { ...dummy } }
    for (const date of dates) {
        const b = bloodByDate.value[date]
        if (!b) continue
        if (result.fasting.value === '—' && toNumber(b.fasting_blood_sugar) !== null) {
            const cur = toNumber(b.fasting_blood_sugar)
            const prev = findPrev('fasting_blood_sugar', date)
            result.fasting = {
                value: String(b.fasting_blood_sugar),
                date: date.slice(5),
                isHigh: b.isHighFasting,
                trend: prev ? makeTrend(cur, prev.value) : null
            }
        }
        if (result.postprandial.value === '—' && toNumber(b.postprandial_blood_sugar) !== null) {
            const cur = toNumber(b.postprandial_blood_sugar)
            const prev = findPrev('postprandial_blood_sugar', date)
            result.postprandial = {
                value: String(b.postprandial_blood_sugar),
                date: date.slice(5),
                isHigh: b.isHighPostprandial,
                trend: prev ? makeTrend(cur, prev.value) : null
            }
        }
        if (result.other.value === '—' && toNumber(b.other_blood_sugar) !== null) {
            const cur = toNumber(b.other_blood_sugar)
            const prev = findPrev('other_blood_sugar', date)
            result.other = {
                value: String(b.other_blood_sugar),
                date: date.slice(5),
                isHigh: b.isHighOther,
                trend: prev ? makeTrend(cur, prev.value) : null
            }
        }
        if (result.fasting.value !== '—' && result.postprandial.value !== '—' && result.other.value !== '—') break
    }
    return result
})

// 给某天某字段计算 day-over-day 趋势（用于日常记录列表的指标小标签）
function getDayTrend(date, field, precision = 1) {
    const b = bloodByDate.value[date]
    if (!b) return null
    const cur = toNumber(b[field])
    if (cur === null) return null
    const prev = findPrev(field, date)
    if (!prev) return null
    return makeTrend(cur, prev.value, precision)
}

function getDayBpTrend(date) {
    const b = bloodByDate.value[date]
    if (!b) return null
    const cur = toNumber(b.systolic_pressure)
    if (cur === null) return null
    const prev = findPrevBp(date)
    if (!prev || prev.systolic == null) return null
    return makeTrend(cur, prev.systolic, 0)
}

// ============ 登录 / 就诊卡门禁 ============
/** 进行中的登录任务（避免与 App.onLaunch 重复发起 mnpLogin，并保证业务接口在其之后） */
let authSessionPromise = null

function hasAuthToken() {
    return !!String(uni.getStorageSync('token') || '').trim()
}

function clearAuthStorage() {
    uni.removeStorageSync('token')
    authSessionPromise = null
}

function wxLoginGetCode() {
    return new Promise((resolve, reject) => {
        uni.login({
            provider: 'weixin',
            success: (res) => {
                if (res && res.code) resolve(res.code)
                else reject(new Error('微信登录未返回 code'))
            },
            fail: reject
        })
    })
}

async function mnpLoginWithCode(code) {
    const res = await proxy.apiUrl({
        url: '/api/login/mnpLogin',
        method: 'POST',
        data: { code }
    }, false)
    if (res && res.code === 1 && res.data && res.data.token) {
        uni.setStorageSync('token', res.data.token)
        uni.setStorageSync('userData', res.data)
        return res.data
    }
    clearAuthStorage()
    throw new Error(formatUserMessage(res?.msg, '登录失败'))
}

async function doWxLogin() {
    const code = await wxLoginGetCode()
    await mnpLoginWithCode(code)
    return hasAuthToken()
}

/** 校验 token 有效；无效则清除并重新 mnpLogin（缺 token 时后端 code=0，不能当作已登录） */
async function verifyOrLogin() {
    if (!hasAuthToken()) {
        return doWxLogin()
    }
    try {
        const res = await proxy.apiUrl({ url: '/api/user/info', method: 'POST' }, false)
        if (res && res.code === 1 && res.data) {
            uni.setStorageSync('userData', res.data)
            return true
        }
    } catch (e) {
        /* 网络异常走重新登录 */
    }
    clearAuthStorage()
    try {
        return await doWxLogin()
    } catch (e2) {
        return false
    }
}

/** 登录完成后再调业务接口；并发调用共享同一 Promise */
async function ensureLoggedIn() {
    if (authSessionPromise) {
        return authSessionPromise
    }
    authSessionPromise = verifyOrLogin()
        .then((ok) => {
            if (!ok) authSessionPromise = null
            return !!ok
        })
        .catch(() => {
            authSessionPromise = null
            return false
        })
    return authSessionPromise
}

function redirectToCardEntry() {
    if (gateRedirected) return
    gateRedirected = true
    const returnUrl = encodeURIComponent('/tongji/pages/more')
    uni.redirectTo({
        url: `/pages/Card/edit_card?add=1&returnUrl=${returnUrl}`
    })
}

// ============ 就诊卡 ============
async function fetchCardList() {
    if (!hasAuthToken()) return []
    cardsLoading.value = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/getCardList',
            method: 'GET',
            data: {}
        }, false)
        if (res && res.code === 1 && Array.isArray(res.data)) {
            cards.value = res.data
            return res.data
        }
        cards.value = []
        return []
    } catch (e) {
        cards.value = []
        return []
    } finally {
        cardsLoading.value = false
    }
}

async function applyCard(card) {
    if (!card || !hasAuthToken()) return
    diagnosisId.value = Number(card.id) || 0
    patientId.value = Number(card.patient_id) || 0
    patientName.value = card.patient_name || ''
    age.value = Number(card.age) || 0
    gender.value = Number(card.gender) || 0
    lastFamilyLikeCount.value = getStoredFamilyLikeSeen(Number(card.id) || 0)
    familyLikeSummary.value = {
        like_count: 0,
        summary_line: '分享战报给家人，邀请他们为您点赞鼓劲',
        recent: []
    }
    gamifyStatusTip.value = ''
    loadTreeSpecies()
    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 || res.data.linked_existing) {
            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 = []
    dietRecords.value = []
    exerciseRecords.value = []
    trackingNotes.value = []
    hoverInfo.value = null
    await applyCard(card)
    fetchAll()
}

// ============ 接口请求 ============
async function fetchAll() {
    if (!hasAuthToken() || !diagnosisId.value) return
    loading.value = true
    const { start, end } = currentRange.value
    try {
        const [winRes, noteRes] = await Promise.all([
            proxy.apiUrl({
                url: '/api/tcm/dailyTrackingWindow',
                method: 'GET',
                data: {
                    diagnosis_id: diagnosisId.value,
                    patient_id: patientId.value || 0,
                    start_date: start,
                    end_date: end
                }
            }, false),
            proxy.apiUrl({
                url: '/api/tcm/dailyTrackingNotes',
                method: 'GET',
                data: {
                    diagnosis_id: diagnosisId.value,
                    patient_id: patientId.value || 0
                }
            }, false)
        ])

        if (winRes && winRes.code === 1) {
            const d = winRes.data || {}
            bloodRecords.value = Array.isArray(d.blood_records) ? d.blood_records : []
            dietRecords.value = Array.isArray(d.diet_records) ? d.diet_records : []
            exerciseRecords.value = Array.isArray(d.exercise_records) ? d.exercise_records : []
            if (d.diagnosis) {
                if (!patientName.value) patientName.value = d.diagnosis.patient_name || ''
                if (!age.value) age.value = Number(d.diagnosis.age) || 0
                if (!gender.value) gender.value = Number(d.diagnosis.gender) || 0
                if (!patientId.value) patientId.value = Number(d.diagnosis.patient_id) || 0
            }
        } else {
            bloodRecords.value = []
            dietRecords.value = []
            exerciseRecords.value = []
            if (winRes?.msg) showUserToast(winRes.msg)
        }

        if (noteRes && noteRes.code === 1) {
            const list = Array.isArray(noteRes.data) ? noteRes.data : []
            trackingNotes.value = list.filter((it) => visibleDates.value.includes(String(it?.note_date || '')))
        } else {
            trackingNotes.value = []
        }
        // 拉取游戏化积分/小树状态
        await fetchGamifyState()
        await loadFamilyLikeSummary()
    } catch (e) {
        uni.showToast({ title: '加载失败，请稍后再试', icon: 'none' })
    } finally {
        loading.value = false
        // 自动播报：数据就绪后朗读一次最新摘要
        if (ttsAutoPlay.value && !recordOverlayOpen.value) {
            const token = ++ttsAutoToken
            setTimeout(() => {
                if (token === ttsAutoToken && !recordOverlayOpen.value) speakLatestSummary()
            }, 350)
        }
    }
}

function onChangeRange(value) {
    if (rangeMode.value === value) return
    rangeMode.value = value
    fetchAll()
}

function onRefresh() {
    fetchAll()
}

// ============ 时段问候 / 打卡 / 健康日历 ============
/** 时段问候：文案 + 统一 TongjiIcon（与全页图标体系一致）*/
function buildTimeGreetingMeta(hour = new Date().getHours()) {
    const h = Number(hour) || 0
    if (h < 5) return { text: '夜深了', icon: 'moon' }
    if (h < 11) return { text: '早上好', icon: 'sun' }
    if (h < 13) return { text: '中午好', icon: 'sun' }
    if (h < 18) return { text: '下午好', icon: 'sun' }
    if (h < 22) return { text: '晚上好', icon: 'sunset' }
    return { text: '夜深了', icon: 'moon' }
}

const timeGreetingMeta = computed(() => buildTimeGreetingMeta())

// 复用已有的 formatDate(date) -> 'YYYY-MM-DD'

function dayHasBloodRecord(key) {
    const b = bloodByDate.value[key]
    if (!b) return false
    return toNumber(b.fasting_blood_sugar) !== null ||
        toNumber(b.postprandial_blood_sugar) !== null ||
        toNumber(b.other_blood_sugar) !== null ||
        toNumber(b.systolic_pressure) !== null ||
        toNumber(b.diastolic_pressure) !== null
}

// 给一天打个完整度分（0-3）：0 无 / 1 仅 1 项 / 2 有 2~3 项 / 3 有 4 项以上
function dayCoverageLevel(key) {
    const b = bloodByDate.value[key]
    const dt = dietByDate.value[key]
    const ex = exerciseByDate.value[key]
    if (!b && !dt && !ex) return 0
    let score = 0
    if (b) {
        const fields = ['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar', 'systolic_pressure', 'diastolic_pressure']
        score += fields.filter((f) => toNumber(b[f]) !== null).length
    }
    if (dt && (dt.breakfast_foods || dt.lunch_foods || dt.dinner_foods)) score += 1
    if (ex && (ex.exercise_type || ex.duration)) score += 1
    if (score === 0) return 0
    if (score === 1) return 1
    if (score <= 3) return 2
    return 3
}

// todayStr 已在上方定义，这里直接复用
const todayChecked = computed(() => dayHasBloodRecord(todayStr.value))

// 连续打卡天数：从今天往前回溯，连续有 blood 记录的天数；今天没打卡时从昨天起算（保留 streak 心态）
const streakDays = computed(() => {
    const today = new Date()
    let count = 0
    let startedFromYesterday = false
    for (let i = 0; i < 90; i++) {
        const d = new Date(today)
        d.setDate(today.getDate() - i)
        const key = formatDate(d)
        const has = dayHasBloodRecord(key)
        if (has) {
            count++
        } else {
            if (i === 0) {
                // 今天还没测：从昨天开始算 streak（不打断）
                startedFromYesterday = true
                continue
            }
            break
        }
    }
    return { days: count, startedFromYesterday }
})

// 近 7 天打卡情况：从 6 天前到今天
const last7Dots = computed(() => {
    const arr = []
    const today = new Date()
    const wkChars = ['日', '一', '二', '三', '四', '五', '六']
    for (let i = 6; i >= 0; i--) {
        const d = new Date(today)
        d.setDate(today.getDate() - i)
        const key = formatDate(d)
        arr.push({
            date: key,
            label: wkChars[d.getDay()],
            isToday: i === 0,
            hasRecord: dayHasBloodRecord(key)
        })
    }
    return arr
})

const last7CheckedCount = computed(() => last7Dots.value.filter((d) => d.hasRecord).length)

function dayHasFastingRecord(key) {
    const b = bloodByDate.value[key]
    return !!(b && toNumber(b.fasting_blood_sugar) !== null)
}

function dayHasPostprandialRecord(key) {
    const b = bloodByDate.value[key]
    return !!(b && toNumber(b.postprandial_blood_sugar) !== null)
}

function dayIsCompleteRecord(key) {
    return dayHasFastingRecord(key) && dayHasPostprandialRecord(key)
}

function dayHasHighBloodSugar(key) {
    const b = bloodByDate.value[key]
    if (!b) return false
    return b.isHighFasting || b.isHighPostprandial
}

const weekCompleteDays = computed(() => last7Dots.value.filter((d) => dayIsCompleteRecord(d.date)).length)

// 健康日历热力图：取当前数据范围内所有日期，按周组装成 6x7（最多）网格
const heatmapWeeks = computed(() => {
    const dates = chartDates.value
    if (!dates || !dates.length) return []
    // 确保按时间正序
    const sorted = [...dates].sort()
    // 找到第一天所在周的周一，最后一天所在周的周日
    const first = new Date(sorted[0])
    const last = new Date(sorted[sorted.length - 1])
    // JS getDay: 0=Sun..6=Sat；我们以周一为起点
    const firstDow = (first.getDay() + 6) % 7 // 0=Mon..6=Sun
    const lastDow = (last.getDay() + 6) % 7
    const gridStart = new Date(first)
    gridStart.setDate(first.getDate() - firstDow)
    const gridEnd = new Date(last)
    gridEnd.setDate(last.getDate() + (6 - lastDow))

    const totalDays = Math.round((gridEnd - gridStart) / 86400000) + 1
    const weeks = []
    let cur = new Date(gridStart)
    for (let w = 0; w < Math.ceil(totalDays / 7); w++) {
        const week = []
        for (let i = 0; i < 7; i++) {
            const key = formatDate(cur)
            const inRange = sorted.includes(key)
            week.push({
                date: key,
                day: cur.getDate(),
                inRange,
                isToday: key === todayStr.value,
                level: inRange ? dayCoverageLevel(key) : -1,
                isHigh: inRange ? dayHasHighBloodSugar(key) : false
            })
            cur.setDate(cur.getDate() + 1)
        }
        weeks.push(week)
    }
    return weeks
})

const heatmapCells = computed(() => {
    const weeks = heatmapWeeks.value
    return weeks.length ? weeks.flat() : []
})

const heatmapTotalRecords = computed(() => {
    let c = 0
    for (const w of heatmapWeeks.value) for (const c2 of w) if (c2.inRange && c2.level > 0) c++
    return c
})

const heatmapInRangeDays = computed(() => {
    let c = 0
    for (const w of heatmapWeeks.value) for (const c2 of w) if (c2.inRange) c++
    return c
})

// 月历的"月份标签"：取热力图覆盖范围里跨度最大的月份
const heatmapMonthLabel = computed(() => {
    const weeks = heatmapWeeks.value
    if (!weeks.length) return ''
    const counter = new Map()
    for (const w of weeks) {
        for (const c of w) {
            if (!c.inRange) continue
            const [y, m] = c.date.split('-')
            const key = `${y}-${m}`
            counter.set(key, (counter.get(key) || 0) + 1)
        }
    }
    if (!counter.size) {
        const first = weeks[0][0].date.split('-')
        return `${Number(first[0])}年${Number(first[1])}月`
    }
    let best = ''
    let bestCount = -1
    for (const [k, v] of counter) {
        if (v > bestCount) { best = k; bestCount = v }
    }
    const [y, m] = best.split('-')
    return `${Number(y)}年${Number(m)}月`
})

function jumpToDayRecord(date) {
    // 滚动到下方对应日期 records 列表项；若不在 visible 列表则提示
    if (!bloodRecordDays.value.some((d) => d.date === date)) {
        uni.showToast({ title: '该日无记录', icon: 'none' })
        return
    }
    uni.createSelectorQuery().in(proxy)
        .select(`#day-${date.replace(/-/g, '')}`)
        .boundingClientRect()
        .selectViewport()
        .scrollOffset()
        .exec((res) => {
            if (!res || !res[0]) return
            const top = res[0].top + (res[1] ? res[1].scrollTop : 0) - 80
            uni.pageScrollTo({ scrollTop: top, duration: 280 })
        })
}

// ============ 录入今日数据 ============
const todayText = computed(() => {
    const d = new Date()
    const m = String(d.getMonth() + 1).padStart(2, '0')
    const day = String(d.getDate()).padStart(2, '0')
    const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
    return `${d.getFullYear()}-${m}-${day} ${weekdays[d.getDay()]}`
})

const emptyInputForm = () => ({
    visible: false,
    loading: false,
    submitting: false,
    id: 0,
    fasting_blood_sugar: '',
    postprandial_blood_sugar: '',
    other_blood_sugar: '',
    remark: ''
})
const inputForm = ref(emptyInputForm())

const emptyBpForm = () => ({
    visible: false,
    loading: false,
    submitting: false,
    id: 0,
    systolic_pressure: '',
    diastolic_pressure: '',
    western_medicine: '',
    insulin: '',
    remark: ''
})
const bpForm = ref(emptyBpForm())

function prepareRecordOverlay() {
    stopVoiceDialog(true)
    ttsStop()
}

function onInputField(field, e) {
    inputForm.value[field] = e?.detail?.value ?? ''
}

async function openInputForm() {
    if (!diagnosisId.value) {
        showUserToast('请先选择就诊卡')
        return
    }
    const token = uni.getStorageSync('token')
    if (!token) {
        showUserToast('请先登录后再录入')
        return
    }
    prepareRecordOverlay()
    inputForm.value = { ...emptyInputForm(), visible: true, loading: true }
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyTodayBloodRecord',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res && res.code === 1 && res.data && res.data.record) {
            const r = res.data.record
            inputForm.value.id = Number(r.id) || 0
            inputForm.value.fasting_blood_sugar = numToStr(r.fasting_blood_sugar)
            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) {
        /* 静默：拉取失败时仍允许新增录入 */
    } finally {
        inputForm.value.loading = false
    }
}

function closeInputForm() {
    if (inputForm.value.submitting) return
    cancelVoiceHold()
    stopVoiceDialog(true)
    inputForm.value.visible = false
}

async function submitInputForm() {
    if (inputForm.value.submitting || inputForm.value.loading) return
    if (voiceDialog.active) stopVoiceDialog(true)
    const f = inputForm.value
    const hasAny = [
        f.fasting_blood_sugar, f.postprandial_blood_sugar, f.other_blood_sugar
    ].some((v) => v !== '' && v !== null && Number(v) > 0) || !!(f.remark && f.remark.trim())
    if (!hasAny) {
        showUserToast('请至少填写一项数据')
        return
    }
    inputForm.value.submitting = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailySaveBloodRecord',
            method: 'POST',
            data: {
                diagnosis_id: diagnosisId.value,
                fasting_blood_sugar: f.fasting_blood_sugar,
                postprandial_blood_sugar: f.postprandial_blood_sugar,
                other_blood_sugar: f.other_blood_sugar,
                remark: f.remark
            }
        }, false)
        if (res && res.code === 1) {
            showUserToast(res.msg || '今日血糖已录入完成', { icon: 'success' })
            inputForm.value.visible = false
            await fetchAll()
            await fetchGamifyState()
            promptWaterTreeGuide('血糖')
        } else {
            showUserToast((res && res.msg) || '保存失败')
        }
    } catch (e) {
        showUserToast('网络异常，请稍后再试')
    } finally {
        inputForm.value.submitting = false
    }
}

function deleteInputRecord() {
    if (!inputForm.value.id || inputForm.value.submitting) return
    if (voiceDialog.active) stopVoiceDialog(true)
    uni.showModal({
        title: '删除确认',
        content: '确定删除今天已录入的血糖数据吗？删除后无法恢复。',
        confirmColor: '#dc2626',
        success: async (m) => {
            if (!m.confirm) return
            inputForm.value.submitting = true
            try {
                const res = await proxy.apiUrl({
                    url: '/api/tcm/dailyDeleteBloodRecord',
                    method: 'POST',
                    data: {
                        diagnosis_id: diagnosisId.value,
                        id: inputForm.value.id
                    }
                }, false)
                if (res && res.code === 1) {
                    uni.showToast({ title: '已删除', icon: 'success' })
                    inputForm.value.visible = false
                    await fetchAll()
                    await fetchGamifyState()
                } else {
                    showUserToast((res && res.msg) || '删除失败')
                }
            } catch (e) {
                uni.showToast({ title: '网络异常，请稍后再试', icon: 'none' })
            } finally {
                inputForm.value.submitting = false
            }
        }
    })
}

function onBpField(field, e) {
    const val = e?.detail?.value ?? ''
    bpForm.value[field] = val
}

async function openBpForm() {
    if (!diagnosisId.value) {
        showUserToast('请先选择就诊卡')
        return
    }
    const token = uni.getStorageSync('token')
    if (!token) {
        showUserToast('请先登录后再录入')
        return
    }
    // 微信小程序 canvas 是原生组件、z-index 无法被弹层覆盖，
    // 这里通过 v-if 卸载它；下次显示时 selectorQuery 拿到的会是新节点，
    // 因此先清空缓存的 ctx / node，避免下次 ensureCanvas 复用旧引用。
    prepareRecordOverlay()
    // 打开弹层，先拉取今日已有记录预填
    bpForm.value = { ...emptyBpForm(), visible: true, loading: true }
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyTodayBloodRecord',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res && res.code === 1 && res.data && res.data.record) {
            const r = res.data.record
            bpForm.value.id = Number(r.id) || 0
            bpForm.value.systolic_pressure = numToStr(r.systolic_pressure)
            bpForm.value.diastolic_pressure = numToStr(r.diastolic_pressure)
            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) {
        // 静默：拉取失败时仍允许新增录入
    } finally {
        bpForm.value.loading = false
    }
}

function numToStr(v) {
    if (v === null || v === undefined || v === '' || v === 0 || v === '0') return ''
    if (typeof v === 'object') return ''
    return String(v)
}

function closeBpForm() {
    if (bpForm.value.submitting) return
    cancelVoiceHold()
    bpForm.value.visible = false
    // 关闭后 canvas 会重新挂载，需触发一次重绘
    nextTick(() => {})
}

const emptyDietForm = () => ({
    visible: false,
    loading: false,
    submitting: false,
    id: 0,
    breakfast_foods: '',
    lunch_foods: '',
    dinner_foods: '',
    note: ''
})
const dietForm = ref(emptyDietForm())
const dietAiPrefill = ref(null)

function fillDietFormFromAi() {
    const r = dietAiPrefill.value
    if (!r) return
    if (r.breakfast) dietForm.value.breakfast_foods = r.breakfast
    if (r.lunch) dietForm.value.lunch_foods = r.lunch
    if (r.dinner) dietForm.value.dinner_foods = r.dinner
    if (r.tips) {
        const note = String(dietForm.value.note || '').trim()
        dietForm.value.note = note ? `${note}；${r.tips}` : r.tips
    }
}

function onDietField(field, e) {
    dietForm.value[field] = e?.detail?.value ?? ''
}

async function openDietForm(options = {}) {
    const autoFill = !!options.autoFill
    if (!dietAiPrefill.value) {
        dietAiPrefill.value = consumeDietAiPrefill()
    }
    if (!diagnosisId.value) {
        showUserToast('请先选择就诊卡')
        return
    }
    const token = uni.getStorageSync('token')
    if (!token) {
        showUserToast('请先登录后再录入')
        return
    }
    prepareRecordOverlay()
    dietForm.value = { ...emptyDietForm(), visible: true, loading: true }
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyTodayDietRecord',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res?.code === 1 && res.data?.record) {
            const r = res.data.record
            dietForm.value.id = Number(r.id) || 0
            dietForm.value.breakfast_foods = String(r.breakfast_foods || '')
            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 {
        dietForm.value.loading = false
        if (autoFill && dietAiPrefill.value?.breakfast) {
            fillDietFormFromAi()
        }
    }
}

function closeDietForm() {
    if (dietForm.value.submitting) return
    cancelVoiceHold()
    dietForm.value.visible = false
    dietAiPrefill.value = null
    nextTick(() => {})
}

async function submitDietForm() {
    if (dietForm.value.submitting || dietForm.value.loading) return
    const f = dietForm.value
    if (!f.breakfast_foods.trim() && !f.lunch_foods.trim() && !f.dinner_foods.trim()) {
        showUserToast('请至少填写一餐饮食')
        return
    }
    dietForm.value.submitting = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailySaveDietRecord',
            method: 'POST',
            data: {
                diagnosis_id: diagnosisId.value,
                breakfast_foods: f.breakfast_foods,
                lunch_foods: f.lunch_foods,
                dinner_foods: f.dinner_foods,
                note: f.note
            }
        }, false)
        if (res?.code === 1) {
            showUserToast(res.msg || '今日饮食已录入完成', { icon: 'success' })
            dietForm.value.visible = false
            await fetchAll()
            await fetchGamifyState()
            promptWaterTreeGuide('饮食')
        } else {
            showUserToast((res && res.msg) || '保存失败')
        }
    } catch (e) {
        showUserToast('网络异常，请稍后再试')
    } finally {
        dietForm.value.submitting = false
    }
}

const exerciseIntensityOptions = [
    { value: 1, label: '低强度' },
    { value: 2, label: '中强度' },
    { value: 3, label: '高强度' }
]
const emptyExerciseForm = () => ({
    visible: false,
    loading: false,
    submitting: false,
    id: 0,
    exercise_type: '',
    duration: '',
    intensity: 2,
    note: ''
})
const exerciseForm = ref(emptyExerciseForm())

/** 任一录入弹层打开时需卸载 canvas（小程序原生组件会盖住普通 view）*/
const recordOverlayOpen = computed(() =>
    inputForm.value.visible || bpForm.value.visible || dietForm.value.visible || exerciseForm.value.visible
)

// ============ 语音录入（长按说话，本地解析回填） ============
/** 当前正在语音录入的目标表单：'glucose' | 'bp' | 'diet' | 'exercise' */
const voiceTarget = ref('')

const {
    sttListening,
    sttHolding,
    sttSupported,
    beginHoldSpeech,
    endHoldSpeech
} = useSpeechToText({
    onResult(text) {
        // 语音一问一答进行中时，结果由 onSettle → 对话流程接管
        if (voiceDialog.active) return
        applyVoiceResult(text)
    },
    onSettle(text) {
        if (voiceDialog.active) {
            handleDialogSettle(text)
            return true
        }
        return false
    },
    onError(msg) {
        if (isBenignSttError(msg)) return true
        if (voiceDialog.active) {
            handleDialogError()
            return true
        }
        return false
    },
    onPartial(text) {
        if (voiceDialog.active) handleDialogPartial(text)
    },
    showToast: (msg) => showUserToast(msg)
})

/** 长按麦克风开始录音（短按不启动，避免误触） */
const VOICE_HOLD_DELAY_MS = 320
let voiceHoldTimer = null
let voiceHoldPendingTarget = ''
const manualVoicePressing = ref(false)
/** 按住期间锁定目标，避免 timer 与松手竞态 */
let manualVoicePressTarget = ''

function blockVoiceTouchMove() {
    /* 阻止 scroll-view 抢走 touch 序列，保证 touchend 能回到按住区域 */
}

function isManualVoiceActive(target) {
    return voiceTarget.value === target && (sttHolding.value || sttListening.value)
}

/** 按住尚未开录时也展示说话动画 */
function isVoicePressing(target) {
    return manualVoicePressing.value && manualVoicePressTarget === target
}

function isVoiceBarLive(target) {
    return isManualVoiceActive(target) || isVoicePressing(target)
}

function clearVoiceHoldTimer() {
    if (voiceHoldTimer) {
        clearTimeout(voiceHoldTimer)
        voiceHoldTimer = null
    }
}

function cancelVoiceHold() {
    manualVoicePressing.value = false
    manualVoicePressTarget = ''
    clearVoiceHoldTimer()
    voiceHoldPendingTarget = ''
    forceEndHoldSpeech()
}

/** 无论底层回调是否滞后，都清掉 UI 与按住态 */
function forceEndHoldSpeech() {
    if (sttHolding.value || sttListening.value) {
        endHoldSpeech()
    } else {
        endHoldSpeech(true)
    }
}

function startVoiceInput(target) {
    if (!sttSupported.value) {
        showUserToast('当前环境不支持语音输入')
        return
    }
    // 长按手动录入与一问一答互斥：手动开始时先停掉对话
    if (voiceDialog.active) stopVoiceDialog(true)
    manualVoicePressing.value = true
    manualVoicePressTarget = target
    clearVoiceHoldTimer()
    voiceHoldPendingTarget = target
    voiceHoldTimer = setTimeout(() => {
        voiceHoldTimer = null
        if (!manualVoicePressing.value || manualVoicePressTarget !== target || voiceHoldPendingTarget !== target) return
        voiceTarget.value = target
        beginHoldSpeech()
    }, VOICE_HOLD_DELAY_MS)
}

/** 松手结束录音 */
function endVoiceInput() {
    const wasPressing = manualVoicePressing.value
    const hadPendingOnly = !!voiceHoldPendingTarget
    const wasRecording = sttHolding.value || sttListening.value

    manualVoicePressing.value = false
    manualVoicePressTarget = ''
    clearVoiceHoldTimer()
    voiceHoldPendingTarget = ''

    if (wasRecording || wasPressing) {
        forceEndHoldSpeech()
    }

    if (hadPendingOnly && !wasRecording) {
        showUserToast('请长按说话', { icon: 'none', duration: 1500 })
    }
}

/** 页面 / scroll-view 级 touchend 兜底 */
function onPageVoiceTouchEnd() {
    if (manualVoicePressing.value) {
        if (voiceDialog.active && voiceDialog.phase === 'listening') {
            onDialogAnswerTouchEnd()
        } else {
            endVoiceInput()
        }
        return
    }
    // 按住态已丢但底层仍在录：强制收尾
    if ((sttHolding.value || sttListening.value) && voiceTarget.value && !voiceDialog.active) {
        endVoiceInput()
    }
}

/** 把识别文本解析后回填到对应表单 */
function applyVoiceResult(text) {
    const raw = String(text || '').trim()
    if (!raw) return
    const target = voiceTarget.value
    let parsed = {}
    let formRef = null
    if (target === 'glucose') {
        parsed = parseGlucose(raw)
        formRef = inputForm
    } else if (target === 'bp') {
        parsed = parseBloodPressure(raw)
        formRef = bpForm
    } else if (target === 'diet') {
        parsed = parseDiet(raw)
        formRef = dietForm
    } else if (target === 'exercise') {
        parsed = parseExercise(raw)
        formRef = exerciseForm
    }
    if (!formRef) return

    const keys = Object.keys(parsed)
    if (!keys.length) {
        // 没解析出结构化字段：饮食/血糖把原文写入备注，避免丢失
        if (target === 'diet') {
            appendToField(formRef, 'note', raw)
            showUserToast('已记入饮食备注，请确认')
        } else if (target === 'glucose') {
            appendToField(formRef, 'remark', raw)
            showUserToast('未识别到血糖数值，已记入备注')
        } else if (target === 'bp') {
            showUserToast('未识别到血压数值，请重试或手动填写')
        } else {
            showUserToast('没听清，请再说一次')
        }
        return
    }

    keys.forEach((k) => {
        if (k === 'note' || k === 'remark' || k === 'western_medicine' || k === 'insulin') {
            appendToField(formRef, k, parsed[k])
        } else {
            formRef.value[k] = parsed[k]
        }
    })
    const summary = summarizeParsed(parsed)
    showUserToast(summary ? `已识别：${summary}` : '已填写', { icon: 'none', duration: 2200 })
}

/** 文本类字段追加（避免覆盖已有内容） */
function appendToField(formRef, field, value) {
    const v = String(value || '').trim()
    if (!v) return
    const prev = String(formRef.value[field] || '').trim()
    formRef.value[field] = prev ? `${prev}；${v}` : v
}

// ============ 语音一问一答录入（默认关闭，逐项语音问询自动回填） ============
const VOICE_DIALOG_LISTEN_MS = 4000   // 问题播完后给用户的说话时长（兜底窗口）
const VOICE_DIALOG_PRE_MS = 250       // 读完问题到开始录音的缓冲，避开 TTS 尾音
const VOICE_DIALOG_ANSWER_MS = 550    // 听到数字后，用户停顿多久即定格（静默防抖，越小越快）
const VOICE_DIALOG_ANSWER_MAX_MS = 1800 // 听到数字后最长收尾时间（防止实时回调不断刷新而不定格）
const VOICE_DIALOG_MAX_RETRY = 2      // 没听清的最大重试次数
const VOICE_DIALOG_ERR_MAX_RETRY = 3  // 录音启动失败的最大自动重试次数
const VOICE_DIALOG_RETRY_MS = 600     // 录音失败后重试的间隔

/** 血糖语音问答的步骤定义 */
// 注意：采集类问题与录音并行，麦克风会录到问题回声，
// 因此问句中不要出现「跳过/没有」等指令词，否则回声会被误判为跳过。
const GLUCOSE_DIALOG_STEPS = [
    { field: 'fasting_blood_sugar', label: '空腹血糖', question: '请问空腹血糖是多少？' },
    { field: 'postprandial_blood_sugar', label: '餐后血糖', question: '餐后血糖是多少？' },
    { field: 'other_blood_sugar', label: '其他时段血糖', question: '其他时段血糖是多少？' }
]

const voiceDialog = reactive({
    active: false,        // 对话流程是否进行中
    phase: 'idle',        // idle | asking | listening （仅表示麦克风/播报状态）
    stage: 'collect',     // collect（逐项采集）| confirm（确认提交）
    stepIndex: 0,
    retries: 0,
    errorRetries: 0,      // 录音启动失败的自动重试计数
    barged: false,        // 用户是否已在问题播报中抢答
    pendingValue: '',     // 实时识别已捕获、待定格的数值
    botText: '',          // 当前机器人提示/问题
    heardText: '',        // 最近一次识别到的用户回答
    steps: GLUCOSE_DIALOG_STEPS,
    collected: []         // [{ label, value }]
})

let voiceListenTimer = null
let voiceAnswerTimer = null
let voiceFlowToken = 0    // 递增令牌，关闭/重启时让旧的异步回调失效

function clearVoiceListenTimer() {
    if (voiceListenTimer) {
        clearTimeout(voiceListenTimer)
        voiceListenTimer = null
    }
}

function clearAnswerTimer() {
    if (voiceAnswerTimer) {
        clearTimeout(voiceAnswerTimer)
        voiceAnswerTimer = null
    }
}

function clearVoiceTimers() {
    clearVoiceListenTimer()
    clearAnswerTimer()
}

/** 当前步骤 */
function currentDialogStep() {
    return voiceDialog.steps[voiceDialog.stepIndex] || null
}

/** 启动血糖语音问答 */
function startGlucoseVoiceDialog() {
    if (!sttSupported.value) {
        showUserToast('当前环境不支持语音输入')
        return
    }
    // 先停掉可能进行中的手动录音/旧对话
    endHoldSpeech()
    clearVoiceListenTimer()
    voiceFlowToken += 1
    voiceDialog.active = true
    voiceDialog.phase = 'asking'
    voiceDialog.stage = 'collect'
    voiceDialog.stepIndex = 0
    voiceDialog.retries = 0
    voiceDialog.errorRetries = 0
    voiceDialog.heardText = ''
    voiceDialog.collected = []
    askCurrentStep()
}

/**
 * 播报当前步骤的问题，读完后再开始聆听。
 * 微信平台已移除实时识别回调（onRecognize），无法做"读题中打断"，
 * 因此采用「读完短问题 → 按住作答区说话 → 松手结束并识别」的可靠模型。
 */
function askCurrentStep(prefix = '') {
    const step = currentDialogStep()
    if (!step) { finishVoiceDialog(); return }
    const token = voiceFlowToken
    voiceDialog.phase = 'asking'
    voiceDialog.errorRetries = 0
    voiceDialog.barged = false
    voiceDialog.pendingValue = ''
    voiceDialog.heardText = ''
    voiceDialog.botText = `${prefix}${step.question}`
    clearVoiceTimers()
    ttsSpeak(voiceDialog.botText, {
        force: true,
        onDone: () => {
            if (token !== voiceFlowToken || !voiceDialog.active) return
            listenForAnswer()
        }
    })
}

/** 到时自动结束录音（最长按住窗口，防止一直录） */
function armListenTimeout(token) {
    clearVoiceListenTimer()
    voiceListenTimer = setTimeout(() => {
        if (token !== voiceFlowToken || !voiceDialog.active) return
        if (voiceDialog.phase !== 'listening') return
        finishListeningEarly()
    }, VOICE_DIALOG_LISTEN_MS)
}

/** 问完题后进入聆听：等用户按住作答区再开录（不自动开麦） */
function listenForAnswer() {
    if (!voiceDialog.active) return
    const token = voiceFlowToken
    voiceDialog.pendingValue = ''
    voiceDialog.heardText = ''
    clearVoiceTimers()
    voiceDialog.phase = 'asking'
    voiceListenTimer = setTimeout(() => {
        if (token !== voiceFlowToken || !voiceDialog.active) return
        voiceDialog.phase = 'listening'
    }, VOICE_DIALOG_PRE_MS)
}

/** 问答聆听：按住作答区开始录音 */
function onDialogAnswerTouchStart() {
    if (!voiceDialog.active || voiceDialog.phase !== 'listening') return
    if (sttHolding.value || sttListening.value) return
    manualVoicePressing.value = true
    manualVoicePressTarget = 'dialog'
    clearVoiceTimers()
    const token = voiceFlowToken
    beginHoldSpeech()
    armListenTimeout(token)
}

/** 问答聆听：松手结束并识别 */
function onDialogAnswerTouchEnd() {
    manualVoicePressing.value = false
    manualVoicePressTarget = ''
    if (!voiceDialog.active || voiceDialog.phase !== 'listening') {
        if (sttHolding.value || sttListening.value) forceEndHoldSpeech()
        return
    }
    clearVoiceTimers()
    if (sttHolding.value || sttListening.value) {
        finishListeningEarly()
        return
    }
    forceEndHoldSpeech()
}

/**
 * 实时识别中间结果（仅在聆听阶段）。
 * 注意：微信当前已不再触发 onRecognize，此处为「若平台支持则提速」的增益逻辑——
 * 一旦实时拿到数字，用户停顿即定格、立即下一题；不支持时则走聆听窗口/「说完了」兜底。
 */
function handleDialogPartial(text) {
    if (!voiceDialog.active) return
    if (voiceDialog.stage !== 'collect') return
    if (voiceDialog.phase !== 'listening') return
    const value = extractGlucoseValue(text)
    if (value === null) return
    const token = voiceFlowToken
    voiceDialog.pendingValue = value
    voiceDialog.heardText = text
    if (!voiceDialog.barged) {
        // 首次拿到数字：取消默认长窗口，改为「停顿即定格」，并设绝对收尾上限
        voiceDialog.barged = true
        clearVoiceListenTimer()
        voiceListenTimer = setTimeout(() => {
            if (token !== voiceFlowToken || !voiceDialog.active) return
            finalizeCollectAnswer()
        }, VOICE_DIALOG_ANSWER_MAX_MS)
    }
    clearAnswerTimer()
    voiceAnswerTimer = setTimeout(() => {
        if (token !== voiceFlowToken || !voiceDialog.active) return
        finalizeCollectAnswer()
    }, VOICE_DIALOG_ANSWER_MS)
}

/** 以实时识别捕获到的数值定格当前采集项，并立即进入下一题 */
function finalizeCollectAnswer() {
    if (!voiceDialog.active) return
    const step = currentDialogStep()
    const value = voiceDialog.pendingValue
    voiceDialog.pendingValue = ''
    clearVoiceTimers()
    endHoldSpeech() // 停掉录音；其最终结果在切到下一题后由阶段判断忽略
    if (!step || !value) return
    inputForm.value[step.field] = value
    voiceDialog.collected.push({ label: step.label, value })
    voiceDialog.retries = 0
    gotoNextStep(`好的，${step.label}已记录。`)
}

/** 录音/识别启动失败（如 record manager recordfailed）：自动重试本题录音 */
function handleDialogError() {
    if (!voiceDialog.active) return
    clearVoiceTimers()
    if (voiceDialog.errorRetries < VOICE_DIALOG_ERR_MAX_RETRY) {
        voiceDialog.errorRetries += 1
        const token = voiceFlowToken
        // 暂离 listening，避免失败回调被当作答案处理；稍后自动重新录音（不重播问题）
        voiceDialog.phase = 'asking'
        voiceListenTimer = setTimeout(() => {
            if (token !== voiceFlowToken || !voiceDialog.active) return
            listenForAnswer()
        }, VOICE_DIALOG_RETRY_MS)
    } else {
        voiceDialog.errorRetries = 0
        // 多次启动失败：温和提示并跳过当前项，避免卡死（措辞不含「跳过」以防回声误判）
        gotoNextStep('录音没成功，这项先空着。')
    }
}

/** 用户点「说完了」：立即结束录音并识别（仅聆听阶段可用） */
function finishListeningEarly() {
    if (!voiceDialog.active || voiceDialog.phase !== 'listening') return
    // 采集类若已实时捕获到数字，直接定格（更快、避免丢失）
    if (voiceDialog.stage === 'collect' && voiceDialog.pendingValue) {
        finalizeCollectAnswer()
        return
    }
    clearVoiceTimers()
    endHoldSpeech()
}

/** 识别结束（含空结果）统一入口 */
function handleDialogSettle(text) {
    if (!voiceDialog.active) return
    // 仅在「聆听」阶段才采纳结果；重听/跳过/停止后产生的残留回调忽略
    if (voiceDialog.phase !== 'listening') return
    clearVoiceTimers()
    // 抢答收尾过快导致最终结果为空时，回退使用最后一次实时识别（中间结果）
    const partial = voiceDialog.heardText
    let raw = String(text || '').trim()
    if (!raw && voiceDialog.barged && partial) raw = partial
    voiceDialog.heardText = raw

    if (voiceDialog.stage === 'confirm') {
        handleConfirmAnswer(raw)
        return
    }

    const step = currentDialogStep()
    if (!step) { finishVoiceDialog(); return }

    // 先解析数字：最终结果优先，其次回退到实时识别已捕获的数值
    // （只要听到了数值就以它为准，避免问题回声里的「跳过/没有」抢先误判为跳过）
    const value = extractGlucoseValue(raw) ?? (voiceDialog.pendingValue || null)
    voiceDialog.pendingValue = ''
    if (value !== null) {
        // 命中数值：回填并确认。注意：过渡播报里不要带数字，
        // 否则下一题并行录音会把这句回声里的数字当成抢答而误触发。
        inputForm.value[step.field] = value
        voiceDialog.collected.push({ label: step.label, value })
        voiceDialog.retries = 0
        gotoNextStep(`好的，${step.label}已记录。`)
        return
    }

    // 没有数字时再判断意图：取消 / 退出
    if (raw && /退出|取消|关闭/.test(raw)) {
        speakDialog('已为您关闭语音问答，可以手动填写。', () => stopVoiceDialog())
        return
    }
    // 跳过当前项
    if (raw && /跳过|没有|不测|没测|没量|不用|下一/.test(raw)) {
        gotoNextStep(`好的，${step.label}先空着。`)
        return
    }

    // 既没数字也没指令：没听清，重试或跳过
    if (voiceDialog.retries < VOICE_DIALOG_MAX_RETRY) {
        voiceDialog.retries += 1
        askCurrentStep('没听清，')
    } else {
        voiceDialog.retries = 0
        gotoNextStep(`暂时没听清${step.label}，先空着。`)
    }
}

/**
 * 从口语文本中提取血糖数值，失败返回 null。
 * 与手动输入保持一致：只要识别到正数即采纳，不做医学范围限制
 * （避免「80」这类数值被误判为没听清）。
 */
function extractGlucoseValue(raw) {
    if (!raw) return null
    const norm = normalizeNumbers(raw)
    const m = norm.match(/\d+(?:\.\d+)?/)
    if (!m) return null
    const num = Number(m[0])
    if (!Number.isFinite(num) || num <= 0) return null
    return m[0]
}

/** 进入下一步（带一句过渡播报） */
function gotoNextStep(prefix = '') {
    voiceDialog.stepIndex += 1
    voiceDialog.retries = 0
    voiceDialog.errorRetries = 0
    if (voiceDialog.stepIndex >= voiceDialog.steps.length) {
        finishVoiceDialog(prefix)
    } else {
        askCurrentStep(prefix)
    }
}

/** 全部问完：汇总并询问是否提交 */
function finishVoiceDialog(prefix = '') {
    if (!voiceDialog.collected.length) {
        speakDialog(`${prefix}还没有录入任何血糖数据，您可以手动填写或重新开始语音问答。`, () => stopVoiceDialog())
        return
    }
    const summary = voiceDialog.collected.map((c) => `${c.label}${c.value}`).join('，')
    voiceDialog.stage = 'confirm'
    voiceDialog.phase = 'asking'
    voiceDialog.retries = 0
    voiceDialog.heardText = ''
    voiceDialog.botText = `${prefix}已为您填写：${summary}。要现在提交吗？说"提交"确认，或说"取消"。`
    const token = voiceFlowToken
    ttsSpeak(voiceDialog.botText, {
        force: true,
        onDone: () => {
            if (token !== voiceFlowToken || !voiceDialog.active) return
            listenForAnswer()
        }
    })
}

/** 处理确认环节的回答 */
function handleConfirmAnswer(raw) {
    if (raw && /提交|确认|可以|好的|好了|是的|对|要|提交吧|保存/.test(raw)) {
        speakDialog('好的，正在为您提交。', () => {
            stopVoiceDialog(true)
            submitInputForm()
        })
        return
    }
    if (raw && /取消|不提交|不要|先不|算了|退出|关闭/.test(raw)) {
        speakDialog('已为您填好，请核对后手动提交。', () => stopVoiceDialog())
        return
    }
    // 没听清，重问一次
    if (voiceDialog.retries < VOICE_DIALOG_MAX_RETRY) {
        voiceDialog.retries += 1
        const token = voiceFlowToken
        voiceDialog.stage = 'confirm'
        voiceDialog.phase = 'asking'
        voiceDialog.botText = '没听清，要现在提交吗？说"提交"或"取消"。'
        ttsSpeak(voiceDialog.botText, {
            force: true,
            onDone: () => {
                if (token !== voiceFlowToken || !voiceDialog.active) return
                listenForAnswer()
            }
        })
    } else {
        speakDialog('已为您填好，请核对后手动提交。', () => stopVoiceDialog())
    }
}

/** 仅播报一句话并在结束后回调（不进入聆听） */
function speakDialog(text, onDone) {
    voiceDialog.botText = text
    const token = voiceFlowToken
    ttsSpeak(text, {
        force: true,
        onDone: () => {
            if (token !== voiceFlowToken) return
            if (typeof onDone === 'function') onDone()
        }
    })
}

/** 重听/重说当前问题 */
function replayCurrentQuestion() {
    if (!voiceDialog.active) return
    if (ttsSpeaking.value) ttsStop()
    endHoldSpeech()
    clearVoiceTimers()
    voiceFlowToken += 1
    if (voiceDialog.stage === 'confirm') {
        finishVoiceDialog()
    } else {
        askCurrentStep()
    }
}

/** 手动跳过当前项 */
function skipCurrentStepManually() {
    if (!voiceDialog.active || voiceDialog.stage === 'confirm') return
    if (ttsSpeaking.value) ttsStop()
    endHoldSpeech()
    clearVoiceTimers()
    voiceFlowToken += 1
    const step = currentDialogStep()
    gotoNextStep(step ? `${step.label}先空着。` : '')
}

/** 停止/退出语音问答（始终停掉录音与播报）；silent=true 时不再额外提示 */
function stopVoiceDialog() {
    clearVoiceTimers()
    voiceFlowToken += 1
    endHoldSpeech()
    ttsStop()
    voiceDialog.active = false
    voiceDialog.phase = 'idle'
    voiceDialog.stage = 'collect'
    voiceDialog.barged = false
    voiceDialog.pendingValue = ''
    voiceDialog.heardText = ''
}

const voiceDialogStatusText = computed(() => {
    if (voiceDialog.phase === 'listening') {
        return voiceDialog.stage === 'confirm' ? '按住作答区：说提交或取消，松手结束' : '按住下方作答区说话，松手结束'
    }
    if (voiceDialog.phase === 'asking') {
        return '正在提问…'
    }
    return ''
})

function onExerciseField(field, e) {
    exerciseForm.value[field] = e?.detail?.value ?? ''
}

async function openExerciseForm() {
    if (!diagnosisId.value) {
        showUserToast('请先选择就诊卡')
        return
    }
    const token = uni.getStorageSync('token')
    if (!token) {
        showUserToast('请先登录后再录入')
        return
    }
    prepareRecordOverlay()
    exerciseForm.value = { ...emptyExerciseForm(), visible: true, loading: true }
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyTodayExerciseRecord',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res?.code === 1 && res.data?.record) {
            const r = res.data.record
            exerciseForm.value.id = Number(r.id) || 0
            exerciseForm.value.exercise_type = String(r.exercise_type || '')
            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 {
        exerciseForm.value.loading = false
    }
}

function closeExerciseForm() {
    if (exerciseForm.value.submitting) return
    cancelVoiceHold()
    exerciseForm.value.visible = false
    nextTick(() => {})
}

async function submitExerciseForm() {
    if (exerciseForm.value.submitting || exerciseForm.value.loading) return
    const f = exerciseForm.value
    if (!f.exercise_type.trim() && !f.duration) {
        showUserToast('请填写运动类型或时长')
        return
    }
    exerciseForm.value.submitting = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailySaveExerciseRecord',
            method: 'POST',
            data: {
                diagnosis_id: diagnosisId.value,
                exercise_type: f.exercise_type,
                duration: f.duration,
                intensity: f.intensity,
                note: f.note
            }
        }, false)
        if (res?.code === 1) {
            showUserToast(res.msg || '今日运动已录入完成', { icon: 'success' })
            exerciseForm.value.visible = false
            await fetchAll()
            await fetchGamifyState()
            promptWaterTreeGuide('运动')
        } else {
            showUserToast((res && res.msg) || '保存失败')
        }
    } catch (e) {
        showUserToast('网络异常，请稍后再试')
    } finally {
        exerciseForm.value.submitting = false
    }
}

async function submitBpForm() {
    if (bpForm.value.submitting || bpForm.value.loading) return

    // 至少填一个有效字段
    const f = bpForm.value
    const hasAny = [
        f.systolic_pressure, f.diastolic_pressure
    ].some((v) => v !== '' && v !== null && Number(v) > 0)
        || !!(String(f.western_medicine || '').trim() || String(f.insulin || '').trim() || String(f.remark || '').trim())
    if (!hasAny) {
        uni.showToast({ title: '请至少填写一项数据', icon: 'none' })
        return
    }

    bpForm.value.submitting = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailySaveBloodRecord',
            method: 'POST',
            data: {
                diagnosis_id: diagnosisId.value,
                systolic_pressure: f.systolic_pressure,
                diastolic_pressure: f.diastolic_pressure,
                western_medicine: f.western_medicine,
                insulin: f.insulin,
                remark: f.remark
            }
        }, false)
        if (res && res.code === 1) {
            showUserToast(res.msg || '今日血压已录入完成', { icon: 'success' })
            bpForm.value.visible = false
            await fetchAll()
            await fetchGamifyState()
            promptWaterTreeGuide('血压')
        } else {
            showUserToast((res && res.msg) || '保存失败')
        }
    } catch (e) {
        uni.showToast({ title: '网络异常，请稍后再试', icon: 'none' })
    } finally {
        bpForm.value.submitting = false
    }
}

function deleteBpRecord() {
    if (!bpForm.value.id || bpForm.value.submitting) return
    uni.showModal({
        title: '删除确认',
        content: '确定删除今天已录入的数据吗？删除后无法恢复。',
        confirmColor: '#dc2626',
        success: async (m) => {
            if (!m.confirm) return
            bpForm.value.submitting = true
            try {
                const res = await proxy.apiUrl({
                    url: '/api/tcm/dailyDeleteBloodRecord',
                    method: 'POST',
                    data: {
                        diagnosis_id: diagnosisId.value,
                        id: bpForm.value.id
                    }
                }, false)
                if (res && res.code === 1) {
                    uni.showToast({ title: '已删除', icon: 'success' })
                    bpForm.value.visible = false
                    fetchAll()
                } else {
                    showUserToast((res && res.msg) || '删除失败')
                }
            } catch (e) {
                uni.showToast({ title: '网络异常，请稍后再试', icon: 'none' })
            } finally {
                bpForm.value.submitting = false
            }
        }
    })
}

function splitTrackingLines(content) {
    return String(content || '')
        .split('\n')
        .map((s) => s.trim())
        .filter(Boolean)
}

// ============ 语音播报（适老化）============
// 优先用 微信同声传译插件 (mp-weixin)；H5 走 window.speechSynthesis；
// 都不可用则轻提示，避免 showModal 挡住录入弹层。
const ttsEnabled = ref(true)
const ttsAutoPlay = ref(false)
const ttsSpeaking = ref(false)
let ttsAudioCtx = null
let ttsAutoToken = 0
let ttsPlayToken = 0   // 每次 ttsStop/ttsSpeak 自增，作废被打断的异步播放（如抢答时仍在合成中的语音）

function teardownTtsAudio() {
    if (ttsAudioCtx) {
        try { ttsAudioCtx.stop() } catch (e) {}
        try { ttsAudioCtx.destroy && ttsAudioCtx.destroy() } catch (e) {}
        ttsAudioCtx = null
    }
}

function ttsStop() {
    ttsPlayToken += 1
    teardownTtsAudio()
    // #ifdef H5
    try {
        if (typeof window !== 'undefined' && window.speechSynthesis) {
            window.speechSynthesis.cancel()
        }
    } catch (e) {}
    // #endif
    ttsSpeaking.value = false
}

function normalizeTtsText(text) {
    if (text == null) return ''
    if (typeof text === 'string') return text.replace(/\s+/g, ' ').trim()
    if (Array.isArray(text)) {
        return text
            .map((item) => {
                if (typeof item === 'string') return item
                if (item && typeof item === 'object') {
                    return item.title || item.label || item.desc || item.msg || item.message || ''
                }
                return formatUserMessage(item, '')
            })
            .filter(Boolean)
            .join('，')
    }
    if (typeof text === 'object') {
        return text.title || text.label || text.desc || text.msg || text.message || ''
    }
    return formatUserMessage(text, '')
}

/**
 * 文字转语音播报。
 * @param {string|object|Array} text 播报内容
 * @param {{ force?: boolean, onDone?: () => void }} [opts]
 *   - force: 忽略 ttsEnabled 开关强制播报（语音问答流程使用）
 *   - onDone: 播报结束/失败后回调（仅触发一次），用于串联「问→听」节奏
 */
function ttsSpeak(text, opts = {}) {
    const { force = false, onDone } = opts || {}
    let doneCalled = false
    const fireDone = () => {
        if (doneCalled) return
        doneCalled = true
        if (typeof onDone === 'function') {
            try { onDone() } catch (e) {}
        }
    }

    if (!ttsEnabled.value && !force) { fireDone(); return }
    const content = normalizeTtsText(text)
    if (!content) { fireDone(); return }

    ttsStop()
    const myToken = ++ttsPlayToken

    // H5：浏览器 SpeechSynthesis（慢速适老化）
    // #ifdef H5
    try {
        if (typeof window !== 'undefined' && window.speechSynthesis) {
            const u = new window.SpeechSynthesisUtterance(content)
            u.lang = 'zh-CN'
            u.rate = 0.85
            u.pitch = 1
            u.volume = 1
            u.onstart = () => { ttsSpeaking.value = true }
            u.onend = () => { ttsSpeaking.value = false; fireDone() }
            u.onerror = () => { ttsSpeaking.value = false; fireDone() }
            window.speechSynthesis.speak(u)
            return
        }
    } catch (e) {}
    // #endif

    // 微信小程序：同声传译插件 TTS → mp3 → innerAudioContext 播放
    // #ifdef MP-WEIXIN
    try {
        // eslint-disable-next-line no-undef
        const plugin = requirePlugin('WechatSI')
        ttsSpeaking.value = true
        plugin.textToSpeech({
            lang: 'zh_CN',
            tts: true,
            content,
            success: (res) => {
                // 已被打断（如用户抢答触发 ttsStop）：不要再播放这段迟到的语音
                if (myToken !== ttsPlayToken) {
                    fireDone()
                    return
                }
                if (!res || !res.filename) {
                    ttsSpeaking.value = false
                    fireDone()
                    return
                }
                teardownTtsAudio()
                ttsAudioCtx = uni.createInnerAudioContext()
                ttsAudioCtx.src = res.filename
                ttsAudioCtx.obeyMuteSwitch = false
                ttsAudioCtx.onPlay(() => { ttsSpeaking.value = true })
                ttsAudioCtx.onEnded(() => { ttsSpeaking.value = false; fireDone() })
                ttsAudioCtx.onError(() => { ttsSpeaking.value = false; fireDone() })
                ttsAudioCtx.onStop(() => { ttsSpeaking.value = false; fireDone() })
                ttsAudioCtx.play()
            },
            fail: (err) => {
                ttsSpeaking.value = false
                console.warn('WechatSI textToSpeech failed', err)
                uni.showToast({ title: '语音不可用，请检查网络/插件', icon: 'none', duration: 2000 })
                fireDone()
            }
        })
        return
    } catch (e) {
        console.warn('WechatSI plugin not available', e)
    }
    // #endif

    // 其它端兜底：轻提示，勿用 modal 遮挡录入等操作
    showUserToast(content, { duration: 3200 })
    // 兜底估算播报时长后回调（按字数粗略估时）
    setTimeout(fireDone, Math.min(6000, 1200 + content.length * 180))
}

function ttsToggleEnabled() {
    ttsEnabled.value = !ttsEnabled.value
    if (!ttsEnabled.value) {
        ttsAutoPlay.value = false
        ttsStop()
    }
}

function ttsToggleAutoPlay() {
    if (!ttsEnabled.value) {
        ttsEnabled.value = true
    }
    ttsAutoPlay.value = !ttsAutoPlay.value
    if (ttsAutoPlay.value) {
        const text = buildLatestSummaryText()
        if (text) ttsSpeak(text)
    } else {
        ttsStop()
    }
}

// ============ 播报文案构造 ============
function readableNumber(v) {
    if (v === null || v === undefined || v === '') return ''
    return String(v).replace(/(\d+)\.(\d+)/g, (_, a, b) => `${a}点${b}`)
}

function buildLatestSummaryText() {
    const f = latestStat.value.fasting
    const p = latestStat.value.postprandial
    const o = latestStat.value.other
    const head = `${patientName.value || '您'}的最新血糖情况：`
    const lines = []
    if (f && f.value !== '—') {
        let line = `空腹血糖 ${readableNumber(f.value)} 毫摩每升`
        if (f.isHigh) line += '，偏高，请遵医嘱。'
        else line += '。'
        if (f.trend && f.trend.dir !== 'flat') {
            line += `比上次${f.trend.dir === 'up' ? '升高' : '下降'} ${readableNumber(f.trend.delta.toFixed(1))}。`
        }
        lines.push(line)
    }
    if (p && p.value !== '—') {
        let line = `餐后血糖 ${readableNumber(p.value)} 毫摩每升`
        if (p.isHigh) line += '，偏高，请注意饮食。'
        else line += '。'
        if (p.trend && p.trend.dir !== 'flat') {
            line += `比上次${p.trend.dir === 'up' ? '升高' : '下降'} ${readableNumber(p.trend.delta.toFixed(1))}。`
        }
        lines.push(line)
    }
    if (o && o.value !== '—') {
        let line = `其他时段血糖 ${readableNumber(o.value)} 毫摩每升`
        if (o.isHigh) line += '，偏高，请遵医嘱。'
        else line += '。'
        if (o.trend && o.trend.dir !== 'flat') {
            line += `比上次${o.trend.dir === 'up' ? '升高' : '下降'} ${readableNumber(o.trend.delta.toFixed(1))}。`
        }
        lines.push(line)
    }
    if (!lines.length) {
        return `${patientName.value || '您'}近期暂无血糖记录。`
    }
    return head + lines.join(' ')
}

function buildDayText(day) {
    if (!day) return ''
    const dateLabel = `${day.dateLabel.replace('-', '月')}日${day.weekday ? ' ' + day.weekday : ''}`
    const lines = [`${dateLabel} 的健康记录：`]
    if (day.blood) {
        const b = day.blood
        if (toNumber(b.fasting_blood_sugar) !== null) {
            lines.push(`空腹血糖 ${readableNumber(b.fasting_blood_sugar)} 毫摩每升${b.isHighFasting ? '，偏高。' : '。'}`)
        }
        if (toNumber(b.postprandial_blood_sugar) !== null) {
            lines.push(`餐后血糖 ${readableNumber(b.postprandial_blood_sugar)} 毫摩每升${b.isHighPostprandial ? '，偏高。' : '。'}`)
        }
        if (toNumber(b.other_blood_sugar) !== null) {
            lines.push(`其他血糖 ${readableNumber(b.other_blood_sugar)} 毫摩每升${b.isHighOther ? '，偏高。' : '。'}`)
        }
        const sys = toNumber(b.systolic_pressure)
        const dia = toNumber(b.diastolic_pressure)
        if (sys !== null || dia !== null) {
            let line = '血压'
            if (sys !== null) line += ` 高压 ${readableNumber(sys)}`
            if (dia !== null) line += `，低压 ${readableNumber(dia)}`
            line += ' 毫米汞柱'
            line += b.isHighBp ? '，偏高。' : '。'
            lines.push(line)
        }
        if (b.western_medicine) lines.push(`西药：${b.western_medicine}。`)
        if (b.insulin) lines.push(`胰岛素：${b.insulin}。`)
    }
    if (day.diet) {
        const d = day.diet
        if (d.breakfast_foods) lines.push(`早餐：${d.breakfast_foods}。`)
        if (d.lunch_foods) lines.push(`午餐：${d.lunch_foods}。`)
        if (d.dinner_foods) lines.push(`晚餐：${d.dinner_foods}。`)
    }
    if (day.exercise) {
        const ex = day.exercise
        const parts = []
        if (ex.exercise_type) parts.push(ex.exercise_type)
        if (ex.duration) parts.push(`${ex.duration} 分钟`)
        if (ex.intensityText) parts.push(ex.intensityText)
        if (parts.length) lines.push(`运动：${parts.join('，')}。`)
    }
    if (day.tracking && day.tracking.content) {
        lines.push(`备注：${String(day.tracking.content).replace(/\n+/g, '，')}。`)
    }
    return lines.join(' ')
}

function speakLatestSummary() {
    const text = buildLatestSummaryText()
    if (text) ttsSpeak(text)
}

function speakDay(day) {
    if (!day || typeof day !== 'object' || !day.date) return
    const text = buildDayText(day)
    if (text) ttsSpeak(text)
}

// ============ 波浪图绘制 ============
function ensureCanvas() {
    return new Promise((resolve) => {
        if (canvasCtx) {
            resolve()
            return
        }
        const query = uni.createSelectorQuery().in(proxy)
        query.select('#bloodWaveChart')
            .fields({ node: true, size: true })
            .exec((res) => {
                const item = res && res[0]
                if (!item || !item.node) {
                    resolve()
                    return
                }
                canvasNode = item.node
                canvasCtx = canvasNode.getContext('2d')
                try {
                    canvasDpr = uni.getSystemInfoSync().pixelRatio || 1
                } catch (e) {
                    canvasDpr = 1
                }
                canvasWidth = item.width
                canvasHeight = item.height
                canvasNode.width = canvasWidth * canvasDpr
                canvasNode.height = canvasHeight * canvasDpr
                canvasCtx.scale(canvasDpr, canvasDpr)
                resolve()
            })
    })
}

let redrawScheduled = false
function scheduleChartRedraw() {
    if (redrawScheduled) return
    redrawScheduled = true
    setTimeout(async () => {
        redrawScheduled = false
        await ensureCanvas()
        drawChart()
    }, 60)
}

function buildSeries(field) {
    return chartDates.value.map((date) => {
        const b = bloodByDate.value[date]
        return b ? toNumber(b[field]) : null
    })
}

function computeYRange(series1, series2) {
    const values = [...series1, ...series2].filter((v) => v !== null)
    if (!values.length) return { yMin: 4, yMax: 12 }
    let min = Math.min(...values)
    let max = Math.max(...values)
    // 给上下留一点空间
    const span = max - min
    if (span < 2) {
        min = Math.max(0, min - 1)
        max = max + 1
    } else {
        min = Math.max(0, min - span * 0.2)
        max = max + span * 0.2
    }
    // 不低于4，不高于25
    min = Math.max(2, Math.floor(min))
    max = Math.min(28, Math.ceil(max))
    if (max - min < 4) max = min + 4
    return { yMin: min, yMax: max }
}

function drawChart() {
    if (!canvasCtx || !canvasWidth || !canvasHeight) return
    const ctx = canvasCtx
    const w = canvasWidth
    const h = canvasHeight

    ctx.clearRect(0, 0, w, h)

    const padding = { left: 40, right: 18, top: 26, bottom: 36 }
    const cw = w - padding.left - padding.right
    const ch = h - padding.top - padding.bottom

    const fastingSeries = buildSeries('fasting_blood_sugar')
    const postSeries = buildSeries('postprandial_blood_sugar')
    const { yMin, yMax } = computeYRange(fastingSeries, postSeries)
    const n = chartDates.value.length
    const threshold = getBloodSugarThresholds(age.value)
    const valueToY = (v) => padding.top + ch * (1 - (v - yMin) / (yMax - yMin))

    // 1) 背景：纯净浅色 + 阈值以上一抹极淡红
    ctx.fillStyle = '#fbfcfd'
    ctx.fillRect(padding.left, padding.top, cw, ch)
    if (threshold) {
        const yThr = valueToY(threshold.fasting)
        const top = Math.max(padding.top, Math.min(yThr, padding.top + ch))
        if (top > padding.top) {
            // 渐变让红色从顶部往阈值线方向过渡，更柔和
            const dangerGrad = ctx.createLinearGradient(0, padding.top, 0, top)
            dangerGrad.addColorStop(0, 'rgba(254, 226, 226, 0.55)')
            dangerGrad.addColorStop(1, 'rgba(254, 226, 226, 0.15)')
            ctx.fillStyle = dangerGrad
            ctx.fillRect(padding.left, padding.top, cw, top - padding.top)
        }
    }

    // 2) 网格 —— 极淡，不喧宾夺主
    ctx.strokeStyle = '#eef2f5'
    ctx.lineWidth = 1
    const yTicks = 4
    for (let i = 0; i <= yTicks; i++) {
        const y = padding.top + (ch * i) / yTicks
        ctx.beginPath()
        ctx.moveTo(padding.left, y)
        ctx.lineTo(padding.left + cw, y)
        ctx.stroke()
    }

    // Y 轴刻度（适老化：加大 + 加深）
    ctx.font = 'bold 12px sans-serif'
    ctx.fillStyle = '#64748b'
    ctx.textAlign = 'right'
    ctx.textBaseline = 'middle'
    for (let i = 0; i <= yTicks; i++) {
        const y = padding.top + (ch * i) / yTicks
        const v = yMax - ((yMax - yMin) * i) / yTicks
        ctx.fillText(v.toFixed(1), padding.left - 8, y)
    }

    // 3) X 轴日期 —— 最多 5 个（适老化：加大、加深）
    ctx.font = 'bold 12px sans-serif'
    ctx.fillStyle = '#475569'
    ctx.textAlign = 'center'
    ctx.textBaseline = 'top'
    if (n > 0) {
        const maxLabels = n > 20 ? 5 : 6
        const labelStep = Math.max(1, Math.ceil(n / maxLabels))
        chartDates.value.forEach((date, i) => {
            if (i !== 0 && i !== n - 1 && i % labelStep !== 0) return
            const x = n === 1 ? padding.left + cw / 2 : padding.left + (cw * i) / (n - 1)
            ctx.fillText(date.slice(5), x, padding.top + ch + 8)
        })
    }

    // 4) 阈值参考线
    if (threshold) {
        const y = valueToY(threshold.fasting)
        if (y >= padding.top && y <= padding.top + ch) {
            ctx.strokeStyle = '#fda4af'
            ctx.lineWidth = 1
            try { ctx.setLineDash([6, 5]) } catch (e) {}
            ctx.beginPath()
            ctx.moveTo(padding.left, y)
            ctx.lineTo(padding.left + cw, y)
            ctx.stroke()
            try { ctx.setLineDash([]) } catch (e) {}
            ctx.font = 'bold 11px sans-serif'
            ctx.fillStyle = '#dc2626'
            ctx.textAlign = 'left'
            ctx.textBaseline = 'middle'
            ctx.fillText(`空腹阈 ${threshold.fasting}`, padding.left + 6, y - 9)
        }
    }

    // 5) 绘制曲线
    const seriesMeta = [
        {
            data: fastingSeries,
            color: '#204E2B',
            fillTop: 'rgba(32,78,43,0.18)',
            fillBottom: 'rgba(32,78,43,0)',
            label: '空腹',
            highThreshold: threshold ? threshold.fasting : null
        },
        {
            data: postSeries,
            color: '#386641',
            fillTop: 'rgba(56,102,65,0.14)',
            fillBottom: 'rgba(56,102,65,0)',
            label: '餐后',
            highThreshold: threshold ? threshold.postprandial : null
        }
    ]

    const xAt = (i) => (n === 1 ? padding.left + cw / 2 : padding.left + (cw * i) / (n - 1))
    const coordCache = []
    // 7 天内显示圆点，30 天以纯曲线为主
    const showDots = n <= 10

    seriesMeta.forEach((meta) => {
        const projected = meta.data.map((v, i) => ({
            x: xAt(i),
            y: v === null ? null : valueToY(v),
            value: v
        }))
        coordCache.push(projected)

        const segments = []
        let cur = []
        projected.forEach((p) => {
            if (p.y !== null) cur.push(p)
            else if (cur.length) { segments.push(cur); cur = [] }
        })
        if (cur.length) segments.push(cur)

        segments.forEach((seg) => {
            if (seg.length === 1) {
                ctx.fillStyle = meta.color
                ctx.beginPath()
                ctx.arc(seg[0].x, seg[0].y, 4, 0, Math.PI * 2)
                ctx.fill()
                return
            }

            const drawPath = () => {
                ctx.beginPath()
                ctx.moveTo(seg[0].x, seg[0].y)
                for (let i = 0; i < seg.length - 1; i++) {
                    const p0 = seg[i - 1] || seg[i]
                    const p1 = seg[i]
                    const p2 = seg[i + 1]
                    const p3 = seg[i + 2] || seg[i + 1]
                    const cp1x = p1.x + (p2.x - p0.x) / 6
                    const cp1y = p1.y + (p2.y - p0.y) / 6
                    const cp2x = p2.x - (p3.x - p1.x) / 6
                    const cp2y = p2.y - (p3.y - p1.y) / 6
                    ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y)
                }
            }

            // 渐变填充
            drawPath()
            ctx.lineTo(seg[seg.length - 1].x, padding.top + ch)
            ctx.lineTo(seg[0].x, padding.top + ch)
            ctx.closePath()
            const grad = ctx.createLinearGradient(0, padding.top, 0, padding.top + ch)
            grad.addColorStop(0, meta.fillTop)
            grad.addColorStop(1, meta.fillBottom)
            ctx.fillStyle = grad
            ctx.fill()

            // 描线
            drawPath()
            ctx.strokeStyle = meta.color
            ctx.lineWidth = 2.4
            ctx.lineCap = 'round'
            ctx.lineJoin = 'round'
            ctx.stroke()

            // 圆点（短窗口期）
            if (showDots) {
                seg.forEach((p) => {
                    ctx.beginPath()
                    ctx.arc(p.x, p.y, 3, 0, Math.PI * 2)
                    ctx.fillStyle = '#ffffff'
                    ctx.fill()
                    ctx.strokeStyle = meta.color
                    ctx.lineWidth = 1.8
                    ctx.stroke()
                })
            }
        })

        // 标注「最高点」—— 仅一个，简洁
        let maxIdx = -1
        let maxVal = -Infinity
        projected.forEach((p, i) => {
            if (p.value != null && p.value > maxVal) {
                maxVal = p.value
                maxIdx = i
            }
        })
        if (maxIdx >= 0) {
            const p = projected[maxIdx]
            const isHigh = meta.highThreshold != null && p.value >= meta.highThreshold

            // 强调点
            ctx.beginPath()
            ctx.arc(p.x, p.y, 5, 0, Math.PI * 2)
            ctx.fillStyle = isHigh ? '#dc2626' : meta.color
            ctx.fill()
            ctx.strokeStyle = '#ffffff'
            ctx.lineWidth = 2.5
            ctx.stroke()

            // 标签（适老化：加大峰值标签）
            const labelText = String(p.value)
            ctx.font = 'bold 12px sans-serif'
            const textWidth = ctx.measureText(labelText).width
            const padX = 8
            const padY = 5
            const boxWidth = textWidth + padX * 2
            const boxHeight = 14 + padY * 2
            let labelX = p.x
            let labelAbove = true
            if (p.y - 12 - boxHeight - 4 < padding.top) labelAbove = false
            let labelY = labelAbove ? p.y - 12 - boxHeight / 2 : p.y + 12 + boxHeight / 2
            // 边界保护
            if (labelX - boxWidth / 2 < padding.left + 2) labelX = padding.left + boxWidth / 2 + 2
            if (labelX + boxWidth / 2 > padding.left + cw - 2) labelX = padding.left + cw - boxWidth / 2 - 2
            // 标签底色
            ctx.fillStyle = isHigh ? '#dc2626' : meta.color
            ctx.fillRect(labelX - boxWidth / 2, labelY - boxHeight / 2, boxWidth, boxHeight)
            // 标签文字
            ctx.fillStyle = '#ffffff'
            ctx.textAlign = 'center'
            ctx.textBaseline = 'middle'
            ctx.fillText(labelText, labelX, labelY)
        }
    })

    chartCoords = {
        padding,
        cw,
        ch,
        n,
        fasting: coordCache[0] || [],
        postprandial: coordCache[1] || []
    }
}

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 = chartCoords.fasting[idx]
    const pp = chartCoords.postprandial[idx]
    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)
}

// ============ 生命周期 ============
function readUserContext() {
    try {
        const userData = uni.getStorageSync('userData')
        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) {}
}

function initHeaderSafeArea() {
    const sys = uni.getSystemInfoSync()
    const statusBar = Number(sys.statusBarHeight) || 0
    const greenBlockH = uni.upx2px(300)
    let capsulePad = uni.upx2px(24)

    try {
        // #ifdef MP-WEIXIN
        const menu = typeof uni.getMenuButtonBoundingClientRect === 'function'
            ? uni.getMenuButtonBoundingClientRect()
            : null
        if (menu && menu.width > 0) {
            capsulePad = sys.windowWidth - menu.left + uni.upx2px(8)
        }
        // #endif
    } catch (e) {}

    const headerTotal = statusBar + greenBlockH
    headerStyle.value = {
        height: `${headerTotal-35}px`,
        paddingTop: `${statusBar}px`,
        boxSizing: 'border-box'
    }
    headerInnerStyle.value = {
        paddingRight: `${capsulePad}px`
    }
    mainPadStyle.value = {
        paddingTop: `${headerTotal + uni.upx2px(24)}px`
    }
}

async function bootstrap(options) {
    options = options || {}
    initHeaderSafeArea()
    gateRedirected = false
    authChecking.value = true

    // 登录完成前不写 diagnosisId，避免模板/子流程提前打业务接口
    const passedDiagnosisId = Number(options.diagnosis_id || options.id || 0)
    const passedPatientId = Number(options.patient_id || 0)
    const passedPatientName = options.patient_name
        ? decodeURIComponent(String(options.patient_name))
        : ''
    const passedAge = Number(options.age) || 0
    const passedGender = Number(options.gender) || 0

    const loggedIn = await ensureLoggedIn()
    if (!loggedIn || !hasAuthToken()) {
        authChecking.value = false
        showUserToast('登录失败，请稍后重试')
        return
    }

    readUserContext()

    const list = await fetchCardList()

    if (Array.isArray(list) && list.length > 0) {
        let target = null
        if (passedDiagnosisId) {
            target = list.find((c) => Number(c.id) === Number(passedDiagnosisId))
        }
        if (!target) target = list[0]
        await applyCard(target)
    } else {
        diagnosisId.value = 0
        patientId.value = 0
        patientName.value = ''
        age.value = 0
        gender.value = 0

        if (readUserMobile()) {
            await ensureDailyContextByPhone()
        }
        if (!diagnosisId.value) {
            authChecking.value = false
            if (readUserMobile()) {
                showUserToast('暂时无法加载记录，请稍后重试')
            } else {
                showUserToast('请先在统计页授权手机号后再查看记录')
            }
            setTimeout(() => {
                const pages = getCurrentPages()
                if (pages.length > 1) uni.navigateBack()
                else uni.redirectTo({ url: '/tongji/pages/weekly' })
            }, 1600)
            return
        }
    }

    authChecking.value = false
    await fetchAll()
    if (options.openDiet === '1' || options.openDiet === 1) {
        await openDietForm({ autoFill: true })
    }
}

onLoad((options) => {
    bootstrap(options)
})

onShareAppMessage(() => {
    const d = weeklyReportData.value
    const code = pendingShareInviteCode.value
    const title =
        shareTitle.value ||
        `【稳糖打卡】${d.patientLabel}本周记录${d.recordDays}天（邀请码今日有效）`
    const path = code
        ? `/tongji/pages/index?from=share&invite_code=${code}`
        : '/tongji/pages/more'
    return { title, path }
})

onShow(() => {
    initHeaderSafeArea()
    if (authChecking.value || gateRedirected) return
    if (diagnosisId.value) {
        loadFamilyLikeSummary()
    }
})

onPullDownRefresh(async () => {
    if (await ensureLoggedIn()) {
        await fetchAll()
    }
    uni.stopPullDownRefresh()
})

onHide(() => {
    cancelVoiceHold()
    stopVoiceDialog(true)
    ttsStop()
})

onUnload(() => {
    cancelVoiceHold()
    stopVoiceDialog(true)
    ttsStop()
    clearWateringFx()
})

// ============ 稳糖乐园 (游戏化积分/小树) ============
const gamifyPoints = ref(0)
const gamifyBadges = ref([])
const gamifyTaskAwards = ref({})
const gamifyTodayTasks = ref([])
const gamifyClaimablePoints = ref(0)
const gamifyStatusTip = ref('')
const TREE_LEVEL_ROADMAP = TREE_LEVELS
const treeWaterLocked = ref(false)
const treePulseActive = ref(false)

function isApiSuccess(res) {
    return !!(res && Number(res.code) === 1)
}

function applyGamifyPayload(data) {
    if (!data || typeof data !== 'object') return
    gamifyPoints.value = Number(data.points) || 0
    gamifyBadges.value = Array.isArray(data.badges) ? [...data.badges] : []
    try {
        gamifyTaskAwards.value = data.task_awards ? JSON.parse(JSON.stringify(data.task_awards)) : {}
    } catch (e) {
        gamifyTaskAwards.value = data.task_awards || {}
    }
    gamifyTodayTasks.value = Array.isArray(data.today_tasks)
        ? normalizeGamifyTasks(data.today_tasks.map((t) => ({
            id: String(t.id || ''),
            name: String(t.name || ''),
            points: Number(t.points) || 0,
            completed: !!t.completed,
            claimed: !!t.claimed
        })))
        : []
    gamifyClaimablePoints.value = Number(data.claimable_points) || 0
    if (data.message) {
        gamifyStatusTip.value = String(data.message)
    }
}

async function fetchGamifyState() {
    if (!hasAuthToken() || !diagnosisId.value) return
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyGetGamify',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (isApiSuccess(res) && res.data) {
            applyGamifyPayload(res.data)
        }
    } catch (e) {
        console.error('获取游戏化数据失败', e)
    }
}

const todayHasGlucose = computed(() => {
    const b = bloodByDate.value[todayStr.value]
    if (!b) return false
    return toNumber(b.fasting_blood_sugar) !== null
        || toNumber(b.postprandial_blood_sugar) !== null
        || toNumber(b.other_blood_sugar) !== null
})

const todayHasBpPressure = computed(() => {
    const b = bloodByDate.value[todayStr.value]
    if (!b) return false
    return toNumber(b.systolic_pressure) !== null || toNumber(b.diastolic_pressure) !== null
})

const todayHasDiet = computed(() => {
    const today = todayStr.value
    const d = dietByDate.value[today]
    return !!(d && (d.breakfast_foods || d.lunch_foods || d.dinner_foods))
})

const todayHasExercise = computed(() => {
    const today = todayStr.value
    const ex = exerciseByDate.value[today]
    return !!(ex && (ex.exercise_type || ex.duration))
})

const GAMIFY_TASK_ORDER = ['glucose', 'bp', 'diet', 'exercise']

const GAMIFY_TASK_ICONS = {
    glucose: 'glucose',
    bp: 'activity',
    diet: 'utensils',
    exercise: 'flame'
}

function isGamifyTaskClaimed(id, awards, completion = {}) {
    if (awards[id]) return true
    if (awards.blood) {
        if (id === 'glucose' && completion.glucose) return true
        if (id === 'bp' && completion.bp) return true
    }
    return false
}

function gamifyTaskCompletionMap() {
    return {
        glucose: todayHasGlucose.value,
        bp: todayHasBpPressure.value,
        diet: todayHasDiet.value,
        exercise: todayHasExercise.value
    }
}

function resolveGamifyTaskCompleted(id, fallback = false) {
    const map = gamifyTaskCompletionMap()
    if (Object.prototype.hasOwnProperty.call(map, id)) {
        return !!map[id]
    }
    return !!fallback
}

function gamifyTaskIcon(id) {
    return GAMIFY_TASK_ICONS[id] || 'check-circle'
}

function normalizeGamifyTasks(tasks) {
    if (!Array.isArray(tasks) || !tasks.length) return []
    const ids = new Set(tasks.map((t) => t.id))
    if (ids.has('glucose') || ids.has('bp')) {
        return tasks
    }
    if (ids.has('blood')) {
        const blood = tasks.find((t) => t.id === 'blood')
        const others = tasks.filter((t) => t.id !== 'blood')
        if (!blood) return tasks
        return [
            { ...blood, id: 'glucose', name: '测血糖' },
            { ...blood, id: 'bp', name: '测血压' },
            ...others
        ]
    }
    return tasks
}

const todayTasks = computed(() => {
    const today = todayStr.value
    const awards = gamifyTaskAwards.value[today] || {}
    const completion = gamifyTaskCompletionMap()
    const mapTask = (t) => ({
        ...t,
        completed: resolveGamifyTaskCompleted(t.id, t.completed),
        claimed: isGamifyTaskClaimed(t.id, awards, completion)
    })
    if (gamifyTodayTasks.value.length) {
        return normalizeGamifyTasks(gamifyTodayTasks.value).map(mapTask)
    }
    return [
        {
            id: 'glucose',
            name: '测血糖',
            points: 10,
            completed: completion.glucose,
            claimed: isGamifyTaskClaimed('glucose', awards, completion)
        },
        {
            id: 'bp',
            name: '测血压',
            points: 10,
            completed: completion.bp,
            claimed: isGamifyTaskClaimed('bp', awards, completion)
        },
        {
            id: 'diet',
            name: '饮食',
            points: 10,
            completed: completion.diet,
            claimed: isGamifyTaskClaimed('diet', awards, completion)
        },
        {
            id: 'exercise',
            name: '运动',
            points: 10,
            completed: completion.exercise,
            claimed: isGamifyTaskClaimed('exercise', awards, completion)
        }
    ]
})

const displayTasksForUi = computed(() => {
    const map = new Map(todayTasks.value.map((t) => [t.id, t]))
    return GAMIFY_TASK_ORDER.map((id) => map.get(id)).filter(Boolean)
})

const claimablePoints = computed(() => {
    return todayTasks.value
        .filter((t) => t.completed && !t.claimed)
        .reduce((sum, t) => sum + t.points, 0)
})

/** 有可领取任务积分时才执行浇水（与后端 dailyWaterTree 一致） */
const canWaterTree = computed(() => claimablePoints.value > 0)

/** 今日控糖任务（UI 展示项）是否全部完成并已领取 */
const displayTasksAllCleared = computed(() => {
    const tasks = displayTasksForUi.value
    return tasks.length > 0 && tasks.every((t) => t.completed && t.claimed)
})

/** 第一个未完成的展示任务，用于引导 */
const firstPendingDisplayTask = computed(() => {
    return displayTasksForUi.value.find((t) => !t.completed) || null
})

const waterHintText = computed(() => {
    if (canWaterTree.value) {
        return `有 ${claimablePoints.value} 积分待领，点击浇水查看进度并领取`
    }
    if (displayTasksAllCleared.value) {
        return '今日任务已全部完成，小树喝饱啦 🌱'
    }
    const pending = firstPendingDisplayTask.value
    if (pending) {
        return `还差「${pending.name}」等项，点击浇水查看今日进度`
    }
    return '点击浇水查看今日任务进度'
})

const allTasksCleared = computed(() => {
    const tasks = todayTasks.value
    return tasks.length > 0 && tasks.every((t) => t.completed && t.claimed)
})

const unlockedBadges = computed(() => {
    const list = []
    const streak = Number(streakDays.value?.days) || 0
    if (todayChecked.value) {
        list.push({ id: 'today', label: '今日打卡', icon: 'check-circle', color: '#386641' })
    }
    if (streak >= 3) {
        list.push({ id: 'streak', label: `连击${streak}天`, icon: 'flame', color: '#EA580C' })
    }
    if (streak >= 7) {
        list.push({ id: 'week', label: '一周达人', icon: 'trophy', color: '#CA8A04' })
    }
    if (last7CheckedCount.value >= 5) {
        list.push({ id: 'w5', label: '本周5天+', icon: 'calendar', color: '#204E2B' })
    }
    if (gamifyPoints.value >= 50) {
        list.push({ id: 'p50', label: '积分50+', icon: 'sparkles', color: '#204E2B' })
    }
    if (gamifyPoints.value >= 100) {
        list.push({ id: 'p100', label: '积分破百', icon: 'trophy', color: '#7C3AED' })
    }
    if (gamifyPoints.value >= 200) {
        list.push({ id: 'p200', label: '积分200+', icon: 'sparkles', color: '#7C3AED' })
    }
    if (treeLevel.value >= 3) {
        list.push({ id: 'tree3', label: '小树苗', icon: 'glucose', color: '#0D9488' })
    }
    if (treeLevel.value >= 5) {
        list.push({ id: 'tree5', label: '拔节高', icon: 'activity', color: '#386641' })
    }
    if (treeLevel.value >= 7) {
        list.push({ id: 'tree7', label: '初绽香', icon: 'heart', color: '#EA580C' })
    }
    if (treeState.value.isMax) {
        list.push({ id: 'tree9', label: '圆满树', icon: 'trophy', color: '#CA8A04' })
    }
    return list
})

const celebrateShow = ref(false)
const celebrateTitle = ref('')
const celebrateSub = ref('')
const floatReward = ref(null)
const inviteHeartBurst = ref(false)
const taskJustDoneId = ref('')
let celebrateTimer = null
let taskPopTimer = null

// ===== 浇水特效状态（对应 code.html 的 createRain / createSparkles / triggerSugarDecreaseEffect） =====
const waterDroplets = ref([])
const sparkles = ref([])
const sugarDecrease = ref(null)
const sugarParticles = ref([])
let fxTimers = []

function clearWateringFx() {
    fxTimers.forEach((t) => clearTimeout(t))
    fxTimers = []
    waterDroplets.value = []
    sparkles.value = []
    sugarDecrease.value = null
    sugarParticles.value = []
}

/** 下雨水滴（点击即播，15 滴蓝色水珠） */
function startRain() {
    const base = Date.now()
    const drops = []
    for (let i = 0; i < 15; i++) {
        drops.push({
            key: `d${base}_${i}`,
            left: Math.random() * 100,
            delay: Math.random() * 0.4
        })
    }
    waterDroplets.value = drops
    fxTimers.push(setTimeout(() => { waterDroplets.value = [] }, 600))
}

/** 600ms 后爆发：成长脉冲 + 闪光星 + 降糖飘字 + 全屏下落箭头（对应 code.html triggerGrowthPulse / createSparkles / triggerSugarDecreaseEffect） */
function burstWateringFx() {
    const base = Date.now()
    const sps = []
    for (let i = 0; i < 12; i++) {
        sps.push({
            key: `s${base}_${i}`,
            left: 40 + (Math.random() - 0.5) * 40,
            top: 40 + (Math.random() - 0.5) * 40,
            dur: 0.6 + Math.random() * 0.4
        })
    }
    sparkles.value = sps
    fxTimers.push(setTimeout(() => { sparkles.value = [] }, 1000))

    sugarDecrease.value = { key: `sd${base}` }
    fxTimers.push(setTimeout(() => { sugarDecrease.value = null }, 2000))

    const parts = []
    for (let i = 0; i < 20; i++) {
        parts.push({
            key: `p${base}_${i}`,
            left: Math.random() * 100,
            size: 32 + Math.random() * 32,
            delay: i * 0.05,
            kind: Math.random() > 0.5 ? 'south' : 'tri'
        })
    }
    sugarParticles.value = parts
    fxTimers.push(setTimeout(() => { sugarParticles.value = [] }, 1500))
}

/** code.html：点击后 600ms 触发成长脉冲 + 全套特效 */
function scheduleWateringBurstFx() {
    fxTimers.push(setTimeout(() => {
        treePulseActive.value = true
        burstWateringFx()
        fxTimers.push(setTimeout(() => { treePulseActive.value = false }, 800))
    }, 600))
}

const INVITE_TREE_WHISPERS = [
    'Ta 正在努力控糖，您的鼓励很重要。',
    '好习惯比完美数值更珍贵，一起加油。',
    '每天记一笔，就是在照顾未来的自己。',
    '点赞一下，让 Ta 知道家人在身边。'
]

function hapticLight() {
    try {
        uni.vibrateShort({ type: 'light' })
    } catch (e) {}
}

function triggerCelebrate(title, sub = '') {
    if (celebrateTimer) clearTimeout(celebrateTimer)
    celebrateTitle.value = title
    celebrateSub.value = sub
    celebrateShow.value = true
    celebrateTimer = setTimeout(() => {
        celebrateShow.value = false
        celebrateSub.value = ''
    }, 2400)
}

function showFloatReward(points) {
    floatReward.value = { text: points, key: Date.now() }
    setTimeout(() => {
        floatReward.value = null
    }, 1100)
}

function popTaskDone(taskId) {
    if (!taskId) return
    taskJustDoneId.value = taskId
    if (taskPopTimer) clearTimeout(taskPopTimer)
    taskPopTimer = setTimeout(() => {
        taskJustDoneId.value = ''
    }, 700)
}

function onTreeWhisper() {
    if (treeWaterLocked.value) return
    const line = pickTreeWhisper(treeLevel.value)
    showUserToast(line, { duration: 2800 })
    hapticLight()
}

function onInviteTreeWhisper() {
    const line = INVITE_TREE_WHISPERS[Math.floor(Math.random() * INVITE_TREE_WHISPERS.length)]
    showUserToast(line, { duration: 2800 })
    hapticLight()
}

/** 记录保存后引导用户给小树浇水领积分 */
function promptWaterTreeGuide(taskLabel = '', delayMs = 900) {
    setTimeout(() => {
        if (claimablePoints.value > 0) {
            const prefix = taskLabel ? `今日${taskLabel}已录入完成，` : '今日已录入完成，'
            showUserToast(
                `${prefix}点击「给小树浇水」领取 ${claimablePoints.value} 积分`,
                { duration: 3200 }
            )
            return
        }
        const pending = firstPendingDisplayTask.value
        if (pending) {
            const prefix = taskLabel ? `今日${taskLabel}已录入完成。` : '今日该项已录入完成。'
            showUserToast(
                `${prefix}继续完成「${pending.name}」，再给小树浇水领积分`,
                { duration: 3200 }
            )
            return
        }
        if (displayTasksAllCleared.value) {
            showUserToast('今日任务已全部完成，小树喝饱啦 🌱', { duration: 2800 })
        }
    }, delayMs)
}

async function waterTree() {
    if (!diagnosisId.value) return
    if (treeWaterLocked.value) return
    if (!canWaterTree.value) return

    const oldLevel = treeLevel.value
    treeWaterLocked.value = true
    clearWateringFx()
    startRain()
    scheduleWateringBurstFx()
    hapticLight()
    fxTimers.push(setTimeout(() => { treeWaterLocked.value = false }, 1400))

    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyWaterTree',
            method: 'POST',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (!isApiSuccess(res)) {
            showUserToast((res && res.msg) || '浇水失败')
            return
        }
        const data = res.data && typeof res.data === 'object' ? res.data : {}
        const addedPoints = Number(data.added_points) || 0
        applyGamifyPayload({
            ...data,
            message: data.message || res.msg || ''
        })
        await nextTick()

        const tip = formatUserMessage(data.message || res.msg, '')
        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))
                const newLevel = treeLevel.value
                if (newLevel > oldLevel) {
                    const meta = TREE_LEVELS[newLevel] || TREE_LEVELS[0]
                    setTimeout(() => triggerCelebrate('小树升级啦', `Lv.${newLevel} ${meta.name} · ${meta.desc}`), 520)
                }
            setTimeout(() => {
                if (todayTasks.value.every((t) => t.completed && t.claimed)) {
                    triggerCelebrate('今日任务大满贯', '明天继续来浇浇水吧')
                }
            }, 900)
            return
        }

        // 无可领积分：先同步 UI，再按服务端 pending 引导或提示
        if (tip) {
            showUserToast(tip, { duration: 2800 })
        }

        const needRecord = todayTasks.value.find((t) => !t.completed && !t.claimed)
        if (needRecord) {
            onGamifyTaskTap(needRecord)
            return
        }

        const pending = Array.isArray(data.pending_tasks) ? data.pending_tasks : []
        if (pending.length) {
            const match = todayTasks.value.find((t) => t.id === pending[0].id)
                || todayTasks.value.find((t) => t.name === pending[0].name)
            if (match && !match.completed) {
                onGamifyTaskTap(match)
            } else if (!tip) {
                showUserToast(`先完成：${pending[0].name}`, { duration: 2800 })
            }
            return
        }

        if (Number(data.claimable_points) > 0) {
            showUserToast('还有积分待领取，请再点一次浇水')
            return
        }

        if (allTasksCleared.value && !tip) {
            showUserToast('今日奖励已全部领取', { duration: 2800 })
        }
    } catch (e) {
        showUserToast('浇水失败，请稍后重试')
    }
}

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(() => 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`
})
const treeNextHint = computed(() => {
    if (treeState.value.isMax) return '您已养成满级稳糖树，习惯就是最好的奖励'
    if (claimablePoints.value > 0) return `浇水后可再积 ${claimablePoints.value} 分，离升级更近一步`
    return `再积 ${treeState.value.pointsToNext} 分，升级「${treeState.value.nextName}」`
})

const treeXpUpgradeHint = computed(() => {
    if (treeState.value.isMax) return '已满级，继续浇水巩固习惯'
    const need = treeState.value.pointsToNext
    const nextLv = treeLevel.value + 1
    return `还差 ${need} XP 升级至 Lv.${nextLv}`
})

// ============ 分享 / 家人点赞 / 邀请观看 ============
const familyNicknameOptions = ['家人', '亲友', '老伴', '子女']

const weeklyReportVisible = ref(false)
const shareTitle = ref('')
const pendingShareInviteCode = ref('')

const familyLikeSummary = ref({
    like_count: 0,
    summary_line: '分享战报给家人，邀请他们为您点赞鼓劲',
    recent: []
})
const lastFamilyLikeCount = ref(0)

const VIEWER_KEY_STORAGE = 'tongji_viewer_key_v1'

function familyLikeSeenStorageKey(dId = diagnosisId.value) {
    return `tongji_family_like_seen_${Number(dId) || 0}_${todayStr.value}`
}

function getStoredFamilyLikeSeen(dId = diagnosisId.value) {
    try {
        return Number(uni.getStorageSync(familyLikeSeenStorageKey(dId))) || 0
    } catch (e) {
        return 0
    }
}

function setStoredFamilyLikeSeen(count, dId = diagnosisId.value) {
    try {
        uni.setStorageSync(familyLikeSeenStorageKey(dId), Number(count) || 0)
    } catch (e) {}
}

function getViewerKey() {
    try {
        let key = uni.getStorageSync(VIEWER_KEY_STORAGE)
        if (!key) {
            key = `v_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`
            uni.setStorageSync(VIEWER_KEY_STORAGE, key)
        }
        return String(key).slice(0, 64)
    } catch (e) {
        return `v_${Date.now()}`
    }
}

function getWeekOfYear(date) {
    const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
    const dayNum = d.getUTCDay() || 7
    d.setUTCDate(d.getUTCDate() + 4 - dayNum)
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
    return Math.ceil((((d - yearStart) / 86400000) + 1) / 7)
}

const weeklyReportData = computed(() => {
    const name = (patientName.value || '我').trim()
    const patientLabel = name.length > 6 ? `${name.slice(0, 6)}…` : name
    const now = new Date()
    const weekLabel = `${now.getFullYear()}年 第${getWeekOfYear(now)}周`
    const rd = last7CheckedCount.value
    const cd = weekCompleteDays.value
    let quote = '坚持记录，就是在照顾未来的自己。'
    if (rd >= 7) quote = '本周每天都留下了记录，这份自律值得骄傲！'
    else if (cd >= 5) quote = `本周有 ${cd} 天完成了空腹+餐后记录，习惯越来越稳。`
    else if (rd >= 4) quote = `本周已记录 ${rd} 天，比不记录强一百倍。`
    else if (rd > 0) quote = '好的开始！下周试着多记录几天。'
    else quote = '从今天记一笔开始，小树就会发芽。'
    return {
        patientLabel,
        weekLabel,
        recordDays: rd,
        completeDays: cd,
        streakDays: streakDays.value.days,
        quote
    }
})

const reportTreeLevel = computed(() => treeLevel.value)

const inviteTreeLevel = computed(() => Number(invitePreview.value.tree_level) || 0)

const inviteDailyRecords = computed(() => {
    const list = invitePreview.value.daily_records
    return Array.isArray(list) ? list : []
})

function formatInviteSugar(v) {
    if (v == null || v === '') return '—'
    return String(v)
}

const ENCOURAGE_LINES = [
    '每天记一笔，就是在照顾未来的自己。',
    '坚持记录比纠结数值更重要，您做得很好。',
    '给树浇浇水，也给自己点个赞。',
    '家人会看到您的努力，习惯比完美更珍贵。',
    '空腹、餐后各记一次，完整一日更有成就感。'
]
const encourageIndex = ref(0)
const encourageLine = computed(() => {
    const pending = displayTasksForUi.value.find((t) => !t.completed)
    if (pending?.id === 'glucose') return '先完成血糖记录，再给小树浇水领积分。'
    if (pending?.id === 'bp') return '先测血压并记录，再给小树浇水领积分。'
    if (pending?.id === 'diet') return '记下今日饮食，再给小树浇水领积分。'
    if (pending?.id === 'exercise') return '动一动、记一记，再给小树浇水领积分。'
    if (canWaterTree.value) {
        return `有 ${claimablePoints.value} 积分待领，点击「给小树浇水」吧。`
    }
    if (displayTasksAllCleared.value) {
        return '今日任务已全部完成，小树喝饱啦，明天继续加油。'
    }
    return ENCOURAGE_LINES[encourageIndex.value % ENCOURAGE_LINES.length]
})

const encourageParts = computed(() => {
    const line = encourageLine.value
    const highlights = ['给小树浇水', '今日第一格就亮起来']
    for (const highlight of highlights) {
        if (line.includes(highlight)) {
            const i = line.indexOf(highlight)
            return {
                prefix: line.slice(0, i),
                highlight,
                suffix: line.slice(i + highlight.length)
            }
        }
    }
    return { prefix: line, highlight: '', suffix: '' }
})

function onEncourageTap() {
    encourageIndex.value = (encourageIndex.value + 1) % ENCOURAGE_LINES.length
    const line = ENCOURAGE_LINES[encourageIndex.value]
    if (ttsEnabled.value) {
        ttsSpeak(line)
    } else {
        showUserToast(line, { duration: 2800 })
    }
}

function goBack() {
    const pages = getCurrentPages()
    if (pages.length > 1) {
        uni.navigateBack({ delta: 1 })
        return
    }
    uni.navigateTo({ url: '/tongji/pages/weekly' })
}

function goGlucosePage() {
    uni.navigateTo({ url: '/tongji/pages/weekly' })
}

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
        }
        const token = uni.getStorageSync('token')
        if (!token) {
            showUserToast('请先登录后再录入')
            return
        }
        if (!skipGuide) {
            showUserToast(`完成「${task.name}」后，点击「给小树浇水」领积分`, { duration: 2600 })
        }
        if (task.id === 'glucose') {
            openInputForm()
            return
        }
        if (task.id === 'bp') {
            openBpForm()
            return
        }
        if (task.id === 'diet') {
            openDietForm()
            return
        }
        if (task.id === 'exercise') {
            openExerciseForm()
            return
        }
        if (task.id === 'blood') {
            openInputForm()
            return
        }
        showUserToast(`请先完成：${task.name}`)
        return
    }
    if (task.claimed) {
        showUserToast(`今日「${task.name}」已录入，积分已领取`)
        return
    }
    openWaterStatusPanel()
}

function shareInviteCacheKey() {
    return `tongji_invite_${diagnosisId.value}_${todayStr.value}`
}

async function ensureShareInvite() {
    if (!diagnosisId.value) return ''
    const cacheKey = shareInviteCacheKey()
    try {
        const cached = uni.getStorageSync(cacheKey)
        if (cached && cached.invite_code && cached.invite_date === todayStr.value) {
            pendingShareInviteCode.value = cached.invite_code
            return cached.invite_code
        }
    } catch (e) {}

    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyCreateShareInvite',
            method: 'POST',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res && res.code === 1 && res.data && res.data.invite_code) {
            const code = res.data.invite_code
            pendingShareInviteCode.value = code
            try {
                uni.setStorageSync(cacheKey, {
                    invite_code: code,
                    invite_date: todayStr.value
                })
            } catch (e) {}
            return code
        }
        if (res?.msg) showUserToast(res.msg)
    } catch (e) {
        showUserToast('生成邀请码失败')
    }
    return ''
}

async function prepareShareWeekly() {
    const code = await ensureShareInvite()
    if (!code) return
    const d = weeklyReportData.value
    shareTitle.value = `【稳糖打卡】${d.patientLabel}本周记录${d.recordDays}天，邀请码 ${code}（今日有效）`
}

function shareWeeklyFallback() {
    prepareShareWeekly().then((code) => {
        if (!code) return
        const d = weeklyReportData.value
        const text = `【稳糖打卡】${d.patientLabel} ${d.weekLabel}\n本周 ${d.recordDays} 天有记录、${d.completeDays} 个完整日、连续 ${d.streakDays} 天\n${d.quote}\n邀请码：${code}（仅今日有效）\n打开小程序「日常记录」输入邀请码查看`
        uni.setClipboardData({
            data: text,
            success: () => showUserToast('战报文案已复制，可发给家人')
        })
    })
}

async function openWeeklyReport() {
    if (!diagnosisId.value) {
        showUserToast('请先选择就诊卡')
        return
    }
    await ensureShareInvite()
    weeklyReportVisible.value = true
}

function closeWeeklyReport() {
    weeklyReportVisible.value = false
}

async function loadFamilyLikeSummary() {
    if (!hasAuthToken() || !diagnosisId.value) return
    const token = uni.getStorageSync('token')
    if (!token) return
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyFamilyLikeSummary',
            method: 'GET',
            data: { diagnosis_id: diagnosisId.value }
        }, false)
        if (res && res.code === 1 && res.data) {
            const storedSeen = getStoredFamilyLikeSeen()
            const prev = Math.max(lastFamilyLikeCount.value, storedSeen)
            const next = Number(res.data.like_count) || 0
            familyLikeSummary.value = {
                like_count: next,
                summary_line: res.data.summary_line || '',
                recent: Array.isArray(res.data.recent) ? res.data.recent : []
            }
            // 仅在有「新增」点赞时庆祝，避免刷新/重进页面重复弹层
            if (next > prev) {
                const delta = next - prev
                triggerCelebrate(
                    delta === 1 && prev === 0 ? '首个家人点赞' : '收到家人鼓励',
                    delta === 1 ? '分享真的有用，继续加油' : `新增 ${delta} 位家人点赞，共 ${next} 人`
                )
                hapticLight()
            }
            lastFamilyLikeCount.value = next
            setStoredFamilyLikeSeen(next)
        }
    } catch (e) {}
}

function onFamilyLikeStripTap() {
    if (familyLikeSummary.value.like_count > 0) {
        if (ttsEnabled.value) {
            ttsSpeak(familyLikeSummary.value.summary_line)
        } else {
            showUserToast(familyLikeSummary.value.summary_line, { duration: 2400 })
        }
        return
    }
    openWeeklyReport()
}


</script>

<style lang="scss" scoped>
.page-gate-loading {
    min-height: 60vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 80rpx 40rpx;
}
.page-gate-loading-text {
    font-size: 28rpx;
    color: #64748b;
}
.daily-page {
    /* 详见 ../styles/daily-theme.scss */
    --radius-card: 32rpx;
    --radius-pill: 999rpx;
    --transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);

    min-height: 100vh;
    padding-bottom: 64rpx;
}

/* === Hero 区域 === */
.hero {
    position: relative;
    padding: 60rpx 36rpx 120rpx; /* 适度增加空间，增强空气感 */
    color: #ffffff;
    overflow: hidden;
}
.hero-bg {
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg, #163d22 0%, #204e2b 55%, #386641 100%);
    z-index: 0;
    &::after {
        content: '';
        position: absolute;
        inset: 0;
        background: radial-gradient(circle at 85% 15%, rgba(175, 226, 179, 0.15) 0%, transparent 60%);
    }
}
.hero-content {
    position: relative;
    z-index: 1;
}
.hero-row {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12rpx;
}
.hero-main {
    flex: 1;
    min-width: 0;
    padding-top: 4rpx;
}
.hero-title {
    font-size: 56rpx; /* 大字体适老化 */
    font-weight: 800;
    color: #ffffff;
    line-height: 1.2;
    letter-spacing: 1rpx;
    text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15);
}
.hero-subtitle {
    display: block;
    font-size: 30rpx;
    color: rgba(255, 255, 255, 0.95);
    margin-top: 6rpx;
    font-weight: 500;
}
.hero-pill-group {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 12rpx;
    max-width: 380rpx;
}
.hero-pill {
    display: inline-flex;
    align-items: center;
    gap: 10rpx;
    background: rgba(255, 255, 255, 0.12);
    border: 1rpx solid rgba(255, 255, 255, 0.2);
    border-radius: var(--radius-pill);
    padding: 18rpx 30rpx;
    backdrop-filter: blur(12px);
    min-height: 80rpx; /* 点击热区符合规范 */
    box-sizing: border-box;
    transition: var(--transition-smooth);
    &:active {
        background: rgba(255, 255, 255, 0.24);
        transform: scale(0.96);
    }
    &.voice {
        background: rgba(255, 255, 255, 0.18);
        border: 2rpx solid rgba(255, 255, 255, 0.4);
        padding: 16rpx 30rpx;
    }
    &.voice.speaking {
        background: #fbbf24;
        border-color: #fde68a;
        color: var(--slate-900);
        animation: voice-pulse 1.4s ease-in-out infinite;
        .hero-pill-text {
            color: var(--slate-900);
        }
    }
    &.voice.disabled {
        opacity: 0.5;
    }
    &.input {
        background: #ffffff;
        border: 2rpx solid #ffffff;
        padding: 16rpx 32rpx;
        box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.1);
        .hero-pill-text {
            color: var(--primary);
            font-weight: 700;
        }
        &:active {
            background: var(--primary-light);
        }
    }
}
@keyframes voice-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.55); }
    50% { box-shadow: 0 0 0 12rpx rgba(251, 191, 36, 0); }
}
.hero-pill-icon-wrap {
    width: 36rpx;
    height: 36rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.hero-pill-text {
    font-size: 32rpx;
    color: #ffffff;
    font-weight: 600;
}

/* === Bento 风格指标卡 === */
.stat-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 20rpx;
    margin: -60rpx 24rpx 0;
    position: relative;
    z-index: 3;
}
.stat-card {
    background: #ffffff;
    border-radius: var(--radius-card);
    padding: 30rpx 26rpx;
    box-shadow: var(--shadow-premium);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    min-height: 250rpx;
    border: 1rpx solid rgba(15, 23, 42, 0.04);
    box-sizing: border-box;
    transition: var(--transition-smooth);
    position: relative;
    overflow: hidden;
    &:active {
        transform: translateY(-2rpx);
        box-shadow: 0 16rpx 36rpx rgba(15, 23, 42, 0.08);
    }
}
.stat-fasting {
    background: radial-gradient(circle at 100% 0%, #e6fdf9 0%, #ffffff 65%);
}
.stat-postprandial {
    background: radial-gradient(circle at 100% 0%, #fff7ed 0%, #ffffff 65%);
}
.stat-other {
    grid-column: span 2;
    background: radial-gradient(circle at 100% 0%, #f5f3ff 0%, #ffffff 65%);
    display: grid !important;
    grid-template-areas: 
        "head value"
        "foot value" !important;
    grid-template-columns: 1fr auto !important;
    grid-template-rows: auto auto !important;
    align-items: center !important;
    min-height: 170rpx !important;
    padding: 26rpx 32rpx !important;
    
    .stat-card-head {
        grid-area: head !important;
        display: flex !important;
        align-items: center !important;
        justify-content: flex-start !important;
        margin: 0 !important;
        padding: 0 !important;
        gap: 12rpx !important;
    }
    .stat-card-value-row {
        grid-area: value !important;
        display: flex !important;
        align-items: baseline !important;
        margin: 0 !important;
        padding: 0 !important;
        gap: 6rpx !important;
    }
    .stat-card-foot {
        grid-area: foot !important;
        display: flex !important;
        align-items: center !important;
        justify-content: flex-start !important;
        margin-top: 12rpx !important;
        padding: 0 !important;
        gap: 16rpx !important;
    }
}
.stat-card.is-high {
    box-shadow: var(--shadow-glow-danger);
    border-color: rgba(239, 68, 68, 0.08);
}
.stat-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8rpx;
}
.stat-card-tag {
    display: inline-flex;
    align-items: center;
    padding: 8rpx 18rpx;
    border-radius: 12rpx;
    font-size: 26rpx;
    font-weight: 700;
}
.stat-fasting .stat-card-tag {
    background: rgba(13, 148, 136, 0.09);
    color: #386641;
}
.stat-postprandial .stat-card-tag {
    background: rgba(245, 158, 11, 0.09);
    color: #d97706;
}
.stat-other .stat-card-tag {
    background: rgba(99, 102, 241, 0.09);
    color: #4f46e5;
}
.stat-card-tag-dot {
    display: none;
}
.stat-card-tag-text {
    font-size: 26rpx;
    font-weight: 700;
}
.stat-card-flag {
    display: inline-flex;
    align-items: center;
    gap: 6rpx;
    padding: 6rpx 14rpx;
    border-radius: var(--radius-pill);
    background: rgba(239, 68, 68, 0.08);
    border: 1rpx solid rgba(239, 68, 68, 0.15);
}
.stat-card-flag-icon {
    font-size: 20rpx;
    color: var(--danger);
    font-weight: 700;
    line-height: 1;
}
.stat-card-flag-text {
    font-size: 22rpx;
    color: var(--danger);
    font-weight: 700;
}
.stat-card-value-row {
    display: flex;
    align-items: baseline;
    gap: 6rpx;
    margin-top: 16rpx;
    margin-bottom: 8rpx;
}
.stat-card-value {
    font-size: 64rpx;
    font-weight: 900;
    background: linear-gradient(180deg, var(--slate-900) 0%, #334155 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    line-height: 1.05;
    letter-spacing: -1rpx;
    &.is-high {
        background: var(--danger-grad);
        -webkit-background-clip: text;
        background-clip: text;
        color: transparent;
    }
}
.stat-card-unit {
    font-size: 24rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.stat-card-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8rpx;
    margin-top: 4rpx;
}
.stat-card-date {
    font-size: 24rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.stat-card-trend {
    display: inline-flex;
    align-items: center;
    gap: 4rpx;
    padding: 6rpx 14rpx;
    border-radius: var(--radius-pill);
    transition: var(--transition-smooth);
    &.trend-up {
        background: rgba(239, 68, 68, 0.08);
        .stat-card-trend-arrow,
        .stat-card-trend-delta {
            color: var(--danger);
        }
    }
    &.trend-down {
        background: rgba(16, 185, 129, 0.08);
        .stat-card-trend-arrow,
        .stat-card-trend-delta {
            color: var(--success);
        }
    }
    &.trend-flat {
        background: var(--slate-100);
        .stat-card-trend-arrow,
        .stat-card-trend-delta {
            color: var(--slate-600);
        }
    }
}
.stat-card-trend-arrow {
    font-size: 24rpx;
    font-weight: 800;
    line-height: 1;
}
.stat-card-trend-delta {
    font-size: 24rpx;
    font-weight: 600;
}

/* === 就诊卡选择器 === */
.card-switcher {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin: 22rpx 24rpx 0;
    padding: 8rpx 12rpx;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
}
.card-switcher-head {
    display: flex;
    align-items: center;
    gap: 8rpx;
    flex-shrink: 0;
    padding-right: 10rpx;
    border-right: 2rpx solid var(--slate-200);
}
.card-switcher-label {
    font-size: 28rpx;
    color: var(--slate-900);
    font-weight: 700;
}
.card-switcher-count {
    font-size: 24rpx;
    color: var(--slate-600);
    background: var(--slate-100);
    padding: 4rpx 14rpx;
    border-radius: var(--radius-pill);
    font-weight: 600;
}
.card-switcher-scroll {
    flex: 1;
    min-width: 0;
    white-space: nowrap;
}
.card-chip-row {
    display: inline-flex;
    flex-wrap: nowrap;
    gap: 8rpx;
    padding: 2rpx;
}
.card-chip {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 8rpx;
    padding: 16rpx 28rpx;
    border-radius: var(--radius-pill);
    background: #ffffff;
    border: 1rpx solid var(--slate-200);
    transition: var(--transition-smooth);
    min-height: 68rpx;
    box-sizing: border-box;
    &.active {
        background: var(--primary-grad);
        border-color: transparent;
        box-shadow: var(--shadow-glow-primary);
        .card-chip-name,
        .card-chip-meta {
            color: #ffffff;
        }
    }
    &:active {
        transform: scale(0.96);
    }
}
.card-chip-name {
    font-size: 28rpx;
    font-weight: 700;
    color: #1e293b;
    line-height: 1.1;
}
.card-chip-meta {
    font-size: 24rpx;
    color: var(--slate-600);
    line-height: 1.1;
}

.range-bar {
    display: flex;
    gap: 12rpx;
    margin: 22rpx 24rpx 0;
    background: #ffffff;
    border-radius: var(--radius-pill);
    padding: 10rpx;
    box-shadow: var(--shadow-sm);
    border: 1rpx solid var(--slate-100);
    position: relative;
    z-index: 2;
}
.range-item {
    flex: 1;
    text-align: center;
    padding: 22rpx 0;
    border-radius: var(--radius-pill);
    font-size: 32rpx;
    color: #334155;
    font-weight: 600;
    min-height: 76rpx;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: var(--transition-smooth);
    &.active {
        background: var(--primary-grad);
        color: #ffffff;
        font-weight: 700;
        box-shadow: var(--shadow-glow-primary);
    }
    &:active:not(.active) {
        background: var(--slate-100);
    }
}

/* === 语音播报设置 === */
.voice-bar {
    margin: 22rpx 24rpx 0;
    background: #ffffff;
    border-radius: var(--radius-card);
    padding: 26rpx;
    box-shadow: var(--shadow-premium);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 18rpx;
    border: 2rpx solid #e0f2fe;
    transition: var(--transition-smooth);
    &:active {
        border-color: #bae6fd;
    }
}
.voice-bar-left {
    display: flex;
    align-items: center;
    gap: 16rpx;
    flex: 1;
    min-width: 0;
}
.voice-bar-icon {
    font-size: 48rpx;
    line-height: 1;
}
.voice-bar-text {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
    min-width: 0;
}
.voice-bar-title {
    font-size: 34rpx;
    font-weight: 700;
    color: var(--slate-900);
}
.voice-bar-hint {
    font-size: 26rpx;
    color: var(--slate-600);
    margin-top: 4rpx;
}
.voice-bar-actions {
    display: flex;
    align-items: center;
    gap: 16rpx;
    flex-shrink: 0;
}
.voice-switch {
    display: flex;
    align-items: center;
    gap: 12rpx;
    padding: 10rpx 12rpx 10rpx 18rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-100);
    transition: var(--transition-smooth);
    min-height: 76rpx;
    box-sizing: border-box;
    &.on {
        background: linear-gradient(135deg, #fef3c7, #fde68a);
    }
}
.voice-switch-label {
    font-size: 28rpx;
    color: #1e293b;
    font-weight: 700;
}
.voice-switch-track {
    width: 80rpx;
    height: 44rpx;
    background: var(--slate-300);
    border-radius: var(--radius-pill);
    position: relative;
    transition: var(--transition-smooth);
    .voice-switch.on & {
        background: var(--warning);
    }
}
.voice-switch-knob {
    position: absolute;
    width: 36rpx;
    height: 36rpx;
    border-radius: var(--radius-pill);
    background: #ffffff;
    top: 4rpx;
    left: 4rpx;
    box-shadow: 0 2rpx 6rpx rgba(15, 23, 42, 0.15);
    transition: var(--transition-smooth);
    .voice-switch.on & {
        transform: translateX(36rpx);
    }
}
.voice-toggle {
    width: 76rpx;
    height: 76rpx;
    border-radius: var(--radius-pill);
    background: var(--primary-grad);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-glow-primary);
    transition: var(--transition-smooth);
    &.off {
        background: var(--slate-400);
        box-shadow: none;
    }
    &:active {
        transform: scale(0.95);
    }
}
.voice-toggle-icon {
    font-size: 36rpx;
    color: #ffffff;
    line-height: 1;
}

.card {
    background: #ffffff;
    border-radius: var(--radius-card);
    margin: 28rpx 24rpx 0;
    padding: 30rpx 28rpx;
    box-shadow: var(--shadow-premium);
    animation: card-rise 0.45s ease-out both;
}

/* 动效延迟展现列表，视觉更灵动 */
.streak-strip { animation: card-rise 0.45s ease-out 0.03s both; }
.card-switcher { animation: card-rise 0.45s ease-out 0.06s both; }
.range-bar { animation: card-rise 0.4s ease-out 0.08s both; }
.voice-bar { animation: card-rise 0.4s ease-out 0.10s both; }
.chart-card { animation: card-rise 0.5s ease-out 0.12s both; }
.calendar-card { animation: card-rise 0.5s ease-out 0.14s both; }
.records-section { animation: card-rise 0.5s ease-out 0.16s both; }

@keyframes card-rise {
    from {
        opacity: 0;
        transform: translateY(16rpx);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes stat-card-pop {
    from {
        opacity: 0;
        transform: translateY(24rpx) scale(0.96);
    }
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

.stat-card {
    animation: stat-card-pop 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.stat-fasting { animation-delay: 0.04s; }
.stat-postprandial { animation-delay: 0.08s; }
.stat-other { animation-delay: 0.12s; }

.card-head {
    display: flex;
    justify-content: space-between;
    align-items: center;
}
.card-title {
    font-size: 36rpx;
    font-weight: 800;
    color: var(--slate-900);
    letter-spacing: 0.5rpx;
}
.card-subtitle {
    display: block;
    font-size: 26rpx;
    color: var(--slate-600);
    margin-top: 6rpx;
}
.card-legend {
    display: flex;
    gap: 18rpx;
}
.legend-item {
    display: flex;
    align-items: center;
    gap: 8rpx;
    font-size: 22rpx;
    color: var(--slate-600);
}
.legend-dot {
    width: 16rpx;
    height: 16rpx;
    border-radius: var(--radius-pill);
}
.legend-dot-fasting { background: var(--primary); }
.legend-dot-postprandial { background: var(--warning); }

.chart-summary {
    display: flex;
    align-items: stretch;
    gap: 0;
    margin-top: 18rpx;
    padding: 16rpx 4rpx;
    background: linear-gradient(135deg, var(--slate-50) 0%, var(--slate-100) 100%);
    border-radius: 18rpx;
    border: 1rpx solid #eef2f5;
}
.chart-summary-item {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4rpx;
    position: relative;
}
.chart-summary-item + .chart-summary-item::before {
    content: '';
    position: absolute;
    left: 0;
    top: 18%;
    bottom: 18%;
    width: 1rpx;
    background: var(--slate-200);
}
.chart-summary-num {
    font-size: 44rpx;
    font-weight: 800;
    color: var(--slate-900);
    line-height: 1.1;
    letter-spacing: -0.5rpx;
}
.chart-summary-item:first-child .chart-summary-num {
    color: var(--danger);
}
.chart-summary-item:nth-child(2) .chart-summary-num {
    color: var(--primary);
}
.chart-summary-label {
    font-size: 24rpx;
    color: var(--slate-600);
    letter-spacing: 1rpx;
    font-weight: 500;
    margin-top: 4rpx;
}

.chart-canvas-wrap {
    position: relative;
    width: 100%;
    height: 460rpx;
    margin-top: 22rpx;
}
.chart-canvas-placeholder {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(180deg, var(--slate-50) 0%, var(--slate-100) 100%);
    border-radius: 18rpx;
    color: var(--slate-600);
    font-size: 28rpx;
    letter-spacing: 1rpx;
    font-weight: 500;
}
.chart-canvas {
    width: 100%;
    height: 100%;
    display: block;
    position: relative;
    z-index: 1;
}
.chart-empty {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14rpx;
    color: var(--slate-600);
}
.chart-empty-icon {
    font-size: 72rpx;
}
.chart-empty-text {
    font-size: 30rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.chart-tip {
    position: absolute;
    min-width: 220rpx;
    padding: 16rpx 20rpx;
    background: rgba(15, 23, 42, 0.94);
    color: #ffffff;
    border-radius: 12rpx;
    box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.18);
    pointer-events: none;
    z-index: 999;
}
.chart-tip-date {
    font-size: 26rpx;
    color: rgba(255, 255, 255, 0.9);
    font-weight: 600;
}
.chart-tip-row {
    display: flex;
    align-items: center;
    gap: 10rpx;
    margin-top: 8rpx;
}
.chart-tip-text {
    font-size: 28rpx;
    color: #ffffff;
    font-weight: 500;
}

/* === 日常记录列表样式 === */
.records-section {
    margin: 28rpx 24rpx 0;
}
.records-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20rpx;
    padding: 0 6rpx;
}
.records-count {
    font-size: 28rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.day-card {
    background: #ffffff;
    border-radius: 28rpx;
    padding: 30rpx 28rpx;
    margin-bottom: 22rpx;
    box-shadow: var(--shadow-premium);
    border: var(--border-premium);
}
.day-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 14rpx;
    border-bottom: 1rpx solid var(--slate-100);
    padding-bottom: 12rpx;
}
.day-head-left {
    display: flex;
    align-items: center;
    gap: 10rpx;
}
.day-date {
    font-size: 36rpx;
    font-weight: 800;
    color: var(--slate-900);
}
.day-today-pill {
    font-size: 24rpx;
    padding: 4rpx 16rpx;
    border-radius: var(--radius-pill);
    background: var(--primary-grad);
    color: #ffffff;
    font-weight: 600;
    box-shadow: var(--shadow-glow-primary);
}
.day-head-right {
    display: flex;
    align-items: center;
    gap: 14rpx;
}
.day-weekday {
    font-size: 26rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.day-speak-btn {
    display: flex;
    align-items: center;
    gap: 8rpx;
    padding: 14rpx 28rpx;
    border-radius: var(--radius-pill);
    background: var(--primary-light);
    border: 1rpx solid rgba(13, 148, 136, 0.2);
    box-shadow: none;
    min-height: 64rpx;
    box-sizing: border-box;
    transition: var(--transition-smooth);
    .day-speak-icon {
        font-size: 26rpx;
        color: var(--primary);
    }
    .day-speak-text {
        font-size: 26rpx;
        color: var(--primary);
        font-weight: 700;
    }
    &:active {
        background: var(--primary-glow);
        transform: scale(0.96);
    }
}
.day-block {
    margin-top: 18rpx;
    padding: 24rpx;
    background: var(--slate-50);
    border-radius: 20rpx;
    border: 1rpx solid var(--slate-100);
}
.day-block-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.day-block-tag {
    font-size: 24rpx;
    padding: 8rpx 20rpx;
    border-radius: var(--radius-pill);
    font-weight: 700;
    line-height: 1;
    &.tag-blood {
        background: var(--primary-light);
        color: var(--primary);
        border: 1rpx solid rgba(13, 148, 136, 0.15);
    }
    &.tag-diet {
        background: var(--warning-light);
        color: #d97706;
        border: 1rpx solid rgba(245, 158, 11, 0.15);
    }
    &.tag-exercise {
        background: var(--violet-light);
        color: #4f46e5;
        border: 1rpx solid rgba(99, 102, 241, 0.15);
    }
    &.tag-tracking {
        background: var(--slate-100);
        color: var(--slate-600);
        border: 1rpx solid var(--slate-200);
    }
}
.day-block-time {
    font-size: 26rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.metric-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 12rpx;
    margin-top: 14rpx;
}
.metric-cell {
    flex: 1 1 calc(50% - 6rpx);
    background: #ffffff;
    border-radius: 14rpx;
    padding: 18rpx 18rpx 16rpx;
    position: relative;
    border: 1rpx solid var(--slate-100);
}
.metric-row-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8rpx;
}
.metric-label {
    font-size: 26rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.metric-flag {
    display: inline-flex;
    align-items: center;
    gap: 4rpx;
    padding: 2rpx 10rpx 2rpx 6rpx;
    border-radius: var(--radius-pill);
    background: var(--danger-light);
    border: 1rpx solid #fecaca;
    &.up {
        background: var(--danger-grad);
        border-color: transparent;
        box-shadow: var(--shadow-glow-danger);
    }
}
.metric-flag-icon {
    font-size: 16rpx;
    color: #ffffff;
    font-weight: 700;
    line-height: 1;
}
.metric-flag-text {
    font-size: 18rpx;
    color: #ffffff;
    font-weight: 600;
}
.metric-value-row {
    display: flex;
    align-items: baseline;
    gap: 6rpx;
    margin-top: 6rpx;
}
.metric-value {
    font-size: 40rpx;
    font-weight: 800;
    color: var(--slate-900);
    line-height: 1.1;
    &.is-high {
        color: var(--danger);
    }
}
.metric-unit {
    font-size: 24rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.metric-trend {
    display: inline-flex;
    align-items: center;
    gap: 4rpx;
    margin-top: 10rpx;
    padding: 2rpx 12rpx 2rpx 8rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-100);
    border: 1rpx solid transparent;
    &.trend-up {
        background: #fef3c7;
        border-color: #fde68a;
        .metric-trend-arrow,
        .metric-trend-text {
            color: #b45309;
        }
    }
    &.trend-down {
        background: #dcfce7;
        border-color: #bbf7d0;
        .metric-trend-arrow,
        .metric-trend-text {
            color: #15803d;
        }
    }
    &.trend-flat {
        background: var(--slate-100);
        border-color: var(--slate-200);
        .metric-trend-arrow,
        .metric-trend-text {
            color: var(--slate-600);
        }
    }
}
.metric-trend-arrow {
    font-size: 20rpx;
    font-weight: 700;
    line-height: 1;
}
.metric-trend-text {
    font-size: 24rpx;
    font-weight: 700;
}
.day-meta-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10rpx;
    margin-top: 14rpx;
}
.meta-chip {
    font-size: 22rpx;
    color: var(--slate-600);
    background: #ffffff;
    border-radius: var(--radius-pill);
    padding: 6rpx 18rpx;
    border: 1rpx solid var(--slate-200);
}
.day-remark {
    margin-top: 14rpx;
    font-size: 28rpx;
    color: var(--slate-900);
    line-height: 1.7;
}
.diet-rows {
    margin-top: 14rpx;
    display: flex;
    flex-direction: column;
    gap: 10rpx;
}
.diet-row {
    display: flex;
    gap: 10rpx;
    align-items: flex-start;
}
.diet-meal {
    flex-shrink: 0;
    width: 140rpx;
    font-size: 28rpx;
    color: #b45309;
    font-weight: 700;
}
.diet-text {
    flex: 1;
    font-size: 28rpx;
    color: var(--slate-900);
    line-height: 1.7;
}
.exercise-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12rpx;
    margin-top: 14rpx;
}
.exercise-type {
    font-size: 26rpx;
    color: var(--slate-900);
    font-weight: 600;
}
.exercise-duration {
    font-size: 24rpx;
    color: var(--violet);
    background: var(--violet-light);
    border-radius: var(--radius-pill);
    padding: 4rpx 16rpx;
    font-weight: 600;
}
.exercise-intensity {
    font-size: 24rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.tracking-content {
    margin-top: 12rpx;
    display: flex;
    flex-direction: column;
    gap: 6rpx;
}
.tracking-line {
    font-size: 28rpx;
    color: var(--slate-900);
    line-height: 1.7;
    word-break: break-all;
}
.state-card {
    background: #ffffff;
    border-radius: var(--radius-card);
    padding: 60rpx 24rpx;
    text-align: center;
    box-shadow: var(--shadow-premium);
    border: var(--border-premium);
}
.state-icon {
    display: block;
    font-size: 76rpx;
    margin-bottom: 16rpx;
}
.state-text {
    font-size: 30rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.footer-tip {
    text-align: center;
    font-size: 24rpx;
    color: var(--slate-400);
    margin-top: 40rpx;
    line-height: 1.6;
}

/* === 顶部 Hero 漂浮小饰品 === */
.hero-orb {
    position: absolute;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.08);
    pointer-events: none;
    filter: blur(4rpx);
}
.hero-orb-1 {
    width: 220rpx;
    height: 220rpx;
    top: -60rpx;
    right: -40rpx;
    background: rgba(255, 255, 255, 0.12);
}
.hero-orb-2 {
    width: 140rpx;
    height: 140rpx;
    top: 40rpx;
    right: 240rpx;
    background: rgba(253, 224, 71, 0.1);
}
.hero-orb-3 {
    width: 90rpx;
    height: 90rpx;
    bottom: 30rpx;
    left: 40%;
    background: rgba(255, 255, 255, 0.08);
}
.hero-greet-row {
    display: flex;
    align-items: center;
    gap: 12rpx;
    margin-bottom: 6rpx;
}
.hero-greet-icon-wrap {
    width: 52rpx;
    height: 52rpx;
    border-radius: 999rpx;
    background: rgba(255, 255, 255, 0.18);
    border: 1rpx solid rgba(255, 255, 255, 0.28);
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.hero-greet-text {
    font-size: 30rpx;
    color: rgba(255, 255, 255, 0.95);
    letter-spacing: 2rpx;
    font-weight: 500;
}
.hero-subtitle-row {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin-top: 10rpx;
    flex-wrap: wrap;
}
.hero-status-chip {
    display: inline-flex;
    align-items: center;
    gap: 8rpx;
    padding: 8rpx 18rpx 8rpx 12rpx;
    border-radius: var(--radius-pill);
    font-size: 28rpx;
    font-weight: 600;
    backdrop-filter: blur(8px);
    &.done {
        background: rgba(34, 197, 94, 0.25);
        color: #ffffff;
        border: 1rpx solid rgba(187, 247, 208, 0.5);
    }
    &.pending {
        background: rgba(251, 191, 36, 0.28);
        color: #ffffff;
        border: 1rpx solid rgba(253, 230, 138, 0.5);
    }
}
.hero-status-icon {
    width: 32rpx;
    height: 32rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.hero-status-text {
    line-height: 1.2;
}

/* === 打卡条 === */
.streak-strip {
    position: relative;
    margin: 22rpx 24rpx 0;
    background: linear-gradient(135deg, #ffffff 0%, var(--primary-light) 100%);
    border: 1rpx solid rgba(13, 148, 136, 0.12);
    border-radius: var(--radius-card);
    padding: 32rpx;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20rpx;
    box-shadow: 0 10rpx 30rpx rgba(13, 148, 136, 0.05);
    overflow: hidden;
    min-height: 132rpx;
    box-sizing: border-box;
    transition: var(--transition-smooth);
    &:active {
        transform: scale(0.99);
    }
    &::after {
        content: '';
        position: absolute;
        top: -40rpx;
        right: -40rpx;
        width: 160rpx;
        height: 160rpx;
        border-radius: 50%;
        background: radial-gradient(circle, rgba(251, 191, 36, 0.15) 0%, rgba(251, 191, 36, 0) 70%);
        pointer-events: none;
    }
}
.streak-strip-left {
    display: flex;
    align-items: center;
    gap: 16rpx;
    flex-shrink: 0;
}
.streak-flame {
    width: 92rpx;
    height: 92rpx;
    border-radius: 24rpx;
    background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: inset 0 -3rpx 0 rgba(245, 158, 11, 0.18);
    flex-shrink: 0;
    &.active {
        background: linear-gradient(135deg, #fb923c 0%, #f97316 100%);
        box-shadow: 0 6rpx 14rpx rgba(249, 115, 22, 0.35), inset 0 -3rpx 0 rgba(0, 0, 0, 0.08);
    }
}
.streak-flame-icon {
    font-size: 52rpx;
    line-height: 1;
}
.streak-info {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
}
.streak-num-row {
    display: flex;
    align-items: baseline;
    gap: 8rpx;
    flex-wrap: wrap;
}
.streak-num {
    font-size: 64rpx;
    font-weight: 800;
    line-height: 1;
    letter-spacing: -1rpx;
    background: var(--primary-grad);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
.streak-num-unit {
    font-size: 32rpx;
    color: var(--slate-900);
    font-weight: 700;
}
.streak-hint {
    margin-left: 8rpx;
    font-size: 22rpx;
    color: var(--slate-600);
    background: var(--slate-100);
    border-radius: var(--radius-pill);
    padding: 4rpx 14rpx;
    font-weight: 500;
}
.streak-label {
    font-size: 26rpx;
    color: var(--slate-600);
    letter-spacing: 0.5rpx;
    margin-top: 2rpx;
}
.streak-strip-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10rpx;
    flex-shrink: 0;
}
.streak-dots-row {
    display: flex;
    gap: 12rpx;
}
.streak-dot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6rpx;
}
.streak-dot-label {
    font-size: 22rpx;
    color: var(--slate-600);
    line-height: 1;
    font-weight: 600;
}
.streak-dot-mark {
    width: 26rpx;
    height: 26rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-200);
}
.streak-dot.done .streak-dot-mark {
    background: var(--primary-grad);
    box-shadow: 0 2rpx 6rpx rgba(32, 78, 43, 0.3);
}
.streak-dot.today {
    .streak-dot-label {
        color: var(--slate-900);
        font-weight: 700;
    }
    .streak-dot-mark {
        width: 30rpx;
        height: 30rpx;
        border: 2rpx solid var(--primary);
    }
}
.streak-dot.today.done .streak-dot-mark {
    border-color: #ffffff;
    box-shadow: 0 0 0 3rpx var(--primary), 0 2rpx 8rpx rgba(32, 78, 43, 0.4);
}
.streak-week-text {
    font-size: 24rpx;
    color: var(--slate-600);
    letter-spacing: 1rpx;
    font-weight: 600;
}

/* === 健康月历 === */
.calendar-card {
    margin: 22rpx 24rpx 0;
}
.calendar-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 16rpx;
}
.calendar-head-left {
    flex: 1;
    min-width: 0;
}
.calendar-stat {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2rpx;
    padding: 4rpx 18rpx 4rpx 22rpx;
    background: linear-gradient(135deg, #eef6ef 0%, #ccfbf1 100%);
    border-radius: 20rpx;
    border-left: 6rpx solid var(--primary);
}
.calendar-stat-num {
    font-size: 48rpx;
    font-weight: 800;
    color: #386641;
    line-height: 1.1;
    letter-spacing: -1rpx;
}
.calendar-stat-label {
    font-size: 22rpx;
    color: #0f766e;
    font-weight: 600;
}
.calendar-weekdays {
    display: flex;
    margin-top: 22rpx;
    padding: 0 4rpx;
}
.calendar-weekday {
    flex: 1;
    text-align: center;
    font-size: 26rpx;
    color: var(--slate-600);
    font-weight: 700;
    letter-spacing: 1rpx;
    line-height: 1.4;
    padding: 8rpx 0;
}
.calendar-weekday-weekend {
    color: var(--warning);
}
.calendar-grid {
    margin-top: 6rpx;
    background: var(--slate-50);
    border-radius: 20rpx;
    padding: 12rpx 6rpx;
    display: flex;
    flex-direction: column;
    gap: 6rpx;
}
.calendar-row {
    display: flex;
    gap: 6rpx;
}
.calendar-cell {
    flex: 1;
    aspect-ratio: 1 / 1;
    min-height: 84rpx;
    border-radius: 18rpx;
    background: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    transition: var(--transition-smooth);
    box-sizing: border-box;
    overflow: hidden;
    border: 2rpx solid transparent;
    &.level-0 {
        background: #ffffff;
        border-color: var(--slate-100);
    }
    &.off {
        background: transparent;
        opacity: 0.4;
    }
    &.level-1 {
        background: var(--primary-light);
        border-color: rgba(13, 148, 136, 0.15);
        .calendar-cell-day { color: var(--primary); }
    }
    &.level-2 {
        background: #ccfbf1;
        border-color: #99f6e4;
        .calendar-cell-day { color: #0f766e; }
    }
    &.level-3 {
        background: var(--primary-grad);
        .calendar-cell-day { color: #ffffff; }
        box-shadow: 0 4rpx 12rpx rgba(13, 148, 136, 0.2);
    }
    &.today {
        background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
        border: 4rpx solid #ffffff;
        box-shadow: 0 0 0 3rpx #ea580c, 0 6rpx 16rpx rgba(234, 88, 12, 0.35);
        .calendar-cell-day { color: #ffffff; }
    }
    &.clickable:active {
        transform: scale(0.94);
    }
}
.calendar-cell-day {
    font-size: 32rpx;
    color: var(--slate-900);
    font-weight: 700;
    line-height: 1;
}
.calendar-cell.today .calendar-cell-day {
    color: #ffffff;
    font-weight: 800;
    transform: translateY(-6rpx);
}
.calendar-cell.off .calendar-cell-day {
    color: var(--slate-400);
    font-weight: 500;
}
.calendar-cell-mark {
    position: absolute;
    right: 8rpx;
    bottom: 8rpx;
    width: 10rpx;
    height: 10rpx;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.85);
    box-shadow: 0 0 0 2rpx rgba(15, 23, 42, 0.08);
}
.calendar-cell.level-1 .calendar-cell-mark {
    background: #386641;
    box-shadow: none;
}
.calendar-cell.today .calendar-cell-mark {
    display: none;
}
.calendar-cell-today-tag {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(220, 38, 38, 0.92);
    color: #ffffff;
    font-size: 18rpx;
    font-weight: 700;
    padding: 3rpx 0;
    text-align: center;
    line-height: 1.2;
    letter-spacing: 2rpx;
}
.calendar-legend {
    margin-top: 18rpx;
    padding: 14rpx 6rpx 4rpx;
}
.calendar-legend-row {
    display: flex;
    align-items: center;
    justify-content: space-around;
    gap: 8rpx;
}
.calendar-legend-item {
    display: flex;
    align-items: center;
    gap: 8rpx;
}
.calendar-legend-cell {
    width: 28rpx;
    height: 28rpx;
    border-radius: 8rpx;
    background: var(--slate-100);
    border: 1rpx solid var(--slate-200);
    &.level-1 { background: #ccfbf1; border-color: #99f6e4; }
    &.level-2 { background: linear-gradient(135deg, #cfe8d3 0%, #afe2b3 100%); border-color: #386641; }
    &.level-3 { background: var(--primary-grad); border-color: transparent; }
    &.legend-today {
        background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
        border: 2rpx solid #ffffff;
        box-shadow: 0 0 0 2rpx #ea580c;
    }
}
.calendar-legend-text {
    font-size: 24rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.heatmap-foot {
    margin-top: 22rpx;
    padding-top: 18rpx;
    border-top: 1rpx dashed var(--slate-200);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12rpx;
}
.heatmap-foot-text {
    font-size: 28rpx;
    color: #334155;
    font-weight: 600;
}
.heatmap-foot-cta {
    background: var(--primary-grad);
    color: #ffffff;
    font-size: 28rpx;
    font-weight: 700;
    padding: 18rpx 30rpx;
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-glow-primary);
    min-height: 76rpx;
    display: flex;
    align-items: center;
    transition: var(--transition-smooth);
    &:active {
        transform: scale(0.96);
    }
}

/* === 弹窗表单（bottom sheet）=== */
.input-modal-mask {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(12px);
    z-index: 999;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    animation: input-modal-mask-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.input-modal {
    width: 100%;
    max-width: 750rpx;
    background: #ffffff;
    border-radius: 36rpx 36rpx 0 0;
    padding: 16rpx 0 0;
    box-shadow: 0 -12rpx 36rpx rgba(15, 23, 42, 0.12);
    max-height: 88vh;
    display: flex;
    flex-direction: column;
    animation: input-modal-rise 0.46s cubic-bezier(0.22, 1, 0.36, 1) both;
    position: relative;
    z-index: 1000;
    will-change: transform, opacity;
    overflow: hidden;
}
@keyframes input-modal-mask-in {
    from { opacity: 0; }
    to { opacity: 1; }
}
@keyframes input-modal-rise {
    from { transform: translate3d(0, 100%, 0); opacity: 0.96; }
    to { transform: translate3d(0, 0, 0); opacity: 1; }
}
@keyframes input-modal-content-in {
    from { opacity: 0; transform: translate3d(0, 24rpx, 0); }
    to { opacity: 1; transform: translate3d(0, 0, 0); }
}
.input-modal-grip {
    width: 80rpx;
    height: 6rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-200);
    margin: 0 auto 8rpx;
}
.input-modal-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12rpx 32rpx 16rpx;
    border-bottom: 1rpx solid var(--slate-100);
}
.input-modal-title-wrap {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
}
.input-modal-title {
    font-size: 40rpx;
    font-weight: 800;
    color: var(--slate-900);
    letter-spacing: 0.5rpx;
}
.input-modal-sub {
    font-size: 26rpx;
    color: var(--slate-600);
    margin-top: 4rpx;
}
.input-modal-close {
    width: 80rpx;
    height: 80rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-pill);
    background: var(--slate-100);
    transition: var(--transition-smooth);
    &:active {
        background: var(--slate-200);
    }
}
.input-modal-close-icon {
    font-size: 42rpx;
    color: var(--slate-600);
    line-height: 1;
    font-weight: 700;
}
.input-modal-loading {
    min-height: 520rpx;
    padding: 120rpx 0;
    text-align: center;
    color: var(--slate-600);
    font-size: 30rpx;
    display: flex;
    align-items: center;
    justify-content: center;
}
.input-modal-body {
    flex: none;
    height: 50vh;
    max-height: 50vh;
    padding: 20rpx 32rpx 28rpx;
    box-sizing: border-box;
    width: 100%;
}
.input-modal-body-ready {
    animation: input-modal-content-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.input-modal-tip {
    background: var(--primary-light);
    border: 1rpx dashed #99f6e4;
    border-radius: 14rpx;
    padding: 18rpx 20rpx;
    font-size: 26rpx;
    color: #0f766e;
    line-height: 1.5;
    margin-bottom: 22rpx;
}

/* === 语音录入区（血糖弹窗） === */
.voice-entry-section {
    margin-bottom: 24rpx;
    padding: 22rpx 20rpx 20rpx;
    border-radius: 22rpx;
    background: #f8fafc;
    border: 1rpx solid #e2e8f0;
}
.voice-entry-head {
    display: flex;
    flex-direction: column;
    gap: 6rpx;
    margin-bottom: 18rpx;
}
.voice-entry-title {
    font-size: 28rpx;
    font-weight: 700;
    color: #334155;
    letter-spacing: 1rpx;
}
.voice-entry-desc {
    font-size: 24rpx;
    color: #64748b;
    line-height: 1.45;
}
.voice-entry-or {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin: 16rpx 0;
}
.voice-entry-or-line {
    flex: 1;
    height: 1rpx;
    background: #e2e8f0;
}
.voice-entry-or-text {
    font-size: 22rpx;
    color: #94a3b8;
    flex-shrink: 0;
}

/* === 语音录入条 === */
.voice-input-bar {
    display: flex;
    align-items: center;
    gap: 18rpx;
    padding: 20rpx 24rpx;
    margin-bottom: 22rpx;
    border-radius: 18rpx;
    background: linear-gradient(135deg, #ecfdf5, #d1fae5);
    border: 2rpx solid #a7f3d0;
    transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease, opacity 0.18s ease;
    position: relative;
    &:active {
        transform: scale(0.985);
    }
    &.active {
        background: linear-gradient(135deg, #006c49, #0d9488);
        border-color: #006c49;
        box-shadow: 0 10rpx 28rpx rgba(0, 108, 73, 0.3);
        .voice-input-icon {
            background: rgba(255, 255, 255, 0.22);
        }
        .voice-input-title { color: #ffffff; }
        .voice-input-hint { color: rgba(255, 255, 255, 0.88); }
        .voice-input-badge { background: rgba(255, 255, 255, 0.22); color: #ffffff; }
        .voice-input-accent { background: rgba(255, 255, 255, 0.35); }
    }
    &.muted {
        opacity: 0.55;
    }
}
.voice-entry-section .voice-input-bar {
    margin-bottom: 0;
    overflow: hidden;
}
.voice-input-bar--primary {
    min-height: 112rpx;
    padding: 36rpx 20rpx 22rpx 28rpx;
    box-shadow: 0 6rpx 20rpx rgba(0, 108, 73, 0.08);
    align-items: center;
    .voice-input-icon,
    .voice-input-text,
    .voice-input-hold-pill {
        position: relative;
        z-index: 2;
    }
}
.voice-input-accent {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 6rpx;
    background: #006c49;
    z-index: 1;
}
.voice-input-badge {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 2;
    padding: 6rpx 16rpx;
    border-radius: 0 16rpx 0 14rpx;
    background: #006c49;
    color: #ffffff;
    font-size: 20rpx;
    font-weight: 700;
    letter-spacing: 1rpx;
    line-height: 1.2;
}
.voice-input-hold-pill {
    flex-shrink: 0;
    padding: 14rpx 22rpx;
    border-radius: 999rpx;
    background: #ffffff;
    border: 2rpx solid #a7f3d0;
    box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.08);
    text {
        font-size: 24rpx;
        font-weight: 700;
        color: #006c49;
        white-space: nowrap;
    }
}
.voice-input-icon {
    width: 72rpx;
    height: 72rpx;
    border-radius: 50%;
    background: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.12);
    overflow: visible;
}
.voice-input-text {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
    flex: 1;
    min-width: 0;
}
.voice-input-title {
    font-size: 30rpx;
    font-weight: 700;
    color: #134e4a;
    letter-spacing: 0.5rpx;
}
.voice-input-hint {
    font-size: 24rpx;
    color: #475569;
    line-height: 1.4;
}
@keyframes voice-icon-pulse {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.12); }
}

/* === 语音录音浮层 === */
.voice-rec-overlay {
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 12000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(15, 23, 42, 0.45);
}
.voice-rec-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 18rpx;
    padding: 56rpx 64rpx;
    border-radius: 28rpx;
    background: rgba(0, 108, 73, 0.94);
    box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.voice-rec-icon {
    width: 132rpx;
    height: 132rpx;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.18);
    display: flex;
    align-items: center;
    justify-content: center;
    &.active {
        animation: voice-rec-wave 1.1s ease-in-out infinite;
    }
}
.voice-rec-title {
    font-size: 34rpx;
    font-weight: 700;
    color: #ffffff;
    letter-spacing: 2rpx;
}
.voice-rec-sub {
    font-size: 26rpx;
    color: rgba(255, 255, 255, 0.8);
}
@keyframes voice-rec-wave {
    0%, 100% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
    }
    50% {
        transform: scale(1.06);
        box-shadow: 0 0 0 24rpx rgba(255, 255, 255, 0);
    }
}

/* === 语音一问一答面板 === */
.voice-dialog {
    margin-bottom: 22rpx;
    border-radius: 20rpx;
    background: linear-gradient(135deg, #f0fdf4, #ecfeff);
    border: 2rpx solid #bbf7d0;
    overflow: hidden;
    &.active {
        border-color: #006c49;
        box-shadow: 0 10rpx 28rpx rgba(0, 108, 73, 0.16);
    }
}
.voice-entry-section .voice-dialog {
    margin-bottom: 0;
}
.voice-dialog--secondary {
    background: #ffffff;
    border: 2rpx solid #e2e8f0;
    &.active {
        background: linear-gradient(180deg, #f0fdf4 0%, #ffffff 100%);
        border-color: #006c49;
    }
    .voice-dialog-orb {
        background: #ecfdf5;
        border: 2rpx solid #a7f3d0;
        &.speaking {
            background: #0d9488;
            border-color: #0d9488;
        }
        &.listening {
            background: #006c49;
            border-color: #006c49;
        }
    }
    .voice-dialog-start {
        background: #ffffff;
        color: #006c49;
        border: 2rpx solid #006c49;
    }
}
.voice-dialog-title-row {
    display: flex;
    align-items: center;
    gap: 10rpx;
    flex-wrap: wrap;
}
.voice-dialog-tag {
    padding: 2rpx 12rpx;
    border-radius: 999rpx;
    font-size: 20rpx;
    font-weight: 600;
    color: #64748b;
    background: #f1f5f9;
    border: 1rpx solid #e2e8f0;
}
.voice-dialog-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx 24rpx;
    gap: 16rpx;
}
.voice-dialog-title-wrap {
    display: flex;
    align-items: center;
    gap: 16rpx;
    flex: 1;
    min-width: 0;
}
.voice-dialog-orb {
    width: 64rpx;
    height: 64rpx;
    border-radius: 50%;
    background: #0d9488;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    &.speaking { animation: voice-icon-pulse 1.3s ease-in-out infinite; }
    &.listening {
        background: #006c49;
        animation: voice-rec-wave 1.1s ease-in-out infinite;
    }
}
.voice-dialog-title-text {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
    min-width: 0;
}
.voice-dialog-title {
    font-size: 30rpx;
    font-weight: 700;
    color: #134e4a;
    letter-spacing: 0.5rpx;
}
.voice-dialog-tip {
    font-size: 23rpx;
    color: #64748b;
    line-height: 1.45;
}
.voice-dialog-start,
.voice-dialog-stop {
    flex-shrink: 0;
    min-height: 64rpx;
    padding: 0 28rpx;
    border-radius: 999rpx;
    font-size: 26rpx;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    &:active { transform: scale(0.97); }
}
.voice-dialog-stop {
    background: #f1f5f9;
    color: #475569;
    border: 2rpx solid #e2e8f0;
}
.voice-dialog-body {
    padding: 0 24rpx 22rpx;
    display: flex;
    flex-direction: column;
    gap: 14rpx;
    border-top: 1rpx solid #e2e8f0;
    margin-top: 4rpx;
    padding-top: 18rpx;
}
.voice-dialog-hold-pad {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 12rpx;
    min-height: 168rpx;
    padding: 28rpx 24rpx;
    border-radius: 18rpx;
    background: linear-gradient(135deg, #ecfdf5, #d1fae5);
    border: 2rpx solid #a7f3d0;
    transition: background 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
    &:active,
    &.active {
        background: linear-gradient(135deg, #006c49, #0d9488);
        border-color: #006c49;
        box-shadow: 0 10rpx 28rpx rgba(0, 108, 73, 0.24);
        .voice-dialog-hold-title,
        .voice-dialog-hold-heard { color: #ffffff; }
        .voice-dialog-hold-icon { background: rgba(255, 255, 255, 0.22); }
    }
}
.voice-dialog-hold-icon {
    width: 80rpx;
    height: 80rpx;
    border-radius: 50%;
    background: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4rpx 12rpx rgba(0, 108, 73, 0.12);
}
.voice-dialog-hold-title {
    font-size: 28rpx;
    font-weight: 700;
    color: #134e4a;
    letter-spacing: 0.5rpx;
}
.voice-dialog-hold-heard {
    font-size: 24rpx;
    color: #475569;
    max-width: 100%;
    text-align: center;
    line-height: 1.4;
}
.voice-dialog-bubble {
    max-width: 86%;
    padding: 16rpx 20rpx;
    border-radius: 18rpx;
    font-size: 28rpx;
    line-height: 1.5;
    &.bot {
        align-self: flex-start;
        background: #ffffff;
        color: #1f2937;
        border: 2rpx solid #d1fae5;
        border-bottom-left-radius: 6rpx;
    }
    &.user {
        align-self: flex-end;
        background: #006c49;
        color: #ffffff;
        border-bottom-right-radius: 6rpx;
    }
}
.voice-dialog-status {
    display: flex;
    align-items: center;
    gap: 14rpx;
    min-height: 36rpx;
}
.voice-dialog-status-text {
    font-size: 24rpx;
    color: #475569;
}
.voice-dialog-wave {
    display: flex;
    align-items: flex-end;
    gap: 6rpx;
    height: 32rpx;
}
.voice-dialog-wave-bar {
    width: 6rpx;
    border-radius: 4rpx;
    background: #006c49;
    animation: voice-dialog-wave-jump 0.9s ease-in-out infinite;
    &:nth-child(1) { height: 14rpx; animation-delay: 0s; }
    &:nth-child(2) { height: 26rpx; animation-delay: 0.15s; }
    &:nth-child(3) { height: 20rpx; animation-delay: 0.3s; }
    &:nth-child(4) { height: 30rpx; animation-delay: 0.45s; }
}
@keyframes voice-dialog-wave-jump {
    0%, 100% { transform: scaleY(0.5); }
    50% { transform: scaleY(1); }
}
.voice-dialog-actions {
    display: flex;
    gap: 14rpx;
    margin-top: 4rpx;
}
.voice-dialog-btn {
    flex: 1;
    text-align: center;
    padding: 16rpx 0;
    border-radius: 14rpx;
    font-size: 26rpx;
    font-weight: 600;
    color: #006c49;
    background: rgba(0, 108, 73, 0.08);
    &:active { transform: scale(0.98); }
    &.primary {
        color: #ffffff;
        background: #006c49;
    }
}
.input-section-title {
    font-size: 28rpx;
    font-weight: 700;
    color: #334155;
    letter-spacing: 1rpx;
    margin: 18rpx 0 14rpx;
}
.intensity-row {
    display: flex;
    gap: 12rpx;
    margin-bottom: 16rpx;
}
.intensity-chip {
    flex: 1;
    text-align: center;
    padding: 16rpx 8rpx;
    border-radius: 14rpx;
    background: var(--slate-50);
    border: 2rpx solid var(--slate-200);
    font-size: 26rpx;
    color: var(--slate-600);
    transition: var(--transition-smooth);
    &.active {
        background: var(--success-light);
        border-color: var(--success);
        color: #047857;
        font-weight: 700;
    }
}
.input-field {
    display: flex;
    align-items: center;
    gap: 18rpx;
    padding: 14rpx 0;
    border-bottom: 1rpx solid var(--slate-100);
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
    min-height: 88rpx;
}
.input-field-label {
    display: flex;
    align-items: center;
    gap: 10rpx;
    flex: 0 0 auto;
    width: 180rpx;
    font-size: 30rpx;
    color: var(--slate-900);
    font-weight: 700;
}
.input-field-dot {
    width: 14rpx;
    height: 14rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-400);
    flex-shrink: 0;
}
.input-field-dot-fasting { background: var(--primary); }
.input-field-dot-postprandial { background: var(--warning); }
.input-field-dot-other { background: var(--violet); }

.input-field-input {
    flex: 1 1 0;
    min-width: 0;
    background: var(--slate-50);
    border-radius: 16rpx;
    padding: 18rpx 20rpx;
    box-sizing: border-box;
    overflow: hidden;
    min-height: 76rpx;
    display: flex;
    align-items: center;
    border: 2rpx solid transparent;
    transition: var(--transition-smooth);
    &:focus-within {
        border-color: var(--primary);
        background: #ffffff;
        box-shadow: 0 0 0 4rpx var(--primary-glow);
    }
    input {
        width: 100%;
        min-width: 0;
        font-size: 34rpx;
        color: var(--slate-900);
        text-align: right;
        box-sizing: border-box;
        font-weight: 600;
    }
}
.input-placeholder {
    color: var(--slate-400);
    font-size: 28rpx;
}
.input-field-row {
    display: flex;
    gap: 18rpx;
    min-width: 0;
    width: 100%;
    box-sizing: border-box;
}
.input-field-half {
    flex: 1 1 0;
    min-width: 0;
    border-bottom: 1rpx solid var(--slate-100);
}
.input-field-half .input-field-label {
    width: 80rpx;
    flex: 0 0 auto;
}
.input-field-row .input-field-input {
    padding: 16rpx 16rpx;
    input {
        font-size: 32rpx;
    }
}
.input-field-textarea {
    flex-direction: column;
    align-items: stretch;
    border-bottom: none;
    padding: 10rpx 0 0;
    textarea {
        width: 100%;
        min-height: 140rpx;
        background: var(--slate-50);
        border-radius: 14rpx;
        padding: 20rpx 22rpx;
        font-size: 30rpx;
        color: var(--slate-900);
        box-sizing: border-box;
        line-height: 1.6;
        border: 2rpx solid transparent;
        transition: var(--transition-smooth);
        &:focus {
            border-color: var(--primary);
            background: #ffffff;
            box-shadow: 0 0 0 4rpx var(--primary-glow);
        }
    }
}
.input-modal-actions {
    display: flex;
    gap: 18rpx;
    flex-shrink: 0;
    padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
    border-top: 1rpx solid #eef6ee;
    background: #ffffff;
}
.input-modal-btn {
    flex: 1;
    height: 100rpx;
    border-radius: 20rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32rpx;
    font-weight: 700;
    letter-spacing: 2rpx;
    transition: opacity 0.2s ease, transform 0.2s ease;
    &.primary {
        background: #006c49;
        color: #ffffff;
        box-shadow: 0 8rpx 24rpx rgba(0, 108, 73, 0.25);
        &:active {
            transform: translateY(1rpx);
            opacity: 0.92;
        }
    }
    &.danger {
        flex: 0 0 180rpx;
        background: var(--danger-light);
        color: var(--danger);
        border: 1rpx solid rgba(239, 68, 68, 0.2);
        &:active {
            background: #ffe4e6;
        }
    }
    &.disabled {
        opacity: 0.5;
        pointer-events: none;
    }
}

/* === 稳糖乐园 === */
.pg-card {
    margin: 28rpx 24rpx 0;
    background: #ffffff;
    border-radius: 32rpx;
    padding: 30rpx 28rpx;
    box-shadow: var(--shadow-premium);
    border: var(--border-premium);
    animation: card-rise 0.45s ease-out 0.08s both;
    .card-head {
        display: flex;
        justify-content: space-between;
        align-items: center;
        border-bottom: 1rpx solid var(--slate-100);
        padding-bottom: 20rpx;
    }
    .pg-points-badge {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        background: linear-gradient(135deg, #eef6ef 0%, #ccfbf1 100%);
        border-left: 6rpx solid var(--primary);
        padding: 8rpx 18rpx 8rpx 20rpx;
        border-radius: 16rpx;
        .pg-points-num {
            font-size: 38rpx;
            font-weight: 800;
            color: #386641;
            line-height: 1.1;
        }
        .pg-points-label {
            font-size: 20rpx;
            color: #0f766e;
            font-weight: 600;
        }
    }
}
.pg-content-stack {
    display: flex;
    flex-direction: column;
    gap: 28rpx;
    margin-top: 24rpx;
}
.record-quick-zone {
    display: flex;
    flex-direction: column;
    gap: 16rpx;
}
.record-quick-head {
    display: flex;
    flex-direction: column;
    gap: 4rpx;
    padding-left: 6rpx;
}
.record-quick-title {
    font-size: 28rpx;
    font-weight: 800;
    color: #334155;
}
.record-quick-hint {
    font-size: 22rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.record-quick-row {
    display: flex;
    gap: 12rpx;
}
.record-quick-chip {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8rpx;
    padding: 24rpx 12rpx;
    background: #ffffff;
    border: 1rpx solid var(--slate-200);
    border-radius: 24rpx;
    box-shadow: 0 4rpx 12rpx rgba(15, 23, 42, 0.02);
    transition: var(--transition-smooth);
    &:active {
        transform: scale(0.98);
    }
    &.pending {
        border-color: rgba(13, 148, 136, 0.2);
        background: linear-gradient(180deg, var(--primary-light) 0%, #ffffff 100%);
    }
    &.done {
        border-color: rgba(16, 185, 129, 0.2);
        background: linear-gradient(180deg, var(--success-light) 0%, #ffffff 100%);
    }
}
.record-quick-icon-wrap {
    width: 56rpx;
    height: 56rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-50);
    display: flex;
    align-items: center;
    justify-content: center;
    .record-quick-chip.pending & {
        background: #ccfbf1;
    }
    .record-quick-chip.done & {
        background: #dcfce7;
    }
}
.record-quick-label {
    font-size: 26rpx;
    font-weight: 800;
    color: var(--slate-900);
}
.record-quick-status {
    font-size: 22rpx;
    font-weight: 700;
    color: #386641;
    .record-quick-chip.done & {
        color: var(--success);
    }
}
.tree-growth-zone {
    display: flex;
    align-items: flex-start;
    gap: 20rpx;
    background: linear-gradient(145deg, #eef6ef 0%, #f8fafc 55%, #fffbeb 100%);
    border-radius: 24rpx;
    padding: 24rpx 20rpx;
    border: 2rpx solid #99f6e4;
}
.tree-graphic-wrap {
    position: relative;
    flex-shrink: 0;
    width: 152rpx;
    min-height: 200rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.tree-lv-badge {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    padding: 4rpx 14rpx;
    border-radius: 999rpx;
    background: linear-gradient(135deg, #204e2b, #386641);
    color: #ffffff;
    font-size: 20rpx;
    font-weight: 800;
    box-shadow: 0 4rpx 12rpx rgba(32, 78, 43, 0.35);
}
.tree-mood-pill {
    position: absolute;
    top: 36rpx;
    right: -8rpx;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 4rpx;
    padding: 4rpx 10rpx;
    border-radius: 999rpx;
    background: rgba(255, 255, 255, 0.92);
    border: 1rpx solid #e2e8f0;
    box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.08);
}
.tree-mood-emoji {
    font-size: 22rpx;
    line-height: 1;
}
.tree-mood-text {
    font-size: 20rpx;
    font-weight: 700;
    color: #64748b;
}
.tree-next-hint {
    display: block;
    margin-top: 8rpx;
    font-size: 22rpx;
    color: #0f766e;
    font-weight: 600;
    line-height: 1.45;
}
.tree-roadmap-scroll {
    width: 100%;
    margin-top: 14rpx;
    white-space: nowrap;
}
.tree-roadmap-row {
    display: inline-flex;
    gap: 10rpx;
    padding-bottom: 4rpx;
}
.tree-roadmap-node {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 4rpx;
    min-width: 56rpx;
    padding: 8rpx 6rpx;
    border-radius: 14rpx;
    background: #faf9f5;
    border: 1rpx solid #e2e8f0;
    opacity: 0.55;
    &.done {
        opacity: 0.85;
        background: #ecfdf5;
        border-color: #a7f3d0;
    }
    &.current {
        opacity: 1;
        background: linear-gradient(180deg, #ccfbf1, #ffffff);
        border-color: #386641;
        box-shadow: 0 4rpx 14rpx rgba(32, 78, 43, 0.25);
        transform: scale(1.06);
    }
    &.locked {
        opacity: 0.4;
    }
}
.tree-roadmap-emoji {
    font-size: 26rpx;
    line-height: 1;
}
.tree-roadmap-lv {
    font-size: 18rpx;
    font-weight: 700;
    color: #64748b;
    .tree-roadmap-node.current & {
        color: #0f766e;
    }
}
.tree-tappable:active {
    opacity: 0.92;
}
.tree-tap-hint,
.invite-tree-tap-hint {
    margin-top: 4rpx;
    font-size: 20rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.invite-tree-tappable {
    display: flex;
    flex-direction: column;
    align-items: center;
}
.float-reward {
    position: absolute;
    top: 8rpx;
    right: 0;
    z-index: 3;
    animation: float-reward-rise 1.1s ease-out forwards;
    text {
        font-size: 32rpx;
        font-weight: 800;
        color: var(--success);
        text-shadow: 0 2rpx 8rpx rgba(5, 150, 105, 0.25);
    }
}
@keyframes float-reward-rise {
    0% { opacity: 1; transform: translateY(0) scale(0.8); }
    100% { opacity: 0; transform: translateY(-56rpx) scale(1.15); }
}
.mission-complete-banner {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin-bottom: 20rpx;
    padding: 18rpx 20rpx;
    background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
    border: 2rpx solid #fde68a;
    border-radius: 20rpx;
    animation: mission-glow 2s ease-in-out infinite;
}
@keyframes mission-glow {
    0%, 100% { box-shadow: 0 0 0 rgba(251, 191, 36, 0); }
    50% { box-shadow: 0 0 20rpx rgba(251, 191, 36, 0.35); }
}
.mission-complete-title {
    display: block;
    font-size: 28rpx;
    font-weight: 800;
    color: #92400e;
}
.mission-complete-sub {
    display: block;
    margin-top: 4rpx;
    font-size: 24rpx;
    color: #b45309;
}
.badge-wall {
    margin-top: 12rpx;
}
.badge-wall-title {
    display: block;
    font-size: 32rpx;
    font-weight: 800;
    color: #475569;
    margin-bottom: 16rpx;
    padding-left: 6rpx;
}
.badge-wall-scroll {
    width: 100%;
    white-space: nowrap;
}
.badge-wall-row {
    display: inline-flex;
    gap: 14rpx;
    padding-bottom: 8rpx;
}
.badge-chip {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 10rpx;
    min-width: 140rpx;
    padding: 18rpx 20rpx;
    background: #ffffff;
    border: 1rpx solid var(--slate-200);
    border-radius: 18rpx;
    box-shadow: var(--shadow-sm);
}
.badge-chip-icon {
    width: 58rpx;
    height: 58rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-50);
    display: flex;
    align-items: center;
    justify-content: center;
}
.badge-chip-label {
    font-size: 28rpx;
    font-weight: 700;
    color: #334155;
    white-space: nowrap;
}
.tree-progress-wrap {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 10rpx;
    min-width: 0;
}
.tree-level-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12rpx;
}
.tree-level-tag {
    font-size: 30rpx;
    font-weight: 700;
    color: var(--slate-900);
    background: var(--slate-200);
    padding: 6rpx 18rpx;
    border-radius: var(--radius-pill);
}
.tree-progress-text {
    font-size: 28rpx;
    color: var(--slate-600);
    font-weight: 600;
}
.tree-progress-track {
    height: 16rpx;
    background: var(--slate-200);
    border-radius: var(--radius-pill);
    overflow: hidden;
}
.tree-progress-bar {
    height: 100%;
    background: var(--primary-grad);
    border-radius: var(--radius-pill);
    transition: width 0.4s ease;
}
.tree-water-action {
    margin-top: 6rpx;
    display: flex;
    flex-direction: column;
    gap: 8rpx;
}
.water-hint {
    font-size: 22rpx;
    color: var(--slate-600);
    font-weight: 600;
    text-align: center;
    line-height: 1.4;
}
.water-btn {
    width: 100%;
    height: 76rpx;
    background: var(--primary-grad);
    color: #ffffff;
    border-radius: var(--radius-pill);
    font-size: 28rpx;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10rpx;
    border: none;
    box-shadow: var(--shadow-glow-primary);
    transition: var(--transition-smooth);
    &:active {
        transform: scale(0.98);
        background: linear-gradient(135deg, #386641 0%, var(--primary) 100%);
    }
    &.disabled {
        opacity: 0.7;
        pointer-events: none;
    }
    &.pulse {
        animation: water-btn-pulse 1.6s ease-in-out infinite;
    }
}
@keyframes water-btn-pulse {
    0%, 100% { box-shadow: var(--shadow-glow-primary); }
    50% { box-shadow: 0 4rpx 24rpx rgba(32, 78, 43, 0.5); }
}

.today-tasks-zone {
    display: flex;
    flex-direction: column;
    gap: 14rpx;
}
.tasks-zone-title {
    font-size: 28rpx;
    font-weight: 800;
    color: #334155;
    letter-spacing: 0.5rpx;
    padding-left: 6rpx;
}
.tasks-list {
    display: flex;
    flex-direction: column;
    gap: 12rpx;
}
.task-row-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--slate-50);
    border: 1rpx solid var(--slate-200);
    border-radius: 20rpx;
    padding: 20rpx 24rpx;
    min-height: 100rpx;
    box-sizing: border-box;
    transition: var(--transition-smooth);
    &.pending {
        border-color: #99f6e4;
        background: linear-gradient(90deg, var(--primary-light) 0%, #ffffff 100%);
    }
    &.completed {
        border-color: #ccfbf1;
        background: var(--primary-light);
    }
    &.claimed {
        opacity: 0.92;
    }
    &.just-done {
        animation: task-pop 0.65s ease;
    }
}
@keyframes task-pop {
    0% { transform: scale(1); }
    40% { transform: scale(1.03); box-shadow: 0 8rpx 20rpx rgba(32, 78, 43, 0.2); }
    100% { transform: scale(1); }
}
.task-left {
    display: flex;
    align-items: center;
    gap: 16rpx;
    min-width: 0;
}
.task-status-icon {
    width: 36rpx;
    height: 36rpx;
    border-radius: var(--radius-pill);
    border: 2rpx solid var(--slate-400);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 24rpx;
    font-weight: 800;
    color: var(--slate-400);
    line-height: 1;
    flex-shrink: 0;
    &.checked {
        border-color: var(--primary);
        background: var(--primary);
        color: #ffffff;
    }
}
.task-name {
    font-size: 28rpx;
    font-weight: 700;
    color: var(--slate-900);
    .task-row-item.completed & {
        color: #0f766e;
    }
}
.task-right {
    display: flex;
    align-items: center;
    gap: 16rpx;
    flex-shrink: 0;
}
.task-points {
    font-size: 24rpx;
    font-weight: 600;
    color: var(--slate-600);
    .task-row-item.completed & {
        color: var(--primary);
    }
}
.task-action-btn {
    padding: 10rpx 22rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-200);
    color: var(--slate-600);
    font-size: 24rpx;
    font-weight: 700;
    min-height: 52rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    transition: var(--transition-smooth);
    &.pending {
        background: var(--primary);
        color: #ffffff;
        box-shadow: 0 4rpx 10rpx rgba(32, 78, 43, 0.2);
        &:active {
            transform: scale(0.96);
        }
    }
    &:not(.disabled):not(.claimed):not(.pending) {
        background: var(--primary);
        color: #ffffff;
        box-shadow: 0 4rpx 10rpx rgba(32, 78, 43, 0.2);
        &:active {
            transform: scale(0.96);
        }
    }
    &.claimed {
        background: #ccfbf1;
        color: #386641;
    }
    &.disabled {
        opacity: 0.6;
    }
}
.pg-share-row {
    margin-top: 8rpx;
}
.pg-share-btn {
    display: flex;
    align-items: center;
    gap: 16rpx;
    padding: 20rpx 22rpx;
    background: var(--primary-light);
    border: 1rpx solid #99f6e4;
    border-radius: 20rpx;
    transition: var(--transition-smooth);
    &:active {
        opacity: 0.85;
        transform: translateY(1rpx);
    }
}
.pg-share-icon-wrap {
    width: 72rpx;
    height: 72rpx;
    border-radius: 18rpx;
    background: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.pg-share-title {
    display: block;
    font-size: 34rpx;
    font-weight: 800;
    color: #0f766e;
}
.pg-share-sub {
    display: block;
    margin-top: 6rpx;
    font-size: 28rpx;
    color: var(--slate-600);
}

/* === 家人点赞及鼓励 === */
.family-like-strip {
    display: flex;
    align-items: center;
    gap: 18rpx;
    margin: 20rpx 24rpx 0;
    padding: 24rpx 28rpx;
    background: #ffffff;
    border-radius: 24rpx;
    border: 1.5rpx solid #fecdd3;
    box-shadow: 0 4rpx 16rpx rgba(225, 29, 72, 0.06);
    transition: var(--transition-smooth);
    &.has-likes {
        background: linear-gradient(135deg, #fff1f2 0%, #ffffff 100%);
    }
    &:active {
        transform: scale(0.99);
    }
}
.family-like-icon {
    font-size: 40rpx;
}
.family-like-body {
    flex: 1;
    min-width: 0;
}
.family-like-title {
    display: block;
    font-size: 34rpx;
    font-weight: 800;
    color: #9f1239;
}
.family-like-sub {
    display: block;
    margin-top: 8rpx;
    font-size: 30rpx;
    color: var(--slate-600);
    font-weight: 500;
}
.family-like-recent {
    margin-top: 10rpx;
    display: flex;
    flex-wrap: wrap;
    gap: 10rpx;
}
.family-like-recent-item {
    font-size: 26rpx;
    color: #be123c;
    background: #ffe4e6;
    padding: 6rpx 16rpx;
    border-radius: var(--radius-pill);
    font-weight: 600;
}
.family-like-arrow {
    font-size: 36rpx;
    color: var(--slate-400);
}

.encourage-strip {
    display: flex;
    align-items: flex-start;
    gap: 16rpx;
    margin: 16rpx 24rpx 0;
    padding: 24rpx 24rpx;
    background: #ecfdf5;
    border-radius: 20rpx;
    border: 1rpx solid #a7f3d0;
    transition: var(--transition-smooth);
    &:active {
        background: #d1fae5;
    }
}
.encourage-icon {
    font-size: 36rpx;
}
.encourage-text {
    display: block;
    font-size: 32rpx;
    font-weight: 700;
    color: #065f46;
    line-height: 1.5;
}
.encourage-tip {
    display: block;
    margin-top: 8rpx;
    font-size: 28rpx;
    color: var(--slate-600);
    font-weight: 500;
}

/* === 共享/邀请查看样式 === */
.invite-view-wrap {
    min-height: 100vh;
    background: linear-gradient(180deg, #eef6ef 0%, #f4f4f0 42%, #faf9f5 100%);
    padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
}
.invite-hero {
    position: relative;
    padding: 48rpx 32rpx 56rpx;
    overflow: hidden;
}
.invite-hero-bg {
    position: absolute;
    inset: 0;
    background: linear-gradient(145deg, #204e2b 0%, #386641 55%, #163d22 100%);
}
.invite-hero-content {
    position: relative;
    z-index: 1;
    color: #ffffff;
}
.invite-hero-badge {
    display: inline-flex;
    align-items: center;
    gap: 10rpx;
    padding: 10rpx 18rpx;
    background: rgba(255, 255, 255, 0.16);
    border: 1rpx solid rgba(255, 255, 255, 0.28);
    border-radius: var(--radius-pill);
    margin-bottom: 18rpx;
}
.invite-hero-badge-text {
    font-size: 24rpx;
    font-weight: 600;
    letter-spacing: 1rpx;
}
.invite-hero-title {
    display: block;
    font-size: 40rpx;
    font-weight: 800;
    line-height: 1.35;
    color: #ffffff;
}
.invite-hero-sub {
    display: block;
    margin-top: 10rpx;
    font-size: 26rpx;
    color: rgba(255, 255, 255, 0.92);
    line-height: 1.45;
}
.invite-hero-patient {
    display: inline-flex;
    align-items: center;
    gap: 8rpx;
    margin-top: 16rpx;
    padding: 8rpx 16rpx;
    background: rgba(255, 255, 255, 0.12);
    border-radius: 12rpx;
    font-size: 26rpx;
    font-weight: 600;
}
.invite-body {
    margin-top: -28rpx;
    padding: 0 24rpx 24rpx;
    position: relative;
    z-index: 2;
}
.invite-card {
    background: #ffffff;
    border-radius: var(--radius-card);
    padding: 24rpx;
    margin-bottom: 20rpx;
    box-shadow: 0 8rpx 28rpx rgba(8, 145, 178, 0.08);
    border: 1rpx solid rgba(8, 145, 178, 0.08);
}
.invite-card-head,
.invite-section-head {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin-bottom: 16rpx;
}
.invite-card-icon-wrap {
    width: 72rpx;
    height: 72rpx;
    border-radius: 18rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.invite-card-icon-wrap--cyan { background: #eef6ef; }
.invite-card-icon-wrap--teal { background: #eef6ef; }
.invite-card-icon-wrap--green { background: #ecfdf5; }
.invite-card-head-text,
.invite-section-head-text {
    flex: 1;
    min-width: 0;
}
.invite-card-label {
    display: block;
    font-size: 28rpx;
    font-weight: 700;
    color: #1b1c1a;
}
.invite-card-hint {
    display: block;
    margin-top: 4rpx;
    font-size: 24rpx;
    color: var(--slate-600);
}
.invite-code-value {
    display: block;
    text-align: center;
    font-size: 44rpx;
    font-weight: 800;
    letter-spacing: 6rpx;
    color: #204e2b;
    padding: 20rpx 12rpx;
    background: linear-gradient(180deg, #eef6ef, #eef6ef);
    border-radius: 16rpx;
    border: 2rpx dashed rgba(8, 145, 178, 0.25);
}
.invite-summary-top {
    display: flex;
    align-items: center;
    gap: 20rpx;
    margin-bottom: 20rpx;
}
.invite-tree-panel {
    flex-shrink: 0;
}
.invite-tree-meta {
    flex: 1;
    min-width: 0;
}
.invite-tree-title {
    display: block;
    font-size: 30rpx;
    font-weight: 800;
    color: #1b1c1a;
}
.invite-tree-desc {
    display: block;
    margin-top: 6rpx;
    font-size: 26rpx;
    color: var(--slate-600);
    line-height: 1.4;
}
.invite-tree-week {
    display: block;
    margin-top: 8rpx;
    font-size: 22rpx;
    color: var(--slate-400);
}
.invite-stat-grid {
    display: flex;
    gap: 12rpx;
}
.invite-stat-item {
    flex: 1;
    text-align: center;
    padding: 18rpx 10rpx;
    background: var(--slate-50);
    border-radius: 18rpx;
    border: 1rpx solid var(--slate-200);
}
.invite-stat-icon-wrap {
    width: 48rpx;
    height: 48rpx;
    margin: 0 auto 8rpx;
    border-radius: var(--radius-pill);
    background: #ffffff;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2rpx 8rpx rgba(15, 23, 42, 0.06);
}
.invite-stat-num {
    display: block;
    font-size: 38rpx;
    font-weight: 800;
    color: #1b1c1a;
    line-height: 1.1;
}
.invite-stat-label {
    display: block;
    font-size: 22rpx;
    color: var(--slate-600);
    margin-top: 6rpx;
}
.invite-quote-box {
    margin-top: 16rpx;
    padding: 18rpx 20rpx;
    background: #eef6ef;
    border-radius: 16rpx;
    border-left: 6rpx solid #204e2b;
}
.invite-quote {
    font-size: 26rpx;
    color: #1b1c1a;
    line-height: 1.55;
}
.invite-section-title {
    display: block;
    font-size: 30rpx;
    font-weight: 800;
    color: #1b1c1a;
}
.invite-section-sub {
    display: block;
    margin-top: 4rpx;
    font-size: 24rpx;
    color: var(--slate-600);
}
.invite-records-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12rpx;
    padding: 36rpx 24rpx;
    text-align: center;
    font-size: 26rpx;
    color: var(--slate-600);
    background: var(--slate-50);
    border-radius: 16rpx;
}
.invite-timeline {
    display: flex;
    flex-direction: column;
    gap: 0;
}
.invite-day-row {
    display: flex;
    gap: 16rpx;
    &.is-empty .invite-day-panel {
        opacity: 0.85;
    }
}
.invite-day-rail {
    width: 28rpx;
    display: flex;
    flex-direction: column;
    align-items: center;
    flex-shrink: 0;
    padding-top: 8rpx;
}
.invite-day-dot {
    width: 16rpx;
    height: 16rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-200);
    border: 3rpx solid #ffffff;
    box-shadow: 0 0 0 2rpx #cbd5e1;
    &.filled {
        background: #204e2b;
        box-shadow: 0 0 0 2rpx #b8dcc0;
    }
}
.invite-day-line {
    flex: 1;
    width: 2rpx;
    min-height: 24rpx;
    margin: 6rpx 0;
    background: linear-gradient(180deg, #cbd5e1, #e2e8f0);
}
.invite-day-panel {
    flex: 1;
    min-width: 0;
    padding: 0 0 20rpx;
}
.invite-day-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 12rpx;
}
.invite-day-date {
    font-size: 30rpx;
    font-weight: 800;
    color: var(--slate-900);
}
.invite-day-week {
    font-size: 24rpx;
    color: var(--slate-600);
}
.invite-metric-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 10rpx;
}
.invite-metric-chip {
    flex: 1;
    min-width: calc(50% - 6rpx);
    padding: 14rpx 16rpx;
    background: var(--slate-50);
    border-radius: 14rpx;
    border: 1rpx solid var(--slate-200);
}
.invite-metric-label {
    display: block;
    font-size: 22rpx;
    font-weight: 600;
    color: var(--slate-600);
    margin-bottom: 6rpx;
}
.invite-metric-value-row {
    display: flex;
    align-items: baseline;
    gap: 6rpx;
    flex-wrap: wrap;
}
.invite-metric-value {
    font-size: 34rpx;
    font-weight: 800;
    color: #204e2b;
    line-height: 1.2;
    &.is-high {
        color: var(--danger);
    }
}
.invite-metric-unit {
    font-size: 22rpx;
    color: var(--slate-600);
}
.invite-metric-flag {
    display: inline-flex;
    align-items: center;
    gap: 4rpx;
    margin-top: 8rpx;
    padding: 4rpx 10rpx;
    font-size: 20rpx;
    font-weight: 600;
    color: var(--danger);
    background: var(--danger-light);
    border-radius: 8rpx;
}
.invite-day-empty {
    display: flex;
    align-items: center;
    gap: 8rpx;
    padding: 14rpx 16rpx;
    font-size: 26rpx;
    color: var(--slate-400);
    background: var(--slate-50);
    border-radius: 14rpx;
}
.invite-nickname-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12rpx;
    margin-bottom: 20rpx;
}
.invite-nickname-chip {
    min-height: 72rpx;
    padding: 14rpx 28rpx;
    border-radius: var(--radius-pill);
    background: var(--slate-50);
    border: 2rpx solid var(--slate-200);
    font-size: 28rpx;
    color: var(--slate-600);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: var(--transition-smooth);
    &:active {
        opacity: 0.88;
    }
    &.active {
        background: var(--success);
        border-color: var(--success);
        color: #ffffff;
        font-weight: 700;
    }
}
.invite-like-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12rpx;
    min-height: 96rpx;
    padding: 24rpx 32rpx;
    background: var(--success-grad);
    color: #ffffff;
    border-radius: var(--radius-pill);
    font-size: 30rpx;
    font-weight: 700;
    box-shadow: var(--shadow-glow-success);
    transition: var(--transition-smooth);
    &:active {
        transform: scale(0.98);
        opacity: 0.92;
    }
    &.done {
        background: var(--success-light);
        color: var(--success);
        border: 2rpx solid #a7f3d0;
        box-shadow: none;
    }
    &.loading { opacity: 0.72; }
    &.burst { animation: invite-heart-burst 0.85s ease; }
}
@keyframes invite-heart-burst {
    0% { transform: scale(1); }
    30% { transform: scale(1.06); box-shadow: 0 12rpx 32rpx rgba(5, 150, 105, 0.4); }
    100% { transform: scale(1); }
}
.invite-footer {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    gap: 10rpx;
    padding: 8rpx 12rpx 0;
}
.invite-disclaimer {
    flex: 1;
    font-size: 22rpx;
    color: var(--slate-600);
    line-height: 1.5;
    text-align: left;
}
.invite-skeleton-wrap {
    padding: 0 24rpx;
    margin-top: -28rpx;
}
.invite-skeleton-card {
    height: 160rpx;
    margin-bottom: 20rpx;
    border-radius: var(--radius-card);
    background: linear-gradient(90deg, var(--slate-200) 25%, var(--slate-100) 50%, var(--slate-200) 75%);
    background-size: 200% 100%;
    animation: invite-shimmer 1.2s ease-in-out infinite;
}
.invite-skeleton-short { height: 120rpx; }
.invite-skeleton-tall { height: 320rpx; }
@keyframes invite-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}
.invite-error-card {
    margin: 32rpx 24rpx;
    padding: 48rpx 32rpx;
    background: #ffffff;
    border-radius: var(--radius-card);
    text-align: center;
    box-shadow: var(--shadow-premium);
}
.invite-error-icon-wrap {
    width: 96rpx;
    height: 96rpx;
    margin: 0 auto 20rpx;
    border-radius: var(--radius-pill);
    background: var(--danger-light);
    display: flex;
    align-items: center;
    justify-content: center;
}
.invite-error-title {
    display: block;
    font-size: 32rpx;
    font-weight: 800;
    color: #1b1c1a;
    margin-bottom: 10rpx;
}
.invite-error-text {
    font-size: 26rpx;
    color: var(--slate-600);
    line-height: 1.5;
}

/* === 战报弹层 === */
.report-mask {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: rgba(15, 23, 42, 0.45);
    backdrop-filter: blur(8px);
    display: flex;
    align-items: flex-end;
}
.report-panel {
    width: 100%;
    max-height: 88vh;
    background: #ffffff;
    border-radius: 28rpx 28rpx 0 0;
    padding: 28rpx 28rpx calc(28rpx + env(safe-area-inset-bottom));
}
.report-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20rpx;
}
.report-panel-title {
    font-size: 34rpx;
    font-weight: 800;
    color: var(--slate-900);
}
.report-panel-close {
    font-size: 44rpx;
    color: var(--slate-400);
    padding: 0 8rpx;
}
.report-poster {
    padding: 24rpx;
    background: linear-gradient(180deg, var(--primary-light) 0%, #ffffff 100%);
    border-radius: var(--radius-card);
    border: 1rpx solid #ccfbf1;
}
.report-poster-top {
    display: flex;
    align-items: center;
    gap: 16rpx;
    margin-bottom: 12rpx;
}
.report-poster-name {
    font-size: 32rpx;
    font-weight: 700;
    color: var(--slate-900);
}
.report-poster-week {
    display: block;
    font-size: 24rpx;
    color: var(--slate-600);
    margin-bottom: 16rpx;
}
.report-stat-grid {
    display: flex;
    gap: 12rpx;
    margin-bottom: 16rpx;
}
.report-stat-item {
    flex: 1;
    text-align: center;
    padding: 14rpx 8rpx;
    background: #ffffff;
    border-radius: 16rpx;
    border: 1rpx solid var(--slate-100);
}
.report-stat-num {
    display: block;
    font-size: 34rpx;
    font-weight: 800;
    color: var(--success);
}
.report-stat-label {
    display: block;
    font-size: 22rpx;
    color: var(--slate-600);
}
.report-poster-quote {
    display: block;
    font-size: 26rpx;
    color: #334155;
    line-height: 1.5;
}
.report-poster-disclaimer {
    display: block;
    margin-top: 12rpx;
    font-size: 22rpx;
    color: var(--slate-400);
}
.report-actions {
    margin-top: 24rpx;
    display: flex;
    flex-direction: column;
    gap: 12rpx;
}
.report-share-btn {
    height: 88rpx;
    line-height: 88rpx;
    text-align: center;
    background: var(--primary-grad);
    color: #ffffff;
    font-size: 30rpx;
    font-weight: 700;
    border-radius: var(--radius-pill);
    border: none;
    box-shadow: var(--shadow-glow-primary);
}
.report-secondary-btn {
    height: 72rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--slate-600);
    font-size: 28rpx;
}

.daily-page.is-invite-viewer {
    background: var(--primary-light, #eef6ef);
    padding-bottom: 0;
}

@import '../styles/stitch-vitalmint-theme.scss';
@import '../styles/more-stitch.scss';
@import '../styles/daily-theme.scss';

.input-modal-ai-tip {
    margin-bottom: 20rpx;
    padding: 20rpx;
    border-radius: 16rpx;
    background: rgba(32, 78, 43, 0.04);
    border: 1rpx solid rgba(32, 78, 43, 0.12);
}
.input-modal-ai-tip-label {
    display: block;
    font-size: 24rpx;
    font-weight: 700;
    color: var(--primary);
    margin-bottom: 8rpx;
}
.input-modal-ai-tip-text {
    display: block;
    font-size: 24rpx;
    line-height: 1.5;
    color: var(--primary-container);
}
.input-modal-ai-tip-btn {
    display: inline-flex;
    margin-top: 12rpx;
    padding: 8rpx 20rpx;
    border-radius: 999rpx;
    border: 2rpx solid var(--primary);
    color: var(--primary);
    font-size: 24rpx;
    font-weight: 600;
}
</style>
