Update: Js 4 Log.html

This commit is contained in:
XOF
2025-11-26 01:46:47 +08:00
parent 04d36e4d9e
commit 01c9b34600
13 changed files with 3675 additions and 1459 deletions

View File

@@ -6,12 +6,13 @@ import { debounce } from '../../utils/utils.js';
import FilterPopover from '../../components/filterPopover.js';
import { STATIC_ERROR_MAP, STATUS_CODE_MAP } from './logList.js';
import SystemLogTerminal from './systemLog.js';
import { initBatchActions } from './batchActions.js';
import flatpickr from '../../vendor/flatpickr.js';
const dataStore = {
groups: new Map(),
keys: new Map(),
};
};
class LogsPage {
constructor() {
this.state = {
@@ -25,7 +26,9 @@ class LogsPage {
key_ids: new Set(),
group_ids: new Set(),
error_types: new Set(),
status_codes: new Set(),
status_codes: new Set(),
start_date: null,
end_date: null,
},
selectedLogIds: new Set(),
currentView: 'error',
@@ -43,6 +46,8 @@ class LogsPage {
this.logList = null;
this.systemLogTerminal = null;
this.debouncedLoadAndRender = debounce(() => this.loadAndRenderLogs(), 300);
this.fp = null;
this.themeObserver = null;
}
}
async init() {
@@ -69,6 +74,16 @@ class LogsPage {
this.systemLogTerminal.disconnect();
this.systemLogTerminal = null;
}
if (this.fp) {
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') {
@@ -100,13 +115,42 @@ class LogsPage {
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.elements.dateRangeFilterBtn = document.getElementById('filter-date-range-btn');
this.logList = new LogList(this.elements.tableBody, dataStore);
const selectContainer = document.querySelector('[data-component="custom-select-v2"]');
if (selectContainer) { new CustomSelectV2(selectContainer); }
this.initFilterPopovers();
this.initDateRangePicker();
this.initEventListeners();
this._observeThemeChanges();
initBatchActions(this);
this.loadAndRenderLogs();
}
_observeThemeChanges() {
const applyTheme = () => {
if (!this.fp || !this.fp.calendarContainer) return;
if (document.documentElement.classList.contains('dark')) {
this.fp.calendarContainer.classList.add('dark');
} else {
this.fp.calendarContainer.classList.remove('dark');
}
};
this.themeObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
applyTheme();
}
}
});
this.themeObserver.observe(document.documentElement, { attributes: true });
// 确保初始状态正确
applyTheme();
}
_initSystemLogView() {
this.systemLogTerminal = new SystemLogTerminal(
this.elements.contentContainer,
@@ -116,9 +160,7 @@ class LogsPage {
width: '20rem',
backdrop: `rgba(0,0,0,0.5)`,
heightAuto: false,
customClass: {
popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}`
},
customClass: { popup: `swal2-custom-style ${document.documentElement.classList.contains('dark') ? 'swal2-dark' : ''}` },
title: '系统终端日志',
text: '您即将连接到实时系统日志流窗口。',
showCancelButton: true,
@@ -153,6 +195,83 @@ class LogsPage {
new FilterPopover(this.elements.errorCodeFilterBtn, statusCodeOptions, '筛选状态码');
}
}
initDateRangePicker() {
if (!this.elements.dateRangeFilterBtn) return;
const buttonTextSpan = this.elements.dateRangeFilterBtn.querySelector('span');
const originalText = buttonTextSpan.textContent;
this.fp = flatpickr(this.elements.dateRangeFilterBtn, {
mode: 'range',
dateFormat: 'Y-m-d',
onClose: (selectedDates) => {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
end.setHours(23, 59, 59, 999);
this.state.filters.start_date = start.toISOString();
this.state.filters.end_date = end.toISOString();
const startDateStr = start.toISOString().split('T')[0];
const endDateStr = end.toISOString().split('T')[0];
buttonTextSpan.textContent = `${startDateStr} ~ ${endDateStr}`;
this.elements.dateRangeFilterBtn.classList.add('!border-primary', '!text-primary');
this.state.filters.page = 1;
this.loadAndRenderLogs();
}
},
onReady: (selectedDates, dateStr, instance) => {
// 暗黑模式和清除按钮的现有逻辑保持不变
if (document.documentElement.classList.contains('dark')) {
instance.calendarContainer.classList.add('dark');
}
const clearButton = document.createElement('button');
clearButton.textContent = '清除';
clearButton.className = 'button flatpickr-button flatpickr-clear-button';
clearButton.addEventListener('click', (e) => {
e.preventDefault();
instance.clear();
this.state.filters.start_date = null;
this.state.filters.end_date = null;
buttonTextSpan.textContent = originalText;
this.elements.dateRangeFilterBtn.classList.remove('!border-primary', '!text-primary');
this.state.filters.page = 1;
this.loadAndRenderLogs();
instance.close();
});
instance.calendarContainer.appendChild(clearButton);
const nativeMonthSelect = instance.monthsDropdownContainer;
if (!nativeMonthSelect) return;
const monthYearContainer = nativeMonthSelect.parentElement;
const wrapper = document.createElement('div');
wrapper.className = 'custom-select-v2-container relative inline-block text-left';
wrapper.innerHTML = `
<button type="button" class="custom-select-trigger inline-flex justify-center items-center w-22 gap-x-1.5 rounded-md bg-transparent px-1 py-1 text-xs font-semibold text-foreground shadow-sm ring-0 ring-inset ring-input hover:bg-accent focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-offset-background focus:ring-primary" aria-haspopup="true">
<span class="truncate"></span>
</button>
`;
const template = document.createElement('template');
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>
`;
nativeMonthSelect.classList.add('hidden');
wrapper.appendChild(nativeMonthSelect);
wrapper.appendChild(template);
monthYearContainer.prepend(wrapper);
const customSelect = new CustomSelectV2(wrapper);
instance.customMonthSelect = customSelect;
},
onMonthChange: (selectedDates, dateStr, instance) => {
if (instance.customMonthSelect) {
instance.customMonthSelect.updateTriggerText();
}
},
});
}
initEventListeners() {
if (this.elements.pageSizeSelect) {
this.elements.pageSizeSelect.addEventListener('change', (e) => this.changePageSize(parseInt(e.target.value, 10)));
@@ -191,16 +310,12 @@ class LogsPage {
this.state.filters.page = 1;
this.loadAndRenderLogs();
}
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;
@@ -219,13 +334,11 @@ class LogsPage {
});
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;
@@ -238,7 +351,6 @@ class LogsPage {
}
this.syncSelectionUI();
}
handleSelectAllChange(event) {
const isChecked = event.target.checked;
this.state.logs.forEach(log => {
@@ -251,36 +363,35 @@ class LogsPage {
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; // 半选状态
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;
const hasSelection = selectedCount > 0;
const deleteSelectedBtn = document.getElementById('delete-selected-logs-btn');
if (deleteSelectedBtn) {
deleteSelectedBtn.disabled = !hasSelection;
}
}
changePageSize(newSize) {
this.state.filters.page_size = newSize;
this.state.filters.page = 1;
@@ -291,18 +402,15 @@ class LogsPage {
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;
@@ -312,7 +420,6 @@ class LogsPage {
this.elements.paginationBtns[3].disabled = isLastPage;
}
}
async loadGroupsOnce() {
if (dataStore.groups.size > 0) return;
try {
@@ -324,7 +431,6 @@ class LogsPage {
console.error("Failed to load key groups:", error);
}
}
async loadAndRenderLogs() {
this.state.isLoading = true;
this.state.selectedLogIds.clear();
@@ -332,19 +438,15 @@ class LogsPage {
this.updatePaginationUI();
this.syncSelectionUI();
try {
// --- 查询参数准备阶段 ---
const finalParams = {};
const { filters } = this.state;
// 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 => {
@@ -356,19 +458,23 @@ class LogsPage {
}
});
}
// 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()}`);
const { success, data } = await apiFetchJson(
`/admin/logs?${query.toString()}`,
{ cache: 'no-cache', noCache: true }
);
if (success && typeof data === 'object' && data.items) {
const { items, total, page, page_size } = data;
this.state.logs = items;
@@ -392,10 +498,9 @@ class LogsPage {
this.syncSelectionUI();
}
}
async enrichLogsWithKeyNames(logs) {
const missingKeyIds = [...new Set(
logs.filter(log => log.KeyID && !dataStore.keys.has(log.KeyID)).map(log => log.KeyID)
logs.filter(log => log.KeyID && !dataStore.keys.has(log.KeyID)).map(log => log.ID)
)];
if (missingKeyIds.length === 0) return;
try {
@@ -409,8 +514,7 @@ class LogsPage {
}
}
}
export default function() {
const page = new LogsPage();
page.init();
}
}