feat: 新增账单导出 Excel 功能
- 后端新增 /api/bills/export 接口,支持当前筛选条件导出全部记录 - 使用 excelize 库生成 xlsx 格式文件 - 前端账单管理页面添加导出按钮 - 更新 Go 版本到 1.24 以支持 excelize 依赖
This commit is contained in:
@@ -316,6 +316,46 @@ export async function fetchBills(params: FetchBillsParams = {}): Promise<BillsRe
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 导出账单为 Excel
|
||||
export async function exportBills(params: FetchBillsParams = {}): Promise<void> {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (params.start_date) searchParams.set('start_date', params.start_date);
|
||||
if (params.end_date) searchParams.set('end_date', params.end_date);
|
||||
if (params.category) searchParams.set('category', params.category);
|
||||
if (params.type) searchParams.set('type', params.type);
|
||||
if (params.income_expense) searchParams.set('income_expense', params.income_expense);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${API_BASE}/api/bills/export${queryString ? '?' + queryString : ''}`;
|
||||
|
||||
const response = await apiFetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = `bills_${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||||
|
||||
if (contentDisposition) {
|
||||
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (match && match[1]) {
|
||||
filename = match[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
const objectUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = objectUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
|
||||
// 手动输入账单数据
|
||||
export interface ManualBillInput {
|
||||
time: string;
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
import BillDetailDrawer from '$lib/components/analysis/BillDetailDrawer.svelte';
|
||||
import { cleanedBillToUIBill, type UIBill } from '$lib/models/bill';
|
||||
import { categories } from '$lib/data/categories';
|
||||
import { formatLocalDate, formatDateTime } from '$lib/utils';
|
||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||
import { formatLocalDate, formatDateTime } from '$lib/utils';
|
||||
import { exportBills } from '$lib/api';
|
||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
||||
import Search from '@lucide/svelte/icons/search';
|
||||
import Receipt from '@lucide/svelte/icons/receipt';
|
||||
@@ -26,9 +27,10 @@
|
||||
import Filter from '@lucide/svelte/icons/filter';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import List from '@lucide/svelte/icons/list';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import List from '@lucide/svelte/icons/list';
|
||||
import Download from '@lucide/svelte/icons/download';
|
||||
|
||||
// 状态
|
||||
let isLoading = $state(false);
|
||||
@@ -221,6 +223,29 @@
|
||||
totalIncome = Math.max(0, totalIncome - deleted.amount);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 Excel
|
||||
let isExporting = $state(false);
|
||||
let exportError = $state('');
|
||||
|
||||
async function handleExport() {
|
||||
isExporting = true;
|
||||
exportError = '';
|
||||
|
||||
try {
|
||||
await exportBills({
|
||||
start_date: startDate || undefined,
|
||||
end_date: endDate || undefined,
|
||||
category: filterCategory || undefined,
|
||||
type: filterBillType || undefined,
|
||||
income_expense: filterIncomeExpense || undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
exportError = err instanceof Error ? err.message : '导出失败';
|
||||
} finally {
|
||||
isExporting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -240,6 +265,10 @@
|
||||
<RefreshCw class="mr-2 h-4 w-4 {isLoading ? 'animate-spin' : ''}" />
|
||||
刷新
|
||||
</Button>
|
||||
<Button variant="outline" onclick={handleExport} disabled={isExporting || totalRecords === 0}>
|
||||
<Download class="mr-2 h-4 w-4 {isExporting ? 'animate-spin' : ''}" />
|
||||
导出 Excel
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user