diff --git a/backend/internal/models/schema.go b/backend/internal/models/schema.go index 0a7064e..4ef919a 100644 --- a/backend/internal/models/schema.go +++ b/backend/internal/models/schema.go @@ -53,15 +53,15 @@ type BackendModel struct { // RequestLog 记录每次API请求的详细信息 type RequestLog struct { gorm.Model - APIKeyID uint `gorm:"index"` // API密钥ID - ProviderName string `gorm:"index"` // 服务商名称 - VirtualModelName string `gorm:"index"` // 虚拟模型名称 - BackendModelName string `gorm:"index"` // 后端模型名称 - RequestTimestamp time.Time `gorm:"index;not null"` // 请求时间戳 - ResponseTimestamp time.Time `gorm:"not null"` // 响应时间戳 - RequestTokens int `gorm:"default:0"` // 请求token数 - ResponseTokens int `gorm:"default:0"` // 响应token数 - Cost float64 `gorm:"type:decimal(10,6)"` // 成本 - RequestBody string `gorm:"type:text"` // 请求体 - ResponseBody string `gorm:"type:text"` // 响应体 + APIKeyID uint `gorm:"index" json:"api_key_id"` // API密钥ID + ProviderName string `gorm:"index" json:"provider_name"` // 服务商名称 + VirtualModelName string `gorm:"index" json:"virtual_model_name"` // 虚拟模型名称 + BackendModelName string `gorm:"index" json:"backend_model_name"` // 后端模型名称 + RequestTimestamp time.Time `gorm:"index;not null" json:"request_timestamp"` // 请求时间戳 + ResponseTimestamp time.Time `gorm:"not null" json:"response_timestamp"` // 响应时间戳 + RequestTokens int `gorm:"default:0" json:"request_tokens"` // 请求token数 + ResponseTokens int `gorm:"default:0" json:"response_tokens"` // 响应token数 + Cost float64 `gorm:"type:decimal(10,6)" json:"cost"` // 成本 + RequestBody string `gorm:"type:text" json:"request_body"` // 请求体 + ResponseBody string `gorm:"type:text" json:"response_body"` // 响应体 } diff --git a/frontend/src/components/ui/JsonViewer.css b/frontend/src/components/ui/JsonViewer.css new file mode 100644 index 0000000..5563ebb --- /dev/null +++ b/frontend/src/components/ui/JsonViewer.css @@ -0,0 +1,69 @@ +/* JsonViewer 自定义样式 */ +/* 针对 dark 主题的样式覆盖 */ + +.json-viewer-dark ._1MGIk { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._3eOF8 { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._3uHL6 { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._1Gho6 { + color: rgb(129, 181, 172) !important; +} + +.json-viewer-dark ._2T6PJ { + color: rgb(129, 181, 172) !important; +} + +.json-viewer-dark ._vGjyY { + color: rgb(203, 75, 22) !important; +} + +.json-viewer-dark ._1bQdo { + color: rgb(211, 54, 130) !important; +} + +.json-viewer-dark ._3zQKs { + color: rgb(174, 129, 255) !important; +} + +.json-viewer-dark ._1xvuR { + color: rgb(38, 139, 210) !important; +} + +.json-viewer-dark ._oLqym { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._2AXVT { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._2KJWg { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._2bSDX { + color: rgb(253, 246, 227) !important; +} + +.json-viewer-dark ._gsbQL { + color: rgb(253, 246, 227) !important; +} + +/* 确保背景色在 dark 主题下正确 */ +.json-viewer-dark { + background-color: transparent !important; +} + +/* 确保整体文本颜色在 dark 主题下正确 */ +.json-viewer-dark, +.json-viewer-dark * { + color-scheme: dark; +} \ No newline at end of file diff --git a/frontend/src/components/ui/JsonViewer.jsx b/frontend/src/components/ui/JsonViewer.jsx new file mode 100644 index 0000000..1469d70 --- /dev/null +++ b/frontend/src/components/ui/JsonViewer.jsx @@ -0,0 +1,84 @@ +import React, { useMemo } from 'react'; +import { JsonView } from 'react-json-view-lite'; +import 'react-json-view-lite/dist/index.css'; +import './JsonViewer.css'; + +const JsonViewer = ({ + data, + theme = 'dark', + collapsed = 1, + collapseAll = true, + enableClipboard = false, + className = '', + style = {}, + fallbackMessage = '无内容' +}) => { + // 根据主题设置容器类名 + const themeClass = theme === 'dark' ? 'json-viewer-dark' : ''; + // 判断数据是否为有效的JSON字符串或对象 + const { isValid, parsedData } = useMemo(() => { + if (!data) { + return { isValid: false, parsedData: null }; + } + + // 如果已经是对象,直接使用 + if (typeof data === 'object' && data !== null) { + return { isValid: true, parsedData: data }; + } + + // 如果是字符串,尝试解析 + if (typeof data === 'string') { + const trimmed = data.trim(); + if (trimmed === '') { + return { isValid: false, parsedData: null }; + } + + try { + const parsed = JSON.parse(trimmed); + return { isValid: true, parsedData: parsed }; + } catch (e) { + return { isValid: false, parsedData: data }; + } + } + + return { isValid: false, parsedData: data }; + }, [data]); + + // 默认样式设置 + const defaultStyle = { + fontFamily: "'Courier New', monospace", + fontSize: '0.875rem', + color: theme === 'dark' ? 'white' : 'black', + ...style + }; + + // 如果是有效的JSON,使用JsonView显示 + if (isValid && parsedData !== null) { + return ( +
+ +
+ ); + } + + // 如果不是JSON或为空,使用纯文本显示 + return ( +
+
+        {parsedData || fallbackMessage}
+      
+
+ ); +}; + +export default JsonViewer; \ No newline at end of file diff --git a/frontend/src/features/logs/components/RequestLogDetailModal.jsx b/frontend/src/features/logs/components/RequestLogDetailModal.jsx index 6a550ca..5e2e1dd 100644 --- a/frontend/src/features/logs/components/RequestLogDetailModal.jsx +++ b/frontend/src/features/logs/components/RequestLogDetailModal.jsx @@ -1,8 +1,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import Modal from '../../../components/ui/Modal'; +import JsonViewer from '../../../components/ui/JsonViewer'; 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); @@ -13,12 +12,12 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => { // 按需加载日志详情 useEffect(() => { - if (isOpen && log && log.ID) { + if (isOpen && log && log.id) { const fetchLogDetail = async () => { try { setLoading(true); setError(null); - const detailData = await getLogDetail(log.ID); + const detailData = await getLogDetail(log.id); setDetailLog(detailData); } catch (err) { setError(err.message || '获取日志详情失败'); @@ -57,8 +56,8 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => { return `¥${parseFloat(cost).toFixed(6)}`; }; - // 使用 useMemo 优化 JSON 格式化 - const formatJSON = useMemo(() => { + // 格式化JSON用于复制 + const formatJSONForCopy = useMemo(() => { return (jsonString) => { if (!jsonString || jsonString.trim() === '') { return '无数据'; @@ -77,7 +76,10 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => { // 复制到剪贴板 const copyToClipboard = async (text, type) => { try { - await navigator.clipboard.writeText(text); + // 格式化JSON内容用于复制 + const formattedText = formatJSONForCopy(text); + await navigator.clipboard.writeText(formattedText); + if (type === 'input') { setCopiedInput(true); setTimeout(() => setCopiedInput(false), 2000); @@ -118,35 +120,35 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => {
请求时间
-
{formatDate(displayLog.RequestTimestamp)}
+
{formatDate(displayLog.request_timestamp)}
响应时间
-
{formatDate(displayLog.ResponseTimestamp)}
+
{formatDate(displayLog.response_timestamp)}
虚拟模型
-
{displayLog.VirtualModelName || 'N/A'}
+
{displayLog.virtual_model_name || 'N/A'}
后端模型
-
{displayLog.BackendModelName || 'N/A'}
+
{displayLog.backend_model_name || 'N/A'}
服务商
-
{displayLog.ProviderName || 'N/A'}
+
{displayLog.provider_name || 'N/A'}
成本
-
{formatCost(displayLog.Cost)}
+
{formatCost(displayLog.cost)}
请求Tokens
-
{displayLog.RequestTokens || 0}
+
{displayLog.request_tokens || 0}
响应Tokens
-
{displayLog.ResponseTokens || 0}
+
{displayLog.response_tokens || 0}
@@ -156,7 +158,7 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => {

请求内容 (Input)

- {/*
-
-              {loading ? '加载中...' : formatJSON(displayLog.RequestBody)}
-            
-
*/} -
- {loading ? '加载中' : - log.RequestBody ? ( - - ) : ( -

无内容

- ) - } +
+ {loading ? ( +
加载中...
+ ) : ( + + )}
@@ -194,7 +189,7 @@ const RequestLogDetailModal = ({ log, isOpen, onClose }) => {

响应内容 (Response)

-
-
-              {loading ? '加载中...' : formatJSON(displayLog.ResponseBody)}
-            
+
+ {loading ? ( +
加载中...
+ ) : ( + + )}
diff --git a/frontend/src/features/logs/components/RequestLogList.jsx b/frontend/src/features/logs/components/RequestLogList.jsx index 3e54439..9f9f936 100644 --- a/frontend/src/features/logs/components/RequestLogList.jsx +++ b/frontend/src/features/logs/components/RequestLogList.jsx @@ -243,27 +243,27 @@ const RequestLogList = () => { ) : ( logs.map((log, index) => ( - + - {formatDate(log.RequestTimestamp)} + {formatDate(log.request_timestamp)} - {log.VirtualModelName || 'N/A'} + {log.virtual_model_name || 'N/A'} - {log.BackendModelName || 'N/A'} + {log.backend_model_name || 'N/A'} - {log.ProviderName || 'N/A'} + {log.provider_name || 'N/A'} - {log.RequestTokens || 0} + {log.request_tokens || 0} - {log.ResponseTokens || 0} + {log.response_tokens || 0} - {formatCost(log.Cost)} + {formatCost(log.cost)}