优化流式传输&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

@@ -5,17 +5,19 @@ 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';
import SystemLogTerminal from './systemLog.js';
const dataStore = {
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,
@@ -26,33 +28,106 @@ class LogsPage {
status_codes: new Set(),
},
selectedLogIds: 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(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(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: '实时系统日志',
text: '您即将连接到实时日志流。这会与服务器建立一个持续的连接。',
icon: 'info',
confirmButtonText: '我明白了,开始连接',
showCancelButton: true,
cancelButtonText: '取消',
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

@@ -0,0 +1,157 @@
// Filename: frontend/js/pages/logs/systemLog.js
export default class SystemLogTerminal {
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', '正在连接到实时日志流...');
this._updateStatus('connecting', '连接中...');
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', '✓ 已连接到系统日志流');
this._updateStatus('connected', '已连接');
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', `✗ WebSocket 错误`);
this._updateStatus('error', '连接错误');
};
this.ws.onclose = () => {
this._appendMessage('error', '✗ 连接已断开');
this._updateStatus('disconnected', '未连接');
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => {
this._appendMessage('info', `尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
this.connect();
}, 3000);
}
};
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.reconnectAttempts = this.maxReconnectAttempts;
this._updateStatus('disconnected', '未连接');
}
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 = '继续';
icon.classList.replace('fa-pause', 'fa-play');
} else {
span.textContent = '暂停';
icon.classList.replace('fa-play', 'fa-pause');
}
}
toggleAutoScroll() {
this.shouldAutoScroll = !this.shouldAutoScroll;
const span = this.elements.scrollBtn.querySelector('span');
if (this.shouldAutoScroll) {
span.textContent = '自动滚动';
} else {
span.textContent = '手动滚动';
}
}
_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}`;
}
}
}