优化流式传输&fix bugs

This commit is contained in:
XOF
2025-11-25 16:58:15 +08:00
parent e026d8f324
commit ad1e6180cf
18 changed files with 1135 additions and 156 deletions

View File

@@ -28,6 +28,7 @@
--color-amber-600: oklch(66.6% 0.179 58.318);
--color-yellow-100: oklch(97.3% 0.071 103.193);
--color-yellow-300: oklch(90.5% 0.182 98.111);
--color-yellow-400: oklch(85.2% 0.199 91.936);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-yellow-600: oklch(68.1% 0.162 75.834);
--color-yellow-700: oklch(55.4% 0.135 66.442);
@@ -420,9 +421,6 @@
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
.bottom-full {
bottom: 100%;
}
.left-0 {
left: calc(var(--spacing) * 0);
}
@@ -450,9 +448,6 @@
.z-50 {
z-index: 50;
}
.z-90 {
z-index: 90;
}
.z-\[100\] {
z-index: 100;
}
@@ -501,9 +496,6 @@
.my-1\.5 {
margin-block: calc(var(--spacing) * 1.5);
}
.mt-0 {
margin-top: calc(var(--spacing) * 0);
}
.mt-0\.5 {
margin-top: calc(var(--spacing) * 0.5);
}
@@ -622,9 +614,6 @@
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);
}
@@ -709,9 +698,6 @@
.w-0 {
width: calc(var(--spacing) * 0);
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
@@ -820,9 +806,6 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
@@ -835,9 +818,6 @@
.caption-bottom {
caption-side: bottom;
}
.border-collapse {
border-collapse: collapse;
}
.origin-center {
transform-origin: center;
}
@@ -864,10 +844,6 @@
--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);
@@ -1024,9 +1000,6 @@
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);
}
@@ -1172,9 +1145,6 @@
--tw-border-style: none;
border-style: none;
}
.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)) {
@@ -1202,9 +1172,6 @@
.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)) {
@@ -1241,9 +1208,6 @@
.border-zinc-300 {
border-color: var(--color-zinc-300);
}
.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)) {
@@ -1316,9 +1280,6 @@
.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)) {
@@ -1533,10 +1494,6 @@
--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)) {
@@ -1607,9 +1564,6 @@
.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);
}
@@ -1658,9 +1612,6 @@
.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);
}
@@ -1785,6 +1736,9 @@
.text-amber-600 {
color: var(--color-amber-600);
}
.text-blue-400 {
color: var(--color-blue-400);
}
.text-blue-500 {
color: var(--color-blue-500);
}
@@ -1899,6 +1853,9 @@
.text-white {
color: var(--color-white);
}
.text-yellow-400 {
color: var(--color-yellow-400);
}
.text-yellow-500 {
color: var(--color-yellow-500);
}
@@ -1938,9 +1895,6 @@
.italic {
font-style: italic;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
@@ -1998,10 +1952,6 @@
--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);
}
@@ -2023,10 +1973,6 @@
--tw-ring-color: color-mix(in oklab, var(--color-black) 15%, 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,);
@@ -5116,11 +5062,6 @@
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -5233,6 +5174,11 @@
inherits: false;
initial-value: 1;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -5290,7 +5236,6 @@
--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;
@@ -5318,6 +5263,7 @@
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
--tw-outline-style: solid;
}
}
}

View File

