<template>
    <view class="weekly-page stitch-vitalmint">
        <view v-if="authChecking" class="page-gate-loading">
            <text class="page-gate-loading-text">正在登录…</text>
        </view>

        <block v-if="!authChecking">
        <!-- TopAppBar（code.html） -->
        <view class="st-header" :style="headerSafeStyle">
            <view class="st-header-left" :style="headerLeftStyle">
                <view class="st-avatar">
                    <TongjiIcon name="person" size="md" color="#006c49" />
                </view>
                <view class="st-header-text">
                    <text class="st-greet">{{ timeGreetingMeta.text }}，{{ displayPatientName }}</text>
                    <text class="st-status">{{ todayChecked ? '今日血糖已记录' : '今日还没记血糖' }}</text>
                </view>
            </view>
            <view
                class="st-voice-round"
                :class="{ disabled: !ttsEnabled, speaking: ttsSpeaking }"
                :style="voiceBtnStyle"
                @click="ttsSpeaking ? ttsStop() : speakLatestSummary()"
            >
                <TongjiIcon :name="ttsSpeaking ? 'pause' : 'volume'" size="md" color="#006c49" />
            </view>
        </view>

        <scroll-view
            v-if="cards.length > 1"
            scroll-x
            class="st-card-scroll"
            :show-scrollbar="false"
        >
            <view class="st-card-row">
                <view
                    v-for="card in cards"
                    :key="card.id"
                    class="st-card-chip"
                    :class="{ active: Number(card.id) === Number(diagnosisId) }"
                    @click="onSelectCard(card)"
                >
                    <text>{{ card.patient_name || '就诊卡' }}</text>
                </view>
            </view>
        </scroll-view>

       

        <view class="st-main">
            <!-- 快捷建档失败提示（toast 字数有限，完整文案展示在页面） -->
            <view v-if="dailyContextError && !diagnosisId" class="st-daily-context-error" role="alert">
                <text class="st-daily-context-error-text">{{ dailyContextError }}</text>
            </view>

            <!-- 2. 血糖趋势（无就诊卡或累计录入不足 7 天显示示例曲线） -->
            <view class="st-chart-card">
                <view class="st-chart-head">
                    <view style="padding: 0 20rpx;">
                        <text class="st-chart-title">血糖趋势</text>
                        <text class="st-chart-sub">{{ rangeText }} · mmol/L</text>
                       
                    </view>
                    <view class="st-chart-legend">
                        <view
                            class="st-legend-item"
                            :class="{ inactive: !chartSeriesVisible.fasting }"
                            @click="toggleChartSeries('fasting')"
                        >
                            <view class="st-dot st-dot-fasting" />
                            <text>空腹</text>
                        </view>
                        <view
                            class="st-legend-item"
                            :class="{ inactive: !chartSeriesVisible.postprandial }"
                            @click="toggleChartSeries('postprandial')"
                        >
                            <view class="st-dot st-dot-post" />
                            <text>餐后</text>
                        </view>
                    </view>
                </view>
                <view class="st-chart-area" @click="onChartTap">
                    <canvas
                        v-if="!recordOverlayOpen"
                        id="bloodWaveChart"
                        type="2d"
                        class="st-chart-canvas"
                        @click.stop="onChartTap"
                    />
                    <view v-if="recordOverlayOpen" class="st-chart-placeholder">
                        <text>图表已暂停显示</text>
                    </view>
                    <view v-if="!chartHasDisplayData && !loading && !recordOverlayOpen" class="st-chart-empty">
                        <text>当前时间范围暂无血糖数据</text>
                    </view>
                </view>
                <view v-if="chartUseMockData" class="st-chart-foot">
                    <text class="st-chart-mock-hint">示例数据，累计记录满 7 天后显示您的真实趋势</text>
                </view>
                <view class="st-game-entry st-game-entry--in-card" @click="goGamePage" style="margin: 0 20rpx 0 20rpx;">
                    <view class="st-game-entry-icon">
                        <TongjiIcon name="activity" size="sm" color="#006c49" />
                    </view>
                    <view class="st-game-entry-body">
                        <text class="st-game-entry-title">识糖小课堂</text>
                        <text class="st-game-entry-sub">认一认：哪些食物含糖高</text>
                    </view>
                    <TongjiIcon name="chevron-right" size="sm" color="#64748b" />
                </view>
            </view>
            <view class="st-training-section">
                <dev-training-entry inline tone="weekly" />
            </view>
            <!-- 3. AI 饮食建议 -->
            <view v-if="diagnosisId" class="st-ai-section">
                <view class="st-ai-deco" aria-hidden="true" />
                <view class="st-ai-head">
                    <view class="st-ai-head-left">
                        <view class="st-ai-icon" :class="{ thinking: dietAiLoading }">
                            <TongjiIcon name="sparkles" size="sm" color="#ffffff" />
                        </view>
                        <text class="st-ai-title">AI 饮食建议</text>
                    </view>
                    <view
                        class="st-ai-refresh"
                        :class="{ disabled: dietAiLoading }"
                        @click="refreshDietAiRecommend"
                    >
                        <TongjiIcon name="refresh" size="sm" color="#006c49" />
                        <text>{{ dietAiLoading ? '生成中' : '换一换' }}</text>
                    </view>
                </view>
                <text class="st-ai-sub">读您近 7 天 · 30 天血糖，定制早餐（含驼奶粉）、喝的与三餐</text>

                <view v-if="dietAiLoading && !hasDietAiMealContent && !dietAiStreaming && !dietAiReplacing" class="st-skeleton">
                    <view v-for="n in 2" :key="'sk-' + n" class="st-skeleton-line" />
                    <text class="st-skeleton-hint">正在生成今日饮食建议…</text>
                </view>
                <view v-else class="st-meal-list">
                    <view class="st-meal-glass">
                        <view class="st-meal-head">
                            <TongjiIcon name="sun" size="sm" color="#fb923c" />
                            <text class="st-meal-label">早餐</text>
                        </view>
                        <text class="st-meal-text">
                            {{ dietAiRecommend.breakfast || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'breakfast'" class="ai-stream-cursor">▍</text>
                        </text>
                    </view>
                    <view
                        v-if="dietAiRecommend.drinks || (dietAiLoading && dietAiStreamField === 'drinks')"
                        class="st-meal-glass st-meal-glass--drinks"
                    >
                        <view class="st-meal-head">
                            <TongjiIcon name="droplet" size="sm" color="#0ea5e9" />
                            <text class="st-meal-label">喝的</text>
                        </view>
                        <text class="st-meal-text">
                            {{ dietAiRecommend.drinks || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'drinks'" class="ai-stream-cursor">▍</text>
                        </text>
                    </view>
                    <view class="st-meal-glass">
                        <view class="st-meal-head">
                            <TongjiIcon name="leaf" size="sm" color="#006c49" />
                            <text class="st-meal-label">午餐</text>
                        </view>
                        <text class="st-meal-text">
                            {{ dietAiRecommend.lunch || (dietAiLoading ? '…' : '—') }}<text v-if="dietAiLoading && dietAiStreamField === 'lunch'" class="ai-stream-cursor">▍</text>
                        </text>
                    </view>
                    <view v-if="dietAiRecommend.dinner || (dietAiLoading && dietAiStreamField === 'dinner')" class="st-meal-glass">
                        <view class="st-meal-head">
                            <TongjiIcon name="moon" size="sm" color="#7e85cc" />
                            <text class="st-meal-label">晚餐</text>
                        </view>
                        <text class="st-meal-text">
                            {{ dietAiRecommend.dinner || '…' }}<text v-if="dietAiLoading && dietAiStreamField === 'dinner'" class="ai-stream-cursor">▍</text>
                        </text>
                    </view>
                </view>
            </view>

            <view class="st-main-spacer" />
        </view>
        
        <!-- 收起后的重新唤起按钮 -->
        <view v-if="diagnosisId && floatAskClosed" class="st-float-reopen" @click="floatAskClosed = false">
            <TongjiIcon name="sparkles" size="sm" color="#ffffff" />
        </view>

        <!-- 底部浮动 AI 输入栏 -->
        <view v-if="diagnosisId && !floatAskClosed" class="st-float-ask">
            <view class="st-float-close" @click="floatAskClosed = true">
                <text class="st-float-close-icon">×</text>
            </view>
            <view class="st-float-inner">
                <view class="st-float-icon">
                    <TongjiIcon name="sparkles" size="sm" color="#006c49" />
                </view>
                <input
                    class="st-float-input"
                    type="text"
                    :value="dietAiAskText"
                    placeholder="长按麦克风说话，或输入文字咨询 AI..."
                    placeholder-class="st-float-placeholder"
                    confirm-type="send"
                    @input="onDietAiAskInput"
                    @confirm="askDietAiFood"
                />
                <view
                    class="st-float-mic"
                    :class="{ listening: sttListening || sttHolding, disabled: dietAiAsking || !sttSupported }"
                    @touchstart.stop.prevent="onDietAiSpeechStart"
                    @touchend.stop.prevent="onDietAiSpeechEnd"
                    @touchcancel.stop.prevent="onDietAiSpeechEnd"
                >
                    <TongjiIcon name="mic" size="sm" :color="(sttListening || sttHolding) ? '#ffffff' : '#006c49'" />
                </view>
                <view
                    class="st-float-send"
                    :class="{ disabled: dietAiAsking }"
                    @click="askDietAiFood"
                >
                    <TongjiIcon name="send" size="sm" color="#ffffff" />
                </view>
            </view>
            <view v-if="sttHolding && !dietAiAsking" class="st-float-hold-hint">
                <text>{{ sttListening ? '松手结束' : '准备录音…' }}</text>
            </view>
            <view v-if="dietAiAsking || dietAiAskResult.advice" class="st-float-result">
                <view v-if="dietAiAskResult.level_label" class="st-float-badge">
                    <text>{{ dietAiAskResult.level_label }}</text>
                </view>
                <text class="st-float-advice">
                    {{ dietAiAskResult.advice || (dietAiAsking ? '正在想…' : '') }}<text v-if="dietAiAsking" class="ai-stream-cursor">▍</text>
                </text>
            </view>
        </view>

        <!-- 语音长按状态浮层：居中显示，避免被键盘/底栏遮挡 -->
        <view v-if="sttHolding && !dietAiAsking" class="st-voice-overlay">
            <view class="st-voice-card">
                <view class="st-voice-icon" :class="{ active: sttListening }">
                    <TongjiIcon name="mic" size="lg" color="#ffffff" />
                </view>
                <text class="st-voice-title">{{ sttListening ? '正在聆听…' : '准备录音…' }}</text>
                <text class="st-voice-sub">松开手指结束并识别</text>
            </view>
        </view>

        <!-- 无就诊卡：授权手机号后可录入 -->
        <view v-if="phoneGateVisible" class="input-modal-mask" @click="closePhoneGate">
            <view class="input-modal phone-gate-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">绑定手机号后即可记录血糖，无需先填写就诊卡</text>
                    </view>
                    <view class="input-modal-close" @click="closePhoneGate">
                        <text class="input-modal-close-icon">×</text>
                    </view>
                </view>
                <view class="phone-gate-body">
                    <view v-if="dailyContextError" class="phone-gate-error" role="alert">
                        <text>{{ dailyContextError }}</text>
                    </view>
                    <text class="phone-gate-tip">微信仅提供手机号，请选择性别以便写入就诊档案。</text>
                    <view class="phone-gate-gender">
                        <text class="phone-gate-gender-label">性别</text>
                        <view class="phone-gate-gender-row">
                            <view
                                class="phone-gate-gender-opt"
                                :class="{ active: phoneGateSex === 1 }"
                                @click="phoneGateSex = 1"
                            >
                                <text>男</text>
                            </view>
                            <view
                                class="phone-gate-gender-opt"
                                :class="{ active: phoneGateSex === 2 }"
                                @click="phoneGateSex = 2"
                            >
                                <text>女</text>
                            </view>
                        </view>
                    </view>
                    <button
                        class="phone-gate-btn"
                        open-type="getPhoneNumber"
                        :loading="phoneBinding"
                        :disabled="phoneBinding || !phoneGateSex"
                        @getphonenumber="onPhoneGateAuthorize"
                    >
                        微信授权手机号
                    </button>
                </view>
            </view>
        </view>

        <!-- 录入今日数据弹层 -->
        <view v-if="inputForm.visible" class="input-modal-mask" @click="closeInputForm">
            <view class="input-modal" :style="keyboardHeight ? { marginBottom: keyboardHeight + 'px' } : {}" @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 scroll-y class="input-modal-body input-modal-body-ready">
                    <view class="input-modal-tip">
                        <text>请如实填写，留空表示该项不上传。当天可多次修改。</text>
                    </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"
                                :adjust-position="false"
                                :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"
                                :adjust-position="false"
                                :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"
                                :adjust-position="false"
                                :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"
                            :adjust-position="false"
                            @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>
        </block>
        <TabBarDock :active="1" />
    </view>
</template>

<script setup>
import { ref, computed, getCurrentInstance, nextTick, watch } 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 { useDietAi } from '../composables/useDietAi.js'
import { useSpeechToText } from '../composables/useSpeechToText.js'
import TabBarDock from '@/components/app-tab-bar/tab-bar-dock.vue'
import DevTrainingEntry from '@/components/dev-training-entry/index.vue'

const { proxy } = getCurrentInstance()

/** 将接口 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: options.duration || 2800
    })
}

/** 解析接口错误文案（兼容 msg / message / data.error） */
function extractApiError(res, fallback = '') {
    if (!res) return fallback
    const raw = res.msg ?? res.message ?? res.data?.error ?? res.data?.msg ?? res.errMsg
    return formatUserMessage(raw, fallback)
}

const diagnosisId = ref(0)
/** 手机号快捷建档失败：页面常驻提示（避免 toast 被 7 字截断） */
const dailyContextError = ref('')

function clearDailyContextError() {
    dailyContextError.value = ''
}

function reportDailyContextError(res, fallback = '无法开始录入', options = {}) {
    const text = extractApiError(res, fallback)
    if (!text) return
    dailyContextError.value = text
    if (options.alert) {
        uni.showModal({
            title: '无法录入血糖',
            content: text,
            showCancel: false,
            confirmText: '知道了'
        })
    } else {
        const short = text.length > 14 ? `${text.slice(0, 14)}…` : text
        showUserToast(short, { duration: 3200 })
    }
}

const patientId = ref(0)
const patientName = ref('')
const displayPatientName = computed(() => {
    const name = String(patientName.value || '').trim()
    if (!name) return '您好'
    return name.length > 10 ? `${name.slice(0, 10)}…` : name
})
const headerSafeStyle = ref({})
const headerLeftStyle = ref({})
const voiceBtnStyle = ref({})
const age = ref(0)
const gender = ref(0)

const {
    dietAiRecommend,
    dietAiLoading,
    dietAiStreaming,
    dietAiReplacing,
    dietAiStreamField,
    dietAiAskText,
    dietAiAsking,
    dietAiAskResult,
    hasDietAiMealContent,
    fetchDietAiRecommend,
    refreshDietAiRecommend,
    askDietAiFood,
    onDietAiAskInput,
    applyDietAiToForm,
    openDietForm
} = useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage })

/** 底部 AI 浮动卡片是否被用户收起 */
const floatAskClosed = ref(false)

/** 键盘高度（px）：录入弹窗据此整体上移，避免输入框被键盘遮挡 */
const keyboardHeight = ref(0)
function onKeyboardHeightChange(res) {
    keyboardHeight.value = res && res.height ? Math.round(res.height) : 0
}

const {
    sttListening,
    sttHolding,
    sttSupported,
    beginHoldSpeech,
    endHoldSpeech,
    stopSpeechToText
} = useSpeechToText({
    onResult(text) {
        const prev = String(dietAiAskText.value || '').trim()
        dietAiAskText.value = prev ? `${prev} ${text}` : text
    },
    showToast: (msg) => showUserToast(msg)
})

function onDietAiSpeechStart() {
    if (dietAiAsking.value) return
    beginHoldSpeech()
}

function onDietAiSpeechEnd() {
    endHoldSpeech()
}

/** AI 饮食建议依据：近7天 / 近30天 血糖统计 */
const dietAnalysisList = computed(() => {
    const list = dietAiRecommend.value && Array.isArray(dietAiRecommend.value.analysis)
        ? dietAiRecommend.value.analysis
        : []
    return list.filter((s) => s && typeof s === 'object')
})

const hasDietAnalysis = computed(() => dietAnalysisList.value.length > 0)

/** AI 运动降糖建议（按血糖统计调整强度） */
const dietExercise = computed(() => {
    const ex = dietAiRecommend.value && dietAiRecommend.value.exercise
    return ex && typeof ex === 'object' && Array.isArray(ex.items) && ex.items.length ? ex : null
})

/** 达标率 → 颜色档位（绿/橙/红） */
function dietRateClass(rate) {
    const n = Number(rate)
    if (!Number.isFinite(n)) return 'rate-mid'
    if (n >= 80) return 'rate-good'
    if (n >= 50) return 'rate-mid'
    return 'rate-low'
}

function dietAvgText(v) {
    return v == null || v === '' ? '—' : v
}

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

const userMobile = ref('')
const phoneGateVisible = ref(false)
const phoneGateSex = ref(0)
const phoneBinding = ref(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 chartSeriesVisible = ref({ fasting: true, postprandial: true })

// 画布上下文相关
let canvasNode = null
let canvasCtx = null
let canvasWidth = 0
let canvasHeight = 0
let canvasDpr = 1
let chartCoords = 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) }
})

