<template>
    <view class="page">
        <!-- 自定义导航：对齐微信小程序胶囊 -->
        <view class="nav" :style="navStyle">
            <view class="nav-row" :style="navRowStyle">
                <view class="nav-back" @click="onBack">
                    <text class="nav-back-icon">‹</text>
                </view>
                <view class="nav-center">
                    <text class="nav-title">握力环训练 2.0</text>
                    <view class="nav-status">
                        <view class="nav-dot" :class="trackingQuality" />
                        <text class="nav-status-text">{{ trackingLabel }}</text>
                    </view>
                </view>
                <view class="nav-capsule-spacer" :style="{ width: capsuleSpacerPx + 'px' }" />
            </view>
        </view>

        <!-- 实时指标 -->
        <view class="metrics-wrap">
            <view class="metrics">
                <view class="metric">
                    <text class="metric-value">{{ validReps }}</text>
                    <text class="metric-label">有效握紧</text>
                </view>
                <view class="metric">
                    <text class="metric-value">{{ accuracyText }}</text>
                    <text class="metric-label">准确率</text>
                </view>
                <view class="metric metric--combo">
                    <text class="metric-value metric-value--combo">连击 x{{ combo }}</text>
                    <text class="metric-label">当前连击</text>
                </view>
                <view class="metric">
                    <text class="metric-value">{{ elapsedText }}</text>
                    <text class="metric-label">训练时间</text>
                </view>
            </view>
        </view>

        <!-- 中间画布：相机/动画 + 反馈层 -->
        <view class="stage" :class="'cap-' + captureState">
            <!-- #ifdef MP-WEIXIN -->
            <camera
                v-if="cameraReady"
                id="gripCamera"
                class="stage-camera"
                device-position="front"
                flash="off"
                frame-size="small"
                @initdone="onCameraInitDone"
                @error="onCameraError"
            />
            <!-- #endif -->

            <!-- 动画模式：示范图跟练 + 粒子特效（识别仍走相机帧） -->
            <view v-if="cameraReady && stageMode === 'anim'" class="stage-anim">
                <view class="ambient-glow" />
                <CrushCanvas v-if="crushEnabled" :crush-signal="crushSignal" />
                <view class="ring-container" :class="['sq-' + energyLevel, { idle: demoIdle }]">
                    <view class="beat-halo" />
                    <view class="video-ring">
                        <image
                            class="demo-video"
                            :src="TUTORIAL_POSTER_URL"
                            mode="aspectFill"
                        />
                        <view class="video-ring-border" />
                    </view>
                </view>
            </view>

            <view v-if="!cameraReady || cameraError" class="stage-fallback">
                <text class="stage-fallback-text">{{ sessionFinished ? '本轮训练已完成' : cameraHint }}</text>
                <view
                    v-if="sessionFinished"
                    class="stage-fallback-btn"
                    @click="onRestart"
                >
                    <text class="stage-fallback-btn-text">再练一次</text>
                </view>
                <view v-else-if="!cameraReady && !sessionPlaying" class="stage-fallback-btn" @click="startCameraSession">
                    <text class="stage-fallback-btn-text">开启摄像头训练</text>
                </view>
            </view>

            <view v-if="cameraReady && stageMode === 'camera'" class="stage-wash" />

            <!-- 画布四角捕捉状态框 -->
            <view v-if="cameraReady && !sessionFinished" class="capture-frame">
                <view class="corner corner--tl" />
                <view class="corner corner--tr" />
                <view class="corner corner--bl" />
                <view class="corner corner--br" />
            </view>

            <!-- 捕捉状态标签 -->
            <view v-if="cameraReady && !sessionFinished" class="capture-pill">
                <view class="capture-dot" />
                <text class="capture-pill-text">{{ captureLabel }}</text>
            </view>

            <!-- 动画/实景切换 -->
            <view v-if="cameraReady && !sessionFinished" class="mode-toggle" @click="toggleStageMode">
                <text class="mode-toggle-text">{{ stageMode === 'anim' ? '看实景' : '看动画' }}</text>
            </view>

            <view v-if="!sessionFinished" class="stage-hud">
                <text class="hit-title" :class="{ active: showHitFeedback }">{{ headlineText }}</text>

                <view v-if="combo >= 2" :key="comboKey" class="combo-float">
                    <view class="combo-pill">
                        <text class="combo-pill-text">连击 x{{ combo }}</text>
                        <view class="combo-drain">
                            <view class="combo-drain-fill" />
                        </view>
                    </view>
                    <view class="speed-line speed-line--1" />
                    <view class="speed-line speed-line--2" />
                    <view class="speed-line speed-line--3" />
                </view>

                <view v-if="combo >= 3" class="combo-ribbon">
                    <text class="combo-ribbon-line">连击 x{{ combo }}！</text>
                    <text class="combo-ribbon-sub">太棒了！</text>
                </view>

                <view v-if="showComboBreak" class="combo-break">
                    <text class="combo-break-text">连击中断</text>
                </view>

                <view v-if="showReward" class="reward-wrap">
                    <view class="reward-glow" />
                    <text class="reward-burst">+{{ lastReward }}</text>
                </view>
            </view>

            <!-- 庆祝层：横幅 + 撒花 -->
            <view v-if="celebrateVisible" :key="'c' + celebrateKey" class="celebrate" :class="celebrateKind">
                <view class="celebrate-banner">
                    <text class="celebrate-banner-text">{{ celebrateText }}</text>
                </view>
                <view v-for="n in 6" :key="n" class="confetti" :class="'confetti-' + n" />
            </view>
        </view>

        <!-- 底部目标与操作：始终可点，结束用强制收尾 -->
        <view class="footer">
            <view class="goal-row">
                <text class="goal-text">任务进度 {{ validReps }}/{{ targetReps }}</text>
                <text class="remain-text">{{ sessionFinished ? '已完成' : `剩余 ${remainText}` }}</text>
            </view>
            <view class="goal-track">
                <view class="goal-fill" :style="{ width: goalProgress * 100 + '%' }" />
            </view>
            <view class="actions">
                <view
                    v-if="!sessionFinished"
                    class="action action--pause"
                    @click.stop="onTogglePause"
                >
                    <text class="action-icon">{{ sessionPlaying && !sessionPaused ? '❚❚' : '▶' }}</text>
                    <text class="action-text">{{ !sessionPlaying ? '开始' : sessionPaused ? '继续' : '暂停' }}</text>
                </view>
                <view
                    v-if="!sessionFinished"
                    class="action action--end"
                    @click.stop="onEndTraining"
                >
                    <text class="action-icon">■</text>
                    <text class="action-text">结束训练</text>
                </view>
                <view
                    v-if="sessionFinished"
                    class="action action--pause"
                    @click.stop="onRestart"
                >
                    <text class="action-text">再练一次</text>
                </view>
                <view
                    v-if="sessionFinished"
                    class="action action--end"
                    @click.stop="onBack"
                >
                    <text class="action-text">返回</text>
                </view>
            </view>
        </view>
    </view>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import CrushCanvas from './components/crush-canvas.vue'
