feat(home): add hp-first quit dashboard experience

This commit is contained in:
nepiedg
2026-04-16 11:11:33 +08:00
parent 7ef21ad39d
commit fefda9ec97
+222 -49
View File
@@ -17,7 +17,7 @@
<view v-else class="content">
<!-- 戒烟模式 -->
<view v-if="isQuitMode" class="quit-home">
<view class="quit-hero-card">
<view class="quit-hero-card" :style="hpVisualStyle">
<view class="quit-hero-top">
<view class="quit-hero-copy">
<text class="quit-hero-eyebrow">无烟旅程</text>
@@ -27,17 +27,36 @@
</view>
<view class="quit-overview">
<view class="quit-days-card">
<view class="quit-days-ring">
<view class="quit-days-ring-inner">
<text class="quit-days-number">{{ quitDays }}</text>
<text class="quit-days-label"></text>
<view class="quit-hp-panel">
<view class="quit-hp-ring" :style="hpGaugeStyle">
<view class="quit-hp-ring-inner">
<text class="quit-hp-ring-kicker">肺部 HP</text>
<text class="quit-hp-ring-value">{{ hpValue }}</text>
<text class="quit-hp-ring-label">{{ hpState.label }}</text>
</view>
</view>
<view class="quit-hp-copy">
<text class="quit-hp-title">{{ hpState.title }}</text>
<text class="quit-hp-desc">{{ hpState.description }}</text>
<view class="quit-hp-pill-row">
<view class="quit-hp-pill">
<text class="quit-hp-pill-label">今日状态</text>
<text class="quit-hp-pill-value">{{ hpChangeText }}</text>
</view>
<view class="quit-hp-pill quit-hp-pill-strong">
<text class="quit-hp-pill-label">下一节点</text>
<text class="quit-hp-pill-value">{{ nextHealthMilestoneText }}</text>
</view>
</view>
</view>
<text class="quit-days-hint">连续无烟</text>
</view>
<view class="quit-metrics-grid">
<view class="quit-metric-card">
<text class="quit-metric-kicker">连续无烟</text>
<text class="quit-metric-value">{{ quitDays }} </text>
</view>
<view class="quit-metric-card">
<text class="quit-metric-kicker">已省下</text>
<text class="quit-metric-value">¥{{ savedMoney }}</text>
@@ -93,13 +112,13 @@
</view>
</view>
<view class="quit-health-card">
<view class="quit-health-card" :style="hpVisualStyle">
<view class="quit-health-header">
<view>
<text class="quit-health-title">肺部恢复可视化</text>
<text class="quit-health-title">恢复轨迹</text>
</view>
<view class="quit-health-badge" :class="{ 'quit-health-badge-done': healthProgress >= 100 }">
{{ healthProgress >= 100 ? '已达成' : `${healthProgress}%` }}
<view class="quit-health-badge" :class="{ 'quit-health-badge-done': hpValue >= 100 }">
{{ `HP ${hpValue}` }}
</view>
</view>
<view class="quit-lung-visual">
@@ -389,8 +408,8 @@
<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">肺部恢复</text>
<text class="checkin-celebration-chip-value">{{ healthProgress }}%</text>
<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>
@@ -533,6 +552,87 @@ const healthProgress = computed(() => {
return 0
})
const hpValue = computed(() => {
const raw = quitSummary.value.hp_current ?? healthProgress.value
const value = Number(raw)
if (Number.isNaN(value)) return 0
return Math.min(Math.max(Math.round(value), 0), 100)
})
const hpState = computed(() => {
if (hpValue.value <= 20) {
return {
label: '危险期',
title: '先保住今天的底线',
description: '身体恢复动力偏弱,先把眼前这一波烟瘾稳住最重要。',
accent: '#64748b',
soft: '#e2e8f0',
deep: '#334155'
}
}
if (hpValue.value <= 40) {
return {
label: '恢复中',
title: '状态正在往回拉',
description: '已经开始脱离最低谷,再多守住几次关键时刻,HP 会明显回升。',
accent: '#0f766e',
soft: '#ccfbf1',
deep: '#115e59'
}
}
if (hpValue.value <= 60) {
return {
label: '稳定期',
title: '节奏开始站稳',
description: '你已经进入可持续恢复阶段,规律打卡会让状态越来越稳。',
accent: '#0891b2',
soft: '#cffafe',
deep: '#155e75'
}
}
if (hpValue.value <= 80) {
return {
label: '强韧期',
title: '恢复势头很不错',
description: '肺部状态正在加速恢复,继续把高风险场景提前处理掉。',
accent: '#16a34a',
soft: '#dcfce7',
deep: '#166534'
}
}
return {
label: '高能期',
title: '你已经进入高能状态',
description: '当前状态非常好,保持日常节奏和打卡,就能继续巩固恢复成果。',
accent: '#ea580c',
soft: '#ffedd5',
deep: '#9a3412'
}
})
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 serverDelta = Number(quitSummary.value.hp_change_today ?? quitSummary.value.hp_delta_today ?? quitSummary.value.hp_change)
if (!Number.isNaN(serverDelta) && serverDelta !== 0) {
return serverDelta > 0 ? `今日 +${serverDelta}` : `今日 ${serverDelta}`
}
if (todayChecked.value) return '今日已保住状态'
if (quitDays.value > 0) return '先打卡再继续回血'
return '从今天开始积累'
})
const healthTip = computed(() => {
if (quitDays.value >= 365) return '肺部功能显著改善,心血管疾病风险大幅降低'
if (quitDays.value >= 180) return '血液循环持续改善,肺功能逐步恢复'
@@ -592,15 +692,17 @@ const smokeFreeTimeLabel = computed(() => {
})
const lungRecoveryFillStyle = computed(() => ({
height: `${Math.max(healthProgress.value, 6)}%`
height: `${Math.max(hpValue.value, 6)}%`,
background: `linear-gradient(180deg, ${hpState.value.soft} 0%, ${hpState.value.accent} 100%)`
}))
const lungPhaseLabel = computed(() => {
if (healthProgress.value >= 100) return '肺功能已接近长期恢复水平'
if (healthProgress.value >= 70) return '肺部纤毛清理能力持续增强'
if (healthProgress.value >= 40) return '呼吸效率进入稳定恢复期'
if (healthProgress.value >= 15) return '肺部正在排出残留刺激物'
return '身体已启动自我修复'
if (hpValue.value >= 100) return '肺部状态已进入长期巩固阶段'
if (hpValue.value >= 80) return '当前恢复速度很稳定'
if (hpValue.value >= 60) return '肺部恢复节奏正在变强'
if (hpValue.value >= 40) return '身体正在持续找回呼吸效率'
if (hpValue.value >= 20) return '恢复已经启动,别让节奏断掉'
return '先把今天守住,HP 就会慢慢往上走'
})
const lungRecoverySummary = computed(() => {
@@ -616,9 +718,9 @@ const nextHealthMilestoneText = computed(() => {
})
const lungMomentumText = computed(() => {
if (healthProgress.value >= 100) return '稳定达成'
if (healthProgress.value >= 60) return '恢复加速中'
if (healthProgress.value >= 20) return '持续升'
if (hpValue.value >= 100) return '稳定达成'
if (hpValue.value >= 70) return '恢复加速中'
if (hpValue.value >= 35) return '持续升'
return '刚刚启动'
})
@@ -1130,12 +1232,15 @@ onShareAppMessage(() => ({
left: -72rpx;
top: -72rpx;
border-radius: 50%;
background: radial-gradient(circle, rgba(52, 200, 160, 0.14) 0%, rgba(52, 200, 160, 0) 72%);
background: radial-gradient(circle, var(--hp-soft, rgba(52, 200, 160, 0.18)) 0%, rgba(52, 200, 160, 0) 72%);
pointer-events: none;
}
.quit-hero-card {
padding: 28rpx 24rpx;
--hp-accent: #14936d;
--hp-soft: #def7ec;
--hp-deep: #0f766e;
}
.quit-hero-top,
@@ -1177,7 +1282,7 @@ onShareAppMessage(() => ({
font-size: 28rpx;
line-height: 1.5;
font-weight: 700;
color: #14936d;
color: var(--hp-accent);
}
.quit-hero-chip {
@@ -1193,63 +1298,131 @@ onShareAppMessage(() => ({
.quit-overview {
display: flex;
align-items: center;
flex-direction: column;
gap: 18rpx;
}
.quit-days-card {
width: 216rpx;
flex-shrink: 0;
.quit-hp-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: 18rpx;
padding: 18rpx;
border-radius: 26rpx;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.94) 0%, rgba(248, 251, 249, 0.92) 100%);
border: 1rpx solid rgba(15, 23, 42, 0.05);
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
}
.quit-days-ring {
.quit-hp-ring {
width: 200rpx;
height: 200rpx;
padding: 14rpx;
padding: 12rpx;
border-radius: 50%;
background: linear-gradient(180deg, #ffffff 0%, #f8fbf9 100%);
border: 1rpx solid rgba(15, 23, 42, 0.05);
box-shadow: inset 0 2rpx 6rpx rgba(15, 23, 42, 0.04), 0 10rpx 24rpx rgba(15, 23, 42, 0.05);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 16rpx 34rpx rgba(15, 23, 42, 0.08);
}
.quit-days-ring-inner {
.quit-hp-ring-inner {
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(180deg, rgba(240, 252, 248, 0.96) 0%, rgba(248, 251, 249, 0.96) 100%);
background: linear-gradient(180deg, #ffffff 0%, #f7faf8 100%);
display: flex;
flex-direction: column;
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-days-number {
.quit-hp-ring-kicker {
font-size: 20rpx;
font-weight: 700;
color: #6b7280;
}
.quit-hp-ring-value {
margin-top: 10rpx;
font-size: 74rpx;
font-weight: 800;
line-height: 1;
color: #14936d;
color: var(--hp-accent);
font-family: 'DIN Alternate', -apple-system, sans-serif;
}
.quit-days-label {
margin-top: 6rpx;
font-size: 26rpx;
font-weight: 600;
color: #6b7280;
.quit-hp-ring-label {
margin-top: 8rpx;
padding: 8rpx 16rpx;
border-radius: 999rpx;
background: var(--hp-soft);
color: var(--hp-deep);
font-size: 21rpx;
font-weight: 700;
}
.quit-days-hint {
margin-top: 16rpx;
font-size: 24rpx;
color: #6b7280;
.quit-hp-copy {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.quit-hp-title {
display: block;
font-size: 32rpx;
line-height: 1.35;
font-weight: 800;
color: var(--hp-deep);
}
.quit-hp-desc {
display: block;
margin-top: 10rpx;
font-size: 23rpx;
line-height: 1.7;
color: #4b5563;
}
.quit-hp-pill-row {
display: flex;
gap: 12rpx;
margin-top: 18rpx;
}
.quit-hp-pill {
flex: 1;
min-width: 0;
padding: 14rpx 16rpx;
border-radius: 18rpx;
background: rgba(255, 255, 255, 0.8);
border: 1rpx solid rgba(15, 23, 42, 0.05);
}
.quit-hp-pill-strong {
background: linear-gradient(180deg, var(--hp-soft) 0%, rgba(255, 255, 255, 0.88) 100%);
}
.quit-hp-pill-label {
display: block;
font-size: 18rpx;
color: #9ca3af;
}
.quit-hp-pill-value {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
font-weight: 700;
color: #111827;
}
.quit-metrics-grid {
flex: 1;
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
@@ -2291,7 +2464,7 @@ onShareAppMessage(() => ({
font-size: 30rpx;
line-height: 1.4;
font-weight: 800;
color: #0f766e;
color: var(--hp-deep, #0f766e);
}
.quit-lung-desc {