mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-02 19:00:49 +08:00
feat(logs): limit lines, incremental updates, and highlighting
This commit is contained in:
308
app.js
308
app.js
@@ -20,6 +20,10 @@ class CLIProxyManager {
|
||||
// 日志自动刷新定时器
|
||||
this.logsRefreshTimer = null;
|
||||
|
||||
// 当前展示的日志行
|
||||
this.displayedLogLines = [];
|
||||
this.maxDisplayLogLines = 10000;
|
||||
|
||||
// 日志时间戳(用于增量加载)
|
||||
this.latestLogTimestamp = null;
|
||||
|
||||
@@ -1786,6 +1790,7 @@ class CLIProxyManager {
|
||||
if (!logsContent) return;
|
||||
|
||||
if (!lines || lines.length === 0) {
|
||||
this.displayedLogLines = [];
|
||||
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
|
||||
i18n.t('logs.empty_title') + '</p><p data-i18n="logs.empty_desc">' +
|
||||
i18n.t('logs.empty_desc') + '</p></div>';
|
||||
@@ -1795,26 +1800,22 @@ class CLIProxyManager {
|
||||
// 过滤掉 /v0/management/logs 相关的日志
|
||||
const filteredLines = lines.filter(line => !line.includes('/v0/management/logs'));
|
||||
|
||||
// 限制前端显示的最大行数为 10000 行
|
||||
const MAX_DISPLAY_LINES = 10000;
|
||||
// 限制前端显示的最大行数
|
||||
let displayedLines = filteredLines;
|
||||
let displayedLineCount = filteredLines.length;
|
||||
|
||||
if (filteredLines.length > MAX_DISPLAY_LINES) {
|
||||
const linesToRemove = filteredLines.length - MAX_DISPLAY_LINES;
|
||||
if (filteredLines.length > this.maxDisplayLogLines) {
|
||||
const linesToRemove = filteredLines.length - this.maxDisplayLogLines;
|
||||
displayedLines = filteredLines.slice(linesToRemove);
|
||||
displayedLineCount = MAX_DISPLAY_LINES;
|
||||
}
|
||||
|
||||
// 将数组转换为文本
|
||||
const logsText = displayedLines.join('\n');
|
||||
const logHtml = `
|
||||
this.displayedLogLines = displayedLines.slice();
|
||||
|
||||
const displayedLineCount = this.displayedLogLines.length;
|
||||
logsContent.innerHTML = `
|
||||
<div class="logs-info">
|
||||
<span><i class="fas fa-list-ol"></i> ${displayedLineCount} ${i18n.t('logs.lines')}</span>
|
||||
</div>
|
||||
<pre class="logs-text">${this.escapeHtml(logsText)}</pre>
|
||||
<pre class="logs-text">${this.buildLogsHtml(this.displayedLogLines)}</pre>
|
||||
`;
|
||||
logsContent.innerHTML = logHtml;
|
||||
|
||||
// 自动滚动到底部
|
||||
if (scrollToBottom) {
|
||||
@@ -1830,37 +1831,37 @@ class CLIProxyManager {
|
||||
const logsContent = document.getElementById('logs-content');
|
||||
if (!logsContent) return;
|
||||
|
||||
const logsTextElement = logsContent.querySelector('.logs-text');
|
||||
const logsInfoElement = logsContent.querySelector('.logs-info');
|
||||
|
||||
if (!logsTextElement || !newLines || newLines.length === 0) {
|
||||
if (!newLines || newLines.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logsTextElement = logsContent.querySelector('.logs-text');
|
||||
const logsInfoElement = logsContent.querySelector('.logs-info');
|
||||
|
||||
// 过滤掉 /v0/management/logs 相关的日志
|
||||
const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/logs'));
|
||||
if (filteredNewLines.length === 0) {
|
||||
return; // 如果过滤后没有新日志,直接返回
|
||||
}
|
||||
|
||||
if (!logsTextElement) {
|
||||
this.renderLogs(filteredNewLines, totalLineCount || filteredNewLines.length, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否正在查看底部(判断是否需要自动滚动)
|
||||
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
|
||||
|
||||
// 追加新日志文本
|
||||
const newLogsText = '\n' + filteredNewLines.join('\n');
|
||||
logsTextElement.textContent += newLogsText;
|
||||
|
||||
// 限制前端显示的最大行数为 10000 行
|
||||
const MAX_DISPLAY_LINES = 10000;
|
||||
const allLines = logsTextElement.textContent.split('\n').filter(line => line.trim());
|
||||
if (allLines.length > MAX_DISPLAY_LINES) {
|
||||
const linesToRemove = allLines.length - MAX_DISPLAY_LINES;
|
||||
logsTextElement.textContent = allLines.slice(linesToRemove).join('\n');
|
||||
this.displayedLogLines = this.displayedLogLines.concat(filteredNewLines);
|
||||
if (this.displayedLogLines.length > this.maxDisplayLogLines) {
|
||||
this.displayedLogLines = this.displayedLogLines.slice(this.displayedLogLines.length - this.maxDisplayLogLines);
|
||||
}
|
||||
|
||||
logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines);
|
||||
|
||||
// 更新行数统计(只显示实际显示的行数,最多 10000 行)
|
||||
if (logsInfoElement) {
|
||||
const displayedLines = logsTextElement.textContent.split('\n').filter(line => line.trim()).length;
|
||||
const displayedLines = this.displayedLogLines.length;
|
||||
logsInfoElement.innerHTML = `<span><i class="fas fa-list-ol"></i> ${displayedLines} ${i18n.t('logs.lines')}</span>`;
|
||||
}
|
||||
|
||||
@@ -1870,6 +1871,259 @@ class CLIProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 根据日志内容构造高亮 HTML
|
||||
buildLogsHtml(lines) {
|
||||
if (!lines || lines.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return lines.map(line => {
|
||||
// 先过滤掉 [GIN] 2025/11/03 - 18:32:59 部分
|
||||
// 匹配模式:[GIN] 后面跟着日期 - 时间
|
||||
let processedLine = line.replace(/\[GIN\]\s+\d{4}\/\d{2}\/\d{2}\s+-\s+\d{2}:\d{2}:\d{2}\s+/g, '');
|
||||
|
||||
// 创建标记数组来跟踪需要高亮的位置
|
||||
const highlights = [];
|
||||
|
||||
// 1. 检测 HTTP 状态码
|
||||
const statusInfo = this.detectHttpStatus(line);
|
||||
if (statusInfo) {
|
||||
const statusPattern = new RegExp(`\\b${statusInfo.code}\\b`);
|
||||
const match = statusPattern.exec(processedLine);
|
||||
if (match) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: `log-status-tag log-status-${statusInfo.bucket}`,
|
||||
priority: 10
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 时间戳(只匹配标准格式,排除日期和时间被分隔符分开的情况)
|
||||
// 匹配格式:
|
||||
// - 2024-01-01 12:00:00 或 2024/01/01 12:00:00(中间只有一个空格)
|
||||
// - 2024-01-01T12:00:00
|
||||
// - [12:00:00]
|
||||
// 排除:2024/01/01 - 12:00:00(中间有 - 分隔符)
|
||||
const timestampPattern = /\d{4}[-/]\d{2}[-/]\d{2}[T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?|\[\d{2}:\d{2}:\d{2}\]/g;
|
||||
let match;
|
||||
while ((match = timestampPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-timestamp',
|
||||
priority: 5
|
||||
});
|
||||
}
|
||||
|
||||
// 2b. 只匹配在方括号内的完整日期时间(如 [2025-11-03 18:23:14])
|
||||
const bracketTimestampPattern = /\[\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?\]/g;
|
||||
while ((match = bracketTimestampPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-timestamp',
|
||||
priority: 5
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 日志级别(只匹配在方括号内的,避免匹配路径中的关键字)
|
||||
const levelPattern = /\[(ERROR|ERRO|ERR|FATAL|CRITICAL|CRIT|WARN|WARNING|INFO|DEBUG|TRACE|PANIC)\]/gi;
|
||||
while ((match = levelPattern.exec(processedLine)) !== null) {
|
||||
const level = match[1].toUpperCase();
|
||||
let className = 'log-level';
|
||||
if (['ERROR', 'ERRO', 'ERR', 'FATAL', 'CRITICAL', 'CRIT', 'PANIC'].includes(level)) {
|
||||
className += ' log-level-error';
|
||||
} else if (['WARN', 'WARNING'].includes(level)) {
|
||||
className += ' log-level-warn';
|
||||
} else if (level === 'INFO') {
|
||||
className += ' log-level-info';
|
||||
} else if (['DEBUG', 'TRACE'].includes(level)) {
|
||||
className += ' log-level-debug';
|
||||
}
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: className,
|
||||
priority: 8
|
||||
});
|
||||
}
|
||||
|
||||
// 4. HTTP 方法
|
||||
const methodPattern = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\b/g;
|
||||
while ((match = methodPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-http-method',
|
||||
priority: 6
|
||||
});
|
||||
}
|
||||
|
||||
// 5. URL(仅匹配 HTTP(S) 完整URL,不匹配路径)
|
||||
const urlPattern = /(https?:\/\/[^\s<>"]+)/g;
|
||||
while ((match = urlPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-path',
|
||||
priority: 4
|
||||
});
|
||||
}
|
||||
|
||||
// 6. IP 地址
|
||||
const ipPattern = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g;
|
||||
while ((match = ipPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-ip',
|
||||
priority: 7
|
||||
});
|
||||
}
|
||||
|
||||
// 7. 关键字
|
||||
const successPattern = /\b(success|successful|succeeded|completed|ok|done|passed)\b/gi;
|
||||
while ((match = successPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-keyword-success',
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
const errorPattern = /\b(failed|failure|error|exception|panic|fatal|critical|aborted|denied|refused|timeout|invalid)\b/gi;
|
||||
while ((match = errorPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-keyword-error',
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
const warnPattern = /\b(warning|warn|deprecated|slow|retry|retrying)\b/gi;
|
||||
while ((match = warnPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-keyword-warn',
|
||||
priority: 3
|
||||
});
|
||||
}
|
||||
|
||||
// 8. 数字和单位
|
||||
const numberPattern = /\b(\d+(?:\.\d+)?)(ms|μs|ns|s|KB|MB|GB|TB|B|%)\b/g;
|
||||
while ((match = numberPattern.exec(processedLine)) !== null) {
|
||||
highlights.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
className: 'log-number-unit',
|
||||
priority: 2
|
||||
});
|
||||
}
|
||||
|
||||
// 移除重叠的高亮(保留优先级高的)
|
||||
highlights.sort((a, b) => {
|
||||
if (a.start !== b.start) return a.start - b.start;
|
||||
return b.priority - a.priority;
|
||||
});
|
||||
|
||||
const filteredHighlights = [];
|
||||
for (const h of highlights) {
|
||||
const overlaps = filteredHighlights.some(existing =>
|
||||
(h.start >= existing.start && h.start < existing.end) ||
|
||||
(h.end > existing.start && h.end <= existing.end) ||
|
||||
(h.start <= existing.start && h.end >= existing.end)
|
||||
);
|
||||
if (!overlaps) {
|
||||
filteredHighlights.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建最终的 HTML
|
||||
filteredHighlights.sort((a, b) => a.start - b.start);
|
||||
let result = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const h of filteredHighlights) {
|
||||
// 添加高亮之前的文本
|
||||
if (h.start > lastIndex) {
|
||||
result += this.escapeHtml(processedLine.substring(lastIndex, h.start));
|
||||
}
|
||||
// 添加高亮的文本
|
||||
result += `<span class="${h.className}">${this.escapeHtml(processedLine.substring(h.start, h.end))}</span>`;
|
||||
lastIndex = h.end;
|
||||
}
|
||||
|
||||
// 添加剩余的文本
|
||||
if (lastIndex < processedLine.length) {
|
||||
result += this.escapeHtml(processedLine.substring(lastIndex));
|
||||
}
|
||||
|
||||
return `<span class="log-line">${result}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// 检测 HTTP 状态码
|
||||
detectHttpStatus(line) {
|
||||
if (!line) return null;
|
||||
|
||||
// 更精确的 HTTP 状态码匹配模式
|
||||
// 匹配:
|
||||
// 1. "| 状态码 |"(如:| 200 | 或 |200|)
|
||||
// 2. "状态码 -" 或 "状态码-"(如:200 - 45ms 或 200-45ms)
|
||||
// 3. "HTTP方法 路径 状态码"(如:GET /api 200)
|
||||
// 4. "状态码 OK/Error/等状态文本"(如:200 OK)
|
||||
// 5. "status: 状态码" 或 "code: 状态码"
|
||||
// 排除:
|
||||
// - 文件路径中的数字(如 .go:396)
|
||||
// - 端口号(如 :8080)
|
||||
// - 普通数字序列
|
||||
|
||||
const patterns = [
|
||||
// 匹配 "| 状态码 |" 或 "|状态码|"(日志中常见格式)
|
||||
/\|\s*([1-5]\d{2})\s*\|/,
|
||||
// 匹配 "状态码 -" 或 "状态码-"
|
||||
/\b([1-5]\d{2})\s*-/,
|
||||
// 匹配 HTTP 方法后的状态码(GET/POST/等 路径 状态码)
|
||||
/\b(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+\S+\s+([1-5]\d{2})\b/,
|
||||
// 匹配 "status:" 或 "code:" 后的状态码
|
||||
/\b(?:status|code|http)[:\s]+([1-5]\d{2})\b/i,
|
||||
// 匹配状态码后跟状态文本(200 OK, 404 Not Found 等)
|
||||
/\b([1-5]\d{2})\s+(?:OK|Created|Accepted|No Content|Moved|Found|Bad Request|Unauthorized|Forbidden|Not Found|Method Not Allowed|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\b/i
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = line.match(pattern);
|
||||
if (match) {
|
||||
const code = parseInt(match[1], 10);
|
||||
if (Number.isNaN(code)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code >= 500) {
|
||||
return { code, bucket: '5xx', match: match[1] };
|
||||
}
|
||||
if (code >= 400) {
|
||||
return { code, bucket: '4xx', match: match[1] };
|
||||
}
|
||||
if (code >= 300) {
|
||||
return { code, bucket: '3xx', match: match[1] };
|
||||
}
|
||||
if (code >= 200) {
|
||||
return { code, bucket: '2xx', match: match[1] };
|
||||
}
|
||||
if (code >= 100) {
|
||||
return { code, bucket: '1xx', match: match[1] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 下载日志
|
||||
async downloadLogs() {
|
||||
try {
|
||||
|
||||
209
styles.css
209
styles.css
@@ -3280,9 +3280,10 @@ input:checked+.slider:before {
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: calc(100vh - 480px);
|
||||
@@ -3291,6 +3292,133 @@ input:checked+.slider:before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.logs-text .log-line {
|
||||
display: block;
|
||||
padding: 2px 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.logs-text .log-line + .log-line {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
margin-right: 6px;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag.log-status-2xx {
|
||||
background: rgba(16, 185, 129, 0.18);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag.log-status-3xx {
|
||||
background: rgba(6, 182, 212, 0.2);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag.log-status-4xx {
|
||||
background: rgba(251, 191, 36, 0.26);
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag.log-status-5xx {
|
||||
background: rgba(239, 68, 68, 0.22);
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.logs-text .log-status-tag.log-status-1xx {
|
||||
background: rgba(165, 180, 252, 0.22);
|
||||
color: #4338ca;
|
||||
}
|
||||
|
||||
/* 日志高亮样式 */
|
||||
|
||||
/* 时间戳高亮 */
|
||||
.logs-text .log-timestamp {
|
||||
color: #8b5cf6;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 日志级别高亮 */
|
||||
.logs-text .log-level {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 700;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.logs-text .log-level-error {
|
||||
background: rgba(239, 68, 68, 0.20);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.logs-text .log-level-warn {
|
||||
background: rgba(251, 191, 36, 0.25);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.logs-text .log-level-info {
|
||||
background: rgba(59, 130, 246, 0.20);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.logs-text .log-level-debug {
|
||||
background: rgba(107, 114, 128, 0.18);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* HTTP 方法高亮 */
|
||||
.logs-text .log-http-method {
|
||||
color: #0891b2;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 路径/URL 高亮 */
|
||||
.logs-text .log-path {
|
||||
color: #059669;
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
text-decoration-color: rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
/* IP 地址高亮 */
|
||||
.logs-text .log-ip {
|
||||
color: #7c3aed;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 关键字高亮 - 成功 */
|
||||
.logs-text .log-keyword-success {
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 关键字高亮 - 错误 */
|
||||
.logs-text .log-keyword-error {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 关键字高亮 - 警告 */
|
||||
.logs-text .log-keyword-warn {
|
||||
color: #ea580c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 数字和单位高亮 */
|
||||
.logs-text .log-number-unit {
|
||||
color: #db2777;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logs-text::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -3424,10 +3552,89 @@ input:checked+.slider:before {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-status-tag.log-status-2xx {
|
||||
background: rgba(16, 185, 129, 0.32);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-status-tag.log-status-3xx {
|
||||
background: rgba(6, 182, 212, 0.34);
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-status-tag.log-status-4xx {
|
||||
background: rgba(251, 191, 36, 0.38);
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-status-tag.log-status-5xx {
|
||||
background: rgba(239, 68, 68, 0.38);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-status-tag.log-status-1xx {
|
||||
background: rgba(165, 180, 252, 0.38);
|
||||
color: #c7d2fe;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-container {
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
}
|
||||
|
||||
/* 暗色主题 - 日志高亮样式 */
|
||||
[data-theme="dark"] .logs-text .log-timestamp {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-level-error {
|
||||
background: rgba(239, 68, 68, 0.32);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-level-warn {
|
||||
background: rgba(251, 191, 36, 0.35);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-level-info {
|
||||
background: rgba(59, 130, 246, 0.32);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-level-debug {
|
||||
background: rgba(107, 114, 128, 0.28);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-http-method {
|
||||
color: #22d3ee;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-path {
|
||||
color: #34d399;
|
||||
text-decoration-color: rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-ip {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-keyword-success {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-keyword-error {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-keyword-warn {
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .logs-text .log-number-unit {
|
||||
color: #f472b6;
|
||||
}
|
||||
|
||||
/* ===== AI提供商统计徽章样式 ===== */
|
||||
|
||||
/* 统计信息容器 */
|
||||
|
||||
Reference in New Issue
Block a user