/** 拉取血糖时向前多看几天，用于判断「累计满 7 天」是否达标（图表仍只画 currentRange） */
const BLOOD_LOOKBACK_DAYS = 30

const fetchRange = computed(() => {
    const displayDays = rangeMode.value === '30' ? 30 : 7
    const lookback = Math.max(BLOOD_LOOKBACK_DAYS, displayDays)
    const end = new Date()
    end.setHours(0, 0, 0, 0)
    const start = new Date(end)
    start.setDate(start.getDate() - (lookback - 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
}

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 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 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)
}

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.isHighBp = isHighBp(m)
        m.bpText = formatBp(m)
    }
    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 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
})

function bloodDayHasSugar(b) {
    if (!b) return false
    return toNumber(b.fasting_blood_sugar) !== null
        || toNumber(b.postprandial_blood_sugar) !== null
        || toNumber(b.other_blood_sugar) !== null
}

const bloodHasData = computed(() => {
    return chartDates.value.some((date) => bloodDayHasSugar(bloodByDate.value[date]))
})

/** 当前图表时间范围内有真实录入的天数（用于小结统计） */
const bloodRecordDayCount = computed(() => {
    return chartDates.value.filter((date) => bloodDayHasSugar(bloodByDate.value[date])).length
})

/** 拉取窗口内累计有血糖录入的天数（不要求连续） */
const bloodCumulativeDayCount = computed(() => {
    let n = 0
    for (const date of Object.keys(bloodByDate.value)) {
        if (bloodDayHasSugar(bloodByDate.value[date])) n++
    }
    return n
})

