1057 lines
27 KiB
Vue
1057 lines
27 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 背景装饰层 -->
|
||
<view class="bg-orb bg-orb-1"></view>
|
||
<view class="bg-orb bg-orb-2"></view>
|
||
<view class="bg-orb bg-orb-3"></view>
|
||
<view class="bg-grid"></view>
|
||
|
||
<view class="nav-placeholder" :style="{ height: navBarHeight + 'px' }"></view>
|
||
|
||
<!-- 骨架屏 -->
|
||
<view v-if="loading" class="skeleton-wrap">
|
||
<view class="sk-hero"></view>
|
||
<view class="sk-card sk-card-wide"></view>
|
||
<view class="sk-action"></view>
|
||
</view>
|
||
|
||
<view v-else class="dashboard">
|
||
<!-- 戒烟模式 -->
|
||
<view v-if="isQuitMode">
|
||
<view class="summary-card">
|
||
<view class="summary-card-top">
|
||
<view>
|
||
<text class="summary-title">今日戒烟状态</text>
|
||
<text class="summary-subtitle">{{ quitSubtitle }}</text>
|
||
</view>
|
||
<view class="summary-mode-badge summary-mode-badge-quit">
|
||
<text class="summary-mode-text">{{ todayChecked ? '今日已打卡' : '今日待打卡' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="summary-main">
|
||
<view class="summary-metrics">
|
||
<view class="summary-metric">
|
||
<text class="summary-metric-label">已坚持</text>
|
||
<view class="summary-metric-value-row">
|
||
<text class="summary-metric-value">{{ quitDays }}</text>
|
||
<text class="summary-metric-unit">天</text>
|
||
</view>
|
||
</view>
|
||
<view class="summary-metric">
|
||
<text class="summary-metric-label">已省下</text>
|
||
<view class="summary-metric-value-row">
|
||
<text class="summary-metric-value">¥{{ savedMoney }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="summary-ring" :style="quitProgressRingStyle">
|
||
<view class="summary-ring-inner">
|
||
<text class="summary-ring-value">{{ healthProgress }}%</text>
|
||
<text class="summary-ring-label">恢复进度</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="primary-row">
|
||
<view class="primary-pill primary-pill-checkin" :class="{ 'primary-pill-done': todayChecked }" @tap="handleQuitCheckin">
|
||
<text class="primary-pill-title">{{ todayChecked ? '今日已打卡' : '今天没抽,去打卡' }}</text>
|
||
<text class="primary-pill-desc">{{ todayChecked ? `已于 ${todayCheckinTime} 记录` : '把今天记成无烟的一天' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="tip-card">
|
||
<text class="tip-title">今日提醒</text>
|
||
<text class="tip-text">{{ quitEncouragement }}</text>
|
||
<text class="tip-extra">{{ healthTip }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 记录模式 -->
|
||
<view v-else>
|
||
<view class="summary-card summary-card-record">
|
||
<view class="summary-card-top summary-card-top-record">
|
||
<view class="summary-heading">
|
||
<view class="summary-title-accent"></view>
|
||
<view>
|
||
<text class="summary-title summary-title-record">记录</text>
|
||
</view>
|
||
</view>
|
||
<text class="record-header-more">···</text>
|
||
</view>
|
||
|
||
<view class="record-overview">
|
||
<view class="record-ring" :style="todayCountRingStyle">
|
||
<view class="record-ring-inner">
|
||
<text class="record-ring-value">{{ timerDisplay }}</text>
|
||
<text class="record-ring-label">距上次抽烟</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-metric-group">
|
||
<view class="record-metric-list">
|
||
<view class="record-metric-item">
|
||
<view class="summary-metric-value-row">
|
||
<text class="summary-metric-value">{{ resistedCount }}</text>
|
||
<text class="summary-metric-unit">次</text>
|
||
</view>
|
||
<text class="summary-metric-label">今日忍住</text>
|
||
</view>
|
||
<view class="record-metric-item">
|
||
<view class="summary-metric-value-row">
|
||
<text class="summary-metric-value">{{ todayCount }}</text>
|
||
<text class="summary-metric-unit">根</text>
|
||
</view>
|
||
<text class="summary-metric-label">今日已抽</text>
|
||
</view>
|
||
</view>
|
||
<text class="record-group-note">{{ nextSmokeTimeText ? `建议下次 ${nextSmokeTimeText}` : changeText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="record-action-row">
|
||
<view class="primary-pill primary-pill-smoke" @tap="openSmokeDialog">
|
||
<text class="primary-pill-title">记录</text>
|
||
</view>
|
||
<view class="primary-pill primary-pill-resist" @tap="openResistedDialog">
|
||
<text class="primary-pill-title">忍住</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<smoke-record-dialog
|
||
v-if="!isQuitMode"
|
||
v-model:show="showDialog"
|
||
:type="dialogType"
|
||
@submit="handleSubmit"
|
||
/>
|
||
</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'
|
||
|
||
const profileStore = useProfileStore()
|
||
const userStore = useUserStore()
|
||
const { waitForLogin } = useLogin()
|
||
|
||
const loading = ref(true)
|
||
const navBarHeight = ref(0)
|
||
const showDialog = ref(false)
|
||
const dialogType = ref('smoke')
|
||
const homeData = ref(null)
|
||
const pageReady = ref(false)
|
||
const quitState = ref(defaultQuitState())
|
||
|
||
let timerInterval = null
|
||
const timerBaseSeconds = ref(-1)
|
||
const timerSeconds = ref(0)
|
||
|
||
const isQuitMode = computed(() => userStore.mode === 'quit')
|
||
const homeSummary = computed(() => homeData.value?.summary || {})
|
||
const homeTimer = computed(() => homeData.value?.timer || {})
|
||
|
||
const userName = computed(() => homeData.value?.greeting?.nickname || userStore.user?.nickname || '戒烟用户')
|
||
const userAvatar = computed(() => homeData.value?.greeting?.avatar_url || userStore.user?.avatar_url || 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png')
|
||
|
||
const greetingTitle = computed(() => {
|
||
const hour = new Date().getHours()
|
||
let greeting = '晚上好'
|
||
if (hour < 6) greeting = '凌晨好'
|
||
else if (hour < 12) greeting = '早上好'
|
||
else if (hour < 14) greeting = '中午好'
|
||
else if (hour < 18) greeting = '下午好'
|
||
return `${greeting},${userName.value}`
|
||
})
|
||
|
||
const greetingSubtitle = computed(() => {
|
||
if (isQuitMode.value) {
|
||
return todayChecked.value ? '今天已经记下来了,继续保持。' : '先把今天记成无烟的一天。'
|
||
}
|
||
return '记录越及时,后面的趋势越准。'
|
||
})
|
||
|
||
const todayCount = computed(() => homeSummary.value.today_count ?? 0)
|
||
const dailyTarget = computed(() => {
|
||
const target = homeSummary.value.daily_target
|
||
if (target !== undefined && target !== null) return target
|
||
return profileStore.profile?.baseline_cigs_per_day || 0
|
||
})
|
||
const resistedCount = computed(() => homeSummary.value.resisted_count ?? 0)
|
||
|
||
const changeText = computed(() => {
|
||
const reduced = homeSummary.value.reduced_from_yesterday
|
||
if (reduced === undefined || reduced === null) return '较昨日暂无对比'
|
||
if (reduced === 0) return '较昨日持平'
|
||
return homeSummary.value.exceeded_yesterday ? `较昨日多 ${reduced} 根` : `较昨日少 ${reduced} 根`
|
||
})
|
||
|
||
const timerDisplay = computed(() => {
|
||
if (timerBaseSeconds.value < 0) return '--:--:--'
|
||
const totalSeconds = timerBaseSeconds.value + timerSeconds.value
|
||
const hours = Math.floor(totalSeconds / 3600)
|
||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||
const seconds = totalSeconds % 60
|
||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
|
||
})
|
||
|
||
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)
|
||
if (Number.isNaN(date.getTime())) return ''
|
||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||
})
|
||
|
||
const baselineCigsPerDay = computed(() => profileStore.profile?.baseline_cigs_per_day || 10)
|
||
const packPriceYuan = computed(() => (profileStore.profile?.pack_price_cent || 2500) / 100)
|
||
const quitDays = computed(() => {
|
||
if (!quitState.value.lastCheckinDate) return 0
|
||
const gap = diffDays(quitState.value.lastCheckinDate, formatDate(new Date()))
|
||
if (gap > 1) return 0
|
||
return Number(quitState.value.streakDays || 0)
|
||
})
|
||
const todayChecked = computed(() => quitState.value.lastCheckinDate === formatDate(new Date()))
|
||
const todayCheckinTime = computed(() => formatClock(quitState.value.lastCheckinAt))
|
||
const savedMoney = computed(() => {
|
||
const total = (quitDays.value * baselineCigsPerDay.value / 20) * packPriceYuan.value
|
||
return Math.round(total)
|
||
})
|
||
const healthProgress = computed(() => {
|
||
if (quitDays.value >= 365) return 100
|
||
if (quitDays.value >= 180) return 86
|
||
if (quitDays.value >= 90) return 72
|
||
if (quitDays.value >= 30) return 58
|
||
if (quitDays.value >= 14) return 38
|
||
if (quitDays.value >= 7) return 18
|
||
if (quitDays.value >= 1) return 6
|
||
return 0
|
||
})
|
||
const healthTip = computed(() => {
|
||
if (quitDays.value >= 365) return '肺部功能已进入长期恢复阶段'
|
||
if (quitDays.value >= 180) return '血液循环和咳嗽症状通常会继续改善'
|
||
if (quitDays.value >= 30) return '味觉和嗅觉通常会逐步恢复'
|
||
if (quitDays.value >= 7) return '一周后,呼吸会比开始时更轻松一些'
|
||
if (quitDays.value >= 1) return '第一阶段最难,坚持住就有意义'
|
||
return '打卡从今天开始,先拿下第一天'
|
||
})
|
||
const quitSubtitle = computed(() => {
|
||
if (!quitState.value.lastCheckinDate) return '还没有开始记录,先拿下第一天。'
|
||
const gap = diffDays(quitState.value.lastCheckinDate, formatDate(new Date()))
|
||
if (gap > 1) return '连续记录已中断,今天重新开始。'
|
||
if (todayChecked.value) return '今天已经打卡,明天继续。'
|
||
return '别漏掉今天这次打卡。'
|
||
})
|
||
const quitEncouragement = computed(() => {
|
||
if (todayChecked.value) return '今天的无烟记录已经落下,尽量把第一支拖得更晚一些。'
|
||
if (quitDays.value === 0) return '先不要想很久,只把今天守住就够了。'
|
||
return `连续 ${quitDays.value} 天很难得,今天再补上一天。`
|
||
})
|
||
const todayCountPercent = computed(() => {
|
||
if (!dailyTarget.value || dailyTarget.value <= 0) return todayCount.value > 0 ? 100 : 0
|
||
const percent = Math.round((todayCount.value / dailyTarget.value) * 100)
|
||
return Math.min(Math.max(percent, 0), 100)
|
||
})
|
||
const todayCountRingStyle = computed(() => {
|
||
const angle = Math.round(todayCountPercent.value * 3.6)
|
||
return {
|
||
background: `conic-gradient(#3fcba2 0deg ${angle}deg, #d8eee6 ${angle}deg 360deg)`
|
||
}
|
||
})
|
||
const quitProgressRingStyle = computed(() => {
|
||
const angle = Math.round(healthProgress.value * 3.6)
|
||
return {
|
||
background: `conic-gradient(#14b882 0deg ${angle}deg, #e8edf2 ${angle}deg 360deg)`
|
||
}
|
||
})
|
||
|
||
function defaultQuitState() {
|
||
return { lastCheckinDate: '', lastCheckinAt: '', streakDays: 0 }
|
||
}
|
||
|
||
function formatDate(date) {
|
||
const y = date.getFullYear()
|
||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||
const d = String(date.getDate()).padStart(2, '0')
|
||
return `${y}-${m}-${d}`
|
||
}
|
||
|
||
function diffDays(fromDate, toDate) {
|
||
if (!fromDate || !toDate) return 0
|
||
const from = new Date(`${fromDate}T00:00:00`)
|
||
const to = new Date(`${toDate}T00:00:00`)
|
||
return Math.floor((to.getTime() - from.getTime()) / (24 * 60 * 60 * 1000))
|
||
}
|
||
|
||
function formatClock(value) {
|
||
if (!value) return '--:--'
|
||
const date = new Date(value)
|
||
if (Number.isNaN(date.getTime())) return '--:--'
|
||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||
}
|
||
|
||
function setupNavBar() {
|
||
const systemInfo = uni.getSystemInfoSync()
|
||
const statusBarH = systemInfo.statusBarHeight || 0
|
||
try {
|
||
const menuBtn = uni.getMenuButtonBoundingClientRect()
|
||
navBarHeight.value = menuBtn.bottom + (menuBtn.top - statusBarH)
|
||
} catch (e) {
|
||
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 openSmokeDialog() {
|
||
dialogType.value = 'smoke'
|
||
showDialog.value = true
|
||
}
|
||
|
||
function openResistedDialog() {
|
||
dialogType.value = 'resisted'
|
||
showDialog.value = true
|
||
}
|
||
|
||
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()
|
||
const data = res.data || {}
|
||
applyHomeData(data)
|
||
return data
|
||
}
|
||
|
||
async function handleSubmit(submitData) {
|
||
try {
|
||
if (dialogType.value === 'smoke') {
|
||
await api.createLog(submitData)
|
||
timerBaseSeconds.value = 0
|
||
timerSeconds.value = 0
|
||
startTimer()
|
||
uni.showToast({ title: '记录成功', icon: 'success' })
|
||
} else {
|
||
await api.createResistedLog({
|
||
smoke_time: submitData.smoke_time,
|
||
smoke_at: submitData.smoke_at,
|
||
remark: submitData.remark,
|
||
level: submitData.level,
|
||
num: submitData.num
|
||
})
|
||
uni.showToast({ title: '已记下这次忍住', icon: 'success' })
|
||
}
|
||
await fetchRecordHomeData()
|
||
} catch (e) {
|
||
console.error('handleSubmit error:', e)
|
||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
function handleQuitCheckin() {
|
||
if (todayChecked.value) {
|
||
uni.showToast({ title: '今天已经打过卡', icon: 'none' })
|
||
return
|
||
}
|
||
const today = formatDate(new Date())
|
||
const previousDate = quitState.value.lastCheckinDate
|
||
let streakDays = 1
|
||
if (previousDate) {
|
||
const gap = diffDays(previousDate, today)
|
||
if (gap === 1) streakDays = Number(quitState.value.streakDays || 0) + 1
|
||
else if (gap === 0) streakDays = Number(quitState.value.streakDays || 0)
|
||
}
|
||
saveQuitState({ lastCheckinDate: today, lastCheckinAt: new Date().toISOString(), streakDays })
|
||
uni.showToast({ title: '打卡成功', icon: 'success' })
|
||
}
|
||
|
||
async function ensureProfileReady() {
|
||
const profileData = await profileStore.fetchProfile()
|
||
const profile = profileData.profile
|
||
const isCompleted = profileData.is_completed ||
|
||
(profile && profile.onboarding_completed_at) ||
|
||
(profile && profile.baseline_cigs_per_day > 0)
|
||
if (!profileData.exists || !isCompleted) {
|
||
uni.navigateTo({ url: '/pages/onboarding/index' })
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function refreshCurrentMode() {
|
||
if (!userStore.mode) return
|
||
const profileReady = await ensureProfileReady()
|
||
if (!profileReady) return
|
||
if (isQuitMode.value) {
|
||
stopTimer()
|
||
loadQuitState()
|
||
return
|
||
}
|
||
await fetchRecordHomeData()
|
||
}
|
||
|
||
async function initPage() {
|
||
setupNavBar()
|
||
loading.value = true
|
||
try {
|
||
await waitForLogin()
|
||
await profileStore.fetchProfile()
|
||
if (!userStore.mode) {
|
||
uni.navigateTo({ 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() })
|
||
|
||
onShareAppMessage(() => ({
|
||
title: isQuitMode.value ? '我在坚持戒烟打卡' : '我在记录自己的抽烟变化',
|
||
path: 'pages/index/index'
|
||
}))
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ===== 页面基础 ===== */
|
||
.page {
|
||
min-height: 100vh;
|
||
position: relative;
|
||
background: #F4F6F8;
|
||
overflow: hidden;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* ===== 背景装饰 ===== */
|
||
.bg-orb {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
filter: blur(80rpx);
|
||
}
|
||
|
||
.bg-orb-1 {
|
||
width: 600rpx;
|
||
height: 600rpx;
|
||
top: -100rpx;
|
||
left: -150rpx;
|
||
background: radial-gradient(circle, rgba(20, 184, 130, 0.1) 0%, transparent 70%);
|
||
}
|
||
|
||
.bg-orb-2 {
|
||
width: 500rpx;
|
||
height: 500rpx;
|
||
top: 300rpx;
|
||
right: -180rpx;
|
||
background: radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%);
|
||
}
|
||
|
||
.bg-orb-3 {
|
||
width: 400rpx;
|
||
height: 400rpx;
|
||
bottom: 200rpx;
|
||
left: 50rpx;
|
||
background: radial-gradient(circle, rgba(20, 184, 130, 0.06) 0%, transparent 70%);
|
||
}
|
||
|
||
.bg-grid {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(0,0,0,0.02) 1rpx, transparent 1rpx),
|
||
linear-gradient(90deg, rgba(0,0,0,0.02) 1rpx, transparent 1rpx);
|
||
background-size: 80rpx 80rpx;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ===== 骨架屏 ===== */
|
||
.skeleton-wrap {
|
||
padding: 32rpx 28rpx;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.sk-header, .sk-hero, .sk-card, .sk-action {
|
||
background: linear-gradient(90deg, #ebebeb 25%, #f5f5f5 50%, #ebebeb 75%);
|
||
background-size: 200% 100%;
|
||
animation: shimmer 1.6s infinite;
|
||
border-radius: 24rpx;
|
||
}
|
||
|
||
.sk-hero { height: 320rpx; margin-bottom: 24rpx; border-radius: 36rpx; }
|
||
.sk-card { height: 120rpx; border-radius: 28rpx; margin-bottom: 24rpx; }
|
||
.sk-card-wide { height: 140rpx; }
|
||
.sk-action { height: 140rpx; border-radius: 28rpx; }
|
||
|
||
@keyframes shimmer {
|
||
0% { background-position: -200% 0; }
|
||
100% { background-position: 200% 0; }
|
||
}
|
||
|
||
/* ===== 主内容 ===== */
|
||
.nav-placeholder, .dashboard, .skeleton-wrap {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.dashboard {
|
||
padding: 20rpx 20rpx 180rpx;
|
||
}
|
||
|
||
.summary-card {
|
||
background: rgba(255, 255, 255, 0.85);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
backdrop-filter: blur(20px);
|
||
border-radius: 40rpx;
|
||
border: 2rpx solid rgba(255, 255, 255, 0.9);
|
||
padding: 40rpx 36rpx;
|
||
margin-bottom: 32rpx;
|
||
box-shadow: 0 24rpx 48rpx rgba(20, 184, 130, 0.05), inset 0 2rpx 8rpx rgba(255, 255, 255, 1);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.summary-card-record {
|
||
background: linear-gradient(145deg, rgba(235, 251, 245, 0.9) 0%, rgba(255, 255, 255, 0.95) 100%);
|
||
border-color: rgba(20, 184, 130, 0.15);
|
||
box-shadow: 0 24rpx 56rpx rgba(20, 184, 130, 0.08), inset 0 2rpx 8rpx rgba(255, 255, 255, 1);
|
||
padding: 28rpx 24rpx;
|
||
border-radius: 32rpx;
|
||
}
|
||
|
||
.summary-card-top {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.summary-card-top-record {
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.record-header-more {
|
||
font-size: 42rpx;
|
||
line-height: 1;
|
||
font-weight: 700;
|
||
letter-spacing: 4rpx;
|
||
color: #1f4b3d;
|
||
transform: translateY(-4rpx);
|
||
}
|
||
|
||
.summary-heading {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.summary-title-accent {
|
||
width: 10rpx;
|
||
height: 44rpx;
|
||
border-radius: 999rpx;
|
||
margin-top: 4rpx;
|
||
flex-shrink: 0;
|
||
background: linear-gradient(180deg, #6ee7be 0%, #2fc596 100%);
|
||
box-shadow: 0 4rpx 8rpx rgba(47, 197, 150, 0.3);
|
||
}
|
||
|
||
.summary-title {
|
||
display: block;
|
||
font-size: 34rpx;
|
||
font-weight: 800;
|
||
color: #1a1a1a;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
|
||
.summary-title-record {
|
||
color: #123329;
|
||
}
|
||
|
||
.summary-subtitle {
|
||
display: block;
|
||
margin-top: 10rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
color: #7b8a84;
|
||
}
|
||
|
||
.summary-subtitle-record {
|
||
margin-top: 8rpx;
|
||
color: #5b8d7a;
|
||
}
|
||
|
||
.summary-mode-badge {
|
||
padding: 12rpx 20rpx;
|
||
border-radius: 999rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.summary-mode-badge-quit {
|
||
background: rgba(20, 184, 130, 0.1);
|
||
border: 1px solid rgba(20, 184, 130, 0.15);
|
||
}
|
||
|
||
.summary-mode-badge-record {
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border: 1rpx solid rgba(77, 144, 119, 0.2);
|
||
}
|
||
|
||
.summary-mode-text {
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
color: #14b882;
|
||
}
|
||
|
||
.summary-card-record .summary-mode-text {
|
||
color: #2a7b61;
|
||
}
|
||
|
||
.summary-main {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.summary-main-record {
|
||
align-items: stretch;
|
||
gap: 18rpx;
|
||
}
|
||
|
||
.record-overview {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.record-ring {
|
||
width: 188rpx;
|
||
height: 188rpx;
|
||
padding: 12rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-shadow: inset 0 4rpx 12rpx rgba(0,0,0,0.03), 0 4rpx 12rpx rgba(255,255,255,0.8);
|
||
background: rgba(248, 255, 251, 0.4);
|
||
}
|
||
|
||
.record-ring-inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 4rpx 16rpx rgba(20, 184, 130, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 0 16rpx;
|
||
}
|
||
|
||
.record-ring-value {
|
||
font-size: 34rpx;
|
||
line-height: 1.2;
|
||
font-weight: 800;
|
||
color: #14b882;
|
||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||
}
|
||
|
||
.record-ring-label {
|
||
margin-top: 8rpx;
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
color: #5c8274;
|
||
}
|
||
|
||
.record-metric-group {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.record-metric-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 0;
|
||
border-radius: 28rpx;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.6);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
backdrop-filter: blur(10px);
|
||
border: 1rpx solid rgba(255, 255, 255, 0.8);
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.record-metric-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6rpx;
|
||
min-height: 108rpx;
|
||
padding: 18rpx 12rpx;
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
|
||
.record-metric-item + .record-metric-item::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 28rpx;
|
||
bottom: 28rpx;
|
||
width: 2rpx;
|
||
background: rgba(20, 184, 130, 0.1);
|
||
}
|
||
|
||
.summary-metric-label {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
color: #8b9691;
|
||
margin-top: 0;
|
||
}
|
||
|
||
.record-metric-item .summary-metric-label {
|
||
color: #6b9184;
|
||
}
|
||
|
||
.summary-metric-value-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.summary-metric-value {
|
||
font-size: 48rpx;
|
||
font-weight: 800;
|
||
color: #1a1a1a;
|
||
line-height: 1;
|
||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||
}
|
||
|
||
.record-metric-item .summary-metric-value {
|
||
color: #14b882;
|
||
}
|
||
|
||
.summary-metric-unit {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #888888;
|
||
}
|
||
|
||
.record-metric-item .summary-metric-unit {
|
||
color: #52776a;
|
||
}
|
||
|
||
.summary-metric-note {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #999999;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.record-group-note {
|
||
display: block;
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 500;
|
||
line-height: 1.5;
|
||
color: #6f9487;
|
||
background: rgba(20, 184, 130, 0.05);
|
||
padding: 10rpx 14rpx;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.primary-row {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-bottom: 28rpx;
|
||
}
|
||
|
||
.primary-row-double {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
}
|
||
|
||
.primary-pill {
|
||
flex: 1;
|
||
min-height: 120rpx;
|
||
padding: 24rpx 32rpx;
|
||
border-radius: 36rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
backdrop-filter: blur(10px);
|
||
border: 2rpx solid rgba(255, 255, 255, 1);
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.03);
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.primary-pill:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.primary-pill-checkin {
|
||
background: linear-gradient(135deg, #14b882 0%, #0ca674 100%);
|
||
border: none;
|
||
box-shadow: 0 16rpx 32rpx rgba(20, 184, 130, 0.25);
|
||
}
|
||
|
||
.primary-pill-done {
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.9);
|
||
box-shadow: none;
|
||
}
|
||
|
||
.primary-pill-smoke {
|
||
background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%);
|
||
border: none;
|
||
box-shadow: 0 16rpx 32rpx rgba(79, 70, 229, 0.25);
|
||
}
|
||
|
||
.primary-pill-resist {
|
||
background: linear-gradient(135deg, #14b882 0%, #0ca674 100%);
|
||
border: none;
|
||
box-shadow: 0 16rpx 32rpx rgba(20, 184, 130, 0.25);
|
||
}
|
||
|
||
.record-action-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20rpx;
|
||
margin-top: 32rpx;
|
||
}
|
||
|
||
.record-action-row .primary-pill {
|
||
min-height: 96rpx;
|
||
padding: 0 20rpx;
|
||
align-items: center;
|
||
text-align: center;
|
||
border-radius: 999rpx;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
backdrop-filter: blur(10px);
|
||
border: 2rpx solid rgba(255, 255, 255, 1);
|
||
box-shadow: 0 12rpx 28rpx rgba(83, 157, 128, 0.1);
|
||
}
|
||
|
||
.record-action-row .primary-pill-smoke {
|
||
background: linear-gradient(180deg, rgba(255, 242, 240, 0.9) 0%, rgba(255, 224, 218, 0.9) 100%);
|
||
border-color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
|
||
.record-action-row .primary-pill-resist {
|
||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(241, 255, 248, 0.95) 100%);
|
||
}
|
||
|
||
.primary-pill-title {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
font-weight: 800;
|
||
color: #ffffff;
|
||
}
|
||
|
||
.record-action-row .primary-pill-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.record-action-row .primary-pill-smoke .primary-pill-title {
|
||
color: #cb5a49;
|
||
}
|
||
|
||
.record-action-row .primary-pill-resist .primary-pill-title {
|
||
color: #1f755e;
|
||
}
|
||
|
||
.primary-pill-desc {
|
||
display: block;
|
||
margin-top: 8rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
color: rgba(255, 255, 255, 0.85);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.primary-pill-done .primary-pill-title {
|
||
color: #666666;
|
||
}
|
||
|
||
.primary-pill-done .primary-pill-desc {
|
||
color: #aaaaaa;
|
||
}
|
||
|
||
/* ===== 提示卡 ===== */
|
||
.tip-card {
|
||
padding: 36rpx 32rpx;
|
||
border-radius: 36rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
-webkit-backdrop-filter: blur(20px);
|
||
backdrop-filter: blur(20px);
|
||
border: 2rpx solid rgba(255, 255, 255, 0.9);
|
||
box-shadow: 0 12rpx 32rpx rgba(20, 184, 130, 0.04);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tip-card::after {
|
||
content: '"';
|
||
position: absolute;
|
||
top: -10rpx;
|
||
right: 30rpx;
|
||
font-size: 140rpx;
|
||
color: rgba(20, 184, 130, 0.06);
|
||
font-family: Georgia, serif;
|
||
line-height: 1;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.tip-title {
|
||
display: inline-block;
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 12rpx;
|
||
background: rgba(20, 184, 130, 0.1);
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
color: #14b882;
|
||
letter-spacing: 1rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.tip-text {
|
||
display: block;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #333333;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.tip-extra {
|
||
display: block;
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
color: #888888;
|
||
}
|
||
|
||
/* 补充缺失的戒烟模式统同样式 */
|
||
.summary-metrics {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 24rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.summary-metric {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.summary-ring {
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
padding: 12rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
box-shadow: inset 0 2rpx 8rpx rgba(0,0,0,0.02), 0 4rpx 12rpx rgba(255,255,255,0.8);
|
||
background: rgba(20, 184, 130, 0.05);
|
||
}
|
||
|
||
.summary-ring-inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 50%;
|
||
background: #ffffff;
|
||
box-shadow: 0 4rpx 16rpx rgba(20, 184, 130, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.summary-ring-value {
|
||
font-size: 32rpx;
|
||
line-height: 1.2;
|
||
font-weight: 800;
|
||
color: #14b882;
|
||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||
}
|
||
|
||
.summary-ring-label {
|
||
margin-top: 6rpx;
|
||
font-size: 20rpx;
|
||
font-weight: 500;
|
||
color: #7b8a84;
|
||
}
|
||
</style>
|