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

@@ -2,6 +2,7 @@
import '../app.css';
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { checkHealth } from '$lib/api';
import * as Sidebar from '$lib/components/ui/sidebar';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Avatar from '$lib/components/ui/avatar';
@@ -14,7 +15,6 @@
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
import Settings from '@lucide/svelte/icons/settings';
import HelpCircle from '@lucide/svelte/icons/help-circle';
import Search from '@lucide/svelte/icons/search';
import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
import Wallet from '@lucide/svelte/icons/wallet';
import LogOut from '@lucide/svelte/icons/log-out';
@@ -35,16 +35,32 @@
let { children } = $props();
let themeMode = $state<ThemeMode>('system');
let serverOnline = $state(true);
let checkingHealth = $state(true);
async function checkServerHealth() {
checkingHealth = true;
serverOnline = await checkHealth();
checkingHealth = false;
}
onMount(() => {
themeMode = loadThemeFromStorage();
applyThemeToDocument(themeMode);
// 检查服务器状态
checkServerHealth();
// 每 30 秒检查一次
const healthInterval = setInterval(checkServerHealth, 30000);
// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => applyThemeToDocument(themeMode);
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
clearInterval(healthInterval);
};
});
function cycleTheme() {
@@ -78,6 +94,18 @@
if (href === '/') return pathname === '/';
return pathname.startsWith(href);
}
// 根据路径获取页面标题
function getPageTitle(pathname: string): string {
const titles: Record<string, string> = {
'/': '上传账单',
'/review': '智能复核',
'/bills': '账单管理',
'/analysis': '数据分析',
'/settings': '设置',
'/help': '帮助'
};
return titles[pathname] || 'BillAI';
}
</script>
<Sidebar.Provider>
@@ -237,18 +265,32 @@
<header class="flex h-14 shrink-0 items-center gap-2 border-b px-4">
<Sidebar.Trigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 h-4" />
<div class="flex items-center gap-2">
<Search class="size-4 text-muted-foreground" />
<span class="text-sm text-muted-foreground">搜索...</span>
</div>
<div class="ml-auto flex items-center gap-2">
<div class="flex items-center gap-1.5 text-sm">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span class="text-muted-foreground">服务运行中</span>
</div>
<h1 class="text-lg font-semibold">{getPageTitle($page.url.pathname)}</h1>
<div class="flex-1" />
<div class="flex items-center gap-3">
<button
class="flex items-center gap-1.5 text-sm hover:opacity-80 transition-opacity"
onclick={checkServerHealth}
title="点击刷新状态"
>
{#if checkingHealth}
<span class="relative flex h-2 w-2">
<span class="relative inline-flex rounded-full h-2 w-2 bg-gray-400 animate-pulse"></span>
</span>
<span class="text-muted-foreground">检查中...</span>
{:else if serverOnline}
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span class="text-muted-foreground">服务运行中</span>
{:else}
<span class="relative flex h-2 w-2">
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
</span>
<span class="text-red-500">服务离线</span>
{/if}
</button>
</div>
</header>