const REAL_BLOOD_DAYS_MIN = 7

/** 无就诊卡或累计录入不足 7 天时，图表用示例数据 */
const chartUseMockData = computed(() => {
    if (!diagnosisId.value) return true
    return bloodCumulativeDayCount.value < REAL_BLOOD_DAYS_MIN
})

/** 示例曲线：7 天内有波动的空腹/餐后，便于老人理解趋势图形态 */
const mockBloodByDate = computed(() => {
    const map = {}
    const dates = chartDates.value
    dates.forEach((date, i) => {
        const t = dates.length > 1 ? i / (dates.length - 1) : 0
        const fasting = Number((5.2 + Math.sin(t * Math.PI) * 0.5 + (i % 2) * 0.15).toFixed(1))
        const post = Number((7.6 + Math.cos(t * Math.PI) * 1.1 + (i % 3) * 0.25).toFixed(1))
        const row = {
            record_date: date,
            fasting_blood_sugar: fasting,
            postprandial_blood_sugar: post,
            other_blood_sugar: '',
            systolic_pressure: '',
            diastolic_pressure: ''
        }
        row.isHighFasting = isHighFasting(row.fasting_blood_sugar)
        row.isHighPostprandial = isHighPostprandial(row.postprandial_blood_sugar)
        row.isHighOther = false
        row.isHighBp = false
        row.bpText = '—'
        map[date] = row
    })
    return map
})

