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,145 @@
<script lang="ts">
import { fetchBillContent, type BillRecord } from '$lib/api';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Input } from '$lib/components/ui/input';
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
import Loader2 from '@lucide/svelte/icons/loader-2';
import AlertCircle from '@lucide/svelte/icons/alert-circle';
// 分析组件
import {
OverviewCards,
DailyTrendChart,
CategoryRanking,
MonthlyTrend,
TopExpenses,
EmptyState
} from '$lib/components/analysis';
// 数据处理服务
import {
calculateCategoryStats,
calculateMonthlyStats,
calculateDailyExpenseData,
calculateTotalStats,
calculatePieChartData,
getTopExpenses
} from '$lib/services/analysis';
// 演示数据
import { demoRecords } from '$lib/data/demo';
// 状态
let fileName = $state('');
let isLoading = $state(false);
let errorMessage = $state('');
let records: BillRecord[] = $state([]);
let isDemo = $state(false);
// 派生数据
let categoryStats = $derived(calculateCategoryStats(records));
let monthlyStats = $derived(calculateMonthlyStats(records));
let dailyExpenseData = $derived(calculateDailyExpenseData(records));
let totalStats = $derived(calculateTotalStats(records));
let pieChartData = $derived(calculatePieChartData(categoryStats, totalStats.expense));
let topExpenses = $derived(getTopExpenses(records, 10));
async function loadData() {
if (!fileName) return;
isLoading = true;
errorMessage = '';
isDemo = false;
try {
records = await fetchBillContent(fileName);
} catch (err) {
errorMessage = err instanceof Error ? err.message : '加载失败';
} finally {
isLoading = false;
}
}
function loadDemoData() {
isDemo = true;
errorMessage = '';
records = demoRecords;
}
</script>
<svelte:head>
<title>数据分析 - BillAI</title>
</svelte:head>
<div class="space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">数据分析</h1>
<p class="text-muted-foreground">可视化你的消费数据,洞察消费习惯</p>
</div>
{#if isDemo}
<Badge variant="secondary" class="text-xs">
📊 示例数据
</Badge>
{/if}
</div>
<!-- 搜索栏 -->
<div class="flex gap-3">
<div class="relative flex-1">
<BarChart3 class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="text"
placeholder="输入文件名..."
class="pl-10"
bind:value={fileName}
onkeydown={(e) => e.key === 'Enter' && loadData()}
/>
</div>
<Button onclick={loadData} disabled={isLoading}>
{#if isLoading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
分析中
{:else}
<BarChart3 class="mr-2 h-4 w-4" />
分析
{/if}
</Button>
</div>
<!-- 错误提示 -->
{#if errorMessage}
<div class="flex items-center gap-2 rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
<AlertCircle class="h-4 w-4" />
{errorMessage}
</div>
{/if}
{#if records.length > 0}
<!-- 总览卡片 -->
<OverviewCards {totalStats} {records} />
<!-- 每日支出趋势图(按分类堆叠) -->
<DailyTrendChart {records} />
<div class="grid gap-6 lg:grid-cols-2">
<!-- 分类支出排行 -->
<CategoryRanking
{categoryStats}
{pieChartData}
totalExpense={totalStats.expense}
{records}
/>
<!-- 月度趋势 -->
<MonthlyTrend {monthlyStats} />
</div>
<!-- Top 10 支出 -->
<TopExpenses records={topExpenses} />
{:else if !isLoading}
<EmptyState onLoadDemo={loadDemoData} />
{/if}
</div>