优化日志查询性能
This commit is contained in:
@@ -18,6 +18,23 @@ type APIHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// RequestLogListItem 精简的请求日志列表项(不包含RequestBody和ResponseBody)
|
||||
type RequestLogListItem struct {
|
||||
ID uint `json:"id"`
|
||||
APIKeyID uint `json:"api_key_id"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
VirtualModelName string `json:"virtual_model_name"`
|
||||
BackendModelName string `json:"backend_model_name"`
|
||||
RequestTimestamp time.Time `json:"request_timestamp"`
|
||||
ResponseTimestamp time.Time `json:"response_timestamp"`
|
||||
StatusCode int `json:"status_code,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
RequestTokens int `json:"request_tokens"`
|
||||
ResponseTokens int `json:"response_tokens"`
|
||||
Cost float64 `json:"cost"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// HealthCheckHandler 健康检查端点(无需认证)
|
||||
func (h *APIHandler) HealthCheckHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -182,7 +199,7 @@ func copyResponseHeaders(c *gin.Context, resp *http.Response) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequestLogsHandler 获取请求日志列表
|
||||
// GetRequestLogsHandler 获取请求日志列表(精简版,不包含RequestBody和ResponseBody)
|
||||
func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
|
||||
// 获取分页参数
|
||||
page := 1
|
||||
@@ -232,7 +249,7 @@ func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取日志列表
|
||||
// 获取日志列表(完整数据)
|
||||
var logs []models.RequestLog
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("request_timestamp DESC").Limit(pageSize).Offset(offset).Find(&logs).Error; err != nil {
|
||||
@@ -240,8 +257,26 @@ func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为精简DTO
|
||||
logItems := make([]RequestLogListItem, len(logs))
|
||||
for i, log := range logs {
|
||||
logItems[i] = RequestLogListItem{
|
||||
ID: log.ID,
|
||||
APIKeyID: log.APIKeyID,
|
||||
ProviderName: log.ProviderName,
|
||||
VirtualModelName: log.VirtualModelName,
|
||||
BackendModelName: log.BackendModelName,
|
||||
RequestTimestamp: log.RequestTimestamp,
|
||||
ResponseTimestamp: log.ResponseTimestamp,
|
||||
RequestTokens: log.RequestTokens,
|
||||
ResponseTokens: log.ResponseTokens,
|
||||
Cost: log.Cost,
|
||||
CreatedAt: log.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"logs": logs,
|
||||
"logs": logItems,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
@@ -249,6 +284,30 @@ func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetRequestLogDetailHandler 获取单个请求日志的完整详情(包含RequestBody和ResponseBody)
|
||||
func (h *APIHandler) GetRequestLogDetailHandler(c *gin.Context) {
|
||||
// 获取日志ID
|
||||
logID := c.Param("id")
|
||||
if logID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Log ID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// 查询日志
|
||||
var log models.RequestLog
|
||||
if err := h.DB.First(&log, logID).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Log not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch log detail"})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回完整的日志信息
|
||||
c.JSON(http.StatusOK, log)
|
||||
}
|
||||
|
||||
// GetRequestLogStatsHandler 获取请求日志统计信息
|
||||
func (h *APIHandler) GetRequestLogStatsHandler(c *gin.Context) {
|
||||
// 获取时间范围参数
|
||||
|
||||
@@ -81,6 +81,7 @@ func main() {
|
||||
// Request Logs
|
||||
api_.GET("/logs", handler.GetRequestLogsHandler)
|
||||
api_.GET("/logs/stats", handler.GetRequestLogStatsHandler)
|
||||
api_.GET("/logs/:id", handler.GetRequestLogDetailHandler)
|
||||
}
|
||||
|
||||
// 启动HTTP服务器
|
||||
|
||||
23
frontend/package-lock.json
generated
23
frontend/package-lock.json
generated
@@ -11,7 +11,8 @@
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
"react-dom": "^19.1.1",
|
||||
"react-json-view-lite": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
@@ -59,7 +60,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -1595,7 +1595,6 @@
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1637,7 +1636,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1798,7 +1796,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -2134,7 +2131,6 @@
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3252,7 +3248,6 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3279,7 +3274,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -3327,7 +3321,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
|
||||
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -3344,6 +3337,17 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-json-view-lite": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz",
|
||||
"integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
|
||||
@@ -3573,7 +3577,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
|
||||
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"@tailwindcss/vite": "^4.1.17",
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
"react-dom": "^19.1.1",
|
||||
"react-json-view-lite": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.36.0",
|
||||
|
||||
@@ -21,3 +21,14 @@ export const getRequestLogStats = async (params = {}) => {
|
||||
throw new Error(error.response?.data?.error || '获取日志统计信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取日志详情
|
||||
export const getLogDetail = async (id) => {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/logs/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取日志详情失败:', error);
|
||||
throw new Error(error.response?.data?.error || '获取日志详情失败');
|
||||
}
|
||||
};
|
||||
218
frontend/src/features/logs/components/RequestLogDetailModal.jsx
Normal file
218
frontend/src/features/logs/components/RequestLogDetailModal.jsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Modal from '../../../components/ui/Modal';
|
||||
import { getLogDetail } from '../api';
|
||||
import { JsonView } from 'react-json-view-lite';
|
||||
import 'react-json-view-lite/dist/index.css';
|
||||
|
||||
const RequestLogDetailModal = ({ log, isOpen, onClose }) => {
|
||||
const [copiedInput, setCopiedInput] = useState(false);
|
||||
const [copiedOutput, setCopiedOutput] = useState(false);
|
||||
const [detailLog, setDetailLog] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// 按需加载日志详情
|
||||
useEffect(() => {
|
||||
if (isOpen && log && log.ID) {
|
||||
const fetchLogDetail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const detailData = await getLogDetail(log.ID);
|
||||
setDetailLog(detailData);
|
||||
} catch (err) {
|
||||
setError(err.message || '获取日志详情失败');
|
||||
console.error('Error fetching log detail:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLogDetail();
|
||||
} else if (!isOpen) {
|
||||
// 弹窗关闭时重置状态
|
||||
setDetailLog(null);
|
||||
setError(null);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isOpen, log]);
|
||||
|
||||
// 格式化日期时间
|
||||
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)}`;
|
||||
};
|
||||
|
||||
// 使用 useMemo 优化 JSON 格式化
|
||||
const formatJSON = useMemo(() => {
|
||||
return (jsonString) => {
|
||||
if (!jsonString || jsonString.trim() === '') {
|
||||
return '无数据';
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (e) {
|
||||
// 如果解析失败,返回原始文本
|
||||
return jsonString;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 复制到剪贴板
|
||||
const copyToClipboard = async (text, type) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (type === 'input') {
|
||||
setCopiedInput(true);
|
||||
setTimeout(() => setCopiedInput(false), 2000);
|
||||
} else {
|
||||
setCopiedOutput(true);
|
||||
setTimeout(() => setCopiedOutput(false), 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!log) return null;
|
||||
|
||||
// 使用详情数据或基本日志数据
|
||||
const displayLog = detailLog || log;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="请求日志详情" size="xl">
|
||||
<div className="space-y-6">
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="text-gray-600">加载中...</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-gray-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">请求时间</div>
|
||||
<div className="text-sm font-medium text-gray-900">{formatDate(displayLog.RequestTimestamp)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">响应时间</div>
|
||||
<div className="text-sm font-medium text-gray-900">{formatDate(displayLog.ResponseTimestamp)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">虚拟模型</div>
|
||||
<div className="text-sm font-medium text-gray-900">{displayLog.VirtualModelName || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">后端模型</div>
|
||||
<div className="text-sm font-medium text-gray-900">{displayLog.BackendModelName || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">服务商</div>
|
||||
<div className="text-sm font-medium text-gray-900">{displayLog.ProviderName || 'N/A'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">成本</div>
|
||||
<div className="text-sm font-medium text-gray-900">{formatCost(displayLog.Cost)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">请求Tokens</div>
|
||||
<div className="text-sm font-medium text-gray-900">{displayLog.RequestTokens || 0}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-500">响应Tokens</div>
|
||||
<div className="text-sm font-medium text-gray-900">{displayLog.ResponseTokens || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 请求内容 (Input) 区域 */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">请求内容 (Input)</h3>
|
||||
<button
|
||||
onClick={() => copyToClipboard(displayLog.RequestBody || '', 'input')}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${copiedInput
|
||||
? 'bg-gray-400 text-white cursor-default'
|
||||
: 'bg-green-500 text-white hover:bg-green-600'
|
||||
}`}
|
||||
disabled={copiedInput || loading}
|
||||
>
|
||||
{copiedInput ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
{/* <div className="bg-gray-800 rounded-lg p-3 overflow-auto" style={{ maxHeight: '300px' }}>
|
||||
<pre className="text-sm text-gray-100 text-left" style={{ fontFamily: "'Courier New', monospace" }}>
|
||||
<code>{loading ? '加载中...' : formatJSON(displayLog.RequestBody)}</code>
|
||||
</pre>
|
||||
</div> */}
|
||||
<div className="bg-gray-800 rounded-lg p-3 overflow-auto" style={{ maxHeight: '300px' }}>
|
||||
{loading ? '加载中' :
|
||||
log.RequestBody ? (
|
||||
<JsonView
|
||||
data={JSON.parse(log.RequestBody)} // 解析字符串为对象
|
||||
style={{ fontFamily: "'Courier New', monospace", fontSize: '0.875rem' }} // 匹配你的text-sm
|
||||
collapsed={1} // 默认折叠1级深度(0=全展开,false=不折叠)
|
||||
collapseAll={true} // 初始全折叠
|
||||
enableClipboard={false} // 禁用内置复制(用你的按钮)
|
||||
shouldLogOnChange={false} // 禁用编辑(只读模式)
|
||||
/>
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm">无内容</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 响应内容 (Response) 区域 */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900">响应内容 (Response)</h3>
|
||||
<button
|
||||
onClick={() => copyToClipboard(displayLog.ResponseBody || '', 'output')}
|
||||
className={`px-3 py-1 text-sm rounded transition-colors ${copiedOutput
|
||||
? 'bg-gray-400 text-white cursor-default'
|
||||
: 'bg-green-500 text-white hover:bg-green-600'
|
||||
}`}
|
||||
disabled={copiedOutput || loading}
|
||||
>
|
||||
{copiedOutput ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded-lg p-3 overflow-auto" style={{ maxHeight: '300px' }}>
|
||||
<pre className="text-sm text-gray-100 text-left" style={{ fontFamily: "'Courier New', monospace" }}>
|
||||
<code>{loading ? '加载中...' : formatJSON(displayLog.ResponseBody)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestLogDetailModal;
|
||||
@@ -1,5 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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([]);
|
||||
@@ -9,6 +27,8 @@ const RequestLogList = () => {
|
||||
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({
|
||||
@@ -18,13 +38,16 @@ const RequestLogList = () => {
|
||||
endDate: ''
|
||||
});
|
||||
|
||||
// 使用防抖的筛选条件
|
||||
const debouncedFilters = useDebounce(filters, 500);
|
||||
|
||||
const fetchLogs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
...filters
|
||||
...debouncedFilters
|
||||
};
|
||||
|
||||
// 移除空值
|
||||
@@ -46,7 +69,7 @@ const RequestLogList = () => {
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
const data = await getRequestLogStats(filters);
|
||||
const data = await getRequestLogStats(debouncedFilters);
|
||||
setStats(data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
@@ -56,13 +79,18 @@ const RequestLogList = () => {
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
fetchStats();
|
||||
}, [page, filters]);
|
||||
}, [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);
|
||||
@@ -201,12 +229,15 @@ const RequestLogList = () => {
|
||||
<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="7" className="px-6 py-4 text-center text-gray-500">
|
||||
<td colSpan="8" className="px-6 py-4 text-center text-gray-500">
|
||||
暂无日志记录
|
||||
</td>
|
||||
</tr>
|
||||
@@ -234,6 +265,14 @@ const RequestLogList = () => {
|
||||
<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>
|
||||
))
|
||||
)}
|
||||
@@ -264,6 +303,13 @@ const RequestLogList = () => {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 详情查看模态框 */}
|
||||
<RequestLogDetailModal
|
||||
log={selectedLog}
|
||||
isOpen={showDetailModal}
|
||||
onClose={() => setShowDetailModal(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
8
push_docker_image.sh
Executable file
8
push_docker_image.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker compose -f docker-compose.cn.yml build
|
||||
|
||||
docker tag airouter-backend:latest nanako1092/airouter-backend:latest
|
||||
docker tag airouter-frontend:latest nanako1092/airouter-frontend:latest
|
||||
docker push nanako1092/airouter-backend:latest
|
||||
docker push nanako1092/airouter-frontend:latest
|
||||
Reference in New Issue
Block a user