From 81590b2d80d0f88e3cf179c6f26ff2fbaef87579 Mon Sep 17 00:00:00 2001 From: openclaw Date: Sat, 25 Jul 2026 23:28:22 +0800 Subject: [PATCH] feat: add ledger month picker modal --- apps/web/src/styles.css | 27 ++++++++ apps/web/src/views/LedgerView.vue | 110 ++++++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 9723c8a..350bfda 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -409,6 +409,33 @@ input:focus-visible { pointer-events: none; } +.month-picker-layer { position:absolute; inset:0; z-index:16; } +.month-picker-scrim { position:absolute; inset:0; width:100%; height:100%; border:0; background:rgba(18,35,32,.4); backdrop-filter:blur(3px); } +.month-picker-modal { position:absolute; right:0; bottom:0; left:0; max-height:82%; overflow-y:auto; border-radius:14px 14px 0 0; padding:0 16px calc(18px + env(safe-area-inset-bottom)); background:#fff; color:#22302c; box-shadow:0 -16px 36px rgba(22,45,40,.22); } +.month-picker-modal > header { min-height:56px; display:grid; grid-template-columns:38px 1fr 38px; align-items:center; border-bottom:1px solid #e1ebe8; } +.month-picker-modal > header strong { text-align:center; font-size:16px; } +.month-picker-modal > header button { width:38px; height:38px; display:grid; place-items:center; border:0; border-radius:8px; background:#f0f5f3; color:#536762; } +.month-picker-year { width:100%; min-height:54px; display:flex; align-items:center; justify-content:center; gap:4px; border:0; background:transparent; color:#26342f; } +.month-picker-year strong { font-size:21px; } +.month-picker-total { display:flex; justify-content:center; gap:18px; margin:-2px 0 14px; font-size:12px; font-variant-numeric:tabular-nums; } +.month-picker-total .income,.month-picker-grid .income,.year-picker-list .income { color:#0d8b67; } +.month-picker-total .expense,.month-picker-grid .expense,.year-picker-list .expense { color:#d84c36; } +.month-picker-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; } +.month-picker-grid button { min-height:68px; display:grid; align-content:center; gap:2px; border:1px solid #e1ebe8; border-radius:8px; padding:7px 5px; background:#fff; text-align:center; } +.month-picker-grid button.selected { border-color:#087f72; background:#eaf7f3; box-shadow:inset 0 0 0 1px #087f72; } +.month-picker-grid button strong { color:#26342f; font-size:14px; } +.month-picker-grid button small { overflow:hidden; color:#7a8783; font-size:10px; line-height:1.2; text-overflow:ellipsis; white-space:nowrap; } +.year-picker-list { display:grid; gap:2px; padding-top:8px; } +.year-picker-list button { min-height:58px; display:flex; align-items:center; justify-content:space-between; border:0; border-bottom:1px solid #e5edeb; padding:0 8px; background:#fff; color:#26342f; text-align:left; } +.year-picker-list button.selected { color:#087f72; } +.year-picker-list button strong { font-size:16px; } +.year-picker-list button span { display:flex; gap:10px; font-size:11px; font-variant-numeric:tabular-nums; } +.year-picker-list button i { font-style:normal; } +.month-picker-enter-active,.month-picker-leave-active { transition:opacity 180ms ease; } +.month-picker-enter-active .month-picker-modal,.month-picker-leave-active .month-picker-modal { transition:transform 220ms ease; } +.month-picker-enter-from,.month-picker-leave-to { opacity:0; } +.month-picker-enter-from .month-picker-modal,.month-picker-leave-to .month-picker-modal { transform:translateY(100%); } + .list-toolbar strong { font-size: 15px; } diff --git a/apps/web/src/views/LedgerView.vue b/apps/web/src/views/LedgerView.vue index 010b6e5..f3d0fd0 100644 --- a/apps/web/src/views/LedgerView.vue +++ b/apps/web/src/views/LedgerView.vue @@ -41,8 +41,10 @@ const generatingInvitation = ref(false); const visibleCount = ref(ENTRY_PAGE_SIZE); const ledgerContent = ref(null); const loadMoreTrigger = ref(null); -const monthInput = ref(null); const selectedMonth = ref(toMonthValue(new Date())); +const monthPickerOpen = ref(false); +const monthPickerMode = ref<"months" | "years">("months"); +const monthPickerYear = ref(new Date().getFullYear()); const monthSwipeStart = ref<{ x: number; y: number } | null>(null); const monthSwipeDelta = ref(0); const monthSwipeDragging = ref(false); @@ -120,6 +122,44 @@ const groupedEntries = computed(() => { })); }); +const ledgerEntries = computed(() => store.entries.filter((entry) => entry.ledgerIds.includes(ledgerStore.currentLedgerId))); + +function summarizeEntries(entries: LedgerEntry[]) { + return entries.reduce( + (result, entry) => { + if (entry.baseAmount === null) return result; + if (entry.type === "income") result.income += entry.baseAmount; + else result.expense += entry.baseAmount; + return result; + }, + { income: 0, expense: 0 }, + ); +} + +const availableYears = computed(() => { + const years = new Set([new Date().getFullYear(), ...ledgerEntries.value.map((entry) => new Date(entry.occurredAt).getFullYear())]); + return [...years].sort((left, right) => right - left); +}); + +const yearSummaries = computed(() => availableYears.value.map((year) => ({ + year, + ...summarizeEntries(ledgerEntries.value.filter((entry) => new Date(entry.occurredAt).getFullYear() === year)), +}))); + +const monthSummaries = computed(() => Array.from({ length: 12 }, (_, month) => ({ + month, + ...summarizeEntries(ledgerEntries.value.filter((entry) => { + const date = new Date(entry.occurredAt); + return date.getFullYear() === monthPickerYear.value && date.getMonth() === month; + })), +}))); + +const pickerYearSummary = computed(() => yearSummaries.value.find((summary) => summary.year === monthPickerYear.value) ?? { + year: monthPickerYear.value, + income: 0, + expense: 0, +}); + const monthTitle = computed(() => { const selected = selectedMonthParts.value; return selected ? `${selected.year}年${selected.month + 1}月` : "选择月份"; @@ -180,10 +220,23 @@ function toMonthValue(date: Date) { } function openMonthPicker() { - const input = monthInput.value; - if (!input) return; - if (typeof input.showPicker === "function") input.showPicker(); - else input.click(); + monthPickerYear.value = selectedMonthParts.value?.year ?? new Date().getFullYear(); + monthPickerMode.value = "months"; + monthPickerOpen.value = true; +} + +function closeMonthPicker() { + monthPickerOpen.value = false; +} + +function selectPickerYear(year: number) { + monthPickerYear.value = year; + monthPickerMode.value = "months"; +} + +function selectPickerMonth(month: number) { + selectedMonth.value = `${monthPickerYear.value}-${String(month + 1).padStart(2, "0")}`; + closeMonthPicker(); } function startMonthSwipe(event: TouchEvent) { @@ -336,6 +389,13 @@ function formatMoney(value: number) { }); } +function formatCompactMoney(value: number) { + const amount = value / 100; + if (amount >= 10000) return `${(amount / 10000).toFixed(1)}万`; + if (amount >= 1000) return `${(amount / 1000).toFixed(1)}k`; + return amount.toLocaleString("zh-CN", { maximumFractionDigits: 0 }); +} + function formatTime(value: string) { return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(value)); } @@ -451,6 +511,45 @@ async function shareLedger() { + +
+ + +
+
+
结余 @@ -497,7 +596,6 @@ async function shareLedger() { {{ monthTitle }} - {{ monthlyEntries.length }} 笔