Improve ledger monthly header

This commit is contained in:
openclaw 2026-07-23 00:17:49 +08:00
parent 7747323588
commit 8627d8404a
2 changed files with 102 additions and 47 deletions

View File

@ -73,7 +73,7 @@ input:focus-visible {
.ledger-header { .ledger-header {
position: relative; position: relative;
z-index: 1; z-index: 1;
min-height: 210px; min-height: 180px;
padding: max(18px, env(safe-area-inset-top)) 18px 20px; padding: max(18px, env(safe-area-inset-top)) 18px 20px;
background: #087f72; background: #087f72;
color: #ffffff; color: #ffffff;
@ -211,8 +211,7 @@ input:focus-visible {
background: #edf7f4; background: #edf7f4;
} }
.ledger-switcher, .ledger-switcher {
.month-switcher {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -228,19 +227,12 @@ input:focus-visible {
font-weight: 780; font-weight: 780;
} }
.month-switcher {
margin-top: 22px;
padding: 0;
color: rgba(255, 255, 255, 0.82);
font-size: 13px;
}
.monthly-summary { .monthly-summary {
display: grid; display: grid;
grid-template-columns: 1.35fr 1fr 1fr; grid-template-columns: minmax(0, 1fr) minmax(132px, .72fr);
align-items: end; align-items: center;
gap: 12px; gap: 18px;
margin-top: 9px; margin-top: 22px;
} }
.summary-main, .summary-main,
@ -255,11 +247,14 @@ input:focus-visible {
} }
.monthly-summary span { .monthly-summary span {
margin-bottom: 3px;
color: rgba(255, 255, 255, 0.72); color: rgba(255, 255, 255, 0.72);
font-size: 12px; font-size: 12px;
} }
.summary-main span {
margin-bottom: 4px;
}
.summary-main strong { .summary-main strong {
overflow: hidden; overflow: hidden;
font-size: 23px; font-size: 23px;
@ -267,17 +262,38 @@ input:focus-visible {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.summary-stat { .summary-main strong.compact {
font-size: 17px;
}
.summary-secondary {
min-width: 0;
border-left: 1px solid rgba(255, 255, 255, 0.22); border-left: 1px solid rgba(255, 255, 255, 0.22);
padding-left: 12px; padding-left: 12px;
} }
.summary-stat {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 6px;
}
.summary-stat + .summary-stat {
margin-top: 8px;
}
.summary-stat strong { .summary-stat strong {
overflow: hidden; overflow: hidden;
font-size: 15px; font-size: 14px;
text-align: right;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.summary-stat strong.compact {
font-size: 10px;
}
.income-stat strong { .income-stat strong {
color: #d9ffe7; color: #d9ffe7;
} }
@ -287,8 +303,8 @@ input:focus-visible {
} }
.ledger-content { .ledger-content {
height: calc(100vh - 210px); height: calc(100vh - 180px);
height: calc(100dvh - 210px); height: calc(100dvh - 180px);
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
padding: 0 16px 112px; padding: 0 16px 112px;
@ -346,10 +362,29 @@ input:focus-visible {
.list-toolbar-heading { .list-toolbar-heading {
display: flex; display: flex;
align-items: baseline; align-items: center;
gap: 8px; gap: 8px;
} }
.list-month-switcher {
min-height: 32px;
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
padding: 0;
background: transparent;
color: #26342f;
}
.native-month-input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.list-toolbar strong { .list-toolbar strong {
font-size: 15px; font-size: 15px;
} }
@ -1368,14 +1403,15 @@ input:focus-visible {
} }
.monthly-summary { .monthly-summary {
gap: 8px; grid-template-columns: minmax(0, 1fr) minmax(126px, .72fr);
gap: 10px;
} }
.summary-main strong { .summary-main strong {
font-size: 20px; font-size: 20px;
} }
.summary-stat { .summary-secondary {
padding-left: 8px; padding-left: 8px;
} }

View File

@ -35,15 +35,23 @@ const generatingInvitation = ref(false);
const visibleCount = ref(ENTRY_PAGE_SIZE); const visibleCount = ref(ENTRY_PAGE_SIZE);
const ledgerContent = ref<HTMLElement | null>(null); const ledgerContent = ref<HTMLElement | null>(null);
const loadMoreTrigger = ref<HTMLElement | null>(null); const loadMoreTrigger = ref<HTMLElement | null>(null);
const monthInput = ref<HTMLInputElement | null>(null);
const selectedMonth = ref(toMonthValue(new Date()));
let loadMoreObserver: IntersectionObserver | null = null; let loadMoreObserver: IntersectionObserver | null = null;
const selectedMonthParts = computed(() => {
const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value);
if (!match) return null;
return { year: Number(match[1]), month: Number(match[2]) - 1 };
});
const monthlyEntries = computed(() => { const monthlyEntries = computed(() => {
const current = new Date(); const selected = selectedMonthParts.value;
if (!selected) return [];
return store.entries.filter((entry) => { return store.entries.filter((entry) => {
const occurredAt = new Date(entry.occurredAt); const occurredAt = new Date(entry.occurredAt);
return entry.ledgerIds.includes(ledgerStore.currentLedgerId) return entry.ledgerIds.includes(ledgerStore.currentLedgerId)
&& occurredAt.getFullYear() === current.getFullYear() && occurredAt.getFullYear() === selected.year
&& occurredAt.getMonth() === current.getMonth(); && occurredAt.getMonth() === selected.month;
}); });
}); });
@ -57,6 +65,9 @@ const totals = computed(() => {
{ income: 0, expense: 0 }, { income: 0, expense: 0 },
); );
}); });
const balanceText = computed(() => formatMoney(totals.value.income - totals.value.expense));
const incomeText = computed(() => formatMoney(totals.value.income));
const expenseText = computed(() => formatMoney(totals.value.expense));
const visibleMonthlyEntries = computed(() => monthlyEntries.value.slice(0, visibleCount.value)); const visibleMonthlyEntries = computed(() => monthlyEntries.value.slice(0, visibleCount.value));
const hasMoreEntries = computed(() => visibleCount.value < monthlyEntries.value.length); const hasMoreEntries = computed(() => visibleCount.value < monthlyEntries.value.length);
@ -91,12 +102,8 @@ const groupedEntries = computed(() => {
}); });
const monthTitle = computed(() => { const monthTitle = computed(() => {
const date = new Date(); const selected = selectedMonthParts.value;
return `${date.getFullYear()}${date.getMonth() + 1}`; return selected ? `${selected.year}${selected.month + 1}` : "选择月份";
});
const monthListTitle = computed(() => {
const date = new Date();
return `${date.getFullYear()}${date.getMonth() + 1}月流水`;
}); });
onMounted(async () => { onMounted(async () => {
@ -129,6 +136,17 @@ function localDateKey(value: string) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
} }
function toMonthValue(date: Date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
}
function openMonthPicker() {
const input = monthInput.value;
if (!input) return;
if (typeof input.showPicker === "function") input.showPicker();
else input.click();
}
function dateHeading(value: string) { function dateHeading(value: string) {
const date = new Date(`${value}T00:00:00`); const date = new Date(`${value}T00:00:00`);
const today = new Date(); const today = new Date();
@ -261,23 +279,20 @@ async function shareLedger() {
</div> </div>
</Transition> </Transition>
<button class="month-switcher" type="button"> <section class="monthly-summary" aria-label="月度收支">
{{ monthTitle }}
<ChevronDown :size="14" />
</button>
<section class="monthly-summary" aria-label="本月收支">
<div class="summary-main"> <div class="summary-main">
<span>本月结余</span> <span>结余</span>
<strong>¥ {{ formatMoney(totals.income - totals.expense) }}</strong> <strong :class="{ compact: balanceText.length > 12 }">¥ {{ balanceText }}</strong>
</div> </div>
<div class="summary-stat income-stat"> <div class="summary-secondary">
<span>收入</span> <div class="summary-stat income-stat">
<strong>{{ formatMoney(totals.income) }}</strong> <span>收入</span>
</div> <strong :class="{ compact: incomeText.length > 10 }">{{ incomeText }}</strong>
<div class="summary-stat expense-stat"> </div>
<span>支出</span> <div class="summary-stat expense-stat">
<strong>{{ formatMoney(totals.expense) }}</strong> <span>支出</span>
<strong :class="{ compact: expenseText.length > 10 }">{{ expenseText }}</strong>
</div>
</div> </div>
</section> </section>
</header> </header>
@ -292,7 +307,11 @@ async function shareLedger() {
<div class="list-toolbar"> <div class="list-toolbar">
<div class="list-toolbar-copy"> <div class="list-toolbar-copy">
<div class="list-toolbar-heading"> <div class="list-toolbar-heading">
<strong>{{ monthListTitle }}</strong> <button class="list-month-switcher" type="button" @click="openMonthPicker">
<strong>{{ monthTitle }}</strong>
<ChevronDown :size="14" />
</button>
<input ref="monthInput" v-model="selectedMonth" class="native-month-input" type="month" aria-label="选择流水月份" tabindex="-1" />
<span>{{ monthlyEntries.length }} </span> <span>{{ monthlyEntries.length }} </span>
</div> </div>
<div class="list-toolbar-totals"> <div class="list-toolbar-totals">