Update Js for logs.html
This commit is contained in:
129
frontend/js/components/customSelectV2.js
Normal file
129
frontend/js/components/customSelectV2.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// Filename: frontend/js/components/customSelectV2.js
|
||||
import { createPopper } from '../vendor/popper.esm.min.js';
|
||||
|
||||
export default class CustomSelectV2 {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.trigger = this.container.querySelector('.custom-select-trigger');
|
||||
this.nativeSelect = this.container.querySelector('select');
|
||||
this.template = this.container.querySelector('.custom-select-panel-template');
|
||||
|
||||
if (!this.trigger || !this.nativeSelect || !this.template) {
|
||||
console.warn('CustomSelectV2 cannot initialize: missing required elements.', this.container);
|
||||
return;
|
||||
}
|
||||
|
||||
this.panel = null;
|
||||
this.popperInstance = null;
|
||||
this.isOpen = false;
|
||||
this.triggerText = this.trigger.querySelector('span');
|
||||
|
||||
if (typeof CustomSelectV2.openInstance === 'undefined') {
|
||||
CustomSelectV2.openInstance = null;
|
||||
CustomSelectV2.initGlobalListener();
|
||||
}
|
||||
|
||||
this.updateTriggerText();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
static initGlobalListener() {
|
||||
document.addEventListener('click', (event) => {
|
||||
const instance = CustomSelectV2.openInstance;
|
||||
if (instance && !instance.container.contains(event.target) && (!instance.panel || !instance.panel.contains(event.target))) {
|
||||
instance.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPanel() {
|
||||
const panelFragment = this.template.content.cloneNode(true);
|
||||
this.panel = panelFragment.querySelector('.custom-select-panel');
|
||||
document.body.appendChild(this.panel);
|
||||
|
||||
this.panel.innerHTML = '';
|
||||
Array.from(this.nativeSelect.options).forEach(option => {
|
||||
const item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'custom-select-option block w-full text-left px-3 py-1.5 text-sm text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-700';
|
||||
item.textContent = option.textContent;
|
||||
item.dataset.value = option.value;
|
||||
if (option.selected) { item.classList.add('is-selected'); }
|
||||
this.panel.appendChild(item);
|
||||
});
|
||||
|
||||
this.panel.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const optionEl = event.target.closest('.custom-select-option');
|
||||
if (optionEl) { this.selectOption(optionEl); }
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
if (CustomSelectV2.openInstance && CustomSelectV2.openInstance !== this) {
|
||||
CustomSelectV2.openInstance.close();
|
||||
}
|
||||
this.toggle();
|
||||
});
|
||||
}
|
||||
|
||||
selectOption(optionEl) {
|
||||
const selectedValue = optionEl.dataset.value;
|
||||
if (this.nativeSelect.value !== selectedValue) {
|
||||
this.nativeSelect.value = selectedValue;
|
||||
this.nativeSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
this.updateTriggerText();
|
||||
this.close();
|
||||
}
|
||||
|
||||
updateTriggerText() {
|
||||
const selectedOption = this.nativeSelect.options[this.nativeSelect.selectedIndex];
|
||||
if (selectedOption) {
|
||||
this.triggerText.textContent = selectedOption.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
toggle() { this.isOpen ? this.close() : this.open(); }
|
||||
|
||||
open() {
|
||||
if (this.isOpen) return;
|
||||
this.isOpen = true;
|
||||
|
||||
if (!this.panel) { this.createPanel(); }
|
||||
|
||||
this.panel.style.display = 'block';
|
||||
this.panel.offsetHeight;
|
||||
|
||||
this.popperInstance = createPopper(this.trigger, this.panel, {
|
||||
placement: 'top-start',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [0, 8] } },
|
||||
{ name: 'flip', options: { fallbackPlacements: ['bottom-start'] } }
|
||||
],
|
||||
});
|
||||
|
||||
CustomSelectV2.openInstance = this;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.isOpen) return;
|
||||
this.isOpen = false;
|
||||
|
||||
if (this.popperInstance) {
|
||||
this.popperInstance.destroy();
|
||||
this.popperInstance = null;
|
||||
}
|
||||
|
||||
if (this.panel) {
|
||||
this.panel.remove();
|
||||
this.panel = null;
|
||||
}
|
||||
|
||||
if (CustomSelectV2.openInstance === this) {
|
||||
CustomSelectV2.openInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
95
frontend/js/components/filterPopover.js
Normal file
95
frontend/js/components/filterPopover.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// Filename: frontend/js/components/filterPopover.js
|
||||
|
||||
import { createPopper } from '../vendor/popper.esm.min.js';
|
||||
|
||||
export default class FilterPopover {
|
||||
constructor(triggerElement, options, title) {
|
||||
if (!triggerElement || typeof createPopper !== 'function') {
|
||||
console.error('FilterPopover: Trigger element or Popper.js not found.');
|
||||
return;
|
||||
}
|
||||
this.triggerElement = triggerElement;
|
||||
this.options = options;
|
||||
this.title = title;
|
||||
this.selectedValues = new Set();
|
||||
|
||||
this._createPopoverHTML();
|
||||
this.popperInstance = createPopper(this.triggerElement, this.popoverElement, {
|
||||
placement: 'bottom-start',
|
||||
modifiers: [{ name: 'offset', options: { offset: [0, 8] } }],
|
||||
});
|
||||
|
||||
this._bindEvents();
|
||||
}
|
||||
|
||||
_createPopoverHTML() {
|
||||
this.popoverElement = document.createElement('div');
|
||||
this.popoverElement.className = 'hidden z-50 min-w-[12rem] rounded-md border bg-popover bg-white dark:bg-zinc-800 p-2 text-popover-foreground shadow-md';
|
||||
this.popoverElement.innerHTML = `
|
||||
<div class="px-2 py-1.5 text-sm font-semibold">${this.title}</div>
|
||||
<div class="space-y-1 p-1">
|
||||
${this.options.map(option => `
|
||||
<label class="flex items-center space-x-2 px-2 py-1.5 rounded-md hover:bg-accent cursor-pointer">
|
||||
<input type="checkbox" value="${option.value}" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-sm">${option.label}</span>
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="border-t border-border mt-2 pt-2 px-2 flex justify-end space-x-2">
|
||||
<button data-action="clear" class="btn btn-ghost h-7 px-2 text-xs">清空</button>
|
||||
<button data-action="apply" class="btn btn-primary h-7 px-2 text-xs">应用</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(this.popoverElement);
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
this.triggerElement.addEventListener('click', () => this.toggle());
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!this.popoverElement.contains(event.target) && !this.triggerElement.contains(event.target)) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this.popoverElement.addEventListener('click', (event) => {
|
||||
const target = event.target.closest('button');
|
||||
if (!target) return;
|
||||
const action = target.dataset.action;
|
||||
if (action === 'clear') this._handleClear();
|
||||
if (action === 'apply') this._handleApply();
|
||||
});
|
||||
}
|
||||
|
||||
_handleClear() {
|
||||
this.popoverElement.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
|
||||
this.selectedValues.clear();
|
||||
this._handleApply();
|
||||
}
|
||||
|
||||
_handleApply() {
|
||||
this.selectedValues.clear();
|
||||
this.popoverElement.querySelectorAll('input:checked').forEach(cb => {
|
||||
this.selectedValues.add(cb.value);
|
||||
});
|
||||
|
||||
const filterChangeEvent = new CustomEvent('filter-change', {
|
||||
detail: {
|
||||
filterKey: this.triggerElement.id,
|
||||
selected: this.selectedValues
|
||||
}
|
||||
});
|
||||
this.triggerElement.dispatchEvent(filterChangeEvent);
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.popoverElement.classList.toggle('hidden');
|
||||
this.popperInstance.update();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.popoverElement.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,235 @@
|
||||
// Filename: frontend/js/pages/logs/index.js
|
||||
|
||||
import { apiFetchJson } from '../../services/api.js';
|
||||
import LogList from './logList.js';
|
||||
|
||||
// [最终版] 创建一个共享的数据仓库,用于缓存 Groups 和 Keys
|
||||
import CustomSelectV2 from '../../components/customSelectV2.js';
|
||||
import { debounce } from '../../utils/utils.js';
|
||||
import FilterPopover from '../../components/filterPopover.js';
|
||||
import { STATIC_ERROR_MAP, STATUS_CODE_MAP } from './logList.js';
|
||||
const dataStore = {
|
||||
groups: new Map(),
|
||||
keys: new Map(),
|
||||
groups: new Map(),
|
||||
keys: new Map(),
|
||||
};
|
||||
|
||||
class LogsPage {
|
||||
constructor() {
|
||||
this.state = {
|
||||
logs: [],
|
||||
pagination: { page: 1, pages: 1, total: 0, page_size: 20 },
|
||||
isLoading: true,
|
||||
filters: { page: 1, page_size: 20 }
|
||||
// [优化] 统一将所有可筛选字段在此处初始化,便于管理
|
||||
filters: {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
q: '',
|
||||
key_ids: new Set(),
|
||||
group_ids: new Set(),
|
||||
error_types: new Set(),
|
||||
status_codes: new Set(),
|
||||
},
|
||||
selectedLogIds: new Set(),
|
||||
};
|
||||
|
||||
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'),
|
||||
};
|
||||
|
||||
this.initialized = !!this.elements.tableBody;
|
||||
|
||||
if (this.initialized) {
|
||||
this.logList = new LogList(this.elements.tableBody, dataStore);
|
||||
const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
|
||||
if (selectContainer) { new CustomSelectV2(selectContainer); }
|
||||
this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.initialized) return;
|
||||
this.initFilterPopovers();
|
||||
this.initEventListeners();
|
||||
// 页面初始化:先加载群组,再加载日志
|
||||
await this.loadGroupsOnce();
|
||||
await this.loadAndRenderLogs();
|
||||
}
|
||||
initFilterPopovers() {
|
||||
const errorTypeOptions = [
|
||||
...Object.values(STATUS_CODE_MAP).map(v => ({ value: v.type, label: v.type })),
|
||||
...Object.values(STATIC_ERROR_MAP).map(v => ({ value: v.type, label: v.type }))
|
||||
];
|
||||
const uniqueErrorTypeOptions = Array.from(new Map(errorTypeOptions.map(item => [item.value, item])).values());
|
||||
if (this.elements.errorTypeFilterBtn) {
|
||||
new FilterPopover(this.elements.errorTypeFilterBtn, uniqueErrorTypeOptions, '筛选错误类型');
|
||||
}
|
||||
const statusCodeOptions = Object.keys(STATUS_CODE_MAP).map(code => ({ value: code, label: code }));
|
||||
if (this.elements.errorCodeFilterBtn) {
|
||||
new FilterPopover(this.elements.errorCodeFilterBtn, statusCodeOptions, '筛选状态码');
|
||||
}
|
||||
}
|
||||
initEventListeners() {
|
||||
if (this.elements.pageSizeSelect) {
|
||||
this.elements.pageSizeSelect.addEventListener('change', (e) => this.changePageSize(parseInt(e.target.value, 10)));
|
||||
}
|
||||
if (this.elements.paginationBtns.length >= 4) {
|
||||
this.elements.paginationBtns[0].addEventListener('click', () => this.goToPage(1));
|
||||
this.elements.paginationBtns[1].addEventListener('click', () => this.goToPage(this.state.pagination.page - 1));
|
||||
this.elements.paginationBtns[2].addEventListener('click', () => this.goToPage(this.state.pagination.page + 1));
|
||||
this.elements.paginationBtns[3].addEventListener('click', () => this.goToPage(this.state.pagination.pages));
|
||||
}
|
||||
if (this.elements.selectAllCheckbox) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
if (this.elements.searchInput) {
|
||||
this.elements.searchInput.addEventListener('input', (event) => this.handleSearchInput(event));
|
||||
}
|
||||
if (this.elements.errorTypeFilterBtn) {
|
||||
this.elements.errorTypeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
|
||||
}
|
||||
if (this.elements.errorCodeFilterBtn) {
|
||||
this.elements.errorCodeFilterBtn.addEventListener('filter-change', (e) => this.handleFilterChange(e));
|
||||
}
|
||||
}
|
||||
handleFilterChange(event) {
|
||||
const { filterKey, selected } = event.detail;
|
||||
if (filterKey === 'filter-error-type-btn') {
|
||||
this.state.filters.error_types = selected;
|
||||
} else if (filterKey === 'filter-error-code-btn') {
|
||||
this.state.filters.status_codes = selected;
|
||||
}
|
||||
this.state.filters.page = 1;
|
||||
this.loadAndRenderLogs();
|
||||
}
|
||||
|
||||
initEventListeners() { /* 分页和筛选的事件监听器 */ }
|
||||
handleSearchInput(event) {
|
||||
const searchTerm = event.target.value.trim().toLowerCase();
|
||||
|
||||
// 重置分页和与本次搜索相关的筛选条件
|
||||
this.state.filters.page = 1;
|
||||
this.state.filters.q = '';
|
||||
this.state.filters.key_ids = new Set();
|
||||
this.state.filters.group_ids = new Set();
|
||||
|
||||
if (searchTerm === '') {
|
||||
this.debouncedLoadAndRender();
|
||||
return;
|
||||
}
|
||||
const matchedGroupIds = new Set();
|
||||
dataStore.groups.forEach(group => {
|
||||
if (group.display_name.toLowerCase().includes(searchTerm)) {
|
||||
matchedGroupIds.add(group.id);
|
||||
}
|
||||
});
|
||||
const matchedKeyIds = new Set();
|
||||
dataStore.keys.forEach(key => {
|
||||
if (key.APIKey && key.APIKey.toLowerCase().includes(searchTerm)) {
|
||||
matchedKeyIds.add(key.ID);
|
||||
}
|
||||
});
|
||||
if (matchedGroupIds.size > 0) this.state.filters.group_ids = matchedGroupIds;
|
||||
if (matchedKeyIds.size > 0) this.state.filters.key_ids = matchedKeyIds;
|
||||
// 如果没有找到任何匹配的ID,则回退到原始的全局模糊搜索
|
||||
if (matchedGroupIds.size === 0 && matchedKeyIds.size === 0) {
|
||||
this.state.filters.q = searchTerm;
|
||||
}
|
||||
this.debouncedLoadAndRender();
|
||||
}
|
||||
|
||||
handleSelectionChange(checkbox) {
|
||||
const row = checkbox.closest('.table-row');
|
||||
if (!row) return;
|
||||
const logId = parseInt(row.dataset.logId, 10);
|
||||
if (isNaN(logId)) return;
|
||||
if (checkbox.checked) {
|
||||
this.state.selectedLogIds.add(logId);
|
||||
} else {
|
||||
this.state.selectedLogIds.delete(logId);
|
||||
}
|
||||
this.syncSelectionUI();
|
||||
}
|
||||
|
||||
handleSelectAllChange(event) {
|
||||
const isChecked = event.target.checked;
|
||||
this.state.logs.forEach(log => {
|
||||
if (isChecked) {
|
||||
this.state.selectedLogIds.add(log.ID);
|
||||
} else {
|
||||
this.state.selectedLogIds.delete(log.ID);
|
||||
}
|
||||
});
|
||||
this.syncRowCheckboxes();
|
||||
this.syncSelectionUI();
|
||||
}
|
||||
|
||||
syncRowCheckboxes() {
|
||||
const isAllChecked = this.elements.selectAllCheckbox.checked;
|
||||
this.elements.tableBody.querySelectorAll('input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = isAllChecked;
|
||||
});
|
||||
}
|
||||
|
||||
syncSelectionUI() {
|
||||
if (!this.elements.selectAllCheckbox || !this.elements.selectedCount) return;
|
||||
const selectedCount = this.state.selectedLogIds.size;
|
||||
const visibleLogsCount = this.state.logs.length;
|
||||
// 1. 更新表头“全选”复选框的状态
|
||||
if (selectedCount === 0) {
|
||||
this.elements.selectAllCheckbox.checked = false;
|
||||
this.elements.selectAllCheckbox.indeterminate = false;
|
||||
} else if (selectedCount < visibleLogsCount) {
|
||||
this.elements.selectAllCheckbox.checked = false;
|
||||
this.elements.selectAllCheckbox.indeterminate = true; // 半选状态
|
||||
} else if (selectedCount === visibleLogsCount && visibleLogsCount > 0) {
|
||||
this.elements.selectAllCheckbox.checked = true;
|
||||
this.elements.selectAllCheckbox.indeterminate = false;
|
||||
}
|
||||
// 2. 更新“已选择”计数
|
||||
this.elements.selectedCount.textContent = selectedCount;
|
||||
// 3. (未来扩展) 更新批量操作按钮的状态
|
||||
// const batchButton = document.querySelector('.batch-action-button');
|
||||
// if (batchButton) batchButton.disabled = selectedCount === 0;
|
||||
}
|
||||
|
||||
changePageSize(newSize) {
|
||||
this.state.filters.page_size = newSize;
|
||||
this.state.filters.page = 1;
|
||||
this.loadAndRenderLogs();
|
||||
}
|
||||
goToPage(page) {
|
||||
if (page < 1 || page > this.state.pagination.pages || this.state.isLoading) return;
|
||||
this.state.filters.page = page;
|
||||
this.loadAndRenderLogs();
|
||||
}
|
||||
|
||||
updatePaginationUI() {
|
||||
const { page, pages, total } = this.state.pagination;
|
||||
|
||||
if (this.elements.pageInfo) {
|
||||
this.elements.pageInfo.textContent = `第 ${page} / ${pages} 页`;
|
||||
}
|
||||
|
||||
if (this.elements.totalCount) {
|
||||
this.elements.totalCount.textContent = total;
|
||||
}
|
||||
|
||||
if (this.elements.paginationBtns.length >= 4) {
|
||||
const isFirstPage = page === 1;
|
||||
const isLastPage = page === pages || pages === 0;
|
||||
this.elements.paginationBtns[0].disabled = isFirstPage;
|
||||
this.elements.paginationBtns[1].disabled = isFirstPage;
|
||||
this.elements.paginationBtns[2].disabled = isLastPage;
|
||||
this.elements.paginationBtns[3].disabled = isLastPage;
|
||||
}
|
||||
}
|
||||
|
||||
async loadGroupsOnce() {
|
||||
if (dataStore.groups.size > 0) return; // 防止重复加载
|
||||
if (dataStore.groups.size > 0) return;
|
||||
try {
|
||||
const { success, data } = await apiFetchJson("/admin/keygroups");
|
||||
if (success && Array.isArray(data)) {
|
||||
@@ -53,30 +242,69 @@ class LogsPage {
|
||||
|
||||
async loadAndRenderLogs() {
|
||||
this.state.isLoading = true;
|
||||
this.logList.renderLoading();
|
||||
|
||||
this.state.selectedLogIds.clear();
|
||||
this.logList.renderLoading();
|
||||
this.updatePaginationUI();
|
||||
this.syncSelectionUI();
|
||||
try {
|
||||
const query = new URLSearchParams(this.state.filters);
|
||||
const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
|
||||
// --- 查询参数准备阶段 ---
|
||||
const finalParams = {};
|
||||
const { filters } = this.state;
|
||||
|
||||
if (success && typeof data === 'object') {
|
||||
// 1. 复制所有非 Set 类型的参数
|
||||
Object.keys(filters).forEach(key => {
|
||||
if (!(filters[key] instanceof Set)) {
|
||||
finalParams[key] = filters[key];
|
||||
}
|
||||
});
|
||||
// 2. 翻译 'error_types'
|
||||
const translatedErrorCodes = new Set();
|
||||
// 从用户直接选择的状态码开始初始化
|
||||
const translatedStatusCodes = 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);
|
||||
}
|
||||
for (const [code, obj] of Object.entries(STATIC_ERROR_MAP)) {
|
||||
if (obj.type === type) translatedErrorCodes.add(code);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 3. 统一处理所有 Set 类型的参数,转换为字符串
|
||||
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(',');
|
||||
// 4. 清理空值
|
||||
Object.keys(finalParams).forEach(key => {
|
||||
if (finalParams[key] === '' || finalParams[key] === null || finalParams[key] === undefined) {
|
||||
delete finalParams[key];
|
||||
}
|
||||
});
|
||||
const query = new URLSearchParams(finalParams);
|
||||
const { success, data } = await apiFetchJson(`/admin/logs?${query.toString()}`);
|
||||
if (success && typeof data === 'object' && data.items) {
|
||||
const { items, total, page, page_size } = data;
|
||||
this.state.logs = items;
|
||||
this.state.pagination = { page, page_size, total, pages: Math.ceil(total / page_size) };
|
||||
|
||||
// [核心] 在渲染前,按需批量加载本页日志所需的、尚未缓存的Key信息
|
||||
const totalPages = Math.ceil(total / page_size);
|
||||
this.state.pagination = { page, page_size, total, pages: totalPages > 0 ? totalPages : 1 };
|
||||
await this.enrichLogsWithKeyNames(items);
|
||||
|
||||
// 调用 render,此时 dataStore 中已包含所有需要的数据
|
||||
this.logList.render(this.state.logs, this.state.pagination);
|
||||
this.logList.render(this.state.logs, this.state.pagination, this.state.selectedLogIds);
|
||||
} else {
|
||||
this.state.logs = [];
|
||||
this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
|
||||
this.logList.render([], this.state.pagination);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load logs:", error);
|
||||
this.state.logs = [];
|
||||
this.state.pagination = { ...this.state.pagination, total: 0, pages: 1, page: 1 };
|
||||
this.logList.render([], this.state.pagination);
|
||||
} finally {
|
||||
this.state.isLoading = false;
|
||||
this.updatePaginationUI();
|
||||
this.syncSelectionUI();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Filename: frontend/js/pages/logs/logList.js
|
||||
import { escapeHTML } from '../../utils/utils.js';
|
||||
|
||||
const STATIC_ERROR_MAP = {
|
||||
export const STATIC_ERROR_MAP = {
|
||||
'API_KEY_INVALID': { type: '密钥无效', style: 'red' },
|
||||
'INVALID_ARGUMENT': { type: '参数无效', style: 'red' },
|
||||
'PERMISSION_DENIED': { type: '权限不足', style: 'red' },
|
||||
@@ -13,8 +13,8 @@ const STATIC_ERROR_MAP = {
|
||||
'INTERNAL': { type: 'Google内部错误', style: 'yellow' },
|
||||
'UNAVAILABLE': { type: '服务不可用', style: 'yellow' },
|
||||
};
|
||||
// --- [更新] HTTP状态码到类型和样式的动态映射表 ---
|
||||
const STATUS_CODE_MAP = {
|
||||
// --- HTTP状态码到类型和样式的动态映射表 ---
|
||||
export const STATUS_CODE_MAP = {
|
||||
400: { type: '错误请求', style: 'red' },
|
||||
401: { type: '认证失败', style: 'red' },
|
||||
403: { type: '禁止访问', style: 'red' },
|
||||
@@ -55,7 +55,7 @@ class LogList {
|
||||
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> 加载日志中...</td></tr>`;
|
||||
}
|
||||
|
||||
render(logs, pagination) {
|
||||
render(logs, pagination, selectedLogIds) {
|
||||
if (!this.container) return;
|
||||
if (!logs || logs.length === 0) {
|
||||
this.container.innerHTML = `<tr><td colspan="9" class="p-8 text-center text-muted-foreground">没有找到相关的日志记录。</td></tr>`;
|
||||
@@ -63,7 +63,10 @@ class LogList {
|
||||
}
|
||||
const { page, page_size } = pagination;
|
||||
const startIndex = (page - 1) * page_size;
|
||||
const logsHtml = logs.map((log, index) => this.createLogRowHtml(log, startIndex + index + 1)).join('');
|
||||
const logsHtml = logs.map((log, index) => {
|
||||
const isChecked = selectedLogIds.has(log.ID);
|
||||
return this.createLogRowHtml(log, startIndex + index + 1, isChecked);
|
||||
}).join('');
|
||||
this.container.innerHTML = logsHtml;
|
||||
}
|
||||
|
||||
@@ -125,7 +128,7 @@ class LogList {
|
||||
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) {
|
||||
createLogRowHtml(log, index, isChecked) {
|
||||
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);
|
||||
@@ -140,9 +143,13 @@ class LogList {
|
||||
const modelNameFormatted = this._formatModelName(log.ModelName);
|
||||
const errorMessageAttr = log.ErrorMessage ? `data-error-message="${escape(log.ErrorMessage)}"` : '';
|
||||
const requestTime = new Date(log.RequestTime).toLocaleString();
|
||||
|
||||
const checkedAttr = isChecked ? 'checked' : '';
|
||||
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">
|
||||
<input type="checkbox" class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500" ${checkedAttr}>
|
||||
</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>
|
||||
@@ -159,5 +166,4 @@ class LogList {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default LogList;
|
||||
|
||||
6
frontend/js/vendor/popper.esm.min.js
vendored
Normal file
6
frontend/js/vendor/popper.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user