feat: 支持账单编辑(PATCH /api/bills/:id)

This commit is contained in:
clz
2026-01-18 20:17:19 +08:00
parent 339b8afe98
commit f5afb0c135
8 changed files with 326 additions and 37 deletions

View File

@@ -249,6 +249,42 @@ export interface CleanedBill {
review_level: string;
}
// 更新账单
export interface UpdateBillRequest {
time?: string;
category?: string;
merchant?: string;
description?: string;
income_expense?: string;
amount?: number;
pay_method?: string;
status?: string;
remark?: string;
}
export interface UpdateBillResponse {
result: boolean;
message?: string;
data?: CleanedBill;
}
export async function updateBill(id: string, patch: UpdateBillRequest): Promise<UpdateBillResponse> {
const response = await apiFetch(`${API_BASE}/api/bills/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(patch),
});
if (!response.ok) {
// keep same behavior as other API calls
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// 账单列表请求参数
export interface FetchBillsParams {
page?: number;

View File

@@ -19,7 +19,7 @@
import Tag from '@lucide/svelte/icons/tag';
import FileText from '@lucide/svelte/icons/file-text';
import CreditCard from '@lucide/svelte/icons/credit-card';
import type { BillRecord } from '$lib/api';
import { updateBill, type BillRecord } from '$lib/api';
interface Props {
records: BillRecord[];
@@ -53,6 +53,7 @@
let selectedRecord = $state<BillRecord | null>(null);
let selectedIndex = $state(-1);
let isEditing = $state(false);
let isSaving = $state(false);
let editForm = $state({
amount: '',
merchant: '',
@@ -135,8 +136,11 @@
}
// 保存编辑
function saveEdit() {
async function saveEdit() {
if (!selectedRecord) return;
if (isSaving) return;
isSaving = true;
const original = { ...selectedRecord };
const updated: BillRecord = {
@@ -147,23 +151,52 @@
description: editForm.description,
payment_method: editForm.payment_method
};
// 更新本地数据
const idx = records.findIndex(r =>
r.time === selectedRecord!.time &&
r.merchant === selectedRecord!.merchant &&
r.amount === selectedRecord!.amount
);
if (idx !== -1) {
records[idx] = updated;
records = [...records]; // 触发响应式更新
try {
// 如果有后端 ID则持久化更新
const billId = (selectedRecord as unknown as { id?: string }).id;
if (billId) {
const resp = await updateBill(billId, {
merchant: editForm.merchant,
category: editForm.category,
amount: Number(editForm.amount),
description: editForm.description,
pay_method: editForm.payment_method,
});
if (resp.result && resp.data) {
// 将后端返回的 CleanedBill 映射为 BillRecord
updated.amount = String(resp.data.amount);
updated.merchant = resp.data.merchant;
updated.category = resp.data.category;
updated.description = resp.data.description || '';
updated.payment_method = resp.data.pay_method || '';
// 让时间展示更稳定:使用后端格式
updated.time = resp.data.time;
}
}
// 更新本地数据fallback按引用/关键字段查找)
const idx = records.findIndex(r => r === selectedRecord);
const finalIdx = idx !== -1
? idx
: records.findIndex(r =>
r.time === selectedRecord!.time &&
r.merchant === selectedRecord!.merchant &&
r.amount === selectedRecord!.amount
);
if (finalIdx !== -1) {
records[finalIdx] = updated;
records = [...records];
}
selectedRecord = updated;
isEditing = false;
onUpdate?.(updated, original);
} finally {
isSaving = false;
}
selectedRecord = updated;
isEditing = false;
// 通知父组件
onUpdate?.(updated, original);
}
// 处理分类选择
@@ -335,7 +368,7 @@
<!-- 详情/编辑弹窗 -->
<Drawer.Root bind:open={detailDialogOpen}>
<Drawer.Content class="sm:max-w-md">
<Drawer.Content class="md:max-w-md">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<Receipt class="h-5 w-5" />
@@ -465,9 +498,9 @@
<X class="h-4 w-4 mr-2" />
取消
</Button>
<Button onclick={saveEdit}>
<Button onclick={saveEdit} disabled={isSaving}>
<Save class="h-4 w-4 mr-2" />
保存
{isSaving ? '保存中…' : '保存'}
</Button>
{:else}
<Button variant="outline" onclick={() => detailDialogOpen = false}>

View File

@@ -15,7 +15,7 @@
import Tag from '@lucide/svelte/icons/tag';
import FileText from '@lucide/svelte/icons/file-text';
import CreditCard from '@lucide/svelte/icons/credit-card';
import type { BillRecord } from '$lib/api';
import { updateBill, type BillRecord } from '$lib/api';
interface Props {
records: BillRecord[];
@@ -29,6 +29,7 @@
let selectedRecord = $state<BillRecord | null>(null);
let selectedRank = $state(0);
let isEditing = $state(false);
let isSaving = $state(false);
// 编辑表单数据
let editForm = $state({
@@ -62,8 +63,11 @@
isEditing = false;
}
function saveEdit() {
async function saveEdit() {
if (!selectedRecord) return;
if (isSaving) return;
isSaving = true;
// 更新记录
const updatedRecord: BillRecord = {
@@ -74,18 +78,42 @@
description: editForm.description,
payment_method: editForm.payment_method
};
// 更新本地数据
const index = records.findIndex(r => r === selectedRecord);
if (index !== -1) {
records[index] = updatedRecord;
try {
const billId = (selectedRecord as unknown as { id?: string }).id;
if (billId) {
const resp = await updateBill(billId, {
merchant: editForm.merchant,
category: editForm.category,
amount: Number(editForm.amount),
description: editForm.description,
pay_method: editForm.payment_method,
});
if (resp.result && resp.data) {
updatedRecord.amount = String(resp.data.amount);
updatedRecord.merchant = resp.data.merchant;
updatedRecord.category = resp.data.category;
updatedRecord.description = resp.data.description || '';
updatedRecord.payment_method = resp.data.pay_method || '';
updatedRecord.time = resp.data.time;
}
}
// 更新本地数据
const index = records.findIndex(r => r === selectedRecord);
if (index !== -1) {
records[index] = updatedRecord;
}
selectedRecord = updatedRecord;
isEditing = false;
// 通知父组件
onUpdate?.(updatedRecord);
} finally {
isSaving = false;
}
selectedRecord = updatedRecord;
isEditing = false;
// 通知父组件
onUpdate?.(updatedRecord);
}
function handleCategoryChange(value: string | undefined) {
@@ -135,7 +163,7 @@
<!-- 账单详情弹窗 -->
<Drawer.Root bind:open={dialogOpen}>
<Drawer.Content class="sm:max-w-[450px]">
<Drawer.Content class="md:max-w-[450px]">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<Receipt class="h-5 w-5" />
@@ -271,9 +299,9 @@
<X class="h-4 w-4 mr-2" />
取消
</Button>
<Button onclick={saveEdit}>
<Button onclick={saveEdit} disabled={isSaving}>
<Save class="h-4 w-4 mr-2" />
保存
{isSaving ? '保存中…' : '保存'}
</Button>
{:else}
<Button variant="outline" onclick={() => dialogOpen = false}>