bdb8d425eb
Made-with: Cursor
1303 lines
28 KiB
Vue
1303 lines
28 KiB
Vue
<template>
|
||
<view class="page">
|
||
<view class="sticky-bar">
|
||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
<view class="segment">
|
||
<view
|
||
v-for="tab in tabs"
|
||
:key="tab.value"
|
||
class="segment-item"
|
||
:class="{ 'segment-active': currentTab === tab.value }"
|
||
@tap="currentTab = tab.value"
|
||
>
|
||
{{ tab.label }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="insight-card">
|
||
<view class="insight-icon" :class="insightIconClass">
|
||
<text class="insight-emoji">{{ insightEmoji }}</text>
|
||
</view>
|
||
<view class="insight-content">
|
||
<text class="insight-title">每周洞察</text>
|
||
<text class="insight-desc">{{ insightText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section-header">
|
||
<view>
|
||
<text class="section-title">吸烟趋势</text>
|
||
<text class="section-sub">{{ trendRangeText }}</text>
|
||
</view>
|
||
<view class="status-chip" :class="statusChipClass">
|
||
<text class="status-icon" :class="statusIconClass">{{ statusArrow }}</text>
|
||
<text class="status-text">{{ statusText }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="trend-card">
|
||
<view class="trend-header">
|
||
<text class="trend-label">日均吸烟量</text>
|
||
<view class="trend-value-row">
|
||
<text class="trend-value">{{ averageCount }}</text>
|
||
<text class="trend-unit">支/天</text>
|
||
</view>
|
||
</view>
|
||
<scroll-view v-if="trendItems.length > 0" scroll-x class="trend-chart-scroll" :show-scrollbar="false">
|
||
<view class="trend-chart" :style="{ minWidth: trendChartMinWidth }">
|
||
<view v-for="(item, index) in trendItems" :key="index" class="trend-bar-item">
|
||
<view v-if="item.isHighlight" class="trend-bubble">{{ item.count }}</view>
|
||
<view class="trend-bar-wrap">
|
||
<view class="trend-bar" :style="{ height: item.height }"></view>
|
||
</view>
|
||
<text class="trend-bar-label" :class="{ 'trend-bar-label-active': item.isHighlight }">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<view v-else class="trend-chart-empty">
|
||
<text class="trend-chart-empty-text">暂无趋势数据</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<text class="section-title">健康与储蓄</text>
|
||
|
||
<view class="savings-card">
|
||
<view class="savings-header">
|
||
<view class="savings-icon" :class="moneyIconClass">
|
||
<text class="icon-emoji">💰</text>
|
||
</view>
|
||
<view class="savings-header-text">
|
||
<text class="savings-title">节省金额</text>
|
||
<text class="savings-subtitle">{{ moneySubtitle }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="savings-body">
|
||
<view class="savings-left">
|
||
<text class="savings-value">{{ savedMoneyText }}</text>
|
||
<view class="savings-bar">
|
||
<view class="savings-bar-fill" :style="{ width: moneyPercent + '%' }"></view>
|
||
</view>
|
||
<text v-if="moneyAvailable" class="savings-sub">目标 ¥{{ moneyTargetYuan }} ({{ moneyPercent }}%)</text>
|
||
<view class="savings-metrics" v-if="moneyAvailable">
|
||
<view class="metric-chip">
|
||
<text class="metric-label">预计</text>
|
||
<text class="metric-value">{{ moneyExpectedTotal }}</text>
|
||
<text class="metric-unit">支</text>
|
||
</view>
|
||
<view class="metric-chip metric-chip-actual">
|
||
<text class="metric-label">实际</text>
|
||
<text class="metric-value">{{ moneyActualTotal }}</text>
|
||
<text class="metric-unit">支</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="savings-ring" :style="moneyRingStyle">
|
||
<view class="savings-ring-inner">
|
||
<text class="ring-value">{{ moneyRingText }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="!moneyAvailable" class="card-empty">
|
||
<text class="card-empty-icon">🔒</text>
|
||
<text class="card-empty-text">完善基础信息后解锁节省金额</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="health-card">
|
||
<view class="health-header">
|
||
<view class="health-title-row">
|
||
<view class="health-icon" :class="healthIconClass">
|
||
<text class="icon-emoji">❤️</text>
|
||
</view>
|
||
<text class="health-title">健康恢复里程碑</text>
|
||
</view>
|
||
<view class="health-badge" :class="{ 'health-badge-muted': !healthAvailable }">{{ healthStatusText }}</view>
|
||
</view>
|
||
<view v-if="healthAvailable" class="health-overview">
|
||
<view class="health-metric">
|
||
<view class="health-metric-icon">⏱️</view>
|
||
<text class="health-metric-value">{{ smokeFreeText }}</text>
|
||
<text class="health-metric-label">无烟时长</text>
|
||
</view>
|
||
<view class="health-ring" :style="healthRingStyle">
|
||
<view class="health-ring-inner">
|
||
<text class="ring-value">{{ lungRecoveryPercent }}%</text>
|
||
<text class="ring-label">肺功能</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="card-empty">
|
||
<text class="card-empty-icon">🫁</text>
|
||
<text class="card-empty-text">暂无健康数据,记录一次后解锁</text>
|
||
</view>
|
||
<view class="health-list">
|
||
<view v-for="(item, index) in healthItems" :key="index" class="health-item">
|
||
<view class="health-item-row">
|
||
<text class="health-item-name">{{ item.name }}</text>
|
||
<text class="health-item-percent" :class="{ 'health-item-percent-muted': item.percent < 100 }">{{ item.percent }}%</text>
|
||
</view>
|
||
<view class="health-bar">
|
||
<view
|
||
class="health-bar-fill"
|
||
:class="{ 'health-bar-fill-muted': item.percent < 100 }"
|
||
:style="{ width: item.percent + '%' }"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="stats-grid">
|
||
<view class="mini-card">
|
||
<view class="mini-icon mini-icon-fire" :class="streakIconClass">
|
||
<text class="icon-emoji">🔥</text>
|
||
</view>
|
||
<text class="mini-label">连续记录</text>
|
||
<view class="mini-value-row">
|
||
<text class="mini-value">{{ streakDays }}</text>
|
||
<text class="mini-unit">天</text>
|
||
</view>
|
||
<text class="mini-sub">连续记录天数</text>
|
||
</view>
|
||
<view class="mini-card">
|
||
<view class="mini-icon mini-icon-block" :class="resistedIconClass">
|
||
<text class="icon-emoji">🛡️</text>
|
||
</view>
|
||
<text class="mini-label">已拒绝</text>
|
||
<view class="mini-value-row">
|
||
<text class="mini-value">{{ resistedTotal }}</text>
|
||
<text class="mini-unit">次</text>
|
||
</view>
|
||
<text class="mini-sub">成功抵抗烟瘾</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, watch } from 'vue'
|
||
import { onShareAppMessage } from '@dcloudio/uni-app'
|
||
import { useLogin } from '@/hooks/useLogin'
|
||
import * as api from '@/api'
|
||
|
||
const { waitForLogin } = useLogin()
|
||
const statusBarHeight = ref(0)
|
||
|
||
const tabs = [
|
||
{ label: '周', value: 'week' },
|
||
{ label: '月', value: 'month' },
|
||
{ label: '年', value: 'year' }
|
||
]
|
||
|
||
const currentTab = ref('week')
|
||
const statsData = ref(null)
|
||
|
||
const changePercent = computed(() => {
|
||
const value = statsData.value?.change_percent
|
||
if (value === undefined || value === null) return null
|
||
const num = Number(value)
|
||
return Number.isNaN(num) ? null : num
|
||
})
|
||
|
||
const insightEmoji = computed(() => {
|
||
if (changePercent.value === null) return '✨'
|
||
return changePercent.value <= 0 ? '🌿' : '⚠️'
|
||
})
|
||
|
||
const insightIconClass = computed(() => {
|
||
if (changePercent.value === null) return 'insight-neutral'
|
||
return changePercent.value <= 0 ? 'insight-good' : 'insight-warn'
|
||
})
|
||
|
||
const insightText = computed(() => {
|
||
if (changePercent.value === null) {
|
||
return '本阶段数据逐步稳定,建议保持记录,下一周期可获得趋势对比。'
|
||
}
|
||
const abs = Math.abs(changePercent.value)
|
||
if (changePercent.value <= 0) {
|
||
return `较上期下降 ${abs}%,趋势向好,继续保持节奏。`
|
||
}
|
||
return `较上期上升 ${abs}%,留意高峰时段,尝试延迟第一支。`
|
||
})
|
||
|
||
const trendRangeText = computed(() => {
|
||
const start = statsData.value?.start
|
||
const end = statsData.value?.end
|
||
if (!start || !end) {
|
||
return ''
|
||
}
|
||
return formatRangeText(start, end)
|
||
})
|
||
|
||
const statusText = computed(() => {
|
||
if (changePercent.value === null) return '暂无对比'
|
||
const sign = changePercent.value > 0 ? '+' : ''
|
||
return `较上期 ${sign}${changePercent.value}%`
|
||
})
|
||
|
||
const statusChipClass = computed(() => {
|
||
if (changePercent.value === null) return 'status-neutral'
|
||
return changePercent.value <= 0 ? 'status-good' : 'status-warn'
|
||
})
|
||
|
||
const statusArrow = computed(() => {
|
||
if (changePercent.value === null) return '→'
|
||
return changePercent.value <= 0 ? '↓' : '↑'
|
||
})
|
||
|
||
const statusIconClass = computed(() => {
|
||
if (changePercent.value === null) return 'status-icon-neutral'
|
||
return changePercent.value <= 0 ? 'status-icon-good' : 'status-icon-warn'
|
||
})
|
||
|
||
const averageCount = computed(() => {
|
||
const avg = statsData.value?.daily_average
|
||
if (avg === undefined || avg === null) return 1
|
||
return avg
|
||
})
|
||
|
||
// 图表仅使用接口返回的 trend 渲染,无数据时为空
|
||
const trendItems = computed(() => {
|
||
const trend = statsData.value?.trend
|
||
const trendUnit = statsData.value?.trend_unit
|
||
if (!trend || !Array.isArray(trend) || trend.length === 0) {
|
||
return []
|
||
}
|
||
const maxCount = Math.max(...trend.map(item => Number(item.count) || 0), 1)
|
||
return trend.map((item, index) => {
|
||
const count = Number(item.count) || 0
|
||
const height = `${Math.max((count / maxCount) * 100, 6)}%`
|
||
return {
|
||
label: formatTrendLabel(item.label, trendUnit),
|
||
count,
|
||
height,
|
||
isHighlight: index === trend.length - 1
|
||
}
|
||
})
|
||
})
|
||
|
||
// 图表最小宽度:保证每根柱子有足够空间,年视图 12 个月时不被挤压或裁切
|
||
const trendChartMinWidth = computed(() => {
|
||
const n = trendItems.value.length
|
||
const perItem = 80
|
||
return `${Math.max(n * perItem, 400)}rpx`
|
||
})
|
||
|
||
const savedMoneyText = computed(() => {
|
||
const money = statsData.value?.money
|
||
if (!money || !money.available) return '--'
|
||
return `¥${(money.saved_cent / 100).toFixed(2)}`
|
||
})
|
||
|
||
const moneyAvailable = computed(() => !!statsData.value?.money?.available)
|
||
|
||
const moneyExpectedTotal = computed(() => {
|
||
const money = statsData.value?.money
|
||
if (!money || !money.available) return 0
|
||
return Number(money.expected_total) || 0
|
||
})
|
||
|
||
const moneyActualTotal = computed(() => {
|
||
const money = statsData.value?.money
|
||
if (!money || !money.available) return 0
|
||
return Number(money.actual_total) || 0
|
||
})
|
||
|
||
const moneySubtitle = computed(() => {
|
||
if (!moneyAvailable.value) return '尚未解锁'
|
||
return `预计 ${moneyExpectedTotal.value} 支`
|
||
})
|
||
|
||
const moneyTargetCent = computed(() => {
|
||
const money = statsData.value?.money
|
||
if (!money || !money.available) return 0
|
||
const { expected_total, pack_price_cent, cigs_per_pack } = money
|
||
if (!expected_total || !pack_price_cent || !cigs_per_pack) return 0
|
||
return Math.round((expected_total / cigs_per_pack) * pack_price_cent)
|
||
})
|
||
|
||
const moneyPercent = computed(() => {
|
||
const money = statsData.value?.money
|
||
const target = moneyTargetCent.value
|
||
if (!money || !money.available || target <= 0) {
|
||
return 0
|
||
}
|
||
const percent = Math.round((money.saved_cent / target) * 100)
|
||
return Math.min(Math.max(percent, 0), 100)
|
||
})
|
||
|
||
const moneyTargetYuan = computed(() => {
|
||
const target = moneyTargetCent.value
|
||
if (target <= 0) return '0'
|
||
return (target / 100).toFixed(0)
|
||
})
|
||
|
||
const moneyRingStyle = computed(() => {
|
||
if (!moneyAvailable.value) {
|
||
return {
|
||
background: 'conic-gradient(#E2E8F0 0deg 360deg)'
|
||
}
|
||
}
|
||
const percent = moneyPercent.value
|
||
const angle = Math.round(percent * 3.6)
|
||
return {
|
||
background: `conic-gradient(#F59E0B 0deg ${angle}deg, #F1F5F9 ${angle}deg 360deg)`
|
||
}
|
||
})
|
||
|
||
const moneyRingText = computed(() => {
|
||
if (!moneyAvailable.value) return '--'
|
||
return `${moneyPercent.value}%`
|
||
})
|
||
|
||
const healthStatusText = computed(() => {
|
||
const health = statsData.value?.health
|
||
if (!health || !health.available) return '暂无数据'
|
||
const percent = Math.round(Number(health.lung_recovery_percent) || 0)
|
||
if (percent >= 100) return '已达成'
|
||
if (percent > 0) return `已恢复 ${percent}%`
|
||
return '进行中'
|
||
})
|
||
|
||
const healthAvailable = computed(() => !!statsData.value?.health?.available)
|
||
|
||
const lungRecoveryPercent = computed(() => {
|
||
const health = statsData.value?.health
|
||
if (!health || !health.available) return 0
|
||
const value = Number(health.lung_recovery_percent) || 0
|
||
return Math.min(Math.max(Math.round(value), 0), 100)
|
||
})
|
||
|
||
const healthRingStyle = computed(() => {
|
||
if (!healthAvailable.value) {
|
||
return {
|
||
background: 'conic-gradient(#E2E8F0 0deg 360deg)'
|
||
}
|
||
}
|
||
const angle = Math.round(lungRecoveryPercent.value * 3.6)
|
||
return {
|
||
background: `conic-gradient(#10B981 0deg ${angle}deg, #E2E8F0 ${angle}deg 360deg)`
|
||
}
|
||
})
|
||
|
||
const smokeFreeText = computed(() => {
|
||
const health = statsData.value?.health
|
||
if (!health || !health.available) return '0分钟'
|
||
const total = Math.max(Number(health.smoke_free_minutes) || 0, 0)
|
||
const days = Math.floor(total / 1440)
|
||
const hours = Math.floor((total % 1440) / 60)
|
||
const minutes = total % 60
|
||
if (days > 0) return `${days}天${hours}小时`
|
||
if (hours > 0) return `${hours}小时${minutes}分钟`
|
||
return `${minutes}分钟`
|
||
})
|
||
|
||
const healthItems = computed(() => {
|
||
const health = statsData.value?.health
|
||
if (!health || !health.available || !health.milestones || health.milestones.length === 0) return []
|
||
const minutes = health.smoke_free_minutes || 0
|
||
return health.milestones.map(item => {
|
||
if (item.reached) {
|
||
return { name: item.name, percent: 100 }
|
||
}
|
||
const baseMinutes = item.minutes || 1
|
||
const percent = Math.min(Math.round((minutes / baseMinutes) * 100), 100)
|
||
return { name: item.name, percent }
|
||
})
|
||
})
|
||
|
||
const streakDays = computed(() => statsData.value?.streak_days ?? 0)
|
||
const resistedTotal = computed(() => statsData.value?.resisted_total ?? 0)
|
||
|
||
const moneyIconClass = computed(() => {
|
||
const money = statsData.value?.money
|
||
if (!money || !money.available) return 'icon-muted'
|
||
if (moneyPercent.value >= 60) return 'icon-strong'
|
||
if (moneyPercent.value >= 30) return 'icon-mid'
|
||
return 'icon-low'
|
||
})
|
||
|
||
const healthIconClass = computed(() => {
|
||
const health = statsData.value?.health
|
||
if (!health || !health.available) return 'icon-muted'
|
||
const minutes = health.smoke_free_minutes || 0
|
||
if (minutes >= 1440) return 'icon-strong'
|
||
if (minutes >= 120) return 'icon-mid'
|
||
return 'icon-low'
|
||
})
|
||
|
||
const streakIconClass = computed(() => {
|
||
if (streakDays.value <= 0) return 'icon-muted'
|
||
if (streakDays.value >= 7) return 'icon-strong'
|
||
if (streakDays.value >= 3) return 'icon-mid'
|
||
return 'icon-low'
|
||
})
|
||
|
||
const resistedIconClass = computed(() => {
|
||
if (resistedTotal.value <= 0) return 'icon-muted'
|
||
if (resistedTotal.value >= 10) return 'icon-strong'
|
||
if (resistedTotal.value >= 5) return 'icon-mid'
|
||
return 'icon-low'
|
||
})
|
||
|
||
function formatTrendLabel(label, unit) {
|
||
if (!label) return ''
|
||
if (unit === 'month') {
|
||
const parts = label.split('-')
|
||
if (parts.length >= 2) {
|
||
return `${parseInt(parts[1], 10)}月`
|
||
}
|
||
return label
|
||
}
|
||
if (label.includes('-')) {
|
||
const parts = label.split('-')
|
||
const day = parts[2] || parts[1]
|
||
if (day) {
|
||
return `${parseInt(day, 10)}日`
|
||
}
|
||
}
|
||
return label
|
||
}
|
||
|
||
function formatRangeText(start, end) {
|
||
const startParts = start.split('-')
|
||
const endParts = end.split('-')
|
||
if (startParts.length < 3 || endParts.length < 3) return `${start} - ${end}`
|
||
const startMonth = parseInt(startParts[1], 10)
|
||
const startDay = parseInt(startParts[2], 10)
|
||
const endMonth = parseInt(endParts[1], 10)
|
||
const endDay = parseInt(endParts[2], 10)
|
||
if (startMonth === endMonth) {
|
||
return `${startMonth}月${startDay}日-${endDay}日`
|
||
}
|
||
return `${startMonth}月${startDay}日-${endMonth}月${endDay}日`
|
||
}
|
||
|
||
async function fetchStats() {
|
||
try {
|
||
await waitForLogin()
|
||
const res = await api.getStats({ range: currentTab.value })
|
||
statsData.value = res.data
|
||
} catch (e) {
|
||
console.error('fetchStats error:', e)
|
||
}
|
||
}
|
||
|
||
watch(currentTab, () => {
|
||
fetchStats()
|
||
})
|
||
|
||
onMounted(() => {
|
||
const sys = uni.getSystemInfoSync()
|
||
statusBarHeight.value = Math.max((sys.statusBarHeight || 0) - 20, 0)
|
||
fetchStats()
|
||
})
|
||
|
||
onShareAppMessage(() => {
|
||
return {
|
||
title: '戒烟助手 - 我的戒烟数据分析',
|
||
path: 'pages/index/index'
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page {
|
||
min-height: 100vh;
|
||
background: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 45%, #FFFFFF 100%);
|
||
padding: 0 32rpx 200rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.status-bar {
|
||
background: linear-gradient(to bottom, #D1FAE5, #F0FDF4);
|
||
height: 0;
|
||
}
|
||
|
||
.sticky-bar {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
background: linear-gradient(to bottom, #D1FAE5 0%, #F0FDF4 70%, rgba(255, 255, 255, 0.95) 100%);
|
||
padding-bottom: 8rpx;
|
||
}
|
||
|
||
.segment {
|
||
display: flex;
|
||
background-color: #FFFFFF;
|
||
padding: 6rpx;
|
||
border-radius: 20rpx;
|
||
gap: 6rpx;
|
||
margin-bottom: 16rpx;
|
||
border: 2rpx solid #ECFDF3;
|
||
box-shadow: 0 10rpx 22rpx rgba(16, 185, 129, 0.08);
|
||
}
|
||
|
||
.segment-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
padding: 14rpx 0;
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #64748B;
|
||
border-radius: 16rpx;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.segment-active {
|
||
background-color: #10B981;
|
||
color: #0B2F23;
|
||
box-shadow: 0 6rpx 16rpx rgba(16, 185, 129, 0.25);
|
||
}
|
||
|
||
.insight-card {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
align-items: flex-start;
|
||
background-color: #ECFDF3;
|
||
border: 2rpx solid #D9FBE7;
|
||
border-radius: 24rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 28rpx;
|
||
box-shadow: 0 10rpx 20rpx rgba(16, 185, 129, 0.1);
|
||
}
|
||
|
||
.insight-icon {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
border-radius: 20rpx;
|
||
background: linear-gradient(135deg, #BBF7D0 0%, #A7F3D0 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 10rpx 18rpx rgba(16, 185, 129, 0.18);
|
||
}
|
||
|
||
.insight-icon.insight-good {
|
||
background: linear-gradient(135deg, #BBF7D0 0%, #A7F3D0 100%);
|
||
box-shadow: 0 10rpx 18rpx rgba(16, 185, 129, 0.18);
|
||
}
|
||
|
||
.insight-icon.insight-warn {
|
||
background: linear-gradient(135deg, #FED7AA 0%, #FDBA74 100%);
|
||
box-shadow: 0 10rpx 18rpx rgba(249, 115, 22, 0.18);
|
||
}
|
||
|
||
.insight-icon.insight-neutral {
|
||
background: linear-gradient(135deg, #E2E8F0 0%, #CBD5F5 100%);
|
||
box-shadow: 0 10rpx 18rpx rgba(100, 116, 139, 0.12);
|
||
}
|
||
|
||
.insight-emoji {
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.insight-title {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #0F172A;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.insight-desc {
|
||
font-size: 24rpx;
|
||
color: #475569;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.section-sub {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
margin-top: 6rpx;
|
||
display: block;
|
||
}
|
||
|
||
.status-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
border: 2rpx solid transparent;
|
||
}
|
||
|
||
.status-good {
|
||
background-color: #E8FFF1;
|
||
color: #16A34A;
|
||
border-color: #BBF7D0;
|
||
}
|
||
|
||
.status-warn {
|
||
background-color: #FEF3C7;
|
||
color: #D97706;
|
||
border-color: #FDE68A;
|
||
}
|
||
|
||
.status-neutral {
|
||
background-color: #F1F5F9;
|
||
color: #64748B;
|
||
border-color: #E2E8F0;
|
||
}
|
||
|
||
.status-icon {
|
||
width: 22rpx;
|
||
height: 22rpx;
|
||
border-radius: 999rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18rpx;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.status-icon-good {
|
||
background-color: rgba(22, 163, 74, 0.15);
|
||
color: #16A34A;
|
||
}
|
||
|
||
.status-icon-warn {
|
||
background-color: rgba(217, 119, 6, 0.15);
|
||
color: #D97706;
|
||
}
|
||
|
||
.status-icon-neutral {
|
||
background-color: rgba(100, 116, 139, 0.15);
|
||
color: #64748B;
|
||
}
|
||
|
||
.status-text {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.trend-card {
|
||
background-color: #FFFFFF;
|
||
border-radius: 28rpx;
|
||
padding: 24rpx;
|
||
border: 2rpx solid #ECFDF3;
|
||
box-shadow: 0 12rpx 24rpx rgba(16, 185, 129, 0.1);
|
||
}
|
||
|
||
.trend-header {
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.trend-label {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
display: block;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.trend-value-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.trend-value {
|
||
font-size: 56rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.trend-unit {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.trend-chart-scroll {
|
||
width: 100%;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.trend-chart-empty {
|
||
width: 100%;
|
||
height: 240rpx;
|
||
margin-top: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #F8FAFC;
|
||
border-radius: 16rpx;
|
||
}
|
||
|
||
.trend-chart-empty-text {
|
||
font-size: 26rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.trend-chart {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
height: 240rpx;
|
||
gap: 12rpx;
|
||
padding: 36rpx 8rpx 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.trend-bar-item {
|
||
flex: 1;
|
||
min-width: 56rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
overflow: visible;
|
||
}
|
||
|
||
.trend-bar-wrap {
|
||
width: 32rpx;
|
||
height: 180rpx;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.trend-bar {
|
||
width: 32rpx;
|
||
min-height: 12rpx;
|
||
background: linear-gradient(180deg, #34D399 0%, #10B981 100%);
|
||
border-radius: 12rpx 12rpx 8rpx 8rpx;
|
||
}
|
||
|
||
.trend-bubble {
|
||
position: absolute;
|
||
top: 0;
|
||
transform: translateY(-12rpx);
|
||
background-color: #0F766E;
|
||
color: #FFFFFF;
|
||
font-size: 20rpx;
|
||
padding: 4rpx 10rpx;
|
||
border-radius: 12rpx;
|
||
}
|
||
|
||
.trend-bar-label {
|
||
margin-top: 12rpx;
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.trend-bar-label-active {
|
||
color: #16A34A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.savings-card {
|
||
background-color: #FFFFFF;
|
||
border-radius: 28rpx;
|
||
padding: 24rpx;
|
||
margin-top: 20rpx;
|
||
border: 2rpx solid #FEF3C7;
|
||
box-shadow: 0 12rpx 24rpx rgba(245, 158, 11, 0.08);
|
||
}
|
||
|
||
.savings-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.savings-icon {
|
||
width: 56rpx;
|
||
height: 56rpx;
|
||
border-radius: 16rpx;
|
||
background: linear-gradient(135deg, #FDE68A 0%, #FBBF24 100%);
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #D97706;
|
||
box-shadow: 0 8rpx 16rpx rgba(245, 158, 11, 0.2);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.savings-icon.icon-muted {
|
||
background: #F1F5F9;
|
||
color: #94A3B8;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.savings-icon.icon-low {
|
||
background: linear-gradient(135deg, #FEF3C7 0%, #FCD34D 100%);
|
||
}
|
||
|
||
.savings-icon.icon-mid {
|
||
background: linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%);
|
||
}
|
||
|
||
.savings-icon.icon-strong {
|
||
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
|
||
}
|
||
|
||
.savings-title {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.savings-header-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.savings-subtitle {
|
||
font-size: 20rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.savings-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.savings-left {
|
||
flex: 1;
|
||
}
|
||
|
||
.savings-value {
|
||
font-size: 44rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-bottom: 16rpx;
|
||
display: block;
|
||
}
|
||
|
||
.savings-bar {
|
||
height: 10rpx;
|
||
background-color: #F1F5F9;
|
||
border-radius: 999rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.savings-bar-fill {
|
||
height: 100%;
|
||
background-color: #F59E0B;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.savings-sub {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
margin-top: 10rpx;
|
||
display: block;
|
||
}
|
||
|
||
.icon-emoji {
|
||
font-size: 28rpx;
|
||
line-height: 1;
|
||
}
|
||
|
||
.savings-icon .icon-emoji {
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.health-icon .icon-emoji {
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.mini-icon .icon-emoji {
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.savings-metrics {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
margin-top: 14rpx;
|
||
}
|
||
|
||
.metric-chip {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
gap: 6rpx;
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
background-color: #FFFBEB;
|
||
color: #B45309;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.metric-chip-actual {
|
||
background-color: #EFF6FF;
|
||
color: #2563EB;
|
||
}
|
||
|
||
.metric-label {
|
||
font-size: 20rpx;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.metric-value {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.metric-unit {
|
||
font-size: 20rpx;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.savings-ring {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.savings-ring-inner {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
background-color: #FFFFFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 6rpx 16rpx rgba(245, 158, 11, 0.12);
|
||
}
|
||
|
||
.ring-value {
|
||
font-size: 22rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.card-empty {
|
||
margin-top: 16rpx;
|
||
padding: 12rpx 16rpx;
|
||
border-radius: 16rpx;
|
||
background-color: #F8FAFC;
|
||
border: 2rpx dashed #E2E8F0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.card-empty-icon {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.card-empty-text {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.health-card {
|
||
background-color: #FFFFFF;
|
||
border-radius: 28rpx;
|
||
padding: 24rpx;
|
||
margin-top: 24rpx;
|
||
border: 2rpx solid #ECFDF3;
|
||
box-shadow: 0 12rpx 24rpx rgba(16, 185, 129, 0.08);
|
||
}
|
||
|
||
.health-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.health-title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.health-icon {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
background: linear-gradient(135deg, #DCFCE7 0%, #A7F3D0 100%);
|
||
border-radius: 14rpx;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #16A34A;
|
||
box-shadow: 0 8rpx 16rpx rgba(16, 185, 129, 0.18);
|
||
}
|
||
|
||
.health-icon.icon-muted {
|
||
background: #F1F5F9;
|
||
color: #94A3B8;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.health-icon.icon-low {
|
||
background: linear-gradient(135deg, #DCFCE7 0%, #86EFAC 100%);
|
||
}
|
||
|
||
.health-icon.icon-mid {
|
||
background: linear-gradient(135deg, #BBF7D0 0%, #4ADE80 100%);
|
||
}
|
||
|
||
.health-icon.icon-strong {
|
||
background: linear-gradient(135deg, #34D399 0%, #10B981 100%);
|
||
}
|
||
|
||
.health-title {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
}
|
||
|
||
.health-badge {
|
||
font-size: 20rpx;
|
||
padding: 6rpx 14rpx;
|
||
border-radius: 999rpx;
|
||
background-color: #E8FFF1;
|
||
color: #16A34A;
|
||
font-weight: 600;
|
||
border: 2rpx solid #BBF7D0;
|
||
}
|
||
|
||
.health-badge-muted {
|
||
background-color: #F1F5F9;
|
||
color: #64748B;
|
||
border-color: #E2E8F0;
|
||
}
|
||
|
||
.health-overview {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.health-metric {
|
||
flex: 1;
|
||
background-color: #F0FDF4;
|
||
border-radius: 18rpx;
|
||
padding: 16rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
border: 2rpx solid #DCFCE7;
|
||
}
|
||
|
||
.health-metric-icon {
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.health-metric-value {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: #0F172A;
|
||
}
|
||
|
||
.health-metric-label {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
}
|
||
|
||
.health-ring {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.health-ring-inner {
|
||
width: 88rpx;
|
||
height: 88rpx;
|
||
border-radius: 50%;
|
||
background-color: #FFFFFF;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2rpx;
|
||
box-shadow: 0 6rpx 16rpx rgba(16, 185, 129, 0.12);
|
||
}
|
||
|
||
.ring-label {
|
||
font-size: 18rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.health-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.health-item-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.health-item-name {
|
||
font-size: 24rpx;
|
||
color: #111827;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.health-item-percent {
|
||
font-size: 22rpx;
|
||
color: #16A34A;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.health-item-percent-muted {
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.health-bar {
|
||
height: 10rpx;
|
||
background-color: #F1F5F9;
|
||
border-radius: 999rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.health-bar-fill {
|
||
height: 100%;
|
||
background-color: #16A34A;
|
||
border-radius: 999rpx;
|
||
}
|
||
|
||
.health-bar-fill-muted {
|
||
background-color: #C7E6D4;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
.mini-card {
|
||
flex: 1;
|
||
background-color: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
padding: 24rpx;
|
||
border: 2rpx solid #ECFDF3;
|
||
box-shadow: 0 12rpx 24rpx rgba(16, 185, 129, 0.06);
|
||
}
|
||
|
||
.mini-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border-radius: 12rpx;
|
||
margin-bottom: 12rpx;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.mini-icon-fire {
|
||
background-color: #FEE2E2;
|
||
color: #F97316;
|
||
}
|
||
|
||
.mini-icon-block {
|
||
background-color: #E0E7FF;
|
||
color: #6366F1;
|
||
}
|
||
|
||
.mini-icon.icon-muted {
|
||
background-color: #F1F5F9;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.mini-icon-fire.icon-low {
|
||
background-color: #FFE8D2;
|
||
}
|
||
|
||
.mini-icon-fire.icon-mid {
|
||
background-color: #FFD8B5;
|
||
}
|
||
|
||
.mini-icon-fire.icon-strong {
|
||
background-color: #FDBA74;
|
||
}
|
||
|
||
.mini-icon-block.icon-low {
|
||
background-color: #E0E7FF;
|
||
}
|
||
|
||
.mini-icon-block.icon-mid {
|
||
background-color: #C7D2FE;
|
||
}
|
||
|
||
.mini-icon-block.icon-strong {
|
||
background-color: #A5B4FC;
|
||
}
|
||
|
||
|
||
.mini-label {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
margin-bottom: 10rpx;
|
||
display: block;
|
||
}
|
||
|
||
.mini-value-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
.mini-value {
|
||
font-size: 40rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.mini-unit {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
|
||
.mini-sub {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
margin-top: 6rpx;
|
||
display: block;
|
||
}
|
||
</style>
|
||
|
||
|
||
|