Update: Js 4 Log.html 95% --next move the loglevel to settingserver
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Filename: frontend/js/pages/logs/index.js
|
||||
import { apiFetchJson } from '../../services/api.js';
|
||||
import LogList from './logList.js';
|
||||
import CustomSelectV2 from '../../components/customSelectV2.js';
|
||||
@@ -145,6 +146,7 @@ class LogsPage {
|
||||
|
||||
switchToView(viewName) {
|
||||
if (this.state.currentView === viewName && this.elements.contentContainer.innerHTML !== '') return;
|
||||
|
||||
if (this.systemLogTerminal) {
|
||||
this.systemLogTerminal.disconnect();
|
||||
this.systemLogTerminal = null;
|
||||
@@ -153,25 +155,22 @@ class LogsPage {
|
||||
this.fp.destroy();
|
||||
this.fp = null;
|
||||
}
|
||||
|
||||
if (this.themeObserver) {
|
||||
this.themeObserver.disconnect();
|
||||
this.themeObserver = null;
|
||||
}
|
||||
|
||||
this.state.currentView = viewName;
|
||||
this.elements.contentContainer.innerHTML = '';
|
||||
if (viewName === 'error') {
|
||||
this.elements.errorFilters.classList.remove('hidden');
|
||||
this.elements.systemControls.classList.add('hidden');
|
||||
const isErrorView = viewName === 'error';
|
||||
this.elements.errorFilters.style.display = isErrorView ? 'flex' : 'none';
|
||||
this.elements.systemControls.style.display = isErrorView ? 'none' : 'flex';
|
||||
if (isErrorView) {
|
||||
const template = this.elements.errorTemplate.content.cloneNode(true);
|
||||
this.elements.contentContainer.appendChild(template);
|
||||
requestAnimationFrame(() => {
|
||||
this._initErrorLogView();
|
||||
});
|
||||
} else if (viewName === 'system') {
|
||||
this.elements.errorFilters.classList.add('hidden');
|
||||
this.elements.systemControls.classList.remove('hidden');
|
||||
const template = this.elements.systemTemplate.content.cloneNode(true);
|
||||
this.elements.contentContainer.appendChild(template);
|
||||
requestAnimationFrame(() => {
|
||||
@@ -328,7 +327,7 @@ class LogsPage {
|
||||
template.className = 'custom-select-panel-template';
|
||||
|
||||
template.innerHTML = `
|
||||
<div class="custom-select-panel absolute z-[1000] my-2 w-24 origin-top-right rounded-md bg-popover dark:bg-zinc-900 shadow-lg ring-1 ring-zinc-500/30 ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" tabindex="-1">
|
||||
<div class="custom-select-panel absolute z-1000 my-2 w-24 origin-top-right rounded-md bg-popover dark:bg-zinc-900 shadow-lg ring-1 ring-zinc-500/30 ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" tabindex="-1">
|
||||
</div>
|
||||
`;
|
||||
nativeMonthSelect.classList.add('hidden');
|
||||
@@ -359,8 +358,14 @@ class LogsPage {
|
||||
this.elements.selectAllCheckbox.addEventListener('change', (event) => this.handleSelectAllChange(event));
|
||||
}
|
||||
if (this.elements.tableBody) {
|
||||
this.elements.tableBody.addEventListener('change', (event) => {
|
||||
if (event.target.type === 'checkbox') this.handleSelectionChange(event.target);
|
||||
this.elements.tableBody.addEventListener('click', (event) => {
|
||||
const checkbox = event.target.closest('input[type="checkbox"]');
|
||||
const actionButton = event.target.closest('button[data-action]');
|
||||
if (checkbox) {
|
||||
this.handleSelectionChange(checkbox);
|
||||
} else if (actionButton) {
|
||||
this._handleLogRowAction(actionButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (this.elements.searchInput) {
|
||||
@@ -465,6 +470,118 @@ class LogsPage {
|
||||
deleteSelectedBtn.disabled = !hasSelection;
|
||||
}
|
||||
}
|
||||
|
||||
async _handleLogRowAction(button) {
|
||||
const action = button.dataset.action;
|
||||
const row = button.closest('.table-row');
|
||||
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||
if (!row) return;
|
||||
const logId = parseInt(row.dataset.logId, 10);
|
||||
const log = this.state.logs.find(l => l.ID === logId);
|
||||
if (!log) {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '找不到日志数据', showConfirmButton: false, timer: 2000 });
|
||||
return;
|
||||
}
|
||||
switch (action) {
|
||||
case 'view-log-details': {
|
||||
const detailsHtml = `
|
||||
<div class="space-y-3 text-left text-sm p-2">
|
||||
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">状态码</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.StatusCode || 'N/A'}</p></div>
|
||||
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">状态</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.Status || 'N/A'}</p></div>
|
||||
<div class="flex"><p class="w-24 font-semibold text-zinc-500 shrink-0">模型</p><p class="font-mono text-zinc-800 dark:text-zinc-200">${log.ModelName || 'N/A'}</p></div>
|
||||
<div class="border-t border-zinc-200 dark:border-zinc-700 my-2"></div>
|
||||
<div>
|
||||
<p class="font-semibold text-zinc-500 mb-1">错误消息</p>
|
||||
<div class="max-h-40 overflow-y-auto bg-zinc-100 dark:bg-zinc-800 p-2 rounded-md text-zinc-700 dark:text-zinc-300 wrap-break-word text-xs">
|
||||
${log.ErrorMessage ? log.ErrorMessage.replace(/\n/g, '<br>') : '无错误消息。'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
Swal.fire({
|
||||
target: '#main-content-wrapper',
|
||||
width: '32rem',
|
||||
backdrop: `rgba(0,0,0,0.5)`,
|
||||
heightAuto: false,
|
||||
customClass: {
|
||||
popup: `swal2-custom-style rounded-xl ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}`,
|
||||
title: 'text-lg font-bold',
|
||||
htmlContainer: 'm-0 text-left',
|
||||
},
|
||||
title: '日志详情',
|
||||
html: detailsHtml,
|
||||
showCloseButton: false,
|
||||
showConfirmButton: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'copy-api-key': {
|
||||
const key = dataStore.keys.get(log.KeyID);
|
||||
if (key && key.APIKey) {
|
||||
navigator.clipboard.writeText(key.APIKey).then(() => {
|
||||
Swal.fire({ toast: true, position: 'top-end', customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` }, icon: 'success', title: 'API Key 已复制', showConfirmButton: false, timer: 1500 });
|
||||
}).catch(err => {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '复制失败', text: err.message, showConfirmButton: false, timer: 2000 });
|
||||
});
|
||||
} else {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'warning', title: '未找到完整的API Key', showConfirmButton: false, timer: 2000 });
|
||||
return;
|
||||
}
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(key.APIKey).then(() => {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: 'API Key 已复制', showConfirmButton: false, timer: 1500 });
|
||||
}).catch(err => {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'error', title: '复制失败', text: err.message, showConfirmButton: false, timer: 2000 });
|
||||
});
|
||||
} else {
|
||||
// 如果不可用,则提供明确的错误提示
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: '复制失败',
|
||||
text: '此功能需要安全连接 (HTTPS) 或在 localhost 环境下使用。',
|
||||
target: '#main-content-wrapper',
|
||||
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete-log': {
|
||||
Swal.fire({
|
||||
width: '20rem',
|
||||
backdrop: `rgba(0,0,0,0.5)`,
|
||||
heightAuto: false,
|
||||
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
|
||||
title: '确认删除',
|
||||
text: `您确定要删除这条日志吗?此操作不可撤销。`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
reverseButtons: false,
|
||||
confirmButtonColor: '#ef4444',
|
||||
cancelButtonColor: '#6b7280',
|
||||
focusCancel: true,
|
||||
target: '#main-content-wrapper',
|
||||
}).then(async (result) => {
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const url = `/admin/logs?ids=${logId}`;
|
||||
const { success, message } = await apiFetchJson(url, { method: 'DELETE' });
|
||||
if (success) {
|
||||
Swal.fire({ toast: true, position: 'top-end', icon: 'success', title: '删除成功', showConfirmButton: false, timer: 2000, timerProgressBar: true });
|
||||
this.loadAndRenderLogs();
|
||||
} else {
|
||||
throw new Error(message || '删除失败,请稍后重试。');
|
||||
}
|
||||
} catch (error) {
|
||||
Swal.fire({ icon: 'error', title: '操作失败', text: error.message, target: '#main-content-wrapper' });
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changePageSize(newSize) {
|
||||
this.state.filters.page_size = newSize;
|
||||
this.state.filters.page = 1;
|
||||
@@ -520,23 +637,33 @@ class LogsPage {
|
||||
finalParams[key] = filters[key];
|
||||
}
|
||||
});
|
||||
const translatedErrorCodes = new Set();
|
||||
const translatedStatusCodes = new Set(filters.status_codes);
|
||||
// --- [MODIFIED] START: Combine all error-related filters into a single parameter for OR logic ---
|
||||
const allErrorCodes = new Set();
|
||||
const allStatusCodes = new Set(filters.status_codes);
|
||||
if (filters.error_types.size > 0) {
|
||||
filters.error_types.forEach(type => {
|
||||
for (const [code, obj] of Object.entries(STATUS_CODE_MAP)) {
|
||||
if (obj.type === type) translatedStatusCodes.add(code);
|
||||
}
|
||||
// Find matching static error codes (e.g., 'API_KEY_INVALID')
|
||||
for (const [code, obj] of Object.entries(STATIC_ERROR_MAP)) {
|
||||
if (obj.type === type) translatedErrorCodes.add(code);
|
||||
if (obj.type === type) {
|
||||
allErrorCodes.add(code);
|
||||
}
|
||||
}
|
||||
// Find matching status codes (e.g., 400, 401)
|
||||
for (const [code, obj] of Object.entries(STATUS_CODE_MAP)) {
|
||||
if (obj.type === type) {
|
||||
allStatusCodes.add(code);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pass the combined codes to the backend. The backend will handle the OR logic.
|
||||
if (allErrorCodes.size > 0) finalParams.error_codes = [...allErrorCodes].join(',');
|
||||
if (allStatusCodes.size > 0) finalParams.status_codes = [...allStatusCodes].join(',');
|
||||
// --- [MODIFIED] END ---
|
||||
|
||||
if (filters.key_ids.size > 0) finalParams.key_ids = [...filters.key_ids].join(',');
|
||||
if (filters.group_ids.size > 0) finalParams.group_ids = [...filters.group_ids].join(',');
|
||||
if (translatedErrorCodes.size > 0) finalParams.error_codes = [...translatedErrorCodes].join(',');
|
||||
if (translatedStatusCodes.size > 0) finalParams.status_codes = [...translatedStatusCodes].join(',');
|
||||
|
||||
Object.keys(finalParams).forEach(key => {
|
||||
if (finalParams[key] === '' || finalParams[key] === null || finalParams[key] === undefined) {
|
||||
|
||||
@@ -78,7 +78,7 @@ class LogList {
|
||||
statusCodeHtml: `<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-600">成功</span>`
|
||||
};
|
||||
}
|
||||
// 2. [新增] 特殊场景优先判断 (结合ErrorCode和ErrorMessage)
|
||||
// 2. 特殊场景优先判断 (结合ErrorCode和ErrorMessage)
|
||||
const codeMatch = log.ErrorCode ? log.ErrorCode.match(errorCodeRegex) : null;
|
||||
if (codeMatch && codeMatch[1] && log.ErrorMessage) {
|
||||
const code = parseInt(codeMatch[1], 10);
|
||||
@@ -146,7 +146,7 @@ class LogList {
|
||||
|
||||
const checkedAttr = isChecked ? 'checked' : '';
|
||||
return `
|
||||
<tr class="table-row" data-log-id="${log.ID}" ${errorMessageAttr}>
|
||||
<tr class="table-row group even:bg-zinc-200/30 dark:even:bg-black/10" data-log-id="${log.ID}" ${errorMessageAttr}>
|
||||
<td class="table-cell">
|
||||
<input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500" ${checkedAttr}>
|
||||
</td>
|
||||
@@ -157,10 +157,26 @@ class LogList {
|
||||
<td class="table-cell">${errorInfo.statusCodeHtml}</td>
|
||||
<td class="table-cell">${modelNameFormatted}</td>
|
||||
<td class="table-cell text-muted-foreground text-xs">${requestTime}</td>
|
||||
<td class="table-cell">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" aria-label="查看详情">
|
||||
<i class="fas fa-ellipsis-h h-4 w-4"></i>
|
||||
</button>
|
||||
<td class="table-cell relative">
|
||||
<!-- [MODIFIED] - 2. 替换原有按钮为悬浮操作菜单 -->
|
||||
<div class="flex items-center justify-center">
|
||||
<!-- 默认显示的图标 -->
|
||||
<span class="text-zinc-400 group-hover:opacity-0 transition-opacity">
|
||||
<i class="fas fa-ellipsis-h h-4 w-4"></i>
|
||||
</span>
|
||||
<!-- 悬浮时显示的操作按钮 -->
|
||||
<div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center bg-zinc-100 dark:bg-zinc-700 rounded-full shadow-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10">
|
||||
<button class="px-2 py-1 text-zinc-500 hover:text-blue-500" data-action="view-log-details" title="查看详情">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button class="px-2 py-1 text-zinc-500 hover:text-green-500" data-action="copy-api-key" title="复制APIKey">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="px-2 py-1 text-zinc-500 hover:text-red-500" data-action="delete-log" title="删除日志">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user