feat(ui): Phase 1 UI/动画优化实现
- 肺部健康可视化组件(SVG肺部+呼吸动画+气泡效果) - 打卡多巴胺反馈动画(成功弹窗+震动反馈) - 悬浮记录按钮(右下角FAB+呼吸动画) - 空状态优化(演示数据预览+引导文案) - 记录模式空状态视觉升级 - 统计页面空状态统一 参考: docs/PRD-UI-Animation-Optimization.md
This commit is contained in:
+856
-19
@@ -96,12 +96,59 @@
|
||||
<view class="quit-health-card">
|
||||
<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>
|
||||
</view>
|
||||
<view class="quit-lung-visual">
|
||||
<view class="quit-lung-stage">
|
||||
<view class="quit-lung-glow"></view>
|
||||
<view class="quit-lung-bubbles">
|
||||
<view
|
||||
v-for="bubble in lungBubbleItems"
|
||||
:key="bubble.id"
|
||||
class="quit-lung-bubble"
|
||||
:style="{
|
||||
left: bubble.left,
|
||||
animationDelay: bubble.delay,
|
||||
animationDuration: bubble.duration
|
||||
}"
|
||||
></view>
|
||||
</view>
|
||||
<view class="quit-lung-figure">
|
||||
<view class="quit-lung-trachea"></view>
|
||||
<view class="quit-lung-branch quit-lung-branch-left"></view>
|
||||
<view class="quit-lung-branch quit-lung-branch-right"></view>
|
||||
<view class="quit-lung-lobe quit-lung-lobe-left">
|
||||
<view class="quit-lung-lobe-fill" :style="lungRecoveryFillStyle"></view>
|
||||
</view>
|
||||
<view class="quit-lung-lobe quit-lung-lobe-right">
|
||||
<view class="quit-lung-lobe-fill" :style="lungRecoveryFillStyle"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quit-lung-copy">
|
||||
<text class="quit-lung-kicker">无烟 {{ smokeFreeTimeLabel }}</text>
|
||||
<text class="quit-lung-phase">{{ lungPhaseLabel }}</text>
|
||||
<text class="quit-lung-desc">{{ lungRecoverySummary }}</text>
|
||||
<view class="quit-lung-tag-row">
|
||||
<view class="quit-lung-tag">
|
||||
<text class="quit-lung-tag-label">下一节点</text>
|
||||
<text class="quit-lung-tag-value">{{ nextHealthMilestoneText }}</text>
|
||||
</view>
|
||||
<view class="quit-lung-tag quit-lung-tag-strong">
|
||||
<text class="quit-lung-tag-label">恢复节奏</text>
|
||||
<text class="quit-lung-tag-value">{{ lungMomentumText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="quit-health-subheader">
|
||||
<text class="quit-health-subtitle">恢复节点</text>
|
||||
<text class="quit-health-subhint">{{ lungRecoveryCaption }}</text>
|
||||
</view>
|
||||
<view class="quit-milestone-list">
|
||||
<view
|
||||
v-for="(ms, index) in healthMilestoneItems"
|
||||
@@ -143,7 +190,7 @@
|
||||
<!-- 记录模式 -->
|
||||
<view v-else>
|
||||
<!-- 记录卡片 -->
|
||||
<view class="summary-card summary-card-record">
|
||||
<view class="summary-card summary-card-record" :class="{ 'summary-card-record-empty': !recordHasData }">
|
||||
<view class="summary-card-top summary-card-top-record">
|
||||
<view class="summary-heading">
|
||||
<view class="summary-title-accent"></view>
|
||||
@@ -155,7 +202,7 @@
|
||||
<text class="record-header-chip">今日概览</text>
|
||||
</view>
|
||||
|
||||
<view class="record-overview">
|
||||
<view v-if="recordHasData" class="record-overview">
|
||||
<view class="record-ring" :style="todayCountRingStyle">
|
||||
<view class="record-ring-inner">
|
||||
<text class="record-ring-value">{{ timerDisplay }}</text>
|
||||
@@ -183,13 +230,46 @@
|
||||
<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 v-else class="record-empty-state">
|
||||
<view class="record-empty-visual">
|
||||
<view class="record-empty-orbit"></view>
|
||||
<view class="record-empty-core">
|
||||
<text class="record-empty-core-text">0</text>
|
||||
<text class="record-empty-core-label">今日记录</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="primary-pill primary-pill-resist" @tap="openResistedDialog">
|
||||
<text class="primary-pill-title">忍住</text>
|
||||
<view class="record-empty-copy">
|
||||
<text class="record-empty-title">今天还没有新记录</text>
|
||||
<text class="record-empty-desc">用悬浮按钮快速记下抽烟或忍住的瞬间,系统会开始生成节奏变化和健康反馈。</text>
|
||||
</view>
|
||||
<view class="record-empty-tag-row">
|
||||
<view class="record-empty-tag">
|
||||
<text class="record-empty-tag-value">{{ dailyTarget }}</text>
|
||||
<text class="record-empty-tag-label">今日目标</text>
|
||||
</view>
|
||||
<view class="record-empty-tag">
|
||||
<text class="record-empty-tag-value">{{ baselineCigsPerDay }}</text>
|
||||
<text class="record-empty-tag-label">基础支数</text>
|
||||
</view>
|
||||
<view class="record-empty-tag">
|
||||
<text class="record-empty-tag-value">立即开始</text>
|
||||
<text class="record-empty-tag-label">首条记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-action-row record-action-row-inline">
|
||||
<view class="record-action-hint">
|
||||
<text class="record-action-hint-title">快速动作</text>
|
||||
<text class="record-action-hint-text">右下角悬浮按钮支持快速记录与忍住反馈</text>
|
||||
</view>
|
||||
<view class="record-action-pills">
|
||||
<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>
|
||||
@@ -279,6 +359,46 @@
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="!loading && !isQuitMode" class="record-fab-stack">
|
||||
<view class="record-fab-secondary" @tap="openResistedDialog">
|
||||
<text class="record-fab-secondary-icon">✓</text>
|
||||
<text class="record-fab-secondary-text">忍住</text>
|
||||
</view>
|
||||
<view class="record-fab-primary" @tap="openSmokeDialog">
|
||||
<view class="record-fab-primary-icon">+</view>
|
||||
<view class="record-fab-primary-copy">
|
||||
<text class="record-fab-primary-title">记录一根</text>
|
||||
<text class="record-fab-primary-desc">快速添加</text>
|
||||
</view>
|
||||
</view>
|
||||
</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">肺部恢复</text>
|
||||
<text class="checkin-celebration-chip-value">{{ healthProgress }}%</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>
|
||||
|
||||
@@ -304,8 +424,10 @@ const pageReady = ref(false)
|
||||
const achievementData = ref(null)
|
||||
const quitHomeData = ref(null)
|
||||
const quitState = ref(defaultQuitState())
|
||||
const checkinCelebrating = ref(false)
|
||||
|
||||
let timerInterval = null
|
||||
let checkinCelebrationTimer = null
|
||||
const timerBaseSeconds = ref(-1)
|
||||
const timerSeconds = ref(0)
|
||||
|
||||
@@ -444,6 +566,69 @@ const healthMilestones = computed(() => [
|
||||
{ days: 365, label: '1年' }
|
||||
])
|
||||
|
||||
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' }
|
||||
]
|
||||
|
||||
const celebrationBursts = [
|
||||
{ id: 1, rotate: 0 },
|
||||
{ id: 2, rotate: 45 },
|
||||
{ id: 3, rotate: 90 },
|
||||
{ id: 4, rotate: 135 },
|
||||
{ id: 5, rotate: 180 },
|
||||
{ id: 6, rotate: 225 },
|
||||
{ id: 7, rotate: 270 },
|
||||
{ id: 8, rotate: 315 }
|
||||
]
|
||||
|
||||
const nextHealthMilestone = computed(() => healthMilestones.value.find(item => quitDays.value < item.days) || null)
|
||||
|
||||
const smokeFreeTimeLabel = computed(() => {
|
||||
if (quitDays.value >= 1) return `${quitDays.value} 天`
|
||||
return '刚开始'
|
||||
})
|
||||
|
||||
const lungRecoveryFillStyle = computed(() => ({
|
||||
height: `${Math.max(healthProgress.value, 6)}%`
|
||||
}))
|
||||
|
||||
const lungPhaseLabel = computed(() => {
|
||||
if (healthProgress.value >= 100) return '肺功能已接近长期恢复水平'
|
||||
if (healthProgress.value >= 70) return '肺部纤毛清理能力持续增强'
|
||||
if (healthProgress.value >= 40) return '呼吸效率进入稳定恢复期'
|
||||
if (healthProgress.value >= 15) return '肺部正在排出残留刺激物'
|
||||
return '身体已启动自我修复'
|
||||
})
|
||||
|
||||
const lungRecoverySummary = computed(() => {
|
||||
if (nextHealthMilestone.value) {
|
||||
return `${healthTip.value},距离下一阶段还差 ${Math.max(nextHealthMilestone.value.days - quitDays.value, 0)} 天。`
|
||||
}
|
||||
return `${healthTip.value},已经进入长期巩固阶段。`
|
||||
})
|
||||
|
||||
const nextHealthMilestoneText = computed(() => {
|
||||
if (!nextHealthMilestone.value) return '已完成全年恢复'
|
||||
return `${nextHealthMilestone.value.label} 里程碑`
|
||||
})
|
||||
|
||||
const lungMomentumText = computed(() => {
|
||||
if (healthProgress.value >= 100) return '稳定达成'
|
||||
if (healthProgress.value >= 60) return '恢复加速中'
|
||||
if (healthProgress.value >= 20) return '持续上升'
|
||||
return '刚刚启动'
|
||||
})
|
||||
|
||||
const lungRecoveryCaption = computed(() => {
|
||||
if (nextHealthMilestone.value) {
|
||||
return `${Math.max(nextHealthMilestone.value.days - quitDays.value, 0)} 天后解锁 ${nextHealthMilestone.value.label}`
|
||||
}
|
||||
return '所有关键节点已完成'
|
||||
})
|
||||
|
||||
const healthMilestoneItems = computed(() => {
|
||||
const milestones = [
|
||||
{ name: '血压心率恢复', minutes: 20 },
|
||||
@@ -499,6 +684,13 @@ const totalResistedCount = computed(() => {
|
||||
return homeSummary.value.total_resisted ?? resistedCount.value
|
||||
})
|
||||
|
||||
const recordHasData = computed(() => {
|
||||
return todayCount.value > 0 ||
|
||||
resistedCount.value > 0 ||
|
||||
totalResistedCount.value > 0 ||
|
||||
timerBaseSeconds.value >= 0
|
||||
})
|
||||
|
||||
const reducePercent = computed(() => {
|
||||
if (dailyTarget.value <= 0) return 0
|
||||
const reduced = Math.max(0, dailyTarget.value - todayCount.value)
|
||||
@@ -575,6 +767,26 @@ function stopTimer() {
|
||||
timerInterval = null
|
||||
}
|
||||
|
||||
function playQuitCheckinCelebration() {
|
||||
checkinCelebrating.value = false
|
||||
if (checkinCelebrationTimer) {
|
||||
clearTimeout(checkinCelebrationTimer)
|
||||
checkinCelebrationTimer = null
|
||||
}
|
||||
setTimeout(() => {
|
||||
checkinCelebrating.value = true
|
||||
try {
|
||||
uni.vibrateShort()
|
||||
} catch (e) {
|
||||
console.error('vibrateShort error:', e)
|
||||
}
|
||||
checkinCelebrationTimer = setTimeout(() => {
|
||||
checkinCelebrating.value = false
|
||||
checkinCelebrationTimer = null
|
||||
}, 1800)
|
||||
}, 20)
|
||||
}
|
||||
|
||||
function openSmokeDialog() {
|
||||
dialogType.value = 'smoke'
|
||||
showDialog.value = true
|
||||
@@ -631,18 +843,26 @@ async function handleQuitCheckin() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const today = formatDate(new Date())
|
||||
const res = await api.quitCheckin({ date: today })
|
||||
applyQuitHomeData(res.data)
|
||||
saveQuitState({
|
||||
lastCheckinDate: today,
|
||||
lastCheckinAt: new Date().toISOString(),
|
||||
streakDays: res.data?.summary?.current_streak_days || 0
|
||||
})
|
||||
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' })
|
||||
return
|
||||
} catch (retryErr) {
|
||||
console.error('handleQuitCheckin retry error:', retryErr)
|
||||
uni.showToast({ title: retryErr?.message || '打卡失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
console.error('handleQuitCheckin error:', e)
|
||||
uni.showToast({ title: '打卡失败,请重试', icon: 'none' })
|
||||
uni.showToast({ title: message || '打卡失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +914,20 @@ async function ensureQuitProfile() {
|
||||
})
|
||||
}
|
||||
|
||||
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 ensureProfileReady() {
|
||||
const profileData = await profileStore.fetchProfile()
|
||||
const profile = profileData.profile
|
||||
@@ -759,7 +993,13 @@ onShow(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => { stopTimer() })
|
||||
onUnmounted(() => {
|
||||
stopTimer()
|
||||
if (checkinCelebrationTimer) {
|
||||
clearTimeout(checkinCelebrationTimer)
|
||||
checkinCelebrationTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
onShareAppMessage(() => ({
|
||||
title: isQuitMode.value ? '我在坚持戒烟打卡' : '我在记录自己的抽烟变化',
|
||||
@@ -1882,4 +2122,601 @@ onShareAppMessage(() => ({
|
||||
color: #14936d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx 24rpx 280rpx;
|
||||
}
|
||||
|
||||
.quit-health-subheader {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin: 22rpx 0 18rpx;
|
||||
}
|
||||
|
||||
.quit-health-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.quit-health-subhint {
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.quit-lung-visual {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 22rpx;
|
||||
padding: 24rpx 22rpx;
|
||||
border-radius: 28rpx;
|
||||
background: linear-gradient(135deg, rgba(238, 250, 245, 0.96) 0%, rgba(246, 251, 249, 0.92) 100%);
|
||||
border: 1rpx solid rgba(20, 147, 109, 0.08);
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.quit-lung-stage {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 232rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quit-lung-glow {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 188rpx;
|
||||
height: 188rpx;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: radial-gradient(circle, rgba(49, 193, 139, 0.22) 0%, rgba(49, 193, 139, 0.02) 70%, rgba(49, 193, 139, 0) 100%);
|
||||
animation: celebrateGlow 3.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.quit-lung-bubbles {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quit-lung-bubble {
|
||||
position: absolute;
|
||||
bottom: 28rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(49, 193, 139, 0.34);
|
||||
box-shadow: 0 0 12rpx rgba(49, 193, 139, 0.2);
|
||||
animation: breathBubble 5s linear infinite;
|
||||
}
|
||||
|
||||
.quit-lung-figure {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 176rpx;
|
||||
height: 208rpx;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.quit-lung-trachea {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
width: 24rpx;
|
||||
height: 76rpx;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #d8f3e6 0%, #93e0bf 100%);
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.quit-lung-branch {
|
||||
position: absolute;
|
||||
top: 62rpx;
|
||||
width: 58rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #aee9cf 0%, #7fd7b2 100%);
|
||||
}
|
||||
|
||||
.quit-lung-branch-left {
|
||||
left: 38rpx;
|
||||
transform: rotate(-24deg);
|
||||
transform-origin: right center;
|
||||
}
|
||||
|
||||
.quit-lung-branch-right {
|
||||
right: 38rpx;
|
||||
transform: rotate(24deg);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.quit-lung-lobe {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
width: 78rpx;
|
||||
height: 126rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, rgba(233, 248, 241, 0.96) 0%, rgba(215, 240, 229, 0.98) 100%);
|
||||
border: 2rpx solid rgba(20, 147, 109, 0.08);
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.quit-lung-lobe-left {
|
||||
left: 10rpx;
|
||||
border-radius: 68rpx 52rpx 84rpx 70rpx;
|
||||
transform: rotate(-6deg);
|
||||
}
|
||||
|
||||
.quit-lung-lobe-right {
|
||||
right: 10rpx;
|
||||
border-radius: 52rpx 68rpx 70rpx 84rpx;
|
||||
transform: rotate(6deg);
|
||||
}
|
||||
|
||||
.quit-lung-lobe-fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(180deg, rgba(110, 231, 190, 0.78) 0%, rgba(20, 147, 109, 0.92) 100%);
|
||||
border-radius: inherit;
|
||||
transition: height 0.7s ease;
|
||||
}
|
||||
|
||||
.quit-lung-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quit-lung-kicker {
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.quit-lung-phase {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.4;
|
||||
font-weight: 800;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.quit-lung-desc {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.7;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.quit-lung-tag-row {
|
||||
display: flex;
|
||||
gap: 10rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.quit-lung-tag {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 12rpx 14rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.quit-lung-tag-strong {
|
||||
background: linear-gradient(180deg, rgba(229, 248, 240, 0.96) 0%, rgba(235, 249, 243, 0.96) 100%);
|
||||
}
|
||||
|
||||
.quit-lung-tag-label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.quit-lung-tag-value {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.summary-card-record-empty::before {
|
||||
background: radial-gradient(circle, rgba(52, 200, 160, 0.18) 0%, rgba(52, 200, 160, 0) 72%);
|
||||
}
|
||||
|
||||
.record-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12rpx 0 0;
|
||||
}
|
||||
|
||||
.record-empty-visual {
|
||||
position: relative;
|
||||
width: 232rpx;
|
||||
height: 232rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.record-empty-orbit {
|
||||
position: absolute;
|
||||
inset: 12rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx dashed rgba(20, 147, 109, 0.22);
|
||||
animation: recordFabFloat 8s linear infinite;
|
||||
}
|
||||
|
||||
.record-empty-core {
|
||||
position: relative;
|
||||
width: 156rpx;
|
||||
height: 156rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f3faf6 100%);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
box-shadow:
|
||||
0 18rpx 36rpx rgba(15, 23, 42, 0.08),
|
||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.record-empty-core-text {
|
||||
font-size: 52rpx;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: #14936d;
|
||||
font-family: 'DIN Alternate', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.record-empty-core-label {
|
||||
margin-top: 10rpx;
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.record-empty-copy {
|
||||
margin-top: 16rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.record-empty-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.record-empty-desc {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.7;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.record-empty-tag-row {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12rpx;
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.record-empty-tag {
|
||||
padding: 16rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(180deg, #fbfcfc 0%, #f7faf8 100%);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.record-empty-tag-value {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.record-empty-tag-label {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.record-action-row-inline {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16rpx;
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.record-action-hint {
|
||||
padding: 16rpx 18rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(248, 250, 249, 0.94);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.record-action-hint-title {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.record-action-hint-text {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 21rpx;
|
||||
line-height: 1.6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.record-action-pills {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.record-fab-stack {
|
||||
position: fixed;
|
||||
right: 24rpx;
|
||||
bottom: calc(138rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 16rpx;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.record-fab-secondary {
|
||||
min-width: 128rpx;
|
||||
height: 76rpx;
|
||||
padding: 0 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
box-shadow: 0 18rpx 34rpx rgba(15, 23, 42, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
animation: recordFabFloat 3.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.record-fab-secondary-icon {
|
||||
font-size: 24rpx;
|
||||
font-weight: 800;
|
||||
color: #14936d;
|
||||
}
|
||||
|
||||
.record-fab-secondary-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.record-fab-primary {
|
||||
min-width: 224rpx;
|
||||
height: 96rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(180deg, #31c18b 0%, #14936d 100%);
|
||||
box-shadow: 0 22rpx 38rpx rgba(20, 147, 109, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
animation: recordFabFloat 3.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.record-fab-primary-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.record-fab-primary-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.record-fab-primary-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.record-fab-primary-desc {
|
||||
margin-top: 4rpx;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
}
|
||||
|
||||
.checkin-celebration-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.24);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 40;
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
.checkin-celebration-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 42rpx 34rpx 34rpx;
|
||||
border-radius: 32rpx;
|
||||
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);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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: 999rpx;
|
||||
background: linear-gradient(180deg, rgba(110, 231, 190, 0.2) 0%, rgba(20, 147, 109, 0.92) 100%);
|
||||
}
|
||||
|
||||
.checkin-celebration-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #31c18b 0%, #14936d 100%);
|
||||
box-shadow: 0 18rpx 36rpx rgba(20, 147, 109, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 52rpx;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
animation: pulseCheck 0.8s ease-out;
|
||||
}
|
||||
|
||||
.checkin-celebration-title {
|
||||
display: block;
|
||||
margin-top: 26rpx;
|
||||
font-size: 34rpx;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.checkin-celebration-desc {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
color: #4b5563;
|
||||
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 16rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
border: 1rpx solid rgba(15, 23, 42, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checkin-celebration-chip-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.checkin-celebration-chip-value {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 800;
|
||||
color: #14936d;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user