feat: enhance logs and stats UI with new headers, summaries, and improved data display

This commit is contained in:
nepiedg
2026-04-27 00:11:13 +08:00
parent 895f5e6d09
commit 203f97385b
3 changed files with 600 additions and 160 deletions
+230 -59
View File
@@ -1,5 +1,16 @@
<template> <template>
<view class="page"> <view class="page">
<view class="page-head">
<view>
<text class="page-title">记录时间线</text>
<text class="page-subtitle">按时间回看真实抽烟节奏</text>
</view>
<view class="head-action" @tap="addLog">
<text class="head-action-plus">+</text>
<text>记录</text>
</view>
</view>
<view class="filters-sticky"> <view class="filters-sticky">
<view class="filters"> <view class="filters">
<view class="tabs"> <view class="tabs">
@@ -24,7 +35,27 @@
@refresherrefresh="onRefresh" @refresherrefresh="onRefresh"
@scrolltolower="onLoadMore" @scrolltolower="onLoadMore"
> >
<text class="section-label">时间记录</text> <view class="overview">
<view class="overview-item overview-primary">
<text class="overview-label">今日已抽</text>
<text class="overview-value">{{ todaySmokeCount }}</text>
<text class="overview-unit"></text>
</view>
<view class="overview-item">
<text class="overview-label">当前列表</text>
<text class="overview-value">{{ filteredLogs.length }}</text>
<text class="overview-unit"></text>
</view>
<view class="overview-item">
<text class="overview-label">最近记录</text>
<text class="overview-clock">{{ latestDisplayTime }}</text>
</view>
</view>
<view class="section-head">
<text class="section-label">时间记录</text>
<text class="section-note">{{ currentTab === 'smoke' ? '仅看抽烟记录' : '按日期倒序' }}</text>
</view>
<view v-if="logsStore.loading && logsStore.logs.length === 0" class="skeleton"> <view v-if="logsStore.loading && logsStore.logs.length === 0" class="skeleton">
<view v-for="i in 3" :key="i" class="skeleton-item"> <view v-for="i in 3" :key="i" class="skeleton-item">
@@ -56,12 +87,12 @@
<view class="log-time-tag"> <view class="log-time-tag">
<text class="log-time">{{ log.displayTime || '--:--' }}</text> <text class="log-time">{{ log.displayTime || '--:--' }}</text>
<text class="log-tag" :class="log.type === 'resisted' ? 'tag-resisted' : 'tag-smoke'"> <text class="log-tag" :class="log.type === 'resisted' ? 'tag-resisted' : 'tag-smoke'">
{{ log.type === 'resisted' ? '已忍住' : '已抽烟' }} {{ log.type === 'resisted' ? '旧记录' : '已抽烟' }}
</text> </text>
</view> </view>
<view class="log-right"> <view class="log-right">
<text v-if="log.type === 'smoke'" class="count-pill">{{ log.num !== undefined && log.num !== null ? log.num : 0 }}</text> <text v-if="log.type === 'smoke'" class="count-pill">{{ log.num !== undefined && log.num !== null ? log.num : 0 }}</text>
<text v-else class="thumb-pill">👍</text> <text v-else class="thumb-pill">0</text>
</view> </view>
</view> </view>
@@ -103,7 +134,7 @@
<text class="empty-icon"></text> <text class="empty-icon"></text>
</view> </view>
<text class="empty-text">今天还没有记录</text> <text class="empty-text">今天还没有记录</text>
<text class="empty-hint">点击右下角悬浮按钮快速记录抽烟或忍住的时刻时间线会从这里开始</text> <text class="empty-hint">完成一次抽烟记录后时间线会从这里开始生成</text>
<view class="empty-action" @tap="addLog">立即记录</view> <view class="empty-action" @tap="addLog">立即记录</view>
</view> </view>
@@ -143,8 +174,7 @@ const logsStore = useLogsStore()
const tabs = [ const tabs = [
{ label: '全部', value: 'all' }, { label: '全部', value: 'all' },
{ label: '抽烟', value: 'smoke' }, { label: '抽烟记录', value: 'smoke' }
{ label: '已忍住', value: 'resisted' }
] ]
const currentTab = ref('all') const currentTab = ref('all')
@@ -174,6 +204,20 @@ const groupedLogs = computed(() => {
}, {}) }, {})
}) })
const todaySmokeCount = computed(() => {
const today = localDateStr(new Date())
return logsStore.formattedLogs.reduce((total, log) => {
if (log.displayDate !== today || log.type !== 'smoke') return total
return total + (Number(log.num) || 0)
}, 0)
})
const latestDisplayTime = computed(() => {
const latest = logsStore.formattedLogs[0]
if (!latest) return '--:--'
return latest.displayTime || '--:--'
})
// 本地日期 YYYY-MM-DD(避免 toISOString 用 UTC 导致日期差一天) // 本地日期 YYYY-MM-DD(避免 toISOString 用 UTC 导致日期差一天)
function localDateStr(d) { function localDateStr(d) {
const y = d.getFullYear() const y = d.getFullYear()
@@ -250,7 +294,7 @@ async function handleUpdate(data) {
function handleDelete(log) { function handleDelete(log) {
uni.showModal({ uni.showModal({
title: '确认删除', title: '确认删除',
content: `确定要删除这条${log.type === 'resisted' ? '忍住' : '抽烟'}记录吗?`, content: `确定要删除这条${log.type === 'resisted' ? '' : '抽烟'}记录吗?`,
confirmColor: '#EF4444', confirmColor: '#EF4444',
success: async (res) => { success: async (res) => {
if (res.confirm) { if (res.confirm) {
@@ -311,14 +355,62 @@ onShareAppMessage(() => {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: #F5F7F6; background:
radial-gradient(circle at 10% 0%, rgba(103, 232, 249, 0.16), transparent 32%),
linear-gradient(180deg, #F6F8F6 0%, #EFF4F1 54%, #E9F0EC 100%);
padding: 0 32rpx; padding: 0 32rpx;
box-sizing: border-box; box-sizing: border-box;
} }
.page-head {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
padding: 34rpx 0 18rpx;
}
.page-title {
display: block;
font-size: 42rpx;
line-height: 1.2;
font-weight: 900;
color: #1E293B;
}
.page-subtitle {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.45;
color: #64748B;
}
.head-action {
flex-shrink: 0;
height: 62rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
gap: 8rpx;
border-radius: 999rpx;
background: linear-gradient(135deg, #10B981, #06B6D4);
box-shadow: 0 12rpx 28rpx rgba(16, 185, 129, 0.18);
color: #FFFFFF;
font-size: 23rpx;
font-weight: 900;
}
.head-action-plus {
font-size: 32rpx;
line-height: 1;
font-weight: 700;
}
.filters-sticky { .filters-sticky {
position: relative; position: relative;
height: 120rpx; height: 96rpx;
flex-shrink: 0; flex-shrink: 0;
z-index: 20; z-index: 20;
} }
@@ -328,38 +420,109 @@ onShareAppMessage(() => {
left: 32rpx; left: 32rpx;
right: 32rpx; right: 32rpx;
z-index: 50; z-index: 50;
margin: 12rpx 0 0; margin: 8rpx 0 0;
} }
.tabs { .tabs {
display: flex; display: flex;
background: #FFFFFF; background: rgba(255, 255, 255, 0.82);
border-radius: 24rpx; border: 1rpx solid rgba(226, 232, 240, 0.82);
border-radius: 22rpx;
padding: 6rpx; padding: 6rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03); box-shadow: 0 12rpx 30rpx rgba(30, 41, 59, 0.06);
backdrop-filter: blur(12px);
} }
.tab { .tab {
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 16rpx 0; padding: 15rpx 0;
border-radius: 20rpx; border-radius: 18rpx;
font-size: 26rpx; font-size: 25rpx;
font-weight: 600; font-weight: 900;
color: #999999; color: #64748B;
} }
.tab-active { .tab-active {
background: #10B981; background: linear-gradient(135deg, #10B981, #06B6D4);
color: #FFFFFF; color: #FFFFFF;
box-shadow: 0 8rpx 18rpx rgba(16, 185, 129, 0.16);
}
.overview {
display: grid;
grid-template-columns: 1.25fr 1fr 1fr;
gap: 14rpx;
margin-bottom: 24rpx;
}
.overview-item {
min-width: 0;
padding: 20rpx 18rpx;
border-radius: 26rpx;
background: rgba(255, 255, 255, 0.76);
border: 1rpx solid rgba(255, 255, 255, 0.82);
box-shadow: 0 12rpx 30rpx rgba(30, 41, 59, 0.05);
}
.overview-primary {
background:
radial-gradient(circle at top right, rgba(251, 191, 36, 0.18), transparent 36%),
rgba(255, 255, 255, 0.78);
}
.overview-label {
display: block;
font-size: 20rpx;
line-height: 1.3;
font-weight: 800;
color: #64748B;
}
.overview-value {
display: inline-block;
margin-top: 8rpx;
font-size: 40rpx;
line-height: 1;
font-weight: 900;
color: #1E293B;
}
.overview-unit {
margin-left: 4rpx;
font-size: 20rpx;
font-weight: 800;
color: #64748B;
}
.overview-clock {
display: block;
margin-top: 12rpx;
font-size: 30rpx;
line-height: 1;
font-weight: 900;
color: #0F766E;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 18rpx;
} }
.section-label { .section-label {
display: block; display: block;
margin: 0 0 18rpx 6rpx; font-size: 29rpx;
font-size: 28rpx; font-weight: 900;
font-weight: 600; color: #1E293B;
color: #666666; }
.section-note {
font-size: 21rpx;
font-weight: 800;
color: #64748B;
} }
.scroll-container { .scroll-container {
@@ -428,27 +591,31 @@ onShareAppMessage(() => {
} }
.log-group { .log-group {
margin-bottom: 28rpx; margin-bottom: 30rpx;
} }
.group-header { .group-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12rpx; justify-content: space-between;
gap: 16rpx;
margin-bottom: 14rpx; margin-bottom: 14rpx;
padding: 0 4rpx;
} }
.group-title { .group-title {
font-size: 28rpx; font-size: 28rpx;
font-weight: 700; font-weight: 900;
color: #1A1A1A; color: #1E293B;
} }
.group-count { .group-count {
font-size: 22rpx; font-size: 21rpx;
color: #999999; font-weight: 800;
background-color: #F0F0F0; color: #64748B;
padding: 6rpx 16rpx; background-color: rgba(255, 255, 255, 0.72);
border: 1rpx solid rgba(226, 232, 240, 0.72);
padding: 6rpx 14rpx;
border-radius: 999rpx; border-radius: 999rpx;
} }
@@ -460,10 +627,11 @@ onShareAppMessage(() => {
.log-card { .log-card {
position: relative; position: relative;
background: #FFFFFF; background: rgba(255, 255, 255, 0.82);
border-radius: 24rpx; border: 1rpx solid rgba(255, 255, 255, 0.86);
padding: 24rpx 24rpx 20rpx 24rpx; border-radius: 28rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03); padding: 24rpx 24rpx 20rpx;
box-shadow: 0 14rpx 34rpx rgba(30, 41, 59, 0.06);
display: flex; display: flex;
gap: 20rpx; gap: 20rpx;
overflow: hidden; overflow: hidden;
@@ -482,18 +650,18 @@ onShareAppMessage(() => {
left: 0; left: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 8rpx; width: 7rpx;
background-color: #10B981; background: linear-gradient(180deg, #67E8F9, #10B981);
} }
.log-card-smoke .log-bar { .log-card-smoke .log-bar {
background-color: #F59E0B; background: linear-gradient(180deg, #FBBF24, #D97706);
} }
.log-icon { .log-icon {
width: 80rpx; width: 76rpx;
height: 80rpx; height: 76rpx;
border-radius: 20rpx; border-radius: 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -503,12 +671,12 @@ onShareAppMessage(() => {
} }
.icon-resisted { .icon-resisted {
background-color: #E8F5F0; background-color: rgba(232, 245, 240, 0.92);
color: #10B981; color: #0F766E;
} }
.icon-smoke { .icon-smoke {
background-color: #FEF3C7; background: linear-gradient(135deg, rgba(254, 243, 199, 0.96), rgba(255, 251, 235, 0.9));
color: #D97706; color: #D97706;
} }
@@ -532,16 +700,16 @@ onShareAppMessage(() => {
} }
.log-time { .log-time {
font-size: 30rpx; font-size: 31rpx;
font-weight: 700; font-weight: 900;
color: #1A1A1A; color: #1E293B;
} }
.log-tag { .log-tag {
font-size: 22rpx; font-size: 22rpx;
padding: 8rpx 16rpx; padding: 7rpx 14rpx;
border-radius: 999rpx; border-radius: 999rpx;
font-weight: 600; font-weight: 900;
} }
.tag-smoke { .tag-smoke {
@@ -564,10 +732,10 @@ onShareAppMessage(() => {
.count-pill { .count-pill {
font-size: 22rpx; font-size: 22rpx;
color: #D97706; color: #D97706;
background-color: #FEF3C7; background-color: rgba(254, 243, 199, 0.92);
padding: 8rpx 16rpx; padding: 8rpx 16rpx;
border-radius: 999rpx; border-radius: 999rpx;
font-weight: 600; font-weight: 900;
} }
.thumb-pill { .thumb-pill {
@@ -580,9 +748,9 @@ onShareAppMessage(() => {
.log-desc { .log-desc {
font-size: 25rpx; font-size: 25rpx;
color: #666666; color: #475569;
line-height: 1.5; line-height: 1.55;
margin-bottom: 10rpx; margin-bottom: 12rpx;
} }
.reason-tag-row { .reason-tag-row {
@@ -612,7 +780,7 @@ onShareAppMessage(() => {
.level-text { .level-text {
font-size: 22rpx; font-size: 22rpx;
font-weight: 600; font-weight: 800;
} }
.log-interval { .log-interval {
@@ -627,12 +795,13 @@ onShareAppMessage(() => {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 16rpx; gap: 16rpx;
margin-top: 12rpx; margin-top: 16rpx;
} }
.action-btn { .action-btn {
font-size: 22rpx; font-size: 22rpx;
padding: 8rpx 16rpx; font-weight: 900;
padding: 9rpx 17rpx;
border-radius: 999rpx; border-radius: 999rpx;
} }
@@ -678,7 +847,9 @@ onShareAppMessage(() => {
justify-content: center; justify-content: center;
padding: 132rpx 32rpx 116rpx; padding: 132rpx 32rpx 116rpx;
border-radius: 32rpx; border-radius: 32rpx;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(247, 250, 248, 0.94) 100%); background:
radial-gradient(circle at top, rgba(103, 232, 249, 0.18), transparent 38%),
linear-gradient(180deg, rgba(255, 255, 255, 0.94) 0%, rgba(247, 250, 248, 0.9) 100%);
box-shadow: 0 16rpx 38rpx rgba(15, 23, 42, 0.06); box-shadow: 0 16rpx 38rpx rgba(15, 23, 42, 0.06);
overflow: hidden; overflow: hidden;
} }
@@ -753,7 +924,7 @@ onShareAppMessage(() => {
min-width: 170rpx; min-width: 170rpx;
height: 96rpx; height: 96rpx;
padding: 0 24rpx; padding: 0 24rpx;
background: linear-gradient(180deg, #16C38B 0%, #10B981 100%); background: linear-gradient(135deg, #10B981, #06B6D4);
border-radius: 999rpx; border-radius: 999rpx;
display: flex; display: flex;
align-items: center; align-items: center;
+310 -71
View File
@@ -1,5 +1,12 @@
<template> <template>
<view class="page"> <view class="page">
<view class="page-head">
<view>
<text class="page-title">日历详情</text>
<text class="page-subtitle">按天查看吸烟数量和记录内容</text>
</view>
<view class="today-pill" @tap="goToday">今天</view>
</view>
<view class="card"> <view class="card">
<view class="month-bar"> <view class="month-bar">
@@ -20,17 +27,17 @@
</view> </view>
</view> </view>
<view class="summary-chip summary-chip-soft"> <view class="summary-chip summary-chip-soft">
<text class="summary-chip-label">本月忍住</text> <text class="summary-chip-label">日均支数</text>
<view class="summary-chip-value-row"> <view class="summary-chip-value-row">
<text class="summary-chip-value">{{ monthResistedTotal }}</text> <text class="summary-chip-value">{{ averageDailySmoke }}</text>
<text class="summary-chip-unit"></text> <text class="summary-chip-unit"></text>
</view> </view>
</view> </view>
<view class="summary-chip summary-chip-soft"> <view class="summary-chip summary-chip-soft">
<text class="summary-chip-label">记录天数</text> <text class="summary-chip-label">最高单日</text>
<view class="summary-chip-value-row"> <view class="summary-chip-value-row">
<text class="summary-chip-value">{{ activeDayCount }}</text> <text class="summary-chip-value">{{ peakDaySmoke }}</text>
<text class="summary-chip-unit"></text> <text class="summary-chip-unit"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -41,8 +48,8 @@
<text class="legend-text">已抽</text> <text class="legend-text">已抽</text>
</view> </view>
<view class="legend-item"> <view class="legend-item">
<view class="legend-dot legend-dot-resisted"></view> <view class="legend-dot legend-dot-selected"></view>
<text class="legend-text">忍住</text> <text class="legend-text">选中日期</text>
</view> </view>
<text class="legend-tip">点击灰色日期可切换到对应月份</text> <text class="legend-tip">点击灰色日期可切换到对应月份</text>
</view> </view>
@@ -76,6 +83,36 @@
</view> </view>
</view> </view>
<view class="detail-card">
<view class="detail-head">
<view>
<text class="detail-title">{{ selectedDateLabel }}</text>
<text class="detail-subtitle">{{ selectedDaySummary }}</text>
</view>
<view class="detail-total">
<text class="detail-total-value">{{ selectedSmokeCount }}</text>
<text class="detail-total-unit"></text>
</view>
</view>
<view v-if="selectedDayLogs.length > 0" class="log-list">
<view v-for="item in selectedDayLogs" :key="item.id || item.smoke_at || item.createtime" class="log-item">
<view class="log-time">{{ formatLogTime(item) }}</view>
<view class="log-main">
<view class="log-line">
<text class="log-count">{{ Number(item.num) || 0 }} </text>
<text v-if="item.scene || item.reason" class="log-tag">{{ item.scene || item.reason }}</text>
</view>
<text v-if="item.remark" class="log-remark">{{ item.remark }}</text>
</view>
</view>
</view>
<view v-else class="empty-detail">
<text class="empty-title">这天还没有吸烟记录</text>
<text class="empty-text">保持记录后日历会更准确地显示少抽趋势</text>
</view>
</view>
<view class="bottom-safe"></view> <view class="bottom-safe"></view>
</view> </view>
</template> </template>
@@ -118,7 +155,7 @@ const calendarDays = computed(() => {
for (let date = new Date(gridStart); date <= gridEnd; date = addDays(date, 1)) { for (let date = new Date(gridStart); date <= gridEnd; date = addDays(date, 1)) {
const dateKey = formatDate(date) const dateKey = formatDate(date)
const summary = summaryMap.get(dateKey) || { smokeCount: 0, resistedCount: 0 } const summary = summaryMap.get(dateKey) || { smokeCount: 0 }
result.push({ result.push({
key: dateKey, key: dateKey,
date: dateKey, date: dateKey,
@@ -126,8 +163,7 @@ const calendarDays = computed(() => {
isCurrentMonth: date.getMonth() === monthStart.getMonth(), isCurrentMonth: date.getMonth() === monthStart.getMonth(),
isToday: dateKey === todayText, isToday: dateKey === todayText,
isFuture: dateKey > todayText, isFuture: dateKey > todayText,
smokeCount: summary.smokeCount, smokeCount: summary.smokeCount
resistedCount: summary.resistedCount
}) })
} }
@@ -135,19 +171,45 @@ const calendarDays = computed(() => {
}) })
const monthSmokeTotal = computed(() => { const monthSmokeTotal = computed(() => {
return monthLogs.value.reduce((total, item) => { return monthLogs.value.reduce((total, item) => total + (Number(item.num) || 0), 0)
return normalizeLogType(item) === 'resisted' ? total : total + (Number(item.num) || 0)
}, 0)
})
const monthResistedTotal = computed(() => {
return monthLogs.value.reduce((total, item) => {
return normalizeLogType(item) === 'resisted' ? total + 1 : total
}, 0)
}) })
const activeDayCount = computed(() => monthSummaryMap.value.size) const activeDayCount = computed(() => monthSummaryMap.value.size)
const averageDailySmoke = computed(() => {
if (activeDayCount.value <= 0) return '0.0'
return (monthSmokeTotal.value / activeDayCount.value).toFixed(1)
})
const peakDaySmoke = computed(() => {
let max = 0
monthSummaryMap.value.forEach(item => {
if (item.smokeCount > max) max = item.smokeCount
})
return max
})
const selectedDayLogs = computed(() => {
return monthLogs.value
.filter(item => resolveLogDate(item) === selectedDate.value)
.sort((a, b) => getLogTimestamp(b) - getLogTimestamp(a))
})
const selectedSmokeCount = computed(() => {
return selectedDayLogs.value.reduce((total, item) => total + (Number(item.num) || 0), 0)
})
const selectedDateLabel = computed(() => {
const date = parseDate(selectedDate.value)
if (!date) return selectedDate.value
return `${date.getMonth() + 1}${date.getDate()}日 周${weekdayNames[date.getDay()]}`
})
const selectedDaySummary = computed(() => {
if (selectedDayLogs.value.length === 0) return '无记录'
return `${selectedDayLogs.value.length} 条记录 · 本月已记录 ${activeDayCount.value}`
})
async function fetchMonthLogs() { async function fetchMonthLogs() {
const start = formatDate(startOfMonth(currentMonth.value)) const start = formatDate(startOfMonth(currentMonth.value))
const end = formatDate(endOfMonth(currentMonth.value)) const end = formatDate(endOfMonth(currentMonth.value))
@@ -156,7 +218,7 @@ async function fetchMonthLogs() {
const res = await api.getLogs({ const res = await api.getLogs({
page: 1, page: 1,
page_size: 200, page_size: 200,
type: 'all', type: 'smoke',
start, start,
end end
}) })
@@ -185,6 +247,10 @@ async function changeMonth(offset) {
await fetchMonthLogs() await fetchMonthLogs()
} }
async function goToday() {
await initPage(todayText)
}
async function selectDay(item) { async function selectDay(item) {
if (item.isFuture) return if (item.isFuture) return
if (!item.isCurrentMonth) { if (!item.isCurrentMonth) {
@@ -205,12 +271,8 @@ function buildDailySummaryMap(logs) {
logs.forEach(log => { logs.forEach(log => {
const dateKey = resolveLogDate(log) const dateKey = resolveLogDate(log)
if (!dateKey) return if (!dateKey) return
const current = map.get(dateKey) || { smokeCount: 0, resistedCount: 0 } const current = map.get(dateKey) || { smokeCount: 0 }
if (normalizeLogType(log) === 'resisted') { current.smokeCount += Number(log.num) || 0
current.resistedCount += 1
} else {
current.smokeCount += Number(log.num) || 0
}
map.set(dateKey, current) map.set(dateKey, current)
}) })
return map return map
@@ -229,21 +291,6 @@ function resolveLogDate(log) {
return '' return ''
} }
function normalizeLogType(log) {
const rawType = log?.type
if (typeof rawType === 'string') {
const value = rawType.toLowerCase()
if (value === 'resisted' || value === 'resist') return 'resisted'
if (value === 'smoke' || value === 'log_smoke') return 'smoke'
}
if (typeof rawType === 'number') {
if (rawType === 0) return 'resisted'
if (rawType === 1) return 'smoke'
}
if (log?.num === 0) return 'resisted'
return 'smoke'
}
function parseDate(value) { function parseDate(value) {
if (!value) return null if (!value) return null
const date = new Date(`${value}T00:00:00`) const date = new Date(`${value}T00:00:00`)
@@ -283,22 +330,79 @@ function addDays(date, offset) {
result.setDate(result.getDate() + offset) result.setDate(result.getDate() + offset)
return result return result
} }
function getLogTimestamp(log) {
if (log?.smoke_at) return new Date(log.smoke_at).getTime() || 0
if (typeof log?.smoke_time === 'string') return new Date(log.smoke_time).getTime() || 0
if (log?.createtime) {
return typeof log.createtime === 'number'
? log.createtime * 1000
: new Date(log.createtime).getTime() || 0
}
return 0
}
function formatLogTime(log) {
const timestamp = getLogTimestamp(log)
if (!timestamp) return '--:--'
const date = new Date(timestamp)
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${hour}:${minute}`
}
</script> </script>
<style scoped> <style scoped>
.page { .page {
min-height: 100vh; min-height: 100vh;
background-color: #F5F7F6; background: linear-gradient(180deg, #E6F7F2 0%, #F4FBF8 42%, #FBFFFD 100%);
padding: 24rpx 32rpx; padding: 28rpx 28rpx 0;
box-sizing: border-box; box-sizing: border-box;
} }
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 24rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: 800;
line-height: 1.15;
color: #0D3D2E;
}
.page-subtitle {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.5;
color: #5D8F7C;
}
.today-pill {
flex-shrink: 0;
padding: 12rpx 20rpx;
border-radius: 999rpx;
background: rgba(255, 255, 255, 0.82);
border: 1.5rpx solid rgba(52, 200, 160, 0.16);
color: #158461;
font-size: 23rpx;
font-weight: 700;
box-shadow: 0 6rpx 18rpx rgba(52, 200, 160, 0.08);
}
.card { .card {
background: #FFFFFF; background: rgba(255, 255, 255, 0.9);
border-radius: 32rpx; border-radius: 24rpx;
padding: 28rpx 24rpx; padding: 24rpx;
margin-bottom: 16rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03); border: 1.5rpx solid rgba(52, 200, 160, 0.14);
box-shadow: 0 6rpx 22rpx rgba(52, 200, 160, 0.08);
box-sizing: border-box; box-sizing: border-box;
} }
@@ -313,13 +417,14 @@ function addDays(date, offset) {
.month-arrow { .month-arrow {
width: 56rpx; width: 56rpx;
height: 56rpx; height: 56rpx;
border-radius: 50%; border-radius: 18rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #F5F7F6; background: rgba(52, 200, 160, 0.08);
font-size: 36rpx; font-size: 36rpx;
color: #1A1A1A; color: #0D3D2E;
font-weight: 700;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -336,14 +441,14 @@ function addDays(date, offset) {
display: block; display: block;
font-size: 32rpx; font-size: 32rpx;
font-weight: 700; font-weight: 700;
color: #1A1A1A; color: #0D3D2E;
} }
.month-subtitle { .month-subtitle {
display: block; display: block;
margin-top: 4rpx; margin-top: 4rpx;
font-size: 22rpx; font-size: 22rpx;
color: #999999; color: #7AA898;
} }
.summary-row { .summary-row {
@@ -356,17 +461,19 @@ function addDays(date, offset) {
flex: 1; flex: 1;
padding: 16rpx 14rpx; padding: 16rpx 14rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #F5F7F6; background: rgba(52, 200, 160, 0.1);
border: 1.5rpx solid rgba(52, 200, 160, 0.12);
} }
.summary-chip-soft { .summary-chip-soft {
background: #F5F7F6; background: rgba(59, 130, 246, 0.06);
border-color: rgba(59, 130, 246, 0.1);
} }
.summary-chip-label { .summary-chip-label {
display: block; display: block;
font-size: 20rpx; font-size: 20rpx;
color: #999999; color: #6B9B8A;
} }
.summary-chip-value-row { .summary-chip-value-row {
@@ -380,12 +487,12 @@ function addDays(date, offset) {
font-size: 36rpx; font-size: 36rpx;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
color: #1A1A1A; color: #0D3D2E;
} }
.summary-chip-unit { .summary-chip-unit {
font-size: 20rpx; font-size: 20rpx;
color: #999999; color: #7AA898;
} }
.calendar-legend { .calendar-legend {
@@ -396,7 +503,7 @@ function addDays(date, offset) {
margin-bottom: 20rpx; margin-bottom: 20rpx;
padding: 14rpx 16rpx; padding: 14rpx 16rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #F9FBFA; background: rgba(52, 200, 160, 0.06);
} }
.legend-item { .legend-item {
@@ -415,19 +522,19 @@ function addDays(date, offset) {
background: #10B981; background: #10B981;
} }
.legend-dot-resisted { .legend-dot-selected {
background: #A7F3D0; background: #F59E0B;
} }
.legend-text { .legend-text {
font-size: 22rpx; font-size: 22rpx;
color: #666666; color: #52806E;
} }
.legend-tip { .legend-tip {
margin-left: auto; margin-left: auto;
font-size: 20rpx; font-size: 20rpx;
color: #CCCCCC; color: #9ABDAE;
} }
.weekday-row { .weekday-row {
@@ -440,7 +547,7 @@ function addDays(date, offset) {
text-align: center; text-align: center;
font-size: 22rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #999999; color: #7AA898;
} }
.calendar-grid { .calendar-grid {
@@ -453,20 +560,21 @@ function addDays(date, offset) {
aspect-ratio: 1 / 1.15; aspect-ratio: 1 / 1.15;
padding: 10rpx 6rpx 8rpx; padding: 10rpx 6rpx 8rpx;
border-radius: 16rpx; border-radius: 16rpx;
background: #F9FBFA; background: rgba(52, 200, 160, 0.05);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
box-sizing: border-box; box-sizing: border-box;
border: 1.5rpx solid transparent;
} }
.calendar-cell-selected { .calendar-cell-selected {
background: #E8F5F0; background: rgba(255, 251, 235, 0.9);
border: 2rpx solid #10B981; border-color: rgba(245, 158, 11, 0.55);
} }
.calendar-cell-today { .calendar-cell-today {
background: #E8F5F0; background: rgba(52, 200, 160, 0.12);
} }
.calendar-cell-muted { .calendar-cell-muted {
@@ -486,7 +594,7 @@ function addDays(date, offset) {
.calendar-day { .calendar-day {
font-size: 22rpx; font-size: 22rpx;
font-weight: 700; font-weight: 700;
color: #1A1A1A; color: #0D3D2E;
} }
.calendar-today-dot { .calendar-today-dot {
@@ -512,7 +620,138 @@ function addDays(date, offset) {
.calendar-count-unit { .calendar-count-unit {
font-size: 16rpx; font-size: 16rpx;
font-weight: 600; font-weight: 600;
color: #999999; color: #7AA898;
}
.detail-card {
background: rgba(255, 255, 255, 0.9);
border-radius: 24rpx;
padding: 24rpx;
border: 1.5rpx solid rgba(52, 200, 160, 0.14);
box-shadow: 0 6rpx 22rpx rgba(52, 200, 160, 0.08);
}
.detail-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-bottom: 20rpx;
}
.detail-title {
display: block;
font-size: 30rpx;
font-weight: 800;
color: #0D3D2E;
}
.detail-subtitle {
display: block;
margin-top: 6rpx;
font-size: 22rpx;
color: #6B9B8A;
}
.detail-total {
flex-shrink: 0;
min-width: 112rpx;
padding: 12rpx 16rpx;
border-radius: 18rpx;
background: rgba(52, 200, 160, 0.1);
text-align: center;
}
.detail-total-value {
font-size: 34rpx;
font-weight: 800;
line-height: 1;
color: #0D3D2E;
}
.detail-total-unit {
margin-left: 4rpx;
font-size: 20rpx;
color: #6B9B8A;
}
.log-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.log-item {
display: flex;
gap: 16rpx;
align-items: flex-start;
padding: 16rpx;
border-radius: 18rpx;
background: rgba(52, 200, 160, 0.05);
}
.log-time {
width: 78rpx;
flex-shrink: 0;
font-size: 24rpx;
font-weight: 800;
color: #158461;
}
.log-main {
flex: 1;
min-width: 0;
}
.log-line {
display: flex;
align-items: center;
gap: 10rpx;
flex-wrap: wrap;
}
.log-count {
font-size: 26rpx;
font-weight: 800;
color: #0D3D2E;
}
.log-tag {
padding: 4rpx 10rpx;
border-radius: 999rpx;
background: rgba(59, 130, 246, 0.08);
color: #2563EB;
font-size: 20rpx;
font-weight: 700;
}
.log-remark {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.5;
color: #6B9B8A;
word-break: break-all;
}
.empty-detail {
padding: 28rpx;
border-radius: 20rpx;
background: rgba(52, 200, 160, 0.05);
}
.empty-title {
display: block;
font-size: 25rpx;
font-weight: 800;
color: #0D3D2E;
margin-bottom: 8rpx;
}
.empty-text {
font-size: 23rpx;
line-height: 1.5;
color: #6B9B8A;
} }
.bottom-safe { .bottom-safe {
+60 -30
View File
@@ -2,6 +2,17 @@
<view class="page"> <view class="page">
<view class="status-bar" :style="{ height: navBarHeight + 'px' }"></view> <view class="status-bar" :style="{ height: navBarHeight + 'px' }"></view>
<view class="page-head">
<view>
<text class="page-title">数据分析</text>
<text class="page-subtitle">少抽趋势节省金额和健康恢复进度</text>
</view>
<view class="head-chip" :class="statusChipClass">
<text class="head-chip-arrow" :class="statusIconClass">{{ statusArrow }}</text>
<text>{{ statusText }}</text>
</view>
</view>
<!-- Tab 切换 --> <!-- Tab 切换 -->
<view class="segment-wrap"> <view class="segment-wrap">
<view class="segment"> <view class="segment">
@@ -165,10 +176,10 @@
</view> </view>
</view> </view>
<view class="mini-card"> <view class="mini-card">
<text class="mini-label">已拒绝</text> <text class="mini-label">累计少抽</text>
<view class="mini-value-row"> <view class="mini-value-row">
<text class="mini-value">{{ resistedTotal }}</text> <text class="mini-value">{{ reducedTotal }}</text>
<text class="mini-unit"></text> <text class="mini-unit"></text>
</view> </view>
</view> </view>
</view> </view>
@@ -226,13 +237,6 @@ const insightText = computed(() => {
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 weeklyTrendRangeText = computed(() => { const weeklyTrendRangeText = computed(() => {
const start = weeklyStatsData.value?.start const start = weeklyStatsData.value?.start
const end = weeklyStatsData.value?.end const end = weeklyStatsData.value?.end
@@ -431,7 +435,7 @@ const healthItems = computed(() => {
}) })
const streakDays = computed(() => statsData.value?.streak_days ?? 0) const streakDays = computed(() => statsData.value?.streak_days ?? 0)
const resistedTotal = computed(() => statsData.value?.resisted_total ?? 0) const reducedTotal = computed(() => Math.max(moneyExpectedTotal.value - moneyActualTotal.value, 0))
function formatRangeText(start, end) { function formatRangeText(start, end) {
const startParts = start.split('-') const startParts = start.split('-')
@@ -514,18 +518,59 @@ onShareAppMessage(() => {
background: transparent; background: transparent;
} }
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20rpx;
margin: 10rpx 0 24rpx;
}
.page-title {
display: block;
font-size: 44rpx;
font-weight: 800;
line-height: 1.15;
color: #0D3D2E;
}
.page-subtitle {
display: block;
margin-top: 8rpx;
font-size: 23rpx;
line-height: 1.5;
color: #5D8F7C;
}
.head-chip {
display: inline-flex;
align-items: center;
gap: 6rpx;
flex-shrink: 0;
padding: 10rpx 16rpx;
border-radius: 999rpx;
font-size: 21rpx;
font-weight: 700;
white-space: nowrap;
border: 1.5rpx solid rgba(52, 200, 160, 0.12);
background: rgba(255, 255, 255, 0.76);
}
.head-chip-arrow {
font-size: 20rpx;
font-weight: 800;
}
/* ── Tab 切换 ── */ /* ── Tab 切换 ── */
.segment-wrap { .segment-wrap {
position: relative; position: relative;
height: 148rpx; height: 112rpx;
flex-shrink: 0; flex-shrink: 0;
z-index: 20; z-index: 20;
} }
.segment { .segment {
position: fixed; position: relative;
left: 28rpx;
right: 28rpx;
z-index: 50; z-index: 50;
display: flex; display: flex;
background: rgba(255, 255, 255, 0.82); background: rgba(255, 255, 255, 0.82);
@@ -667,17 +712,6 @@ onShareAppMessage(() => {
display: block; display: block;
} }
/* ── 状态标签 ── */
.status-chip {
display: flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 14rpx;
border-radius: 999rpx;
font-size: 21rpx;
font-weight: 600;
}
.chip-good { .chip-good {
background: rgba(52, 200, 160, 0.12); background: rgba(52, 200, 160, 0.12);
color: #1a8c62; color: #1a8c62;
@@ -702,10 +736,6 @@ onShareAppMessage(() => {
.arrow-warn { color: #D97706; } .arrow-warn { color: #D97706; }
.arrow-neutral { color: #7aA898; } .arrow-neutral { color: #7aA898; }
.status-text {
font-size: 21rpx;
}
/* ── 日均 ── */ /* ── 日均 ── */
.avg-row { .avg-row {
display: flex; display: flex;