更新网页和后端
This commit is contained in:
parent
5d10f6c2bd
commit
ef3f3dd1b8
@ -6,7 +6,7 @@ minecraft_version=1.21.4
|
|||||||
yarn_mappings=1.21.4+build.8
|
yarn_mappings=1.21.4+build.8
|
||||||
loader_version=0.16.10
|
loader_version=0.16.10
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.14.514.118
|
mod_version=1.14.514.120
|
||||||
maven_group=org.example1
|
maven_group=org.example1
|
||||||
archives_base_name=playerOnlineTimeTrackerMod
|
archives_base_name=playerOnlineTimeTrackerMod
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.example.playertime;
|
package com.example.playertime;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import com.sun.net.httpserver.HttpHandler;
|
import com.sun.net.httpserver.HttpHandler;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
@ -9,6 +10,7 @@ import net.minecraft.server.PlayerManager;
|
|||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.lang.management.ManagementFactory;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@ -124,11 +126,9 @@ public class WebServer {
|
|||||||
JsonObject response = new JsonObject();
|
JsonObject response = new JsonObject();
|
||||||
response.addProperty("onlineCount", playerManager.getCurrentPlayerCount());
|
response.addProperty("onlineCount", playerManager.getCurrentPlayerCount());
|
||||||
|
|
||||||
// 获取白名单在线玩家
|
|
||||||
JsonArray whitelistPlayers = new JsonArray();
|
JsonArray whitelistPlayers = new JsonArray();
|
||||||
Map<String, Long> playerTimeMap = new HashMap<>();
|
Map<String, Long> playerTimeMap = new HashMap<>();
|
||||||
|
|
||||||
// 先收集所有白名单玩家UUID
|
|
||||||
Set<UUID> whitelistUuids = new HashSet<>();
|
Set<UUID> whitelistUuids = new HashSet<>();
|
||||||
for (String name : playerManager.getWhitelist().getNames()) {
|
for (String name : playerManager.getWhitelist().getNames()) {
|
||||||
server.getUserCache().findByName(name).ifPresent(profile -> {
|
server.getUserCache().findByName(name).ifPresent(profile -> {
|
||||||
@ -136,7 +136,6 @@ public class WebServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查在线玩家
|
|
||||||
for (ServerPlayerEntity player : playerManager.getPlayerList()) {
|
for (ServerPlayerEntity player : playerManager.getPlayerList()) {
|
||||||
UUID uuid = player.getUuid();
|
UUID uuid = player.getUuid();
|
||||||
if (whitelistUuids.contains(uuid)) {
|
if (whitelistUuids.contains(uuid)) {
|
||||||
@ -152,7 +151,6 @@ public class WebServer {
|
|||||||
}
|
}
|
||||||
response.add("whitelistPlayers", whitelistPlayers);
|
response.add("whitelistPlayers", whitelistPlayers);
|
||||||
|
|
||||||
// 获取时长前三玩家(包括离线玩家)
|
|
||||||
JsonArray topPlayers = new JsonArray();
|
JsonArray topPlayers = new JsonArray();
|
||||||
timeTracker.getPlayerData().entrySet().stream()
|
timeTracker.getPlayerData().entrySet().stream()
|
||||||
.filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家
|
.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<UUID> 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<UUID> 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<GameProfile> 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) {
|
private long getPlayerTotalTime(String uuid, JsonObject timeData) {
|
||||||
if (!timeData.has(uuid)) {
|
if (!timeData.has(uuid)) {
|
||||||
|
@ -431,3 +431,104 @@ tbody tr:nth-child(10) { animation-delay: 0.5s; }
|
|||||||
font-size: 0.8rem;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -3,10 +3,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>[在线时间] 玩家在线时间</title>
|
<title>[在线时间] 玩家在线时间及服务器状态</title>
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -21,22 +22,66 @@
|
|||||||
<div class="warning">数据统计时间开始于此MOD安装时间,不包含安装之前的所有数据</div>
|
<div class="warning">数据统计时间开始于此MOD安装时间,不包含安装之前的所有数据</div>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<!-- 新增的在线状态卡片 -->
|
<!-- 更新后的状态卡片 -->
|
||||||
<div class="status-cards">
|
<div class="status-cards">
|
||||||
<div class="status-card">
|
|
||||||
<h3>当前在线</h3>
|
|
||||||
<div class="online-count" id="online-count">0</div>
|
|
||||||
</div>
|
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<h3>在线玩家</h3>
|
<h3>在线玩家</h3>
|
||||||
<ul class="online-players" id="online-players"></ul>
|
<div class="player-counts">
|
||||||
|
<div class="count-item">
|
||||||
|
<span class="count-label">总数:</span>
|
||||||
|
<span class="count-value" id="total-count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="count-item">
|
||||||
|
<span class="count-label">玩家:</span>
|
||||||
|
<span class="count-value" id="whitelist-count">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="count-item">
|
||||||
|
<span class="count-label">假人:</span>
|
||||||
|
<span class="count-value" id="non-whitelist-count">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<h3>时长前三</h3>
|
<h3>在线玩家列表</h3>
|
||||||
<ul class="top-players" id="top-players"></ul>
|
<div class="player-lists">
|
||||||
|
<div class="player-list">
|
||||||
|
<h4>玩家</h4>
|
||||||
|
<ul class="online-players" id="whitelist-players"></ul>
|
||||||
|
</div>
|
||||||
|
<div class="player-list">
|
||||||
|
<h4>假人</h4>
|
||||||
|
<ul class="online-players" id="non-whitelist-players"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务器状态可视化 -->
|
||||||
|
<div class="status-section">
|
||||||
|
<h2>服务器状态</h2>
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-item">
|
||||||
|
<h3>内存使用</h3>
|
||||||
|
<canvas id="memory-chart"></canvas>
|
||||||
|
<div class="memory-stats">
|
||||||
|
<div>已用: <span id="memory-used">0</span> MB</div>
|
||||||
|
<div>可用: <span id="memory-free">0</span> MB</div>
|
||||||
|
<div>使用率: <span id="memory-percent">0</span>%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<h3>服务器性能</h3>
|
||||||
|
<canvas id="performance-chart"></canvas>
|
||||||
|
<div class="performance-stats">
|
||||||
|
<div>平均Tick: <span id="avg-tick">0</span> ms</div>
|
||||||
|
<div>运行时间: <span id="uptime">0</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 玩家时长统计表格 -->
|
||||||
|
<h2>玩家在线时长 (白名单)</h2>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="refresh-btn" class="refresh-btn">
|
<button id="refresh-btn" class="refresh-btn">
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
@ -45,6 +90,8 @@
|
|||||||
<p class="info-note">仅跟踪和显示列入白名单的玩家</p>
|
<p class="info-note">仅跟踪和显示列入白名单的玩家</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-container">
|
<div class="stats-container">
|
||||||
|
|
||||||
|
|
||||||
<table id="stats-table">
|
<table id="stats-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -65,6 +112,5 @@
|
|||||||
<p>Copyright © 2025 <a href="https://git.branulf.top/BRanulf" target="_blank">BRanulf</a></p>
|
<p>Copyright © 2025 <a href="https://git.branulf.top/BRanulf" target="_blank">BRanulf</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,116 +1,226 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const refreshBtn = document.getElementById('refresh-btn');
|
// 获取DOM元素(带安全检查)
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const elements = {
|
||||||
const statsTable = document.getElementById('stats-table').getElementsByTagName('tbody')[0];
|
refreshBtn: document.getElementById('refresh-btn'),
|
||||||
const onlineCountEl = document.getElementById('online-count');
|
themeToggle: document.getElementById('theme-toggle'),
|
||||||
const onlinePlayersEl = document.getElementById('online-players');
|
statsTable: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0],
|
||||||
const topPlayersEl = document.getElementById('top-players');
|
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';
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
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 currentTheme = document.documentElement.getAttribute('data-theme');
|
||||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
document.documentElement.setAttribute('data-theme', newTheme);
|
document.documentElement.setAttribute('data-theme', newTheme);
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('theme', newTheme);
|
||||||
});
|
}
|
||||||
|
|
||||||
loadAllData();
|
function handleRefresh() {
|
||||||
|
this.classList.add('loading');
|
||||||
refreshBtn.addEventListener('click', function() {
|
|
||||||
refreshBtn.classList.add('loading');
|
|
||||||
loadAllData();
|
loadAllData();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshBtn.classList.remove('loading');
|
this.classList.remove('loading');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
}
|
||||||
|
|
||||||
function loadAllData() {
|
function initMemoryChart() {
|
||||||
// 加载统计数据
|
const ctx = document.getElementById('memory-chart')?.getContext('2d');
|
||||||
fetch('/api/stats')
|
if (!ctx) return null;
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
updateTable(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('获取统计信息时出错:', error);
|
|
||||||
showError('未能加载玩家统计信息。 检查控制台以获取详细信息');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 加载在线状态数据
|
return new Chart(ctx, {
|
||||||
fetch('/api/widget-data')
|
type: 'doughnut',
|
||||||
.then(response => response.json())
|
data: {
|
||||||
.then(data => {
|
labels: ['已使用', '未使用'],
|
||||||
updateOnlineStatus(data);
|
datasets: [{
|
||||||
})
|
data: [0, 100],
|
||||||
.catch(error => {
|
backgroundColor: ['#4361ee', '#e9ecef']
|
||||||
console.error('获取在线状态时出错:', error);
|
}]
|
||||||
showError('未能加载在线状态信息。 检查控制台以获取详细信息');
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTable(statsData) {
|
function initPerformanceChart() {
|
||||||
statsTable.innerHTML = '';
|
const ctx = document.getElementById('performance-chart')?.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
|
|
||||||
Object.entries(statsData).forEach(([playerName, statString]) => {
|
return new Chart(ctx, {
|
||||||
const row = statsTable.insertRow();
|
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: '毫秒'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const nameCell = row.insertCell(0);
|
async function loadAllData() {
|
||||||
nameCell.textContent = playerName;
|
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 = {};
|
const stats = {};
|
||||||
statString.split(" | ").forEach(part => {
|
statString.split(" | ").forEach(part => {
|
||||||
const [label, value] = part.split(": ");
|
const [label, value] = part.split(": ");
|
||||||
stats[label.trim()] = value;
|
stats[label.trim()] = value;
|
||||||
});
|
});
|
||||||
|
return { name, stats };
|
||||||
|
})
|
||||||
|
.sort((a, b) => parseTimeToSeconds(b.stats["总时长"]) - parseTimeToSeconds(a.stats["总时长"]));
|
||||||
|
|
||||||
const totalCell = row.insertCell(1);
|
sortedPlayers.forEach(player => {
|
||||||
totalCell.textContent = stats["总时长"];
|
const row = elements.statsTable.insertRow();
|
||||||
|
row.insertCell(0).textContent = player.name;
|
||||||
const thirtyCell = row.insertCell(2);
|
row.insertCell(1).textContent = player.stats["总时长"];
|
||||||
thirtyCell.textContent = stats["30天"];
|
row.insertCell(2).textContent = player.stats["30天"];
|
||||||
|
row.insertCell(3).textContent = player.stats["7天"];
|
||||||
const sevenCell = row.insertCell(3);
|
|
||||||
sevenCell.textContent = stats["7天"];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOnlineStatus(data) {
|
function updateOnlinePlayers(data) {
|
||||||
// 更新在线人数
|
if (!elements.whitelistPlayers || !elements.nonWhitelistPlayers) return;
|
||||||
onlineCountEl.textContent = data.onlineCount;
|
|
||||||
|
|
||||||
// 更新在线玩家列表
|
const updateList = (element, players, emptyMessage) => {
|
||||||
onlinePlayersEl.innerHTML = '';
|
element.innerHTML = '';
|
||||||
if (data.whitelistPlayers && data.whitelistPlayers.length > 0) {
|
if (players?.length > 0) {
|
||||||
data.whitelistPlayers.forEach(player => {
|
players.forEach(player => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.innerHTML = `
|
li.innerHTML = `<span class="player-name">${player.name}</span>`;
|
||||||
<span class="player-name">${player.name}</span>
|
element.appendChild(li);
|
||||||
<span class="player-time">${player.time}</span>
|
|
||||||
`;
|
|
||||||
onlinePlayersEl.appendChild(li);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onlinePlayersEl.innerHTML = '<li>暂无在线玩家</li>';
|
element.innerHTML = `<li>${emptyMessage}</li>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateList(elements.whitelistPlayers, data.whitelisted, '暂无玩家在线');
|
||||||
|
updateList(elements.nonWhitelistPlayers, data.non_whitelisted, '暂无假人在线');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新时长前三玩家
|
function updatePlayerCounts(data) {
|
||||||
topPlayersEl.innerHTML = '';
|
if (elements.totalCount) elements.totalCount.textContent = data.total || 0;
|
||||||
if (data.topPlayers && data.topPlayers.length > 0) {
|
if (elements.whitelistCount) elements.whitelistCount.textContent = data.whitelisted || 0;
|
||||||
data.topPlayers.forEach(player => {
|
if (elements.nonWhitelistCount) elements.nonWhitelistCount.textContent = data.non_whitelisted || 0;
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
|
||||||
<span class="player-name">${player.name}</span>
|
|
||||||
<span class="player-time">${player.time}</span>
|
|
||||||
`;
|
|
||||||
topPlayersEl.appendChild(li);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
topPlayersEl.innerHTML = '<li>暂无数据</li>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新性能信息
|
||||||
|
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) {
|
function showError(message) {
|
||||||
@ -121,20 +231,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
errorEl.classList.add('show');
|
errorEl.classList.add('show');
|
||||||
}, 10);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
errorEl.classList.remove('show');
|
errorEl.classList.remove('show');
|
||||||
setTimeout(() => {
|
setTimeout(() => document.body.removeChild(errorEl), 300);
|
||||||
document.body.removeChild(errorEl);
|
|
||||||
}, 300);
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}, 10);
|
||||||
|
|
||||||
function parseTime(timeStr) {
|
|
||||||
const [hPart, mPart] = timeStr.split(' ');
|
|
||||||
const hours = parseInt(hPart.replace('h', ''));
|
|
||||||
const minutes = parseInt(mPart.replace('m', ''));
|
|
||||||
return hours * 60 + minutes;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user