import { createGesturePipeline } from '../motion/gesture-pipeline'
import type { GesturePhase, GestureSource, TrackingQuality } from '../motion/types'

// 与旧握力环页共用封面（跟练动画主体；训练中不叠播网络视频，避免与 camera 抢原生线程）
const TUTORIAL_POSTER_URL = 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/images/20260529/202605291030231aed12802.jpg'

const TARGET_REPS = 30
const TARGET_SECONDS = 300

const statusBarPx = ref(20)
const navRowPx = ref(44)
const capsuleSpacerPx = ref(96)

const cameraReady = ref(false)
const cameraInited = ref(false)
const cameraError = ref('')
const sessionPlaying = ref(false)
const sessionPaused = ref(false)
const sessionFinished = ref(false)
const sessionEnding = ref(false)

const trackingQuality = ref<TrackingQuality>('lost')
const gesturePhase = ref<GesturePhase>('UNTRACKED')
const energyLevel = ref(0)
const pipelineSource = ref<GestureSource>('none')
const frameFps = ref(0)

const validReps = ref(0)
const attemptReps = ref(0)
const combo = ref(0)
const bestCombo = ref(0)
const elapsedSeconds = ref(0)
const lastReward = ref(1)
const showReward = ref(false)
const showHitFeedback = ref(false)

/** 画布模式：默认动画演示，可切实景 */
const stageMode = ref<'anim' | 'camera'>('anim')

/** 连击：时限内完成下一次计连击，超时或脱离画面则中断 */
const COMBO_WINDOW_MS = 6000
const comboKey = ref(0)
const showComboBreak = ref(false)

/** 庆祝层（横幅 + 撒花） */
const celebrateVisible = ref(false)
const celebrateKind = ref<'combo' | 'progress' | 'finish'>('combo')
const celebrateText = ref('')
const celebrateKey = ref(0)

/** 捏碎粒子：连击达标才触发，随进度换款，末段减负 */
const crushEnabled = ref(false)
const crushSignal = ref<{ type: 'egg' | 'walnut' | 'can' | 'balloon'; nonce: number; intensity?: number } | null>(null)
type CrushType = 'egg' | 'walnut' | 'can' | 'balloon'
let lastCrushTs = 0

const targetReps = TARGET_REPS

let timerId: ReturnType<typeof setInterval> | null = null
let rewardTimer: ReturnType<typeof setTimeout> | null = null
let comboBreakTimer: ReturnType<typeof setTimeout> | null = null
let comboBreakHideTimer: ReturnType<typeof setTimeout> | null = null
let celebrateTimer: ReturnType<typeof setTimeout> | null = null
let teardownTimer: ReturnType<typeof setTimeout> | null = null
let lastRepTs = 0
let startingPipeline = false
let endModalShown = false
let isLeaving = false
let cleanedUp = false

const pipeline = createGesturePipeline({
    onObservation(obs) {
        if (sessionEnding.value || sessionFinished.value || !sessionPlaying.value) return
        if (gesturePhase.value !== obs.phase) {
            gesturePhase.value = obs.phase
            if (obs.phase === 'UNTRACKED' && combo.value > 0) breakCombo()
        }
        // 能量环降频：只在档位变化时更新
        const nextLevel = Math.min(4, Math.round(obs.compression * 4))
        if (energyLevel.value !== nextLevel) energyLevel.value = nextLevel
        if (trackingQuality.value !== obs.trackingQuality) {
            trackingQuality.value = obs.trackingQuality
        }
        // 源/fps 很少变，降低写入
        if (pipelineSource.value !== obs.source) {
            pipelineSource.value = obs.source
        }
        const hit = obs.phase === 'CLOSED_CONFIRMED' || obs.phase === 'CLOSING'
        if (showHitFeedback.value !== hit) showHitFeedback.value = hit
    },
    onRepCompleted() {
        onRepCompleted()
    },
    onWarning(message) {
        uni.showToast({ title: message.slice(0, 40), icon: 'none', duration: 2200 })
    },
    onError(message) {
        cameraError.value = message
        trackingQuality.value = 'lost'
        uni.showToast({ title: message, icon: 'none', duration: 2500 })
    },
})

const navStyle = computed(() => ({
    paddingTop: `${statusBarPx.value}px`,
}))

const navRowStyle = computed(() => ({
    height: `${navRowPx.value}px`,
}))

const trackingLabel = computed(() => {
    if (!cameraReady.value) return '待开启'
    if (cameraError.value) return '识别不可用'
    if (sessionPaused.value) return '已暂停'
    if (trackingQuality.value === 'lost') return '追踪丢失'
    if (trackingQuality.value === 'weak') return '识别较弱'
    const src = pipelineSource.value === 'roi' ? 'ROI' : ''
    return src ? `识别中 · ${src}` : '识别中'
})

const accuracyText = computed(() => {
    if (attemptReps.value <= 0) return '—'
    return `${Math.round((validReps.value / attemptReps.value) * 100)}%`
})

