document.addEventListener('DOMContentLoaded', function() { // 1. 首先初始化所有变量 const tpsHistory = Array(30).fill(20); const msptHistory = Array(30).fill(50); let memoryChart = null; let performanceChart = null; // 2. 获取DOM元素(带安全检查) const elements = { refreshBtn: document.getElementById('refresh-btn'), themeToggle: document.getElementById('theme-toggle'), statsTable: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], whitelistPlayers: document.getElementById('whitelist-players'), nonWhitelistPlayers: document.getElementById('non-whitelist-players'), totalCount: document.getElementById('total-count'), whitelistCount: document.getElementById('whitelist-count'), nonWhitelistCount: document.getElementById('non-whitelist-count'), memoryUsed: document.getElementById('memory-used'), memoryFree: document.getElementById('memory-free'), memoryPercent: document.getElementById('memory-percent'), avgTick: document.getElementById('avg-tick'), uptime: document.getElementById('uptime'), tpsValue: document.getElementById('tps-value'), msptValue: document.getElementById('mspt-value') }; // 3. 初始化主题 const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); // 4. 初始化图表(必须在其他函数之前) initCharts(); // 5. 事件监听器 if (elements.themeToggle) { elements.themeToggle.addEventListener('click', toggleTheme); } if (elements.refreshBtn) { elements.refreshBtn.addEventListener('click', handleRefresh); } // 6. 初始加载数据 loadAllData(); // 7. 设置定时刷新(10秒) const refreshInterval = setInterval(loadAllData, 10000); /*** 功能函数 ***/ function initCharts() { // 内存图表 const memoryCtx = document.getElementById('memory-chart')?.getContext('2d'); if (memoryCtx) { memoryChart = new Chart(memoryCtx, { type: 'doughnut', data: { labels: ['已使用', '未使用'], datasets: [{ data: [0, 100], backgroundColor: ['#4361ee', '#e9ecef'] }] }, options: { responsive: true, maintainAspectRatio: false } }); } // 性能图表 const perfCtx = document.getElementById('performance-chart')?.getContext('2d'); if (perfCtx) { performanceChart = new Chart(perfCtx, { type: 'line', data: { labels: Array(30).fill(''), datasets: [ { label: 'TPS', data: tpsHistory, borderColor: '#4CAF50', tension: 0.1, yAxisID: 'y' }, { label: 'MSPT', data: msptHistory, borderColor: '#FF5722', tension: 0.1, yAxisID: 'y1' } ] }, options: { responsive: true, interaction: { mode: 'index', intersect: false }, scales: { y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'TPS' }, min: 0, max: 20 }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'MSPT (ms)' }, min: 0, grid: { drawOnChartArea: false } } } } }); } } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'light' ? 'dark' : 'light'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); } function handleRefresh() { this.classList.add('loading'); loadAllData(); setTimeout(() => { this.classList.remove('loading'); }, 1000); } async function loadAllData() { try { await Promise.all([ fetchData('/api/stats', updateTable), fetchData('/api/online-players', updateOnlinePlayers), fetchData('/api/player-count', updatePlayerCounts), fetchData('/api/server-status', updateServerStatus) ]); } catch (error) { console.error('加载数据出错:', error); showError('加载数据失败,请检查控制台'); } } async function fetchData(url, callback) { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); callback(data); } catch (error) { console.error(`获取 ${url} 数据失败:`, error); throw error; } } function updateTable(data) { if (!elements.statsTable) return; elements.statsTable.innerHTML = ''; const sortedPlayers = Object.entries(data) .map(([name, statString]) => { const stats = {}; statString.split(" | ").forEach(part => { const [label, value] = part.split(": "); stats[label.trim()] = value; }); return { name, stats }; }) .sort((a, b) => parseTimeToSeconds(b.stats["总时长"]) - parseTimeToSeconds(a.stats["总时长"])); sortedPlayers.forEach(player => { const row = elements.statsTable.insertRow(); row.insertCell(0).textContent = player.name; row.insertCell(1).textContent = player.stats["总时长"]; row.insertCell(2).textContent = player.stats["30天"]; row.insertCell(3).textContent = player.stats["7天"]; }); } function updateOnlinePlayers(data) { if (!elements.whitelistPlayers || !elements.nonWhitelistPlayers) return; const updateList = (element, players, emptyMessage) => { element.innerHTML = ''; if (players?.length > 0) { players.forEach(player => { const li = document.createElement('li'); li.innerHTML = `${player.name}`; element.appendChild(li); }); } else { element.innerHTML = `
  • ${emptyMessage}
  • `; } }; updateList(elements.whitelistPlayers, data.whitelisted, '暂无玩家在线'); updateList(elements.nonWhitelistPlayers, data.non_whitelisted, '暂无假人在线'); } function updatePlayerCounts(data) { if (elements.totalCount) elements.totalCount.textContent = data.total || 0; if (elements.whitelistCount) elements.whitelistCount.textContent = data.whitelisted || 0; if (elements.nonWhitelistCount) elements.nonWhitelistCount.textContent = data.non_whitelisted || 0; } function updateServerStatus(data) { // 更新内存信息 if (data.memory) { const usedMB = Math.round(data.memory.used / (1024 * 1024)); const freeMB = Math.round((data.memory.max - data.memory.used) / (1024 * 1024)); const percent = Math.round(data.memory.usage_percentage); if (memoryChart) { memoryChart.data.datasets[0].data = [usedMB, freeMB]; memoryChart.update(); } if (elements.memoryUsed) elements.memoryUsed.textContent = usedMB; if (elements.memoryFree) elements.memoryFree.textContent = freeMB; if (elements.memoryPercent) elements.memoryPercent.textContent = percent; } // 更新性能信息 if (data.server) { // 计算TPS (限制在0-20之间) const tps = Math.min(20, 1000 / (data.server.average_tick_time_ms || 50)).toFixed(1); const mspt = data.server.average_tick_time_ms.toFixed(1); // 更新显示 if (elements.tpsValue) elements.tpsValue.textContent = tps; if (elements.msptValue) elements.msptValue.textContent = mspt; // 更新图表数据 if (performanceChart) { // 更新历史数据 tpsHistory.shift(); tpsHistory.push(parseFloat(tps)); msptHistory.shift(); msptHistory.push(parseFloat(mspt)); // 只更新数据不重新创建图表 performanceChart.data.datasets[0].data = tpsHistory; performanceChart.data.datasets[1].data = msptHistory; performanceChart.update(); } if (elements.avgTick) elements.avgTick.textContent = data.server.average_tick_time_ms?.toFixed(2) || '0'; } // 更新性能图表 if (performanceChart && data.server) { // 计算TPS和MSPT const tps = Math.min(20, 1000 / (data.server.average_tick_time_ms || 50)).toFixed(1); const mspt = data.server.average_tick_time_ms.toFixed(1); // 更新历史数据 tpsHistory.shift(); tpsHistory.push(parseFloat(tps)); msptHistory.shift(); msptHistory.push(parseFloat(mspt)); // 只更新数据(不重新创建图表) performanceChart.data.datasets[0].data = tpsHistory; performanceChart.data.datasets[1].data = msptHistory; // 平滑更新 performanceChart.update('none'); // 更新数值显示 if (elements.tpsValue) elements.tpsValue.textContent = tps; if (elements.msptValue) elements.msptValue.textContent = mspt; // 根据数值设置颜色 setMetricColor(elements.tpsValue, tps, 15, 10); setMetricColor(elements.msptValue, mspt, 50, 100, true); } if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0'; } function parseTimeToSeconds(timeStr) { if (!timeStr) return 0; return timeStr.split(' ').reduce((total, part) => { if (part.includes('h')) return total + parseInt(part.replace('h', '')) * 3600; if (part.includes('m')) return total + parseInt(part.replace('m', '')) * 60; return total; }, 0); } function showError(message) { const errorEl = document.createElement('div'); errorEl.className = 'error-message'; errorEl.textContent = message; document.body.appendChild(errorEl); setTimeout(() => { errorEl.classList.add('show'); setTimeout(() => { errorEl.classList.remove('show'); setTimeout(() => document.body.removeChild(errorEl), 300); }, 5000); }, 10); } function initPerformanceChart() { const perfCtx = document.getElementById('performance-chart')?.getContext('2d'); if (!perfCtx) return null; return new Chart(perfCtx, { type: 'line', data: { labels: Array(30).fill(''), datasets: [ { label: 'TPS', data: tpsHistory, borderColor: '#4CAF50', backgroundColor: 'rgba(76, 175, 80, 0.1)', borderWidth: 2, pointRadius: 3, pointHoverRadius: 5, tension: 0.3, yAxisID: 'y' }, { label: 'MSPT', data: msptHistory, borderColor: '#FF5722', backgroundColor: 'rgba(255, 87, 34, 0.1)', borderWidth: 2, pointRadius: 3, pointHoverRadius: 5, tension: 0.3, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 500 }, plugins: { legend: { position: 'top', labels: { boxWidth: 12, padding: 20, font: { size: 12 } } }, tooltip: { mode: 'index', intersect: false, bodySpacing: 8 } }, scales: { x: { grid: { display: false }, ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 10 } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'TPS', font: { weight: 'bold' } }, min: 0, max: 20, grid: { color: 'rgba(0, 0, 0, 0.05)' } }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'MSPT (ms)', font: { weight: 'bold' } }, min: 0, grid: { drawOnChartArea: false } } } } }); } function setMetricColor(element, value, warnThreshold, dangerThreshold, reverse = false) { if (!element) return; const numValue = parseFloat(value); element.style.color = reverse ? (numValue > dangerThreshold ? '#FF5722' : numValue > warnThreshold ? '#FFC107' : '#4CAF50') : (numValue < dangerThreshold ? '#FF5722' : numValue < warnThreshold ? '#FFC107' : '#4CAF50'); } });