211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
|
|
} from 'antd';
|
|
import {
|
|
TableOutlined, ReloadOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getApiUsageAll } from '../api';
|
|
import dayjs from 'dayjs';
|
|
|
|
interface UsageItem {
|
|
id: string;
|
|
apiKeyId: string;
|
|
companyName: string;
|
|
apiKeyPrefix: string | null;
|
|
taskId: string | null;
|
|
requestType: string;
|
|
modelName: string;
|
|
genType: string;
|
|
creditsCost: number;
|
|
tokensUsed: number;
|
|
requestDurationMs: number;
|
|
status: string;
|
|
errorMessage: string | null;
|
|
errorCode: string | null;
|
|
createdAt: string | null;
|
|
}
|
|
|
|
const AdminApiUsage: React.FC = () => {
|
|
const [items, setItems] = useState<UsageItem[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(50);
|
|
const [searchCompany, setSearchCompany] = useState('');
|
|
const [filterGenType, setFilterGenType] = useState<string | undefined>(undefined);
|
|
const [filterStatus, setFilterStatus] = useState<string | undefined>(undefined);
|
|
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const params: any = {
|
|
skip: (page - 1) * pageSize,
|
|
limit: pageSize,
|
|
};
|
|
if (filterGenType) params.genType = filterGenType;
|
|
if (filterStatus) params.status = filterStatus;
|
|
if (dateRange[0]) params.startDate = dateRange[0].startOf('day').toISOString();
|
|
if (dateRange[1]) params.endDate = dateRange[1].endOf('day').toISOString();
|
|
|
|
const data = await getApiUsageAll(params);
|
|
setItems(data?.items || []);
|
|
setTotal(data?.total || 0);
|
|
} catch {
|
|
message.error('加载失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { load(); }, [page, pageSize, filterGenType, filterStatus, dateRange]);
|
|
|
|
const handleSearch = () => {
|
|
setPage(1);
|
|
load();
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '时间',
|
|
dataIndex: 'createdAt',
|
|
width: 160,
|
|
render: (v: string) => v ? new Date(v).toLocaleString() : '-',
|
|
},
|
|
{
|
|
title: '公司',
|
|
dataIndex: 'companyName',
|
|
width: 120,
|
|
render: (v: string) => v || '-',
|
|
},
|
|
{
|
|
title: 'Key 前缀',
|
|
dataIndex: 'apiKeyPrefix',
|
|
width: 110,
|
|
render: (v: string) => v ? <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>{v}</code> : '-',
|
|
},
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'genType',
|
|
width: 70,
|
|
render: (v: string) => <Tag color={v === 'video' ? 'blue' : 'green'}>{v === 'video' ? '视频' : '图片'}</Tag>,
|
|
},
|
|
{
|
|
title: '模型',
|
|
dataIndex: 'modelName',
|
|
width: 160,
|
|
ellipsis: true,
|
|
},
|
|
{
|
|
title: '消耗(元)',
|
|
dataIndex: 'creditsCost',
|
|
width: 90,
|
|
render: (v: number) => <span style={{ color: v > 0 ? '#f5222d' : '#52c41a', fontWeight: 500 }}>{v?.toFixed(2) || '0.00'}</span>,
|
|
},
|
|
{
|
|
title: 'Token',
|
|
dataIndex: 'tokensUsed',
|
|
width: 80,
|
|
render: (v: number) => v || '-',
|
|
},
|
|
{
|
|
title: '耗时(ms)',
|
|
dataIndex: 'requestDurationMs',
|
|
width: 90,
|
|
render: (v: number) => v || '-',
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 80,
|
|
render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v === 'success' ? '成功' : '失败'}</Tag>,
|
|
},
|
|
{
|
|
title: '错误信息',
|
|
dataIndex: 'errorMessage',
|
|
width: 200,
|
|
ellipsis: true,
|
|
render: (v: string) => v ? <span style={{ color: '#f5222d' }}>{v}</span> : '-',
|
|
},
|
|
];
|
|
|
|
// 统计
|
|
const totalCost = items.reduce((sum, item) => sum + (item.creditsCost || 0), 0);
|
|
const successCount = items.filter(i => i.status === 'success').length;
|
|
const failedCount = items.filter(i => i.status === 'failed').length;
|
|
|
|
return (
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Space>
|
|
<div style={{ width: 36, height: 36, borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<TableOutlined style={{ color: '#fff', fontSize: 18 }} />
|
|
</div>
|
|
<Typography.Text strong style={{ fontSize: 16 }}>API 消耗列表</Typography.Text>
|
|
<Tag color="purple">{total} 条</Tag>
|
|
</Space>
|
|
<Space>
|
|
<Tag color="blue">本页消耗: {totalCost.toFixed(2)} 元</Tag>
|
|
<Tag color="green">成功: {successCount}</Tag>
|
|
<Tag color="red">失败: {failedCount}</Tag>
|
|
<Button icon={<ReloadOutlined />} onClick={handleSearch}>刷新</Button>
|
|
</Space>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 筛选栏 */}
|
|
<Card variant="outlined" style={{ borderRadius: 12 }}>
|
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
<Select
|
|
placeholder="类型"
|
|
value={filterGenType}
|
|
onChange={v => { setFilterGenType(v); setPage(1); }}
|
|
allowClear
|
|
style={{ width: 100 }}
|
|
>
|
|
<Select.Option value="video">视频</Select.Option>
|
|
<Select.Option value="image">图片</Select.Option>
|
|
</Select>
|
|
<Select
|
|
placeholder="状态"
|
|
value={filterStatus}
|
|
onChange={v => { setFilterStatus(v); setPage(1); }}
|
|
allowClear
|
|
style={{ width: 100 }}
|
|
>
|
|
<Select.Option value="success">成功</Select.Option>
|
|
<Select.Option value="failed">失败</Select.Option>
|
|
</Select>
|
|
<DatePicker.RangePicker
|
|
value={dateRange}
|
|
onChange={(dates) => { setDateRange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null]); setPage(1); }}
|
|
/>
|
|
<Button type="primary" onClick={handleSearch}>筛选</Button>
|
|
</div>
|
|
</Card>
|
|
|
|
<Card variant="outlined" style={{ borderRadius: 12 }}>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={items}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total,
|
|
onChange: (p, ps) => { setPage(p); setPageSize(ps || 50); },
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
}}
|
|
scroll={{ x: 1300 }}
|
|
/>
|
|
</Card>
|
|
</Space>
|
|
);
|
|
};
|
|
|
|
export default AdminApiUsage;
|