feat: 完善项目架构并增强分析页面功能

- 新增项目文档和 Docker 配置
  - 添加 README.md 和 TODO.md 项目文档
  - 为各服务添加 Dockerfile 和 docker-compose 配置

- 重构后端架构
  - 新增 adapter 层(HTTP/Python 适配器)
  - 新增 repository 层(数据访问抽象)
  - 新增 router 模块统一管理路由
  - 新增账单处理 handler

- 扩展前端 UI 组件库
  - 新增 Calendar、DateRangePicker、Drawer、Popover 等组件
  - 集成 shadcn-svelte 组件库

- 增强分析页面功能
  - 添加时间范围筛选器(支持本月默认值)
  - 修复 DateRangePicker 默认值显示问题
  - 优化数据获取和展示逻辑

- 完善分析器服务
  - 新增 FastAPI 服务接口
  - 改进账单清理器实现
This commit is contained in:
2026-01-10 01:15:52 +08:00
parent 94f8ea12e6
commit 087ae027cc
96 changed files with 4301 additions and 482 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as Table from '$lib/components/ui/table';
import * as Dialog from '$lib/components/ui/dialog';
import * as Drawer from '$lib/components/ui/drawer';
import * as Select from '$lib/components/ui/select';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -334,22 +334,22 @@
{/if}
<!-- 详情/编辑弹窗 -->
<Dialog.Root bind:open={detailDialogOpen}>
<Dialog.Content class="sm:max-w-md">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Drawer.Root bind:open={detailDialogOpen}>
<Drawer.Content class="sm:max-w-md">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<Receipt class="h-5 w-5" />
{isEditing ? '编辑账单' : '账单详情'}
</Dialog.Title>
<Dialog.Description>
</Drawer.Title>
<Drawer.Description>
{isEditing ? '修改这笔支出的信息' : '查看这笔支出的详细信息'}
</Dialog.Description>
</Dialog.Header>
</Drawer.Description>
</Drawer.Header>
{#if selectedRecord}
{#if isEditing}
<!-- 编辑表单 -->
<div class="space-y-4 py-4">
<div class="space-y-4 py-4 px-4 md:px-0">
<div class="space-y-2">
<Label>金额</Label>
<div class="relative">
@@ -400,7 +400,7 @@
</div>
{:else}
<!-- 详情展示 -->
<div class="py-4">
<div class="py-4 px-4 md:px-0">
<div class="text-center mb-6">
<div class="text-3xl font-bold text-red-600 dark:text-red-400 font-mono">
¥{selectedRecord.amount}
@@ -459,7 +459,7 @@
{/if}
{/if}
<Dialog.Footer>
<Drawer.Footer>
{#if isEditing}
<Button variant="outline" onclick={cancelEdit}>
<X class="h-4 w-4 mr-2" />
@@ -478,6 +478,6 @@
编辑
</Button>
{/if}
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Dialog from '$lib/components/ui/dialog';
import * as Drawer from '$lib/components/ui/drawer';
import { Button } from '$lib/components/ui/button';
import PieChartIcon from '@lucide/svelte/icons/pie-chart';
import ListIcon from '@lucide/svelte/icons/list';
@@ -147,6 +147,7 @@
{@const x4 = Math.cos(startAngle) * innerRadius}
{@const y4 = Math.sin(startAngle) * innerRadius}
{@const largeArc = (endAngle - startAngle) > Math.PI ? 1 : 0}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<path
d="M {x1} {y1} A {outerRadius} {outerRadius} 0 {largeArc} 1 {x2} {y2} L {x3} {y3} A {innerRadius} {innerRadius} 0 {largeArc} 0 {x4} {y4} Z"
fill={item.color}
@@ -197,28 +198,28 @@
</Card.Root>
<!-- 分类详情弹窗 -->
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="w-fit min-w-[500px] max-w-[90vw] max-h-[80vh] overflow-hidden flex flex-col">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Drawer.Root bind:open={dialogOpen}>
<Drawer.Content class="sm:max-w-4xl">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<PieChartIcon class="h-5 w-5" />
{selectedCategory} - 账单明细
</Dialog.Title>
<Dialog.Description>
</Drawer.Title>
<Drawer.Description>
{#if selectedStat}
{selectedStat.count} 笔,合计 ¥{selectedStat.expense.toFixed(2)}
{/if}
</Dialog.Description>
</Dialog.Header>
</Drawer.Description>
</Drawer.Header>
<div class="flex-1 overflow-auto mt-4">
<div class="flex-1 overflow-auto px-4 md:px-0">
<BillRecordsTable records={selectedRecords} showDescription={true} {categories} />
</div>
<Dialog.Footer class="mt-4">
<Drawer.Footer>
<Button variant="outline" onclick={() => dialogOpen = false}>
关闭
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Select from '$lib/components/ui/select';
import * as Dialog from '$lib/components/ui/dialog';
import * as Drawer from '$lib/components/ui/drawer';
import Activity from '@lucide/svelte/icons/activity';
import TrendingUp from '@lucide/svelte/icons/trending-up';
import TrendingDown from '@lucide/svelte/icons/trending-down';
@@ -46,6 +46,12 @@
hiddenCategories = newSet;
}
// 提取日期字符串 (YYYY-MM-DD) - 兼容多种格式
function extractDateStr(timeStr: string): string {
// 处理 ISO 格式 (2025-12-29T10:30:00Z) 或空格格式 (2025-12-29 10:30:00)
return timeStr.split('T')[0].split(' ')[0];
}
const timeRangeOptions = [
{ value: '7d', label: '最近 7 天' },
{ value: 'week', label: '本周' },
@@ -119,7 +125,7 @@
// 过滤支出记录
const expenseRecords = records.filter(r => {
if (r.income_expense !== '支出') return false;
const recordDate = new Date(r.time.split(' ')[0]);
const recordDate = new Date(extractDateStr(r.time));
return recordDate >= cutoffDate;
});
@@ -130,7 +136,7 @@
const categoryTotals: Record<string, number> = {};
expenseRecords.forEach(record => {
const dateStr = record.time.split(' ')[0];
const dateStr = extractDateStr(record.time);
const category = record.category || '其他';
const amount = parseFloat(record.amount) || 0;
@@ -526,7 +532,7 @@
selectedDate = clickedDate;
selectedDateRecords = records.filter(r => {
if (r.income_expense !== '支出') return false;
const recordDateStr = r.time.split(' ')[0];
const recordDateStr = extractDateStr(r.time);
return recordDateStr === dateStr;
});
@@ -641,7 +647,7 @@
<!-- 趋势图 (自定义 SVG) -->
<div class="relative w-full" style="aspect-ratio: {chartWidth}/{chartHeight};">
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex a11y_no_noninteractive_element_interactions a11y_click_events_have_key_events -->
<svg
viewBox="0 0 {chartWidth} {chartHeight}"
class="w-full h-full cursor-pointer outline-none focus:outline-none"
@@ -827,25 +833,25 @@
</Card.Root>
{/if}
<!-- 当日详情 Dialog -->
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="w-fit min-w-[500px] max-w-[90vw] max-h-[80vh] overflow-hidden flex flex-col">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<!-- 当日详情 Drawer -->
<Drawer.Root bind:open={dialogOpen}>
<Drawer.Content class="sm:max-w-4xl">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<Calendar class="h-5 w-5" />
{#if selectedDate}
{selectedDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' })}
{/if}
</Dialog.Title>
<Dialog.Description>
</Drawer.Title>
<Drawer.Description>
{#if selectedDateStats}
{@const stats = selectedDateStats}
{stats!.count} 笔支出,合计 ¥{stats!.total.toFixed(2)}
{/if}
</Dialog.Description>
</Dialog.Header>
</Drawer.Description>
</Drawer.Header>
<div class="flex-1 overflow-auto py-4">
<div class="flex-1 overflow-auto py-4 px-4 md:px-0">
{#if selectedDateStats}
{@const stats = selectedDateStats}
@@ -886,5 +892,5 @@
<p class="text-center text-muted-foreground py-8">暂无数据</p>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>
</Drawer.Content>
</Drawer.Root>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import * as Dialog from '$lib/components/ui/dialog';
import * as Drawer from '$lib/components/ui/drawer';
import * as Select from '$lib/components/ui/select';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -134,10 +134,10 @@
</Card.Root>
<!-- 账单详情弹窗 -->
<Dialog.Root bind:open={dialogOpen}>
<Dialog.Content class="sm:max-w-[450px]">
<Dialog.Header>
<Dialog.Title class="flex items-center gap-2">
<Drawer.Root bind:open={dialogOpen}>
<Drawer.Content class="sm:max-w-[450px]">
<Drawer.Header>
<Drawer.Title class="flex items-center gap-2">
<Receipt class="h-5 w-5" />
{isEditing ? '编辑账单' : '账单详情'}
{#if selectedRank <= 3 && !isEditing}
@@ -149,16 +149,16 @@
Top {selectedRank}
</span>
{/if}
</Dialog.Title>
<Dialog.Description>
</Drawer.Title>
<Drawer.Description>
{isEditing ? '修改这笔支出的信息' : '查看这笔支出的完整信息'}
</Dialog.Description>
</Dialog.Header>
</Drawer.Description>
</Drawer.Header>
{#if selectedRecord}
{#if isEditing}
<!-- 编辑模式 -->
<div class="py-4 space-y-4">
<div class="py-4 space-y-4 px-4 md:px-0">
<div class="space-y-2">
<Label for="amount">金额</Label>
<div class="relative">
@@ -206,7 +206,7 @@
</div>
{:else}
<!-- 查看模式 -->
<div class="py-4 space-y-4">
<div class="py-4 space-y-4 px-4 md:px-0">
<!-- 金额 -->
<div class="text-center py-4 bg-red-50 dark:bg-red-950/30 rounded-lg">
<p class="text-sm text-muted-foreground mb-1">支出金额</p>
@@ -265,7 +265,7 @@
{/if}
{/if}
<Dialog.Footer class="flex gap-2">
<Drawer.Footer class="flex gap-2">
{#if isEditing}
<Button variant="outline" onclick={cancelEdit}>
<X class="h-4 w-4 mr-2" />
@@ -284,6 +284,6 @@
编辑
</Button>
{/if}
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Root>