feat(analysis): 添加账单详情查看和编辑功能

- BillRecordsTable: 新增点击行查看详情弹窗,支持编辑模式
- CategoryRanking: 分类支出表格支持点击查看/编辑账单详情
- DailyTrendChart: 每日趋势表格支持点击查看/编辑账单详情
- TopExpenses: Top10支出支持点击查看/编辑,前三名高亮显示
- OverviewCards/MonthlyTrend: 添加卡片hover效果
- 新增 categories.ts: 集中管理账单分类数据
- 分类下拉按使用频率排序
This commit is contained in:
CHE LIANG ZHAO
2026-01-08 10:48:11 +08:00
parent 9d409d6a93
commit b226c85fa7
10 changed files with 922 additions and 387 deletions

View File

@@ -29,6 +29,8 @@
// 演示数据
import { demoRecords } from '$lib/data/demo';
// 分类数据
import { categories as allCategories } from '$lib/data/categories';
// 状态
let fileName = $state('');
@@ -44,6 +46,25 @@
let totalStats = $derived(calculateTotalStats(records));
let pieChartData = $derived(calculatePieChartData(categoryStats, totalStats.expense));
let topExpenses = $derived(getTopExpenses(records, 10));
// 分类列表按数据中出现次数排序(出现次数多的优先)
let sortedCategories = $derived(() => {
// 统计每个分类的记录数量
const categoryCounts = new Map<string, number>();
for (const record of records) {
categoryCounts.set(record.category, (categoryCounts.get(record.category) || 0) + 1);
}
// 对分类进行排序:先按数据中的数量降序,未出现的分类按原顺序排在后面
return [...allCategories].sort((a, b) => {
const countA = categoryCounts.get(a) || 0;
const countB = categoryCounts.get(b) || 0;
// 数量大的排前面
if (countA !== countB) return countB - countA;
// 数量相同时保持原有顺序
return allCategories.indexOf(a) - allCategories.indexOf(b);
});
});
async function loadData() {
if (!fileName) return;
@@ -122,7 +143,7 @@
<OverviewCards {totalStats} {records} />
<!-- 每日支出趋势图(按分类堆叠) -->
<DailyTrendChart {records} />
<DailyTrendChart bind:records categories={sortedCategories()} />
<div class="grid gap-6 lg:grid-cols-2">
<!-- 分类支出排行 -->
@@ -130,7 +151,8 @@
{categoryStats}
{pieChartData}
totalExpense={totalStats.expense}
{records}
bind:records
categories={sortedCategories()}
/>
<!-- 月度趋势 -->
@@ -138,7 +160,7 @@
</div>
<!-- Top 10 支出 -->
<TopExpenses records={topExpenses} />
<TopExpenses records={topExpenses} categories={sortedCategories()} />
{:else if !isLoading}
<EmptyState onLoadDemo={loadDemoData} />
{/if}