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');
}
});