fix: 修复账单编辑后刷新才生效的问题

- 在分析页面添加 handleBillUpdated 回调,编辑后同步更新 records 和 allRecords
- 为 TopExpenses、CategoryRanking、DailyTrendChart 组件添加 onUpdate prop
- 修复 TopExpenses 组件内响应式更新,使用新数组引用触发更新
- 建立完整的更新传播链:BillDetailDrawer -> BillRecordsTable -> 各分析组件 -> 分析页面
- 确保所有派生数据(topExpenses、categoryStats、pieChartData)自动刷新
This commit is contained in:
clz
2026-01-19 01:12:33 +08:00
parent 9abd0d964f
commit 654989d3dd
4 changed files with 90 additions and 6 deletions

View File

@@ -16,9 +16,25 @@
totalExpense: number; totalExpense: number;
records: UIBill[]; records: UIBill[];
categories?: string[]; categories?: string[];
onUpdate?: (updated: UIBill, original: UIBill) => void;
} }
let { categoryStats, pieChartData, totalExpense, records = $bindable(), categories = [] }: Props = $props(); let { categoryStats, pieChartData, totalExpense, records = $bindable(), categories = [], onUpdate }: Props = $props();
function handleRecordUpdated(updated: UIBill, original: UIBill) {
// 更新本地 records 数组
const idx = records.findIndex(r =>
r === original ||
(r.time === original.time && r.merchant === original.merchant && r.amount === original.amount)
);
if (idx !== -1) {
records[idx] = updated;
records = [...records];
}
// 传播到父组件
onUpdate?.(updated, original);
}
let mode = $state<'bar' | 'pie'>('bar'); let mode = $state<'bar' | 'pie'>('bar');
let dialogOpen = $state(false); let dialogOpen = $state(false);
@@ -213,7 +229,7 @@
</Drawer.Header> </Drawer.Header>
<div class="flex-1 overflow-auto px-4 md:px-0"> <div class="flex-1 overflow-auto px-4 md:px-0">
<BillRecordsTable records={selectedRecords} showDescription={true} {categories} /> <BillRecordsTable records={selectedRecords} showDescription={true} {categories} onUpdate={handleRecordUpdated} />
</div> </div>
<Drawer.Footer> <Drawer.Footer>

View File

@@ -17,9 +17,35 @@
interface Props { interface Props {
records: UIBill[]; records: UIBill[];
categories?: string[]; categories?: string[];
onUpdate?: (updated: UIBill, original: UIBill) => void;
} }
let { records = $bindable(), categories = [] }: Props = $props(); let { records = $bindable(), categories = [], onUpdate }: Props = $props();
function handleRecordUpdated(updated: UIBill, original: UIBill) {
// 更新 records 数组
const idx = records.findIndex(r =>
r === original ||
(r.time === original.time && r.merchant === original.merchant && r.amount === original.amount)
);
if (idx !== -1) {
records[idx] = updated;
records = [...records];
}
// 更新 selectedDateRecords如果账单在当前选中的日期记录中
const dateIdx = selectedDateRecords.findIndex(r =>
r === original ||
(r.time === original.time && r.merchant === original.merchant && r.amount === original.amount)
);
if (dateIdx !== -1) {
selectedDateRecords[dateIdx] = updated;
selectedDateRecords = [...selectedDateRecords];
}
// 传播到父组件
onUpdate?.(updated, original);
}
// Dialog 状态 // Dialog 状态
let dialogOpen = $state(false); let dialogOpen = $state(false);
@@ -896,6 +922,7 @@
showDescription={false} showDescription={false}
pageSize={8} pageSize={8}
{categories} {categories}
onUpdate={handleRecordUpdated}
/> />
</div> </div>
{:else} {:else}

View File

@@ -24,7 +24,11 @@
function handleRecordUpdated(updated: UIBill, original: UIBill) { function handleRecordUpdated(updated: UIBill, original: UIBill) {
const idx = records.findIndex(r => r === original); const idx = records.findIndex(r => r === original);
if (idx !== -1) records[idx] = updated; if (idx !== -1) {
records[idx] = updated;
// 触发响应式更新
records = [...records];
}
selectedRecord = updated; selectedRecord = updated;
onUpdate?.(updated); onUpdate?.(updated);
} }

View File

@@ -91,6 +91,42 @@
let pieChartData = $derived(calculatePieChartData(categoryStats, totalStats.expense)); let pieChartData = $derived(calculatePieChartData(categoryStats, totalStats.expense));
let topExpenses = $derived(getTopExpenses(analysisRecords, 10)); let topExpenses = $derived(getTopExpenses(analysisRecords, 10));
// 账单更新处理
function handleBillUpdated(updated: UIBill) {
// 在 records 中查找并更新对应的账单
const idx = records.findIndex(r =>
r.id === (updated as unknown as { id?: string }).id ||
(r.time === updated.time && r.merchant === updated.merchant && r.amount === updated.amount)
);
if (idx !== -1) {
// 更新后端格式的记录
records[idx] = {
...records[idx],
time: updated.time,
category: updated.category,
merchant: updated.merchant,
description: updated.description || '',
amount: updated.amount,
pay_method: updated.paymentMethod || '',
status: updated.status || records[idx].status,
remark: updated.remark || records[idx].remark,
};
// 触发响应式更新
records = [...records];
// 同时更新 allRecords如果账单在全部数据中
const allIdx = allRecords.findIndex(r =>
r.id === (updated as unknown as { id?: string }).id ||
(r.time === updated.time && r.merchant === updated.merchant)
);
if (allIdx !== -1) {
allRecords[allIdx] = records[idx];
allRecords = [...allRecords];
}
}
}
// 分类列表按数据中出现次数排序 // 分类列表按数据中出现次数排序
let sortedCategories = $derived(() => { let sortedCategories = $derived(() => {
const categoryCounts = new Map<string, number>(); const categoryCounts = new Map<string, number>();
@@ -253,7 +289,7 @@
<OverviewCards {totalStats} records={analysisRecords} /> <OverviewCards {totalStats} records={analysisRecords} />
<!-- 每日支出趋势图(按分类堆叠) - 使用全部数据 --> <!-- 每日支出趋势图(按分类堆叠) - 使用全部数据 -->
<DailyTrendChart records={allAnalysisRecords} categories={sortedCategories()} /> <DailyTrendChart records={allAnalysisRecords} categories={sortedCategories()} onUpdate={handleBillUpdated} />
<div class="grid gap-6 lg:grid-cols-2"> <div class="grid gap-6 lg:grid-cols-2">
<!-- 分类支出排行 --> <!-- 分类支出排行 -->
@@ -263,6 +299,7 @@
totalExpense={totalStats.expense} totalExpense={totalStats.expense}
records={analysisRecords} records={analysisRecords}
categories={sortedCategories()} categories={sortedCategories()}
onUpdate={handleBillUpdated}
/> />
<!-- 月度趋势 --> <!-- 月度趋势 -->
@@ -270,7 +307,7 @@
</div> </div>
<!-- Top 10 支出 --> <!-- Top 10 支出 -->
<TopExpenses records={topExpenses} categories={sortedCategories()} /> <TopExpenses records={topExpenses} categories={sortedCategories()} onUpdate={handleBillUpdated} />
{:else} {:else}
<!-- 空状态:服务器不可用或没有数据时显示示例按钮 --> <!-- 空状态:服务器不可用或没有数据时显示示例按钮 -->
<Card.Root> <Card.Root>