Files
myaps_api/static/monitor/live-logs.html
T

1012 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n-page-title="page.live_logs">实时日志 - 系统监控</title>
<link rel="icon" href="/static/swagger/favicon.png" type="image/png">
<link rel="stylesheet" href="/static/monitor/css/monitor.css">
<script src="/static/lib/i18n/i18n.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f7fa;
margin: 0;
padding: 20px;
}
.live-logs-container {
max-width: 100%;
margin: 0 auto;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
overflow: hidden;
}
.live-logs-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.live-logs-header-left {
display: flex;
align-items: center;
gap: 20px;
}
.live-logs-header h1 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.connection-status {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e74c3c;
}
.status-dot.connected {
background: #2ecc71;
}
.status-dot.connecting {
background: #f39c12;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.header-controls {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.header-control-group {
display: flex;
align-items: center;
gap: 6px;
}
.header-control-group label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
}
.header-control-group select,
.header-control-group input {
padding: 5px 10px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
font-size: 12px;
background: rgba(255, 255, 255, 0.15);
color: white;
}
.header-control-group input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.header-control-group select option {
background: #667eea;
color: white;
}
.toggle-switch {
position: relative;
width: 44px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.3);
transition: 0.3s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #2ecc71;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(20px);
}
.btn {
padding: 6px 14px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn-primary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.3);
}
.btn-secondary {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.25);
}
.btn-danger {
background: rgba(220, 53, 69, 0.8);
color: white;
}
.btn-danger:hover {
background: rgba(220, 53, 69, 1);
}
.logs-container {
height: calc(100vh - 180px);
min-height: 400px;
overflow-y: auto;
padding: 15px 30px;
background: #1e1e1e;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 15px;
line-height: 1.7;
}
.log-entry {
padding: 6px 0;
border-bottom: 1px solid #2d2d2d;
display: flex;
gap: 15px;
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: #888;
white-space: nowrap;
width: 85px;
flex-shrink: 0;
}
.log-level {
font-weight: 600;
width: 70px;
text-align: center;
flex-shrink: 0;
}
.log-level.DEBUG { color: #9e9e9e; }
.log-level.INFO { color: #4fc3f7; }
.log-level.WARNING { color: #ffb74d; }
.log-level.ERROR { color: #ef5350; }
.log-level.CRITICAL { color: #e57373; background: #3e2723; padding: 0 6px; border-radius: 3px; }
.log-module {
color: #9e9e9e;
width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.log-message {
color: #e0e0e0;
flex: 1;
word-break: break-all;
}
.log-entry.DEBUG .log-message {
color: #a0a0a0;
}
.log-entry.INFO .log-message {
color: #4fc3f7;
}
.log-entry.WARNING .log-message {
color: #ffb74d;
}
.log-entry.ERROR .log-message {
color: #ef5350;
}
.log-entry.CRITICAL .log-message {
color: #e57373;
font-weight: 500;
}
.no-logs {
text-align: center;
color: #666;
padding: 60px 20px;
}
.log-stats {
display: flex;
gap: 20px;
padding: 10px 30px;
background: #2d2d2d;
color: #888;
font-size: 12px;
}
.log-stats span {
display: flex;
align-items: center;
gap: 5px;
}
.level-count {
padding: 2px 6px;
border-radius: 3px;
font-weight: 500;
}
.level-count.DEBUG { background: #424242; color: #888; }
.level-count.INFO { background: #1565c0; color: #4fc3f7; }
.level-count.WARNING { background: #e65100; color: #ffb74d; }
.level-count.ERROR { background: #b71c1c; color: #ef5350; }
.level-count.CRITICAL { background: #3e2723; color: #e57373; }
.auto-scroll-indicator {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(102, 126, 234, 0.9);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
display: none;
}
.auto-scroll-indicator.show {
display: block;
}
/* 失活超时模态框 */
.inactivity-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
justify-content: center;
align-items: center;
}
.inactivity-modal.show {
display: flex;
}
.inactivity-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(3px);
}
.inactivity-content {
position: relative;
background: #2d2d2d;
border-radius: 12px;
padding: 30px;
min-width: 400px;
max-width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
text-align: center;
}
.inactivity-content .modal-icon {
font-size: 48px;
margin-bottom: 15px;
}
.inactivity-content h3 {
color: #ffb74d;
margin: 0 0 10px 0;
font-size: 20px;
}
.inactivity-content p {
color: #a0a0a0;
margin: 8px 0;
font-size: 14px;
}
.inactivity-content .btn {
margin-top: 20px;
padding: 10px 30px;
font-size: 14px;
}
</style>
</head>
<body>
<div class="live-logs-container">
<div class="live-logs-header">
<div class="live-logs-header-left">
<h3 data-i18n="monitor.page.live_logs">实时日志流</h3>
<div class="connection-status">
<div class="status-dot connecting" id="statusDot"></div>
<span id="statusText" data-i18n="monitor.status.connecting">连接中...</span>
</div>
</div>
<div class="header-controls">
<select id="lang-selector" class="lang-selector" onchange="i18n.switchLanguage(this.value)" style="margin-right: 12px;">
<option value="zh-CN">🇨🇳 中文</option>
<option value="en-US">🇺🇸 English</option>
<option value="de-DE">🇩🇪 Deutsch</option>
</select>
<div class="header-control-group">
<label data-i18n="monitor.filter.level">级别:</label>
<select id="levelFilter" onchange="applyFilters()">
<option value="" data-i18n="monitor.filter.level">全部</option>
<option value="INFO" selected>INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="CRITICAL">CRITICAL</option>
</select>
</div>
<div class="header-control-group">
<input type="text" id="searchInput" data-i18n-placeholder="filter.keyword_placeholder" placeholder="搜索日志..." onkeyup="handleSearch(event)">
</div>
<div class="header-control-group">
<label data-i18n="monitor.other.auto_scroll">自动滚动</label>
<label class="toggle-switch">
<input type="checkbox" id="autoScroll" checked onchange="toggleAutoScroll()">
<span class="toggle-slider"></span>
</label>
</div>
<button class="btn btn-secondary" onclick="togglePause()">
<span id="pauseBtnText" data-i18n="monitor.btn.pause">暂停</span>
</button>
<button class="btn btn-danger" onclick="clearLogs()" data-i18n="monitor.btn.clear">清空</button>
<button class="btn btn-primary" onclick="openInNewWindow()" data-i18n="monitor.btn.new_window">新窗口</button>
</div>
</div>
<div class="log-stats">
<span data-i18n="monitor.metric.total">总日志: <strong id="totalCount">0</strong></span>
<span>DEBUG: <span class="level-count DEBUG" id="debugCount">0</span></span>
<span>INFO: <span class="level-count INFO" id="infoCount">0</span></span>
<span>WARNING: <span class="level-count WARNING" id="warningCount">0</span></span>
<span>ERROR: <span class="level-count ERROR" id="errorCount">0</span></span>
<span>CRITICAL: <span class="level-count CRITICAL" id="criticalCount">0</span></span>
</div>
<div class="logs-container" id="logsContainer">
<div class="no-logs" id="noLogs" data-i18n="monitor.other.waiting_logs">
正在等待日志数据...
</div>
</div>
</div>
<div class="auto-scroll-indicator" id="autoScrollIndicator">
⬇️ 已暂停自动滚动 - 点击滚动到底部
</div>
<!-- 失活超时模态框 -->
<div class="inactivity-modal" id="inactivityModal">
<div class="inactivity-overlay"></div>
<div class="inactivity-content">
<div class="modal-icon"></div>
<h3>日志流已暂停</h3>
<p>由于长时间未活动,日志流已自动暂停以节省资源。</p>
<p>如需继续查看实时日志,请点击下方按钮恢复连接。</p>
<button class="btn btn-primary" onclick="resumeLiveLogs()">恢复连接</button>
</div>
</div>
<script>
let eventSource = null;
let logs = [];
const MAX_LOGS = 500; // 前端日志数量上限,控制内存占用
let isPaused = false;
let autoScroll = true;
let searchTerm = '';
let levelFilter = 'INFO';
let stats = { DEBUG: 0, INFO: 0, WARNING: 0, ERROR: 0, CRITICAL: 0 };
let lastActivity = Date.now();
const INACTIVITY_TIMEOUT = 180 * 60 * 1000; // 180分钟超时
// SSE重连配置
let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;
const BASE_RECONNECT_DELAY = 1000; // 基础重连延迟1秒
const MAX_RECONNECT_DELAY = 30000; // 最大重连延迟30秒
// 心跳检测配置
let lastHeartbeatTime = Date.now();
const HEARTBEAT_TIMEOUT = 60000; // 心跳超时60秒
let heartbeatTimer = null;
let isDomReady = false;
let isRendering = false; // 防止渲染冲突
let pendingRerender = false; // 标记是否有待处理的重渲染
const levelPriority = { DEBUG: 10, INFO: 20, WARNING: 30, ERROR: 40, CRITICAL: 50 }; // 全局级别优先级
let inactivityTimer = null; // 失活检查定时器引用
let eventListeners = []; // 存储事件监听器引用,用于清理
function checkDomReady() {
const container = document.getElementById('logsContainer');
const noLogs = document.getElementById('noLogs');
const statusDot = document.getElementById('statusDot');
const statusText = document.getElementById('statusText');
if (container && noLogs && statusDot && statusText) {
isDomReady = true;
console.log('DOM元素已就绪');
return true;
}
return false;
}
function connect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
// 如果暂停状态,不建立连接
if (isPaused) {
return;
}
if (isDomReady) {
updateStatus('connecting');
}
const sseUrl = `/monitor/api/live-logs/stream?_=${Date.now()}`;
console.log('正在连接 SSE:', sseUrl);
eventSource = new EventSource(sseUrl);
eventSource.onopen = () => {
// 连接成功,重置重连计数器
reconnectAttempts = 0;
lastHeartbeatTime = Date.now();
updateStatus('connected');
// 启动心跳检测
startHeartbeatCheck();
};
eventSource.onmessage = (event) => {
if (isPaused) return;
// 更新心跳时间
lastHeartbeatTime = Date.now();
try {
if (!event.data) {
console.warn('收到空数据');
return;
}
// 处理心跳消息
if (event.data === 'heartbeat') {
console.debug('收到心跳');
return;
}
const logData = JSON.parse(event.data);
if (!logData || !logData.level || !logData.message) {
console.warn('无效的日志数据:', logData);
return;
}
addLog(logData);
} catch (e) {
console.error('解析日志数据失败:', e);
}
};
eventSource.onerror = (error) => {
console.error('SSE 连接错误:', error);
// 停止心跳检测
stopHeartbeatCheck();
if (isDomReady) {
updateStatus('disconnected');
}
// 使用指数退避策略重连
scheduleReconnect();
};
}
// 计算指数退避延迟
function getReconnectDelay() {
const delay = Math.min(
BASE_RECONNECT_DELAY * Math.pow(2, reconnectAttempts),
MAX_RECONNECT_DELAY
);
// 添加随机抖动,避免多个客户端同时重连
const jitter = delay * 0.1 * (Math.random() - 0.5);
return delay + jitter;
}
// 调度重连
function scheduleReconnect() {
if (isPaused) {
console.log('当前处于暂停状态,跳过重连');
return;
}
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error(`已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`);
if (isDomReady) {
const statusText = document.getElementById('statusText');
if (statusText) {
statusText.textContent = i18n.t('monitor.error.connection_failed');
}
}
return;
}
const delay = getReconnectDelay();
reconnectAttempts++;
console.log(`${reconnectAttempts} 次重连,延迟 ${Math.round(delay)}ms`);
setTimeout(() => {
// 再次检查状态
if (!isPaused) {
connect();
}
}, delay);
}
// 启动心跳检测
function startHeartbeatCheck() {
stopHeartbeatCheck();
heartbeatTimer = setInterval(() => {
const now = Date.now();
if (now - lastHeartbeatTime > HEARTBEAT_TIMEOUT) {
console.warn('心跳超时,尝试重连');
if (eventSource) {
eventSource.close();
eventSource = null;
}
scheduleReconnect();
}
}, 10000); // 每10秒检查一次
}
// 停止心跳检测
function stopHeartbeatCheck() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
function updateStatus(status) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
if (!dot || !text) {
return;
}
dot.className = 'status-dot';
switch (status) {
case 'connected':
dot.classList.add('connected');
text.textContent = i18n.t('monitor.status.connected');
break;
case 'connecting':
dot.classList.add('connecting');
text.textContent = i18n.t('monitor.status.connecting');
break;
case 'disconnected':
text.textContent = i18n.t('monitor.status.disconnected') + ' (' + i18n.t('monitor.other.auto_reconnect') + ')';
break;
}
}
let lastRenderIndex = 0; // 记录上次渲染到的索引
function addLog(logData) {
logs.push(logData);
// 限制前端日志数量,防止内存泄漏
if (logs.length > MAX_LOGS) {
const removeCount = logs.length - MAX_LOGS;
// 同步更新统计
for (let i = 0; i < removeCount; i++) {
const removedLog = logs[i];
if (stats[removedLog.level] > 0) {
stats[removedLog.level]--;
}
}
logs.splice(0, removeCount);
lastRenderIndex = Math.max(0, lastRenderIndex - removeCount);
}
stats[logData.level] = (stats[logData.level] || 0) + 1;
if (isDomReady) {
updateStats();
scheduleRender(); // 使用调度渲染代替直接渲染
}
}
let renderDebounceTimer = null;
function scheduleRender() {
// 如果正在渲染,标记待处理
if (isRendering) {
pendingRerender = true;
return;
}
// 防抖:16ms(约1帧)内合并新的渲染请求,平衡性能和响应速度
clearTimeout(renderDebounceTimer);
renderDebounceTimer = setTimeout(() => {
requestAnimationFrame(() => {
isRendering = true;
renderLogs();
isRendering = false;
// 如果有新的待处理渲染请求
if (pendingRerender) {
pendingRerender = false;
scheduleRender();
}
});
}, 16);
}
function updateStats() {
const totalCount = document.getElementById('totalCount');
const debugCount = document.getElementById('debugCount');
const infoCount = document.getElementById('infoCount');
const warningCount = document.getElementById('warningCount');
const errorCount = document.getElementById('errorCount');
const criticalCount = document.getElementById('criticalCount');
if (!totalCount || !debugCount || !infoCount || !warningCount || !errorCount || !criticalCount) {
return;
}
totalCount.textContent = logs.length;
debugCount.textContent = stats.DEBUG || 0;
infoCount.textContent = stats.INFO || 0;
warningCount.textContent = stats.WARNING || 0;
errorCount.textContent = stats.ERROR || 0;
criticalCount.textContent = stats.CRITICAL || 0;
}
function createLogElement(log) {
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${log.level}`;
const timeSpan = document.createElement('span');
timeSpan.className = 'log-time';
timeSpan.textContent = new Date(log.timestamp * 1000).toLocaleTimeString('zh-CN');
const levelSpan = document.createElement('span');
levelSpan.className = `log-level ${log.level}`;
levelSpan.textContent = log.level;
const moduleSpan = document.createElement('span');
moduleSpan.className = 'log-module';
moduleSpan.textContent = log.module;
moduleSpan.title = log.module;
const messageSpan = document.createElement('span');
messageSpan.className = 'log-message';
messageSpan.textContent = log.message;
logEntry.appendChild(timeSpan);
logEntry.appendChild(levelSpan);
logEntry.appendChild(moduleSpan);
logEntry.appendChild(messageSpan);
return logEntry;
}
function renderLogs() {
const container = document.getElementById('logsContainer');
if (!container) {
console.error('日志容器元素 logsContainer 未找到');
return;
}
if (logs.length === 0) {
container.innerHTML = '<div class="no-logs" id="noLogs">' + i18n.t('monitor.other.waiting_logs') + '</div>';
lastRenderIndex = 0;
return;
}
const hasFilter = levelFilter || searchTerm;
if (hasFilter) {
// 有过滤器时,需要全量渲染
const filteredLogs = logs.filter(log => {
if (levelFilter && levelPriority[log.level] < levelPriority[levelFilter]) return false;
if (searchTerm && !log.message.toLowerCase().includes(searchTerm.toLowerCase())) return false;
return true;
});
if (filteredLogs.length === 0) {
container.innerHTML = '<div class="no-logs" id="noLogs">' + i18n.t('monitor.other.no_matching_logs') + '</div>';
lastRenderIndex = 0;
return;
}
// 使用 DocumentFragment 提高 DOM 操作效率
const fragment = document.createDocumentFragment();
for (const log of filteredLogs) {
fragment.appendChild(createLogElement(log));
}
container.innerHTML = '';
container.appendChild(fragment);
lastRenderIndex = logs.length;
if (autoScroll && !isPaused) {
container.scrollTop = container.scrollHeight;
}
} else {
// 无过滤器时,使用增量更新
const newLogs = logs.slice(lastRenderIndex);
if (newLogs.length > 0) {
const fragment = document.createDocumentFragment();
for (const log of newLogs) {
fragment.appendChild(createLogElement(log));
}
container.appendChild(fragment);
lastRenderIndex = logs.length;
}
// 检查是否需要清理超出显示范围的旧日志(保持DOM节点数量合理)
const maxDomEntries = 500; // 最多保持500个DOM节点
const childCount = container.children.length;
if (childCount > maxDomEntries) {
const removeCount = childCount - maxDomEntries;
for (let i = 0; i < removeCount; i++) {
container.removeChild(container.firstChild);
}
}
if (autoScroll && !isPaused) {
container.scrollTop = container.scrollHeight;
}
}
}
function applyFilters() {
const levelFilterEl = document.getElementById('levelFilter');
const searchInputEl = document.getElementById('searchInput');
if (levelFilterEl) {
levelFilter = levelFilterEl.value;
}
if (searchInputEl) {
searchTerm = searchInputEl.value;
}
scheduleRender();
}
function handleSearch(event) {
if (event.key === 'Enter') {
applyFilters();
}
}
function togglePause() {
isPaused = !isPaused;
const pauseBtnText = document.getElementById('pauseBtnText');
const autoScrollIndicator = document.getElementById('autoScrollIndicator');
if (pauseBtnText) {
pauseBtnText.textContent = isPaused ? i18n.t('monitor.btn.resume') : i18n.t('monitor.btn.pause');
}
if (autoScrollIndicator) {
autoScrollIndicator.classList.toggle('show', isPaused);
}
if (isPaused) {
// 暂停时关闭 SSE 连接,节省资源
stopHeartbeatCheck();
if (eventSource) {
eventSource.close();
eventSource = null;
if (isDomReady) {
updateStatus('disconnected');
}
}
// 重置重连计数器
reconnectAttempts = 0;
} else {
// 继续时重新建立连接
reconnectAttempts = 0;
connect();
}
}
function toggleAutoScroll() {
const autoScrollEl = document.getElementById('autoScroll');
if (autoScrollEl) {
autoScroll = autoScrollEl.checked;
}
if (autoScroll && !isPaused) {
const container = document.getElementById('logsContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
}
}
function clearLogs() {
logs = [];
stats = { DEBUG: 0, INFO: 0, WARNING: 0, ERROR: 0, CRITICAL: 0 };
lastRenderIndex = 0;
updateStats();
const logsContainer = document.getElementById('logsContainer');
if (logsContainer) {
logsContainer.innerHTML = '<div class="no-logs">' + i18n.t('monitor.success.logs_cleared') + '</div>';
}
}
function openInNewWindow() {
window.open('/monitor/live-logs', '_blank');
}
const logsContainer = document.getElementById('logsContainer');
const autoScrollIndicator = document.getElementById('autoScrollIndicator');
// 添加事件监听器并存储引用(使用唯一名称避免覆盖全局方法)
function bindEventListener(el, event, handler) {
el.addEventListener(event, handler);
eventListeners.push({ el, event, handler });
}
if (logsContainer) {
bindEventListener(logsContainer, 'click', () => {
if (isPaused) {
togglePause();
}
});
}
if (autoScrollIndicator) {
bindEventListener(autoScrollIndicator, 'click', () => {
if (isPaused) {
togglePause();
}
const autoScrollEl = document.getElementById('autoScroll');
if (autoScrollEl) {
autoScrollEl.checked = true;
}
autoScroll = true;
const container = document.getElementById('logsContainer');
if (container) {
container.scrollTop = container.scrollHeight;
}
});
}
// 每10秒检查一次失活状态
inactivityTimer = setInterval(() => {
if (Date.now() - lastActivity >= INACTIVITY_TIMEOUT && !isPaused) {
handleInactivityTimeout();
}
}, 10000);
// 监听用户活动,重置失活计时器
const activityHandler = () => { lastActivity = Date.now(); };
bindEventListener(window, 'mousemove', activityHandler);
bindEventListener(window, 'keydown', activityHandler);
bindEventListener(window, 'scroll', activityHandler);
bindEventListener(window, 'click', activityHandler);
// 处理失活超时
function handleInactivityTimeout() {
togglePause();
showInactivityModal();
}
// 显示失活模态框
function showInactivityModal() {
const modal = document.getElementById('inactivityModal');
if (modal) {
modal.classList.add('show');
}
}
// 隐藏失活模态框
function hideInactivityModal() {
const modal = document.getElementById('inactivityModal');
if (modal) {
modal.classList.remove('show');
}
}
// 恢复日志流
function resumeLiveLogs() {
hideInactivityModal();
togglePause();
lastActivity = Date.now();
}
// 清理所有事件监听器
function cleanupEventListeners() {
eventListeners.forEach(({ el, event, handler }) => {
el.removeEventListener(event, handler);
});
eventListeners = [];
}
// 页面卸载时清理资源,防止内存泄漏
window.addEventListener('beforeunload', () => {
// 关闭 SSE 连接
if (eventSource) {
eventSource.close();
eventSource = null;
}
// 清理定时器
if (inactivityTimer) {
clearInterval(inactivityTimer);
inactivityTimer = null;
}
// 清理心跳检测定时器
stopHeartbeatCheck();
// 清理渲染防抖定时器
if (renderDebounceTimer) {
clearTimeout(renderDebounceTimer);
renderDebounceTimer = null;
}
// 清理事件监听器
cleanupEventListeners();
});
async function checkServiceStatus() {
try {
const response = await fetch('/monitor/api/live-logs/status');
const status = await response.json();
console.log('日志流服务状态:', status);
return status.is_running;
} catch (error) {
console.error('检查服务状态失败:', error);
return false;
}
}
document.addEventListener('DOMContentLoaded', async () => {
// DOMContentLoaded 事件触发时,DOM已经就绪
if (checkDomReady()) {
updateStats();
renderLogs();
}
// 立即连接,不要延迟
connect();
// 检查服务状态(仅用于日志输出)
const serviceStatus = await checkServiceStatus();
console.log('日志流服务是否运行:', serviceStatus);
});
</script>
</body>
</html>