Update: Js 4 Log.html 95% --next move the loglevel to settingserver

This commit is contained in:
XOF
2025-11-27 00:51:04 +08:00
parent c86e7a7ba4
commit 166437c0ac
26 changed files with 20500 additions and 2435 deletions

View File

@@ -5,4 +5,5 @@ esbuild ./frontend/js/main.js \
--outdir=./web/static/js \
--splitting \
--format=esm \
--loader:.css=css \
--watch=forever

View File

@@ -104,7 +104,7 @@
.flatpickr-calendar {
/* --- 主题样式 --- */
@apply bg-background text-foreground rounded-lg shadow-lg border border-border border-zinc-500/30 w-auto font-sans;
@apply bg-background text-foreground rounded-lg shadow-lg border border-zinc-500/30 w-auto font-sans;
animation: var(--animation-panel-in);
width: 200px;
/* --- 核心结构样式 --- */
@@ -168,7 +168,7 @@
.flatpickr-current-month .cur-month { @apply font-semibold; }
.flatpickr-current-month .flatpickr-monthDropdown-months {
@apply w-[5.5rem] font-semibold bg-transparent border-0 p-0 text-sm text-foreground text-right;
@apply w-22 font-semibold bg-transparent border-0 p-0 text-sm text-foreground text-right;
@apply appearance-none focus:outline-none focus:ring-0;
@@ -191,7 +191,7 @@
focus-visible:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[(var(--ring))]
disabled:pointer-events-none disabled:opacity-50
hover:text-accent-foreground
h-7 w-7 flex-shrink-0;
h-7 w-7 shrink-0;
position: relative;
}
.flatpickr-prev-month svg,
@@ -213,7 +213,7 @@
box-sizing: border-box;
}
.flatpickr-day {
@apply w-4 h-6.5 flex items-center justify-center rounded-full border-0 text-foreground transition-colors flex-shrink-0; /* <--- 从 w-9 h-9 缩小 */
@apply w-4 h-6.5 flex items-center justify-center rounded-full border-0 text-foreground transition-colors shrink-0; /* <--- 从 w-9 h-9 缩小 */
flex-basis: 14.2857%;
line-height: 1;
cursor: pointer;
@@ -363,8 +363,8 @@
@apply w-[1.2rem] text-center;
@apply transition-all duration-300 ease-in-out;
/* 悬停和激活状态 */
@apply group-hover:text-[#60a5fa] group-hover:[filter:drop-shadow(0_0_5px_rgba(59,130,246,0.5))];
@apply group-data-[active='true']:text-[#60a5fa] group-data-[active='true']:[filter:drop-shadow(0_0_5px_rgba(59,130,246,0.7))];
@apply group-hover:text-[#60a5fa] group-hover:filter-[drop-shadow(0_0_5px_rgba(59,130,246,0.5))];
@apply group-data-[active='true']:text-[#60a5fa] group-data-[active='true']:filter-[drop-shadow(0_0_5px_rgba(59,130,246,0.7))];
}
/* 4. 指示器 */
.nav-indicator {
@@ -392,13 +392,13 @@
@apply flex items-start p-3 w-full rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/10 bg-white/80 dark:bg-zinc-800/80 backdrop-blur-md pointer-events-auto;
}
.toast-icon {
@apply flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-white mr-3;
@apply shrink-0 w-8 h-8 rounded-full flex items-center justify-center text-white mr-3;
}
.toast-icon-loading {@apply bg-blue-500;}
.toast-icon-success {@apply bg-green-500;}
.toast-icon-error {@apply bg-red-500;}
.toast-content {
@apply flex-grow;
@apply grow
}
.toast-title {
@apply font-semibold text-sm text-zinc-800 dark:text-zinc-100;
@@ -422,7 +422,7 @@
/* --- 任务项主内容区 (左栏) --- */
.task-item-main {
@apply flex items-center justify-between flex-grow gap-1; /* flex-grow 使其占据所有可用空间 */
@apply flex items-center justify-between grow gap-1; /* flex-grow 使其占据所有可用空间 */
}
/* 2. 任务项头部: 包含标题和时间戳 */
.task-item-header {
@@ -434,7 +434,7 @@
}
.task-item-timestamp {
/* 融合了您原有的字体样式 */
@apply text-xs self-start pt-1.5 pl-2 text-zinc-400 dark:text-zinc-500 flex-shrink-0;
@apply text-xs self-start pt-1.5 pl-2 text-zinc-400 dark:text-zinc-500 shrink-0
}
/* 3. [新增] 阶段动画的核心容器 */
@@ -447,13 +447,13 @@
@apply flex items-center gap-2 p-1.5 rounded-md transition-all duration-300 ease-in-out relative;
}
.task-stage-icon {
@apply w-4 h-4 relative flex-shrink-0 text-zinc-400;
@apply w-4 h-4 relative shrink-0 text-zinc-400;
}
.task-stage-icon i {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-200;
}
.task-stage-content {
@apply flex-grow flex justify-between items-baseline text-xs;
@apply grow justify-between items-baseline text-xs;
}
.task-stage-name {
@apply text-zinc-600 dark:text-zinc-400;
@@ -522,7 +522,7 @@
}
/* --- 4. 折叠/展开的雪佛兰图标 --- */
.task-toggle-icon {
@apply transition-transform duration-300 ease-in-out text-zinc-400 flex-shrink-0 ml-2;
@apply transition-transform duration-300 ease-in-out text-zinc-400 shrink-0 ml-2;
}
/* --- 5. 展开状态下的图标旋转 --- */
/*
@@ -619,7 +619,7 @@
* 2. 【新增】移动端首屏的 "当前分组" 选择器样式
*/
.mobile-group-selector {
@apply flex-grow flex items-center justify-between p-3 border border-zinc-200 dark:border-zinc-700 rounded-lg;
@apply grow flex items-center justify-between p-3 border border-zinc-200 dark:border-zinc-700 rounded-lg;
}
/* 移动端群组下拉列表样式 */
.mobile-group-menu-active {
@@ -770,7 +770,7 @@
/* Tag Input Component */
.tag-input-container {
@apply flex flex-wrap items-center gap-2 mt-1 w-full rounded-md bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600 p-2 min-h-[40px] focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500;
@apply flex flex-wrap items-center gap-2 mt-1 w-full rounded-md bg-white dark:bg-zinc-700 border border-zinc-300 dark:border-zinc-600 p-2 min-h-10 focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500;
}
.tag-item {
@apply flex items-center gap-x-1.5 bg-blue-100 dark:bg-blue-500/20 text-blue-800 dark:text-blue-200 text-sm font-medium rounded-full px-2.5 py-0.5;
@@ -780,7 +780,7 @@
}
.tag-input-new {
/* 使其在容器内垂直居中,感觉更好 */
@apply flex-grow bg-transparent focus:outline-none text-sm self-center;
@apply grow bg-transparent focus:outline-none text-sm self-center;
}
/* 为复制按钮提供基础样式 */
@@ -851,7 +851,7 @@
}
/* .tooltip-text is now dynamically generated by JS */
.global-tooltip {
@apply fixed z-[9999] w-max max-w-xs whitespace-normal rounded-lg bg-zinc-800 px-3 py-2 text-sm font-medium text-white shadow-lg transition-opacity duration-200;
@apply fixed z-9999 w-max max-w-xs whitespace-normal rounded-lg bg-zinc-800 px-3 py-2 text-sm font-medium text-white shadow-lg transition-opacity duration-200;
}
}

View File

@@ -1,5 +1,7 @@
// Filename: frontend/js/main.js
import Swal from './vendor/sweetalert2.esm.js';
import './vendor/sweetalert2.min.css';
import anime from './vendor/anime.esm.js';
// === 1. 导入通用组件 (这些是所有页面都可能用到的,保持静态导入) ===
import SlidingTabs from './components/slidingTabs.js';
import CustomSelect from './components/customSelect.js';
@@ -48,3 +50,5 @@ window.modalManager = modalManager;
window.taskCenterManager = taskCenterManager;
window.toastManager = toastManager;
window.uiPatterns = uiPatterns;
window.Swal = Swal;
window.anime = anime;

View File

@@ -427,7 +427,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">验证任务出错: ${maskedKey}</p>
<p class="task-item-status text-red-500 truncate" title="${safeError}">${safeError}</p>
</div>
@@ -443,7 +443,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary"><i class="${iconClass}"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">${title}: ${maskedKey}</p>
<p class="task-item-status truncate" title="${safeMessage}">${safeMessage}</p>
</div>
@@ -455,7 +455,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main gap-3">
<div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">正在验证: ${maskedKey}</p>
<p class="task-item-status">运行中... (${data.processed}/${data.total})</p>
</div>
@@ -495,7 +495,7 @@ class ApiKeyList {
data-mapping-id="${mappingId}">
<input type="checkbox" class="api-key-checkbox h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 shrink-0">
<span data-status-indicator class="w-2 h-2 rounded-full shrink-0"></span>
<div class="flex-grow min-w-0">
<div class="grow min-w-0">
<p class="font-mono text-xs font-semibold truncate">${maskedKey}</p>
<p class="text-xs text-zinc-400 mt-1">失败: ${errorCount} 次</p>
</div>
@@ -854,7 +854,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main gap-3">
<div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">批量验证 ${data.total} 个Key</p>
<p class="task-item-status">运行中... (${data.processed}/${data.total})</p>
</div>
@@ -867,7 +867,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">批量验证任务出错</p>
<p class="task-item-status text-red-500 truncate" title="${data.error}">${data.error}</p>
</div>
@@ -893,7 +893,7 @@ class ApiKeyList {
return `
<div class="flex items-start text-xs">
<i class="fas fa-check-circle text-green-500 mt-0.5 mr-2"></i>
<div class="flex-grow">
<div class="grow">
<p class="font-mono">${maskedKey}</p>
<p class="text-zinc-400">${safeMessage}</p>
</div>
@@ -902,7 +902,7 @@ class ApiKeyList {
return `
<div class="flex items-start text-xs">
<i class="fas fa-times-circle text-red-500 mt-0.5 mr-2"></i>
<div class="flex-grow">
<div class="grow">
<p class="font-mono">${maskedKey}</p>
<p class="text-zinc-400">${safeMessage}</p>
</div>
@@ -913,7 +913,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary"><i class="${overallIconClass}"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<div class="flex justify-between items-center cursor-pointer" data-task-toggle>
<p class="task-item-title">${summaryTitle}</p>
<i class="fas fa-chevron-down task-toggle-icon"></i>
@@ -1248,7 +1248,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main gap-3">
<div class="task-item-icon task-item-icon-running"><i class="fas fa-spinner animate-spin"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">${title}</p>
<p class="task-item-status">运行中... (${data.processed}/${data.total})</p>
</div>
@@ -1258,7 +1258,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary text-red-500"><i class="fas fa-exclamation-triangle"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">${title}任务出错</p>
<p class="task-item-status text-red-500 truncate" title="${safeError}">${safeError}</p>
</div>
@@ -1295,7 +1295,7 @@ class ApiKeyList {
contentHtml = `
<div class="task-item-main">
<div class="task-item-icon-summary"><i class="${iconClass}"></i></div>
<div class="task-item-content flex-grow">
<div class="task-item-content grow">
<p class="task-item-title">${title}</p>
<p class="task-item-status truncate" title="${safeSummary}">${safeSummary}</p>
</div>

View File

@@ -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) {

View File

@@ -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>
`;

1311
frontend/js/vendor/anime.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

4611
frontend/js/vendor/sweetalert2.esm.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -195,7 +195,10 @@ func (h *ProxyHandler) serveTransparentProxy(c *gin.Context, requestBody []byte,
finalRecorder, finalProxyErr, isSuccess = recorder, attemptErr, attemptSuccess
// ✅ 修正 isSuccess
if finalProxyErr != nil || (finalRecorder != nil && finalRecorder.Code >= 400) {
if attemptSuccess && (finalRecorder == nil || finalRecorder.Code < 400) {
isSuccess = true
finalProxyErr = nil // ✅ 清除错误
} else {
isSuccess = false
}
lastUsedResources = resources

View File

@@ -1,3 +1,4 @@
// Filename: internal/service/log_service.go
package service
import (
@@ -73,11 +74,13 @@ func (s *LogService) GetLogs(ctx context.Context, params LogQueryParams) ([]mode
}
func (s *LogService) applyFilters(query *gorm.DB, params LogQueryParams) *gorm.DB {
if params.ModelName != "" {
query = query.Where("model_name = ?", params.ModelName)
}
if params.IsSuccess != nil {
query = query.Where("is_success = ?", *params.IsSuccess)
} else {
query = query.Where("is_success = ?", false)
}
if params.ModelName != "" {
query = query.Where("model_name = ?", params.ModelName)
}
if params.StatusCode != nil {
query = query.Where("status_code = ?", *params.StatusCode)
@@ -88,10 +91,16 @@ func (s *LogService) applyFilters(query *gorm.DB, params LogQueryParams) *gorm.D
if len(params.GroupIDs) > 0 {
query = query.Where("group_id IN (?)", params.GroupIDs)
}
if len(params.ErrorCodes) > 0 {
hasErrorCodes := len(params.ErrorCodes) > 0
hasStatusCodes := len(params.StatusCodes) > 0
if hasErrorCodes && hasStatusCodes {
query = query.Where(
s.db.Where("error_code IN (?)", params.ErrorCodes).
Or("status_code IN (?)", params.StatusCodes),
)
} else if hasErrorCodes {
query = query.Where("error_code IN (?)", params.ErrorCodes)
}
if len(params.StatusCodes) > 0 {
} else if hasStatusCodes {
query = query.Where("status_code IN (?)", params.StatusCodes)
}
if params.Q != "" {

File diff suppressed because it is too large Load Diff

View File

@@ -449,6 +449,9 @@
.z-50 {
z-index: 50;
}
.z-1000 {
z-index: 1000;
}
.z-\[100\] {
z-index: 100;
}
@@ -503,6 +506,9 @@
.my-2 {
margin-block: calc(var(--spacing) * 2);
}
.mt-0 {
margin-top: calc(var(--spacing) * 0);
}
.mt-0\.5 {
margin-top: calc(var(--spacing) * 0.5);
}
@@ -624,6 +630,9 @@
width: calc(var(--spacing) * 6);
height: calc(var(--spacing) * 6);
}
.h-0 {
height: calc(var(--spacing) * 0);
}
.h-0\.5 {
height: calc(var(--spacing) * 0.5);
}
@@ -708,6 +717,9 @@
.w-0 {
width: calc(var(--spacing) * 0);
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
@@ -822,6 +834,9 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
@@ -834,6 +849,9 @@
.caption-bottom {
caption-side: bottom;
}
.border-collapse {
border-collapse: collapse;
}
.origin-center {
transform-origin: center;
}
@@ -860,6 +878,10 @@
--tw-translate-x: 100%;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1028,6 +1050,9 @@
margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)));
}
}
.gap-x-1 {
column-gap: calc(var(--spacing) * 1);
}
.gap-x-1\.5 {
column-gap: calc(var(--spacing) * 1.5);
}
@@ -1180,6 +1205,9 @@
.\!border-primary {
border-color: var(--color-primary) !important;
}
.border-black {
border-color: var(--color-black);
}
.border-black\/10 {
border-color: color-mix(in srgb, #000 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1207,6 +1235,9 @@
.border-green-200 {
border-color: var(--color-green-200);
}
.border-primary {
border-color: var(--color-primary);
}
.border-primary\/20 {
border-color: var(--color-primary);
@supports (color: color-mix(in lab, red, red)) {
@@ -1243,12 +1274,18 @@
.border-zinc-300 {
border-color: var(--color-zinc-300);
}
.border-zinc-500 {
border-color: var(--color-zinc-500);
}
.border-zinc-500\/30 {
border-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
}
}
.border-zinc-700 {
border-color: var(--color-zinc-700);
}
.border-zinc-700\/50 {
border-color: color-mix(in srgb, oklch(37% 0.013 285.805) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1324,6 +1361,9 @@
.bg-gray-500 {
background-color: var(--color-gray-500);
}
.bg-gray-950 {
background-color: var(--color-gray-950);
}
.bg-gray-950\/5 {
background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1538,6 +1578,10 @@
--tw-gradient-position: to right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-blue-500 {
--tw-gradient-from: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-blue-500\/30 {
--tw-gradient-from: color-mix(in srgb, oklch(62.3% 0.214 259.815) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@@ -1608,6 +1652,9 @@
.px-8 {
padding-inline: calc(var(--spacing) * 8);
}
.py-0 {
padding-block: calc(var(--spacing) * 0);
}
.py-0\.5 {
padding-block: calc(var(--spacing) * 0.5);
}
@@ -1656,6 +1703,9 @@
.pr-20 {
padding-right: calc(var(--spacing) * 20);
}
.pb-1 {
padding-bottom: calc(var(--spacing) * 1);
}
.pb-1\.5 {
padding-bottom: calc(var(--spacing) * 1.5);
}
@@ -1765,6 +1815,9 @@
.break-words {
overflow-wrap: break-word;
}
.wrap-break-word {
overflow-wrap: break-word;
}
.break-all {
word-break: break-all;
}
@@ -1949,6 +2002,9 @@
--tw-ordinal: ordinal;
font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,);
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
@@ -2010,6 +2066,10 @@
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, oklab(from rgb(0 0 0 / 0.05) l a b / 25%));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.inset-shadow-sm {
--tw-inset-shadow: inset 0 2px 4px var(--tw-inset-shadow-color, rgb(0 0 0 / 0.05));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.ring-black {
--tw-ring-color: var(--color-black);
}
@@ -2034,12 +2094,19 @@
.ring-input {
--tw-ring-color: var(--color-input);
}
.ring-zinc-500 {
--tw-ring-color: var(--color-zinc-500);
}
.ring-zinc-500\/30 {
--tw-ring-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
}
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
@@ -2145,6 +2212,13 @@
.ring-inset {
--tw-ring-inset: inset;
}
.group-hover\:opacity-0 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
opacity: 0%;
}
}
}
.group-hover\:opacity-100 {
&:is(:where(.group):hover *) {
@media (hover: hover) {
@@ -2152,6 +2226,38 @@
}
}
}
.even\:bg-zinc-50\/50 {
&:nth-child(even) {
background-color: color-mix(in srgb, oklch(98.5% 0 0) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-50) 50%, transparent);
}
}
}
.even\:bg-zinc-100\/50 {
&:nth-child(even) {
background-color: color-mix(in srgb, oklch(96.7% 0.001 286.375) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-100) 50%, transparent);
}
}
}
.even\:bg-zinc-200\/30 {
&:nth-child(even) {
background-color: color-mix(in srgb, oklch(92% 0.004 286.32) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-200) 30%, transparent);
}
}
}
.even\:bg-zinc-200\/50 {
&:nth-child(even) {
background-color: color-mix(in srgb, oklch(92% 0.004 286.32) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-zinc-200) 50%, transparent);
}
}
}
.focus-within\:border-blue-500 {
&:focus-within {
border-color: var(--color-blue-500);
@@ -3129,6 +3235,106 @@
}
}
}
.dark\:even\:bg-black\/5 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 5%, transparent);
}
}
}
}
.dark\:even\:bg-black\/10 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
}
}
}
}
.dark\:even\:bg-black\/20 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 20%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 20%, transparent);
}
}
}
}
.dark\:even\:bg-black\/30 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 30%, transparent);
}
}
}
}
.dark\:even\:bg-black\/50 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 50%, transparent);
}
}
}
}
.dark\:even\:bg-black\/60 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 60%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 60%, transparent);
}
}
}
}
.dark\:even\:bg-black\/90 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #000 90%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-black) 90%, transparent);
}
}
}
}
.dark\:even\:bg-white\/5 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #fff 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 5%, transparent);
}
}
}
}
.dark\:even\:bg-white\/15 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #fff 15%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 15%, transparent);
}
}
}
}
.dark\:even\:bg-white\/50 {
&:where(.dark, .dark *) {
&:nth-child(even) {
background-color: color-mix(in srgb, #fff 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 50%, transparent);
}
}
}
}
.dark\:hover\:border-blue-400 {
&:where(.dark, .dark *) {
&:hover {
@@ -3448,7 +3654,6 @@
border-radius: var(--radius-lg);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-border);
border-color: color-mix(in srgb, oklch(55.2% 0.016 285.938) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-zinc-500) 30%, transparent);
@@ -3564,7 +3769,7 @@
font-weight: var(--font-weight-semibold);
}
.flatpickr-current-month .flatpickr-monthDropdown-months {
width: 5.5rem;
width: calc(var(--spacing) * 22);
border-style: var(--tw-border-style);
border-width: 0px;
background-color: transparent;
@@ -4165,7 +4370,6 @@
transition-duration: 200ms;
}
.task-stage-content {
display: flex;
flex-grow: 1;
align-items: baseline;
justify-content: space-between;
@@ -4916,7 +5120,7 @@
.tag-input-container {
margin-top: calc(var(--spacing) * 1);
display: flex;
min-height: 40px;
min-height: calc(var(--spacing) * 10);
width: 100%;
flex-wrap: wrap;
align-items: center;
@@ -5582,6 +5786,11 @@
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -5679,11 +5888,6 @@
syntax: "*";
inherits: false;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -5749,6 +5953,7 @@
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
@@ -5773,7 +5978,6 @@
--tw-backdrop-sepia: initial;
--tw-duration: initial;
--tw-ease: initial;
--tw-outline-style: solid;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1609,7 +1609,7 @@ var LogList = class {
const requestTime = new Date(log.RequestTime).toLocaleString();
const checkedAttr = isChecked ? "checked" : "";
return `
<tr class="table-row" data-log-id="${log.ID}" ${errorMessageAttr}>
<tr class="table-row group" 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>
@@ -1620,10 +1620,26 @@ var LogList = class {
<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="\u67E5\u770B\u8BE6\u60C5">
<i class="fas fa-ellipsis-h h-4 w-4"></i>
</button>
<td class="table-cell relative">
<!-- [MODIFIED] - 2. \u66FF\u6362\u539F\u6709\u6309\u94AE\u4E3A\u60AC\u6D6E\u64CD\u4F5C\u83DC\u5355 -->
<div class="flex items-center justify-center">
<!-- \u9ED8\u8BA4\u663E\u793A\u7684\u56FE\u6807 -->
<span class="text-zinc-400 group-hover:opacity-0 transition-opacity">
<i class="fas fa-ellipsis-h h-4 w-4"></i>
</span>
<!-- \u60AC\u6D6E\u65F6\u663E\u793A\u7684\u64CD\u4F5C\u6309\u94AE -->
<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="\u67E5\u770B\u8BE6\u60C5">
<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="\u590D\u5236APIKey">
<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="\u5220\u9664\u65E5\u5FD7">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
</td>
</tr>
`;
@@ -2559,8 +2575,14 @@ var LogsPage = class {
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) {
@@ -2663,6 +2685,115 @@ var LogsPage = class {
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: "\u627E\u4E0D\u5230\u65E5\u5FD7\u6570\u636E", showConfirmButton: false, timer: 2e3 });
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">\u72B6\u6001\u7801</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">\u72B6\u6001</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">\u6A21\u578B</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">\u9519\u8BEF\u6D88\u606F</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 break-words text-xs">
${log.ErrorMessage ? log.ErrorMessage.replace(/\n/g, "<br>") : "\u65E0\u9519\u8BEF\u6D88\u606F\u3002"}
</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: "\u65E5\u5FD7\u8BE6\u60C5",
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 \u5DF2\u590D\u5236", showConfirmButton: false, timer: 1500 });
}).catch((err) => {
Swal.fire({ toast: true, position: "top-end", icon: "error", title: "\u590D\u5236\u5931\u8D25", text: err.message, showConfirmButton: false, timer: 2e3 });
});
} else {
Swal.fire({ toast: true, position: "top-end", icon: "warning", title: "\u672A\u627E\u5230\u5B8C\u6574\u7684API Key", showConfirmButton: false, timer: 2e3 });
return;
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(key.APIKey).then(() => {
Swal.fire({ toast: true, position: "top-end", icon: "success", title: "API Key \u5DF2\u590D\u5236", showConfirmButton: false, timer: 1500 });
}).catch((err) => {
Swal.fire({ toast: true, position: "top-end", icon: "error", title: "\u590D\u5236\u5931\u8D25", text: err.message, showConfirmButton: false, timer: 2e3 });
});
} else {
Swal.fire({
icon: "error",
title: "\u590D\u5236\u5931\u8D25",
text: "\u6B64\u529F\u80FD\u9700\u8981\u5B89\u5168\u8FDE\u63A5 (HTTPS) \u6216\u5728 localhost \u73AF\u5883\u4E0B\u4F7F\u7528\u3002",
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: "\u786E\u8BA4\u5220\u9664",
text: `\u60A8\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u6761\u65E5\u5FD7\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002`,
showCancelButton: true,
confirmButtonText: "\u786E\u8BA4\u5220\u9664",
cancelButtonText: "\u53D6\u6D88",
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: "\u5220\u9664\u6210\u529F", showConfirmButton: false, timer: 2e3, timerProgressBar: true });
this.loadAndRenderLogs();
} else {
throw new Error(message || "\u5220\u9664\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002");
}
} catch (error) {
Swal.fire({ icon: "error", title: "\u64CD\u4F5C\u5931\u8D25", text: error.message, target: "#main-content-wrapper" });
}
}
});
break;
}
}
}
changePageSize(newSize) {
this.state.filters.page_size = newSize;
this.state.filters.page = 1;

1237
web/static/js/main.css Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,13 @@
<link href="/static/css/output.css" rel="stylesheet">
<link rel="stylesheet" href="https://fonts.googleapis.cn/css2?family=Pixelify+Sans:wght@400..700&family=QuinqueFive&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="/static/vendor/fontawesome/css/all.min.css">
<link rel="stylesheet" href="/static/js/main.css">
<style>
.fas, .fa-solid {
font-display: swap;
}
</style>
<title>{% block title %}GEMINI BALANCER{% endblock %}</title>
{% block head_extra %}{% endblock %}
@@ -122,8 +128,6 @@
</div>
{% block core_scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sweetalert2/11.23.0/sweetalert2.all.min.js"></script>
<script src="/static/js/main.js" type="module" defer></script>
{% endblock core_scripts %}
<!-- [核心] Block 2: 留给子页面的脚本扩展插槽 -->

View File

@@ -91,8 +91,8 @@
</div>
</div>
<!-- [新增] 3.2 系统日志的快捷操作栏 (默认隐藏) -->
<div id="system-logs-controls" class="hidden flex items-center justify-end shrink-0 py-4 space-x-4">
<!--3.2 系统日志的快捷操作栏 (默认隐藏) -->
<div id="system-logs-controls" class="flex items-center justify-end shrink-0 py-4 space-x-4">
<div id="terminal-status-indicator" class="flex items-center text-xs text-zinc-500 dark:text-zinc-400">
<span class="relative flex h-2 w-2 mr-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-zinc-400 dark:bg-zinc-500 opacity-75"></span>