From ef3f3dd1b890c17890153b1cc5c1805ad6862913 Mon Sep 17 00:00:00 2001 From: BRanulf Date: Sat, 19 Apr 2025 21:14:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BD=91=E9=A1=B5=E5=92=8C?= =?UTF-8?q?=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle.properties | 2 +- .../com/example/playertime/WebServer.java | 252 +++++++++++++- .../assets/playertime/web/css/style.css | 101 ++++++ .../assets/playertime/web/index.html | 66 +++- .../resources/assets/playertime/web/js/app.js | 312 ++++++++++++------ 5 files changed, 612 insertions(+), 121 deletions(-) diff --git a/gradle.properties b/gradle.properties index 9eafc02..94c83ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ minecraft_version=1.21.4 yarn_mappings=1.21.4+build.8 loader_version=0.16.10 # Mod Properties -mod_version=1.14.514.118 +mod_version=1.14.514.120 maven_group=org.example1 archives_base_name=playerOnlineTimeTrackerMod # Dependencies diff --git a/src/main/java/com/example/playertime/WebServer.java b/src/main/java/com/example/playertime/WebServer.java index d9fb8f2..9762c05 100644 --- a/src/main/java/com/example/playertime/WebServer.java +++ b/src/main/java/com/example/playertime/WebServer.java @@ -1,6 +1,7 @@ package com.example.playertime; import com.google.gson.*; +import com.mojang.authlib.GameProfile; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; @@ -9,6 +10,7 @@ import net.minecraft.server.PlayerManager; import net.minecraft.server.network.ServerPlayerEntity; import java.io.*; +import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -124,11 +126,9 @@ public class WebServer { JsonObject response = new JsonObject(); response.addProperty("onlineCount", playerManager.getCurrentPlayerCount()); - // 获取白名单在线玩家 JsonArray whitelistPlayers = new JsonArray(); Map playerTimeMap = new HashMap<>(); - // 先收集所有白名单玩家UUID Set whitelistUuids = new HashSet<>(); for (String name : playerManager.getWhitelist().getNames()) { server.getUserCache().findByName(name).ifPresent(profile -> { @@ -136,7 +136,6 @@ public class WebServer { }); } - // 检查在线玩家 for (ServerPlayerEntity player : playerManager.getPlayerList()) { UUID uuid = player.getUuid(); if (whitelistUuids.contains(uuid)) { @@ -152,7 +151,6 @@ public class WebServer { } response.add("whitelistPlayers", whitelistPlayers); - // 获取时长前三玩家(包括离线玩家) JsonArray topPlayers = new JsonArray(); timeTracker.getPlayerData().entrySet().stream() .filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家 @@ -175,6 +173,240 @@ public class WebServer { } }); + // 在线玩家列表 + server.createContext("/api/online-players", exchange -> { + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + return; + } + + if (!"GET".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 405, "Method Not Allowed"); + return; + } + + try { + PlayerManager playerManager = minecraftServer.getPlayerManager(); + JsonObject response = new JsonObject(); + + // 获取白名单玩家UUID集合 + Set whitelistUuids = new HashSet<>(); + for (String name : playerManager.getWhitelist().getNames()) { + minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + + // 分类玩家 + JsonArray whitelistedPlayers = new JsonArray(); + JsonArray nonWhitelistedPlayers = new JsonArray(); + + for (ServerPlayerEntity player : playerManager.getPlayerList()) { + UUID uuid = player.getUuid(); + JsonObject playerJson = new JsonObject(); + playerJson.addProperty("name", player.getName().getString()); + playerJson.addProperty("uuid", uuid.toString()); + + if (whitelistUuids.contains(uuid)) { + whitelistedPlayers.add(playerJson); + } else { + nonWhitelistedPlayers.add(playerJson); + } + } + + response.add("whitelisted", whitelistedPlayers); + response.add("non_whitelisted", nonWhitelistedPlayers); + + sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); + } catch (Exception e) { + PlayerTimeMod.LOGGER.error("[在线时间] 无法获取在线玩家列表", e); + sendResponse(exchange, 500, "Internal Server Error"); + } + }); + + // 玩家计数 + server.createContext("/api/player-count", exchange -> { + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + return; + } + + if (!"GET".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 405, "Method Not Allowed"); + return; + } + + try { + PlayerManager playerManager = minecraftServer.getPlayerManager(); + JsonObject response = new JsonObject(); + + // 获取白名单玩家UUID集合 + Set whitelistUuids = new HashSet<>(); + for (String name : playerManager.getWhitelist().getNames()) { + minecraftServer.getUserCache().findByName(name).ifPresent(profile -> { + whitelistUuids.add(profile.getId()); + }); + } + + // 分类计数 + int whitelistedCount = 0; + int nonWhitelistedCount = 0; + + for (ServerPlayerEntity player : playerManager.getPlayerList()) { + if (whitelistUuids.contains(player.getUuid())) { + whitelistedCount++; + } else { + nonWhitelistedCount++; + } + } + + response.addProperty("total", playerManager.getCurrentPlayerCount()); + response.addProperty("whitelisted", whitelistedCount); + response.addProperty("non_whitelisted", nonWhitelistedCount); + + sendResponse(exchange, 200, GSON.toJson(response).getBytes(StandardCharsets.UTF_8), "application/json"); + } catch (Exception e) { + PlayerTimeMod.LOGGER.error("[在线时间] 无法获取玩家计数", e); + sendResponse(exchange, 500, "Internal Server Error"); + } + }); + + // 白名单玩家 + server.createContext("/api/whitelist", exchange -> { + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + return; + } + + if (!"GET".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 405, "Method Not Allowed"); + return; + } + + try { + PlayerManager playerManager = minecraftServer.getPlayerManager(); + JsonArray whitelist = new JsonArray(); + + for (String name : playerManager.getWhitelist().getNames()) { + JsonObject player = new JsonObject(); + player.addProperty("name", name); + + // 尝试获取UUID + Optional profile = minecraftServer.getUserCache().findByName(name); + if (profile.isPresent()) { + player.addProperty("uuid", profile.get().getId().toString()); + + // 检查是否在线 + ServerPlayerEntity onlinePlayer = playerManager.getPlayer(profile.get().getId()); + player.addProperty("online", onlinePlayer != null); + } else { + player.addProperty("online", false); + } + + whitelist.add(player); + } + + sendResponse(exchange, 200, GSON.toJson(whitelist).getBytes(StandardCharsets.UTF_8), "application/json"); + } catch (Exception e) { + PlayerTimeMod.LOGGER.error("[在线时间] 无法获取白名单", e); + sendResponse(exchange, 500, "Internal Server Error"); + } + }); + + // 服务器状态 + server.createContext("/api/server-status", exchange -> { + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "GET, OPTIONS"); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Content-Type"); + + if ("OPTIONS".equals(exchange.getRequestMethod())) { + exchange.sendResponseHeaders(204, -1); + return; + } + + if (!"GET".equals(exchange.getRequestMethod())) { + sendResponse(exchange, 405, "Method Not Allowed"); + return; + } + + try { + Runtime runtime = Runtime.getRuntime(); + JsonObject status = new JsonObject(); + + // 内存使用情况 + long maxMemory = runtime.maxMemory(); + long totalMemory = runtime.totalMemory(); + long freeMemory = runtime.freeMemory(); + long usedMemory = totalMemory - freeMemory; + + JsonObject memory = new JsonObject(); + memory.addProperty("max", maxMemory); + memory.addProperty("total", totalMemory); + memory.addProperty("used", usedMemory); + memory.addProperty("free", freeMemory); + memory.addProperty("usage_percentage", (double) usedMemory / maxMemory * 100); + status.add("memory", memory); + + status.addProperty("available_processors", runtime.availableProcessors()); + + long uptime = ManagementFactory.getRuntimeMXBean().getUptime(); + status.addProperty("uptime", uptime); + status.addProperty("uptime_formatted", formatUptime(uptime)); + + File diskPartition = new File("."); + long totalSpace = diskPartition.getTotalSpace(); + long freeSpace = diskPartition.getFreeSpace(); + long usableSpace = diskPartition.getUsableSpace(); + + JsonObject disk = new JsonObject(); + disk.addProperty("total", totalSpace); + disk.addProperty("free", freeSpace); + disk.addProperty("usable", usableSpace); + disk.addProperty("usage_percentage", (double) (totalSpace - freeSpace) / totalSpace * 100); + status.add("disk", disk); + + JsonObject serverInfo = new JsonObject(); + serverInfo.addProperty("version", minecraftServer.getVersion()); + serverInfo.addProperty("player_count", minecraftServer.getCurrentPlayerCount()); + serverInfo.addProperty("max_players", minecraftServer.getMaxPlayerCount()); + + serverInfo.addProperty("average_tick_time_ms", minecraftServer.getAverageTickTime()); + + long[] tickTimes = minecraftServer.getTickTimes(); + if (tickTimes != null && tickTimes.length > 0) { + double recentAvgTickTime = Arrays.stream(tickTimes).average().orElse(0) / 1000000.0; + serverInfo.addProperty("recent_avg_tick_time_ms", recentAvgTickTime); + + JsonArray recentTicks = new JsonArray(); + int sampleCount = Math.min(10, tickTimes.length); + for (int i = 0; i < sampleCount; i++) { + recentTicks.add(tickTimes[i] / 1000000.0); + } + serverInfo.add("recent_tick_samples_ms", recentTicks); + } + + status.add("server", serverInfo); + + sendResponse(exchange, 200, GSON.toJson(status).getBytes(StandardCharsets.UTF_8), "application/json"); + } catch (Exception e) { + PlayerTimeMod.LOGGER.error("[在线时间] 无法获取服务器状态", e); + sendResponse(exchange, 500, "Internal Server Error"); + } + }); + + @@ -244,6 +476,18 @@ public class WebServer { } } + private String formatUptime(long millis) { + long seconds = millis / 1000; + long days = seconds / 86400; + seconds %= 86400; + long hours = seconds / 3600; + seconds %= 3600; + long minutes = seconds / 60; + seconds %= 60; + + return String.format("%d天 %02d小时 %02d分钟 %02d秒", days, hours, minutes, seconds); + } + // 获取玩家总时长 private long getPlayerTotalTime(String uuid, JsonObject timeData) { if (!timeData.has(uuid)) { diff --git a/src/main/resources/assets/playertime/web/css/style.css b/src/main/resources/assets/playertime/web/css/style.css index 3a04886..3407cf7 100644 --- a/src/main/resources/assets/playertime/web/css/style.css +++ b/src/main/resources/assets/playertime/web/css/style.css @@ -431,3 +431,104 @@ tbody tr:nth-child(10) { animation-delay: 0.5s; } font-size: 0.8rem; } } + +.player-counts { + display: flex; + flex-direction: column; + gap: 8px; +} + +.count-item { + display: flex; + justify-content: space-between; +} + +.count-label { + font-weight: 500; +} + +.count-value { + color: var(--primary-color-c); +} + +.player-lists { + display: flex; + gap: 20px; +} + +.player-list { + flex: 1; +} + +.player-list h4 { + margin-bottom: 8px; + color: var(--primary-color-c); +} + +.status-section { + margin: 2rem 0; +} + +.status-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 1rem; +} + +@media (max-width: 768px) { + .status-grid { + grid-template-columns: 1fr; + } + + .player-lists { + flex-direction: column; + } +} + +.status-item { + background: var(--card-bg); + padding: 1rem; + border-radius: 8px; + box-shadow: var(--shadow); +} + +.status-item h3 { + margin-top: 0; + color: var(--primary-color-c); +} + +.memory-stats, .performance-stats { + margin-top: 1rem; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.memory-stats div, .performance-stats div { + font-size: 0.9rem; +} + +canvas { + max-height: 300px; + width: 100% !important; +} + +.error-message { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: #ff4444; + color: white; + padding: 10px 20px; + border-radius: 5px; + opacity: 0; + transition: opacity 0.3s; + z-index: 1000; +} + +.error-message.show { + opacity: 1; +} + diff --git a/src/main/resources/assets/playertime/web/index.html b/src/main/resources/assets/playertime/web/index.html index b1053ab..a0c037c 100644 --- a/src/main/resources/assets/playertime/web/index.html +++ b/src/main/resources/assets/playertime/web/index.html @@ -3,10 +3,11 @@ - [在线时间] 玩家在线时间 + [在线时间] 玩家在线时间及服务器状态 +
@@ -21,22 +22,66 @@
数据统计时间开始于此MOD安装时间,不包含安装之前的所有数据

