317 lines
11 KiB
JavaScript
317 lines
11 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { getRequestLogs, getRequestLogStats } from '../api';
|
|
import RequestLogDetailModal from './RequestLogDetailModal';
|
|
|
|
// 自定义防抖 Hook
|
|
const useDebounce = (value, delay) => {
|
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedValue(value);
|
|
}, delay);
|
|
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [value, delay]);
|
|
|
|
return debouncedValue;
|
|
};
|
|
|
|
const RequestLogList = () => {
|
|
const [logs, setLogs] = useState([]);
|
|
const [stats, setStats] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize] = useState(20);
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
const [selectedLog, setSelectedLog] = useState(null);
|
|
const [showDetailModal, setShowDetailModal] = useState(false);
|
|
|
|
// 筛选条件
|
|
const [filters, setFilters] = useState({
|
|
virtualModel: '',
|
|
backendModel: '',
|
|
startDate: '',
|
|
endDate: ''
|
|
});
|
|
|
|
// 使用防抖的筛选条件
|
|
const debouncedFilters = useDebounce(filters, 500);
|
|
|
|
const fetchLogs = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const params = {
|
|
page,
|
|
page_size: pageSize,
|
|
...debouncedFilters
|
|
};
|
|
|
|
// 移除空值
|
|
Object.keys(params).forEach(key => {
|
|
if (!params[key]) delete params[key];
|
|
});
|
|
|
|
const data = await getRequestLogs(params);
|
|
setLogs(data.logs || []);
|
|
setTotalPages(Math.ceil((data.total || 0) / pageSize));
|
|
setError(null);
|
|
} catch (err) {
|
|
setError(err.message || '获取日志失败');
|
|
console.error('Error fetching logs:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
const data = await getRequestLogStats(debouncedFilters);
|
|
setStats(data);
|
|
} catch (err) {
|
|
console.error('Error fetching stats:', err);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchLogs();
|
|
fetchStats();
|
|
}, [page, debouncedFilters]);
|
|
|
|
const handleFilterChange = (key, value) => {
|
|
setFilters(prev => ({ ...prev, [key]: value }));
|
|
setPage(1); // 重置到第一页
|
|
};
|
|
|
|
const handleViewDetail = (log) => {
|
|
setSelectedLog(log);
|
|
setShowDetailModal(true);
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
};
|
|
|
|
const formatCost = (cost) => {
|
|
if (cost === null || cost === undefined) return 'N/A';
|
|
return `¥${parseFloat(cost).toFixed(6)}`;
|
|
};
|
|
|
|
if (loading && logs.length === 0) {
|
|
return (
|
|
<div className="flex justify-center items-center py-8">
|
|
<div className="text-gray-600">加载中...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 统计信息卡片 */}
|
|
{stats && (
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500">总请求数</div>
|
|
<div className="text-2xl font-bold text-gray-900">{stats.total_requests || 0}</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500">总Token数</div>
|
|
<div className="text-2xl font-bold text-blue-600">{stats.total_tokens || 0}</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500">总成本</div>
|
|
<div className="text-2xl font-bold text-green-600">{formatCost(stats.total_cost)}</div>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<div className="text-sm text-gray-500">平均响应时间</div>
|
|
<div className="text-2xl font-bold text-purple-600">
|
|
{stats.avg_response_time ? `${stats.avg_response_time.toFixed(2)}s` : 'N/A'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 筛选器 */}
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
虚拟模型
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={filters.virtualModel}
|
|
onChange={(e) => handleFilterChange('virtualModel', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="筛选虚拟模型"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
后端模型
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={filters.backendModel}
|
|
onChange={(e) => handleFilterChange('backendModel', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
placeholder="筛选后端模型"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
开始时间
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={filters.startDate}
|
|
onChange={(e) => handleFilterChange('startDate', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
结束时间
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={filters.endDate}
|
|
onChange={(e) => handleFilterChange('endDate', e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 错误提示 */}
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* 日志表格 */}
|
|
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
时间
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
虚拟模型
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
后端模型
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
运营商
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
请求Token
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
响应Token
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
成本
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
操作
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{logs.length === 0 ? (
|
|
<tr>
|
|
<td colSpan="8" className="px-6 py-4 text-center text-gray-500">
|
|
暂无日志记录
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
logs.map((log, index) => (
|
|
<tr key={log.ID || index} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatDate(log.RequestTimestamp)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{log.VirtualModelName || 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{log.BackendModelName || 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{log.ProviderName || 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{log.RequestTokens || 0}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{log.ResponseTokens || 0}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{formatCost(log.Cost)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
|
<button
|
|
onClick={() => handleViewDetail(log)}
|
|
className="text-blue-600 hover:text-blue-800 font-medium transition-colors"
|
|
>
|
|
查看详情
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 分页 */}
|
|
{totalPages > 1 && (
|
|
<div className="flex justify-center items-center space-x-2">
|
|
<button
|
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
disabled={page === 1}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
上一页
|
|
</button>
|
|
<span className="text-sm text-gray-700">
|
|
第 {page} / {totalPages} 页
|
|
</span>
|
|
<button
|
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
|
disabled={page === totalPages}
|
|
className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
下一页
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 详情查看模态框 */}
|
|
<RequestLogDetailModal
|
|
log={selectedLog}
|
|
isOpen={showDetailModal}
|
|
onClose={() => setShowDetailModal(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RequestLogList; |