Update Js for logs.html

This commit is contained in:
XOF
2025-11-24 20:47:12 +08:00
parent f2706d6fc8
commit e026d8f324
23 changed files with 1884 additions and 396 deletions

View File

@@ -420,6 +420,9 @@
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
.bottom-full {
bottom: 100%;
}
.left-0 {
left: calc(var(--spacing) * 0);
}
@@ -447,6 +450,9 @@
.z-50 {
z-index: 50;
}
.z-90 {
z-index: 90;
}
.z-\[100\] {
z-index: 100;
}
@@ -495,6 +501,9 @@
.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);
}
@@ -613,6 +622,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);
}
@@ -637,6 +649,9 @@
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-7 {
height: calc(var(--spacing) * 7);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
@@ -694,6 +709,9 @@
.w-0 {
width: calc(var(--spacing) * 0);
}
.w-1 {
width: calc(var(--spacing) * 1);
}
.w-1\/4 {
width: calc(1/4 * 100%);
}
@@ -790,6 +808,9 @@
.min-w-0 {
min-width: calc(var(--spacing) * 0);
}
.min-w-\[12rem\] {
min-width: 12rem;
}
.min-w-full {
min-width: 100%;
}
@@ -799,6 +820,9 @@
.flex-1 {
flex: 1;
}
.flex-shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
@@ -811,6 +835,9 @@
.caption-bottom {
caption-side: bottom;
}
.border-collapse {
border-collapse: collapse;
}
.origin-center {
transform-origin: center;
}
@@ -837,6 +864,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);
@@ -993,6 +1024,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);
}
@@ -1138,6 +1172,9 @@
--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)) {
@@ -1165,6 +1202,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)) {
@@ -1201,6 +1241,9 @@
.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)) {
@@ -1273,6 +1316,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)) {
@@ -1459,6 +1505,9 @@
.bg-zinc-200 {
background-color: var(--color-zinc-200);
}
.bg-zinc-400 {
background-color: var(--color-zinc-400);
}
.bg-zinc-500 {
background-color: var(--color-zinc-500);
}
@@ -1484,6 +1533,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)) {
@@ -1554,6 +1607,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);
}
@@ -1602,6 +1658,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);
}
@@ -1849,6 +1908,9 @@
.text-zinc-100 {
color: var(--color-zinc-100);
}
.text-zinc-200 {
color: var(--color-zinc-200);
}
.text-zinc-400 {
color: var(--color-zinc-400);
}
@@ -1876,6 +1938,9 @@
.italic {
font-style: italic;
}
.underline {
text-decoration-line: underline;
}
.opacity-0 {
opacity: 0%;
}
@@ -1933,6 +1998,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);
}
@@ -1954,6 +2023,10 @@
--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,);
@@ -2672,6 +2745,11 @@
border-color: var(--color-zinc-800);
}
}
.dark\:bg-black {
&:where(.dark, .dark *) {
background-color: var(--color-black);
}
}
.dark\:bg-blue-900 {
&:where(.dark, .dark *) {
background-color: var(--color-blue-900);
@@ -2788,6 +2866,16 @@
}
}
}
.dark\:bg-zinc-500 {
&:where(.dark, .dark *) {
background-color: var(--color-zinc-500);
}
}
.dark\:bg-zinc-600 {
&:where(.dark, .dark *) {
background-color: var(--color-zinc-600);
}
}
.dark\:bg-zinc-700 {
&:where(.dark, .dark *) {
background-color: var(--color-zinc-700);
@@ -5028,6 +5116,11 @@
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-blur {
syntax: "*";
inherits: false;
@@ -5140,11 +5233,6 @@
inherits: false;
initial-value: 1;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -5202,6 +5290,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;
@@ -5229,7 +5318,6 @@
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-scale-z: 1;
--tw-outline-style: solid;
}
}
}

View File

@@ -0,0 +1,30 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
export {
__commonJS,
__toESM
};

View File

