This commit is contained in:
2026-08-06 17:47:59 +08:00
parent 1029555fdc
commit 61c3761745
7 changed files with 180 additions and 96 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-BxYDA-8S.js"></script>
<script type="module" crossorigin src="/assets/index-D4AbZER1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
</head>
<body>
+6 -2
View File
@@ -438,8 +438,12 @@ export async function deleteApiKey(id: string): Promise<void> {
await api.delete(`/admin/api-keys/${id}`);
}
export async function getApiKeyUsage(id: string, days?: number): Promise<any> {
const qs = days ? `?days=${days}` : '';
export async function getApiKeyUsage(id: string, days?: number, page?: number, pageSize?: number): Promise<any> {
const params = new URLSearchParams();
if (days) params.set('days', String(days));
if (page) params.set('page', String(page));
if (pageSize) params.set('page_size', String(pageSize));
const qs = params.toString() ? `?${params.toString()}` : '';
return api.get(`/admin/api-keys/${id}/usage${qs}`);
}
+27 -7
View File
@@ -264,9 +264,18 @@ const AdminApiKeys: React.FC = () => {
document.body.removeChild(textarea);
};
const loadUsageDetail = async (keyId: string, page = 1, pageSize = 20) => {
try {
const usage = await getApiKeyUsage(keyId, 30, page, pageSize);
setUsageModal(prev => ({ ...prev, usage }));
} catch {
message.error('加载使用统计失败');
}
};
const viewUsage = async (key: ApiKey) => {
try {
const usage = await getApiKeyUsage(key.id, 30);
const usage = await getApiKeyUsage(key.id, 30, 1, 20);
setUsageModal({ open: true, key, usage });
} catch {
message.error('加载使用统计失败');
@@ -614,15 +623,26 @@ const AdminApiKeys: React.FC = () => {
</div>
<Table
columns={[
{ title: '时间', dataIndex: 'createdAt', render: (v: string) => new Date(v).toLocaleString() },
{ title: '类型', dataIndex: 'requestType' },
{ title: '型', dataIndex: 'modelName' },
{ title: '消耗(元)', dataIndex: 'creditsCost' },
{ title: '状态', dataIndex: 'status', render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v}</Tag> },
{ title: '时间', dataIndex: 'createdAt', width: 160, render: (v: string) => v ? new Date(v).toLocaleString() : '-' },
{
title: '型', dataIndex: 'genType', width: 70,
render: (v: string) => <Tag color={v === 'video' ? 'blue' : 'green'}>{v === 'video' ? '视频' : '图片'}</Tag>,
},
{ title: '模型', dataIndex: 'modelName', width: 140, ellipsis: true },
{ title: '消耗(元)', dataIndex: 'creditsCost', width: 90, render: (v: number) => v?.toFixed(2) || '0.00' },
{ title: '状态', dataIndex: 'status', width: 70, render: (v: string) => <Tag color={v === 'success' ? 'green' : 'red'}>{v === 'success' ? '成功' : '失败'}</Tag> },
]}
dataSource={usageModal.usage.items || []}
rowKey="id"
pagination={false}
pagination={{
current: usageModal.usage.page || 1,
pageSize: usageModal.usage.pageSize || 20,
total: usageModal.usage.total || 0,
onChange: (p, ps) => loadUsageDetail(usageModal.key?.id || '', p, ps || 20),
showSizeChanger: true,
showTotal: (t) => `${t}`,
size: 'small',
}}
size="small"
/>
</div>
+51 -7
View File
@@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import {
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
} from 'antd';
import {
TableOutlined, ReloadOutlined,
TableOutlined, ReloadOutlined, SearchOutlined, ExportOutlined,
} from '@ant-design/icons';
import { getApiUsageAll } from '../api';
import dayjs from 'dayjs';
@@ -32,12 +32,12 @@ const AdminApiUsage: React.FC = () => {
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [searchCompany, setSearchCompany] = useState('');
const [searchText, setSearchText] = 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 () => {
const load = useCallback(async () => {
setLoading(true);
try {
const params: any = {
@@ -48,6 +48,7 @@ const AdminApiUsage: React.FC = () => {
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 || []);
@@ -57,15 +58,48 @@ const AdminApiUsage: React.FC = () => {
} finally {
setLoading(false);
}
};
}, [page, pageSize, filterGenType, filterStatus, dateRange, searchText]);
useEffect(() => { load(); }, [page, pageSize, filterGenType, filterStatus, dateRange]);
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.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: '时间',
@@ -80,7 +114,7 @@ const AdminApiUsage: React.FC = () => {
render: (v: string) => v || '-',
},
{
title: 'Key 前缀',
title: 'api-key',
dataIndex: 'apiKeyPrefix',
width: 110,
render: (v: string) => v ? <code style={{ background: '#f5f5f5', padding: '2px 6px', borderRadius: 4 }}>{v}</code> : '-',
@@ -150,6 +184,7 @@ const AdminApiUsage: React.FC = () => {
<Tag color="blue">: {totalCost.toFixed(2)} </Tag>
<Tag color="green">: {successCount}</Tag>
<Tag color="red">: {failedCount}</Tag>
<Button icon={<ExportOutlined />} onClick={handleExport}></Button>
<Button icon={<ReloadOutlined />} onClick={handleSearch}></Button>
</Space>
</div>
@@ -158,6 +193,15 @@ const AdminApiUsage: React.FC = () => {
{/* 筛选栏 */}
<Card variant="outlined" style={{ borderRadius: 12 }}>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
<Input
placeholder="搜索公司名或 Key 前缀"
prefix={<SearchOutlined />}
value={searchText}
onChange={e => setSearchText(e.target.value)}
onPressEnter={handleSearch}
allowClear
style={{ width: 220 }}
/>
<Select
placeholder="类型"
value={filterGenType}