feat: enhance logs and stats UI with new headers, summaries, and improved data display
This commit is contained in:
+229
-58
@@ -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"
|
||||||
>
|
>
|
||||||
|
<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-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;
|
||||||
|
|||||||
@@ -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.resistedCount += 1
|
|
||||||
} else {
|
|
||||||
current.smokeCount += Number(log.num) || 0
|
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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user