feat: 支持账单编辑(PATCH /api/bills/:id)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user