const elapsedText = computed(() => formatClock(elapsedSeconds.value))
const remainText = computed(() => formatClock(Math.max(0, TARGET_SECONDS - elapsedSeconds.value)))
const goalProgress = computed(() => Math.min(1, validReps.value / targetReps))

const headlineText = computed(() => {
    switch (gesturePhase.value) {
        case 'OPEN_READY':
            return '准备握紧'
        case 'CLOSING':
            return '用力握紧'
        case 'CLOSED_CONFIRMED':
            return '保持住'
        case 'REOPENING':
            return '慢慢松开'
        default:
            return '把手放入画面'
    }
})

/** 画布四周捕捉状态：lost / seek / open / grip / hold / release */
const captureState = computed(() => {
    if (sessionPaused.value) return 'seek'
    switch (gesturePhase.value) {
        case 'OPEN_READY':
            return 'open'
        case 'CLOSING':
            return 'grip'
        case 'CLOSED_CONFIRMED':
            return 'hold'
        case 'REOPENING':
            return 'release'
        default:
            return trackingQuality.value === 'lost' ? 'lost' : 'seek'
    }
})

const captureLabel = computed(() => {
    switch (captureState.value) {
        case 'open':
            return '已捕捉到手'
        case 'grip':
            return '握紧中'
        case 'hold':
            return '保持中'
        case 'release':
            return '松开中'
        case 'seek':
            return '正在捕捉…'
        default:
            return '脱离画面'
    }
})

const cameraHint = computed(() => {
    if (cameraError.value) return cameraError.value
    if (!cameraReady.value) return '开启前置摄像头后开始训练（裸手开合试验）'
    return '正在准备相机…'
})

function toggleStageMode() {
    stageMode.value = stageMode.value === 'anim' ? 'camera' : 'anim'
}

/** 未捕捉到手时，演示动画自动循环示范动作 */
const demoIdle = computed(() => captureState.value === 'seek' || captureState.value === 'lost')

function formatClock(total: number) {
    const m = Math.floor(total / 60)
    const s = total % 60
    return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}

function initNavMetrics() {
    try {
        const sys = uni.getSystemInfoSync()
        statusBarPx.value = Number(sys.statusBarHeight) || 20
        // #ifdef MP-WEIXIN
        const menu = typeof uni.getMenuButtonBoundingClientRect === 'function'
            ? uni.getMenuButtonBoundingClientRect()
            : null
        if (menu && menu.height) {
            navRowPx.value = menu.height + (menu.top - statusBarPx.value) * 2
            capsuleSpacerPx.value = Math.max(88, sys.windowWidth - menu.left + 8)
        }
        // #endif
    } catch (_) {}
}

function requestCameraPermission(): Promise<boolean> {
    // #ifdef MP-WEIXIN
    return new Promise((resolve) => {
        // @ts-ignore
        const wxApi = typeof wx !== 'undefined' ? wx : null
        if (!wxApi?.authorize) {
            resolve(true)
            return
        }
        wxApi.authorize({
            scope: 'scope.camera',
            success: () => resolve(true),
            fail: () => {
                uni.showModal({
                    title: '需要摄像头权限',
                    content: '握力环 2.0 需要前置摄像头做视觉识别（试验），请在设置中开启。',
                    confirmText: '去设置',
                    success: (res) => {
                        if (res.confirm) uni.openSetting({})
                        resolve(false)
                    },
                })
            },
        })
    })
    // #endif
    // #ifndef MP-WEIXIN
    return Promise.resolve(false)
    // #endif
}

async function startCameraSession() {
    const ok = await requestCameraPermission()
    if (!ok) {
        cameraError.value = '未获得摄像头权限'
        return
    }
    cameraError.value = ''
    cameraInited.value = false
    cameraReady.value = true
    trackingQuality.value = 'weak'
    await nextTick()
    // initdone 回调里再启动 pipeline；若迟迟未到则兜底
    setTimeout(() => {
        if (
            cameraReady.value
            && !pipeline.isActive()
            && !startingPipeline
            && !sessionFinished.value
            && !sessionEnding.value
        ) {
            beginSession()
        }
    }, 600)
}

function onCameraInitDone() {
    cameraInited.value = true
    if (sessionFinished.value || sessionEnding.value) return
    if (!sessionPlaying.value || !pipeline.isActive()) {
        beginSession()
    }
}

async function beginSession() {
    if (startingPipeline || sessionEnding.value || sessionFinished.value) return
    sessionFinished.value = false
    endModalShown = false
    sessionPlaying.value = true
    sessionPaused.value = false
    startTimer()
    startingPipeline = true
    try {
        const ok = await pipeline.start()
        // 达目标/结束过程中若 start 仍在飞，立刻停掉，避免“结束后又活过来”
        if (!ok || sessionEnding.value || sessionFinished.value) {
            pipeline.stop()
            sessionPlaying.value = false
            stopTimer()
            return
        }
        try {
            uni.setKeepScreenOn({ keepScreenOn: true })
        } catch (_) {}
    } finally {
        startingPipeline = false
    }
}

function startTimer() {
    if (timerId) return
    timerId = setInterval(() => {
        if (sessionPaused.value || sessionEnding.value || sessionFinished.value) return
        elapsedSeconds.value += 1
        if (elapsedSeconds.value >= TARGET_SECONDS) {
            setTimeout(() => finishSession('time'), 0)
        }
    }, 1000)
}

function stopTimer() {
    if (timerId) {
        clearInterval(timerId)
        timerId = null
    }
}

/** 连击中断：超时未完成下一次，或手脱离画面 */
function breakCombo() {
    if (comboBreakTimer) {
        clearTimeout(comboBreakTimer)
        comboBreakTimer = null
    }
    if (combo.value >= 2) {
        showComboBreak.value = true
        if (comboBreakHideTimer) clearTimeout(comboBreakHideTimer)
        comboBreakHideTimer = setTimeout(() => {
            showComboBreak.value = false
        }, 1200)
    }
    combo.value = 0
}