/** 图表绘制用数据源：示例或真实 */
const chartBloodByDate = computed(() => (
    chartUseMockData.value ? mockBloodByDate.value : bloodByDate.value
))

const chartHasDisplayData = computed(() => {
    if (chartUseMockData.value) return chartDates.value.length > 0
    return bloodHasData.value
})

const bloodHighDayCount = computed(() => {
    return chartDates.value.filter((date) => {
        const b = bloodByDate.value[date]
        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
})

function elderMetricFromBlood(blood, field, label) {
    if (!blood) return { label, value: '—', isHigh: false }
    const num = toNumber(blood[field])
    if (num === null) return { label, value: '—', isHigh: false }
    const isHigh = field === 'fasting_blood_sugar'
        ? blood.isHighFasting
        : field === 'postprandial_blood_sugar'
            ? blood.isHighPostprandial
            : blood.isHighOther
    return { label, value: String(blood[field]), isHigh: !!isHigh }
}

const todayBlood = computed(() => bloodByDate.value[todayStr.value] || null)

const elderFasting = computed(() =>
    elderMetricFromBlood(todayBlood.value, 'fasting_blood_sugar', '空腹')
)

const elderPostprandial = computed(() =>
    elderMetricFromBlood(todayBlood.value, 'postprandial_blood_sugar', '餐后')
)

/** 最新测量：优先展示餐后，其次空腹（对齐 MCP 设计稿） */
const latestMeasurement = computed(() => {
    const blood = todayBlood.value
    if (!blood) {
        return { hasValue: false, typeLabel: '', value: '—', isHigh: false }
    }
    const post = toNumber(blood.postprandial_blood_sugar)
    if (post !== null) {
        return {
            hasValue: true,
            typeLabel: '餐后',
            value: String(blood.postprandial_blood_sugar),
            isHigh: !!blood.isHighPostprandial
        }
    }
    const fasting = toNumber(blood.fasting_blood_sugar)
    if (fasting !== null) {
        return {
            hasValue: true,
            typeLabel: '空腹',
            value: String(blood.fasting_blood_sugar),
            isHigh: !!blood.isHighFasting
        }
    }
    return { hasValue: false, typeLabel: '', value: '—', isHigh: false }
})

// 给某天某字段计算 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
}

// ============ 就诊卡 ============
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
}

