From e64622d17364d8a48284f0b9a98b8d870f97a804 Mon Sep 17 00:00:00 2001 From: snowgitea Date: Wed, 29 Apr 2026 10:53:42 +0800 Subject: [PATCH] chore: add frontend/src/utils/exportHelper.ts --- frontend/src/utils/exportHelper.ts | 218 +++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 frontend/src/utils/exportHelper.ts diff --git a/frontend/src/utils/exportHelper.ts b/frontend/src/utils/exportHelper.ts new file mode 100644 index 0000000..a1f35c6 --- /dev/null +++ b/frontend/src/utils/exportHelper.ts @@ -0,0 +1,218 @@ +// 导出工具函数 - 将统计数据导出为 Excel 文件 +import * as XLSX from 'xlsx'; +import type { TrendStat, MonthlyCompare } from '../types'; + +/** + * 导出趋势数据到 Excel + * @param data 趋势数据 [{date, income, expense}] + * @param filename 文件名(不含扩展名) + */ +export function exportTrendData(data: TrendStat[], filename: string): void { + if (!data || data.length === 0) { + throw new Error('没有可导出的趋势数据'); + } + + // 构建工作表数据 + const wsData = [ + ['个人记账 - 收支趋势报表', '', '', ''], + [`导出时间: ${new Date().toLocaleString('zh-CN')}`, '', '', ''], + [], // 空行 + ['日期', '收入(元)', '支出(元)', '盈余(元)'], + ...data.map(d => [ + d.date, + d.income, + d.expense, + d.income - d.expense + ]), + [], // 空行 + // 汇总行 + ['合计', + data.reduce((sum, d) => sum + d.income, 0), + data.reduce((sum, d) => sum + d.expense, 0), + data.reduce((sum, d) => sum + (d.income - d.expense), 0) + ], + ]; + + // 创建工作表 + const ws = XLSX.utils.aoa_to_sheet(wsData); + + // 合并单元格(标题) + ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 3 } }]; + + // 设置列宽 + ws['!cols'] = [{ wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 14 }]; + + // 创建工作簿 + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '收支趋势'); + + // 导出文件 + XLSX.writeFile(wb, `${filename}.xlsx`); +} + +/** + * 导出月度对比数据 + * @param data 月度对比数据 + * @param filename 文件名(不含扩展名) + */ +export function exportCompareData(data: MonthlyCompare, filename: string): void { + if (!data) { + throw new Error('没有可导出的月度对比数据'); + } + + const wsData = [ + ['个人记账 - 月度对比报表', '', '', '', ''], + [`导出时间: ${new Date().toLocaleString('zh-CN')}`, '', '', '', ''], + [], // 空行 + ['项目', data.currentMonth.label, '上月环比', data.lastMonth.label, ''], + ['收入(元)', data.currentMonth.income, '', data.lastMonth.income, ''], + ['支出(元)', data.currentMonth.expense, '', data.lastMonth.expense, ''], + ['盈余(元)', + data.currentMonth.income - data.currentMonth.expense, + '', + data.lastMonth.income - data.lastMonth.expense, + '' + ], + [], // 空行 + // 收支占比分析 + ['支出占比', + data.currentMonth.income > 0 + ? `${((data.currentMonth.expense / data.currentMonth.income) * 100).toFixed(1)}%` + : '0%', + '', + data.lastMonth.income > 0 + ? `${((data.lastMonth.expense / data.lastMonth.income) * 100).toFixed(1)}%` + : '0%', + ''], + ]; + + const ws = XLSX.utils.aoa_to_sheet(wsData); + ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 4 } }]; + ws['!cols'] = [{ wch: 12 }, { wch: 14 }, { wch: 12 }, { wch: 14 }, { wch: 12 }]; + + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '月度对比'); + + XLSX.writeFile(wb, `${filename}.xlsx`); +} + +/** + * 导出支出构成数据 + * @param data 分类数据 [{name, value, color}] + * @param totalExpense 总支出 + * @param filename 文件名(不含扩展名) + */ +export function exportCategoryData( + data: { name: string; value: number; color: string }[], + totalExpense: number, + filename: string +): void { + if (!data || data.length === 0) { + throw new Error('没有可导出的支出构成数据'); + } + + const wsData = [ + ['个人记账 - 支出构成报表', '', ''], + [`导出时间: ${new Date().toLocaleString('zh-CN')}`, '', ''], + [`总支出: ¥${totalExpense.toLocaleString()}`, '', ''], + [], // 空行 + ['分类', '金额(元)', '占比'], + ...data.map(item => [ + item.name, + item.value, + totalExpense > 0 ? `${((item.value / totalExpense) * 100).toFixed(1)}%` : '0%', + ]), + [], // 空行 + // 按金额排序的排名 + ['排名', '分类', '金额(元)'], + ...data + .sort((a, b) => b.value - a.value) + .map((item, index) => [ + index + 1, + item.name, + item.value, + ]), + ]; + + const ws = XLSX.utils.aoa_to_sheet(wsData); + ws['!merges'] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }]; + ws['!cols'] = [{ wch: 10 }, { wch: 14 }, { wch: 10 }]; + + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, '支出构成'); + + XLSX.writeFile(wb, `${filename}.xlsx`); +} + +/** + * 导出全部统计数据(综合报表) + * @param params 所有统计数据 + * @param filename 文件名(不含扩展名) + */ +export function exportAllStats( + params: { + trendData?: TrendStat[]; + monthlyCompare?: MonthlyCompare; + categoryData?: { name: string; value: number; color: string }[]; + totalExpense?: number; + }, + filename: string +): void { + const wb = XLSX.utils.book_new(); + + // 添加趋势数据工作表 + if (params.trendData && params.trendData.length > 0) { + const trendWsData = [ + ['日期', '收入(元)', '支出(元)', '盈余(元)'], + ...params.trendData.map(d => [ + d.date, + d.income, + d.expense, + d.income - d.expense + ]), + ['合计', + params.trendData.reduce((sum, d) => sum + d.income, 0), + params.trendData.reduce((sum, d) => sum + d.expense, 0), + params.trendData.reduce((sum, d) => sum + (d.income - d.expense), 0) + ], + ]; + const ws = XLSX.utils.aoa_to_sheet(trendWsData); + ws['!cols'] = [{ wch: 14 }, { wch: 14 }, { wch: 14 }, { wch: 14 }]; + XLSX.utils.book_append_sheet(wb, ws, '收支趋势'); + } + + // 添加月度对比工作表 + if (params.monthlyCompare) { + const { monthlyCompare } = params; + const compareWsData = [ + ['项目', monthlyCompare.currentMonth.label, monthlyCompare.lastMonth.label], + ['收入(元)', monthlyCompare.currentMonth.income, monthlyCompare.lastMonth.income], + ['支出(元)', monthlyCompare.currentMonth.expense, monthlyCompare.lastMonth.expense], + ['盈余(元)', + monthlyCompare.currentMonth.income - monthlyCompare.currentMonth.expense, + monthlyCompare.lastMonth.income - monthlyCompare.lastMonth.expense + ], + ]; + const ws = XLSX.utils.aoa_to_sheet(compareWsData); + ws['!cols'] = [{ wch: 12 }, { wch: 14 }, { wch: 14 }]; + XLSX.utils.book_append_sheet(wb, ws, '月度对比'); + } + + // 添加支出构成工作表 + if (params.categoryData && params.categoryData.length > 0) { + const totalExpense = params.totalExpense || 0; + const categoryWsData = [ + ['分类', '金额(元)', '占比'], + ...params.categoryData.map(item => [ + item.name, + item.value, + totalExpense > 0 ? `${((item.value / totalExpense) * 100).toFixed(1)}%` : '0%', + ]), + ]; + const ws = XLSX.utils.aoa_to_sheet(categoryWsData); + ws['!cols'] = [{ wch: 10 }, { wch: 14 }, { wch: 10 }]; + XLSX.utils.book_append_sheet(wb, ws, '支出构成'); + } + + XLSX.writeFile(wb, `${filename}.xlsx`); +}