diff --git a/src/pages/logs/index.vue b/src/pages/logs/index.vue
index d72be7c..f60b813 100644
--- a/src/pages/logs/index.vue
+++ b/src/pages/logs/index.vue
@@ -1,5 +1,16 @@
+
+
+ 记录时间线
+ 按时间回看真实抽烟节奏
+
+
+ +
+ 记录
+
+
+
@@ -24,7 +35,27 @@
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
- 时间记录
+
+
+ 今日已抽
+ {{ todaySmokeCount }}
+ 根
+
+
+ 当前列表
+ {{ filteredLogs.length }}
+ 条
+
+
+ 最近记录
+ {{ latestDisplayTime }}
+
+
+
+
+ 时间记录
+ {{ currentTab === 'smoke' ? '仅看抽烟记录' : '按日期倒序' }}
+
@@ -56,12 +87,12 @@
{{ log.displayTime || '--:--' }}
- {{ log.type === 'resisted' ? '已忍住' : '已抽烟' }}
+ {{ log.type === 'resisted' ? '旧记录' : '已抽烟' }}
{{ log.num !== undefined && log.num !== null ? log.num : 0 }}根
- 👍
+ 0根
@@ -103,7 +134,7 @@
记
今天还没有记录
- 点击右下角悬浮按钮,快速记录抽烟或忍住的时刻,时间线会从这里开始。
+ 完成一次抽烟记录后,时间线会从这里开始生成。
立即记录
@@ -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;
diff --git a/src/pages/stats-calendar/index.vue b/src/pages/stats-calendar/index.vue
index 6564376..123e3c0 100644
--- a/src/pages/stats-calendar/index.vue
+++ b/src/pages/stats-calendar/index.vue
@@ -1,5 +1,12 @@
+
+
+ 日历详情
+ 按天查看吸烟数量和记录内容
+
+ 今天
+
@@ -20,17 +27,17 @@
- 本月忍住
+ 日均支数
- {{ monthResistedTotal }}
- 次
+ {{ averageDailySmoke }}
+ 支
- 记录天数
+ 最高单日
- {{ activeDayCount }}
- 天
+ {{ peakDaySmoke }}
+ 支
@@ -41,8 +48,8 @@
已抽
-
- 忍住
+
+ 选中日期
点击灰色日期可切换到对应月份
@@ -76,6 +83,36 @@
+
+
+
+ {{ selectedDateLabel }}
+ {{ selectedDaySummary }}
+
+
+ {{ selectedSmokeCount }}
+ 支
+
+
+
+
+
+ {{ formatLogTime(item) }}
+
+
+ {{ Number(item.num) || 0 }} 支
+ {{ item.scene || item.reason }}
+
+
+
+
+
+
+ 这天还没有吸烟记录
+ 保持记录后,日历会更准确地显示少抽趋势。
+
+
+
@@ -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}`
+}