mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 03:10:50 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
181cba6886 | ||
|
|
aa729914c5 | ||
|
|
f98f31f2ed | ||
|
|
1e79f918e2 | ||
|
|
257260b1d2 |
469
app.js
469
app.js
@@ -20,6 +20,10 @@ class CLIProxyManager {
|
||||
// 日志自动刷新定时器
|
||||
this.logsRefreshTimer = null;
|
||||
|
||||
// 当前展示的日志行
|
||||
this.displayedLogLines = [];
|
||||
this.maxDisplayLogLines = 10000;
|
||||
|
||||
// 日志时间戳(用于增量加载)
|
||||
this.latestLogTimestamp = null;
|
||||
|
||||
@@ -1498,24 +1502,16 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// Gemini 密钥
|
||||
if (config['generative-language-api-key']) {
|
||||
await this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
}
|
||||
await this.renderGeminiKeys(Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : []);
|
||||
|
||||
// Codex 密钥
|
||||
if (config['codex-api-key']) {
|
||||
await this.renderCodexKeys(config['codex-api-key']);
|
||||
}
|
||||
await this.renderCodexKeys(Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : []);
|
||||
|
||||
// Claude 密钥
|
||||
if (config['claude-api-key']) {
|
||||
await this.renderClaudeKeys(config['claude-api-key']);
|
||||
}
|
||||
await this.renderClaudeKeys(Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : []);
|
||||
|
||||
// OpenAI 兼容提供商
|
||||
if (config['openai-compatibility']) {
|
||||
await this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
}
|
||||
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : []);
|
||||
}
|
||||
|
||||
// 回退方法:原来的逐个加载方式
|
||||
@@ -1794,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>';
|
||||
@@ -1803,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) {
|
||||
@@ -1838,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>`;
|
||||
}
|
||||
|
||||
@@ -1878,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 {
|
||||
@@ -2179,9 +2425,8 @@ class CLIProxyManager {
|
||||
async loadGeminiKeys() {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['generative-language-api-key']) {
|
||||
await this.renderGeminiKeys(config['generative-language-api-key']);
|
||||
}
|
||||
const keys = Array.isArray(config['generative-language-api-key']) ? config['generative-language-api-key'] : [];
|
||||
await this.renderGeminiKeys(keys);
|
||||
} catch (error) {
|
||||
console.error('加载Gemini密钥失败:', error);
|
||||
}
|
||||
@@ -2190,8 +2435,12 @@ class CLIProxyManager {
|
||||
// 渲染Gemini密钥列表
|
||||
async renderGeminiKeys(keys) {
|
||||
const container = document.getElementById('gemini-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(keys) ? keys : [];
|
||||
|
||||
if (keys.length === 0) {
|
||||
if (list.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fab fa-google"></i>
|
||||
@@ -2205,7 +2454,7 @@ class CLIProxyManager {
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((key, index) => {
|
||||
container.innerHTML = list.map((key, index) => {
|
||||
const masked = this.maskApiKey(key);
|
||||
const keyStats = stats[key] || stats[masked] || { success: 0, failure: 0 };
|
||||
return `
|
||||
@@ -2344,9 +2593,8 @@ class CLIProxyManager {
|
||||
async loadCodexKeys() {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['codex-api-key']) {
|
||||
await this.renderCodexKeys(config['codex-api-key']);
|
||||
}
|
||||
const keys = Array.isArray(config['codex-api-key']) ? config['codex-api-key'] : [];
|
||||
await this.renderCodexKeys(keys);
|
||||
} catch (error) {
|
||||
console.error('加载Codex密钥失败:', error);
|
||||
}
|
||||
@@ -2355,8 +2603,12 @@ class CLIProxyManager {
|
||||
// 渲染Codex密钥列表
|
||||
async renderCodexKeys(keys) {
|
||||
const container = document.getElementById('codex-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(keys) ? keys : [];
|
||||
|
||||
if (keys.length === 0) {
|
||||
if (list.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-code"></i>
|
||||
@@ -2370,7 +2622,7 @@ class CLIProxyManager {
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((config, index) => {
|
||||
container.innerHTML = list.map((config, index) => {
|
||||
const rawKey = config['api-key'];
|
||||
const masked = rawKey ? this.maskApiKey(rawKey) : '';
|
||||
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
|
||||
@@ -2549,9 +2801,8 @@ class CLIProxyManager {
|
||||
async loadClaudeKeys() {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['claude-api-key']) {
|
||||
await this.renderClaudeKeys(config['claude-api-key']);
|
||||
}
|
||||
const keys = Array.isArray(config['claude-api-key']) ? config['claude-api-key'] : [];
|
||||
await this.renderClaudeKeys(keys);
|
||||
} catch (error) {
|
||||
console.error('加载Claude密钥失败:', error);
|
||||
}
|
||||
@@ -2560,8 +2811,12 @@ class CLIProxyManager {
|
||||
// 渲染Claude密钥列表
|
||||
async renderClaudeKeys(keys) {
|
||||
const container = document.getElementById('claude-keys-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(keys) ? keys : [];
|
||||
|
||||
if (keys.length === 0) {
|
||||
if (list.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-brain"></i>
|
||||
@@ -2575,7 +2830,7 @@ class CLIProxyManager {
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = keys.map((config, index) => {
|
||||
container.innerHTML = list.map((config, index) => {
|
||||
const rawKey = config['api-key'];
|
||||
const masked = rawKey ? this.maskApiKey(rawKey) : '';
|
||||
const keyStats = (rawKey && (stats[rawKey] || stats[masked])) || { success: 0, failure: 0 };
|
||||
@@ -2754,9 +3009,8 @@ class CLIProxyManager {
|
||||
async loadOpenAIProviders() {
|
||||
try {
|
||||
const config = await this.getConfig();
|
||||
if (config['openai-compatibility']) {
|
||||
await this.renderOpenAIProviders(config['openai-compatibility']);
|
||||
}
|
||||
const providers = Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [];
|
||||
await this.renderOpenAIProviders(providers);
|
||||
} catch (error) {
|
||||
console.error('加载OpenAI提供商失败:', error);
|
||||
}
|
||||
@@ -2765,8 +3019,12 @@ class CLIProxyManager {
|
||||
// 渲染OpenAI提供商列表
|
||||
async renderOpenAIProviders(providers) {
|
||||
const container = document.getElementById('openai-providers-list');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(providers) ? providers : [];
|
||||
|
||||
if (!Array.isArray(providers) || providers.length === 0) {
|
||||
if (list.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-plug"></i>
|
||||
@@ -2781,7 +3039,7 @@ class CLIProxyManager {
|
||||
}
|
||||
|
||||
// 根据提供商数量设置滚动条
|
||||
if (providers.length > 5) {
|
||||
if (list.length > 5) {
|
||||
container.style.maxHeight = '400px';
|
||||
container.style.overflowY = 'auto';
|
||||
} else {
|
||||
@@ -2792,7 +3050,7 @@ class CLIProxyManager {
|
||||
// 获取使用统计,按 source 聚合
|
||||
const stats = await this.getKeyStats();
|
||||
|
||||
container.innerHTML = providers.map((provider, index) => {
|
||||
container.innerHTML = list.map((provider, index) => {
|
||||
const item = typeof provider === 'object' && provider !== null ? provider : {};
|
||||
|
||||
// 处理两种API密钥格式:新的 api-key-entries 和旧的 api-keys
|
||||
@@ -3132,13 +3390,31 @@ class CLIProxyManager {
|
||||
// 使用API返回的文件类型
|
||||
const fileType = file.type || 'unknown';
|
||||
// 首字母大写显示类型,特殊处理 iFlow
|
||||
let typeDisplay;
|
||||
if (fileType === 'iflow') {
|
||||
typeDisplay = 'iFlow';
|
||||
} else {
|
||||
typeDisplay = fileType.charAt(0).toUpperCase() + fileType.slice(1);
|
||||
let typeDisplayKey;
|
||||
switch (fileType) {
|
||||
case 'qwen':
|
||||
typeDisplayKey = 'auth_files.type_qwen';
|
||||
break;
|
||||
case 'gemini':
|
||||
typeDisplayKey = 'auth_files.type_gemini';
|
||||
break;
|
||||
case 'claude':
|
||||
typeDisplayKey = 'auth_files.type_claude';
|
||||
break;
|
||||
case 'codex':
|
||||
typeDisplayKey = 'auth_files.type_codex';
|
||||
break;
|
||||
case 'iflow':
|
||||
typeDisplayKey = 'auth_files.type_iflow';
|
||||
break;
|
||||
case 'empty':
|
||||
typeDisplayKey = 'auth_files.type_empty';
|
||||
break;
|
||||
default:
|
||||
typeDisplayKey = 'auth_files.type_unknown';
|
||||
break;
|
||||
}
|
||||
const typeBadge = `<span class="file-type-badge ${fileType}">${typeDisplay}</span>`;
|
||||
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-type="${fileType}">
|
||||
@@ -3188,13 +3464,13 @@ class CLIProxyManager {
|
||||
|
||||
// 预定义的按钮顺序和显示文本
|
||||
const predefinedTypes = [
|
||||
{ type: 'all', label: 'All' },
|
||||
{ type: 'qwen', label: 'Qwen' },
|
||||
{ type: 'gemini', label: 'Gemini' },
|
||||
{ type: 'claude', label: 'Claude' },
|
||||
{ type: 'codex', label: 'Codex' },
|
||||
{ type: 'iflow', label: 'iFlow' },
|
||||
{ type: 'empty', label: 'Empty' }
|
||||
{ type: 'all', labelKey: 'auth_files.filter_all' },
|
||||
{ type: 'qwen', labelKey: 'auth_files.filter_qwen' },
|
||||
{ type: 'gemini', labelKey: 'auth_files.filter_gemini' },
|
||||
{ type: 'claude', labelKey: 'auth_files.filter_claude' },
|
||||
{ type: 'codex', labelKey: 'auth_files.filter_codex' },
|
||||
{ type: 'iflow', labelKey: 'auth_files.filter_iflow' },
|
||||
{ type: 'empty', labelKey: 'auth_files.filter_empty' }
|
||||
];
|
||||
|
||||
// 获取现有按钮
|
||||
@@ -3209,6 +3485,11 @@ class CLIProxyManager {
|
||||
const btnType = btn.dataset.type;
|
||||
if (existingTypes.has(btnType)) {
|
||||
btn.style.display = 'inline-block';
|
||||
const match = predefinedTypes.find(item => item.type === btnType);
|
||||
if (match) {
|
||||
btn.textContent = i18n.t(match.labelKey);
|
||||
btn.setAttribute('data-i18n-text', match.labelKey);
|
||||
}
|
||||
} else {
|
||||
btn.style.display = 'none';
|
||||
}
|
||||
@@ -3222,9 +3503,17 @@ class CLIProxyManager {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'filter-btn';
|
||||
btn.dataset.type = type;
|
||||
// 首字母大写
|
||||
btn.textContent = type.charAt(0).toUpperCase() + type.slice(1);
|
||||
|
||||
|
||||
const match = predefinedTypes.find(item => item.type === type);
|
||||
if (match) {
|
||||
btn.setAttribute('data-i18n-text', match.labelKey);
|
||||
btn.textContent = i18n.t(match.labelKey);
|
||||
} else {
|
||||
const dynamicKey = `auth_files.filter_${type}`;
|
||||
btn.setAttribute('data-i18n-text', dynamicKey);
|
||||
btn.textContent = this.generateDynamicTypeLabel(type);
|
||||
}
|
||||
|
||||
// 插入到 Empty 按钮之前(如果存在)
|
||||
const emptyBtn = filterContainer.querySelector('[data-type="empty"]');
|
||||
if (emptyBtn) {
|
||||
@@ -3256,6 +3545,21 @@ class CLIProxyManager {
|
||||
item.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// 更新筛选按钮文本(以防语言切换后新按钮未刷新)
|
||||
this.refreshFilterButtonTexts();
|
||||
}
|
||||
|
||||
// 生成动态类型标签
|
||||
generateDynamicTypeLabel(type) {
|
||||
if (!type) return '';
|
||||
const key = `auth_files.type_${type}`;
|
||||
const translated = i18n.t(key);
|
||||
if (translated && translated !== key) {
|
||||
return translated;
|
||||
}
|
||||
if (type.toLowerCase() === 'iflow') return 'iFlow';
|
||||
return type.charAt(0).toUpperCase() + type.slice(1);
|
||||
}
|
||||
|
||||
// 绑定认证文件筛选事件
|
||||
@@ -3277,6 +3581,19 @@ class CLIProxyManager {
|
||||
|
||||
filterContainer._filterListener = listener;
|
||||
filterContainer.addEventListener('click', listener);
|
||||
|
||||
// 首次渲染时刷新按钮文本
|
||||
this.refreshFilterButtonTexts();
|
||||
}
|
||||
|
||||
// 刷新筛选按钮文本(根据 data-i18n-text)
|
||||
refreshFilterButtonTexts() {
|
||||
document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => {
|
||||
const key = btn.getAttribute('data-i18n-text');
|
||||
if (key) {
|
||||
btn.textContent = i18n.t(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定认证文件操作按钮事件(使用事件委托)
|
||||
|
||||
30
i18n.js
30
i18n.js
@@ -222,6 +222,21 @@ const i18n = {
|
||||
'auth_files.delete_success': '文件删除成功',
|
||||
'auth_files.delete_all_success': '成功删除',
|
||||
'auth_files.files_count': '个文件',
|
||||
'auth_files.filter_all': '全部',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
'auth_files.filter_claude': 'Claude',
|
||||
'auth_files.filter_codex': 'Codex',
|
||||
'auth_files.filter_iflow': 'iFlow',
|
||||
'auth_files.filter_empty': '空文件',
|
||||
'auth_files.filter_unknown': '其他',
|
||||
'auth_files.type_qwen': 'Qwen',
|
||||
'auth_files.type_gemini': 'Gemini',
|
||||
'auth_files.type_claude': 'Claude',
|
||||
'auth_files.type_codex': 'Codex',
|
||||
'auth_files.type_iflow': 'iFlow',
|
||||
'auth_files.type_empty': '空文件',
|
||||
'auth_files.type_unknown': '其他',
|
||||
|
||||
|
||||
// Codex OAuth
|
||||
@@ -649,6 +664,21 @@ const i18n = {
|
||||
'auth_files.delete_success': 'File deleted successfully',
|
||||
'auth_files.delete_all_success': 'Successfully deleted',
|
||||
'auth_files.files_count': 'files',
|
||||
'auth_files.filter_all': 'All',
|
||||
'auth_files.filter_qwen': 'Qwen',
|
||||
'auth_files.filter_gemini': 'Gemini',
|
||||
'auth_files.filter_claude': 'Claude',
|
||||
'auth_files.filter_codex': 'Codex',
|
||||
'auth_files.filter_iflow': 'iFlow',
|
||||
'auth_files.filter_empty': 'Empty',
|
||||
'auth_files.filter_unknown': 'Other',
|
||||
'auth_files.type_qwen': 'Qwen',
|
||||
'auth_files.type_gemini': 'Gemini',
|
||||
'auth_files.type_claude': 'Claude',
|
||||
'auth_files.type_codex': 'Codex',
|
||||
'auth_files.type_iflow': 'iFlow',
|
||||
'auth_files.type_empty': 'Empty',
|
||||
'auth_files.type_unknown': 'Other',
|
||||
|
||||
// Codex OAuth
|
||||
'auth_login.codex_oauth_title': 'Codex OAuth',
|
||||
|
||||
14
index.html
14
index.html
@@ -424,13 +424,13 @@
|
||||
data-i18n="auth_files.title_section">认证文件</span></h3>
|
||||
<!-- 类型筛选 -->
|
||||
<div class="auth-file-filter">
|
||||
<button class="filter-btn active" data-type="all">All</button>
|
||||
<button class="filter-btn" data-type="qwen">Qwen</button>
|
||||
<button class="filter-btn" data-type="gemini">Gemini</button>
|
||||
<button class="filter-btn" data-type="claude">Claude</button>
|
||||
<button class="filter-btn" data-type="codex">Codex</button>
|
||||
<button class="filter-btn" data-type="iflow">iFlow</button>
|
||||
<button class="filter-btn" data-type="empty">Empty</button>
|
||||
<button class="filter-btn active" data-type="all" data-i18n-text="auth_files.filter_all">All</button>
|
||||
<button class="filter-btn" data-type="qwen" data-i18n-text="auth_files.filter_qwen">Qwen</button>
|
||||
<button class="filter-btn" data-type="gemini" data-i18n-text="auth_files.filter_gemini">Gemini</button>
|
||||
<button class="filter-btn" data-type="claude" data-i18n-text="auth_files.filter_claude">Claude</button>
|
||||
<button class="filter-btn" data-type="codex" data-i18n-text="auth_files.filter_codex">Codex</button>
|
||||
<button class="filter-btn" data-type="iflow" data-i18n-text="auth_files.filter_iflow">iFlow</button>
|
||||
<button class="filter-btn" data-type="empty" data-i18n-text="auth_files.filter_empty">Empty</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
|
||||
297
styles.css
297
styles.css
@@ -1699,6 +1699,7 @@ input:checked+.slider:before {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-item.hidden {
|
||||
@@ -1718,6 +1719,13 @@ input:checked+.slider:before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 为 API Keys 和 AI Providers 的项优化按钮位置 */
|
||||
.key-item .item-content,
|
||||
.provider-item .item-content {
|
||||
padding-right: 120px; /* 为按钮预留空间,防止内容重叠 */
|
||||
min-width: 0; /* 确保内容可以正常换行 */
|
||||
}
|
||||
|
||||
.item-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -1725,6 +1733,7 @@ input:checked+.slider:before {
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.item-stats {
|
||||
@@ -1736,9 +1745,46 @@ input:checked+.slider:before {
|
||||
|
||||
.item-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* API Keys 和 AI Providers 的按钮组 - 绝对定位到右侧垂直居中 */
|
||||
.key-item .item-actions,
|
||||
.provider-item .item-actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 15px;
|
||||
transform: translateY(-50%);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式优化 */
|
||||
.key-item .item-actions .btn,
|
||||
.provider-item .item-actions .btn {
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.key-item .item-actions .btn:hover,
|
||||
.provider-item .item-actions .btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.key-item .item-actions .btn i,
|
||||
.provider-item .item-actions .btn i {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
@@ -3234,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);
|
||||
@@ -3245,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;
|
||||
@@ -3378,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提供商统计徽章样式 ===== */
|
||||
|
||||
/* 统计信息容器 */
|
||||
@@ -3460,6 +3713,44 @@ input:checked+.slider:before {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 - API Keys 和 AI Providers 按钮优化 */
|
||||
@media (max-width: 768px) {
|
||||
/* 移动端按钮位置调整 */
|
||||
.key-item .item-actions,
|
||||
.provider-item .item-actions {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
transform: none;
|
||||
margin-top: 12px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* 移动端移除内容右侧内边距 */
|
||||
.key-item .item-content,
|
||||
.provider-item .item-content {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
/* 移动端按钮尺寸调整 */
|
||||
.key-item .item-actions .btn,
|
||||
.provider-item .item-actions .btn {
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.key-item .item-actions .btn:hover,
|
||||
.provider-item .item-actions .btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.key-item .item-actions .btn i,
|
||||
.provider-item .item-actions .btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
#config-management .CodeMirror .CodeMirror-lines {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user