feat(analysis): 增强图表交互功能

- 分类支出排行: 饼图支持点击类别切换显示/隐藏,百分比动态重新计算
- 每日支出趋势: 图例支持点击切换类别显示,隐藏类别不参与堆叠计算
- Dialog列表: 添加列排序功能(时间/商家/描述/金额)
- Dialog列表: 添加分页功能,每页10条(分类)/8条(每日)
- 饼图hover效果: 扇形放大、阴影增强、中心显示详情
This commit is contained in:
clz
2026-01-08 02:55:54 +08:00
parent c40a118a3d
commit 9d409d6a93
161 changed files with 9155 additions and 0 deletions

View File

@@ -0,0 +1,235 @@
<script lang="ts">
import '../app.css';
import { page } from '$app/stores';
import * as Sidebar from '$lib/components/ui/sidebar';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Avatar from '$lib/components/ui/avatar';
import { Separator } from '$lib/components/ui/separator';
import Upload from '@lucide/svelte/icons/upload';
import ClipboardCheck from '@lucide/svelte/icons/clipboard-check';
import FileText from '@lucide/svelte/icons/file-text';
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 Moon from '@lucide/svelte/icons/moon';
import Sun from '@lucide/svelte/icons/sun';
import ChevronsUpDown from '@lucide/svelte/icons/chevrons-up-down';
let { children } = $props();
let darkMode = $state(false);
function toggleDarkMode() {
darkMode = !darkMode;
if (darkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
const mainNavItems = [
{ href: '/', label: '上传', icon: Upload },
{ href: '/review', label: '复核', icon: ClipboardCheck },
{ href: '/bills', label: '账单', icon: FileText },
{ href: '/analysis', label: '分析', icon: BarChart3 },
];
function isActive(href: string, pathname: string): boolean {
if (href === '/') return pathname === '/';
return pathname.startsWith(href);
}
</script>
<Sidebar.Provider>
<Sidebar.Root>
<Sidebar.Header>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
{...props}
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
<span class="text-lg">💰</span>
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">BillAI</span>
<span class="truncate text-xs text-muted-foreground">智能账单分析</span>
</div>
<ChevronsUpDown class="ml-auto" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-[--bits-dropdown-menu-anchor-width] min-w-56 rounded-lg"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenu.Label class="text-xs text-muted-foreground">
版本信息
</DropdownMenu.Label>
<DropdownMenu.Item>
<span class="mr-2">📦</span>
v0.1.0 Beta
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>主要功能</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each mainNavItems as item}
<Sidebar.MenuItem>
<Sidebar.MenuButton
isActive={isActive(item.href, $page.url.pathname)}
>
{#snippet child({ props })}
<a href={item.href} {...props}>
<item.icon class="size-4" />
<span>{item.label}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>系统</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<button {...props} onclick={toggleDarkMode}>
{#if darkMode}
<Sun class="size-4" />
<span>浅色模式</span>
{:else}
<Moon class="size-4" />
<span>深色模式</span>
{/if}
</button>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href="/settings" {...props}>
<Settings class="size-4" />
<span>设置</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href="/help" {...props}>
<HelpCircle class="size-4" />
<span>帮助</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
</Sidebar.Content>
<Sidebar.Footer>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
{...props}
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar.Root class="h-8 w-8 rounded-lg">
<Avatar.Fallback class="rounded-lg bg-gradient-to-br from-primary to-chart-1 text-primary-foreground">
U
</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">用户</span>
<span class="truncate text-xs text-muted-foreground">user@example.com</span>
</div>
<ChevronsUpDown class="ml-auto size-4" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-[--bits-dropdown-menu-anchor-width] min-w-56 rounded-lg"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenu.Label class="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar.Root class="h-8 w-8 rounded-lg">
<Avatar.Fallback class="rounded-lg bg-gradient-to-br from-primary to-chart-1 text-primary-foreground">
U
</Avatar.Fallback>
</Avatar.Root>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">用户</span>
<span class="truncate text-xs text-muted-foreground">user@example.com</span>
</div>
</div>
</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item>
<Settings class="mr-2 size-4" />
账户设置
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>
<Sidebar.Inset>
<header class="flex h-16 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>
</div>
</header>
<main class="flex-1 p-6">
{@render children()}
</main>
</Sidebar.Inset>
</Sidebar.Provider>