Files
video-gen/video-gen-admin/src/pages/AdminApiUsage.tsx
T
2026-08-06 13:55:51 +08:00

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;