@@ -1,3 +1,5 @@
import "./chunk-JSBRDJBE.js";
// frontend/js/pages/dashboard.js
function init() {
console.log("[Modern Frontend] Dashboard module loaded. Future logic will execute here.");

View File

@@ -13,6 +13,7 @@ import {
apiFetch,
apiFetchJson
} from "./chunk-PLQL6WIO.js";
import "./chunk-JSBRDJBE.js";
// frontend/js/components/tagInput.js
var TagInput = class {

View File

@@ -1,240 +0,0 @@
import {
escapeHTML
} from "./chunk-A4OOMLXK.js";
import {
apiFetchJson
} from "./chunk-PLQL6WIO.js";
// frontend/js/pages/logs/logList.js
var STATIC_ERROR_MAP = {
"API_KEY_INVALID": { type: "\u5BC6\u94A5\u65E0\u6548", style: "red" },
"INVALID_ARGUMENT": { type: "\u53C2\u6570\u65E0\u6548", style: "red" },
"PERMISSION_DENIED": { type: "\u6743\u9650\u4E0D\u8DB3", style: "red" },
"NOT_FOUND": { type: "\u8D44\u6E90\u672A\u627E\u5230", style: "gray" },
"RESOURCE_EXHAUSTED": { type: "\u8D44\u6E90\u8017\u5C3D", style: "orange" },
"QUOTA_EXCEEDED": { type: "\u914D\u989D\u8017\u5C3D", style: "orange" },
"DEADLINE_EXCEEDED": { type: "\u8BF7\u6C42\u8D85\u65F6", style: "yellow" },
"CANCELLED": { type: "\u8BF7\u6C42\u5DF2\u53D6\u6D88", style: "gray" },
"INTERNAL": { type: "Google\u5185\u90E8\u9519\u8BEF", style: "yellow" },
"UNAVAILABLE": { type: "\u670D\u52A1\u4E0D\u53EF\u7528", style: "yellow" }
};
var STATUS_CODE_MAP = {
400: { type: "\u9519\u8BEF\u8BF7\u6C42", style: "red" },
401: { type: "\u8BA4\u8BC1\u5931\u8D25", style: "red" },
403: { type: "\u7981\u6B62\u8BBF\u95EE", style: "red" },
404: { type: "\u8D44\u6E90\u672A\u627E\u5230", style: "gray" },
413: { type: "\u8BF7\u6C42\u4F53\u8FC7\u5927", style: "orange" },
429: { type: "\u8BF7\u6C42\u9891\u7387\u8FC7\u9AD8", style: "orange" },
500: { type: "\u5185\u90E8\u670D\u52A1\u9519\u8BEF", style: "yellow" },
503: { type: "\u670D\u52A1\u4E0D\u53EF\u7528", style: "yellow" }
};
var SPECIAL_CASE_MAP = [
{ code: 400, keyword: "api key not found", type: "\u65E0\u6548\u5BC6\u94A5", style: "red" },
{ code: 404, keyword: "call listmodels", type: "\u6A21\u578B\u914D\u7F6E\u9519\u8BEF", style: "orange" }
];
var styleToClass = (style) => {
switch (style) {
case "red":
return "bg-red-500/10 text-red-600";
case "orange":
return "bg-orange-500/10 text-orange-600";
case "yellow":
return "bg-yellow-500/10 text-yellow-600";
case "gray":
return "bg-zinc-500/10 text-zinc-600";
default:
return "bg-destructive/10 text-destructive";
}
};
var errorCodeRegex = /(\d+)$/;
var LogList = class {
constructor(container, dataStore2) {
this.container = container;
this.dataStore = dataStore2;
if (!this.container) console.error("LogList: container element (tbody) not found.");
}
renderLoading() {
if (!this.container) return;
this.container.innerHTML = `<tr><td colspan="9" class="p-8 text-center text-muted-foreground"><i class="fas fa-spinner fa-spin mr-2"></i> \u52A0\u8F7D\u65E5\u5FD7\u4E2D...</td></tr>`;
}
render(logs, pagination) {
if (!this.container) return;
if (!logs || logs.length === 0) {
this.container.innerHTML = `<tr><td colspan="9" class="p-8 text-center text-muted-foreground">\u6CA1\u6709\u627E\u5230\u76F8\u5173\u7684\u65E5\u5FD7\u8BB0\u5F55\u3002</td></tr>`;
return;
}
const { page, page_size } = pagination;
const startIndex = (page - 1) * page_size;
const logsHtml = logs.map((log, index) => this.createLogRowHtml(log, startIndex + index + 1)).join("");
this.container.innerHTML = logsHtml;
}
_interpretError(log) {
if (log.IsSuccess) {
return {
type: "N/A",
statusCodeHtml: `<span class="inline-flex items-center rounded-md bg-green-500/10 px-2 py-1 text-xs font-medium text-green-600">\u6210\u529F</span>`
};
}
const codeMatch = log.ErrorCode ? log.ErrorCode.match(errorCodeRegex) : null;
if (codeMatch && codeMatch[1] && log.ErrorMessage) {
const code = parseInt(codeMatch[1], 10);
const lowerCaseMsg = log.ErrorMessage.toLowerCase();
for (const rule of SPECIAL_CASE_MAP) {
if (code === rule.code && lowerCaseMsg.includes(rule.keyword)) {
return {
type: rule.type,
statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(rule.style)}">${code}</span>`
};
}
}
}
if (log.ErrorCode && STATIC_ERROR_MAP[log.ErrorCode]) {
const mapping = STATIC_ERROR_MAP[log.ErrorCode];
return {
type: mapping.type,
statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(mapping.style)}">${log.ErrorCode}</span>`
};
}
if (codeMatch && codeMatch[1]) {
const code = parseInt(codeMatch[1], 10);
let mapping = STATUS_CODE_MAP[code];
if (!mapping && code >= 500 && code < 600) {
mapping = STATUS_CODE_MAP[500];
}
if (mapping) {
return {
type: mapping.type,
statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass(mapping.style)}">${code}</span>`
};
}
}
if (!log.ErrorCode && !log.ErrorMessage) {
return { type: "\u672A\u77E5", statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass("gray")}">N/A</span>` };
}
return { type: "\u672A\u77E5\u9519\u8BEF", statusCodeHtml: `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${styleToClass("default")}">\u5931\u8D25</span>` };
}
_formatModelName(modelName) {
const styleClass = "";
return `<div class="inline-block rounded bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5"><span class="font-quinquefive text-xs tracking-wider ${styleClass}">${modelName}</span></div>`;
}
createLogRowHtml(log, index) {
const group = this.dataStore.groups.get(log.GroupID);
const groupName = group ? group.display_name : log.GroupID ? `Group #${log.GroupID}` : "N/A";
const key = this.dataStore.keys.get(log.KeyID);
let apiKeyDisplay;
if (key && key.APIKey && key.APIKey.length >= 8) {
const masked = `${key.APIKey.substring(0, 4)}......${key.APIKey.substring(key.APIKey.length - 4)}`;
apiKeyDisplay = escapeHTML(masked);
} else {
apiKeyDisplay = log.KeyID ? `Key #${log.KeyID}` : "N/A";
}
const errorInfo = this._interpretError(log);
const modelNameFormatted = this._formatModelName(log.ModelName);
const errorMessageAttr = log.ErrorMessage ? `data-error-message="${escape(log.ErrorMessage)}"` : "";
const requestTime = new Date(log.RequestTime).toLocaleString();
return `
<tr class="table-row" 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"></td>
<td class="table-cell font-mono text-muted-foreground">${index}</td>
<td class="table-cell font-medium font-mono">${apiKeyDisplay}</td>
<td class="table-cell">${groupName}</td>
<td class="table-cell">${errorInfo.type}</td>
<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>
</tr>
`;
}
};
var logList_default = LogList;
// frontend/js/pages/logs/index.js
var dataStore = {
groups: /* @__PURE__ */ new Map(),
keys: /* @__PURE__ */ new Map()
};
var LogsPage = class {
constructor() {
this.state = {
logs: [],
pagination: { page: 1, pages: 1, total: 0, page_size: 20 },
isLoading: true,
filters: { page: 1, page_size: 20 }
};
this.elements = {
tableBody: document.getElementById("logs-table-body")
};
this.initialized = !!this.elements.tableBody;
if (this.initialized) {
this.logList = new logList_default(this.elements.tableBody, dataStore);
}
}
async init() {
if (!this.initialized) return;
this.initEventListeners();
await this.loadGroupsOnce();
await this.loadAndRenderLogs();
}
initEventListeners() {
}
async loadGroupsOnce() {
if (dataStore.groups.size > 0) return;
try {
const { success, data } = await apiFetchJson("/admin/keygroups");
if (success && Array.isArray(data)) {
data.forEach((group) => dataStore.groups.set(group.id, group));
}
} catch (error) {
console.error("Failed to load key groups:", error);
}
}
async loadAndRenderLogs() {
this.state.isLoading = true;
this.logList.renderLoading();
try {
const query = new URLSearchParams(this.state.filters);
const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
if (success && typeof data === "object") {
const { items, total, page, page_size } = data;
this.state.logs = items;
this.state.pagination = { page, page_size, total, pages: Math.ceil(total / page_size) };
await this.enrichLogsWithKeyNames(items);
this.logList.render(this.state.logs, this.state.pagination);
} else {
this.logList.render([], this.state.pagination);
}
} catch (error) {
console.error("Failed to load logs:", error);
this.logList.render([], this.state.pagination);
} finally {
this.state.isLoading = false;
}
}
async enrichLogsWithKeyNames(logs) {
const missingKeyIds = [...new Set(
logs.filter((log) => log.KeyID && !dataStore.keys.has(log.KeyID)).map((log) => log.KeyID)
)];
if (missingKeyIds.length === 0) return;
try {
const idsQuery = missingKeyIds.join(",");
const { success, data } = await apiFetchJson(`/admin/apikeys?ids=${idsQuery}`);
if (success && Array.isArray(data)) {
data.forEach((key) => dataStore.keys.set(key.ID, key));
}
} catch (error) {
console.error(`Failed to fetch key details:`, error);
}
}
};
function logs_default() {
const page = new LogsPage();
page.init();
}
export {
logs_default as default
};

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import {
apiFetch,
apiFetchJson
} from "./chunk-PLQL6WIO.js";
import "./chunk-JSBRDJBE.js";
// frontend/js/components/slidingTabs.js
var SlidingTabs = class {
@@ -179,9 +180,9 @@ var base_default = initLayout;
var pageModules = {
// 键 'dashboard' 对应一个函数,该函数调用 import() 返回一个 Promise
// esbuild 看到这个 import() 语法,就会自动将 dashboard.js 及其依赖打包成一个独立的 chunk 文件
"dashboard": () => import("./dashboard-CJJWKYPR.js"),
"keys": () => import("./keys-4GCIJ7HW.js"),
"logs": () => import("./logs-AG4TD2DO.js")
"dashboard": () => import("./dashboard-XFUWX3IN.js"),
"keys": () => import("./keys-HRP4JR7B.js"),
"logs": () => import("./logs-OFCAHOEI.js")
// 'settings': () => import('./pages/settings.js'), // 未来启用 settings 页面
// 未来新增的页面只需在这里添加一行映射esbuild会自动处理
};

View File

@@ -41,16 +41,16 @@
<div class="flex flex-1 items-center space-x-2">
<input class="input h-8 w-[150px] lg:w-[250px]" placeholder="筛选密钥..." value="">
<input id="log-search-input" class="input h-8 w-[150px] lg:w-[250px]" placeholder="全局模糊查找..." value="">
<button class="btn btn-outline border-dashed h-8 px-3 text-xs">
<button id="filter-error-type-btn" class="btn btn-outline border-dashed 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">
<circle cx="12" cy="12" r="10"></circle><path d="M8 12h8"></path><path d="M12 8v8"></path>
</svg>
错误类型
</button>
<button class="btn btn-outline border-dashed h-8 px-3 text-xs">
<button id="filter-error-code-btn" class="btn btn-outline border-dashed 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">
<circle cx="12" cy="12" r="10"></circle><path d="M8 12h8"></path><path d="M12 8v8"></path>
</svg>
@@ -100,23 +100,39 @@
</table>
</div>
<!-- 3.3 分页控制器 -->
<!-- 分页控制器部分 -->
<div class="flex items-center justify-between p-2 shrink-0 border-t border-zinc-200 dark:border-zinc-700">
<div class="flex-1 text-sm text-zinc-500 dark:text-zinc-400">
已选择 <span class="font-semibold text-zinc-900 dark:text-white">0</span> / <span class="font-semibold text-zinc-900 dark:text-white">100</span>
已选择 <span class="font-semibold text-zinc-900 dark:text-white">0</span> / <span class="font-semibold text-zinc-900 dark:text-white">0</span>
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">每页行数</p>
<button type="button" class="btn btn-secondary h-8 w-[70px] flex justify-between items-center px-2">
<span>10</span>
<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="h-4 w-4 opacity-50"><path d="m6 9 6 6 6-6"></path></svg>
</button>
<p class="text-sm font-medium">每页行数</p>
<div data-component="custom-select-v2">
<button type="button" class="custom-select-trigger btn btn-secondary h-8 w-[70px] flex justify-between items-center px-2">
<span>20</span>
<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="h-4 w-4 opacity-50"><path d="m6 9 6 6 6-6"></path></svg>
</button>
<select class="hidden">
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<template class="custom-select-panel-template">
<div class="custom-select-panel w-[70px] bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-md shadow-lg z-[100]">
<!-- JS <select> -->
</div>
</template>
</div>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
第 1 / 10
第 1 / 1 页
</div>
<div class="flex items-center space-x-2">
<!-- [修正 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>
<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="h-4 w-4"><path d="m11 17-5-5 5-5"></path><path d="m18 17-5-5 5-5"></path></svg>