import React, { useEffect, useState, useCallback } from 'react'; import { Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography, } from 'antd'; import { TableOutlined, ReloadOutlined, SearchOutlined, ExportOutlined, } 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; duration: number | null; resolution: string | null; status: string; errorMessage: string | null; errorCode: string | null; createdAt: string | null; } const AdminApiUsage: React.FC = () => { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [searchText, setSearchText] = useState(''); const [filterGenType, setFilterGenType] = useState(undefined); const [filterStatus, setFilterStatus] = useState(undefined); const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([null, null]); const load = useCallback(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(); if (searchText.trim()) params.search = searchText.trim(); const data = await getApiUsageAll(params); setItems(data?.items || []); setTotal(data?.total || 0); } catch { message.error('加载失败'); } finally { setLoading(false); } }, [page, pageSize, filterGenType, filterStatus, dateRange, searchText]); useEffect(() => { load(); }, [load]); const handleSearch = () => { setPage(1); load(); }; // 导出 CSV const handleExport = () => { const headers = ['时间', '公司', 'api-key', '类型', '模型', '时长(秒)', '分辨率', '消耗(元)', 'Token', '耗时(ms)', '状态', '错误信息']; const rows = items.map(item => [ item.createdAt ? new Date(item.createdAt).toLocaleString() : '', item.companyName || '', item.apiKeyPrefix || '', item.genType === 'video' ? '视频' : '图片', item.modelName || '', item.duration || '', item.resolution || '', (item.creditsCost || 0).toFixed(2), item.tokensUsed || '', item.requestDurationMs || '', item.status === 'success' ? '成功' : '失败', item.errorMessage || '', ]); const csvContent = [headers, ...rows] .map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const BOM = ''; const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `api_usage_${dayjs().format('YYYYMMDD_HHmmss')}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); message.success('导出成功'); }; 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: 'api-key', dataIndex: 'apiKeyPrefix', width: 110, render: (v: string) => v ? {v} : '-', }, { title: '类型', dataIndex: 'genType', width: 70, render: (v: string) => {v === 'video' ? '视频' : '图片'}, }, { title: '模型', dataIndex: 'modelName', width: 160, ellipsis: true, }, { title: '消耗(元)', dataIndex: 'creditsCost', width: 90, render: (v: number) => 0 ? '#f5222d' : '#52c41a', fontWeight: 500 }}>{v?.toFixed(2) || '0.00'}, }, { 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) => {v === 'success' ? '成功' : '失败'}, }, { title: '错误信息', dataIndex: 'errorMessage', width: 200, ellipsis: true, render: (v: string) => v ? {v} : '-', }, ]; // 统计 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 (
API 消耗列表 {total} 条
本页消耗: {totalCost.toFixed(2)} 元 成功: {successCount} 失败: {failedCount}
{/* 筛选栏 */}
} value={searchText} onChange={e => setSearchText(e.target.value)} onPressEnter={handleSearch} allowClear style={{ width: 220 }} /> { setDateRange(dates as [dayjs.Dayjs | null, dayjs.Dayjs | null]); setPage(1); }} />
{ setPage(p); setPageSize(ps || 50); }, showSizeChanger: true, showTotal: (t) => `共 ${t} 条`, }} scroll={{ x: 1300 }} /> ); }; export default AdminApiUsage;