function armComboWindow() {
    if (comboBreakTimer) clearTimeout(comboBreakTimer)
    comboBreakTimer = setTimeout(() => {
        comboBreakTimer = null
        if (sessionPlaying.value && !sessionFinished.value) breakCombo()
    }, COMBO_WINDOW_MS)
}

function celebrate(kind: 'combo' | 'progress' | 'finish', text: string, durationMs = 1400) {
    if (sessionEnding.value || sessionFinished.value) return
    celebrateKind.value = kind
    celebrateText.value = text
    celebrateKey.value += 1
    celebrateVisible.value = true
    if (celebrateTimer) clearTimeout(celebrateTimer)
    celebrateTimer = setTimeout(() => {
        celebrateVisible.value = false
    }, durationMs)
}

/**
 * 粒子策略：
 * - 普通次数：不播（减卡顿）
 * - 连击 ≥3 且命中里程碑才播
 * - 前段 egg → 中段 walnut/can → 高潮 balloon → 末段停播/极简
 */
function pickCrushEffect(reps: number, currentCombo: number, target: number): { type: CrushType; intensity: number } | null {
    if (reps >= target - 2) return null // 最后 2 次全力保流畅
    if (currentCombo < 3) return null

    const isMilestone =
        currentCombo === 3
        || currentCombo === 5
        || currentCombo === 8
        || currentCombo === 10
        || currentCombo === 15
        || currentCombo === 20
        || (currentCombo >= 12 && currentCombo % 5 === 0)
    if (!isMilestone) return null

    // 两次粒子至少隔 900ms，避免连发叠帧
    const now = Date.now()
    if (now - lastCrushTs < 900) return null

    const progress = reps / target
    if (progress < 0.35) {
        return { type: 'egg', intensity: 0.75 }
    }
    if (progress < 0.6) {
        return { type: currentCombo >= 8 ? 'can' : 'walnut', intensity: 0.9 }
    }
    if (progress < 0.8) {
        // 高潮：高连击用气球彩纸，否则易拉罐
        return { type: currentCombo >= 10 ? 'balloon' : 'can', intensity: 1 }
    }
    // 接近尾声：只保留轻量 egg，且要求更高连击
    if (currentCombo >= 10 && currentCombo % 5 === 0) {
        return { type: 'egg', intensity: 0.55 }
    }
    return null
}

function triggerCrushIfNeeded(reps: number, currentCombo: number) {
    const picked = pickCrushEffect(reps, currentCombo, targetReps)
    if (!picked) return
    lastCrushTs = Date.now()
    if (!crushEnabled.value) crushEnabled.value = true
    crushSignal.value = {
        type: picked.type,
        intensity: picked.intensity,
        nonce: Date.now(),
    }
}

function onRepCompleted() {
    if (sessionEnding.value || sessionFinished.value || !sessionPlaying.value) return

    const now = Date.now()
    attemptReps.value += 1
    validReps.value += 1

    if (lastRepTs && now - lastRepTs <= COMBO_WINDOW_MS) {
        combo.value += 1
    } else {
        combo.value = 1
    }
    lastRepTs = now
    comboKey.value += 1
    showComboBreak.value = false
    armComboWindow()

    bestCombo.value = Math.max(bestCombo.value, combo.value)
    lastReward.value = 1
    showReward.value = true
    showHitFeedback.value = true

    // 粒子：连击里程碑 + 进度换款，非每次都播
    triggerCrushIfNeeded(validReps.value, combo.value)

    if (validReps.value >= targetReps - 2) {
        pipeline.setNearGoal(true)
    }

    if (rewardTimer) clearTimeout(rewardTimer)
    rewardTimer = setTimeout(() => {
        showReward.value = false
    }, 480)

    if (validReps.value >= targetReps) {
        setTimeout(() => finishSession('goal'), 0)
        return
    }

    if (combo.value >= 5 && combo.value % 5 === 0) {
        celebrate('combo', `连击 x${combo.value}！太棒了！`)
    } else if (validReps.value === Math.ceil(targetReps / 2)) {
        celebrate('progress', `已完成一半，继续加油！`)
    }
}

function onTogglePause() {
    if (isLeaving) return
    if (sessionFinished.value) {
        onRestart()
        return
    }
    if (!sessionPlaying.value) {
        startCameraSession()
        return
    }
    sessionPaused.value = !sessionPaused.value
    try {
        pipeline.setPaused(sessionPaused.value)
    } catch (_) {}
    if (sessionPaused.value) trackingQuality.value = 'weak'
}

function onEndTraining() {
    if (isLeaving) return
    // 强制结束也要跳出当前事件栈，避免与帧回调打架
    setTimeout(() => finishSession('manual'), 0)
}

function clearFeedbackTimers() {
    if (rewardTimer) {
        clearTimeout(rewardTimer)
        rewardTimer = null
    }
    if (comboBreakTimer) {
        clearTimeout(comboBreakTimer)
        comboBreakTimer = null
    }
    if (comboBreakHideTimer) {
        clearTimeout(comboBreakHideTimer)
        comboBreakHideTimer = null
    }
    if (celebrateTimer) {
        clearTimeout(celebrateTimer)
        celebrateTimer = null
    }
    if (teardownTimer) {
        clearTimeout(teardownTimer)
        teardownTimer = null
    }
}

