feat(analysis): 增强图表交互功能

- 分类支出排行: 饼图支持点击类别切换显示/隐藏,百分比动态重新计算
- 每日支出趋势: 图例支持点击切换类别显示,隐藏类别不参与堆叠计算
- Dialog列表: 添加列排序功能(时间/商家/描述/金额)
- Dialog列表: 添加分页功能,每页10条(分类)/8条(每日)
- 饼图hover效果: 扇形放大、阴影增强、中心显示详情
This commit is contained in:
clz
2026-01-08 02:55:54 +08:00
parent c40a118a3d
commit 9d409d6a93
161 changed files with 9155 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
import type { BillRecord } from '$lib/api';
import type { CategoryStat, MonthlyStat, DailyExpenseData, TotalStats, PieChartDataItem } from '$lib/types/analysis';
import { pieColors } from '$lib/constants/chart';
/**
* 计算分类统计
*/
export function calculateCategoryStats(records: BillRecord[]): CategoryStat[] {
const stats = new Map<string, { expense: number; income: number; count: number }>();
for (const r of records) {
if (!stats.has(r.category)) {
stats.set(r.category, { expense: 0, income: 0, count: 0 });
}
const s = stats.get(r.category)!;
s.count++;
const amount = parseFloat(r.amount || '0');
if (r.income_expense === '支出') {
s.expense += amount;
} else {
s.income += amount;
}
}
return [...stats.entries()]
.map(([category, data]) => ({ category, ...data }))
.sort((a, b) => b.expense - a.expense);
}
/**
* 计算月度统计
*/
export function calculateMonthlyStats(records: BillRecord[]): MonthlyStat[] {
const stats = new Map<string, { expense: number; income: number }>();
for (const r of records) {
const month = r.time.substring(0, 7); // YYYY-MM
if (!stats.has(month)) {
stats.set(month, { expense: 0, income: 0 });
}
const s = stats.get(month)!;
const amount = parseFloat(r.amount || '0');
if (r.income_expense === '支出') {
s.expense += amount;
} else {
s.income += amount;
}
}
return [...stats.entries()]
.map(([month, data]) => ({ month, ...data }))
.sort((a, b) => a.month.localeCompare(b.month));
}
/**
* 计算每日支出数据(用于面积图)
*/
export function calculateDailyExpenseData(records: BillRecord[]): DailyExpenseData[] {
const stats = new Map<string, number>();
for (const r of records) {
if (r.income_expense !== '支出') continue;
const date = r.time.substring(0, 10); // YYYY-MM-DD
const amount = parseFloat(r.amount || '0');
stats.set(date, (stats.get(date) || 0) + amount);
}
return [...stats.entries()]
.map(([date, value]) => ({ date: new Date(date), value }))
.sort((a, b) => a.date.getTime() - b.date.getTime());
}
/**
* 计算总计统计
*/
export function calculateTotalStats(records: BillRecord[]): TotalStats {
return {
expense: records
.filter(r => r.income_expense === '支出')
.reduce((sum, r) => sum + parseFloat(r.amount || '0'), 0),
income: records
.filter(r => r.income_expense === '收入')
.reduce((sum, r) => sum + parseFloat(r.amount || '0'), 0),
count: records.length,
};
}
/**
* 计算百分比
*/
export function getPercentage(value: number, total: number): number {
return total > 0 ? (value / total) * 100 : 0;
}
/**
* 计算饼状图数据
*/
export function calculatePieChartData(
categoryStats: CategoryStat[],
totalExpense: number
): PieChartDataItem[] {
return categoryStats
.filter(s => s.expense > 0)
.map((stat, i) => ({
category: stat.category,
value: stat.expense,
color: pieColors[i % pieColors.length],
percentage: getPercentage(stat.expense, totalExpense)
}));
}
/**
* 获取 Top N 支出记录
*/
export function getTopExpenses(records: BillRecord[], n: number = 10): BillRecord[] {
return records
.filter(r => r.income_expense === '支出')
.sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount))
.slice(0, n);
}
/**
* 统计支出/收入笔数
*/
export function countByType(records: BillRecord[], type: '支出' | '收入'): number {
return records.filter(r => r.income_expense === type).length;
}