Update: Js 4 Log.html
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user