feat: enhance logs and stats UI with new headers, summaries, and improved data display
This commit is contained in:
+230
-59
@@ -1,5 +1,16 @@
|
||||
<template>
|
||||
<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">
|
||||
<view class="tabs">
|
||||
@@ -24,7 +35,27 @@
|
||||
@refresherrefresh="onRefresh"
|
||||
@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-for="i in 3" :key="i" class="skeleton-item">
|
||||
@@ -56,12 +87,12 @@
|
||||
<view class="log-time-tag">
|
||||
<text class="log-time">{{ log.displayTime || '--:--' }}</text>
|
||||
<text class="log-tag" :class="log.type === 'resisted' ? 'tag-resisted' : 'tag-smoke'">
|
||||
{{ log.type === 'resisted' ? '已忍住' : '已抽烟' }}
|
||||
{{ log.type === 'resisted' ? '旧记录' : '已抽烟' }}
|
||||
</text>
|
||||
</view>
|
||||
<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-else class="thumb-pill">👍</text>
|
||||
<text v-else class="thumb-pill">0根</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -103,7 +134,7 @@
|
||||
<text class="empty-icon">记</text>
|
||||
</view>
|
||||
<text class="empty-text">今天还没有记录</text>
|
||||
<text class="empty-hint">点击右下角悬浮按钮,快速记录抽烟或忍住的时刻,时间线会从这里开始。</text>
|
||||
<text class="empty-hint">完成一次抽烟记录后,时间线会从这里开始生成。</text>
|
||||
<view class="empty-action" @tap="addLog">立即记录</view>
|
||||
</view>
|
||||
|
||||
@@ -143,8 +174,7 @@ const logsStore = useLogsStore()
|
||||
|
||||
const tabs = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已抽烟', value: 'smoke' },
|
||||
{ label: '已忍住', value: 'resisted' }
|
||||
{ label: '抽烟记录', value: 'smoke' }
|
||||
]
|
||||
|
||||
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 导致日期差一天)
|
||||
function localDateStr(d) {
|
||||
const y = d.getFullYear()
|
||||
@@ -250,7 +294,7 @@ async function handleUpdate(data) {
|
||||
function handleDelete(log) {
|
||||
uni.showModal({
|
||||
title: '确认删除',
|
||||
content: `确定要删除这条${log.type === 'resisted' ? '忍住' : '抽烟'}记录吗?`,
|
||||
content: `确定要删除这条${log.type === 'resisted' ? '旧' : '抽烟'}记录吗?`,
|
||||
confirmColor: '#EF4444',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
@@ -311,14 +355,62 @@ onShareAppMessage(() => {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
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;
|
||||
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 {
|
||||
position: relative;
|
||||
height: 120rpx;
|
||||
height: 96rpx;
|
||||
flex-shrink: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
@@ -328,38 +420,109 @@ onShareAppMessage(() => {
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
z-index: 50;
|
||||
margin: 12rpx 0 0;
|
||||
margin: 8rpx 0 0;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1rpx solid rgba(226, 232, 240, 0.82);
|
||||
border-radius: 22rpx;
|
||||
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 {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
border-radius: 20rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #999999;
|
||||
padding: 15rpx 0;
|
||||
border-radius: 18rpx;
|
||||
font-size: 25rpx;
|
||||
font-weight: 900;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.tab-active {
|
||||
background: #10B981;
|
||||
background: linear-gradient(135deg, #10B981, #06B6D4);
|
||||
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 {
|
||||
display: block;
|
||||
margin: 0 0 18rpx 6rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #666666;
|
||||
font-size: 29rpx;
|
||||
font-weight: 900;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.section-note {
|
||||
font-size: 21rpx;
|
||||
font-weight: 800;
|
||||
color: #64748B;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
@@ -428,27 +591,31 @@ onShareAppMessage(() => {
|
||||
}
|
||||
|
||||
.log-group {
|
||||
margin-bottom: 28rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 14rpx;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
font-weight: 900;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.group-count {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
background-color: #F0F0F0;
|
||||
padding: 6rpx 16rpx;
|
||||
font-size: 21rpx;
|
||||
font-weight: 800;
|
||||
color: #64748B;
|
||||
background-color: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(226, 232, 240, 0.72);
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
@@ -460,10 +627,11 @@ onShareAppMessage(() => {
|
||||
|
||||
.log-card {
|
||||
position: relative;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 24rpx 20rpx 24rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.86);
|
||||
border-radius: 28rpx;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
box-shadow: 0 14rpx 34rpx rgba(30, 41, 59, 0.06);
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
overflow: hidden;
|
||||
@@ -482,18 +650,18 @@ onShareAppMessage(() => {
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8rpx;
|
||||
background-color: #10B981;
|
||||
width: 7rpx;
|
||||
background: linear-gradient(180deg, #67E8F9, #10B981);
|
||||
}
|
||||
|
||||
.log-card-smoke .log-bar {
|
||||
background-color: #F59E0B;
|
||||
background: linear-gradient(180deg, #FBBF24, #D97706);
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -503,12 +671,12 @@ onShareAppMessage(() => {
|
||||
}
|
||||
|
||||
.icon-resisted {
|
||||
background-color: #E8F5F0;
|
||||
color: #10B981;
|
||||
background-color: rgba(232, 245, 240, 0.92);
|
||||
color: #0F766E;
|
||||
}
|
||||
|
||||
.icon-smoke {
|
||||
background-color: #FEF3C7;
|
||||
background: linear-gradient(135deg, rgba(254, 243, 199, 0.96), rgba(255, 251, 235, 0.9));
|
||||
color: #D97706;
|
||||
}
|
||||
|
||||
@@ -532,16 +700,16 @@ onShareAppMessage(() => {
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
font-size: 31rpx;
|
||||
font-weight: 900;
|
||||
color: #1E293B;
|
||||
}
|
||||
|
||||
.log-tag {
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
padding: 7rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-weight: 600;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.tag-smoke {
|
||||
@@ -564,10 +732,10 @@ onShareAppMessage(() => {
|
||||
.count-pill {
|
||||
font-size: 22rpx;
|
||||
color: #D97706;
|
||||
background-color: #FEF3C7;
|
||||
background-color: rgba(254, 243, 199, 0.92);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-weight: 600;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.thumb-pill {
|
||||
@@ -580,9 +748,9 @@ onShareAppMessage(() => {
|
||||
|
||||
.log-desc {
|
||||
font-size: 25rpx;
|
||||
color: #666666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 10rpx;
|
||||
color: #475569;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.reason-tag-row {
|
||||
@@ -612,7 +780,7 @@ onShareAppMessage(() => {
|
||||
|
||||
.level-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.log-interval {
|
||||
@@ -627,12 +795,13 @@ onShareAppMessage(() => {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
margin-top: 12rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
font-size: 22rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
font-weight: 900;
|
||||
padding: 9rpx 17rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
@@ -678,7 +847,9 @@ onShareAppMessage(() => {
|
||||
justify-content: center;
|
||||
padding: 132rpx 32rpx 116rpx;
|
||||
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);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -753,7 +924,7 @@ onShareAppMessage(() => {
|
||||
min-width: 170rpx;
|
||||
height: 96rpx;
|
||||
padding: 0 24rpx;
|
||||
background: linear-gradient(180deg, #16C38B 0%, #10B981 100%);
|
||||
background: linear-gradient(135deg, #10B981, #06B6D4);
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<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="month-bar">
|
||||
@@ -20,17 +27,17 @@
|
||||
</view>
|
||||
</view>
|
||||
<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">
|
||||
<text class="summary-chip-value">{{ monthResistedTotal }}</text>
|
||||
<text class="summary-chip-unit">次</text>
|
||||
<text class="summary-chip-value">{{ averageDailySmoke }}</text>
|
||||
<text class="summary-chip-unit">支</text>
|
||||
</view>
|
||||
</view>
|
||||
<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">
|
||||
<text class="summary-chip-value">{{ activeDayCount }}</text>
|
||||
<text class="summary-chip-unit">天</text>
|
||||
<text class="summary-chip-value">{{ peakDaySmoke }}</text>
|
||||
<text class="summary-chip-unit">支</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -41,8 +48,8 @@
|
||||
<text class="legend-text">已抽</text>
|
||||
</view>
|
||||
<view class="legend-item">
|
||||
<view class="legend-dot legend-dot-resisted"></view>
|
||||
<text class="legend-text">忍住</text>
|
||||
<view class="legend-dot legend-dot-selected"></view>
|
||||
<text class="legend-text">选中日期</text>
|
||||
</view>
|
||||
<text class="legend-tip">点击灰色日期可切换到对应月份</text>
|
||||
</view>
|
||||
@@ -76,6 +83,36 @@
|
||||
</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>
|
||||
</template>
|
||||
@@ -118,7 +155,7 @@ const calendarDays = computed(() => {
|
||||
|
||||
for (let date = new Date(gridStart); date <= gridEnd; date = addDays(date, 1)) {
|
||||
const dateKey = formatDate(date)
|
||||
const summary = summaryMap.get(dateKey) || { smokeCount: 0, resistedCount: 0 }
|
||||
const summary = summaryMap.get(dateKey) || { smokeCount: 0 }
|
||||
result.push({
|
||||
key: dateKey,
|
||||
date: dateKey,
|
||||
@@ -126,8 +163,7 @@ const calendarDays = computed(() => {
|
||||
isCurrentMonth: date.getMonth() === monthStart.getMonth(),
|
||||
isToday: dateKey === todayText,
|
||||
isFuture: dateKey > todayText,
|
||||
smokeCount: summary.smokeCount,
|
||||
resistedCount: summary.resistedCount
|
||||
smokeCount: summary.smokeCount
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,19 +171,45 @@ const calendarDays = computed(() => {
|
||||
})
|
||||
|
||||
const monthSmokeTotal = computed(() => {
|
||||
return monthLogs.value.reduce((total, item) => {
|
||||
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)
|
||||
return monthLogs.value.reduce((total, item) => total + (Number(item.num) || 0), 0)
|
||||
})
|
||||
|
||||
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() {
|
||||
const start = formatDate(startOfMonth(currentMonth.value))
|
||||
const end = formatDate(endOfMonth(currentMonth.value))
|
||||
@@ -156,7 +218,7 @@ async function fetchMonthLogs() {
|
||||
const res = await api.getLogs({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
type: 'all',
|
||||
type: 'smoke',
|
||||
start,
|
||||
end
|
||||
})
|
||||
@@ -185,6 +247,10 @@ async function changeMonth(offset) {
|
||||
await fetchMonthLogs()
|
||||
}
|
||||
|
||||
async function goToday() {
|
||||
await initPage(todayText)
|
||||
}
|
||||
|
||||
async function selectDay(item) {
|
||||
if (item.isFuture) return
|
||||
if (!item.isCurrentMonth) {
|
||||
@@ -205,12 +271,8 @@ function buildDailySummaryMap(logs) {
|
||||
logs.forEach(log => {
|
||||
const dateKey = resolveLogDate(log)
|
||||
if (!dateKey) return
|
||||
const current = map.get(dateKey) || { smokeCount: 0, resistedCount: 0 }
|
||||
if (normalizeLogType(log) === 'resisted') {
|
||||
current.resistedCount += 1
|
||||
} else {
|
||||
current.smokeCount += Number(log.num) || 0
|
||||
}
|
||||
const current = map.get(dateKey) || { smokeCount: 0 }
|
||||
current.smokeCount += Number(log.num) || 0
|
||||
map.set(dateKey, current)
|
||||
})
|
||||
return map
|
||||
@@ -229,21 +291,6 @@ function resolveLogDate(log) {
|
||||
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) {
|
||||
if (!value) return null
|
||||
const date = new Date(`${value}T00:00:00`)
|
||||
@@ -283,22 +330,79 @@ function addDays(date, offset) {
|
||||
result.setDate(result.getDate() + offset)
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background-color: #F5F7F6;
|
||||
padding: 24rpx 32rpx;
|
||||
background: linear-gradient(180deg, #E6F7F2 0%, #F4FBF8 42%, #FBFFFD 100%);
|
||||
padding: 28rpx 28rpx 0;
|
||||
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 {
|
||||
background: #FFFFFF;
|
||||
border-radius: 32rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -313,13 +417,14 @@ function addDays(date, offset) {
|
||||
.month-arrow {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #F5F7F6;
|
||||
background: rgba(52, 200, 160, 0.08);
|
||||
font-size: 36rpx;
|
||||
color: #1A1A1A;
|
||||
color: #0D3D2E;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -336,14 +441,14 @@ function addDays(date, offset) {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
color: #0D3D2E;
|
||||
}
|
||||
|
||||
.month-subtitle {
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
color: #7AA898;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
@@ -356,17 +461,19 @@ function addDays(date, offset) {
|
||||
flex: 1;
|
||||
padding: 16rpx 14rpx;
|
||||
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 {
|
||||
background: #F5F7F6;
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
border-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.summary-chip-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
color: #6B9B8A;
|
||||
}
|
||||
|
||||
.summary-chip-value-row {
|
||||
@@ -380,12 +487,12 @@ function addDays(date, offset) {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
color: #1A1A1A;
|
||||
color: #0D3D2E;
|
||||
}
|
||||
|
||||
.summary-chip-unit {
|
||||
font-size: 20rpx;
|
||||
color: #999999;
|
||||
color: #7AA898;
|
||||
}
|
||||
|
||||
.calendar-legend {
|
||||
@@ -396,7 +503,7 @@ function addDays(date, offset) {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 14rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #F9FBFA;
|
||||
background: rgba(52, 200, 160, 0.06);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@@ -415,19 +522,19 @@ function addDays(date, offset) {
|
||||
background: #10B981;
|
||||
}
|
||||
|
||||
.legend-dot-resisted {
|
||||
background: #A7F3D0;
|
||||
.legend-dot-selected {
|
||||
background: #F59E0B;
|
||||
}
|
||||
|
||||
.legend-text {
|
||||
font-size: 22rpx;
|
||||
color: #666666;
|
||||
color: #52806E;
|
||||
}
|
||||
|
||||
.legend-tip {
|
||||
margin-left: auto;
|
||||
font-size: 20rpx;
|
||||
color: #CCCCCC;
|
||||
color: #9ABDAE;
|
||||
}
|
||||
|
||||
.weekday-row {
|
||||
@@ -440,7 +547,7 @@ function addDays(date, offset) {
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #999999;
|
||||
color: #7AA898;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
@@ -453,20 +560,21 @@ function addDays(date, offset) {
|
||||
aspect-ratio: 1 / 1.15;
|
||||
padding: 10rpx 6rpx 8rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #F9FBFA;
|
||||
background: rgba(52, 200, 160, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
border: 1.5rpx solid transparent;
|
||||
}
|
||||
|
||||
.calendar-cell-selected {
|
||||
background: #E8F5F0;
|
||||
border: 2rpx solid #10B981;
|
||||
background: rgba(255, 251, 235, 0.9);
|
||||
border-color: rgba(245, 158, 11, 0.55);
|
||||
}
|
||||
|
||||
.calendar-cell-today {
|
||||
background: #E8F5F0;
|
||||
background: rgba(52, 200, 160, 0.12);
|
||||
}
|
||||
|
||||
.calendar-cell-muted {
|
||||
@@ -486,7 +594,7 @@ function addDays(date, offset) {
|
||||
.calendar-day {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #1A1A1A;
|
||||
color: #0D3D2E;
|
||||
}
|
||||
|
||||
.calendar-today-dot {
|
||||
@@ -512,7 +620,138 @@ function addDays(date, offset) {
|
||||
.calendar-count-unit {
|
||||
font-size: 16rpx;
|
||||
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 {
|
||||
|
||||
+60
-30
@@ -2,6 +2,17 @@
|
||||
<view class="page">
|
||||
<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 切换 -->
|
||||
<view class="segment-wrap">
|
||||
<view class="segment">
|
||||
@@ -165,10 +176,10 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="mini-card">
|
||||
<text class="mini-label">已拒绝</text>
|
||||
<text class="mini-label">累计少抽</text>
|
||||
<view class="mini-value-row">
|
||||
<text class="mini-value">{{ resistedTotal }}</text>
|
||||
<text class="mini-unit">次</text>
|
||||
<text class="mini-value">{{ reducedTotal }}</text>
|
||||
<text class="mini-unit">支</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -226,13 +237,6 @@ const insightText = computed(() => {
|
||||
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 start = weeklyStatsData.value?.start
|
||||
const end = weeklyStatsData.value?.end
|
||||
@@ -431,7 +435,7 @@ const healthItems = computed(() => {
|
||||
})
|
||||
|
||||
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) {
|
||||
const startParts = start.split('-')
|
||||
@@ -514,18 +518,59 @@ onShareAppMessage(() => {
|
||||
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 切换 ── */
|
||||
.segment-wrap {
|
||||
position: relative;
|
||||
height: 148rpx;
|
||||
height: 112rpx;
|
||||
flex-shrink: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.segment {
|
||||
position: fixed;
|
||||
left: 28rpx;
|
||||
right: 28rpx;
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
@@ -667,17 +712,6 @@ onShareAppMessage(() => {
|
||||
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 {
|
||||
background: rgba(52, 200, 160, 0.12);
|
||||
color: #1a8c62;
|
||||
@@ -702,10 +736,6 @@ onShareAppMessage(() => {
|
||||
.arrow-warn { color: #D97706; }
|
||||
.arrow-neutral { color: #7aA898; }
|
||||
|
||||
.status-text {
|
||||
font-size: 21rpx;
|
||||
}
|
||||
|
||||
/* ── 日均 ── */
|
||||
.avg-row {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user