Files
smt/src/pages/index/index.vue
T

3222 lines
78 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :class="{ 'page-record': !isQuitMode }">
<!-- 背景装饰 -->
<view v-if="isQuitMode" class="bg-gradient"></view>
<view v-if="isQuitMode" class="bg-pattern"></view>
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton-wrap">
<view class="sk-circle"></view>
<view class="sk-row"></view>
<view class="sk-row sk-row-short"></view>
<view class="sk-card"></view>
</view>
<view v-else class="content">
<!-- 戒烟模式 -->
<view v-if="isQuitMode" class="bio-home" :style="hpVisualStyle">
<view class="bio-status-card">
<view class="bio-breath-line"></view>
<view class="bio-profile-row">
<view class="bio-avatar"><text>{{ bioAvatarText }}</text></view>
<view class="bio-profile-main">
<text class="bio-rank">{{ bioRankText }} <text class="bio-level">{{ bioLevelText }}</text></text>
<text class="bio-subtitle">冷静恢复中 · {{ todayChecked ? '今日已确认' : '等待今日确认' }}</text>
</view>
<view class="bio-coin-pill">
<text class="bio-coin-icon">💰</text>
<text class="bio-coin-value">×{{ savedMoney }}</text>
</view>
</view>
<view class="bio-hp-box">
<view class="bio-hp-track">
<view class="bio-hp-fill" :style="bioHpBarStyle"></view>
<view class="bio-hp-clear"></view>
</view>
<view class="bio-hp-meta">
<text>HP: {{ hpValue }}%</text>
<text> </text>
</view>
</view>
<view class="bio-buff-pill">
<text class="bio-buff-star"></text>
<text>{{ bioBuffText }}</text>
</view>
</view>
<view class="bio-console-card" @tap="handleQuitCheckin">
<view class="bio-console-orbit">
<view class="bio-console-core" :class="{ 'bio-console-core-done': todayChecked }">
<text>{{ todayChecked ? '✓' : '◐' }}</text>
</view>
</view>
<view class="bio-console-copy">
<text class="bio-console-kicker">CORE CONSOLE</text>
<text class="bio-console-title">{{ todayChecked ? '今天的清澈已记录' : '确认今天没有抽烟' }}</text>
<text class="bio-console-desc">{{ todayChecked ? `${todayCheckinTime} 已完成打卡,继续维持身体修复节奏。` : '轻点完成今日确认,用一次温和反馈替代刺激奖励。' }}</text>
</view>
<view class="bio-console-action">
<text>{{ todayChecked ? '已完成' : '打卡' }}</text>
</view>
</view>
<view class="bio-health-card">
<view class="bio-section-head">
<view>
<text class="bio-section-title">健康仪表盘</text>
<text class="bio-section-subtitle">身体正在把每一次选择转化为回收值</text>
</view>
<text class="bio-section-chip">{{ nextHealthMilestoneText }}</text>
</view>
<view class="bio-health-body">
<view class="bio-health-ring" :style="hpGaugeStyle">
<view class="bio-health-ring-inner">
<text class="bio-health-ring-value">{{ hpValue }}%</text>
<text class="bio-health-ring-label">健康指数</text>
</view>
</view>
<view class="bio-health-metrics">
<view v-for="item in bioHealthItems" :key="item.name" class="bio-health-row">
<text class="bio-health-name">{{ item.name }}</text>
<text class="bio-health-value">{{ item.value }}</text>
</view>
<view class="bio-health-divider"></view>
<view class="bio-life-row">
<text class="bio-life-value">+{{ lifeSaved }} 小时</text>
<text class="bio-life-label">生命已回收</text>
</view>
</view>
</view>
</view>
<view class="bio-trend-card">
<view class="bio-section-head bio-section-head-compact">
<text class="bio-section-title">7 天趋势</text>
<text class="bio-section-chip">{{ lungMomentumText }}</text>
</view>
<view class="bio-trend-chart">
<view v-for="item in bioTrendItems" :key="item.label" class="bio-trend-col">
<view class="bio-trend-line" :style="{ height: item.height }">
<view class="bio-trend-dot" :class="{ 'bio-trend-dot-active': item.active }"></view>
</view>
<text class="bio-trend-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="bio-action-grid">
<view class="bio-action-card" @tap="openSmokeDialog">
<text class="bio-action-title">没扛住</text>
<text class="bio-action-desc">记录诱因比责备自己更有用</text>
</view>
<view class="bio-action-card" @tap="gotoDreamGoals">
<text class="bio-action-title">梦想清单</text>
<text class="bio-action-desc">{{ activeGoalText }}</text>
</view>
</view>
<view class="bio-tip-card">
<text class="bio-tip-icon"></text>
<text class="bio-tip-text">{{ healthTip }}</text>
</view>
</view>
<!-- 记录模式 -->
<view v-else class="record-bio-home">
<view class="record-bio-status-card">
<view class="record-bio-breath-line"></view>
<view class="record-bio-profile-row">
<view class="record-bio-avatar-medal">
<image class="record-bio-avatar-bg" src="/static/achievements/theme-medallion.png" mode="aspectFill" />
<text class="record-bio-avatar-icon">{{ recordTitle.icon }}</text>
</view>
<view class="record-bio-profile-main">
<text class="record-bio-rank">{{ recordTitle.name }} <text class="record-bio-level">{{ recordTitle.level }}</text></text>
<text class="record-bio-subtitle">{{ recordTitle.description }}</text>
</view>
<view class="record-bio-target-pill">
<text class="record-bio-target-icon"></text>
<text class="record-bio-target-value"> {{ recordReducedCigs }} </text>
</view>
</view>
<view class="record-bio-title-panel">
<view class="record-bio-title-copy">
<text class="record-bio-title-kicker">REDUCTION TITLE</text>
<text class="record-bio-title-name">{{ recordTitle.badge }}</text>
</view>
<text class="record-bio-title-hint">{{ recordNextTitleHint }}</text>
</view>
<view class="record-bio-hp-box">
<view class="record-bio-hp-track">
<view class="record-bio-hp-fill" :style="{ width: recordControlScore + '%' }"></view>
<view class="record-bio-hp-clear"></view>
</view>
<view class="record-bio-hp-meta">
<text>控制率: {{ recordControlScore }}%</text>
<text>刺激 清醒</text>
</view>
</view>
<view class="record-bio-buff-pill">
<text class="record-bio-buff-star"></text>
<text>{{ recordBioBuffText }}</text>
</view>
</view>
<view class="record-bio-console-card">
<view class="record-bio-console-left">
<view class="record-bio-orbit" :style="todayCountRingStyle">
<view class="record-bio-orbit-inner">
<text class="record-bio-orbit-kicker">距上次</text>
<text class="record-bio-orbit-value">{{ timerDisplay }}</text>
<text class="record-bio-orbit-label">抽烟间隔</text>
</view>
</view>
</view>
<view class="record-bio-console-main">
<text class="record-bio-console-kicker">CONTROL CONSOLE</text>
<text class="record-bio-console-title">{{ recordHeroTitle }}</text>
<text class="record-bio-console-desc">{{ recordRhythmText }}</text>
<view class="record-bio-action-row">
<view class="record-bio-action record-bio-action-primary" :class="{ 'record-bio-action-disabled': quickSubmitting }" @tap="quickRecordSmoke">
<text class="record-bio-action-icon">+</text>
<text>快捷记录</text>
</view>
<view class="record-bio-action record-bio-action-ghost" @tap="openDetailedSmokeDialog">
<text class="record-bio-action-icon"></text>
<text>详细记录</text>
</view>
</view>
</view>
</view>
<view class="record-bio-health-card">
<view class="record-bio-section-head">
<view>
<text class="record-bio-section-title">控烟仪表盘</text>
<text class="record-bio-section-subtitle">用真实记录换取更清醒的下一次选择</text>
</view>
<text class="record-bio-section-chip">{{ reducePercent }}% 少抽</text>
</view>
<view class="record-bio-health-body">
<view class="record-bio-health-ring" :style="recordControlRingStyle">
<view class="record-bio-health-ring-inner">
<text class="record-bio-health-ring-value">{{ todayCount }}</text>
<text class="record-bio-health-ring-label">今日已抽</text>
</view>
</view>
<view class="record-bio-metrics">
<view v-for="item in recordBioHealthItems" :key="item.name" class="record-bio-metric-row">
<text class="record-bio-metric-name">{{ item.name }}</text>
<text class="record-bio-metric-value">{{ item.value }}</text>
</view>
</view>
</view>
</view>
<view class="record-bio-tip-card">
<text class="record-bio-tip-icon"></text>
<text class="record-bio-tip-text">{{ recordHealthTip }}</text>
</view>
</view>
<smoke-record-dialog
v-model:show="showDialog"
:type="dialogType"
:quick-mode="dialogQuickMode"
@submit="handleSubmit"
/>
</view>
<view v-if="checkinCelebrating" class="checkin-celebration-mask">
<view class="checkin-celebration-card">
<view class="checkin-celebration-burst">
<view
v-for="burst in celebrationBursts"
:key="burst.id"
class="checkin-celebration-ray"
:style="{ transform: `rotate(${burst.rotate}deg) translateY(-88rpx)` }"
></view>
</view>
<view class="checkin-celebration-icon"></view>
<text class="checkin-celebration-title">今日打卡完成</text>
<text class="checkin-celebration-desc">已连续无烟 {{ quitDays }} {{ lungPhaseLabel }}</text>
<view class="checkin-celebration-chip-row">
<view class="checkin-celebration-chip">
<text class="checkin-celebration-chip-label">肺部 HP</text>
<text class="checkin-celebration-chip-value">{{ hpValue }}</text>
</view>
<view class="checkin-celebration-chip">
<text class="checkin-celebration-chip-label">已省下</text>
<text class="checkin-celebration-chip-value">¥{{ savedMoney }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onShareAppMessage, onShow } from '@dcloudio/uni-app'
import * as api from '@/api'
import { useLogin } from '@/hooks/useLogin'
import { useProfileStore } from '@/stores/profile'
import { useUserStore } from '@/stores/user'
import { storage, QUIT_CHECKIN_KEY } from '@/utils/storage'
// formatDate / formatTime 来自公共工具,避免重复实现
import { formatDate, formatTime } from '@/utils/time'
const profileStore = useProfileStore()
const userStore = useUserStore()
const { waitForLogin } = useLogin()
// ---- 页面状态 ----
const loading = ref(true)
const pageReady = ref(false)
const navBarHeight = ref(0)
const showDialog = ref(false)
const dialogType = ref('smoke')
const dialogQuickMode = ref(false)
const quickSubmitting = ref(false)
// ---- 数据 ----
const homeData = ref(null) // 记录模式首页数据
const quitHomeData = ref(null) // 戒烟模式首页数据
const achievementData = ref(null) // 成就数据
const quitState = ref(defaultQuitState()) // 本地缓存的打卡状态(网络失败时兜底)
// ---- 打卡庆祝动画 ----
const checkinCelebrating = ref(false)
let checkinCelebrationTimer = null
// ---- 记录模式计时器:距上次抽烟的秒数 ----
let timerInterval = null
const timerBaseSeconds = ref(-1) // -1 表示从未记录过
const timerSeconds = ref(0) // 页面存活期间累计秒数
// ========== 基础计算 ==========
const isQuitMode = computed(() => userStore.mode === 'quit')
const homeSummary = computed(() => homeData.value?.summary || {})
const homeTimer = computed(() => homeData.value?.timer || {})
const homeMotivation = computed(() => homeData.value?.motivation || {})
const quitSummary = computed(() => quitHomeData.value?.summary || {})
const quitDailyStatus = computed(() => quitHomeData.value?.daily_status || {})
// 用户档案基础数据
const baselineCigsPerDay = computed(() => profileStore.profile?.baseline_cigs_per_day || 10)
const packPriceYuan = computed(() => (profileStore.profile?.pack_price_cent || 2500) / 100)
// ========== 记录模式 ==========
const todayCount = computed(() => homeSummary.value.today_count ?? 0)
const dailyTarget = computed(() => {
const target = homeSummary.value.daily_target
if (target != null) return target
return profileStore.profile?.baseline_cigs_per_day || 0
})
const recordReducedCigs = computed(() => Math.max(0, (dailyTarget.value || 0) - todayCount.value))
const changeText = computed(() => {
const reduced = homeSummary.value.reduced_from_yesterday
if (reduced == null) return '较昨日暂无对比'
if (reduced === 0) return '较昨日持平'
return homeSummary.value.exceeded_yesterday ? `较昨日多 ${reduced}` : `较昨日少 ${reduced}`
})
// 格式化计时器显示 HH:MM:SS
const timerDisplay = computed(() => {
if (timerBaseSeconds.value < 0) return '--:--:--'
const total = timerBaseSeconds.value + timerSeconds.value
const h = Math.floor(total / 3600)
const m = Math.floor((total % 3600) / 60)
const s = total % 60
return [h, m, s].map(n => String(n).padStart(2, '0')).join(':')
})
// 下次建议抽烟时间
const nextSmokeTimeText = computed(() => {
const timer = homeTimer.value
if (!timer) return ''
if (timer.next_suggested_clock) return timer.next_suggested_clock
if (!timer.next_suggested_at) return ''
const date = new Date(timer.next_suggested_at)
return Number.isNaN(date.getTime()) ? '' : formatTime(date)
})
// 今日进度环百分比(已抽/目标)
const todayCountPercent = computed(() => {
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 100 : 0
return Math.min(Math.max(Math.round((todayCount.value / dailyTarget.value) * 100), 0), 100)
})
const todayCountRingStyle = computed(() => {
const angle = Math.round(todayCountPercent.value * 3.6)
const accent = todayCount.value > dailyTarget.value && dailyTarget.value > 0 ? '#f97316' : '#1fbf8f'
return { background: `conic-gradient(${accent} 0deg ${angle}deg, rgba(226, 239, 233, 0.88) ${angle}deg 360deg)` }
})
const recordHasData = computed(() => todayCount.value > 0 || timerBaseSeconds.value >= 0)
const recordHeroTitle = computed(() => {
if (!recordHasData.value) return '先记录第一刻'
if (todayCount.value === 0) return '今天还没抽烟'
if (todayCount.value < dailyTarget.value) return '节奏控制不错'
if (todayCount.value === dailyTarget.value) return '已经到达目标线'
return '今天需要收一收'
})
const recordRhythmText = computed(() => {
if (!recordHasData.value) return '点击下方按钮开始记录,建立自己的控烟节奏。'
if (nextSmokeTimeText.value) return `建议下次 ${nextSmokeTimeText.value} 后再抽,给身体留一点恢复时间。`
return changeText.value
})
const reducePercent = computed(() => {
if (dailyTarget.value <= 0) return 0
return Math.round(recordReducedCigs.value / dailyTarget.value * 100)
})
const recordHealthTip = computed(() => {
if (homeMotivation.value.message) return homeMotivation.value.message
if (todayCount.value === 0) return '今天还没抽烟,继续保持!'
if (todayCount.value < dailyTarget.value) return `今天比目标少抽了${dailyTarget.value - todayCount.value}根,很棒!`
if (todayCount.value === dailyTarget.value) return '今天已达到目标,加油!'
return '今天超标了,明天继续努力'
})
const recordControlScore = computed(() => {
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 40 : 88
const score = Math.round((1 - Math.min(todayCount.value / dailyTarget.value, 1)) * 72) + 18
return Math.min(Math.max(score, todayCount.value > dailyTarget.value ? 12 : 18), 100)
})
const recordStatusLabel = computed(() => {
if (!recordHasData.value) return '等待第一条真实记录'
if (todayCount.value === 0) return '清醒保持中'
if (todayCount.value < dailyTarget.value) return '节奏稳定'
if (todayCount.value === dailyTarget.value) return '接近边界'
return '需要降速'
})
const recordBioBuffText = computed(() => nextSmokeTimeText.value ? `下一次建议 ${nextSmokeTimeText.value}` : '正在建立控烟节奏')
const recordControlRingStyle = computed(() => {
const angle = Math.round(recordControlScore.value * 3.6)
const accent = todayCount.value > dailyTarget.value && dailyTarget.value > 0 ? '#D97706' : '#67E8F9'
return { background: `conic-gradient(${accent} 0deg ${angle}deg, rgba(226, 232, 240, 0.78) ${angle}deg 360deg)` }
})
const recordYesterdayText = computed(() => {
const reduced = Number(homeSummary.value.reduced_from_yesterday)
if (Number.isNaN(reduced)) return '暂无对比'
if (reduced === 0) return '持平'
return homeSummary.value.exceeded_yesterday ? `${reduced}` : `${reduced}`
})
const recordBioHealthItems = computed(() => [
{ name: '今日目标', value: `${todayCount.value}/${dailyTarget.value || '-'}` },
{ name: '较昨日', value: recordYesterdayText.value },
{ name: '建议时间', value: nextSmokeTimeText.value || '--:--' }
])
const recordTitle = computed(() => {
const current = achievementData.value?.current
const metrics = achievementData.value?.metrics || {}
return {
icon: current?.icon || achievementData.value?.theme_icon || '◎',
name: current?.name || '节奏观察者',
level: achievementData.value?.score !== undefined ? `积分 ${achievementData.value.score}` : '积分 0',
badge: achievementData.value?.theme_name || '成就称号',
description: metrics.scored_days > 0
? `累计少抽 ${metrics.total_reduced_cigs || 0} 根,稳定记录 ${metrics.stable_days || 0} 天。`
: '完成记录后,会按少抽数量解锁称号。'
}
})
const recordNextTitleHint = computed(() => {
const next = achievementData.value?.next
if (!next) return achievementData.value?.current ? '已达当前最高称号' : '记录后开始进阶'
const score = Number(achievementData.value?.score) || 0
const required = Number(next.required_score ?? next.required_days) || 0
return `距「${next.name}」还差 ${Math.max(0, required - score)}`
})
// ========== 戒烟模式 ==========
// 服务端连续无烟天数优先,失败时从本地缓存推算
const quitDays = computed(() => {
if (quitSummary.value.current_streak_days !== undefined) return quitSummary.value.current_streak_days
if (!quitState.value.lastCheckinDate) return 0
// 超过 1 天未打卡则连续中断归零
const gap = diffDays(quitState.value.lastCheckinDate, formatDate(new Date()))
return gap > 1 ? 0 : Number(quitState.value.streakDays || 0)
})
const todayChecked = computed(() => {
if (quitDailyStatus.value.status) return quitDailyStatus.value.status === 'checked_in'
return quitState.value.lastCheckinDate === formatDate(new Date())
})
const todayCheckinTime = computed(() => {
const src = quitDailyStatus.value.checkin_at || quitState.value.lastCheckinAt
return formatClock(src)
})
const savedMoney = computed(() => {
if (quitSummary.value.saved_money_cent !== undefined) return Math.round(quitSummary.value.saved_money_cent / 100)
return Math.round((quitDays.value * baselineCigsPerDay.value / 20) * packPriceYuan.value)
})
const avoidedCigs = computed(() => quitSummary.value.avoided_cigs ?? quitDays.value * baselineCigsPerDay.value)
const lifeSaved = computed(() => Math.round(avoidedCigs.value * 11 / 60))
// HP 值:服务端优先,否则按天数本地估算
const healthProgress = computed(() => {
if (quitSummary.value.health_recovery_percent !== undefined) return quitSummary.value.health_recovery_percent
const d = quitDays.value
if (d >= 365) return 100; if (d >= 180) return 85; if (d >= 90) return 70
if (d >= 30) return 55; if (d >= 14) return 35; if (d >= 7) return 20
if (d >= 3) return 10; if (d >= 1) return 5; return 0
})
const hpValue = computed(() => {
const v = Number(quitSummary.value.hp_current ?? healthProgress.value)
return Number.isNaN(v) ? 0 : Math.min(Math.max(Math.round(v), 0), 100)
})
// 根据 HP 区间决定主题色和文案
const hpState = computed(() => {
const hp = hpValue.value
if (hp <= 20) return { label: '危险期', title: '先保住今天的底线', description: '身体恢复动力偏弱,先把眼前这一波烟瘾稳住最重要。', accent: '#64748b', soft: '#e2e8f0', deep: '#334155' }
if (hp <= 40) return { label: '恢复中', title: '状态正在往回拉', description: '已经开始脱离最低谷,再多守住几次关键时刻,HP 会明显回升。', accent: '#0f766e', soft: '#ccfbf1', deep: '#115e59' }
if (hp <= 60) return { label: '稳定期', title: '节奏开始站稳', description: '你已经进入可持续恢复阶段,规律打卡会让状态越来越稳。', accent: '#0891b2', soft: '#cffafe', deep: '#155e75' }
if (hp <= 80) return { label: '强韧期', title: '恢复势头很不错', description: '肺部状态正在加速恢复,继续把高风险场景提前处理掉。', accent: '#16a34a', soft: '#dcfce7', deep: '#166534' }
return { label: '高能期', title: '你已经进入高能状态', description: '当前状态非常好,保持日常节奏和打卡,就能继续巩固恢复成果。', accent: '#ea580c', soft: '#ffedd5', deep: '#9a3412' }
})
// CSS 变量注入(供卡片内动态主题色使用)
const hpVisualStyle = computed(() => ({
'--hp-accent': hpState.value.accent,
'--hp-soft': hpState.value.soft,
'--hp-deep': hpState.value.deep
}))
const hpGaugeStyle = computed(() => {
const angle = Math.round(hpValue.value * 3.6)
return { background: `conic-gradient(var(--hp-accent) 0deg ${angle}deg, rgba(226, 232, 240, 0.88) ${angle}deg 360deg)` }
})
const hpChangeText = computed(() => {
const delta = Number(quitSummary.value.hp_change_today ?? quitSummary.value.hp_delta_today ?? quitSummary.value.hp_change)
if (!Number.isNaN(delta) && delta !== 0) return delta > 0 ? `今日 +${delta}` : `今日 ${delta}`
if (todayChecked.value) return '今日已保住状态'
return quitDays.value > 0 ? '先打卡再继续回血' : '从今天开始积累'
})
const healthTip = computed(() => {
const d = quitDays.value
if (d >= 365) return '肺部功能显著改善,心血管疾病风险大幅降低'
if (d >= 180) return '血液循环持续改善,肺功能逐步恢复'
if (d >= 90) return '味觉嗅觉恢复,呼吸更加顺畅'
if (d >= 30) return '咳嗽减少,体能开始恢复'
if (d >= 14) return '尼古丁戒断症状明显减轻'
if (d >= 7) return '一氧化碳水平恢复正常'
if (d >= 3) return '呼吸开始变得顺畅'
if (d >= 1) return '身体开始自我修复'
return '开始戒烟,身体即将启动修复'
})
const quitEncouragement = computed(() => {
const d = quitDays.value
if (todayChecked.value) {
if (d >= 30) return '太棒了!坚持一个月以上,你已经战胜了最难的部分'
if (d >= 7) return '一周没抽了!身体正在快速恢复中'
if (d >= 3) return '三天没抽了,最难的时期正在过去'
return '今天打卡成功,继续保持!'
}
if (d === 0) return '迈出第一步,从今天开始无烟生活'
if (d < 3) return '坚持住,前三天是最关键的'
if (d < 7) return '你已经走了很远,继续加油'
return `已坚持 ${d} 天,你很了不起`
})
// 静态数据:肺泡动画气泡配置
const lungBubbleItems = [
{ id: 1, left: '18rpx', delay: '0s', duration: '4.6s' },
{ id: 2, left: '64rpx', delay: '1.2s', duration: '5.2s' },
{ id: 3, left: '116rpx', delay: '0.8s', duration: '4.9s' },
{ id: 4, left: '158rpx', delay: '1.8s', duration: '5.5s' }
]
// 静态数据:打卡庆祝放射线(8 根均匀分布)
const celebrationBursts = Array.from({ length: 8 }, (_, i) => ({ id: i + 1, rotate: i * 45 }))
// 健康里程碑(天数节点)
const healthMilestones = computed(() => [
{ days: 1, label: '1天' }, { days: 7, label: '1周' },
{ days: 30, label: '1月' }, { days: 90, label: '3月' }, { days: 365, label: '1年' }
])
const nextHealthMilestone = computed(() => healthMilestones.value.find(m => quitDays.value < m.days) || null)
const nextHealthMilestoneText = computed(() => nextHealthMilestone.value ? `${nextHealthMilestone.value.label} 里程碑` : '已完成全年恢复')
const smokeFreeTimeLabel = computed(() => quitDays.value >= 1 ? `${quitDays.value}` : '刚开始')
const lungRecoveryFillStyle = computed(() => ({
height: `${Math.max(hpValue.value, 6)}%`,
background: `linear-gradient(180deg, ${hpState.value.soft} 0%, ${hpState.value.accent} 100%)`
}))
const lungPhaseLabel = computed(() => {
const hp = hpValue.value
if (hp >= 100) return '肺部状态已进入长期巩固阶段'
if (hp >= 80) return '当前恢复速度很稳定'
if (hp >= 60) return '肺部恢复节奏正在变强'
if (hp >= 40) return '身体正在持续找回呼吸效率'
if (hp >= 20) return '恢复已经启动,别让节奏断掉'
return '先把今天守住,HP 就会慢慢往上走'
})
const lungRecoverySummary = computed(() => {
const suffix = nextHealthMilestone.value
? `,距离下一阶段还差 ${Math.max(nextHealthMilestone.value.days - quitDays.value, 0)} 天。`
: ',已经进入长期巩固阶段。'
return healthTip.value + suffix
})
const lungMomentumText = computed(() => {
const hp = hpValue.value
if (hp >= 100) return '稳定达成'
if (hp >= 70) return '恢复加速中'
if (hp >= 35) return '持续回升'
return '刚刚启动'
})
const lungRecoveryCaption = computed(() => {
if (!nextHealthMilestone.value) return '所有关键节点已完成'
return `${Math.max(nextHealthMilestone.value.days - quitDays.value, 0)} 天后解锁 ${nextHealthMilestone.value.label}`
})
// 6 个关键健康恢复节点及进度
const healthMilestoneItems = computed(() => {
const milestones = [
{ name: '血压心率恢复', minutes: 20 },
{ name: '一氧化碳排出', minutes: 480 },
{ name: '尼古丁代谢完', minutes: 4320 },
{ name: '味觉嗅觉恢复', minutes: 43200 },
{ name: '血液循环改善', minutes: 129600 },
{ name: '肺功能提升', minutes: 525600 }
]
const minutes = quitDays.value * 1440
return milestones.map(m => ({
name: m.name,
percent: Math.min(Math.round((minutes / m.minutes) * 100), 100)
}))
})
const activeGoalText = computed(() => {
const goal = quitHomeData.value?.goal
if (!goal) return '设定一个小目标,攒钱实现它'
const remaining = goal.target_amount_cent - (goal.current_amount_cent || 0)
if (remaining <= 0) return `${goal.title}」已攒够!`
return `${goal.title}」还差 ¥${Math.round(remaining / 100)}`
})
const bioAvatarText = computed(() => achievementData.value?.theme_icon || '净')
const bioRankText = computed(() => achievementData.value?.current?.name || '意志骑士')
const bioLevelText = computed(() => `Lv.${Math.max(1, Math.floor(quitDays.value / 7) + 1)}`)
const bioHpBarStyle = computed(() => ({ width: `${hpValue.value}%` }))
const bioBuffText = computed(() => `深度含氧(已坚持 ${smokeFreeTimeLabel.value}`)
const bioHealthItems = computed(() => {
const items = healthMilestoneItems.value
return [
{ name: '呼吸系统', value: `+${items[5]?.percent || 0}%` },
{ name: '心脏功能', value: `+${items[0]?.percent || 0}%` },
{ name: '血液纯净', value: `+${items[1]?.percent || 0}%` }
]
})
const bioTrendItems = computed(() => {
const labels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
const step = Math.max(3, Math.round(Math.max(hpValue.value, 12) / 16))
return labels.map((label, index) => {
const value = Math.min(100, Math.max(12, hpValue.value - (6 - index) * step))
return {
label,
height: `${Math.max(24, Math.round(value * 0.9))}rpx`,
active: index === labels.length - 1
}
})
})
// ========== 工具函数 ==========
function defaultQuitState() {
return { lastCheckinDate: '', lastCheckinAt: '', streakDays: 0 }
}
/** 两日期之间的天数差(有符号,floor),用于判断打卡是否连续 */
function diffDays(fromDate, toDate) {
if (!fromDate || !toDate) return 0
const from = new Date(`${fromDate}T00:00:00`)
const to = new Date(`${toDate}T00:00:00`)
if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return 0
return Math.floor((to.getTime() - from.getTime()) / 86400000)
}
/** 将 ISO 时间字符串格式化为 HH:MM,无效时返回 '--:--' */
function formatClock(value) {
if (!value) return '--:--'
const date = new Date(value)
return Number.isNaN(date.getTime()) ? '--:--' : formatTime(date)
}
function setupNavBar() {
const systemInfo = uni.getSystemInfoSync()
const statusBarH = systemInfo.statusBarHeight || 0
try {
const menuBtn = uni.getMenuButtonBoundingClientRect()
navBarHeight.value = menuBtn.bottom + (menuBtn.top - statusBarH)
} catch {
navBarHeight.value = statusBarH + 44
}
}
// ========== 本地状态持久化 ==========
function loadQuitState() {
quitState.value = { ...defaultQuitState(), ...(storage.get(QUIT_CHECKIN_KEY) || {}) }
}
function saveQuitState(nextState) {
quitState.value = { ...defaultQuitState(), ...nextState }
storage.set(QUIT_CHECKIN_KEY, quitState.value)
}
// ========== 计时器 ==========
function startTimer() {
stopTimer()
if (timerBaseSeconds.value < 0) return
timerInterval = setInterval(() => { timerSeconds.value++ }, 1000)
}
function stopTimer() {
if (!timerInterval) return
clearInterval(timerInterval)
timerInterval = null
}
// ========== 交互 ==========
function playQuitCheckinCelebration() {
// 先重置再触发,避免重复动画
checkinCelebrating.value = false
if (checkinCelebrationTimer) { clearTimeout(checkinCelebrationTimer); checkinCelebrationTimer = null }
setTimeout(() => {
checkinCelebrating.value = true
try { uni.vibrateShort() } catch { /* 部分平台不支持震动,忽略 */ }
checkinCelebrationTimer = setTimeout(() => {
checkinCelebrating.value = false
checkinCelebrationTimer = null
}, 1800)
}, 20)
}
function openSmokeDialog() { openDetailedSmokeDialog() }
function openDetailedSmokeDialog() { dialogType.value = 'smoke'; dialogQuickMode.value = false; showDialog.value = true }
function gotoDreamGoals() { uni.navigateTo({ url: '/pages/dream-goals/index' }) }
function buildQuickSmokePayload() {
const now = new Date()
const dateStr = formatDate(now)
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
return {
smoke_time: dateStr,
smoke_at: `${dateStr} ${timeStr}:00`,
remark: '',
reason_tags: [],
level: 2,
num: 1
}
}
// ========== API 调用 ==========
function applyHomeData(data) {
homeData.value = data
const seconds = data?.timer?.seconds_since_last
timerBaseSeconds.value = typeof seconds === 'number' ? seconds : -1
timerSeconds.value = 0
startTimer()
}
async function fetchRecordHomeData() {
const res = await api.getHome()
applyHomeData(res.data || {})
}
async function fetchAchievement() {
try {
const res = await api.getAchievement()
achievementData.value = res.data?.achievement || null
} catch (e) {
console.error('fetchAchievement error:', e)
}
}
function applyQuitHomeData(data) {
if (!data) return
quitHomeData.value = data
if (data.daily_status?.status === 'checked_in' && data.daily_status?.checkin_at) {
saveQuitState({
lastCheckinDate: data.daily_status.date,
lastCheckinAt: data.daily_status.checkin_at,
streakDays: data.summary?.current_streak_days || 0
})
}
}
async function fetchQuitHomeData() {
try {
const res = await api.getQuitCheckinHome()
applyQuitHomeData(res.data)
} catch (e) {
const msg = e?.message || ''
if (msg.includes('基础资料')) {
// 首次使用戒烟模式时后端要求先补填档案,自动补填后重试一次
await ensureQuitProfile()
try {
const res = await api.getQuitCheckinHome()
applyQuitHomeData(res.data)
} catch (retryErr) {
console.error('fetchQuitHomeData retry error:', retryErr)
loadQuitState()
}
} else {
console.error('fetchQuitHomeData error:', e)
loadQuitState()
}
}
}
async function ensureQuitProfile() {
const profile = profileStore.profile
if (!profile) return
await api.upsertQuitCheckinProfile({
quit_start_date: formatDate(new Date()),
pack_price_cent: profile.pack_price_cent || 2500,
baseline_cigs_per_day: profile.baseline_cigs_per_day || 10
})
}
async function submitQuitCheckin() {
const today = formatDate(new Date())
const res = await api.quitCheckin({ date: today })
applyQuitHomeData(res.data)
saveQuitState({
lastCheckinDate: res.data?.daily_status?.date || today,
lastCheckinAt: res.data?.daily_status?.checkin_at || new Date().toISOString(),
streakDays: res.data?.summary?.current_streak_days || 0
})
await fetchQuitHomeData()
await fetchAchievement()
return res
}
async function handleSubmit(submitData) {
try {
await api.createLog(submitData)
if (isQuitMode.value) {
await fetchQuitHomeData()
uni.showToast({ title: '已记下这次波动', icon: 'success' })
} else {
timerBaseSeconds.value = 0
timerSeconds.value = 0
startTimer()
uni.showToast({ title: '记录成功', icon: 'success' })
}
if (isQuitMode.value) { await fetchAchievement(); return }
await fetchRecordHomeData()
} catch (e) {
console.error('handleSubmit error:', e)
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
async function quickRecordSmoke() {
if (quickSubmitting.value) return
quickSubmitting.value = true
try {
await handleSubmit(buildQuickSmokePayload())
} finally {
quickSubmitting.value = false
}
}
async function handleQuitCheckin() {
if (todayChecked.value) { uni.showToast({ title: '今天已经打过卡', icon: 'none' }); return }
try {
await submitQuitCheckin()
playQuitCheckinCelebration()
uni.showToast({ title: '打卡成功', icon: 'success' })
} catch (e) {
const message = e?.message || ''
if (message.includes('基础资料')) {
try {
await ensureQuitProfile()
await submitQuitCheckin()
playQuitCheckinCelebration()
uni.showToast({ title: '打卡成功', icon: 'success' })
} catch (retryErr) {
console.error('handleQuitCheckin retry error:', retryErr)
uni.showToast({ title: retryErr?.message || '打卡失败,请重试', icon: 'none' })
}
return
}
console.error('handleQuitCheckin error:', e)
uni.showToast({ title: message || '打卡失败,请重试', icon: 'none' })
}
}
// 确认 profile 已完善,未完成则跳转 onboarding
async function ensureProfileReady() {
const profileData = await profileStore.fetchProfile()
const { profile, is_completed, exists } = profileData
const completed = is_completed || !!profile?.onboarding_completed_at || (profile?.baseline_cigs_per_day > 0)
if (!exists || !completed) {
uni.navigateTo({ url: '/pages/onboarding/index' })
return false
}
if (!userStore.mode) {
userStore.setMode(profile?.mode || 'record')
}
return true
}
async function refreshCurrentMode() {
if (!await ensureProfileReady()) return
if (isQuitMode.value) {
stopTimer()
await Promise.all([fetchQuitHomeData(), fetchAchievement()])
return
}
await Promise.all([fetchRecordHomeData(), fetchAchievement()])
}
async function initPage() {
setupNavBar()
loading.value = true
try {
await waitForLogin()
await refreshCurrentMode()
} catch (e) {
console.error('initPage error:', e)
} finally {
pageReady.value = true
loading.value = false
}
}
onMounted(() => { initPage() })
onShow(async () => {
if (!pageReady.value) return
try { await refreshCurrentMode() } catch (e) { console.error('home onShow error:', e) }
})
onUnmounted(() => {
stopTimer()
if (checkinCelebrationTimer) { clearTimeout(checkinCelebrationTimer); checkinCelebrationTimer = null }
})
onShareAppMessage(() => ({
title: isQuitMode.value ? '我在坚持戒烟打卡' : '我在记录自己的抽烟变化',
path: 'pages/index/index'
}))
</script>
<style lang="scss" scoped>
// ===== 页面基础 =====
.page {
min-height: 100vh;
position: relative;
background: $bg-page;
overflow: hidden;
box-sizing: border-box;
}
.page-record {
background: linear-gradient(180deg, #F6F8F6 0%, #EFF4F1 52%, #E9F0EC 100%);
}
// ===== 背景装饰 =====
.bg-gradient {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 600rpx;
background: linear-gradient(180deg, $color-primary-soft 0%, $color-primary-softer 40%, $bg-page 100%);
pointer-events: none;
}
.bg-pattern {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 400rpx;
background-image:
radial-gradient(circle at 20% 30%, rgba($color-primary-dark, 0.08) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba($color-primary, 0.06) 0%, transparent 40%);
pointer-events: none;
}
// ===== 骨架屏 =====
.skeleton-wrap {
padding: 48rpx $spacing-xl;
position: relative;
z-index: 1;
}
.sk-circle {
width: 280rpx;
height: 280rpx;
border-radius: 50%;
@include skeleton-shimmer;
margin: 0 auto 48rpx;
}
.sk-row {
height: 32rpx;
border-radius: $radius-md;
@include skeleton-shimmer;
margin-bottom: $spacing-lg;
&-short { width: 60%; }
}
.sk-card {
height: 160rpx;
border-radius: $radius-xl;
@include skeleton-shimmer;
margin-top: $spacing-xl;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
// ===== 主内容 =====
.nav-placeholder, .content {
position: relative;
z-index: 1;
}
.content {
padding: $spacing-lg $spacing-lg 200rpx;
}
// ===== 戒烟模式 =====
.quit-home {
@include flex-col;
gap: $spacing-lg;
}
// 玻璃卡片基础样式
.quit-hero-card,
.quit-checkin-card,
.quit-health-card {
position: relative;
overflow: hidden;
background: $gradient-card;
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
border-radius: $radius-2xl;
border: 1rpx solid $border-light;
box-shadow: $shadow-lg, 0 6rpx 14rpx rgba(15, 23, 42, 0.03), inset 0 1rpx 0 rgba(255, 255, 255, 0.88);
&::before {
content: '';
position: absolute;
width: 240rpx;
height: 240rpx;
left: -72rpx;
top: -72rpx;
border-radius: 50%;
background: radial-gradient(circle, var(--hp-soft, rgba($color-primary, 0.18)) 0%, rgba($color-primary, 0) 72%);
pointer-events: none;
}
}
.quit-hero-card {
padding: 28rpx $spacing-lg;
--hp-accent: #{$color-primary-deeper};
--hp-soft: #def7ec;
--hp-deep: #0f766e;
}
.quit-hero-top,
.quit-overview,
.quit-health-header,
.quit-health-milestones {
position: relative;
z-index: 1;
}
.quit-hero-top {
@include flex-between;
align-items: flex-start;
gap: $spacing-md;
margin-bottom: 20rpx;
}
.quit-hero-copy {
flex: 1;
min-width: 0;
}
.quit-hero-eyebrow {
@include chip;
background: rgba($color-primary-softer, 0.94);
border: 1rpx solid $border-light;
color: $text-tertiary;
}
.quit-hero-title {
display: block;
margin-top: 10rpx;
font-size: $font-lg;
line-height: $line-height-normal;
font-weight: $font-weight-bold;
color: var(--hp-accent);
}
.quit-hero-chip {
@include chip;
flex-shrink: 0;
padding: 10rpx 18rpx;
background: rgba($color-primary-softer, 0.94);
border: 1rpx solid $border-light;
color: $color-primary-deeper;
}
.quit-overview {
@include flex-col;
gap: 18rpx;
}
// HP 面板
.quit-hp-panel {
display: flex;
align-items: center;
gap: 18rpx;
padding: 18rpx;
border-radius: $radius-xl + 2rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94) 0%, rgba(248, 251, 249, 0.92) 100%);
border: 1rpx solid $border-light;
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
}
.quit-hp-ring {
@include ring(200rpx);
padding: 12rpx;
box-shadow: 0 16rpx 34rpx rgba(15, 23, 42, 0.08);
}
.quit-hp-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(180deg, $bg-card 0%, $bg-card-hover 100%);
@include flex-col;
align-items: center;
justify-content: center;
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
padding: 0 12rpx;
text-align: center;
}
.quit-hp-ring-kicker {
font-size: $font-xs;
font-weight: $font-weight-bold;
color: $text-tertiary;
}
.quit-hp-ring-value {
margin-top: 10rpx;
font-size: $font-display;
font-weight: $font-weight-heavy;
line-height: 1;
color: var(--hp-accent);
font-family: $font-family-number;
}
.quit-hp-ring-label {
margin-top: $spacing-xs;
padding: $spacing-xs $spacing-md;
border-radius: $radius-full;
background: var(--hp-soft);
color: var(--hp-deep);
font-size: $font-sm;
font-weight: $font-weight-bold;
}
.quit-hp-copy {
flex: 1;
min-width: 0;
@include flex-col;
justify-content: center;
}
.quit-hp-title {
display: block;
font-size: $font-2xl;
line-height: 1.35;
font-weight: $font-weight-heavy;
color: var(--hp-deep);
}
.quit-hp-desc {
display: block;
margin-top: 10rpx;
font-size: 23rpx;
line-height: $line-height-relaxed;
color: $text-secondary;
}
.quit-hp-pill-row {
display: flex;
gap: $spacing-sm;
margin-top: 18rpx;
}
.quit-hp-pill {
flex: 1;
min-width: 0;
padding: 14rpx $spacing-md;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.8);
border: 1rpx solid $border-light;
&-strong {
background: linear-gradient(180deg, var(--hp-soft) 0%, rgba(255, 255, 255, 0.88) 100%);
}
&-label {
display: block;
font-size: 18rpx;
color: $text-muted;
}
&-value {
display: block;
margin-top: 6rpx;
font-size: $font-sm;
font-weight: $font-weight-bold;
color: $text-primary;
}
}
// 指标网格
.quit-metrics-grid {
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
}
.quit-metric-card {
padding: 18rpx 18rpx 20rpx;
border-radius: $radius-lg + 2rpx;
background: $gradient-card-subtle;
border: 1rpx solid $border-light;
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
&-wide { grid-column: 1 / -1; }
}
.quit-metric-kicker {
display: block;
font-size: $font-sm;
color: $text-tertiary;
}
.quit-metric-value {
display: block;
margin-top: 10rpx;
font-size: $font-2xl;
line-height: $line-height-tight;
font-weight: $font-weight-heavy;
color: $text-primary;
font-family: $font-family-number;
}
// 打卡卡片
.quit-checkin-card {
@include flex-between;
gap: 18rpx;
padding: 26rpx $spacing-lg;
&-done {
background: linear-gradient(180deg, rgba($color-primary-softer, 0.98) 0%, rgba(249, 252, 250, 0.94) 100%);
}
}
.quit-checkin-main {
display: flex;
align-items: center;
gap: 18rpx;
flex: 1;
min-width: 0;
}
.quit-checkin-icon {
width: 84rpx;
height: 84rpx;
border-radius: 28rpx;
flex-shrink: 0;
background: $gradient-primary;
box-shadow: 0 16rpx 30rpx rgba($color-primary-dark, 0.22);
@include flex-center;
text {
font-size: 36rpx;
font-weight: $font-weight-bold;
color: $text-inverse;
}
&-done {
background: linear-gradient(180deg, #26b47f 0%, $color-primary-deeper 100%);
}
}
.quit-checkin-copy {
flex: 1;
min-width: 0;
}
.quit-checkin-title {
display: block;
font-size: $font-2xl;
font-weight: $font-weight-bold;
color: $text-primary;
}
.quit-checkin-desc {
display: block;
margin-top: $spacing-xs;
font-size: 23rpx;
line-height: 1.6;
color: $text-tertiary;
}
.quit-checkin-side {
flex-shrink: 0;
padding: $spacing-sm 18rpx;
border-radius: $radius-full;
background: rgba($color-primary-softer, 0.94);
border: 1rpx solid $border-light;
&-label {
font-size: $font-sm;
font-weight: $font-weight-semibold;
color: $color-primary-deeper;
}
}
// 健康卡片
.quit-health-card {
padding: 26rpx $spacing-lg $spacing-lg;
}
.quit-health-header {
@include flex-between;
gap: $spacing-md;
margin-bottom: 20rpx;
}
.quit-health-title {
display: block;
font-size: $font-xl;
font-weight: $font-weight-bold;
color: $text-primary;
}
.quit-health-badge {
@include chip;
background: rgba($color-primary-softer, 0.94);
border: 1rpx solid $border-light;
color: $color-primary-deeper;
&-done {
background: linear-gradient(135deg, $color-success, $color-primary-deeper);
color: $text-inverse;
border-color: transparent;
}
}
// 破戒入口
.quit-slip-entry {
@include flex-between;
gap: $spacing-md;
padding: 22rpx $spacing-lg;
border-radius: $radius-xl;
background: linear-gradient(135deg, var(--hp-soft, #def7ec) 0%, rgba(255, 255, 255, 0.96) 100%);
border: 1rpx solid $border-light;
box-shadow: 0 10rpx $spacing-lg rgba(15, 23, 42, 0.05);
}
.quit-slip-copy {
flex: 1;
min-width: 0;
}
.quit-slip-title {
display: block;
font-size: $font-lg;
font-weight: $font-weight-bold;
color: $text-primary;
}
.quit-slip-desc {
display: block;
margin-top: $spacing-xs;
font-size: $font-sm;
line-height: 1.6;
color: $text-tertiary;
}
.quit-slip-action {
flex-shrink: 0;
padding: $spacing-md 18rpx;
border-radius: 18rpx;
background: linear-gradient(180deg, var(--hp-accent, $color-primary-deeper) 0%, var(--hp-deep, #0f766e) 100%);
box-shadow: 0 14rpx 26rpx rgba($color-primary-deeper, 0.2);
&-main {
display: block;
font-size: $font-base;
font-weight: $font-weight-bold;
color: $text-inverse;
}
&-sub {
display: block;
margin-top: 6rpx;
font-size: 19rpx;
color: rgba(255, 255, 255, 0.82);
}
}
// 里程碑
.quit-milestone-list {
position: relative;
z-index: 1;
@include flex-col;
gap: $spacing-md;
}
.quit-ms-item { position: relative; }
.quit-ms-top {
@include flex-between;
margin-bottom: $spacing-xs;
}
.quit-ms-name {
font-size: $font-base;
color: #374151;
font-weight: $font-weight-medium;
}
.quit-ms-pct {
font-size: $font-sm;
color: $text-muted;
font-weight: $font-weight-semibold;
&-done { color: $color-primary-deeper; }
}
.quit-ms-bar {
@include progress-bar;
}
.quit-ms-fill {
@include progress-fill;
&-done { background: linear-gradient(90deg, $color-primary, $color-primary-deeper); }
&-pending { background: rgba($color-primary, 0.3); }
}
// 成就内联
.quit-ach-inline {
@include flex-between;
gap: $spacing-md;
margin-top: 20rpx;
padding: 18rpx 20rpx;
border-radius: $radius-lg + 2rpx;
background: linear-gradient(180deg, #f5fbf9 0%, #eef6f2 100%);
border: 1rpx solid $border-light;
position: relative;
z-index: 1;
}
.quit-ach-left {
display: flex;
align-items: center;
gap: $spacing-sm;
flex-shrink: 0;
}
.quit-ach-icon {
font-size: 36rpx;
line-height: 1;
}
.quit-ach-info {
@include flex-col;
}
.quit-ach-rank {
font-size: $font-lg;
font-weight: $font-weight-heavy;
color: $color-primary-deeper;
line-height: $line-height-tight;
}
.quit-ach-theme {
font-size: $font-xs;
color: $text-muted;
margin-top: 2rpx;
}
.quit-ach-right {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.quit-ach-progress-area {
width: 100%;
max-width: 280rpx;
}
.quit-ach-bar {
@include progress-bar(8rpx);
}
.quit-ach-fill {
@include progress-fill;
}
.quit-ach-hint {
display: block;
margin-top: 6rpx;
font-size: $font-xs;
color: $text-muted;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quit-ach-max {
font-size: $font-xs;
color: $color-primary-deeper;
font-weight: $font-weight-semibold;
}
// 梦想入口
.quit-dream-entry {
@include flex-between;
padding: 22rpx $spacing-lg;
background: $gradient-card;
border-radius: $radius-xl;
border: 1rpx solid $border-light;
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.04);
}
.quit-dream-left {
display: flex;
align-items: center;
gap: $spacing-md;
flex: 1;
min-width: 0;
}
.quit-dream-icon {
font-size: 36rpx;
flex-shrink: 0;
}
.quit-dream-info {
flex: 1;
min-width: 0;
}
.quit-dream-title {
display: block;
font-size: $font-lg;
font-weight: $font-weight-bold;
color: $text-primary;
}
.quit-dream-desc {
display: block;
margin-top: 4rpx;
font-size: $font-sm;
color: $text-muted;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quit-dream-arrow {
font-size: 36rpx;
color: $text-disabled;
flex-shrink: 0;
font-weight: 300;
}
// 提示栏
.quit-tip-bar {
display: flex;
align-items: center;
gap: $spacing-xs;
padding: 14rpx 18rpx;
border-radius: $radius-md;
background: rgba($color-primary-softer, 0.6);
border: 1rpx solid rgba(15, 23, 42, 0.03);
}
.quit-tip-icon {
font-size: $font-sm;
flex-shrink: 0;
}
.quit-tip-text {
font-size: $font-xs;
line-height: $line-height-normal;
color: $text-muted;
}
// ===== 记录模式 =====
.record-home {
@include flex-col;
gap: $spacing-lg;
}
.record-hero-card,
.record-insight-card,
.record-achievement-card {
position: relative;
overflow: hidden;
border-radius: 36rpx;
background: rgba(255, 255, 255, 0.86);
-webkit-backdrop-filter: blur(22px);
backdrop-filter: blur(22px);
border: 1rpx solid rgba(255, 255, 255, 0.82);
box-shadow: 0 24rpx 60rpx rgba(23, 55, 46, 0.08), inset 0 1rpx 0 rgba(255, 255, 255, 0.96);
}
.record-hero-card {
padding: 32rpx;
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.92) 0%, rgba(241, 250, 246, 0.9) 56%, rgba(232, 244, 238, 0.88) 100%);
}
.record-hero-card-empty {
background:
linear-gradient(145deg, rgba(255, 255, 255, 0.94) 0%, rgba(244, 250, 247, 0.92) 56%, rgba(237, 247, 242, 0.9) 100%);
}
.record-hero-bg {
position: absolute;
border-radius: 50%;
pointer-events: none;
filter: blur(2rpx);
}
.record-hero-bg-one {
width: 360rpx;
height: 360rpx;
right: -138rpx;
top: -150rpx;
background: radial-gradient(circle, rgba(31, 191, 143, 0.2) 0%, rgba(31, 191, 143, 0) 70%);
}
.record-hero-bg-two {
width: 280rpx;
height: 280rpx;
left: -120rpx;
bottom: -132rpx;
background: radial-gradient(circle, rgba(14, 165, 233, 0.12) 0%, rgba(14, 165, 233, 0) 72%);
}
.record-hero-header,
.record-hero-body,
.record-action-card {
position: relative;
z-index: 1;
}
.record-hero-header {
@include flex-between;
align-items: flex-start;
gap: $spacing-md;
margin-bottom: 30rpx;
}
.record-hero-title-block {
flex: 1;
min-width: 0;
}
.record-hero-eyebrow {
display: block;
font-size: $font-sm;
font-weight: $font-weight-semibold;
letter-spacing: 2rpx;
color: $color-primary-deeper;
}
.record-hero-title {
display: block;
margin-top: 10rpx;
font-size: 46rpx;
line-height: 1.16;
font-weight: $font-weight-heavy;
color: #13251f;
letter-spacing: -0.8rpx;
}
.record-hero-chip {
flex-shrink: 0;
display: flex;
align-items: baseline;
padding: 14rpx 18rpx;
border-radius: 22rpx;
background: rgba(255, 255, 255, 0.76);
border: 1rpx solid rgba(31, 191, 143, 0.12);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
}
.record-hero-chip-value {
font-size: $font-2xl;
font-weight: $font-weight-heavy;
line-height: 1;
color: $color-primary-deeper;
font-family: $font-family-number;
}
.record-hero-chip-label {
font-size: $font-xs;
font-weight: $font-weight-semibold;
color: $text-muted;
}
.record-hero-body {
@include flex-col;
gap: 22rpx;
}
.record-timer-panel {
display: flex;
align-items: center;
gap: 24rpx;
padding: 24rpx;
border-radius: 32rpx;
background: rgba(255, 255, 255, 0.66);
border: 1rpx solid rgba(255, 255, 255, 0.86);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
}
.record-ring {
@include ring(220rpx);
flex-shrink: 0;
padding: 12rpx;
box-shadow: 0 18rpx 34rpx rgba(31, 191, 143, 0.12), inset 0 2rpx 8rpx rgba(15, 23, 42, 0.04);
}
.record-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 252, 249, 0.96) 100%);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 1);
@include flex-col;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 18rpx;
}
.record-ring-kicker,
.record-ring-label {
font-size: 20rpx;
font-weight: $font-weight-semibold;
color: $text-muted;
}
.record-ring-value {
margin: 8rpx 0;
font-size: 38rpx;
line-height: 1;
font-weight: $font-weight-heavy;
color: $color-primary-deeper;
font-family: $font-family-number;
letter-spacing: -1rpx;
}
.record-progress-copy {
flex: 1;
min-width: 0;
}
.record-progress-row {
@include flex-between;
gap: $spacing-sm;
margin-bottom: 12rpx;
}
.record-progress-label,
.record-progress-value {
font-size: $font-sm;
font-weight: $font-weight-bold;
color: $text-secondary;
}
.record-progress-value { color: $color-primary-deeper; }
.record-progress-track {
height: 16rpx;
border-radius: $radius-full;
background: rgba(213, 232, 223, 0.82);
overflow: hidden;
}
.record-progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #1fbf8f 0%, #79ddbd 100%);
box-shadow: 0 0 16rpx rgba(31, 191, 143, 0.32);
transition: width 0.35s ease;
}
.record-progress-note {
display: block;
margin-top: 16rpx;
font-size: $font-sm;
line-height: $line-height-relaxed;
color: $text-secondary;
}
.record-today-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.record-today-grid-single {
grid-template-columns: 1fr;
}
.record-today-card {
padding: 22rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.7);
border: 1rpx solid rgba(255, 255, 255, 0.86);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.9);
}
.record-today-card-smoke { background: linear-gradient(145deg, rgba(255, 255, 255, 0.78) 0%, rgba(255, 247, 237, 0.68) 100%); }
.record-today-value {
display: block;
font-size: 48rpx;
line-height: 1;
font-weight: $font-weight-heavy;
color: #14261f;
font-family: $font-family-number;
}
.record-today-label {
display: block;
margin-top: 10rpx;
font-size: $font-sm;
font-weight: $font-weight-semibold;
color: $text-muted;
}
.record-action-card {
margin-top: 24rpx;
padding: 24rpx;
border-radius: 30rpx;
background: rgba(18, 37, 31, 0.92);
box-shadow: 0 18rpx 40rpx rgba(19, 37, 31, 0.14);
}
.record-action-title {
display: block;
font-size: $font-lg;
font-weight: $font-weight-heavy;
color: $text-inverse;
}
.record-action-desc {
display: block;
margin-top: 8rpx;
font-size: $font-sm;
line-height: $line-height-normal;
color: rgba(255, 255, 255, 0.68);
}
.record-action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16rpx;
margin-top: 22rpx;
}
.record-action-btn {
height: 88rpx;
border-radius: $radius-full;
@include flex-center;
gap: 10rpx;
font-weight: $font-weight-heavy;
}
.record-action-btn-primary {
background: linear-gradient(135deg, #1fbf8f 0%, #42d6a7 100%);
box-shadow: 0 14rpx 26rpx rgba(31, 191, 143, 0.34);
color: $text-inverse;
}
.record-action-btn-ghost {
background: rgba(255, 255, 255, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.18);
color: $text-inverse;
}
.record-action-btn-disabled {
opacity: 0.68;
pointer-events: none;
}
.record-action-btn-icon {
font-size: 30rpx;
line-height: 1;
}
.record-action-btn-text {
font-size: $font-base;
}
.record-insight-card {
padding: 28rpx;
}
.record-section-header {
@include flex-between;
align-items: flex-start;
gap: $spacing-md;
margin-bottom: 22rpx;
}
.record-section-title {
display: block;
font-size: $font-xl;
font-weight: $font-weight-heavy;
color: #13251f;
}
.record-section-subtitle {
display: block;
margin-top: 6rpx;
font-size: $font-sm;
color: $text-muted;
}
.record-section-badge {
flex-shrink: 0;
padding: 10rpx 16rpx;
border-radius: $radius-full;
background: rgba(31, 191, 143, 0.1);
color: $color-primary-deeper;
font-size: $font-xs;
font-weight: $font-weight-heavy;
}
.health-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14rpx;
}
.health-metric-item {
@include flex-col;
align-items: flex-start;
gap: 14rpx;
padding: 18rpx;
min-height: 142rpx;
background: linear-gradient(180deg, rgba(247, 251, 249, 0.96) 0%, rgba(241, 248, 245, 0.96) 100%);
border-radius: 24rpx;
border: 1rpx solid $border-light;
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
}
.health-metric-icon {
width: 48rpx;
height: 48rpx;
border-radius: 16rpx;
@include flex-center;
font-size: $font-base;
font-weight: $font-weight-heavy;
&-money { color: #a16207; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); }
&-time { color: #075985; background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%); }
}
.health-metric-content { @include flex-col; }
.health-metric-value {
font-size: $font-xl;
font-weight: $font-weight-heavy;
line-height: 1;
color: $text-primary;
font-family: $font-family-number;
}
.health-metric-label {
margin-top: 8rpx;
font-size: 21rpx;
line-height: $line-height-normal;
color: $text-tertiary;
}
.health-tip-bar {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-top: 20rpx;
padding: 18rpx 20rpx;
background: linear-gradient(135deg, rgba(236, 253, 245, 0.82) 0%, rgba(240, 249, 255, 0.7) 100%);
border-radius: 24rpx;
border: 1rpx solid rgba(31, 191, 143, 0.1);
}
.health-tip-icon {
font-size: $font-base;
color: $color-primary-deeper;
font-weight: $font-weight-heavy;
}
.health-tip-text {
flex: 1;
font-size: $font-sm;
color: $text-secondary;
line-height: $line-height-relaxed;
}
.record-achievement-card {
padding: 28rpx;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9) 0%, rgba(248, 250, 252, 0.9) 100%);
}
.ach-header {
@include flex-between;
align-items: flex-start;
gap: $spacing-md;
margin-bottom: $spacing-md;
}
.ach-title {
display: block;
font-size: $font-md;
font-weight: $font-weight-heavy;
color: $text-primary;
}
.ach-theme-name {
display: block;
margin-top: 8rpx;
font-size: $font-xs;
color: $text-muted;
}
.ach-days {
flex-shrink: 0;
font-size: $font-xs;
font-weight: $font-weight-bold;
color: $color-primary-deeper;
padding: 8rpx 14rpx;
border-radius: $radius-full;
background: rgba(31, 191, 143, 0.1);
}
.ach-body {
position: relative;
z-index: 1;
}
.ach-rank {
display: block;
font-size: $font-3xl;
font-weight: $font-weight-heavy;
color: $color-primary-deeper;
font-family: $font-family-number;
margin-bottom: $spacing-md;
}
.ach-progress-wrap { margin-bottom: 4rpx; }
.ach-progress-bar {
@include progress-bar(12rpx);
border-radius: 6rpx;
margin-bottom: 10rpx;
}
.ach-progress-fill {
@include progress-fill;
border-radius: 6rpx;
}
.ach-next-hint {
font-size: $font-sm;
color: $text-tertiary;
}
.ach-max { margin-top: 4rpx; }
.ach-max-text {
font-size: $font-sm;
color: $color-primary-deeper;
font-weight: $font-weight-semibold;
}
// ===== 庆祝弹窗 =====
.checkin-celebration-mask {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.24);
@include flex-center;
z-index: 40;
padding: 0 $spacing-2xl;
}
.checkin-celebration-card {
position: relative;
width: 100%;
padding: 42rpx $spacing-2xl - 6rpx $spacing-2xl - 6rpx;
border-radius: $radius-2xl;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(242, 251, 247, 0.96) 100%);
box-shadow: 0 26rpx 56rpx rgba(15, 23, 42, 0.18);
@include flex-col;
align-items: center;
animation: celebrateEnter 0.28s ease-out;
}
.checkin-celebration-burst {
position: absolute;
left: 50%;
top: 82rpx;
width: 0;
height: 0;
}
.checkin-celebration-ray {
position: absolute;
left: 50%;
top: 50%;
width: 8rpx;
height: 34rpx;
margin-left: -4rpx;
border-radius: $radius-full;
background: linear-gradient(180deg, rgba($color-primary-light, 0.2) 0%, rgba($color-primary-deeper, 0.92) 100%);
}
.checkin-celebration-icon {
position: relative;
z-index: 1;
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: $gradient-primary;
box-shadow: 0 18rpx 36rpx rgba($color-primary-deeper, 0.3);
@include flex-center;
font-size: $font-4xl;
font-weight: $font-weight-heavy;
color: $text-inverse;
animation: pulseCheck 0.8s ease-out;
}
.checkin-celebration-title {
display: block;
margin-top: 26rpx;
font-size: $font-2xl;
font-weight: $font-weight-heavy;
color: $text-primary;
}
.checkin-celebration-desc {
display: block;
margin-top: 10rpx;
font-size: $font-base;
line-height: $line-height-relaxed;
color: $text-secondary;
text-align: center;
}
.checkin-celebration-chip-row {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14rpx;
margin-top: 26rpx;
}
.checkin-celebration-chip {
padding: 18rpx $spacing-md;
border-radius: $radius-lg;
background: rgba(255, 255, 255, 0.76);
border: 1rpx solid $border-light;
text-align: center;
&-label {
display: block;
font-size: $font-xs;
color: $text-muted;
}
&-value {
display: block;
margin-top: $spacing-xs;
font-size: $font-lg;
font-weight: $font-weight-heavy;
color: $color-primary-deeper;
}
}
@keyframes breathBubble {
0% {
transform: translateY(0) scale(0.88);
opacity: 0;
}
20% {
opacity: 0.95;
}
100% {
transform: translateY(-170rpx) scale(1.2);
opacity: 0;
}
}
@keyframes recordFabFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6rpx); }
}
@keyframes celebrateEnter {
0% {
opacity: 0;
transform: translateY(24rpx) scale(0.94);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes celebrateGlow {
0%, 100% {
transform: translate(-50%, -50%) scale(0.96);
opacity: 0.9;
}
50% {
transform: translate(-50%, -50%) scale(1.04);
opacity: 1;
}
}
@keyframes pulseCheck {
0% {
transform: scale(0.82);
}
55% {
transform: scale(1.08);
}
100% {
transform: scale(1);
}
}
// ===== UI.md 生物数据仪表盘首页 =====
.bio-home {
--bio-hp-clear: #67E8F9;
--bio-hp-life: #6EE7B7;
--bio-amber: #D97706;
--bio-gold: #FBBF24;
--bio-lavender: #A78BFA;
--bio-bg: #F8FAFC;
--bio-text: #1E293B;
@include flex-col;
gap: 20rpx;
color: var(--bio-text);
}
.bio-status-card,
.bio-console-card,
.bio-health-card,
.bio-trend-card,
.bio-action-card,
.bio-tip-card {
position: relative;
overflow: hidden;
border: 1rpx solid rgba(255, 255, 255, 0.82);
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 18rpx 44rpx rgba(30, 41, 59, 0.07);
backdrop-filter: blur(12px);
}
.bio-status-card {
padding: 28rpx;
border-radius: 34rpx;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.76), rgba(240, 249, 255, 0.62)),
radial-gradient(circle at top right, rgba(103, 232, 249, 0.22), transparent 36%);
}
.bio-breath-line {
position: absolute;
top: 0;
left: 32rpx;
right: 32rpx;
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(103, 232, 249, 0.58), rgba(110, 231, 183, 0.48), transparent);
animation: bioBreathLine 6s ease-in-out infinite;
}
.bio-profile-row {
display: flex;
align-items: center;
gap: 18rpx;
}
.bio-avatar {
width: 76rpx;
height: 76rpx;
border-radius: 28rpx;
@include flex-center;
background: linear-gradient(135deg, rgba(110, 231, 183, 0.95), rgba(103, 232, 249, 0.88));
box-shadow: 0 12rpx 28rpx rgba(20, 184, 166, 0.18);
color: #ffffff;
font-size: 34rpx;
font-weight: 900;
}
.bio-profile-main {
flex: 1;
min-width: 0;
@include flex-col;
gap: 6rpx;
}
.bio-rank {
font-size: 29rpx;
line-height: 1.25;
font-weight: 900;
color: var(--bio-text);
}
.bio-level {
font-size: 23rpx;
font-weight: 800;
color: #64748B;
}
.bio-subtitle {
font-size: 21rpx;
line-height: 1.45;
color: #64748B;
}
.bio-coin-pill {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6rpx;
padding: 10rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 251, 235, 0.78);
border: 1rpx solid rgba(251, 191, 36, 0.22);
}
.bio-coin-icon,
.bio-coin-value {
font-size: 22rpx;
font-weight: 900;
color: #92400E;
}
.bio-hp-box {
margin-top: 30rpx;
padding: 18rpx;
border-radius: 26rpx;
background: rgba(248, 250, 252, 0.72);
border: 1rpx solid rgba(148, 163, 184, 0.12);
}
.bio-hp-track {
position: relative;
height: 24rpx;
border-radius: 999rpx;
overflow: hidden;
background: linear-gradient(90deg, rgba(217, 119, 6, 0.18), rgba(226, 232, 240, 0.66));
}
.bio-hp-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--bio-hp-life), var(--bio-hp-clear));
box-shadow: 0 0 18rpx rgba(103, 232, 249, 0.26);
animation: bioHpPulse 5s ease-in-out infinite;
}
.bio-hp-clear {
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
animation: bioClearFlow 2s ease-in-out infinite;
}
.bio-hp-meta {
margin-top: 12rpx;
display: flex;
justify-content: space-between;
font-size: 22rpx;
font-weight: 800;
color: #475569;
}
.bio-buff-pill {
align-self: flex-start;
margin-top: 24rpx;
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid rgba(167, 139, 250, 0.44);
background: rgba(250, 245, 255, 0.58);
color: #6D28D9;
font-size: 22rpx;
font-weight: 800;
}
.bio-buff-star {
color: var(--bio-lavender);
}
.bio-console-card {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx;
border-radius: 34rpx;
background:
radial-gradient(circle at 18% 50%, rgba(110, 231, 183, 0.25), transparent 36%),
linear-gradient(135deg, rgba(255, 255, 255, 0.8), rgba(236, 253, 245, 0.62));
}
.bio-console-orbit {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
@include flex-center;
background: conic-gradient(from 180deg, rgba(110, 231, 183, 0.9), rgba(103, 232, 249, 0.55), rgba(217, 119, 6, 0.22), rgba(110, 231, 183, 0.9));
}
.bio-console-core {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
@include flex-center;
background: #ffffff;
color: #0F766E;
font-size: 46rpx;
font-weight: 900;
}
.bio-console-core-done {
background: linear-gradient(135deg, var(--bio-hp-life), var(--bio-hp-clear));
color: #ffffff;
}
.bio-console-copy {
flex: 1;
min-width: 0;
@include flex-col;
gap: 8rpx;
}
.bio-console-kicker {
font-size: 19rpx;
font-weight: 900;
letter-spacing: 1.6rpx;
color: #0891B2;
}
.bio-console-title {
font-size: 31rpx;
line-height: 1.25;
font-weight: 900;
color: var(--bio-text);
}
.bio-console-desc {
font-size: 22rpx;
line-height: 1.55;
color: #64748B;
}
.bio-console-action {
align-self: stretch;
@include flex-center;
min-width: 86rpx;
padding: 0 18rpx;
border-radius: 22rpx;
background: linear-gradient(180deg, #FBBF24, #D97706);
color: #ffffff;
font-size: 23rpx;
font-weight: 900;
}
.bio-health-card,
.bio-trend-card {
padding: 28rpx;
border-radius: 34rpx;
}
.bio-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18rpx;
}
.bio-section-head-compact {
align-items: center;
}
.bio-section-title {
display: block;
font-size: 29rpx;
font-weight: 900;
color: var(--bio-text);
}
.bio-section-subtitle {
display: block;
margin-top: 8rpx;
font-size: 21rpx;
line-height: 1.45;
color: #64748B;
}
.bio-section-chip {
flex-shrink: 0;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(236, 253, 245, 0.86);
border: 1rpx solid rgba(110, 231, 183, 0.28);
font-size: 20rpx;
font-weight: 900;
color: #0F766E;
}
.bio-health-body {
margin-top: 24rpx;
display: flex;
align-items: center;
gap: 26rpx;
}
.bio-health-ring {
width: 166rpx;
height: 166rpx;
border-radius: 50%;
padding: 12rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.bio-health-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
@include flex-center;
flex-direction: column;
background: rgba(255, 255, 255, 0.92);
box-shadow: inset 0 0 0 1rpx rgba(148, 163, 184, 0.1);
}
.bio-health-ring-value {
font-size: 34rpx;
font-weight: 900;
color: var(--bio-text);
}
.bio-health-ring-label {
margin-top: 4rpx;
font-size: 20rpx;
font-weight: 800;
color: #64748B;
}
.bio-health-metrics {
flex: 1;
min-width: 0;
padding: 18rpx 20rpx;
border-radius: 26rpx;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.78), rgba(255, 255, 255, 0.68));
border: 1rpx solid rgba(148, 163, 184, 0.1);
}
.bio-health-row {
display: flex;
justify-content: space-between;
gap: 14rpx;
padding: 8rpx 0;
}
.bio-health-name {
font-size: 22rpx;
font-weight: 800;
color: #475569;
}
.bio-health-value {
font-size: 22rpx;
font-weight: 900;
color: #0F766E;
}
.bio-health-divider {
height: 1rpx;
margin: 12rpx 0 14rpx;
background: rgba(148, 163, 184, 0.18);
}
.bio-life-row {
@include flex-col;
gap: 4rpx;
}
.bio-life-value {
font-size: 34rpx;
line-height: 1.2;
font-weight: 900;
color: var(--bio-gold);
}
.bio-life-label {
font-size: 21rpx;
font-weight: 800;
color: #64748B;
}
.bio-trend-card {
background: linear-gradient(135deg, #ECFDF5 0%, #F0F9FF 100%);
}
.bio-trend-chart {
height: 176rpx;
margin-top: 22rpx;
padding: 0 4rpx;
display: flex;
align-items: flex-end;
justify-content: space-between;
border-bottom: 1rpx solid rgba(100, 116, 139, 0.18);
}
.bio-trend-col {
flex: 1;
min-width: 0;
@include flex-center;
flex-direction: column;
justify-content: flex-end;
gap: 10rpx;
}
.bio-trend-line {
position: relative;
width: 4rpx;
border-radius: 999rpx;
background: linear-gradient(180deg, rgba(103, 232, 249, 0.86), rgba(110, 231, 183, 0.44));
}
.bio-trend-dot {
position: absolute;
top: -8rpx;
left: 50%;
width: 16rpx;
height: 16rpx;
margin-left: -8rpx;
border-radius: 50%;
background: #67E8F9;
box-shadow: 0 0 0 5rpx rgba(103, 232, 249, 0.14);
}
.bio-trend-dot-active {
background: var(--bio-gold);
box-shadow: 0 0 0 6rpx rgba(251, 191, 36, 0.18);
}
.bio-trend-label {
font-size: 18rpx;
color: #64748B;
}
.bio-action-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18rpx;
}
.bio-action-card {
padding: 22rpx;
border-radius: 28rpx;
background: rgba(255, 255, 255, 0.74);
}
.bio-action-title {
display: block;
font-size: 25rpx;
font-weight: 900;
color: var(--bio-text);
}
.bio-action-desc {
display: block;
margin-top: 8rpx;
font-size: 21rpx;
line-height: 1.45;
color: #64748B;
}
.bio-tip-card {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 20rpx 22rpx;
border-radius: 26rpx;
background: rgba(250, 245, 255, 0.54);
border-color: rgba(167, 139, 250, 0.2);
}
.bio-tip-icon {
font-size: 24rpx;
color: var(--bio-lavender);
flex-shrink: 0;
}
.bio-tip-text {
font-size: 22rpx;
line-height: 1.55;
color: #475569;
}
@keyframes bioHpPulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.05); }
}
@keyframes bioClearFlow {
0% { transform: translateX(-100%); opacity: 0.15; }
50% { opacity: 0.28; }
100% { transform: translateX(100%); opacity: 0.15; }
}
@keyframes bioBreathLine {
0%, 100% { opacity: 0.1; }
50% { opacity: 0.22; }
}
// ===== UI.md 记录模式生物数据仪表盘 =====
.record-bio-home {
--record-clear: #67E8F9;
--record-life: #6EE7B7;
--record-amber: #D97706;
--record-gold: #FBBF24;
--record-lavender: #A78BFA;
--record-text: #1E293B;
@include flex-col;
gap: 20rpx;
color: var(--record-text);
}
.record-bio-status-card,
.record-bio-console-card,
.record-bio-health-card,
.record-bio-tip-card {
position: relative;
overflow: hidden;
border: 1rpx solid rgba(255, 255, 255, 0.82);
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 18rpx 44rpx rgba(30, 41, 59, 0.07);
backdrop-filter: blur(12px);
}
.record-bio-status-card {
padding: 28rpx;
border-radius: 34rpx;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.76), rgba(240, 249, 255, 0.62)),
radial-gradient(circle at top right, rgba(251, 191, 36, 0.17), transparent 34%);
}
.record-bio-breath-line {
position: absolute;
top: 0;
left: 32rpx;
right: 32rpx;
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(251, 191, 36, 0.38), rgba(103, 232, 249, 0.44), transparent);
animation: bioBreathLine 6s ease-in-out infinite;
}
.record-bio-profile-row {
display: flex;
align-items: center;
gap: 18rpx;
}
.record-bio-avatar-medal {
position: relative;
width: 96rpx;
height: 96rpx;
flex-shrink: 0;
@include flex-center;
}
.record-bio-avatar-bg {
position: absolute;
inset: 0;
width: 96rpx;
height: 96rpx;
border-radius: 30rpx;
box-shadow: 0 12rpx 28rpx rgba(15, 118, 110, 0.12);
}
.record-bio-avatar-icon {
position: relative;
z-index: 1;
font-size: 44rpx;
line-height: 1;
}
.record-bio-profile-main {
flex: 1;
min-width: 0;
@include flex-col;
gap: 6rpx;
}
.record-bio-rank {
font-size: 29rpx;
line-height: 1.25;
font-weight: 900;
color: var(--record-text);
}
.record-bio-level {
font-size: 23rpx;
font-weight: 800;
color: #64748B;
}
.record-bio-subtitle {
font-size: 21rpx;
line-height: 1.45;
color: #64748B;
}
.record-bio-target-pill {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6rpx;
padding: 10rpx 14rpx;
border-radius: 999rpx;
background: rgba(236, 253, 245, 0.82);
border: 1rpx solid rgba(16, 185, 129, 0.16);
}
.record-bio-target-icon,
.record-bio-target-value {
font-size: 22rpx;
font-weight: 900;
color: #0F766E;
}
.record-bio-title-panel {
margin-top: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
padding: 18rpx 20rpx;
border-radius: 26rpx;
background:
radial-gradient(circle at 12% 30%, rgba(103, 232, 249, 0.2), transparent 34%),
linear-gradient(135deg, rgba(255, 251, 235, 0.82), rgba(236, 253, 245, 0.72));
border: 1rpx solid rgba(251, 191, 36, 0.18);
}
.record-bio-title-copy {
min-width: 0;
@include flex-col;
gap: 4rpx;
}
.record-bio-title-kicker {
font-size: 18rpx;
font-weight: 900;
letter-spacing: 1.2rpx;
color: #0891B2;
}
.record-bio-title-name {
font-size: 27rpx;
line-height: 1.25;
font-weight: 900;
color: #0F766E;
}
.record-bio-title-hint {
flex-shrink: 0;
max-width: 280rpx;
font-size: 20rpx;
line-height: 1.4;
font-weight: 800;
text-align: right;
color: #B45309;
}
.record-bio-hp-box {
margin-top: 30rpx;
padding: 18rpx;
border-radius: 26rpx;
background: rgba(248, 250, 252, 0.72);
border: 1rpx solid rgba(148, 163, 184, 0.12);
}
.record-bio-hp-track {
position: relative;
height: 24rpx;
border-radius: 999rpx;
overflow: hidden;
background: linear-gradient(90deg, rgba(217, 119, 6, 0.18), rgba(226, 232, 240, 0.66));
}
.record-bio-hp-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--record-life), var(--record-clear));
box-shadow: 0 0 18rpx rgba(103, 232, 249, 0.26);
animation: bioHpPulse 5s ease-in-out infinite;
}
.record-bio-hp-clear {
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.28), transparent);
animation: bioClearFlow 2s ease-in-out infinite;
}
.record-bio-hp-meta {
margin-top: 12rpx;
display: flex;
justify-content: space-between;
font-size: 22rpx;
font-weight: 800;
color: #475569;
}
.record-bio-buff-pill {
align-self: flex-start;
margin-top: 24rpx;
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 18rpx;
border-radius: 999rpx;
border: 1rpx solid rgba(167, 139, 250, 0.36);
background: rgba(250, 245, 255, 0.58);
color: #6D28D9;
font-size: 22rpx;
font-weight: 800;
}
.record-bio-buff-star {
color: var(--record-lavender);
}
.record-bio-console-card {
display: flex;
align-items: center;
gap: 22rpx;
padding: 28rpx;
border-radius: 34rpx;
background:
radial-gradient(circle at 18% 50%, rgba(251, 191, 36, 0.18), transparent 34%),
linear-gradient(135deg, rgba(255, 255, 255, 0.8), rgba(236, 253, 245, 0.62));
}
.record-bio-console-left {
flex-shrink: 0;
}
.record-bio-orbit {
width: 154rpx;
height: 154rpx;
border-radius: 50%;
padding: 12rpx;
box-sizing: border-box;
}
.record-bio-orbit-inner {
width: 100%;
height: 100%;
border-radius: 50%;
@include flex-center;
flex-direction: column;
background: rgba(255, 255, 255, 0.94);
}
.record-bio-orbit-kicker,
.record-bio-orbit-label {
font-size: 18rpx;
font-weight: 800;
color: #64748B;
}
.record-bio-orbit-value {
margin: 5rpx 0;
font-size: 24rpx;
line-height: 1.1;
font-weight: 900;
color: var(--record-text);
}
.record-bio-console-main {
flex: 1;
min-width: 0;
@include flex-col;
gap: 8rpx;
}
.record-bio-console-kicker {
font-size: 19rpx;
font-weight: 900;
letter-spacing: 1.6rpx;
color: #0891B2;
}
.record-bio-console-title {
font-size: 31rpx;
line-height: 1.25;
font-weight: 900;
color: var(--record-text);
}
.record-bio-console-desc {
font-size: 22rpx;
line-height: 1.55;
color: #64748B;
}
.record-bio-action-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12rpx;
margin-top: 12rpx;
}
.record-bio-action {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
height: 64rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 900;
}
.record-bio-action-primary {
background: linear-gradient(135deg, #FBBF24, #D97706);
color: #ffffff;
box-shadow: 0 10rpx 22rpx rgba(217, 119, 6, 0.16);
}
.record-bio-action-ghost {
background: rgba(255, 255, 255, 0.78);
border: 1rpx solid rgba(15, 118, 110, 0.16);
color: #0F766E;
}
.record-bio-action-disabled {
opacity: 0.58;
}
.record-bio-action-icon {
font-size: 24rpx;
font-weight: 900;
}
.record-bio-health-card {
padding: 28rpx;
border-radius: 34rpx;
}
.record-bio-section-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18rpx;
}
.record-bio-section-head-compact {
align-items: center;
}
.record-bio-section-title {
display: block;
font-size: 29rpx;
font-weight: 900;
color: var(--record-text);
}
.record-bio-section-subtitle {
display: block;
margin-top: 8rpx;
font-size: 21rpx;
line-height: 1.45;
color: #64748B;
}
.record-bio-section-chip {
flex-shrink: 0;
padding: 8rpx 14rpx;
border-radius: 999rpx;
background: rgba(255, 251, 235, 0.9);
border: 1rpx solid rgba(251, 191, 36, 0.22);
font-size: 20rpx;
font-weight: 900;
color: #B45309;
}
.record-bio-health-body {
margin-top: 24rpx;
display: flex;
align-items: center;
gap: 26rpx;
}
.record-bio-health-ring {
width: 166rpx;
height: 166rpx;
border-radius: 50%;
padding: 12rpx;
box-sizing: border-box;
flex-shrink: 0;
}
.record-bio-health-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
@include flex-center;
flex-direction: column;
background: rgba(255, 255, 255, 0.92);
box-shadow: inset 0 0 0 1rpx rgba(148, 163, 184, 0.1);
}
.record-bio-health-ring-value {
font-size: 36rpx;
font-weight: 900;
color: var(--record-text);
}
.record-bio-health-ring-label {
margin-top: 4rpx;
font-size: 20rpx;
font-weight: 800;
color: #64748B;
}
.record-bio-metrics {
flex: 1;
min-width: 0;
padding: 18rpx 20rpx;
border-radius: 26rpx;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.78), rgba(255, 255, 255, 0.68));
border: 1rpx solid rgba(148, 163, 184, 0.1);
}
.record-bio-metric-row {
display: flex;
justify-content: space-between;
gap: 14rpx;
padding: 8rpx 0;
}
.record-bio-metric-name {
font-size: 22rpx;
font-weight: 800;
color: #475569;
}
.record-bio-metric-value {
font-size: 22rpx;
font-weight: 900;
color: #0F766E;
}
.record-bio-tip-card {
display: flex;
align-items: flex-start;
gap: 12rpx;
padding: 20rpx 22rpx;
border-radius: 26rpx;
background: rgba(250, 245, 255, 0.54);
border-color: rgba(167, 139, 250, 0.2);
}
.record-bio-tip-icon {
font-size: 24rpx;
color: var(--record-lavender);
flex-shrink: 0;
}
.record-bio-tip-text {
font-size: 22rpx;
line-height: 1.55;
color: #475569;
}
</style>