2302 lines
61 KiB
Vue
2302 lines
61 KiB
Vue
<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="quit-home">
|
||
<view class="quit-hero-card" :style="hpVisualStyle">
|
||
<view class="quit-hero-top">
|
||
<view class="quit-hero-copy">
|
||
<text class="quit-hero-eyebrow">无烟旅程</text>
|
||
<text class="quit-hero-title">{{ quitEncouragement }}</text>
|
||
</view>
|
||
<text class="quit-hero-chip">{{ todayChecked ? '今日已完成' : '等待打卡' }}</text>
|
||
</view>
|
||
|
||
<view class="quit-overview">
|
||
<view class="quit-hp-panel">
|
||
<view class="quit-hp-ring" :style="hpGaugeStyle">
|
||
<view class="quit-hp-ring-inner">
|
||
<text class="quit-hp-ring-kicker">肺部 HP</text>
|
||
<text class="quit-hp-ring-value">{{ hpValue }}</text>
|
||
<text class="quit-hp-ring-label">{{ hpState.label }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quit-hp-copy">
|
||
<text class="quit-hp-title">{{ hpState.title }}</text>
|
||
<text class="quit-hp-desc">{{ hpState.description }}</text>
|
||
<view class="quit-hp-pill-row">
|
||
<view class="quit-hp-pill">
|
||
<text class="quit-hp-pill-label">今日状态</text>
|
||
<text class="quit-hp-pill-value">{{ hpChangeText }}</text>
|
||
</view>
|
||
<view class="quit-hp-pill quit-hp-pill-strong">
|
||
<text class="quit-hp-pill-label">下一节点</text>
|
||
<text class="quit-hp-pill-value">{{ nextHealthMilestoneText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quit-metrics-grid">
|
||
<view class="quit-metric-card">
|
||
<text class="quit-metric-kicker">连续无烟</text>
|
||
<text class="quit-metric-value">{{ quitDays }} 天</text>
|
||
</view>
|
||
<view class="quit-metric-card">
|
||
<text class="quit-metric-kicker">已省下</text>
|
||
<text class="quit-metric-value">¥{{ savedMoney }}</text>
|
||
</view>
|
||
<view class="quit-metric-card">
|
||
<text class="quit-metric-kicker">少抽</text>
|
||
<text class="quit-metric-value">{{ avoidedCigs }} 支</text>
|
||
</view>
|
||
<view class="quit-metric-card quit-metric-card-wide">
|
||
<text class="quit-metric-kicker">延长生命</text>
|
||
<text class="quit-metric-value">{{ lifeSaved }} 小时</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="achievementData" class="quit-ach-inline">
|
||
<view class="quit-ach-left">
|
||
<text class="quit-ach-icon">{{ achievementData.theme_icon }}</text>
|
||
<view class="quit-ach-info">
|
||
<text class="quit-ach-rank">{{ achievementData.current?.name || '--' }}</text>
|
||
<text class="quit-ach-theme">{{ achievementData.theme_name }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="quit-ach-right">
|
||
<view v-if="achievementData.next" class="quit-ach-progress-area">
|
||
<view class="quit-ach-bar">
|
||
<view class="quit-ach-fill" :style="{ width: (achievementData.progress * 100) + '%' }"></view>
|
||
</view>
|
||
<text class="quit-ach-hint">距「{{ achievementData.next.name }}」还需 {{ achievementData.next.required_days - achievementData.days }} 天</text>
|
||
</view>
|
||
<text v-else class="quit-ach-max">已达最高等级</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
class="quit-checkin-card"
|
||
:class="{ 'quit-checkin-card-done': todayChecked }"
|
||
@tap="handleQuitCheckin"
|
||
>
|
||
<view class="quit-checkin-main">
|
||
<view class="quit-checkin-icon" :class="{ 'quit-checkin-icon-done': todayChecked }">
|
||
<text v-if="todayChecked">✓</text>
|
||
<text v-else>+</text>
|
||
</view>
|
||
<view class="quit-checkin-copy">
|
||
<text class="quit-checkin-title">{{ todayChecked ? '今日已打卡' : '今天没抽烟' }}</text>
|
||
<text class="quit-checkin-desc">{{ todayChecked ? `${todayCheckinTime} 已完成记录` : '点击打卡,确认自己度过了无烟的一天' }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="quit-checkin-side">
|
||
<text class="quit-checkin-side-label">{{ todayChecked ? '已完成' : '去打卡' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quit-health-card" :style="hpVisualStyle">
|
||
<view class="quit-health-header">
|
||
<view>
|
||
<text class="quit-health-title">恢复轨迹</text>
|
||
</view>
|
||
<view class="quit-health-badge" :class="{ 'quit-health-badge-done': hpValue >= 100 }">
|
||
{{ `HP ${hpValue}` }}
|
||
</view>
|
||
</view>
|
||
<view class="quit-lung-visual">
|
||
<view class="quit-lung-stage">
|
||
<view class="quit-lung-glow"></view>
|
||
<view class="quit-lung-bubbles">
|
||
<view
|
||
v-for="bubble in lungBubbleItems"
|
||
:key="bubble.id"
|
||
class="quit-lung-bubble"
|
||
:style="{
|
||
left: bubble.left,
|
||
animationDelay: bubble.delay,
|
||
animationDuration: bubble.duration
|
||
}"
|
||
></view>
|
||
</view>
|
||
<view class="quit-lung-figure">
|
||
<view class="quit-lung-trachea"></view>
|
||
<view class="quit-lung-branch quit-lung-branch-left"></view>
|
||
<view class="quit-lung-branch quit-lung-branch-right"></view>
|
||
<view class="quit-lung-lobe quit-lung-lobe-left">
|
||
<view class="quit-lung-lobe-fill" :style="lungRecoveryFillStyle"></view>
|
||
</view>
|
||
<view class="quit-lung-lobe quit-lung-lobe-right">
|
||
<view class="quit-lung-lobe-fill" :style="lungRecoveryFillStyle"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="quit-lung-copy">
|
||
<text class="quit-lung-kicker">无烟 {{ smokeFreeTimeLabel }}</text>
|
||
<text class="quit-lung-phase">{{ lungPhaseLabel }}</text>
|
||
<text class="quit-lung-desc">{{ lungRecoverySummary }}</text>
|
||
<view class="quit-lung-tag-row">
|
||
<view class="quit-lung-tag">
|
||
<text class="quit-lung-tag-label">下一节点</text>
|
||
<text class="quit-lung-tag-value">{{ nextHealthMilestoneText }}</text>
|
||
</view>
|
||
<view class="quit-lung-tag quit-lung-tag-strong">
|
||
<text class="quit-lung-tag-label">恢复节奏</text>
|
||
<text class="quit-lung-tag-value">{{ lungMomentumText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="quit-health-subheader">
|
||
<text class="quit-health-subtitle">恢复节点</text>
|
||
<text class="quit-health-subhint">{{ lungRecoveryCaption }}</text>
|
||
</view>
|
||
<view class="quit-milestone-list">
|
||
<view
|
||
v-for="(ms, index) in healthMilestoneItems"
|
||
:key="index"
|
||
class="quit-ms-item"
|
||
>
|
||
<view class="quit-ms-top">
|
||
<text class="quit-ms-name">{{ ms.name }}</text>
|
||
<text class="quit-ms-pct" :class="{ 'quit-ms-pct-done': ms.percent >= 100 }">{{ ms.percent }}%</text>
|
||
</view>
|
||
<view class="quit-ms-bar">
|
||
<view
|
||
class="quit-ms-fill"
|
||
:class="ms.percent >= 100 ? 'quit-ms-fill-done' : 'quit-ms-fill-pending'"
|
||
:style="{ width: ms.percent + '%' }"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quit-slip-entry" :style="hpVisualStyle" @tap="openSmokeDialog">
|
||
<view class="quit-slip-copy">
|
||
<text class="quit-slip-title">如果刚刚没扛住,先记下来</text>
|
||
<text class="quit-slip-desc">用快速记录留下时间和诱因,比“等会再说”更能帮你找回节奏。</text>
|
||
</view>
|
||
<view class="quit-slip-action">
|
||
<text class="quit-slip-action-main">快速记录</text>
|
||
<text class="quit-slip-action-sub">一根 / 当前时间</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="quit-dream-entry" @tap="gotoDreamGoals">
|
||
<view class="quit-dream-left">
|
||
<text class="quit-dream-icon">🎯</text>
|
||
<view class="quit-dream-info">
|
||
<text class="quit-dream-title">梦想清单</text>
|
||
<text class="quit-dream-desc">{{ activeGoalText }}</text>
|
||
</view>
|
||
</view>
|
||
<text class="quit-dream-arrow">›</text>
|
||
</view>
|
||
|
||
<view class="quit-tip-bar">
|
||
<text class="quit-tip-icon">💡</text>
|
||
<text class="quit-tip-text">{{ healthTip }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 记录模式 -->
|
||
<view v-else class="record-home">
|
||
<view class="record-hero-card" :class="{ 'record-hero-card-empty': !recordHasData }">
|
||
<view class="record-hero-bg record-hero-bg-one"></view>
|
||
<view class="record-hero-bg record-hero-bg-two"></view>
|
||
|
||
<view class="record-hero-header">
|
||
<view class="record-hero-title-block">
|
||
<text class="record-hero-eyebrow">今日控烟节奏</text>
|
||
<text class="record-hero-title">{{ recordHeroTitle }}</text>
|
||
</view>
|
||
<view class="record-hero-chip">
|
||
<text class="record-hero-chip-value">{{ todayCount }}</text>
|
||
<text class="record-hero-chip-label">/{{ dailyTarget || '-' }} 根</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-hero-body">
|
||
<view class="record-timer-panel">
|
||
<view class="record-ring" :style="todayCountRingStyle">
|
||
<view class="record-ring-inner">
|
||
<text class="record-ring-kicker">距上次</text>
|
||
<text class="record-ring-value">{{ timerDisplay }}</text>
|
||
<text class="record-ring-label">抽烟间隔</text>
|
||
</view>
|
||
</view>
|
||
<view class="record-progress-copy">
|
||
<view class="record-progress-row">
|
||
<text class="record-progress-label">今日目标进度</text>
|
||
<text class="record-progress-value">{{ todayCountPercent }}%</text>
|
||
</view>
|
||
<view class="record-progress-track">
|
||
<view class="record-progress-fill" :style="{ width: todayCountPercent + '%' }"></view>
|
||
</view>
|
||
<text class="record-progress-note">{{ recordRhythmText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-today-grid record-today-grid-single">
|
||
<view class="record-today-card record-today-card-smoke">
|
||
<text class="record-today-value">{{ todayCount }}</text>
|
||
<text class="record-today-label">今日已抽</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-action-card">
|
||
<view class="record-action-copy">
|
||
<text class="record-action-title">现在发生了什么?</text>
|
||
<text class="record-action-desc">记录真实瞬间,系统会帮你看见节奏变化</text>
|
||
</view>
|
||
<view class="record-action-buttons">
|
||
<view class="record-action-btn record-action-btn-primary" :class="{ 'record-action-btn-disabled': quickSubmitting }" @tap="quickRecordSmoke">
|
||
<text class="record-action-btn-icon">+</text>
|
||
<text class="record-action-btn-text">快捷记录</text>
|
||
</view>
|
||
<view class="record-action-btn record-action-btn-ghost" @tap="openDetailedSmokeDialog">
|
||
<text class="record-action-btn-icon">✎</text>
|
||
<text class="record-action-btn-text">详细记录</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-insight-card">
|
||
<view class="record-section-header">
|
||
<view>
|
||
<text class="record-section-title">健康回报</text>
|
||
<text class="record-section-subtitle">少抽带来的即时反馈</text>
|
||
</view>
|
||
<text class="record-section-badge">{{ reducePercent }}% 少抽</text>
|
||
</view>
|
||
|
||
<view class="health-metrics">
|
||
<view class="health-metric-item">
|
||
<view class="health-metric-icon health-metric-icon-money"><text>¥</text></view>
|
||
<view class="health-metric-content">
|
||
<text class="health-metric-value">{{ recordSavedMoney }}</text>
|
||
<text class="health-metric-label">今日节省</text>
|
||
</view>
|
||
</view>
|
||
<view class="health-metric-item">
|
||
<view class="health-metric-icon health-metric-icon-time"><text>h</text></view>
|
||
<view class="health-metric-content">
|
||
<text class="health-metric-value">{{ recordLifeSaved }}</text>
|
||
<text class="health-metric-label">生命回收/小时</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="health-tip-bar">
|
||
<text class="health-tip-icon">✦</text>
|
||
<text class="health-tip-text">{{ recordHealthTip }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="achievementData" class="record-achievement-card">
|
||
<view class="ach-header">
|
||
<view>
|
||
<text class="ach-title">{{ achievementData.theme_icon }} {{ achievementCardTitle }}</text>
|
||
<text class="ach-theme-name">{{ achievementData.theme_name }}</text>
|
||
</view>
|
||
<text class="ach-days">第 {{ achievementData.days }} 天</text>
|
||
</view>
|
||
<view class="ach-body">
|
||
<text class="ach-rank">{{ achievementData.current?.name || '--' }}</text>
|
||
<view v-if="achievementData.next" class="ach-progress-wrap">
|
||
<view class="ach-progress-bar">
|
||
<view class="ach-progress-fill" :style="{ width: (achievementData.progress * 100) + '%' }"></view>
|
||
</view>
|
||
<text class="ach-next-hint">距下一级「{{ achievementData.next.name }}」还需 {{ achievementData.next.required_days - achievementData.days }} 天</text>
|
||
</view>
|
||
<view v-else class="ach-max"><text class="ach-max-text">已达最高等级</text></view>
|
||
</view>
|
||
</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 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 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 recordSavedMoney = computed(() => {
|
||
const saved = Math.max(0, (dailyTarget.value - todayCount.value) * (packPriceYuan.value / 20))
|
||
return saved.toFixed(1)
|
||
})
|
||
|
||
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 achievementCardTitle = computed(() => isQuitMode.value ? '无烟等级' : '长期记录等级')
|
||
|
||
// 今日延长生命时长(每少一支 = 11 分钟)
|
||
const recordLifeSaved = computed(() => {
|
||
const minutes = Math.max(0, (dailyTarget.value - todayCount.value) * 11)
|
||
return Math.round(minutes / 60 * 10) / 10
|
||
})
|
||
|
||
const reducePercent = computed(() => {
|
||
if (dailyTarget.value <= 0) return 0
|
||
return Math.round(Math.max(0, dailyTarget.value - todayCount.value) / dailyTarget.value * 100)
|
||
})
|
||
|
||
const recordHealthTip = computed(() => {
|
||
if (todayCount.value === 0) return '今天还没抽烟,继续保持!'
|
||
if (todayCount.value < dailyTarget.value) return `今天比目标少抽了${dailyTarget.value - todayCount.value}根,很棒!`
|
||
if (todayCount.value === dailyTarget.value) return '今天已达到目标,加油!'
|
||
return '今天超标了,明天继续努力'
|
||
})
|
||
|
||
// ========== 戒烟模式 ==========
|
||
|
||
// 服务端连续无烟天数优先,失败时从本地缓存推算
|
||
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)}`
|
||
})
|
||
|
||
// ========== 工具函数 ==========
|
||
|
||
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
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function refreshCurrentMode() {
|
||
if (!userStore.mode) return
|
||
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 profileStore.fetchProfile()
|
||
if (!userStore.mode) {
|
||
uni.reLaunch({ url: '/pages/mode-select/index' })
|
||
return
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
</style>
|