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:
96
frontend/src/pages/Stats/StatsTable.tsx
Normal file
96
frontend/src/pages/Stats/StatsTable.tsx
Normal 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 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
13
frontend/src/pages/Stats/index.tsx
Normal file
13
frontend/src/pages/Stats/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user