3222 lines
78 KiB
Vue
3222 lines
78 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="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>
|