@@ -843,6 +843,140 @@ var FilterPopover = class {
}
};
// frontend/js/pages/logs/systemLog.js
var SystemLogTerminal = class {
constructor(container, controlsContainer) {
this.container = container;
this.controlsContainer = controlsContainer;
this.ws = null;
this.isPaused = false;
this.shouldAutoScroll = true;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.elements = {
output: this.container.querySelector("#log-terminal-output"),
statusIndicator: this.controlsContainer.querySelector("#terminal-status-indicator"),
clearBtn: this.controlsContainer.querySelector('[data-action="clear-terminal"]'),
pauseBtn: this.controlsContainer.querySelector('[data-action="toggle-pause-terminal"]'),
scrollBtn: this.controlsContainer.querySelector('[data-action="toggle-scroll-terminal"]'),
disconnectBtn: this.controlsContainer.querySelector('[data-action="disconnect-terminal"]')
};
this._initEventListeners();
}
_initEventListeners() {
this.elements.clearBtn.addEventListener("click", () => this.clear());
this.elements.pauseBtn.addEventListener("click", () => this.togglePause());
this.elements.scrollBtn.addEventListener("click", () => this.toggleAutoScroll());
this.elements.disconnectBtn.addEventListener("click", () => this.disconnect());
}
connect() {
this.clear();
this._appendMessage("info", "\u6B63\u5728\u8FDE\u63A5\u5230\u5B9E\u65F6\u65E5\u5FD7\u6D41...");
this._updateStatus("connecting", "\u8FDE\u63A5\u4E2D...");
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/ws/system-logs`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this._appendMessage("info", "\u2713 \u5DF2\u8FDE\u63A5\u5230\u7CFB\u7EDF\u65E5\u5FD7\u6D41");
this._updateStatus("connected", "\u5DF2\u8FDE\u63A5");
this.reconnectAttempts = 0;
};
this.ws.onmessage = (event) => {
if (this.isPaused) return;
try {
const data = JSON.parse(event.data);
const levelColors = {
"error": "text-red-500",
"warning": "text-yellow-400",
"info": "text-blue-400",
"debug": "text-zinc-400"
};
const color = levelColors[data.level] || "text-zinc-200";
const timestamp = new Date(data.timestamp).toLocaleTimeString();
const msg = `[${timestamp}] [${data.level.toUpperCase()}] ${data.message}`;
this._appendMessage(color, msg);
} catch (e) {
this._appendMessage("text-zinc-200", event.data);
}
};
this.ws.onerror = (error) => {
this._appendMessage("error", `\u2717 WebSocket \u9519\u8BEF`);
this._updateStatus("error", "\u8FDE\u63A5\u9519\u8BEF");
};
this.ws.onclose = () => {
this._appendMessage("error", "\u2717 \u8FDE\u63A5\u5DF2\u65AD\u5F00");
this._updateStatus("disconnected", "\u672A\u8FDE\u63A5");
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this._appendMessage("info", `\u5C1D\u8BD5\u91CD\u65B0\u8FDE\u63A5 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
this.connect();
}, 3e3);
}
};
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.reconnectAttempts = this.maxReconnectAttempts;
this._updateStatus("disconnected", "\u672A\u8FDE\u63A5");
}
clear() {
if (this.elements.output) {
this.elements.output.innerHTML = "";
}
}
togglePause() {
this.isPaused = !this.isPaused;
const span = this.elements.pauseBtn.querySelector("span");
const icon = this.elements.pauseBtn.querySelector("i");
if (this.isPaused) {
span.textContent = "\u7EE7\u7EED";
icon.classList.replace("fa-pause", "fa-play");
} else {
span.textContent = "\u6682\u505C";
icon.classList.replace("fa-play", "fa-pause");
}
}
toggleAutoScroll() {
this.shouldAutoScroll = !this.shouldAutoScroll;
const span = this.elements.scrollBtn.querySelector("span");
if (this.shouldAutoScroll) {
span.textContent = "\u81EA\u52A8\u6EDA\u52A8";
} else {
span.textContent = "\u624B\u52A8\u6EDA\u52A8";
}
}
_appendMessage(colorClass, text) {
if (!this.elements.output) return;
const p = document.createElement("p");
p.className = colorClass;
p.textContent = text;
this.elements.output.appendChild(p);
if (this.shouldAutoScroll) {
this.elements.output.scrollTop = this.elements.output.scrollHeight;
}
}
_updateStatus(status, text) {
const indicator = this.elements.statusIndicator.querySelector("span.relative");
const statusText = this.elements.statusIndicator.childNodes[2];
const colors = {
"connecting": "bg-yellow-500",
"connected": "bg-green-500",
"disconnected": "bg-zinc-500",
"error": "bg-red-500"
};
indicator.querySelectorAll("span").forEach((span) => {
span.className = span.className.replace(/bg-\w+-\d+/g, colors[status] || colors.disconnected);
});
if (statusText) {
statusText.textContent = ` ${text}`;
}
}
};
// frontend/js/pages/logs/index.js
var dataStore = {
groups: /* @__PURE__ */ new Map(),
@@ -854,7 +988,6 @@ var LogsPage = class {
logs: [],
pagination: { page: 1, pages: 1, total: 0, page_size: 20 },
isLoading: true,
// [优化] 统一将所有可筛选字段在此处初始化,便于管理
filters: {
page: 1,
page_size: 20,
@@ -864,36 +997,109 @@ var LogsPage = class {
error_types: /* @__PURE__ */ new Set(),
status_codes: /* @__PURE__ */ new Set()
},
selectedLogIds: /* @__PURE__ */ new Set()
selectedLogIds: /* @__PURE__ */ new Set(),
currentView: "error"
};
this.elements = {
tableBody: document.getElementById("logs-table-body"),
selectedCount: document.querySelector(".flex-1.text-sm span.font-semibold:nth-child(1)"),
totalCount: document.querySelector(".flex-1.text-sm span:last-child"),
pageSizeSelect: document.querySelector('[data-component="custom-select-v2"] select'),
pageInfo: document.querySelector(".flex.w-\\[100px\\]"),
paginationBtns: document.querySelectorAll("[data-pagination-controls] button"),
selectAllCheckbox: document.querySelector('thead .table-head-cell input[type="checkbox"]'),
searchInput: document.getElementById("log-search-input"),
errorTypeFilterBtn: document.getElementById("filter-error-type-btn"),
errorCodeFilterBtn: document.getElementById("filter-error-code-btn")
tabsContainer: document.querySelector("[data-sliding-tabs-container]"),
contentContainer: document.getElementById("log-content-container"),
errorFilters: document.getElementById("error-logs-filters"),
systemControls: document.getElementById("system-logs-controls"),
errorTemplate: document.getElementById("error-logs-template"),
systemTemplate: document.getElementById("system-logs-template")
};
this.initialized = !!this.elements.tableBody;
this.initialized = !!this.elements.contentContainer;
if (this.initialized) {
this.logList = new logList_default(this.elements.tableBody, dataStore);
const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
if (selectContainer) {
new CustomSelectV2(selectContainer);
}
this.logList = null;
this.systemLogTerminal = null;
this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
}
}
async init() {
if (!this.initialized) return;
this._initPermanentEventListeners();
await this.loadGroupsOnce();
this.state.currentView = null;
this.switchToView("error");
}
_initPermanentEventListeners() {
this.elements.tabsContainer.addEventListener("click", (event) => {
const tabItem = event.target.closest("[data-tab-target]");
if (!tabItem) return;
event.preventDefault();
const viewName = tabItem.dataset.tabTarget;
if (viewName) {
this.switchToView(viewName);
}
});
}
switchToView(viewName) {
if (this.state.currentView === viewName && this.elements.contentContainer.innerHTML !== "") return;
if (this.systemLogTerminal) {
this.systemLogTerminal.disconnect();
this.systemLogTerminal = 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 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(() => {
this._initSystemLogView();
});
}
}
_initErrorLogView() {
this.elements.tableBody = document.getElementById("logs-table-body");
this.elements.selectedCount = document.querySelector(".flex-1.text-sm span.font-semibold:nth-child(1)");
this.elements.totalCount = document.querySelector(".flex-1.text-sm span:last-child");
this.elements.pageSizeSelect = document.querySelector('[data-component="custom-select-v2"] select');
this.elements.pageInfo = document.querySelector(".flex.w-\\[100px\\]");
this.elements.paginationBtns = document.querySelectorAll("[data-pagination-controls] button");
this.elements.selectAllCheckbox = document.querySelector('thead .table-head-cell input[type="checkbox"]');
this.elements.searchInput = document.getElementById("log-search-input");
this.elements.errorTypeFilterBtn = document.getElementById("filter-error-type-btn");
this.elements.errorCodeFilterBtn = document.getElementById("filter-error-code-btn");
this.logList = new logList_default(this.elements.tableBody, dataStore);
const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
if (selectContainer) {
new CustomSelectV2(selectContainer);
}
this.initFilterPopovers();
this.initEventListeners();
await this.loadGroupsOnce();
await this.loadAndRenderLogs();
this.loadAndRenderLogs();
}
_initSystemLogView() {
this.systemLogTerminal = new SystemLogTerminal(
this.elements.contentContainer,
this.elements.systemControls
);
Swal.fire({
title: "\u5B9E\u65F6\u7CFB\u7EDF\u65E5\u5FD7",
text: "\u60A8\u5373\u5C06\u8FDE\u63A5\u5230\u5B9E\u65F6\u65E5\u5FD7\u6D41\u3002\u8FD9\u4F1A\u4E0E\u670D\u52A1\u5668\u5EFA\u7ACB\u4E00\u4E2A\u6301\u7EED\u7684\u8FDE\u63A5\u3002",
icon: "info",
confirmButtonText: "\u6211\u660E\u767D\u4E86\uFF0C\u5F00\u59CB\u8FDE\u63A5",
showCancelButton: true,
cancelButtonText: "\u53D6\u6D88",
target: "#main-content-wrapper"
}).then((result) => {
if (result.isConfirmed) {
this.systemLogTerminal.connect();
} else {
const errorLogTab = Array.from(this.elements.tabsContainer.querySelectorAll('[data-tab-target="error"]'))[0];
if (errorLogTab) errorLogTab.click();
}
});
}
initFilterPopovers() {
const errorTypeOptions = [

View File

@@ -182,7 +182,7 @@ var pageModules = {
// esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件
"dashboard": () => import("./dashboard-XFUWX3IN.js"),
"keys": () => import("./keys-HRP4JR7B.js"),
"logs": () => import("./logs-OFCAHOEI.js")
"logs": () => import("./logs-43KF5HY3.js")
// 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面
// 未来新增的页面只需在这里添加一行映射esbuild会自动处理
};

View File

@@ -26,8 +26,8 @@
<div class="w-full overflow-x-auto scrollbar-hide">
<div role="tablist" class="relative inline-flex h-10 items-center justify-center inset-shadow-sm/25 rounded-lg bg-zinc-800/50 dark:bg-zinc-950 p-1" data-sliding-tabs-container>
<div class="absolute left-0 h-[calc(100%-0.5rem)] rounded-md bg-white dark:bg-zinc-700 shadow-sm" data-tab-indicator style="transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);"></div>
<a href="#" role="tab" class="tab-item tab-active" data-tab-item>错误日志</a>
<a href="#" role="tab" class="tab-item" data-tab-item>系统日志</a>
<a href="#" role="tab" class="tab-item tab-active" data-tab-item data-tab-target="error">错误日志</a>
<a href="#" role="tab" class="tab-item" data-tab-item data-tab-target="system">系统日志</a>
<a href="#" role="tab" class="tab-item" data-tab-item>保留标签</a>
<a href="#" role="tab" class="tab-item" data-tab-item>保留标签</a>
</div>
@@ -35,9 +35,9 @@
</div>
<!-- =================================================================== -->
<!-- 3. 主内容区:过滤器 + 表格 (精确复刻版) -->
<!-- 3. 主内容区:过滤器 + 表格 -->
<!-- =================================================================== -->
<div class="flex items-center justify-between shrink-0 py-4">
<div id="error-logs-filters" class="flex items-center justify-between shrink-0 py-4">
<div class="flex flex-1 items-center space-x-2">
@@ -63,7 +63,6 @@
</button>
</div>
<!-- [应用] .btn .btn-outline, 并覆盖高度/字体 -->
<button class="btn btn-outline hidden lg:flex h-8 px-3 text-xs">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 h-4 w-4">
<path d="M20 7h-9"></path><path d="M14 17H5"></path><circle cx="17" cy="17" r="3"></circle><circle cx="7" cy="7" r="3"></circle>
@@ -72,20 +71,55 @@
</button>
</div>
<!-- 3.2 数据表格容器 (带边框和圆角) -->
<div class="rounded-lg border border-zinc-200 dark:border-zinc-700 flex-grow overflow-hidden flex flex-col">
<!-- 容器需要 overflow-auto 以便表格内容超出时滚动 -->
<!-- [新增] 3.2 系统日志的快捷操作栏 (默认隐藏) -->
<div id="system-logs-controls" class="hidden flex items-center justify-between shrink-0 py-4">
<div class="flex flex-1 items-center space-x-2">
<button data-action="clear-terminal" class="btn btn-outline h-8 px-3 text-xs">
<i class="fas fa-trash-alt mr-2 h-4 w-4"></i>
清屏
</button>
<button data-action="toggle-pause-terminal" class="btn btn-outline h-8 px-3 text-xs">
<i class="fas fa-pause mr-2 h-4 w-4"></i>
<span>暂停</span>
</button>
<button data-action="toggle-scroll-terminal" class="btn btn-outline h-8 px-3 text-xs">
<i class="fas fa-arrow-down mr-2 h-4 w-4"></i>
<span>自动滚动</span>
</button>
</div>
<div class="flex items-center space-x-3">
<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>
<span class="relative inline-flex rounded-full h-2 w-2 bg-zinc-500 dark:bg-zinc-600"></span>
</span>
未连接
</div>
<button data-action="disconnect-terminal" class="btn btn-danger h-8 px-3 text-xs">
<i class="fas fa-times-circle mr-2 h-4 w-4"></i>
关闭
</button>
</div>
</div>
<!-- [修改] 3.3 核心内容容器,它的 *内部* 将被动态替换 -->
<div id="log-content-container" class="rounded-lg border border-zinc-200 dark:border-zinc-700 flex-grow overflow-hidden flex flex-col">
<!-- template 在这里切换 -->
</div>
</div>
<template id="error-logs-template">
<div class="relative w-full overflow-auto flex-grow main-content-scroll">
<table class="table">
<thead class="table-header">
<tr class="table-row">
<!-- [修正] 恢复全选复选框 -->
<th class="table-head-cell w-4">
<input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500">
</th>
<th class="table-head-cell w-16">序号</th>
<th class="table-head-cell">Gemini 密钥</th>
<!-- [新增] 群组名称列 -->
<th class="table-head-cell">群组名称</th>
<th class="table-head-cell">错误类型</th>
<th class="table-head-cell">状态码</th>
@@ -131,7 +165,6 @@
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
第 1 / 1 页
</div>
<!-- [修正 1.2] 为分页按钮组添加一个稳定的 data 属性,用于 JS 选择 -->
<div class="flex items-center space-x-2" data-pagination-controls>
<button class="btn btn-secondary h-8 w-8 p-0 hidden lg:flex" disabled>
<span class="sr-only">Go to first page</span>
@@ -152,9 +185,13 @@
</div>
</div>
</div>
</div>
</template>
<template id="system-logs-template">
<div id="log-terminal-output" class="w-full h-full overflow-y-auto main-content-scroll p-4 font-mono text-sm text-zinc-200 bg-zinc-900 dark:bg-black whitespace-pre-wrap break-all">
<!-- WebSocket 日志将在此处实时渲染 -->
</div>
</div>
</template>
{% endblock %}
{% block modals %}