1
0

feat: 完成前端重构,采用 Ant Design 5 和完整测试体系

- 采用 Ant Design 5 作为 UI 组件库,替换自定义组件
- 集成 React Router v7 提供路由导航
- 使用 TanStack Query v5 管理数据获取和缓存
- 建立 Vitest + React Testing Library 测试体系
- 添加 Playwright E2E 测试覆盖
- 使用 MSW mock API 响应
- 配置 TypeScript strict 模式
- 采用 SCSS Modules 组织样式
- 更新 OpenSpec 规格以反映前端架构变更
- 归档 frontend-refactor 变更记录
This commit is contained in:
2026-04-16 11:21:48 +08:00
parent c17903dcbc
commit 9359ca7f62
61 changed files with 4588 additions and 1095 deletions

View File

@@ -0,0 +1,96 @@
import { useState, 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[];
}
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);
const providerMap = useMemo(() => {
const map = new Map<string, string>();
for (const p of providers) {
map.set(p.id, p.name);
}
return map;
}, [providers]);
const columns: ColumnsType<UsageStats> = [
{
title: '供应商',
dataIndex: 'providerId',
key: 'providerId',
render: (id: string) => providerMap.get(id) ?? id,
},
{
title: '模型',
dataIndex: 'modelName',
key: 'modelName',
},
{
title: '日期',
dataIndex: 'date',
key: 'date',
},
{
title: '请求数',
dataIndex: 'requestCount',
key: 'requestCount',
},
];
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>
<Table<UsageStats>
columns={columns}
dataSource={stats}
rowKey="id"
loading={isLoading}
pagination={{ pageSize: 20 }}
/>
</>
);
}

View File

@@ -0,0 +1,13 @@
import { useProviders } from '@/hooks/useProviders';
import { StatsTable } from './StatsTable';
export function StatsPage() {
const { data: providers = [] } = useProviders();
return (
<div>
<h1></h1>
<StatsTable providers={providers} />
</div>
);
}