fix: 修复账单删除功能并支持分析页面删除操作
Some checks are pending
Deploy BillAI / Deploy to Production (push) Waiting to run
Some checks are pending
Deploy BillAI / Deploy to Production (push) Waiting to run
- 将删除接口从 DELETE /api/bills/:id 改为 POST /api/bills/:id/delete 以兼容 SvelteKit 代理 - 分析页面组件 (TopExpenses/BillRecordsTable/DailyTrendChart) 支持删除并同步更新统计数据 - Review 接口改为直接查询 MongoDB 而非读取文件 - 软删除时记录 updated_at 时间戳 - 添加 .dockerignore 文件优化构建 - 完善 AGENTS.md 文档
This commit is contained in:
5
web/.dockerignore
Normal file
5
web/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.git
|
||||
.DS_Store
|
||||
.svelte-kit
|
||||
build
|
||||
@@ -128,17 +128,6 @@ export async function uploadBill(
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 获取复核记录
|
||||
export async function getReviewRecords(fileName: string): Promise<ReviewResponse> {
|
||||
const response = await apiFetch(`${API_BASE}/api/review?file=${encodeURIComponent(fileName)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 获取月度统计(全部数据,不受筛选条件影响)
|
||||
export async function fetchMonthlyStats(): Promise<MonthlyStatsResponse> {
|
||||
const response = await apiFetch(`${API_BASE}/api/monthly-stats`);
|
||||
@@ -403,8 +392,8 @@ export interface DeleteBillResponse {
|
||||
|
||||
// 删除账单(软删除)
|
||||
export async function deleteBill(id: string): Promise<DeleteBillResponse> {
|
||||
const response = await apiFetch(`${API_BASE}/api/bills/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE'
|
||||
const response = await apiFetch(`${API_BASE}/api/bills/${encodeURIComponent(id)}/delete`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
pageSize?: number;
|
||||
categories?: string[];
|
||||
onUpdate?: (updated: UIBill, original: UIBill) => void;
|
||||
onDelete?: (deleted: UIBill) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -24,7 +25,8 @@
|
||||
showDescription = true,
|
||||
pageSize = 10,
|
||||
categories = [],
|
||||
onUpdate
|
||||
onUpdate,
|
||||
onDelete
|
||||
}: Props = $props();
|
||||
|
||||
// 排序状态
|
||||
@@ -112,6 +114,24 @@
|
||||
onUpdate?.(updated, original);
|
||||
}
|
||||
|
||||
function handleRecordDeleted(deleted: UIBill) {
|
||||
const idx = records.findIndex(r => r === deleted);
|
||||
const finalIdx = idx !== -1
|
||||
? idx
|
||||
: records.findIndex(r =>
|
||||
r.time === deleted.time &&
|
||||
r.merchant === deleted.merchant &&
|
||||
r.amount === deleted.amount
|
||||
);
|
||||
|
||||
if (finalIdx !== -1) {
|
||||
records.splice(finalIdx, 1);
|
||||
records = [...records];
|
||||
}
|
||||
|
||||
onDelete?.(deleted);
|
||||
}
|
||||
|
||||
// 重置分页(当记录变化时)
|
||||
$effect(() => {
|
||||
records;
|
||||
@@ -280,4 +300,6 @@
|
||||
viewDescription="查看这笔支出的详细信息"
|
||||
editDescription="修改这笔支出的信息"
|
||||
onUpdate={handleRecordUpdated}
|
||||
onDelete={handleRecordDeleted}
|
||||
allowDelete={true}
|
||||
/>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
records: UIBill[];
|
||||
categories?: string[];
|
||||
onUpdate?: (updated: UIBill, original: UIBill) => void;
|
||||
onDelete?: (deleted: UIBill) => void;
|
||||
}
|
||||
|
||||
let { records = $bindable(), categories = [], onUpdate }: Props = $props();
|
||||
let { records = $bindable(), categories = [], onUpdate, onDelete }: Props = $props();
|
||||
|
||||
function handleRecordUpdated(updated: UIBill, original: UIBill) {
|
||||
// 更新 records 数组
|
||||
@@ -46,6 +47,28 @@
|
||||
// 传播到父组件
|
||||
onUpdate?.(updated, original);
|
||||
}
|
||||
|
||||
function handleRecordDeleted(deleted: UIBill) {
|
||||
const idx = records.findIndex(r =>
|
||||
r === deleted ||
|
||||
(r.time === deleted.time && r.merchant === deleted.merchant && r.amount === deleted.amount)
|
||||
);
|
||||
if (idx !== -1) {
|
||||
records.splice(idx, 1);
|
||||
records = [...records];
|
||||
}
|
||||
|
||||
const dateIdx = selectedDateRecords.findIndex(r =>
|
||||
r === deleted ||
|
||||
(r.time === deleted.time && r.merchant === deleted.merchant && r.amount === deleted.amount)
|
||||
);
|
||||
if (dateIdx !== -1) {
|
||||
selectedDateRecords.splice(dateIdx, 1);
|
||||
selectedDateRecords = [...selectedDateRecords];
|
||||
}
|
||||
|
||||
onDelete?.(deleted);
|
||||
}
|
||||
|
||||
// Dialog 状态
|
||||
let dialogOpen = $state(false);
|
||||
@@ -923,6 +946,7 @@
|
||||
pageSize={8}
|
||||
{categories}
|
||||
onUpdate={handleRecordUpdated}
|
||||
onDelete={handleRecordDeleted}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
records: UIBill[];
|
||||
categories: string[]; // 可用的分类列表
|
||||
onUpdate?: (record: UIBill) => void;
|
||||
onDelete?: (record: UIBill) => void;
|
||||
}
|
||||
|
||||
let { records, categories, onUpdate }: Props = $props();
|
||||
let { records, categories, onUpdate, onDelete }: Props = $props();
|
||||
|
||||
let dialogOpen = $state(false);
|
||||
let selectedRecord = $state<UIBill | null>(null);
|
||||
@@ -32,6 +33,26 @@
|
||||
selectedRecord = updated;
|
||||
onUpdate?.(updated);
|
||||
}
|
||||
|
||||
function handleRecordDeleted(deleted: UIBill) {
|
||||
const idx = records.findIndex(r => r === deleted);
|
||||
const finalIdx = idx !== -1
|
||||
? idx
|
||||
: records.findIndex(r =>
|
||||
r.time === deleted.time &&
|
||||
r.merchant === deleted.merchant &&
|
||||
r.amount === deleted.amount
|
||||
);
|
||||
|
||||
if (finalIdx !== -1) {
|
||||
records.splice(finalIdx, 1);
|
||||
records = [...records];
|
||||
}
|
||||
|
||||
selectedRecord = null;
|
||||
selectedRank = 0;
|
||||
onDelete?.(deleted);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card.Root class="transition-all duration-200 hover:shadow-lg hover:-translate-y-1">
|
||||
@@ -80,6 +101,8 @@
|
||||
viewDescription="查看这笔支出的完整信息"
|
||||
editDescription="修改这笔支出的信息"
|
||||
onUpdate={handleRecordUpdated}
|
||||
onDelete={handleRecordDeleted}
|
||||
allowDelete={true}
|
||||
>
|
||||
{#snippet titleExtra({ isEditing })}
|
||||
{#if selectedRank <= 3 && !isEditing}
|
||||
|
||||
@@ -126,6 +126,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBillDeleted(deleted: UIBill) {
|
||||
const idx = records.findIndex(r =>
|
||||
r.id === (deleted as unknown as { id?: string }).id ||
|
||||
(r.time === deleted.time && r.merchant === deleted.merchant && r.amount === deleted.amount)
|
||||
);
|
||||
if (idx !== -1) {
|
||||
records.splice(idx, 1);
|
||||
records = [...records];
|
||||
}
|
||||
|
||||
const allIdx = allRecords.findIndex(r =>
|
||||
r.id === (deleted as unknown as { id?: string }).id ||
|
||||
(r.time === deleted.time && r.merchant === deleted.merchant && r.amount === deleted.amount)
|
||||
);
|
||||
if (allIdx !== -1) {
|
||||
allRecords.splice(allIdx, 1);
|
||||
allRecords = [...allRecords];
|
||||
}
|
||||
|
||||
if (deleted.incomeExpense === '支出') {
|
||||
backendTotalExpense = Math.max(0, backendTotalExpense - deleted.amount);
|
||||
} else if (deleted.incomeExpense === '收入') {
|
||||
backendTotalIncome = Math.max(0, backendTotalIncome - deleted.amount);
|
||||
}
|
||||
}
|
||||
|
||||
// 分类列表按数据中出现次数排序
|
||||
let sortedCategories = $derived(() => {
|
||||
@@ -289,7 +315,12 @@
|
||||
<OverviewCards {totalStats} records={analysisRecords} />
|
||||
|
||||
<!-- 每日支出趋势图(按分类堆叠) - 使用全部数据 -->
|
||||
<DailyTrendChart records={allAnalysisRecords} categories={sortedCategories()} onUpdate={handleBillUpdated} />
|
||||
<DailyTrendChart
|
||||
records={allAnalysisRecords}
|
||||
categories={sortedCategories()}
|
||||
onUpdate={handleBillUpdated}
|
||||
onDelete={handleBillDeleted}
|
||||
/>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-2">
|
||||
<!-- 分类支出排行 -->
|
||||
@@ -307,7 +338,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Top 10 支出 -->
|
||||
<TopExpenses records={topExpenses} categories={sortedCategories()} onUpdate={handleBillUpdated} />
|
||||
<TopExpenses
|
||||
records={topExpenses}
|
||||
categories={sortedCategories()}
|
||||
onUpdate={handleBillUpdated}
|
||||
onDelete={handleBillDeleted}
|
||||
/>
|
||||
{:else}
|
||||
<!-- 空状态:服务器不可用或没有数据时显示示例按钮 -->
|
||||
<Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user