function finishSession(reason: 'goal' | 'time' | 'manual') {
    if (sessionFinished.value && endModalShown) return
    if (sessionEnding.value && sessionFinished.value) return

    sessionEnding.value = true
    sessionPlaying.value = false
    sessionPaused.value = false
    stopTimer()
    clearFeedbackTimers()
    celebrateVisible.value = false
    showReward.value = false
    showHitFeedback.value = false
    showComboBreak.value = false
    crushSignal.value = null
    celebrateVisible.value = false
    showReward.value = false
    showHitFeedback.value = false
    showComboBreak.value = false
    lastRepTs = 0
    lastCrushTs = 0
    gesturePhase.value = 'UNTRACKED'
    energyLevel.value = 0
    pipelineSource.value = 'none'
    trackingQuality.value = 'lost'

    // 1) 先停识别（仍保留 camera 节点，避开原生销毁死锁）
    try {
        pipeline.setNearGoal(false)
        pipeline.stop()
    } catch (_) {}

    sessionFinished.value = true
    sessionEnding.value = false

    const title = reason === 'goal' ? '目标完成' : reason === 'time' ? '时间到' : '训练结束'
    const content = `有效握紧 ${validReps.value} 次 · 准确率 ${accuracyText.value} · 最高连击 x${bestCombo.value}`

    // 2) 延后拆相机 + 弹窗，确保已离开帧回调栈
    teardownTimer = setTimeout(() => {
        teardownTimer = null
        cameraReady.value = false
        cameraInited.value = false
        try {
            uni.setKeepScreenOn({ keepScreenOn: false })
        } catch (_) {}

        if (endModalShown) return
        endModalShown = true
        uni.showModal({
            title,
            content,
            confirmText: '再练一次',
            cancelText: '返回',
            success: (res) => {
                endModalShown = false
                if (res.confirm) onRestart()
                else onBack()
            },
            fail: () => {
                endModalShown = false
            },
        })
    }, 120)
}

function onRestart() {
    if (isLeaving) return
    endModalShown = false
    sessionEnding.value = false
    sessionFinished.value = false
    validReps.value = 0
    attemptReps.value = 0
    combo.value = 0
    bestCombo.value = 0
    elapsedSeconds.value = 0
    energyLevel.value = 0
    gesturePhase.value = 'UNTRACKED'
    showReward.value = false
    showHitFeedback.value = false
    showComboBreak.value = false
    celebrateVisible.value = false
    crushSignal.value = null
    crushEnabled.value = false
    lastCrushTs = 0
    lastRepTs = 0
    cameraError.value = ''
    stageMode.value = 'anim'
    try {
        pipeline.setNearGoal(false)
    } catch (_) {}
    startCameraSession()
}

function leavePage() {
    if (isLeaving) return
    isLeaving = true
    // 先停识别，但不要在跳转前强拆页面；完整清理交给 onUnload
    stopTimer()
    pipeline.stop()
    try {
        uni.setKeepScreenOn({ keepScreenOn: false })
    } catch (_) {}
    sessionPlaying.value = false
    sessionPaused.value = false

    uni.navigateBack({
        fail: () => {
            // 无上一页时回首页，避免 redirectTo 子包页与销毁竞态导致 pageId 错乱
            uni.reLaunch({
                url: '/pages/index/index',
                fail: () => {
                    isLeaving = false
                },
            })
        },
    })
}

function onBack() {
    if (isLeaving) return
    if (sessionPlaying.value && validReps.value > 0 && !sessionFinished.value) {
        uni.showModal({
            title: '退出训练？',
            content: '当前为视觉识别试验进度，不会写入旧版握力环页。',
            success: (res) => {
                if (res.confirm) leavePage()
            },
        })
        return
    }
    leavePage()
}

function onCameraError(e: any) {
    cameraError.value = e?.detail?.errMsg || '摄像头不可用'
    cameraReady.value = false
    trackingQuality.value = 'lost'
    pipeline.stop()
}

function cleanup() {
    if (cleanedUp) return
    cleanedUp = true
    sessionEnding.value = true
    stopTimer()
    pipeline.stop()
    cameraReady.value = false
    clearFeedbackTimers()
    try {
        uni.setKeepScreenOn({ keepScreenOn: false })
    } catch (_) {}
    sessionPlaying.value = false
    sessionPaused.value = false
    sessionEnding.value = false
}

onLoad(() => {
    initNavMetrics()
    isLeaving = false
    cleanedUp = false
})

// 仅在真正离开页面时清理；onHide 会在弹窗/短暂切后台时误伤会话
onUnload(cleanup)
onBeforeUnmount(cleanup)
</script>

<style lang="scss" scoped>
$bg: #f8fafc;
$card: #ffffff;
$text: #0f172a;
$muted: #64748b;
$brand: #14b8a6;
$brand-deep: #0f766e;
$green: #22b85a;
$orange: #ff6b21;
$danger: #e83e3e;