function readUserMobile() {
    try {
        const userData = uni.getStorageSync('userData') || {}
        userMobile.value = String(userData.mobile || userData.phone || '').trim()
    } catch (e) {
        userMobile.value = ''
    }
    return userMobile.value
}

function syncPhoneGateSexFromUser() {
    try {
        const userData = uni.getStorageSync('userData') || {}
        const sex = Number(userData.sex)
        phoneGateSex.value = sex === 1 || sex === 2 ? sex : 0
    } catch (e) {
        phoneGateSex.value = 0
    }
}

async function openPhoneGate() {
    syncPhoneGateSexFromUser()
    readUserMobile()
    if (userMobile.value) {
        phoneGateVisible.value = false
        await resolveDailyPhoneContext(false)
        return
    }
    phoneGateVisible.value = true
}

async function refreshUserMobile() {
    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)
            userMobile.value = String(res.data.mobile || res.data.phone || '').trim()
        }
    } catch (e) {
        /* 静默 */
    }
    return userMobile.value
}

async function ensureDailyContextByPhone(sex, options = {}) {
    if (diagnosisId.value) {
        clearDailyContextError()
        return true
    }

    const payload = {}
    const sexVal = sex ?? phoneGateSex.value
    if (sexVal === 1 || sexVal === 2) {
        payload.sex = sexVal
    }

    const res = await proxy.apiUrl({
        url: '/api/tcm/dailyEnsurePhoneContext',
        method: 'POST',
        data: payload
    }, false)

    if (res && res.code === 1 && res.data && res.data.diagnosis_id) {
        clearDailyContextError()
        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) {
        clearDailyContextError()
        return 'need_mobile'
    }

    reportDailyContextError(res, '无法开始录入', options)
    return false
}

/** 已绑定手机号时直接关联档案，不再弹出授权建档 */
async function resolveDailyPhoneContext(openFormAfter = false) {
    const ok = await ensureDailyContextByPhone(phoneGateSex.value, { alert: true })
    if (ok === true) {
        if (openFormAfter) await openInputForm(true)
        return true
    }
    if (ok === 'need_mobile') {
        syncPhoneGateSexFromUser()
        phoneGateVisible.value = true
    }
    return false
}

async function ensureCanRecordGlucose() {
    if (diagnosisId.value) return true

    readUserMobile()
    if (!userMobile.value) {
        openPhoneGate()
        return false
    }

    const ok = await ensureDailyContextByPhone(undefined, { alert: true })
    if (ok === 'need_mobile') {
        openPhoneGate()
        return false
    }
    return !!ok
}

function closePhoneGate() {
    if (phoneBinding.value) return
    phoneGateVisible.value = false
    resetCanvasCache()
    nextTick(() => scheduleChartRedraw())
}

async function onPhoneGateAuthorize(e) {
    const detail = e?.detail || {}
    if (detail.errMsg && !String(detail.errMsg).includes('ok')) {
        showUserToast('需要授权手机号后才能录入血糖')
        return
    }
    const code = detail.code
    if (!code) {
        showUserToast('获取手机号失败，请重试')
        return
    }
    if (phoneGateSex.value !== 1 && phoneGateSex.value !== 2) {
        showUserToast('请先选择性别')
        return
    }

    phoneBinding.value = true
    try {
        readUserMobile()
        if (userMobile.value) {
            phoneGateVisible.value = false
            await resolveDailyPhoneContext(true)
            return
        }

        const res = await proxy.apiUrl({
            url: '/api/user/getMobileByMnp',
            method: 'POST',
            data: { code, sex: phoneGateSex.value }
        }, false)
        if (!res || res.code !== 1) {
            showUserToast(formatUserMessage(res?.msg, '绑定手机号失败'))
            return
        }
        await refreshUserMobile()
        phoneGateVisible.value = false
        await resolveDailyPhoneContext(true)
    } catch (err) {
        showUserToast('网络异常，请稍后再试')
    } finally {
        phoneBinding.value = 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 } = fetchRange.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 fetchDietAiRecommend(false)
    } catch (e) {
        uni.showToast({ title: '加载失败，请稍后再试', icon: 'none' })
    } finally {
        loading.value = false
        await nextTick()
        scheduleChartRedraw()
        // 自动播报：数据就绪后朗读一次最新摘要
        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()
}

