Files
smt/src/pages/index/index.vue
T
2026-03-19 15:41:26 +08:00

1057 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- 背景装饰层 -->
<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>