feat(analysis): 增强图表交互功能
- 分类支出排行: 饼图支持点击类别切换显示/隐藏,百分比动态重新计算 - 每日支出趋势: 图例支持点击切换类别显示,隐藏类别不参与堆叠计算 - Dialog列表: 添加列排序功能(时间/商家/描述/金额) - Dialog列表: 添加分页功能,每页10条(分类)/8条(每日) - 饼图hover效果: 扇形放大、阴影增强、中心显示详情
This commit is contained in:
145
web/src/routes/analysis/+page.svelte
Normal file
145
web/src/routes/analysis/+page.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user