chore: add frontend/src/utils/exportHelper.ts
This commit is contained in:
@@ -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`);
|
||||
}
|
||||
Reference in New Issue
Block a user