From 52c6563ac7ac48dfb597772e148f003612480ab8 Mon Sep 17 00:00:00 2001 From: openclaw Date: Sun, 26 Jul 2026 16:17:11 +0800 Subject: [PATCH] fix: keep stats range separate from chart granularity --- apps/web/src/views/StatsView.vue | 77 ++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/apps/web/src/views/StatsView.vue b/apps/web/src/views/StatsView.vue index 27b68be..5f8b87b 100644 --- a/apps/web/src/views/StatsView.vue +++ b/apps/web/src/views/StatsView.vue @@ -175,6 +175,13 @@ const entries = computed(() => { const totals = computed(() => summarize(entries.value)); const convertedEntries = computed(() => entries.value.filter((entry) => entry.baseAmount !== null)); const pendingConversionCount = computed(() => entries.value.length - convertedEntries.value.length); +const calendarCursorMonth = ref(selectedMonth.value); + +watch([rangeMode, selectedMonth, selectedYear, customStart, customEnd], () => { + const bounds = rangeBounds.value; + if (!bounds) return; + calendarCursorMonth.value = `${bounds.start.getFullYear()}-${String(bounds.start.getMonth() + 1).padStart(2, "0")}`; +}); const dailyStats = computed(() => { const groups = new Map(); @@ -208,7 +215,12 @@ const flowStats = computed(() => { const calendarMonthStart = computed(() => { const bounds = rangeBounds.value; - return bounds ? new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1) : null; + if (!bounds) return null; + const match = /^(\d{4})-(\d{2})$/.exec(calendarCursorMonth.value); + const candidate = match ? new Date(Number(match[1]), Number(match[2]) - 1, 1) : bounds.start; + const first = new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1); + const last = new Date(bounds.end.getFullYear(), bounds.end.getMonth(), 1); + return candidate < first ? first : candidate >= last ? new Date(last.getFullYear(), last.getMonth() - 1, 1) : candidate; }); const calendarMonthLabel = computed(() => { @@ -230,6 +242,35 @@ const calendarDays = computed(() => { return cells; }); +const calendarMonthCells = computed(() => { + const bounds = rangeBounds.value; + if (!bounds) return []; + const summary = new Map(flowStats.value.map((item) => [item.date, item])); + const cells: Array<{ date: string; label: string; income: number; expense: number }> = []; + const cursor = new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1); + const end = new Date(bounds.end.getFullYear(), bounds.end.getMonth(), 1); + while (cursor < end) { + const date = `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`; + const item = summary.get(date); + cells.push({ date, label: `${cursor.getFullYear()}年${cursor.getMonth() + 1}月`, income: item?.income ?? 0, expense: item?.expense ?? 0 }); + cursor.setMonth(cursor.getMonth() + 1); + } + return cells; +}); + +const calendarYearCells = computed(() => { + const bounds = rangeBounds.value; + if (!bounds) return []; + const summary = new Map(flowStats.value.map((item) => [item.date, item])); + const cells: Array<{ date: string; label: string; income: number; expense: number }> = []; + for (let year = bounds.start.getFullYear(); year <= new Date(bounds.end.getTime() - 1).getFullYear(); year += 1) { + const date = String(year); + const item = summary.get(date); + cells.push({ date, label: `${year}年`, income: item?.income ?? 0, expense: item?.expense ?? 0 }); + } + return cells; +}); + const lineStats = computed(() => [...flowStats.value].reverse()); const lineMaxAmount = computed(() => Math.max(1, ...lineStats.value.flatMap((day) => [day.income, day.expense]))); @@ -466,11 +507,15 @@ function shiftRangePickerYear(offset: -1 | 1) { } function shiftCalendarMonth(offset: -1 | 1) { - const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value); + const match = /^(\d{4})-(\d{2})$/.exec(calendarCursorMonth.value); if (!match) return; const next = new Date(Number(match[1]), Number(match[2]) - 1 + offset, 1); - selectedMonth.value = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`; - rangeMode.value = "month"; + const bounds = rangeBounds.value; + if (!bounds) return; + const first = new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1); + const last = new Date(bounds.end.getFullYear(), bounds.end.getMonth(), 1); + if (next < first || next >= last) return; + calendarCursorMonth.value = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`; } @@ -560,11 +605,18 @@ function shiftCalendarMonth(offset: -1 | 1) {
收支趋势
-
{{ calendarMonthLabel }}
-
{{ weekday }}
-
- - + +
+ + {{ cell.label }}{{ compactDailyAmount(cell.income || cell.expense) }}
收入支出
@@ -680,6 +732,13 @@ function shiftCalendarMonth(offset: -1 | 1) { .calendar-cell i.income { background:#20a57d; } .calendar-cell i.expense { background:#ef6a4d; } .calendar-cell small { color:#7b8884; font-size:8px; line-height:1; } +.period-calendar-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; } +.period-calendar-cell { min-width:0; min-height:66px; display:grid; align-content:center; justify-items:center; gap:5px; border:1px solid #e6eeec; border-radius:7px; padding:7px 5px; background:#fff; } +.period-calendar-cell b { color:#536762; font-size:11px; font-weight:680; } +.period-calendar-cell i { width:0; max-width:80%; height:4px; display:block; border-radius:3px; } +.period-calendar-cell i.income { background:#20a57d; } +.period-calendar-cell i.expense { background:#ef6a4d; } +.period-calendar-cell small { color:#7b8884; font-size:9px; line-height:1; } .daily-view-legend { display:flex; justify-content:center; gap:15px; margin-top:10px; font-size:10px; } .daily-view-legend span::before { display:inline-block; width:7px; height:7px; margin-right:4px; border-radius:50%; background:currentColor; content:""; } .daily-line-chart { padding:12px; }