mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 19:20:49 +08:00
feat: add log search functionality with UI input, filtering logic, and internationalization support
This commit is contained in:
11
app.js
11
app.js
@@ -77,7 +77,9 @@ class CLIProxyManager {
|
|||||||
this.logsRefreshTimer = null;
|
this.logsRefreshTimer = null;
|
||||||
|
|
||||||
// 当前展示的日志行
|
// 当前展示的日志行
|
||||||
|
this.allLogLines = [];
|
||||||
this.displayedLogLines = [];
|
this.displayedLogLines = [];
|
||||||
|
this.logSearchQuery = '';
|
||||||
this.maxDisplayLogLines = MAX_LOG_LINES;
|
this.maxDisplayLogLines = MAX_LOG_LINES;
|
||||||
this.logFetchLimit = LOG_FETCH_LIMIT;
|
this.logFetchLimit = LOG_FETCH_LIMIT;
|
||||||
|
|
||||||
@@ -333,6 +335,7 @@ class CLIProxyManager {
|
|||||||
const downloadLogs = document.getElementById('download-logs');
|
const downloadLogs = document.getElementById('download-logs');
|
||||||
const clearLogs = document.getElementById('clear-logs');
|
const clearLogs = document.getElementById('clear-logs');
|
||||||
const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
|
const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
|
||||||
|
const logsSearchInput = document.getElementById('logs-search-input');
|
||||||
|
|
||||||
if (refreshLogs) {
|
if (refreshLogs) {
|
||||||
refreshLogs.addEventListener('click', () => this.refreshLogs());
|
refreshLogs.addEventListener('click', () => this.refreshLogs());
|
||||||
@@ -349,6 +352,14 @@ class CLIProxyManager {
|
|||||||
if (logsAutoRefreshToggle) {
|
if (logsAutoRefreshToggle) {
|
||||||
logsAutoRefreshToggle.addEventListener('change', (e) => this.toggleLogsAutoRefresh(e.target.checked));
|
logsAutoRefreshToggle.addEventListener('change', (e) => this.toggleLogsAutoRefresh(e.target.checked));
|
||||||
}
|
}
|
||||||
|
if (logsSearchInput) {
|
||||||
|
const debouncedLogSearch = this.debounce((value) => {
|
||||||
|
this.updateLogSearchQuery(value);
|
||||||
|
}, 200);
|
||||||
|
logsSearchInput.addEventListener('input', (e) => {
|
||||||
|
debouncedLogSearch(e?.target?.value ?? '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// API 密钥管理
|
// API 密钥管理
|
||||||
const addApiKey = document.getElementById('add-api-key');
|
const addApiKey = document.getElementById('add-api-key');
|
||||||
|
|||||||
6
i18n.js
6
i18n.js
@@ -529,6 +529,9 @@ const i18n = {
|
|||||||
'logs.auto_refresh': '自动刷新',
|
'logs.auto_refresh': '自动刷新',
|
||||||
'logs.auto_refresh_enabled': '自动刷新已开启',
|
'logs.auto_refresh_enabled': '自动刷新已开启',
|
||||||
'logs.auto_refresh_disabled': '自动刷新已关闭',
|
'logs.auto_refresh_disabled': '自动刷新已关闭',
|
||||||
|
'logs.search_placeholder': '搜索日志内容或关键字',
|
||||||
|
'logs.search_empty_title': '未找到匹配的日志',
|
||||||
|
'logs.search_empty_desc': '尝试更换关键字或清空搜索条件。',
|
||||||
'logs.lines': '行',
|
'logs.lines': '行',
|
||||||
'logs.removed': '已删除',
|
'logs.removed': '已删除',
|
||||||
'logs.upgrade_required_title': '需要升级 CLI Proxy API',
|
'logs.upgrade_required_title': '需要升级 CLI Proxy API',
|
||||||
@@ -1161,6 +1164,9 @@ const i18n = {
|
|||||||
'logs.auto_refresh': 'Auto Refresh',
|
'logs.auto_refresh': 'Auto Refresh',
|
||||||
'logs.auto_refresh_enabled': 'Auto refresh enabled',
|
'logs.auto_refresh_enabled': 'Auto refresh enabled',
|
||||||
'logs.auto_refresh_disabled': 'Auto refresh disabled',
|
'logs.auto_refresh_disabled': 'Auto refresh disabled',
|
||||||
|
'logs.search_placeholder': 'Search logs by content or keyword',
|
||||||
|
'logs.search_empty_title': 'No matching logs found',
|
||||||
|
'logs.search_empty_desc': 'Try a different keyword or clear the search filter.',
|
||||||
'logs.lines': 'lines',
|
'logs.lines': 'lines',
|
||||||
'logs.removed': 'Removed',
|
'logs.removed': 'Removed',
|
||||||
'logs.upgrade_required_title': 'Please Upgrade CLI Proxy API',
|
'logs.upgrade_required_title': 'Please Upgrade CLI Proxy API',
|
||||||
|
|||||||
@@ -839,8 +839,14 @@
|
|||||||
<h2 data-i18n="logs.title">日志查看</h2>
|
<h2 data-i18n="logs.title">日志查看</h2>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header logs-header">
|
||||||
|
<div class="logs-header-main">
|
||||||
<h3><i class="fas fa-scroll"></i> <span data-i18n="logs.log_content">日志内容</span></h3>
|
<h3><i class="fas fa-scroll"></i> <span data-i18n="logs.log_content">日志内容</span></h3>
|
||||||
|
<div class="logs-search">
|
||||||
|
<i class="fas fa-search"></i>
|
||||||
|
<input type="text" id="logs-search-input" aria-label="搜索日志" data-i18n-placeholder="logs.search_placeholder" placeholder="搜索日志...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<div class="toggle-group" style="margin-right: 15px;">
|
<div class="toggle-group" style="margin-right: 15px;">
|
||||||
<label class="toggle-switch" style="margin-right: 5px;">
|
<label class="toggle-switch" style="margin-right: 5px;">
|
||||||
|
|||||||
@@ -50,20 +50,19 @@ export const logsModule = {
|
|||||||
} else if (!incremental && response.lines.length > 0) {
|
} else if (!incremental && response.lines.length > 0) {
|
||||||
this.renderLogs(response.lines, response['line-count'] || response.lines.length, true);
|
this.renderLogs(response.lines, response['line-count'] || response.lines.length, true);
|
||||||
} else if (!incremental) {
|
} else if (!incremental) {
|
||||||
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>';
|
|
||||||
this.latestLogTimestamp = null;
|
this.latestLogTimestamp = null;
|
||||||
|
this.renderLogs([], 0, false);
|
||||||
}
|
}
|
||||||
} else if (!incremental) {
|
} else if (!incremental) {
|
||||||
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>';
|
|
||||||
this.latestLogTimestamp = null;
|
this.latestLogTimestamp = null;
|
||||||
|
this.renderLogs([], 0, false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载日志失败:', error);
|
console.error('加载日志失败:', error);
|
||||||
if (!incremental) {
|
if (!incremental) {
|
||||||
|
this.allLogLines = [];
|
||||||
|
this.displayedLogLines = [];
|
||||||
|
this.latestLogTimestamp = null;
|
||||||
const is404 = error.message && (error.message.includes('404') || error.message.includes('Not Found'));
|
const is404 = error.message && (error.message.includes('404') || error.message.includes('Not Found'));
|
||||||
|
|
||||||
if (is404) {
|
if (is404) {
|
||||||
@@ -82,7 +81,17 @@ export const logsModule = {
|
|||||||
const logsContent = document.getElementById('logs-content');
|
const logsContent = document.getElementById('logs-content');
|
||||||
if (!logsContent) return;
|
if (!logsContent) return;
|
||||||
|
|
||||||
if (!lines || lines.length === 0) {
|
const sourceLines = Array.isArray(lines) ? lines : [];
|
||||||
|
const filteredLines = sourceLines.filter(line => !line.includes('/v0/management/'));
|
||||||
|
let displayedLines = filteredLines;
|
||||||
|
if (filteredLines.length > this.maxDisplayLogLines) {
|
||||||
|
const linesToRemove = filteredLines.length - this.maxDisplayLogLines;
|
||||||
|
displayedLines = filteredLines.slice(linesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allLogLines = displayedLines.slice();
|
||||||
|
|
||||||
|
if (displayedLines.length === 0) {
|
||||||
this.displayedLogLines = [];
|
this.displayedLogLines = [];
|
||||||
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-inbox"></i><p data-i18n="logs.empty_title">' +
|
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_title') + '</p><p data-i18n="logs.empty_desc">' +
|
||||||
@@ -90,14 +99,15 @@ export const logsModule = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredLines = lines.filter(line => !line.includes('/v0/management/'));
|
const visibleLines = this.filterLogLinesBySearch(displayedLines);
|
||||||
let displayedLines = filteredLines;
|
this.displayedLogLines = visibleLines.slice();
|
||||||
if (filteredLines.length > this.maxDisplayLogLines) {
|
|
||||||
const linesToRemove = filteredLines.length - this.maxDisplayLogLines;
|
|
||||||
displayedLines = filteredLines.slice(linesToRemove);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.displayedLogLines = displayedLines.slice();
|
if (visibleLines.length === 0) {
|
||||||
|
logsContent.innerHTML = '<div class="empty-state"><i class="fas fa-search"></i><p data-i18n="logs.search_empty_title">' +
|
||||||
|
i18n.t('logs.search_empty_title') + '</p><p data-i18n="logs.search_empty_desc">' +
|
||||||
|
i18n.t('logs.search_empty_desc') + '</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const displayedLineCount = this.displayedLogLines.length;
|
const displayedLineCount = this.displayedLogLines.length;
|
||||||
logsContent.innerHTML = `
|
logsContent.innerHTML = `
|
||||||
@@ -107,7 +117,7 @@ export const logsModule = {
|
|||||||
<pre class="logs-text">${this.buildLogsHtml(this.displayedLogLines)}</pre>
|
<pre class="logs-text">${this.buildLogsHtml(this.displayedLogLines)}</pre>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (scrollToBottom) {
|
if (scrollToBottom && !this.logSearchQuery) {
|
||||||
const logsTextElement = logsContent.querySelector('.logs-text');
|
const logsTextElement = logsContent.querySelector('.logs-text');
|
||||||
if (logsTextElement) {
|
if (logsTextElement) {
|
||||||
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
||||||
@@ -138,9 +148,21 @@ export const logsModule = {
|
|||||||
|
|
||||||
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
|
const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50;
|
||||||
|
|
||||||
this.displayedLogLines = this.displayedLogLines.concat(filteredNewLines);
|
const baseLines = Array.isArray(this.allLogLines) && this.allLogLines.length > 0
|
||||||
if (this.displayedLogLines.length > this.maxDisplayLogLines) {
|
? this.allLogLines
|
||||||
this.displayedLogLines = this.displayedLogLines.slice(this.displayedLogLines.length - this.maxDisplayLogLines);
|
: (Array.isArray(this.displayedLogLines) ? this.displayedLogLines : []);
|
||||||
|
|
||||||
|
this.allLogLines = baseLines.concat(filteredNewLines);
|
||||||
|
if (this.allLogLines.length > this.maxDisplayLogLines) {
|
||||||
|
this.allLogLines = this.allLogLines.slice(this.allLogLines.length - this.maxDisplayLogLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleLines = this.filterLogLinesBySearch(this.allLogLines);
|
||||||
|
this.displayedLogLines = visibleLines.slice();
|
||||||
|
|
||||||
|
if (visibleLines.length === 0) {
|
||||||
|
this.renderLogs(this.allLogLines, this.allLogLines.length, false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines);
|
logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines);
|
||||||
@@ -150,11 +172,44 @@ export const logsModule = {
|
|||||||
logsInfoElement.innerHTML = `<span><i class="fas fa-list-ol"></i> ${displayedLines} ${i18n.t('logs.lines')}</span>`;
|
logsInfoElement.innerHTML = `<span><i class="fas fa-list-ol"></i> ${displayedLines} ${i18n.t('logs.lines')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAtBottom) {
|
if (isAtBottom && !this.logSearchQuery) {
|
||||||
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
logsTextElement.scrollTop = logsTextElement.scrollHeight;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
filterLogLinesBySearch(lines) {
|
||||||
|
const keyword = (this.logSearchQuery || '').toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return Array.isArray(lines) ? lines.slice() : [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(lines) || lines.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return lines.filter(line => (line || '').toLowerCase().includes(keyword));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLogSearchQuery(value = '') {
|
||||||
|
const normalized = (value || '').trim();
|
||||||
|
if (this.logSearchQuery === normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logSearchQuery = normalized;
|
||||||
|
this.applyLogSearchFilter();
|
||||||
|
},
|
||||||
|
|
||||||
|
applyLogSearchFilter() {
|
||||||
|
const logsContent = document.getElementById('logs-content');
|
||||||
|
if (!logsContent) return;
|
||||||
|
if (logsContent.querySelector('.upgrade-notice') || logsContent.querySelector('.error-state')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const baseLines = Array.isArray(this.allLogLines) ? this.allLogLines : [];
|
||||||
|
if (baseLines.length === 0 && logsContent.querySelector('.loading-placeholder')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.renderLogs(baseLines, baseLines.length, false);
|
||||||
|
},
|
||||||
|
|
||||||
buildLogsHtml(lines) {
|
buildLogsHtml(lines) {
|
||||||
if (!lines || lines.length === 0) {
|
if (!lines || lines.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
69
styles.css
69
styles.css
@@ -4331,6 +4331,60 @@ input:checked+.slider:before {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 日志页面头部布局 */
|
||||||
|
#logs .card-header.logs-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header-main h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03);
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 420px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .logs-search {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-search i {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-search input {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-search input:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 日志页面头部操作区域 */
|
/* 日志页面头部操作区域 */
|
||||||
#logs .card-header .header-actions {
|
#logs .card-header .header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4362,6 +4416,21 @@ input:checked+.slider:before {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#logs .card-header.logs-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header-main {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-search {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
#logs .card-header .header-actions {
|
#logs .card-header .header-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
Reference in New Issue
Block a user