This commit is contained in:
nepiedg
2026-02-23 22:24:29 +08:00
parent ff68f7f3ae
commit 031eef9643
7 changed files with 494 additions and 148 deletions
+18 -4
View File
@@ -1,9 +1,19 @@
import { BASE_URL } from '@/config'
import { storage, SESSION_KEY } from '@/utils/storage'
import { login as authLogin } from './auth'
// 是否为 token 失效(HTTP 401 或 body code 401,如 invalid token
function isInvalidToken(res) {
if (res.statusCode === 401) return true
const body = res.data
if (body && body.code === 401) return true
return false
}
export const request = {
async request(options) {
const sessionKey = storage.get(SESSION_KEY)
const isRetryAfter401 = options._retryAfter401 === true
return new Promise((resolve, reject) => {
uni.request({
@@ -15,11 +25,15 @@ export const request = {
'Authorization': sessionKey ? `Bearer ${sessionKey}` : ''
},
success: async (res) => {
if (res.statusCode === 401) {
const { login } = await import('./auth')
if (isInvalidToken(res)) {
if (isRetryAfter401) {
reject(new Error(res.data?.message || 'invalid token'))
return
}
try {
await login()
resolve(this.request(options))
await authLogin()
const nextOpts = { ...options, _retryAfter401: true }
resolve(this.request(nextOpts))
} catch (e) {
reject(e)
}
@@ -147,9 +147,12 @@ export default {
num: this.initialData.num ?? 1
}
} else {
// 新建模式,使用当前时间
// 新建模式,使用当前本地时间(不用 toISOString,避免 UTC 导致日期差一天)
const now = new Date()
const dateStr = now.toISOString().split('T')[0]
const y = now.getFullYear()
const m = String(now.getMonth() + 1).padStart(2, '0')
const d = String(now.getDate()).padStart(2, '0')
const dateStr = `${y}-${m}-${d}`
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
const datetimeStr = `${dateStr} ${timeStr}:00`
+1 -1
View File
@@ -1,6 +1,6 @@
const ENV = {
development: {
BASE_URL: ' http://192.168.31.132:8080/api/v1',
BASE_URL: 'http://192.168.31.46:8080/api/v1',
MINI_PROGRAM_ID: 2
},
production: {
+10 -5
View File
@@ -13,9 +13,6 @@
{{ tab.label }}
</view>
</view>
<view class="filter-btn">
<text class="filter-icon">📅</text>
</view>
</view>
<!-- 记录列表 -->
@@ -165,6 +162,14 @@ const groupedLogs = computed(() => {
}, {})
})
// 本地日期 YYYY-MM-DD(避免 toISOString 用 UTC 导致日期差一天)
function localDateStr(d) {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
// 格式化分组标题
function formatGroupTitle(dateStr) {
if (!dateStr) return ''
@@ -174,8 +179,8 @@ function formatGroupTitle(dateStr) {
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
const todayStr = today.toISOString().split('T')[0]
const yesterdayStr = yesterday.toISOString().split('T')[0]
const todayStr = localDateStr(today)
const yesterdayStr = localDateStr(yesterday)
if (dateStr === todayStr) {
return '今天'
+453 -131
View File
@@ -1,4 +1,4 @@
<template>
<template>
<view class="page">
<view class="sticky-bar">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
@@ -16,7 +16,9 @@
</view>
<view class="insight-card">
<view class="insight-icon"></view>
<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>
@@ -30,7 +32,7 @@
<text class="section-sub">{{ trendRangeText }}</text>
</view>
<view class="status-chip" :class="statusChipClass">
<view class="status-dot"></view>
<text class="status-icon" :class="statusIconClass">{{ statusArrow }}</text>
<text class="status-text">{{ statusText }}</text>
</view>
</view>
@@ -43,12 +45,19 @@
<text class="trend-unit">/</text>
</view>
</view>
<view class="trend-chart">
<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" :style="{ height: item.height }"></view>
<text class="trend-bar-label" :class="{ 'trend-bar-label-active': item.isHighlight }">{{ item.label }}</text>
<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>
@@ -58,37 +67,72 @@
<view class="savings-card">
<view class="savings-header">
<view class="savings-icon" :class="moneyIconClass">
<view class="icon-coin"></view>
<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>
<text class="savings-title">节省金额</text>
</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 class="savings-sub">目标 ¥{{ moneyTargetYuan }} ({{ moneyPercent }}%)</text>
<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">{{ moneyPercent }}%</text>
<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">
<view class="icon-plus-vertical"></view>
<view class="icon-plus-horizontal"></view>
<text class="icon-emoji"></text>
</view>
<text class="health-title">健康恢复里程碑</text>
</view>
<view class="health-badge">{{ healthStatusText }}</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">
@@ -110,18 +154,18 @@
<view class="stats-grid">
<view class="mini-card">
<view class="mini-icon mini-icon-fire" :class="streakIconClass">
<view class="icon-flame"></view>
<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>
<text class="mini-sub">连续记录天数</text>
</view>
<view class="mini-card">
<view class="mini-icon mini-icon-block" :class="resistedIconClass">
<view class="icon-shield"></view>
<text class="icon-emoji">🛡</text>
</view>
<text class="mini-label">已拒绝</text>
<view class="mini-value-row">
@@ -152,15 +196,32 @@ const tabs = [
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(() => {
const change = statsData.value?.change_percent
if (change === undefined || change === null) {
return '前5天保持完成记录!周日虽有小量吸烟,但整体趋势依然可控,下周继续保持。'
if (changePercent.value === null) {
return '本阶段数据逐步稳定,建议保持记录,下一周期可获得趋势对比。'
}
if (change <= 0) {
return '本阶段总体保持良好,趋势持续向好,继续保持节奏。'
const abs = Math.abs(changePercent.value)
if (changePercent.value <= 0) {
return `较上期下降 ${abs}%,趋势向好,继续保持节奏。`
}
return '近期吸烟量略有上升,试着减少高峰时段的冲动。'
return `较上期上升 ${abs}%,留意高峰时段,尝试延迟第一支。`
})
const trendRangeText = computed(() => {
@@ -173,15 +234,24 @@ const trendRangeText = computed(() => {
})
const statusText = computed(() => {
const change = statsData.value?.change_percent
if (change === undefined || change === null) return '总体良好'
return change <= 0 ? '总体良好' : '有所增加'
if (changePercent.value === null) return '暂无对比'
const sign = changePercent.value > 0 ? '+' : ''
return `较上期 ${sign}${changePercent.value}%`
})
const statusChipClass = computed(() => {
const change = statsData.value?.change_percent
if (change === undefined || change === null) return 'status-good'
return change <= 0 ? 'status-good' : 'status-warn'
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(() => {
@@ -190,40 +260,57 @@ const averageCount = computed(() => {
return avg
})
// 图表仅使用接口返回的 trend 渲染,无数据时为空
const trendItems = computed(() => {
const trend = statsData.value?.trend
const trendUnit = statsData.value?.trend_unit
if (!trend || trend.length === 0) {
return [
{ label: '19日', count: 0, height: '6%', isHighlight: false },
{ label: '20日', count: 0, height: '6%', isHighlight: false },
{ label: '21日', count: 0, height: '6%', isHighlight: false },
{ label: '22日', count: 0, height: '6%', isHighlight: false },
{ label: '23日', count: 0, height: '6%', isHighlight: false },
{ label: '24日', count: 0, height: '6%', isHighlight: false },
{ label: '25日', count: 6, height: '85%', isHighlight: true }
]
if (!trend || !Array.isArray(trend) || trend.length === 0) {
return []
}
const maxCount = Math.max(...trend.map(item => item.count), 1)
const maxCount = Math.max(...trend.map(item => Number(item.count) || 0), 1)
return trend.map((item, index) => {
const height = `${Math.max((item.count / maxCount) * 100, 6)}%`
const count = Number(item.count) || 0
const height = `${Math.max((count / maxCount) * 100, 6)}%`
return {
label: formatTrendLabel(item.label, trendUnit),
count: item.count,
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 '¥0.00'
}
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
@@ -250,6 +337,11 @@ const moneyTargetYuan = computed(() => {
})
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 {
@@ -257,24 +349,56 @@ const moneyRingStyle = computed(() => {
}
})
const moneyRingText = computed(() => {
if (!moneyAvailable.value) return '--'
return `${moneyPercent.value}%`
})
const healthStatusText = computed(() => {
const health = statsData.value?.health
if (!health || !health.available) return '进行中'
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 [
{ name: '心率血压恢复正常', percent: 100 },
{ name: '血氧水平恢复', percent: 100 },
{ name: '一氧化碳排出', percent: 95 },
{ name: '嗅觉味觉改善', percent: 40 },
{ name: '肺部功能恢复', percent: 10 }
]
}
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) {
@@ -286,8 +410,8 @@ const healthItems = computed(() => {
})
})
const streakDays = computed(() => statsData.value?.streak_days ?? 12)
const resistedTotal = computed(() => statsData.value?.resisted_total ?? 24)
const streakDays = computed(() => statsData.value?.streak_days ?? 0)
const resistedTotal = computed(() => statsData.value?.resisted_total ?? 0)
const moneyIconClass = computed(() => {
const money = statsData.value?.money
@@ -438,11 +562,30 @@ onMounted(() => {
.insight-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background-color: #D9FBE7;
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;
}
@@ -507,11 +650,37 @@ onMounted(() => {
border-color: #FDE68A;
}
.status-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: currentColor;
.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 {
@@ -554,25 +723,61 @@ onMounted(() => {
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;
}
@@ -616,40 +821,35 @@ onMounted(() => {
}
.savings-icon {
width: 32rpx;
height: 32rpx;
border-radius: 8rpx;
background-color: #FDE68A;
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-color: #F1F5F9;
background: #F1F5F9;
color: #94A3B8;
box-shadow: none;
}
.savings-icon.icon-low {
color: #E59E0B;
background: linear-gradient(135deg, #FEF3C7 0%, #FCD34D 100%);
}
.savings-icon.icon-mid {
color: #D97706;
background: linear-gradient(135deg, #FCD34D 0%, #F59E0B 100%);
}
.savings-icon.icon-strong {
color: #B45309;
}
.icon-coin {
width: 18rpx;
height: 12rpx;
border-radius: 4rpx;
border: 3rpx solid currentColor;
border-top-width: 6rpx;
background: linear-gradient(135deg, #F59E0B 0%, #D97706 100%);
}
.savings-title {
@@ -658,6 +858,17 @@ onMounted(() => {
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;
@@ -697,6 +908,62 @@ onMounted(() => {
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;
@@ -723,6 +990,26 @@ onMounted(() => {
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;
@@ -746,52 +1033,34 @@ onMounted(() => {
}
.health-icon {
width: 28rpx;
height: 28rpx;
background-color: #DCFCE7;
border-radius: 8rpx;
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-color: #F1F5F9;
background: #F1F5F9;
color: #94A3B8;
box-shadow: none;
}
.health-icon.icon-low {
color: #16A34A;
background: linear-gradient(135deg, #DCFCE7 0%, #86EFAC 100%);
}
.health-icon.icon-mid {
color: #16A34A;
background: linear-gradient(135deg, #BBF7D0 0%, #4ADE80 100%);
}
.health-icon.icon-strong {
color: #0F766E;
}
.icon-plus-vertical,
.icon-plus-horizontal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: currentColor;
border-radius: 999rpx;
}
.icon-plus-vertical {
width: 4rpx;
height: 16rpx;
}
.icon-plus-horizontal {
width: 16rpx;
height: 4rpx;
background: linear-gradient(135deg, #34D399 0%, #10B981 100%);
}
.health-title {
@@ -810,6 +1079,73 @@ onMounted(() => {
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;
@@ -898,43 +1234,29 @@ onMounted(() => {
}
.mini-icon-fire.icon-low {
color: #FB923C;
background-color: #FFE8D2;
}
.mini-icon-fire.icon-mid {
color: #F97316;
background-color: #FFD8B5;
}
.mini-icon-fire.icon-strong {
color: #EA580C;
background-color: #FDBA74;
}
.mini-icon-block.icon-low {
color: #818CF8;
background-color: #E0E7FF;
}
.mini-icon-block.icon-mid {
color: #6366F1;
background-color: #C7D2FE;
}
.mini-icon-block.icon-strong {
color: #4F46E5;
background-color: #A5B4FC;
}
.icon-flame {
width: 16rpx;
height: 20rpx;
background-color: currentColor;
border-radius: 50% 50% 50% 50%;
transform: rotate(45deg);
}
.icon-shield {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
border: 3rpx solid currentColor;
}
.mini-label {
font-size: 22rpx;
+5 -2
View File
@@ -88,7 +88,7 @@ export const useLogsStore = defineStore('logs', {
}
}
// 获取显示日期
// 获取显示日期(用本地日期,避免 UTC 导致差一天)
let displayDate = ''
if (log.smoke_time) {
displayDate = log.smoke_time.split('T')[0]
@@ -96,7 +96,10 @@ export const useLogsStore = defineStore('logs', {
const date = typeof log.createtime === 'number'
? new Date(log.createtime * 1000)
: new Date(log.createtime)
displayDate = date.toISOString().split('T')[0]
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
displayDate = `${y}-${m}-${d}`
}
return {
+2 -3
View File
@@ -59,14 +59,13 @@ export function getGreeting() {
}
export function isToday(dateStr) {
const today = new Date().toISOString().split('T')[0]
return dateStr === today
return dateStr === formatDate(new Date())
}
export function isYesterday(dateStr) {
const yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)
return dateStr === yesterday.toISOString().split('T')[0]
return dateStr === formatDate(yesterday)
}
export function daysBetween(date1, date2) {