- +
-
-

当前在线

-
0
-

在线玩家

-
    +
    +
    + 总数: + 0 +
    +
    + 玩家: + 0 +
    +
    + 假人: + 0 +
    +
    -

    时长前三

    -
      +

      在线玩家列表

      +
      +
      +

      玩家

      +
        +
        +
        +

        假人

        +
          +
          +
          + +
          +

          服务器状态

          +
          +
          +

          内存使用

          + +
          +
          已用: 0 MB
          +
          可用: 0 MB
          +
          使用率: 0%
          +
          +
          +
          +

          服务器性能

          + +
          +
          平均Tick: 0 ms
          +
          运行时间: 0
          +
          +
          +
          +
          + + +

          玩家在线时长 (白名单)

          + + @@ -65,6 +112,5 @@

          Copyright © 2025 BRanulf

          - diff --git a/src/main/resources/assets/playertime/web/js/app.js b/src/main/resources/assets/playertime/web/js/app.js index 5df8deb..69ab06b 100644 --- a/src/main/resources/assets/playertime/web/js/app.js +++ b/src/main/resources/assets/playertime/web/js/app.js @@ -1,116 +1,226 @@ document.addEventListener('DOMContentLoaded', function() { - const refreshBtn = document.getElementById('refresh-btn'); - const themeToggle = document.getElementById('theme-toggle'); - const statsTable = document.getElementById('stats-table').getElementsByTagName('tbody')[0]; - const onlineCountEl = document.getElementById('online-count'); - const onlinePlayersEl = document.getElementById('online-players'); - const topPlayersEl = document.getElementById('top-players'); + // 获取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') + }; // 初始化主题 const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme); - // 主题切换功能 - themeToggle.addEventListener('click', () => { + // 主题切换 + if (elements.themeToggle) { + elements.themeToggle.addEventListener('click', toggleTheme); + } + + // 刷新按钮 + if (elements.refreshBtn) { + elements.refreshBtn.addEventListener('click', handleRefresh); + } + + // 初始化图表 + const memoryChart = initMemoryChart(); + const performanceChart = initPerformanceChart(); + + // 初始加载数据 + loadAllData(); + + // 设置定时刷新(10秒) + setInterval(loadAllData, 10000); + + /*** 功能函数 ***/ + 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); - }); - - loadAllData(); - - refreshBtn.addEventListener('click', function() { - refreshBtn.classList.add('loading'); - loadAllData(); - setTimeout(() => { - refreshBtn.classList.remove('loading'); - }, 1000); - }); - - function loadAllData() { - // 加载统计数据 - fetch('/api/stats') - .then(response => response.json()) - .then(data => { - updateTable(data); - }) - .catch(error => { - console.error('获取统计信息时出错:', error); - showError('未能加载玩家统计信息。 检查控制台以获取详细信息'); - }); - - // 加载在线状态数据 - fetch('/api/widget-data') - .then(response => response.json()) - .then(data => { - updateOnlineStatus(data); - }) - .catch(error => { - console.error('获取在线状态时出错:', error); - showError('未能加载在线状态信息。 检查控制台以获取详细信息'); - }); } - function updateTable(statsData) { - statsTable.innerHTML = ''; + function handleRefresh() { + this.classList.add('loading'); + loadAllData(); + setTimeout(() => { + this.classList.remove('loading'); + }, 1000); + } - Object.entries(statsData).forEach(([playerName, statString]) => { - const row = statsTable.insertRow(); + function initMemoryChart() { + const ctx = document.getElementById('memory-chart')?.getContext('2d'); + if (!ctx) return null; - const nameCell = row.insertCell(0); - nameCell.textContent = playerName; - - const stats = {}; - statString.split(" | ").forEach(part => { - const [label, value] = part.split(": "); - stats[label.trim()] = value; - }); - - const totalCell = row.insertCell(1); - totalCell.textContent = stats["总时长"]; - - const thirtyCell = row.insertCell(2); - thirtyCell.textContent = stats["30天"]; - - const sevenCell = row.insertCell(3); - sevenCell.textContent = stats["7天"]; + return new Chart(ctx, { + type: 'doughnut', + data: { + labels: ['已使用', '未使用'], + datasets: [{ + data: [0, 100], + backgroundColor: ['#4361ee', '#e9ecef'] + }] + }, + options: { + responsive: true, + maintainAspectRatio: false + } }); } - function updateOnlineStatus(data) { - // 更新在线人数 - onlineCountEl.textContent = data.onlineCount; + function initPerformanceChart() { + const ctx = document.getElementById('performance-chart')?.getContext('2d'); + if (!ctx) return null; - // 更新在线玩家列表 - onlinePlayersEl.innerHTML = ''; - if (data.whitelistPlayers && data.whitelistPlayers.length > 0) { - data.whitelistPlayers.forEach(player => { - const li = document.createElement('li'); - li.innerHTML = ` - ${player.name} - ${player.time} - `; - onlinePlayersEl.appendChild(li); - }); - } else { - onlinePlayersEl.innerHTML = '
        • 暂无在线玩家
        • '; + return new Chart(ctx, { + type: 'line', + data: { + labels: Array(10).fill('').map((_, i) => i + 1), + datasets: [{ + label: 'Tick时间 (ms)', + data: Array(10).fill(0), + borderColor: '#4361ee', + tension: 0.1, + fill: false + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: '毫秒' + } + } + } + } + }); + } + + 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', (data) => updateServerStatus(data, memoryChart, performanceChart)) + ]); + } 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, memoryChart, performanceChart) { + // 更新内存信息 + 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; } - // 更新时长前三玩家 - topPlayersEl.innerHTML = ''; - if (data.topPlayers && data.topPlayers.length > 0) { - data.topPlayers.forEach(player => { - const li = document.createElement('li'); - li.innerHTML = ` - ${player.name} - ${player.time} - `; - topPlayersEl.appendChild(li); - }); - } else { - topPlayersEl.innerHTML = '
        • 暂无数据
        • '; + // 更新性能信息 + if (data.server) { + if (performanceChart && data.server.recent_tick_samples_ms) { + performanceChart.data.datasets[0].data = data.server.recent_tick_samples_ms; + performanceChart.update(); + } + + if (elements.avgTick) elements.avgTick.textContent = data.server.average_tick_time_ms?.toFixed(2) || '0'; } + + 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) { @@ -121,20 +231,10 @@ document.addEventListener('DOMContentLoaded', function() { setTimeout(() => { errorEl.classList.add('show'); - }, 10); - - setTimeout(() => { - errorEl.classList.remove('show'); setTimeout(() => { - document.body.removeChild(errorEl); - }, 300); - }, 5000); - } - - function parseTime(timeStr) { - const [hPart, mPart] = timeStr.split(' '); - const hours = parseInt(hPart.replace('h', '')); - const minutes = parseInt(mPart.replace('m', '')); - return hours * 60 + minutes; + errorEl.classList.remove('show'); + setTimeout(() => document.body.removeChild(errorEl), 300); + }, 5000); + }, 10); } });