feat(home): add hp-first quit dashboard experience
This commit is contained in:
+222
-49
@@ -17,7 +17,7 @@
|
|||||||
<view v-else class="content">
|
<view v-else class="content">
|
||||||
<!-- 戒烟模式 -->
|
<!-- 戒烟模式 -->
|
||||||
<view v-if="isQuitMode" class="quit-home">
|
<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-top">
|
||||||
<view class="quit-hero-copy">
|
<view class="quit-hero-copy">
|
||||||
<text class="quit-hero-eyebrow">无烟旅程</text>
|
<text class="quit-hero-eyebrow">无烟旅程</text>
|
||||||
@@ -27,17 +27,36 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="quit-overview">
|
<view class="quit-overview">
|
||||||
<view class="quit-days-card">
|
<view class="quit-hp-panel">
|
||||||
<view class="quit-days-ring">
|
<view class="quit-hp-ring" :style="hpGaugeStyle">
|
||||||
<view class="quit-days-ring-inner">
|
<view class="quit-hp-ring-inner">
|
||||||
<text class="quit-days-number">{{ quitDays }}</text>
|
<text class="quit-hp-ring-kicker">肺部 HP</text>
|
||||||
<text class="quit-days-label">天</text>
|
<text class="quit-hp-ring-value">{{ hpValue }}</text>
|
||||||
|
<text class="quit-hp-ring-label">{{ hpState.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="quit-hp-copy">
|
||||||
|
<text class="quit-hp-title">{{ hpState.title }}</text>
|
||||||
|
<text class="quit-hp-desc">{{ hpState.description }}</text>
|
||||||
|
<view class="quit-hp-pill-row">
|
||||||
|
<view class="quit-hp-pill">
|
||||||
|
<text class="quit-hp-pill-label">今日状态</text>
|
||||||
|
<text class="quit-hp-pill-value">{{ hpChangeText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="quit-hp-pill quit-hp-pill-strong">
|
||||||
|
<text class="quit-hp-pill-label">下一节点</text>
|
||||||
|
<text class="quit-hp-pill-value">{{ nextHealthMilestoneText }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="quit-days-hint">连续无烟</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="quit-metrics-grid">
|
<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">
|
<view class="quit-metric-card">
|
||||||
<text class="quit-metric-kicker">已省下</text>
|
<text class="quit-metric-kicker">已省下</text>
|
||||||
<text class="quit-metric-value">¥{{ savedMoney }}</text>
|
<text class="quit-metric-value">¥{{ savedMoney }}</text>
|
||||||
@@ -93,13 +112,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="quit-health-card">
|
<view class="quit-health-card" :style="hpVisualStyle">
|
||||||
<view class="quit-health-header">
|
<view class="quit-health-header">
|
||||||
<view>
|
<view>
|
||||||
<text class="quit-health-title">肺部恢复可视化</text>
|
<text class="quit-health-title">恢复轨迹</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="quit-health-badge" :class="{ 'quit-health-badge-done': healthProgress >= 100 }">
|
<view class="quit-health-badge" :class="{ 'quit-health-badge-done': hpValue >= 100 }">
|
||||||
{{ healthProgress >= 100 ? '已达成' : `${healthProgress}%` }}
|
{{ `HP ${hpValue}` }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="quit-lung-visual">
|
<view class="quit-lung-visual">
|
||||||
@@ -389,8 +408,8 @@
|
|||||||
<text class="checkin-celebration-desc">已连续无烟 {{ quitDays }} 天,{{ lungPhaseLabel }}</text>
|
<text class="checkin-celebration-desc">已连续无烟 {{ quitDays }} 天,{{ lungPhaseLabel }}</text>
|
||||||
<view class="checkin-celebration-chip-row">
|
<view class="checkin-celebration-chip-row">
|
||||||
<view class="checkin-celebration-chip">
|
<view class="checkin-celebration-chip">
|
||||||
<text class="checkin-celebration-chip-label">肺部恢复</text>
|
<text class="checkin-celebration-chip-label">肺部 HP</text>
|
||||||
<text class="checkin-celebration-chip-value">{{ healthProgress }}%</text>
|
<text class="checkin-celebration-chip-value">{{ hpValue }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="checkin-celebration-chip">
|
<view class="checkin-celebration-chip">
|
||||||
<text class="checkin-celebration-chip-label">已省下</text>
|
<text class="checkin-celebration-chip-label">已省下</text>
|
||||||
@@ -533,6 +552,87 @@ const healthProgress = computed(() => {
|
|||||||
return 0
|
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(() => {
|
const healthTip = computed(() => {
|
||||||
if (quitDays.value >= 365) return '肺部功能显著改善,心血管疾病风险大幅降低'
|
if (quitDays.value >= 365) return '肺部功能显著改善,心血管疾病风险大幅降低'
|
||||||
if (quitDays.value >= 180) return '血液循环持续改善,肺功能逐步恢复'
|
if (quitDays.value >= 180) return '血液循环持续改善,肺功能逐步恢复'
|
||||||
@@ -592,15 +692,17 @@ const smokeFreeTimeLabel = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const lungRecoveryFillStyle = 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(() => {
|
const lungPhaseLabel = computed(() => {
|
||||||
if (healthProgress.value >= 100) return '肺功能已接近长期恢复水平'
|
if (hpValue.value >= 100) return '肺部状态已进入长期巩固阶段'
|
||||||
if (healthProgress.value >= 70) return '肺部纤毛清理能力持续增强'
|
if (hpValue.value >= 80) return '当前恢复速度很稳定'
|
||||||
if (healthProgress.value >= 40) return '呼吸效率进入稳定恢复期'
|
if (hpValue.value >= 60) return '肺部恢复节奏正在变强'
|
||||||
if (healthProgress.value >= 15) return '肺部正在排出残留刺激物'
|
if (hpValue.value >= 40) return '身体正在持续找回呼吸效率'
|
||||||
return '身体已启动自我修复'
|
if (hpValue.value >= 20) return '恢复已经启动,别让节奏断掉'
|
||||||
|
return '先把今天守住,HP 就会慢慢往上走'
|
||||||
})
|
})
|
||||||
|
|
||||||
const lungRecoverySummary = computed(() => {
|
const lungRecoverySummary = computed(() => {
|
||||||
@@ -616,9 +718,9 @@ const nextHealthMilestoneText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const lungMomentumText = computed(() => {
|
const lungMomentumText = computed(() => {
|
||||||
if (healthProgress.value >= 100) return '稳定达成'
|
if (hpValue.value >= 100) return '稳定达成'
|
||||||
if (healthProgress.value >= 60) return '恢复加速中'
|
if (hpValue.value >= 70) return '恢复加速中'
|
||||||
if (healthProgress.value >= 20) return '持续上升'
|
if (hpValue.value >= 35) return '持续回升'
|
||||||
return '刚刚启动'
|
return '刚刚启动'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1130,12 +1232,15 @@ onShareAppMessage(() => ({
|
|||||||
left: -72rpx;
|
left: -72rpx;
|
||||||
top: -72rpx;
|
top: -72rpx;
|
||||||
border-radius: 50%;
|
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;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-hero-card {
|
.quit-hero-card {
|
||||||
padding: 28rpx 24rpx;
|
padding: 28rpx 24rpx;
|
||||||
|
--hp-accent: #14936d;
|
||||||
|
--hp-soft: #def7ec;
|
||||||
|
--hp-deep: #0f766e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-hero-top,
|
.quit-hero-top,
|
||||||
@@ -1177,7 +1282,7 @@ onShareAppMessage(() => ({
|
|||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #14936d;
|
color: var(--hp-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-hero-chip {
|
.quit-hero-chip {
|
||||||
@@ -1193,63 +1298,131 @@ onShareAppMessage(() => ({
|
|||||||
|
|
||||||
.quit-overview {
|
.quit-overview {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 18rpx;
|
gap: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-days-card {
|
.quit-hp-panel {
|
||||||
width: 216rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
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;
|
width: 200rpx;
|
||||||
height: 200rpx;
|
height: 200rpx;
|
||||||
padding: 14rpx;
|
padding: 12rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbf9 100%);
|
display: flex;
|
||||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
align-items: center;
|
||||||
box-shadow: inset 0 2rpx 6rpx rgba(15, 23, 42, 0.04), 0 10rpx 24rpx rgba(15, 23, 42, 0.05);
|
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%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 50%;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.92);
|
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-size: 74rpx;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #14936d;
|
color: var(--hp-accent);
|
||||||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-days-label {
|
.quit-hp-ring-label {
|
||||||
margin-top: 6rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 26rpx;
|
padding: 8rpx 16rpx;
|
||||||
font-weight: 600;
|
border-radius: 999rpx;
|
||||||
color: #6b7280;
|
background: var(--hp-soft);
|
||||||
|
color: var(--hp-deep);
|
||||||
|
font-size: 21rpx;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-days-hint {
|
.quit-hp-copy {
|
||||||
margin-top: 16rpx;
|
flex: 1;
|
||||||
font-size: 24rpx;
|
min-width: 0;
|
||||||
color: #6b7280;
|
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 {
|
.quit-metrics-grid {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 14rpx;
|
gap: 14rpx;
|
||||||
@@ -2291,7 +2464,7 @@ onShareAppMessage(() => ({
|
|||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
color: #0f766e;
|
color: var(--hp-deep, #0f766e);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quit-lung-desc {
|
.quit-lung-desc {
|
||||||
|
|||||||
Reference in New Issue
Block a user