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

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