1
0

feat: 现代化 UI 布局,实现侧边栏导航和统计仪表盘

- 重构 AppLayout 为可折叠侧边栏导航布局
- 实现统计仪表盘:统计摘要卡片 + 请求趋势图表
- Provider 页面使用 Card 包裹优化视觉层次
- 主题切换按钮移至侧边栏底部,支持折叠态
- Header 适配暗色主题,添加分隔线优化视觉过渡
- 添加全局样式重置(SCSS)
- 完善组件测试和 E2E 测试覆盖
- 同步 OpenSpec 规范到主 specs
This commit is contained in:
2026-04-16 19:24:02 +08:00
parent 5dd26d29a7
commit 870004af23
24 changed files with 983 additions and 153 deletions

View File

@@ -1,31 +1,32 @@
import { useState, useMemo } from 'react';
import { useMemo } from 'react';
import { Table, Select, Input, DatePicker, Space, Card } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { Dayjs } from 'dayjs';
import type { UsageStats, Provider } from '@/types';
import { useStats } from '@/hooks/useStats';
interface StatsTableProps {
providers: Provider[];
stats: UsageStats[];
loading: boolean;
providerId?: string;
modelName?: string;
dateRange: [Dayjs | null, Dayjs | null] | null;
onProviderIdChange: (value: string | undefined) => void;
onModelNameChange: (value: string | undefined) => void;
onDateRangeChange: (dates: [Dayjs | null, Dayjs | null] | null) => void;
}
export function StatsTable({ providers }: StatsTableProps) {
const [providerId, setProviderId] = useState<string | undefined>();
const [modelName, setModelName] = useState<string | undefined>();
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null);
const params = useMemo(
() => ({
providerId,
modelName,
startDate: dateRange?.[0]?.format('YYYY-MM-DD'),
endDate: dateRange?.[1]?.format('YYYY-MM-DD'),
}),
[providerId, modelName, dateRange],
);
const { data: stats = [], isLoading } = useStats(params);
export function StatsTable({
providers,
stats,
loading,
providerId,
modelName,
dateRange,
onProviderIdChange,
onModelNameChange,
onDateRangeChange,
}: StatsTableProps) {
const providerMap = useMemo(() => {
const map = new Map<string, string>();
for (const p of providers) {
@@ -59,38 +60,36 @@ export function StatsTable({ providers }: StatsTableProps) {
];
return (
<>
<Card style={{ marginBottom: 16 }}>
<Space wrap>
<Select
allowClear
placeholder="所有供应商"
style={{ width: 200 }}
value={providerId}
onChange={(value) => setProviderId(value)}
options={providers.map((p) => ({ label: p.name, value: p.id }))}
/>
<Input
allowClear
placeholder="模型名称"
style={{ width: 200 }}
value={modelName ?? ''}
onChange={(e) => setModelName(e.target.value || undefined)}
/>
<DatePicker.RangePicker
value={dateRange}
onChange={(dates) => setDateRange(dates)}
/>
</Space>
</Card>
<Card title="统计数据">
<Space wrap style={{ marginBottom: 16 }}>
<Select
allowClear
placeholder="所有供应商"
style={{ width: 200 }}
value={providerId}
onChange={(value) => onProviderIdChange(value)}
options={providers.map((p) => ({ label: p.name, value: p.id }))}
/>
<Input
allowClear
placeholder="模型名称"
style={{ width: 200 }}
value={modelName ?? ''}
onChange={(e) => onModelNameChange(e.target.value || undefined)}
/>
<DatePicker.RangePicker
value={dateRange}
onChange={(dates) => onDateRangeChange(dates)}
/>
</Space>
<Table<UsageStats>
columns={columns}
dataSource={stats}
rowKey="id"
loading={isLoading}
loading={loading}
pagination={{ pageSize: 20 }}
/>
</>
</Card>
);
}