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