.page {
    min-height: 100vh;
    background: linear-gradient(180deg, #f0fdfa 0%, $bg 28%, $bg 100%);
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
}

.nav {
    background: #ffffff;
}

.nav-row {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 12rpx 0 8rpx;
}

.nav-back {
    position: relative;
    z-index: 2;
    width: 72rpx;
    height: 72rpx;
    display: flex;
    align-items: center;
    justify-content: center;
}

.nav-back-icon {
    font-size: 48rpx;
    line-height: 1;
    color: #181818;
    font-weight: 500;
}

.nav-center {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

.nav-title {
    font-size: 32rpx;
    font-weight: 600;
    color: #181818;
}

.nav-status {
    margin-top: 4rpx;
    display: flex;
    align-items: center;
    gap: 8rpx;
}

.nav-dot {
    width: 12rpx;
    height: 12rpx;
    border-radius: 50%;
    background: #94a3b8;

    &.good {
        background: #07c160;
    }
    &.weak {
        background: #f59e0b;
    }
    &.lost {
        background: #dc2626;
    }
}

.nav-status-text {
    font-size: 22rpx;
    color: #888888;
}

.nav-capsule-spacer {
    position: relative;
    z-index: 2;
    flex-shrink: 0;
}

.metrics-wrap {
    padding: 16rpx 32rpx 12rpx;
}

.metrics {
    background: $card;
    border-radius: 24rpx;
    box-shadow: 0 8rpx 28rpx rgba(15, 23, 42, 0.08);
    display: flex;
    align-items: center;
    min-height: 120rpx;
    padding: 20rpx 12rpx;
}

.metric {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8rpx;
}

.metric-value {
    font-size: 36rpx;
    font-weight: 800;
    color: $text;
    line-height: 1.1;
}

.metric-value--combo {
    font-size: 28rpx;
    color: $orange;
}

.metric-label {
    font-size: 20rpx;
    color: $muted;
}

.stage {
    position: relative;
    flex: 1;
    margin: 8rpx 0 0;
    min-height: 720rpx;
    background: #f3faf9;
    overflow: hidden;
}

.stage-camera,
.stage-fallback {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.stage-fallback {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 24rpx;
    background: linear-gradient(180deg, #e8faf8 0%, #f7fbfa 100%);
}

.stage-fallback-text {
    font-size: 26rpx;
    color: $muted;
}

.stage-fallback-btn {
    padding: 18rpx 36rpx;
    border-radius: 999rpx;
    background: $brand;
}

.stage-fallback-btn-text {
    color: #fff;
    font-size: 28rpx;
    font-weight: 700;
}

.stage-wash {
    position: absolute;
    inset: 0;
    z-index: 10;
    pointer-events: none;
    background: linear-gradient(
        180deg,
        rgba(248, 250, 252, 0.88) 0%,
        rgba(240, 253, 250, 0.28) 22%,
        rgba(255, 255, 255, 0.12) 55%,
        rgba(248, 250, 252, 0.9) 100%
    );
}

.stage-hud {
    position: absolute;
    inset: 0;
    z-index: 30;
    pointer-events: none;
}

.hit-title {
    position: absolute;
    top: 30rpx;
    left: 0;
    right: 0;
    text-align: center;
    font-size: 56rpx;
    font-weight: 900;
    letter-spacing: 4rpx;
    color: $brand-deep;
    text-shadow:
        0 0 8rpx #ffffff,
        0 3rpx 0 rgba(255, 255, 255, 0.9),
        0 8rpx 20rpx rgba(15, 118, 110, 0.25);

    &.active {
        color: #17b357;
        text-shadow:
            0 0 8rpx #ffffff,
            0 3rpx 0 rgba(255, 255, 255, 0.9),
            0 8rpx 22rpx rgba(23, 179, 87, 0.35);
    }
}

/* ===== 悬浮连击胶囊（仿视频：橙色渐变 + 速度线） ===== */
.combo-float {
    position: absolute;
    top: 118rpx;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    align-items: center;
    animation: combo-pop 300ms cubic-bezier(0.18, 0.89, 0.32, 1.4);
}

@keyframes combo-pop {
    0% {
        transform: translateX(-50%) scale(0.6);
        opacity: 0;
    }
    100% {
        transform: translateX(-50%) scale(1);
        opacity: 1;
    }
}

.combo-pill {
    padding: 10rpx 30rpx 12rpx;
    border-radius: 999rpx;
    background: linear-gradient(95deg, #ff5a1f 0%, #ff9e2c 70%, #ffc93c 100%);
    box-shadow: 0 10rpx 26rpx rgba(255, 199, 44, 0.45), 0 4rpx 10rpx rgba(255, 90, 31, 0.35);
}

.combo-pill-text {
    color: #fff;
    font-size: 30rpx;
    font-weight: 900;
    font-style: italic;
    text-shadow: 0 2rpx 4rpx rgba(200, 60, 0, 0.35);
}

/* 连击时限条：时限内完成下一次才算连击 */
.combo-drain {
    margin-top: 6rpx;
    height: 5rpx;
    border-radius: 999rpx;
    background: rgba(255, 255, 255, 0.35);
    overflow: hidden;
}

.combo-drain-fill {
    height: 100%;
    border-radius: 999rpx;
    background: #ffffff;
    animation: combo-drain 6s linear forwards;
}

@keyframes combo-drain {
    from {
        width: 100%;
    }
    to {
        width: 0%;
    }
}

/* 速度线：胶囊右侧的冲刺短线 */
.speed-line {
    height: 8rpx;
    border-radius: 999rpx;
    background: linear-gradient(90deg, #ffc93c 0%, rgba(255, 201, 60, 0) 100%);
    margin-left: 12rpx;
}

.speed-line--1 {
    width: 44rpx;
    transform: translateY(-12rpx);
}
.speed-line--2 {
    width: 64rpx;
    margin-left: -52rpx;
    transform: translateY(4rpx);
}
.speed-line--3 {
    width: 36rpx;
    transform: translateY(18rpx);
    margin-left: -48rpx;
}

/* ===== 左上角飘带（仿视频：绿色斜标） ===== */
.combo-ribbon {
    position: absolute;
    left: -14rpx;
    top: 40rpx;
    padding: 14rpx 34rpx 14rpx 26rpx;
    background: linear-gradient(120deg, #17b357 0%, #35d97b 100%);
    border-radius: 10rpx 18rpx 18rpx 10rpx;
    transform: rotate(-8deg);
    box-shadow: 0 10rpx 24rpx rgba(23, 179, 87, 0.35);
    animation: ribbon-in 380ms cubic-bezier(0.18, 0.89, 0.32, 1.3);
}

@keyframes ribbon-in {
    0% {
        transform: rotate(-8deg) translateX(-120rpx);
        opacity: 0;
    }
    100% {
        transform: rotate(-8deg) translateX(0);
        opacity: 1;
    }
}

.combo-ribbon-line {
    display: block;
    color: #fff;
    font-size: 30rpx;
    font-weight: 900;
    font-style: italic;
    text-shadow: 0 2rpx 4rpx rgba(9, 92, 45, 0.4);
}

.combo-ribbon-sub {
    display: block;
    margin-top: 2rpx;
    color: #eafff2;
    font-size: 22rpx;
    font-weight: 800;
    font-style: italic;
}

.combo-break {
    position: absolute;
    top: 118rpx;
    left: 50%;
    transform: translateX(-50%);
    padding: 8rpx 24rpx;
    border-radius: 999rpx;
    background: rgba(100, 116, 139, 0.85);
}

.combo-break-text {
    color: #fff;
    font-size: 24rpx;
    font-weight: 700;
}

/* ===== +N 爆发（仿视频：橙色星芒） ===== */
.reward-wrap {
    position: absolute;
    right: 24rpx;
    top: 22%;
    width: 220rpx;
    height: 220rpx;
    animation: reward-pop 640ms cubic-bezier(0.18, 0.89, 0.32, 1.35);
}

@keyframes reward-pop {
    0% {
        transform: scale(0.4) rotate(-8deg);
        opacity: 0;
    }
    45% {
        transform: scale(1.12) rotate(2deg);
        opacity: 1;
    }
    100% {
        transform: scale(1) rotate(0deg);
    }
}

.reward-glow {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background: radial-gradient(
        circle,
        rgba(255, 236, 170, 0.95) 0%,
        rgba(255, 196, 66, 0.55) 42%,
        rgba(255, 170, 40, 0) 70%
    );
}

.reward-burst {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%) rotate(-6deg);
    font-size: 84rpx;
    font-weight: 900;
    font-style: italic;
    color: #ff8a1e;
    text-shadow: 0 0 6rpx #fff7e0, 0 6rpx 18rpx rgba(255, 138, 30, 0.5);
}

/* ===== 动画演示层（纯代码绘制，默认显示，可切实景） ===== */
.stage-anim {
    position: absolute;
    inset: 0;
    z-index: 8;
    background: linear-gradient(180deg, #e8f4fb 0%, #f0fbf7 55%, #e9f7f3 100%);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ambient-glow {
    position: absolute;
    width: 1100rpx;
    height: 1100rpx;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: radial-gradient(
        circle,
        rgba(204, 251, 241, 0.9) 0%,
        rgba(240, 253, 250, 0.5) 32%,
        transparent 70%
    );
    pointer-events: none;
}

/* ===== 移植自旧握力环页的圆形视频舞台（手势驱动） ===== */
.ring-container {
    position: relative;
    width: 640rpx;
    height: 640rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1;
}

/* 节拍光晕：随握力实时收缩变亮 */
.beat-halo {
    position: absolute;
    width: 640rpx;
    height: 640rpx;
    border-radius: 50%;
    background: radial-gradient(
        circle,
        rgba(20, 184, 166, 0.45) 0%,
        rgba(45, 212, 191, 0.18) 38%,
        rgba(20, 184, 166, 0) 70%
    );
    opacity: 0.35;
    transform: scale(1);
    transition: transform 140ms ease-out, opacity 140ms linear;
    will-change: transform, opacity;
}

/* 圆形示范视频主体 */
.video-ring {
    position: relative;
    width: 560rpx;
    height: 560rpx;
    border-radius: 50%;
    overflow: hidden;
    box-shadow:
        0 20rpx 56rpx rgba(20, 184, 166, 0.35),
        0 10rpx 28rpx rgba(45, 212, 191, 0.22);
    transform: scale(1);
    transition: transform 140ms ease-out;
    will-change: transform;
    z-index: 1;
}

.demo-video {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    background: #e2f5f1;
}

.video-ring-border {
    position: absolute;
    inset: 0;
    border-radius: 50%;
    pointer-events: none;
    box-shadow:
        inset 0 0 0 4rpx rgba(255, 255, 255, 0.6),
        inset 0 8rpx 24rpx rgba(15, 118, 110, 0.25);
    transition: box-shadow 200ms linear;
}

/* ---- 握力驱动：环随压缩收缩、光晕增强 ---- */
.sq-1 .video-ring { transform: scale(0.985); }
.sq-2 .video-ring { transform: scale(0.965); }
.sq-3 .video-ring { transform: scale(0.945); }
.sq-4 .video-ring { transform: scale(0.92); }

.sq-1 .beat-halo { opacity: 0.5; transform: scale(0.97); }
.sq-2 .beat-halo { opacity: 0.65; transform: scale(0.94); }
.sq-3 .beat-halo { opacity: 0.8; transform: scale(0.9); }
.sq-4 .beat-halo {
    opacity: 1;
    transform: scale(0.86);
    background: radial-gradient(
        circle,
        rgba(34, 184, 90, 0.5) 0%,
        rgba(74, 222, 128, 0.22) 38%,
        rgba(34, 184, 90, 0) 70%
    );
}

.sq-4 .video-ring-border {
    box-shadow:
        inset 0 0 0 6rpx rgba(74, 222, 128, 0.85),
        inset 0 8rpx 24rpx rgba(15, 118, 110, 0.25);
}

/* ---- 未捕捉到手：舞台自动循环示范（沿用旧页 beat-pulse 节奏） ---- */
.idle .beat-halo {
    opacity: 1;
    animation: beat-pulse 1900ms ease-in-out infinite;
}

.idle .video-ring {
    animation: video-squeeze 1900ms ease-in-out infinite;
}

@keyframes beat-pulse {
    0%, 100% {
        transform: scale(1);
        opacity: 0.4;
    }
    50% {
        transform: scale(0.86);
        opacity: 0.95;
    }
}

@keyframes video-squeeze {
    0%, 100% {
        transform: scale(1);
    }
    50% {
        transform: scale(0.95);
    }
}

/* ===== 画布四角捕捉框 + 状态 ===== */
.capture-frame {
    position: absolute;
    inset: 20rpx;
    z-index: 20;
    pointer-events: none;
}

.corner {
    position: absolute;
    width: 56rpx;
    height: 56rpx;
    border: 8rpx solid #94a3b8;
    transition: border-color 200ms linear;
}

.corner--tl {
    left: 0;
    top: 0;
    border-right: none;
    border-bottom: none;
    border-top-left-radius: 20rpx;
}
.corner--tr {
    right: 0;
    top: 0;
    border-left: none;
    border-bottom: none;
    border-top-right-radius: 20rpx;
}
.corner--bl {
    left: 0;
    bottom: 0;
    border-right: none;
    border-top: none;
    border-bottom-left-radius: 20rpx;
}
.corner--br {
    right: 0;
    bottom: 0;
    border-left: none;
    border-top: none;
    border-bottom-right-radius: 20rpx;
}

/* 捕捉状态驱动四角颜色 */
.cap-lost .corner {
    border-color: #dc2626;
}
.cap-seek .corner {
    border-color: #94a3b8;
}
.cap-open .corner {
    border-color: $brand;
}
.cap-grip .corner,
.cap-hold .corner {
    border-color: $green;
}
.cap-release .corner {
    border-color: #f59e0b;
}

.capture-pill {
    position: absolute;
    left: 50%;
    bottom: 36rpx;
    transform: translateX(-50%);
    z-index: 25;
    display: flex;
    align-items: center;
    gap: 12rpx;
    padding: 12rpx 30rpx;
    border-radius: 999rpx;
    background: rgba(15, 23, 42, 0.55);
}

.capture-dot {
    width: 16rpx;
    height: 16rpx;
    border-radius: 50%;
    background: #94a3b8;
}

.cap-lost .capture-dot {
    background: #dc2626;
}
.cap-open .capture-dot {
    background: #25d9d0;
}
.cap-grip .capture-dot,
.cap-hold .capture-dot {
    background: #22b85a;
}
.cap-release .capture-dot {
    background: #f59e0b;
}

.capture-pill-text {
    color: #fff;
    font-size: 28rpx;
    font-weight: 700;
}

.mode-toggle {
    position: absolute;
    right: 16rpx;
    top: 12rpx;
    z-index: 45;
    padding: 10rpx 22rpx;
    border-radius: 999rpx;
    background: rgba(15, 23, 42, 0.45);
}

.mode-toggle-text {
    color: #fff;
    font-size: 22rpx;
    font-weight: 600;
}

/* ===== 庆祝层：横幅 + 撒花 ===== */
.celebrate {
    position: absolute;
    inset: 0;
    z-index: 50;
    pointer-events: none;
    overflow: hidden;
}

.celebrate-banner {
    position: absolute;
    left: 50%;
    top: 38%;
    transform: translateX(-50%);
    padding: 20rpx 48rpx;
    border-radius: 999rpx;
    background: linear-gradient(90deg, #ff6a2d 0%, #ffb52d 100%);
    box-shadow: 0 12rpx 36rpx rgba(255, 107, 33, 0.4);
    animation: banner-in 420ms cubic-bezier(0.18, 0.89, 0.32, 1.28);
    white-space: nowrap;
}

.celebrate.finish .celebrate-banner {
    background: linear-gradient(90deg, #16a34a 0%, #4ade80 100%);
    box-shadow: 0 12rpx 36rpx rgba(22, 163, 74, 0.4);
}

@keyframes banner-in {
    0% {
        transform: translateX(-50%) scale(0.5);
        opacity: 0;
    }
    100% {
        transform: translateX(-50%) scale(1);
        opacity: 1;
    }
}

.celebrate-banner-text {
    color: #fff;
    font-size: 40rpx;
    font-weight: 900;
}

.confetti {
    position: absolute;
    top: -30rpx;
    width: 18rpx;
    height: 30rpx;
    border-radius: 4rpx;
    opacity: 0;
    animation: confetti-fall 1.6s ease-in forwards;
}

@keyframes confetti-fall {
    0% {
        transform: translateY(0) rotate(0deg);
        opacity: 1;
    }
    100% {
        transform: translateY(120vh) rotate(540deg);
        opacity: 0.6;
    }
}

/* 14 片彩纸：错开位置 / 颜色 / 延迟 */
.confetti-1 { left: 6%; background: #ff6a2d; animation-delay: 0ms; }
.confetti-2 { left: 13%; background: #22b85a; animation-delay: 120ms; }
.confetti-3 { left: 20%; background: #25d9d0; animation-delay: 60ms; }
.confetti-4 { left: 27%; background: #ffb52d; animation-delay: 200ms; }
.confetti-5 { left: 34%; background: #f472b6; animation-delay: 30ms; }
.confetti-6 { left: 41%; background: #22b85a; animation-delay: 160ms; }
.confetti-7 { left: 48%; background: #ff6a2d; animation-delay: 90ms; }
.confetti-8 { left: 55%; background: #25d9d0; animation-delay: 240ms; }
.confetti-9 { left: 62%; background: #ffb52d; animation-delay: 50ms; }
.confetti-10 { left: 69%; background: #f472b6; animation-delay: 180ms; }
.confetti-11 { left: 76%; background: #22b85a; animation-delay: 110ms; }
.confetti-12 { left: 83%; background: #ff6a2d; animation-delay: 260ms; }
.confetti-13 { left: 90%; background: #25d9d0; animation-delay: 140ms; }
.confetti-14 { left: 96%; background: #ffb52d; animation-delay: 20ms; }

.footer {
    position: relative;
    z-index: 100;
    background: #fff;
    padding: 24rpx 36rpx calc(24rpx + env(safe-area-inset-bottom));
}

.goal-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.goal-text {
    font-size: 28rpx;
    font-weight: 800;
    color: $text;
}

.remain-text {
    font-size: 26rpx;
    font-weight: 700;
    color: $brand-deep;
}

.goal-track {
    margin-top: 16rpx;
    height: 14rpx;
    border-radius: 999rpx;
    background: #e2e8f0;
    overflow: hidden;
}

.goal-fill {
    height: 100%;
    border-radius: 999rpx;
    background: linear-gradient(90deg, #42c7c6 0%, #54c9c2 100%);
}

.actions {
    margin-top: 28rpx;
    display: flex;
    gap: 20rpx;
}

.action {
    flex: 1;
    height: 96rpx;
    border-radius: 20rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12rpx;
    box-sizing: border-box;
}

.action--pause {
    background: #f0fdfa;
    border: 2rpx solid $brand;
}

.action--end {
    background: #fef2f2;
    border: 2rpx solid #f28b8b;
}

.action-icon {
    font-size: 24rpx;
}

.action--pause .action-icon,
.action--pause .action-text {
    color: $brand-deep;
    font-weight: 800;
}

.action--end .action-icon,
.action--end .action-text {
    color: $danger;
    font-weight: 800;
}

.action-text {
    font-size: 30rpx;
}
</style>