function buildMorePageUrl() {
    if (!diagnosisId.value) return '/tongji/pages/more'
    const q = [
        `diagnosis_id=${diagnosisId.value}`,
        patientId.value ? `patient_id=${patientId.value}` : ''
    ].filter(Boolean).join('&')
    return `/tongji/pages/more?${q}`
}

/** 点击血糖趋势图：进入记录页 more（无就诊卡时先手机号快捷建档） */
async function onChartTap() {
    if (!diagnosisId.value) {
        const ok = await ensureCanRecordGlucose()
        if (!ok || !diagnosisId.value) return
    }
    uni.navigateTo({ url: buildMorePageUrl() })
}

async function goRecordsPage() {
    onChartTap()
}

async function goMorePage() {
    if (!diagnosisId.value) {
        const ok = await ensureCanRecordGlucose()
        if (!ok || !diagnosisId.value) return
    }
    uni.navigateTo({ url: buildMorePageUrl() })
}

function goGamePage() {
    uni.navigateTo({
        url: '/tongji/pages/game',
        fail() {
            uni.showToast({ title: '暂时无法打开游戏', icon: 'none' })
        }
    })
}

function navToUser() {
    uni.redirectTo({ url: '/pages/user/user' })
}

// ============ 时段问候 / 打卡 / 健康日历 ============
/** 时段问候：文案 + 统一 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
}

function dayCoverageLevel(key) {
    const b = bloodByDate.value[key]
    if (!b) return 0
    const fields = ['fasting_blood_sugar', 'postprandial_blood_sugar', 'other_blood_sugar']
    const score = fields.filter((f) => toNumber(b[f]) !== null).length
    if (score === 0) return 0
    if (score === 1) return 1
    if (score === 2) 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)
}

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
            })
            cur.setDate(cur.getDate() + 1)
        }
        weeks.push(week)
    }
    return weeks
})

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 (!recordDays.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())

function prepareRecordOverlay() {
    resetCanvasCache()
    ttsStop()
}

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

async function openInputForm(skipEnsure = false) {
    const token = uni.getStorageSync('token')
    if (!token) {
        showUserToast('请先登录后再录入')
        return
    }
    if (!skipEnsure) {
        const can = await ensureCanRecordGlucose()
        if (!can) return
    }
    if (!diagnosisId.value) {
        showUserToast('请先授权手机号')
        return
    }
    // 微信小程序 canvas 是原生组件、z-index 无法被弹层覆盖，
    // 这里通过 v-if 卸载它；下次显示时 selectorQuery 拿到的会是新节点，
    // 因此先清空缓存的 ctx / node，避免下次 ensureCanvas 复用旧引用。
    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 || '')
        }
    } catch (e) {
        // 静默：拉取失败时仍允许新增录入
    } finally {
        inputForm.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 closeInputForm() {
    if (inputForm.value.submitting) return
    inputForm.value.visible = false
    resetCanvasCache()
    nextTick(() => scheduleChartRedraw())
}

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

watch(recordOverlayOpen, (open) => {
    if (!open) {
        resetCanvasCache()
        nextTick(() => scheduleChartRedraw())
    }
})

async function submitInputForm() {
    if (inputForm.value.submitting || inputForm.value.loading) return

    // 至少填一个有效字段
    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) {
        uni.showToast({ title: '请至少填写一项数据', icon: 'none' })
        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()
        } else {
            showUserToast((res && res.msg) || '保存失败')
        }
    } catch (e) {
        uni.showToast({ title: '网络异常，请稍后再试', icon: 'none' })
    } finally {
        inputForm.value.submitting = false
    }
}

function deleteInputRecord() {
    if (!inputForm.value.id || inputForm.value.submitting) return
    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
                    fetchAll()
                } else {
                    showUserToast((res && res.msg) || '删除失败')
                }
            } catch (e) {
                uni.showToast({ title: '网络异常，请稍后再试', icon: 'none' })
            } finally {
                inputForm.value.submitting = false
            }
        }
    })
}

// ============ 语音播报（适老化）============
// 优先用 微信同声传译插件 (mp-weixin)；H5 走 window.speechSynthesis；
// 都不可用则轻提示，避免 showModal 挡住录入弹层。
const ttsEnabled = ref(true)
const ttsAutoPlay = ref(false)
const ttsSpeaking = ref(false)
let ttsAudioCtx = null
let ttsAutoToken = 0

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

function ttsStop() {
    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, '')
}

function ttsSpeak(text) {
    if (!ttsEnabled.value) return
    const content = normalizeTtsText(text)
    if (!content) return

    ttsStop()

    // 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 }
            u.onerror = () => { ttsSpeaking.value = false }
            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) => {
                if (!res || !res.filename) {
                    ttsSpeaking.value = false
                    return
                }
                teardownTtsAudio()
                ttsAudioCtx = uni.createInnerAudioContext()
                ttsAudioCtx.src = res.filename
                ttsAudioCtx.obeyMuteSwitch = false
                ttsAudioCtx.onPlay(() => { ttsSpeaking.value = true })
                ttsAudioCtx.onEnded(() => { ttsSpeaking.value = false })
                ttsAudioCtx.onError(() => { ttsSpeaking.value = false })
                ttsAudioCtx.onStop(() => { ttsSpeaking.value = false })
                ttsAudioCtx.play()
            },
            fail: (err) => {
                ttsSpeaking.value = false
                console.warn('WechatSI textToSpeech failed', err)
                uni.showToast({ title: '语音不可用，请检查网络/插件', icon: 'none', duration: 2000 })
            }
        })
        return
    } catch (e) {
        console.warn('WechatSI plugin not available', e)
    }
    // #endif

    // 其它端兜底：轻提示，勿用 modal 遮挡录入等操作
    showUserToast(content, { duration: 3200 })
}

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 resetCanvasCache() {
    canvasCtx = null
    canvasNode = null
    canvasWidth = 0
    canvasHeight = 0
}

function fallbackCanvasSize() {
    try {
        const sys = uni.getSystemInfoSync()
        const margin = uni.upx2px(40) * 2
        const cardPad = uni.upx2px(40) * 2
        return {
            width: Math.max(280, sys.windowWidth - margin - cardPad),
            height: uni.upx2px(360)
        }
    } catch (e) {
        return { width: 320, height: 180 }
    }
}

function ensureCanvas(retryCount = 0) {
    return new Promise((resolve) => {
        if (canvasCtx && canvasWidth > 0 && canvasHeight > 0) {
            resolve(true)
            return
        }
        if (canvasCtx && (canvasWidth <= 0 || canvasHeight <= 0)) {
            resetCanvasCache()
        }
        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) {
                    if (retryCount < 10) {
                        setTimeout(() => ensureCanvas(retryCount + 1).then(resolve), retryCount < 4 ? 100 : 180)
                    } else {
                        resolve(false)
                    }
                    return
                }

                let w = item.width || 0
                let h = item.height || 0
                if (w <= 0 || h <= 0) {
                    const fallback = fallbackCanvasSize()
                    w = w > 0 ? w : fallback.width
                    h = h > 0 ? h : fallback.height
                }
                if ((item.width <= 0 || item.height <= 0) && retryCount < 6) {
                    setTimeout(() => ensureCanvas(retryCount + 1).then(resolve), 120)
                    return
                }

                canvasNode = item.node
                canvasCtx = canvasNode.getContext('2d')
                try {
                    canvasDpr = uni.getSystemInfoSync().pixelRatio || 1
                } catch (e) {
                    canvasDpr = 1
                }
                canvasWidth = w
                canvasHeight = h
                canvasNode.width = canvasWidth * canvasDpr
                canvasNode.height = canvasHeight * canvasDpr
                canvasCtx.setTransform(1, 0, 0, 1, 0, 0)
                canvasCtx.scale(canvasDpr, canvasDpr)
                resolve(true)
            })
    })
}

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 = chartBloodByDate.value[date]
        return b ? toNumber(b[field]) : null
    })
}

function computeYRange(...seriesList) {
    const values = seriesList.flat().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: 12, bottom: 28 }
    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 n = chartDates.value.length
    const threshold = getBloodSugarThresholds(age.value)
    const seriesDefs = [
        {
            key: 'fasting',
            data: fastingSeries,
            color: '#006c49',
            fillTop: 'rgba(0,108,73,0.18)',
            fillBottom: 'rgba(0,108,73,0)',
            label: '空腹',
            highThreshold: threshold ? threshold.fasting : null
        },
        {
            key: 'postprandial',
            data: postSeries,
            color: '#a43a3a',
            fillTop: 'rgba(164,58,58,0.18)',
            fillBottom: 'rgba(164,58,58,0)',
            label: '餐后',
            highThreshold: threshold ? threshold.postprandial : null
        }
    ]
    const visibleDefs = seriesDefs.filter((item) => chartSeriesVisible.value[item.key])
    const rangeSources = visibleDefs.length
        ? visibleDefs.map((item) => item.data)
        : [fastingSeries, postSeries]
    const { yMin, yMax } = computeYRange(...rangeSources)
    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 xAt = (i) => (n === 1 ? padding.left + cw / 2 : padding.left + (cw * i) / (n - 1))
    const coordByKey = { fasting: [], postprandial: [] }
    // 7 天内显示圆点，30 天以纯曲线为主
    const showDots = n <= 10

    seriesDefs.forEach((meta) => {
        const projected = meta.data.map((v, i) => ({
            x: xAt(i),
            y: v === null ? null : valueToY(v),
            value: v
        }))
        coordByKey[meta.key] = projected

        if (!chartSeriesVisible.value[meta.key]) return

        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: coordByKey.fasting,
        postprandial: coordByKey.postprandial
    }
}

function toggleChartSeries(key) {
    if (key !== 'fasting' && key !== 'postprandial') return
    chartSeriesVisible.value[key] = !chartSeriesVisible.value[key]
    hoverInfo.value = null
    scheduleChartRedraw()
}

watch(rangeMode, () => {
    scheduleChartRedraw()
})

watch([chartUseMockData, bloodRecords], () => {
    scheduleChartRedraw()
})

// ============ 生命周期 ============
function readUserContext() {
    try {
        const userData = uni.getStorageSync('userData')
        if (!userData) return
        // 勿用问诊会话里的 diagnosis 作为日常血糖诊单（无 view_records 会报无权）
        const name = String(userData.real_name || userData.nickname || '').trim()
        if (!patientName.value && name) patientName.value = name
        const sex = Number(userData.sex)
        if (!gender.value && (sex === 1 || sex === 2)) gender.value = sex === 1 ? 1 : 0
        if (!age.value && userData.age) age.value = Number(userData.age) || 0
    } catch (e) {}
}

function initHeaderSafeArea() {
    const voiceSize = uni.upx2px(80)
    const voiceGap = uni.upx2px(16)
    let capsulePad = uni.upx2px(200)
    let paddingTopPx = uni.upx2px(24)

    try {
        const sys = uni.getSystemInfoSync()
        const statusBarHeight = Number(sys.statusBarHeight) || 0
        paddingTopPx = statusBarHeight + uni.upx2px(16)

        // #ifdef MP-WEIXIN
        const menu = typeof uni.getMenuButtonBoundingClientRect === 'function'
            ? uni.getMenuButtonBoundingClientRect()
            : null
        if (menu && menu.width > 0) {
            capsulePad = sys.windowWidth - menu.left + uni.upx2px(12)
            paddingTopPx = Math.max(paddingTopPx, menu.top)
            voiceBtnStyle.value = {
                position: 'absolute',
                right: `${capsulePad}px`,
                top: `${menu.top + (menu.height - voiceSize) / 2}px`
            }
        } else {
            voiceBtnStyle.value = {}
        }
        // #endif

        // #ifndef MP-WEIXIN
        voiceBtnStyle.value = {}
        // #endif
    } catch (e) {
        paddingTopPx = uni.upx2px(48)
        voiceBtnStyle.value = {}
    }

    headerSafeStyle.value = {
        paddingTop: `${paddingTopPx}px`,
        paddingRight: `${capsulePad + voiceSize + voiceGap}px`
    }
    headerLeftStyle.value = {}
}

async function bootstrap(options) {
    options = options || {}
    initHeaderSafeArea()
    // weekly 专页：不走家人邀请观看，只保留血糖记录 + AI 饮食
    isInviteViewer.value = 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 {
        // 无可见就诊卡：勿用 URL/缓存里的隐藏诊单 ID
        diagnosisId.value = 0
        patientId.value = 0
        patientName.value = ''
        age.value = 0
        gender.value = 0
        readUserMobile()
        if (userMobile.value) {
            await ensureDailyContextByPhone(undefined, { alert: false })
        }
    }

    authChecking.value = false
    if (diagnosisId.value) {
        await fetchAll()
    } else {
        bloodRecords.value = []
        await nextTick()
        scheduleChartRedraw()
    }
}

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

onShow(() => {
    initHeaderSafeArea()
    try { uni.onKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
    if (authChecking.value) return
    resetCanvasCache()
    nextTick(() => scheduleChartRedraw())
})

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

onHide(() => {
    ttsStop()
    stopSpeechToText()
    try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
    keyboardHeight.value = 0
})

onUnload(() => {
    ttsStop()
    stopSpeechToText()
    try { uni.offKeyboardHeightChange(onKeyboardHeightChange) } catch (e) {}
    keyboardHeight.value = 0
})

// ============ 邀请观看（家人分享页）============
const isInviteViewer = ref(false)
const inviteLoading = ref(false)
const inviteError = ref('')
const invitePreview = ref({})
const inviteLiking = ref(false)
const inviteLikeNickname = ref('家人')
const inviteHeartBurst = ref(false)
const familyNicknameOptions = ['家人', '亲友', '老伴', '子女']

const VIEWER_KEY_STORAGE = 'tongji_viewer_key_v1'

function hapticLight() {
    try {
        uni.vibrateShort({ type: 'light' })
    } 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()}`
    }
}

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

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

async function bootstrapInviteViewer(inviteCode) {
    isInviteViewer.value = true
    inviteLoading.value = true
    inviteError.value = ''
    invitePreview.value = {}
    inviteHeartBurst.value = false
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailySharePreview',
            method: 'GET',
            data: {
                invite_code: inviteCode,
                viewer_key: getViewerKey()
            }
        }, false)
        if (res && res.code === 1 && res.data) {
            invitePreview.value = res.data
        } else {
            inviteError.value = formatUserMessage(res && res.msg, '邀请码无效或已过期')
        }
    } catch (e) {
        inviteError.value = '加载失败，请稍后再试'
    } finally {
        inviteLoading.value = false
    }
}

async function onFamilyLike() {
    const code = invitePreview.value.invite_code
    if (!code || inviteLiking.value) return
    if (invitePreview.value.liked_by_me) {
        showUserToast('您今天已经点过赞啦')
        return
    }
    inviteLiking.value = true
    try {
        const res = await proxy.apiUrl({
            url: '/api/tcm/dailyFamilyLike',
            method: 'POST',
            data: {
                invite_code: code,
                viewer_key: getViewerKey(),
                nickname: inviteLikeNickname.value
            }
        }, false)
        if (res && res.code === 1 && res.data) {
            const data = res.data
            const alreadyLiked = !!data.already_liked
            invitePreview.value = {
                ...invitePreview.value,
                like_count: Number(data.like_count) || 0,
                liked_by_me: true
            }
            if (alreadyLiked) {
                showUserToast(data.praise_message || '您今天已经点过赞啦')
                return
            }
            inviteHeartBurst.value = true
            setTimeout(() => {
                inviteHeartBurst.value = false
            }, 900)
            hapticLight()
            triggerCelebrate('鼓励已送达', data.praise_message || '谢谢您的点赞')
        } else {
            showUserToast((res && res.msg) || '点赞失败')
        }
    } catch (e) {
        showUserToast('点赞失败，请稍后再试')
    } finally {
        inviteLiking.value = false
    }
}
</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: #6c7a71;
}
</style>
<style lang="scss">
@import '../styles/stitch-vitalmint-theme.scss';
@import '../styles/weekly-stitch.scss';
@import '../styles/weekly-input-modal.scss';
</style>
