-
+
{/each}
+
+
+
+
+
+
+ {isEditing ? '编辑账单' : '账单详情'}
+ {#if selectedRank <= 3 && !isEditing}
+
+ Top {selectedRank}
+
+ {/if}
+
+
+ {isEditing ? '修改这笔支出的信息' : '查看这笔支出的完整信息'}
+
+
+
+ {#if selectedRecord}
+ {#if isEditing}
+
+
+
+
+
+
+
+
+
+
+
+
+ {editForm.category || '选择分类'}
+
+
+
+ {#each categories as category}
+ {category}
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {:else}
+
+
+
+
+
支出金额
+
+ ¥{selectedRecord.amount}
+
+
+
+
+
+
+
+
+
商家
+
{selectedRecord.merchant}
+
+
+
+
+
+
+
分类
+
{selectedRecord.category}
+
+
+
+
+
+
+
时间
+
{selectedRecord.time}
+
+
+
+ {#if selectedRecord.description}
+
+
+
+
描述
+
{selectedRecord.description}
+
+
+ {/if}
+
+ {#if selectedRecord.payment_method}
+
+
+
+
支付方式
+
{selectedRecord.payment_method}
+
+
+ {/if}
+
+
+ {/if}
+ {/if}
+
+
+ {#if isEditing}
+
+
+ {:else}
+
+
+ {/if}
+
+
+
diff --git a/web/src/lib/components/analysis/index.ts b/web/src/lib/components/analysis/index.ts
index a6f939c..0571c9f 100644
--- a/web/src/lib/components/analysis/index.ts
+++ b/web/src/lib/components/analysis/index.ts
@@ -4,5 +4,6 @@ export { default as CategoryRanking } from './CategoryRanking.svelte';
export { default as MonthlyTrend } from './MonthlyTrend.svelte';
export { default as TopExpenses } from './TopExpenses.svelte';
export { default as EmptyState } from './EmptyState.svelte';
+export { default as BillRecordsTable } from './BillRecordsTable.svelte';
diff --git a/web/src/lib/data/categories.ts b/web/src/lib/data/categories.ts
new file mode 100644
index 0000000..8a09680
--- /dev/null
+++ b/web/src/lib/data/categories.ts
@@ -0,0 +1,53 @@
+/** 账单分类列表 */
+export const categories = [
+ '餐饮美食',
+ '交通出行',
+ '日用百货',
+ '充值缴费',
+ '家居家装',
+ '运动健身',
+ '文化休闲',
+ '数码电器',
+ '医疗健康',
+ '教育培训',
+ '美容护理',
+ '服饰鞋包',
+ '宠物相关',
+ '住房物业',
+ '退款',
+ '工资收入',
+ '投资理财',
+ '其他收入',
+ '其他支出',
+] as const;
+
+/** 分类类型 */
+export type Category = (typeof categories)[number];
+
+/** 支出分类(用于分析页面筛选) */
+export const expenseCategories = [
+ '餐饮美食',
+ '交通出行',
+ '日用百货',
+ '充值缴费',
+ '家居家装',
+ '运动健身',
+ '文化休闲',
+ '数码电器',
+ '医疗健康',
+ '教育培训',
+ '美容护理',
+ '服饰鞋包',
+ '宠物相关',
+ '住房物业',
+ '其他支出',
+] as const;
+
+/** 收入分类 */
+export const incomeCategories = [
+ '退款',
+ '工资收入',
+ '投资理财',
+ '其他收入',
+] as const;
+
diff --git a/web/src/lib/data/index.ts b/web/src/lib/data/index.ts
new file mode 100644
index 0000000..6fd0ab3
--- /dev/null
+++ b/web/src/lib/data/index.ts
@@ -0,0 +1,8 @@
+export { demoRecords } from './demo';
+export {
+ categories,
+ expenseCategories,
+ incomeCategories,
+ type Category
+} from './categories';
+
diff --git a/web/src/routes/analysis/+page.svelte b/web/src/routes/analysis/+page.svelte
index c5d3724..d3e31f4 100644
--- a/web/src/routes/analysis/+page.svelte
+++ b/web/src/routes/analysis/+page.svelte
@@ -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
();
+ 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 @@
-
+
@@ -130,7 +151,8 @@
{categoryStats}
{pieChartData}
totalExpense={totalStats.expense}
- {records}
+ bind:records
+ categories={sortedCategories()}
/>
@@ -138,7 +160,7 @@
-
+
{:else if !isLoading}
{/if}