refactor: 迁移 UI 组件库从 Ant Design 至 TDesign
- 替换 antd 为 tdesign-react 作为主要 UI 组件库 - 引入 Recharts 替代 @ant-design/charts 实现图表功能 - 移除主题系统相关代码(ThemeContext、themes 目录) - 更新所有组件以适配 TDesign 组件 API - 更新测试用例以匹配新的组件实现 - 新增 TDesign 和 Recharts 集成规范文档
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
import { Button, Result } from 'antd';
|
||||
import { Button } from 'tdesign-react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="抱歉,您访问的页面不存在。"
|
||||
extra={
|
||||
<Button color="primary" variant="solid" onClick={() => navigate('/providers')}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
padding: '2rem',
|
||||
}}>
|
||||
<h1 style={{ fontSize: '6rem', margin: 0, color: '#999' }}>404</h1>
|
||||
<p style={{ fontSize: '1.25rem', color: '#666', marginBottom: '2rem' }}>
|
||||
抱歉,您访问的页面不存在。
|
||||
</p>
|
||||
<Button theme="primary" onClick={() => navigate('/providers')}>
|
||||
返回首页
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Modal, Form, Input, Select, Switch } from 'antd';
|
||||
import { Dialog, Form, Input, Select, Switch } from 'tdesign-react';
|
||||
import type { Provider, Model } from '@/types';
|
||||
import type { SubmitContext } from 'tdesign-react/es/form/type';
|
||||
|
||||
interface ModelFormValues {
|
||||
id: string;
|
||||
@@ -28,12 +29,14 @@ export function ModelForm({
|
||||
onCancel,
|
||||
loading,
|
||||
}: ModelFormProps) {
|
||||
const [form] = Form.useForm<ModelFormValues>();
|
||||
const [form] = Form.useForm();
|
||||
const isEdit = !!model;
|
||||
|
||||
// 当弹窗打开或model变化时,设置表单值
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (open && form) {
|
||||
if (model) {
|
||||
// 编辑模式:设置现有值
|
||||
form.setFieldsValue({
|
||||
id: model.id,
|
||||
providerId: model.providerId,
|
||||
@@ -41,29 +44,40 @@ export function ModelForm({
|
||||
enabled: model.enabled,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ providerId });
|
||||
// 新增模式:重置表单并设置默认providerId
|
||||
form.reset();
|
||||
form.setFieldsValue({
|
||||
providerId,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [open, model, providerId, form]);
|
||||
}, [open, model, providerId]); // 移除form依赖,避免循环
|
||||
|
||||
const handleSubmit = (context: SubmitContext) => {
|
||||
if (context.validateResult === true && form) {
|
||||
const values = form.getFieldsValue(true) as ModelFormValues;
|
||||
onSave(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? '编辑模型' : '添加模型'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
<Dialog
|
||||
header={isEdit ? '编辑模型' : '添加模型'}
|
||||
visible={open}
|
||||
onConfirm={() => { form?.submit(); return false; }}
|
||||
onClose={onCancel}
|
||||
confirmLoading={loading}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
destroyOnHidden
|
||||
confirmBtn="保存"
|
||||
cancelBtn="取消"
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={onSave} initialValues={{ enabled: true }}>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true, message: '请输入模型 ID' }]}>
|
||||
<Form form={form} layout="vertical" onSubmit={handleSubmit}>
|
||||
<Form.FormItem label="ID" name="id" rules={[{ required: true, message: '请输入模型 ID' }]}>
|
||||
<Input disabled={isEdit} placeholder="例如: gpt-4o" />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item
|
||||
<Form.FormItem
|
||||
label="供应商"
|
||||
name="providerId"
|
||||
rules={[{ required: true, message: '请选择供应商' }]}
|
||||
@@ -71,20 +85,20 @@ export function ModelForm({
|
||||
<Select
|
||||
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item
|
||||
<Form.FormItem
|
||||
label="模型名称"
|
||||
name="modelName"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: gpt-4o" />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||
<Form.FormItem label="启用" name="enabled">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Table, Tag, Popconfirm, Space } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Button, Table, Tag, Popconfirm, Space } from 'tdesign-react';
|
||||
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
|
||||
import type { Model } from '@/types';
|
||||
import { useModels, useDeleteModel } from '@/hooks/useModels';
|
||||
|
||||
@@ -13,39 +13,35 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
const { data: models = [], isLoading } = useModels(providerId);
|
||||
const deleteModel = useDeleteModel();
|
||||
|
||||
const columns: ColumnsType<Model> = [
|
||||
const columns: PrimaryTableCol<Model>[] = [
|
||||
{
|
||||
title: '模型名称',
|
||||
dataIndex: 'modelName',
|
||||
key: 'modelName',
|
||||
ellipsis: { showTitle: true },
|
||||
colKey: 'modelName',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled: boolean) =>
|
||||
enabled ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
|
||||
colKey: 'enabled',
|
||||
width: 80,
|
||||
cell: ({ row }) =>
|
||||
row.enabled ? <Tag theme="success">启用</Tag> : <Tag theme="danger">禁用</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
colKey: 'action',
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
{onEdit && (
|
||||
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||
<Button variant="text" size="small" onClick={() => onEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定要删除这个模型吗?"
|
||||
onConfirm={() => deleteModel.mutate(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
content="确定要删除这个模型吗?"
|
||||
onConfirm={() => deleteModel.mutate(row.id)}
|
||||
>
|
||||
<Button variant="link" color="danger" size="small">
|
||||
<Button variant="text" theme="danger" size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -59,19 +55,19 @@ export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) {
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span style={{ fontWeight: 500 }}>关联模型 ({models.length})</span>
|
||||
{onAdd && (
|
||||
<Button variant="link" size="small" onClick={onAdd}>
|
||||
<Button variant="text" size="small" onClick={onAdd}>
|
||||
添加模型
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Table<Model>
|
||||
columns={columns}
|
||||
dataSource={models}
|
||||
data={models}
|
||||
rowKey="id"
|
||||
loading={isLoading}
|
||||
pagination={false}
|
||||
pagination={undefined}
|
||||
size="small"
|
||||
locale={{ emptyText: '暂无模型,点击上方按钮添加' }}
|
||||
empty="暂无模型,点击上方按钮添加"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Modal, Form, Input, Switch } from 'antd';
|
||||
import { Dialog, Form, Input, Switch } from 'tdesign-react';
|
||||
import type { Provider } from '@/types';
|
||||
import type { SubmitContext } from 'tdesign-react/es/form/type';
|
||||
|
||||
interface ProviderFormValues {
|
||||
id: string;
|
||||
@@ -25,12 +26,14 @@ export function ProviderForm({
|
||||
onCancel,
|
||||
loading,
|
||||
}: ProviderFormProps) {
|
||||
const [form] = Form.useForm<ProviderFormValues>();
|
||||
const [form] = Form.useForm();
|
||||
const isEdit = !!provider;
|
||||
|
||||
// 当弹窗打开或provider变化时,设置表单值
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (open && form) {
|
||||
if (provider) {
|
||||
// 编辑模式:设置现有值
|
||||
form.setFieldsValue({
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
@@ -39,54 +42,63 @@ export function ProviderForm({
|
||||
enabled: provider.enabled,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
// 新增模式:重置表单
|
||||
form.reset();
|
||||
form.setFieldsValue({ enabled: true });
|
||||
}
|
||||
}
|
||||
}, [open, provider, form]);
|
||||
}, [open, provider]); // 移除form依赖,避免循环
|
||||
|
||||
const handleSubmit = (context: SubmitContext) => {
|
||||
if (context.validateResult === true && form) {
|
||||
const values = form.getFieldsValue(true) as ProviderFormValues;
|
||||
onSave(values);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={isEdit ? '编辑供应商' : '添加供应商'}
|
||||
open={open}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
<Dialog
|
||||
header={isEdit ? '编辑供应商' : '添加供应商'}
|
||||
visible={open}
|
||||
onConfirm={() => { form?.submit(); return false; }}
|
||||
onClose={onCancel}
|
||||
confirmLoading={loading}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
destroyOnHidden
|
||||
confirmBtn="保存"
|
||||
cancelBtn="取消"
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={onSave} initialValues={{ enabled: true }}>
|
||||
<Form.Item label="ID" name="id" rules={[{ required: true, message: '请输入供应商 ID' }]}>
|
||||
<Form form={form} layout="vertical" onSubmit={handleSubmit}>
|
||||
<Form.FormItem label="ID" name="id" rules={[{ required: true, message: '请输入供应商 ID' }]}>
|
||||
<Input disabled={isEdit} placeholder="例如: openai" />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Form.FormItem label="名称" name="name" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="例如: OpenAI" />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item
|
||||
<Form.FormItem
|
||||
label={isEdit ? 'API Key(留空则不修改)' : 'API Key'}
|
||||
name="apiKey"
|
||||
rules={isEdit ? [] : [{ required: true, message: '请输入 API Key' }]}
|
||||
>
|
||||
<Input.Password placeholder="sk-..." />
|
||||
</Form.Item>
|
||||
<Input type="password" placeholder="sk-..." autocomplete="current-password" />
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item
|
||||
<Form.FormItem
|
||||
label="Base URL"
|
||||
name="baseUrl"
|
||||
rules={[
|
||||
{ required: true, message: '请输入 Base URL' },
|
||||
{ type: 'url', message: '请输入有效的 URL' },
|
||||
{ url: true, message: '请输入有效的 URL' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如: https://api.openai.com/v1" />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
|
||||
<Form.Item label="启用" name="enabled" valuePropName="checked">
|
||||
<Form.FormItem label="启用" name="enabled">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form.FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Table, Tag, Popconfirm, Space, Card, Tooltip } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Button, Table, Tag, Popconfirm, Space, Card, Tooltip } from 'tdesign-react';
|
||||
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
|
||||
import type { Provider, Model } from '@/types';
|
||||
import { ModelTable } from './ModelTable';
|
||||
|
||||
@@ -28,56 +28,50 @@ export function ProviderTable({
|
||||
onAddModel,
|
||||
onEditModel,
|
||||
}: ProviderTableProps) {
|
||||
const columns: ColumnsType<Provider> = [
|
||||
const columns: PrimaryTableCol<Provider>[] = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
colKey: 'name',
|
||||
width: 180,
|
||||
ellipsis: { showTitle: true },
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Base URL',
|
||||
dataIndex: 'baseUrl',
|
||||
key: 'baseUrl',
|
||||
ellipsis: { showTitle: true },
|
||||
colKey: 'baseUrl',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'API Key',
|
||||
dataIndex: 'apiKey',
|
||||
key: 'apiKey',
|
||||
colKey: 'apiKey',
|
||||
width: 120,
|
||||
ellipsis: { showTitle: true },
|
||||
render: (key: string | null | undefined) => (
|
||||
<Tooltip title={maskApiKey(key)}>
|
||||
<span>{maskApiKey(key)}</span>
|
||||
ellipsis: true,
|
||||
cell: ({ row }) => (
|
||||
<Tooltip content={maskApiKey(row.apiKey)}>
|
||||
<span>{maskApiKey(row.apiKey)}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled: boolean) =>
|
||||
enabled ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>,
|
||||
colKey: 'enabled',
|
||||
width: 80,
|
||||
cell: ({ row }) =>
|
||||
row.enabled ? <Tag theme="success">启用</Tag> : <Tag theme="danger">禁用</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
colKey: 'action',
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
cell: ({ row }) => (
|
||||
<Space>
|
||||
<Button variant="link" size="small" onClick={() => onEdit(record)}>
|
||||
<Button variant="text" size="small" onClick={() => onEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个供应商吗?关联的模型也会被删除。"
|
||||
onConfirm={() => onDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
content="确定要删除这个供应商吗?关联的模型也会被删除。"
|
||||
onConfirm={() => onDelete(row.id)}
|
||||
>
|
||||
<Button variant="link" color="danger" size="small">
|
||||
<Button variant="text" theme="danger" size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -89,29 +83,26 @@ export function ProviderTable({
|
||||
return (
|
||||
<Card
|
||||
title="供应商列表"
|
||||
extra={
|
||||
<Button color="primary" variant="solid" onClick={onAdd}>
|
||||
actions={
|
||||
<Button theme="primary" onClick={onAdd}>
|
||||
添加供应商
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table<Provider>
|
||||
columns={columns}
|
||||
dataSource={providers}
|
||||
data={providers}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
expandable={{
|
||||
expandedRowRender: (record) => (
|
||||
<ModelTable
|
||||
providerId={record.id}
|
||||
onAdd={() => onAddModel(record.id)}
|
||||
onEdit={onEditModel}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={false}
|
||||
scroll={{ x: 840 }}
|
||||
locale={{ emptyText: '暂无供应商,点击上方按钮添加' }}
|
||||
expandedRow={({ row }) => (
|
||||
<ModelTable
|
||||
providerId={row.id}
|
||||
onAdd={() => onAddModel(row.id)}
|
||||
onEdit={onEditModel}
|
||||
/>
|
||||
)}
|
||||
pagination={undefined}
|
||||
empty="暂无供应商,点击上方按钮添加"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types';
|
||||
import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders';
|
||||
import { useCreateModel, useUpdateModel } from '@/hooks/useModels';
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
import { Card, Select, Switch, Space, Typography } from 'antd';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { themeOptions, type ThemeId } from '@/themes';
|
||||
|
||||
const { Text } = Typography;
|
||||
import { Card } from 'tdesign-react';
|
||||
|
||||
export function SettingsPage() {
|
||||
const { themeId, followSystem, setThemeId, setFollowSystem } = useTheme();
|
||||
|
||||
return (
|
||||
<Space vertical size="large" style={{ width: '100%' }}>
|
||||
<Card title="主题">
|
||||
<Space vertical size="middle" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Text strong>主题</Text>
|
||||
</div>
|
||||
<Select
|
||||
value={themeId}
|
||||
onChange={(value: ThemeId) => setThemeId(value)}
|
||||
style={{ width: 180 }}
|
||||
options={themeOptions.map((opt) => ({
|
||||
value: opt.id,
|
||||
label: opt.label,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<Text strong>跟随系统</Text>
|
||||
<br />
|
||||
<Text type="secondary">开启后自动跟随系统暗色模式</Text>
|
||||
</div>
|
||||
<Switch checked={followSystem} onChange={setFollowSystem} />
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
<Card title="设置">
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
||||
设置功能开发中...
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Row, Col, Card, Statistic } from 'antd';
|
||||
import { Row, Col, Card, Statistic } from 'tdesign-react';
|
||||
import type { UsageStats } from '@/types';
|
||||
|
||||
interface StatCardsProps {
|
||||
@@ -17,22 +17,22 @@ export function StatCards({ stats }: StatCardsProps) {
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="总请求量" value={totalRequests} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="活跃模型数" value={activeModels} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="活跃供应商数" value={activeProviders} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Col xs={12} md={6}>
|
||||
<Card>
|
||||
<Statistic title="今日请求量" value={todayRequests} />
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 { Table, Select, Input, DateRangePicker, Space, Card } from 'tdesign-react';
|
||||
import type { PrimaryTableCol } from 'tdesign-react/es/table/type';
|
||||
import type { UsageStats, Provider } from '@/types';
|
||||
|
||||
interface StatsTableProps {
|
||||
@@ -10,10 +9,10 @@ interface StatsTableProps {
|
||||
loading: boolean;
|
||||
providerId?: string;
|
||||
modelName?: string;
|
||||
dateRange: [Dayjs | null, Dayjs | null] | null;
|
||||
dateRange: [Date | null, Date | null] | null;
|
||||
onProviderIdChange: (value: string | undefined) => void;
|
||||
onModelNameChange: (value: string | undefined) => void;
|
||||
onDateRangeChange: (dates: [Dayjs | null, Dayjs | null] | null) => void;
|
||||
onDateRangeChange: (dates: [Date | null, Date | null] | null) => void;
|
||||
}
|
||||
|
||||
export function StatsTable({
|
||||
@@ -35,69 +34,76 @@ export function StatsTable({
|
||||
return map;
|
||||
}, [providers]);
|
||||
|
||||
const columns: ColumnsType<UsageStats> = [
|
||||
const columns: PrimaryTableCol<UsageStats>[] = [
|
||||
{
|
||||
title: '供应商',
|
||||
dataIndex: 'providerId',
|
||||
key: 'providerId',
|
||||
colKey: 'providerId',
|
||||
width: 180,
|
||||
ellipsis: { showTitle: true },
|
||||
render: (id: string) => providerMap.get(id) ?? id,
|
||||
ellipsis: true,
|
||||
cell: ({ row }) => providerMap.get(row.providerId) ?? row.providerId,
|
||||
},
|
||||
{
|
||||
title: '模型',
|
||||
dataIndex: 'modelName',
|
||||
key: 'modelName',
|
||||
colKey: 'modelName',
|
||||
width: 250,
|
||||
ellipsis: { showTitle: true },
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
colKey: 'date',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '请求数',
|
||||
dataIndex: 'requestCount',
|
||||
key: 'requestCount',
|
||||
colKey: 'requestCount',
|
||||
width: 100,
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const handleDateChange = (value: unknown) => {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
// 将值转换为Date对象
|
||||
const startDate = value[0] ? new Date(value[0] as string | number | Date) : null;
|
||||
const endDate = value[1] ? new Date(value[1] as string | number | Date) : null;
|
||||
onDateRangeChange([startDate, endDate]);
|
||||
} else {
|
||||
onDateRangeChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="统计数据">
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Select
|
||||
allowClear
|
||||
clearable
|
||||
placeholder="所有供应商"
|
||||
style={{ width: 200 }}
|
||||
value={providerId}
|
||||
onChange={(value) => onProviderIdChange(value)}
|
||||
onChange={(value) => onProviderIdChange(value as string | undefined)}
|
||||
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
clearable
|
||||
placeholder="模型名称"
|
||||
style={{ width: 200 }}
|
||||
value={modelName ?? ''}
|
||||
onChange={(e) => onModelNameChange(e.target.value || undefined)}
|
||||
onChange={(value) => onModelNameChange((value as string) || undefined)}
|
||||
/>
|
||||
<DatePicker.RangePicker
|
||||
value={dateRange}
|
||||
onChange={(dates) => onDateRangeChange(dates)}
|
||||
<DateRangePicker
|
||||
mode="date"
|
||||
value={dateRange && dateRange[0] && dateRange[1] ? [dateRange[0], dateRange[1]] : []}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Table<UsageStats>
|
||||
columns={columns}
|
||||
dataSource={stats}
|
||||
data={stats}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 20 }}
|
||||
scroll={{ x: 650 }}
|
||||
locale={{ emptyText: '暂无统计数据' }}
|
||||
empty="暂无统计数据"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card } from 'antd';
|
||||
import { Line } from '@ant-design/charts';
|
||||
import { Card } from 'tdesign-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
import type { UsageStats } from '@/types';
|
||||
|
||||
interface UsageChartProps {
|
||||
@@ -16,17 +16,24 @@ export function UsageChart({ stats }: UsageChartProps) {
|
||||
.map(([date, requestCount]) => ({ date, requestCount }))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
|
||||
const config = {
|
||||
data: chartData,
|
||||
xField: 'date',
|
||||
yField: 'requestCount',
|
||||
smooth: true,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title="请求趋势" style={{ marginBottom: 16 }}>
|
||||
{chartData.length > 0 ? (
|
||||
<Line {...config} />
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="requestCount"
|
||||
stroke="#0052D9"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#0052D9' }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
||||
暂无数据
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { useProviders } from '@/hooks/useProviders';
|
||||
import { useStats } from '@/hooks/useStats';
|
||||
import { StatCards } from './StatCards';
|
||||
@@ -11,14 +10,14 @@ export function StatsPage() {
|
||||
|
||||
const [providerId, setProviderId] = useState<string | undefined>();
|
||||
const [modelName, setModelName] = useState<string | undefined>();
|
||||
const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null);
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null] | null>(null);
|
||||
|
||||
const params = useMemo(
|
||||
() => ({
|
||||
providerId,
|
||||
modelName,
|
||||
startDate: dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
startDate: dateRange?.[0]?.toISOString().split('T')[0],
|
||||
endDate: dateRange?.[1]?.toISOString().split('T')[0],
|
||||
}),
|
||||
[providerId, modelName, dateRange],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user