利用已有api添加新元素
This commit is contained in:
parent
0d9e2db5ee
commit
75bacc1ec7
@ -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.133
|
||||
mod_version=1.14.514.134
|
||||
maven_group=org.example1
|
||||
archives_base_name=ServerPlayerOnlineTracker
|
||||
# Dependencies
|
||||
|
@ -39,9 +39,7 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
private static ModConfig config;
|
||||
public static LocalizationManager localizationManager;
|
||||
|
||||
// RSS Feed URL for releases
|
||||
private static final String RSS_FEED_URL = "https://git.branulf.top/Branulf/ServerPlayerOnlineTracker/releases.rss";
|
||||
// Executor for update check to avoid blocking server startup
|
||||
private static final ExecutorService updateCheckExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
|
||||
@ -53,12 +51,10 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
try {
|
||||
LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod");
|
||||
|
||||
// Check for updates asynchronously on mod initialization
|
||||
checkForUpdates();
|
||||
|
||||
// 在 SERVER_STARTING 阶段创建 Tracker 和 WebServer 实例
|
||||
ServerLifecycleEvents.SERVER_STARTING.register(server -> {
|
||||
timeTracker = new PlayerTimeTracker(server); // Tracker 构造函数不再加载数据
|
||||
timeTracker = new PlayerTimeTracker(server);
|
||||
try {
|
||||
webServer = new WebServer(timeTracker, config.getWebPort(), server);
|
||||
webServer.start();
|
||||
@ -68,10 +64,9 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
}
|
||||
});
|
||||
|
||||
// 在 SERVER_STARTED 阶段加载数据 (此时 UserCache 应该已可用)
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(server -> {
|
||||
if (timeTracker != null) {
|
||||
timeTracker.loadData(); // 在服务器启动完成后加载数据
|
||||
timeTracker.loadData();
|
||||
} else {
|
||||
LOGGER.error("[在线时间] PlayerTimeTracker 未在 SERVER_STARTING 阶段成功初始化!");
|
||||
}
|
||||
@ -97,15 +92,11 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
webServer.stop();
|
||||
}
|
||||
if (timeTracker != null) {
|
||||
// 在服务器停止前,确保所有在线玩家的会话时间被记录
|
||||
for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) {
|
||||
timeTracker.onPlayerLeave(player); // onPlayerLeave 会自动保存该玩家数据
|
||||
timeTracker.onPlayerLeave(player);
|
||||
}
|
||||
// timeTracker.saveAll(); // onPlayerLeave 已经异步保存,这里可以考虑是否还需要 saveAll
|
||||
// 简单的处理是直接调用 saveAll,它会覆盖旧数据
|
||||
timeTracker.saveAll();
|
||||
}
|
||||
// Shutdown the update check executor
|
||||
updateCheckExecutor.shutdownNow();
|
||||
});
|
||||
|
||||
@ -144,11 +135,10 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
}
|
||||
|
||||
private static int showOnlineTime(ServerCommandSource source, int requestedPage) {
|
||||
// Commands are typically executed after SERVER_STARTED, so UserCache should be available here.
|
||||
ServerPlayerEntity player = source.getPlayer();
|
||||
if (player == null) return 0;
|
||||
|
||||
// Use the server's main worker executor for command processing
|
||||
|
||||
Util.getMainWorkerExecutor().execute(() -> {
|
||||
PlayerTimeTracker tracker = getTimeTracker();
|
||||
if (tracker != null) {
|
||||
@ -170,18 +160,14 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
}
|
||||
|
||||
private static int comparePlayTime(String a, String b) {
|
||||
// This parsing logic is specific to the format generated by getWhitelistedPlayerStats
|
||||
// Example: "PlayerName: 10h 30m | 7d: 5h 15m | 30d: 20h 45m"
|
||||
// We need to extract the total time part (e.g., "10h 30m")
|
||||
try {
|
||||
// Find the first colon and the first pipe
|
||||
int firstColon = a.indexOf(':');
|
||||
int firstPipe = a.indexOf('|');
|
||||
String timeA;
|
||||
if (firstColon > 0) {
|
||||
timeA = (firstPipe > firstColon && firstPipe != -1) ? a.substring(firstColon + 1, firstPipe).trim() : a.substring(firstColon + 1).trim();
|
||||
} else {
|
||||
timeA = a.trim(); // Fallback if format changes unexpectedly
|
||||
timeA = a.trim();
|
||||
}
|
||||
|
||||
|
||||
@ -191,26 +177,25 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
if (firstColonB > 0) {
|
||||
timeB = (firstPipeB > firstColonB && firstPipeB != -1) ? b.substring(firstColonB + 1, firstPipeB).trim() : b.substring(firstColonB + 1).trim();
|
||||
} else {
|
||||
timeB = b.trim(); // Fallback if format changes unexpectedly
|
||||
timeB = b.trim();
|
||||
}
|
||||
|
||||
|
||||
return Long.compare(parseTimeToSeconds(timeB), parseTimeToSeconds(timeA)); // Descending order
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error comparing play times: {} vs {}", a, b, e);
|
||||
return 0; // Fallback to equal if parsing fails
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static long parseTimeToSeconds(String timeStr) {
|
||||
long seconds = 0;
|
||||
// Handle potential empty or null strings
|
||||
if (timeStr == null || timeStr.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String[] parts = timeStr.trim().split("\\s+"); // Split by one or more spaces
|
||||
String[] parts = timeStr.trim().split("\\s+");
|
||||
for (String part : parts) {
|
||||
if (part.endsWith("h")) {
|
||||
try {
|
||||
@ -221,14 +206,11 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
seconds += Integer.parseInt(part.substring(0, part.length() - 1)) * 60;
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
// Ignore other parts like "d:" or numbers without units
|
||||
}
|
||||
return seconds;
|
||||
}
|
||||
|
||||
private static String formatPlayerTime(String name, String timeStr) {
|
||||
// timeStr is already formatted like "Total: Xh Ym | 7d: ... | 30d: ..."
|
||||
// We just need to prepend the player name
|
||||
return String.format("§e%s§r: %s", name, timeStr);
|
||||
}
|
||||
|
||||
@ -277,13 +259,9 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
player.sendMessage(footer, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the RSS feed for new releases asynchronously.
|
||||
*/
|
||||
private void checkForUpdates() {
|
||||
updateCheckExecutor.submit(() -> {
|
||||
try {
|
||||
// Get current mod version
|
||||
Optional<ModContainer> modContainer = FabricLoader.getInstance().getModContainer("playertime");
|
||||
if (modContainer.isEmpty()) {
|
||||
LOGGER.warn("[在线时间] 无法获取 Mod 容器,跳过更新检查。");
|
||||
@ -294,7 +272,6 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
|
||||
LOGGER.info("[在线时间] 正在检查更新...");
|
||||
|
||||
// Fetch and parse RSS feed
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
Document doc = builder.parse(new URL(RSS_FEED_URL).openStream());
|
||||
@ -307,14 +284,12 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the latest item (first one in the feed)
|
||||
Element latestItem = (Element) itemList.item(0);
|
||||
String latestVersionString = latestItem.getElementsByTagName("title").item(0).getTextContent();
|
||||
String latestVersionLink = latestItem.getElementsByTagName("link").item(0).getTextContent();
|
||||
|
||||
LOGGER.info("[在线时间] 当前版本: {}, 最新版本 (RSS): {}", currentVersionString, latestVersionString);
|
||||
|
||||
// Compare versions
|
||||
if (isNewerVersion(latestVersionString, currentVersionString)) {
|
||||
LOGGER.warn("==================================================");
|
||||
LOGGER.warn("[在线时间] 发现新版本!");
|
||||
@ -338,17 +313,12 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two version strings (e.g., "1.2.3" vs "1.2.4").
|
||||
* Returns true if newVersion is newer than currentVersion.
|
||||
* Assumes versions are dot-separated integers.
|
||||
*/
|
||||
|
||||
private boolean isNewerVersion(String newVersion, String currentVersion) {
|
||||
if (newVersion == null || currentVersion == null || newVersion.isEmpty() || currentVersion.isEmpty()) {
|
||||
return false; // Cannot compare
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clean up version strings - remove potential prefixes like "v"
|
||||
String cleanNewVersion = newVersion.toLowerCase().startsWith("v") ? newVersion.substring(1) : newVersion;
|
||||
String cleanCurrentVersion = currentVersion.toLowerCase().startsWith("v") ? currentVersion.substring(1) : currentVersion;
|
||||
|
||||
@ -363,15 +333,14 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
int currentPart = (i < currentParts.length) ? parseIntOrZero(currentParts[i]) : 0;
|
||||
|
||||
if (newPart > currentPart) {
|
||||
return true; // New version is newer
|
||||
return true;
|
||||
}
|
||||
if (newPart < currentPart) {
|
||||
return false; // New version is older
|
||||
return false;
|
||||
}
|
||||
// If parts are equal, continue to the next part
|
||||
}
|
||||
|
||||
return false; // Versions are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -381,7 +350,7 @@ public class PlayerTimeMod implements ModInitializer {
|
||||
try {
|
||||
return Integer.parseInt(s);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0; // Treat non-numeric parts as 0 for comparison
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,11 +24,9 @@ public class PlayerTimeTracker {
|
||||
public PlayerTimeTracker(MinecraftServer server) {
|
||||
this.server = server;
|
||||
this.dataFile = server.getRunDirectory().resolve("player_time_data.json");
|
||||
// loadData() is now called later in ServerLifecycleEvents.SERVER_STARTED
|
||||
// loadData(); // <-- Remove this line
|
||||
// loadData();
|
||||
}
|
||||
|
||||
// Make loadData public so it can be called from PlayerTimeMod
|
||||
public void loadData() {
|
||||
if (!Files.exists(dataFile)) {
|
||||
PlayerTimeMod.LOGGER.info("[在线时间] 数据文件未找到,跳过加载");
|
||||
@ -53,7 +51,6 @@ public class PlayerTimeTracker {
|
||||
|
||||
if (data.lastLogin > 0) {
|
||||
PlayerTimeMod.LOGGER.warn(
|
||||
// 修改日志,直接使用 UUID,避免在加载阶段调用 getPlayerName()
|
||||
"[在线时间] 在数据加载过程中发现玩家(UUID:{})的最后登录时间大于0({})。将其重置为0。",
|
||||
uuid, data.lastLogin
|
||||
);
|
||||
@ -75,7 +72,6 @@ public class PlayerTimeTracker {
|
||||
} catch (JsonParseException e) {
|
||||
PlayerTimeMod.LOGGER.error("[在线时间] 玩家在线时间数据文件格式错误", e);
|
||||
} catch (Exception e) {
|
||||
// 捕获更广泛的异常,以防其他未知错误
|
||||
PlayerTimeMod.LOGGER.error("[在线时间] 加载玩家在线时间数据时发生未知错误", e);
|
||||
}
|
||||
}
|
||||
@ -96,7 +92,6 @@ public class PlayerTimeTracker {
|
||||
if (sessionTime > 0) {
|
||||
data.totalTime += sessionTime;
|
||||
|
||||
// 维护30天滚动窗口
|
||||
data.rolling30Days.addPlayTime(now, sessionTime);
|
||||
data.rolling7Days.addPlayTime(now, sessionTime);
|
||||
|
||||
@ -124,7 +119,6 @@ public class PlayerTimeTracker {
|
||||
PlayerTimeStats stats = new PlayerTimeStats();
|
||||
stats.totalTime = data.totalTime;
|
||||
|
||||
// 检查玩家是否当前在线,只在在线时才计算
|
||||
if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) {
|
||||
long currentSessionTime = now - data.lastLogin;
|
||||
if (currentSessionTime > 0) {
|
||||
@ -142,9 +136,7 @@ public class PlayerTimeTracker {
|
||||
Map<String, String> stats = new LinkedHashMap<>();
|
||||
long now = Instant.now().getEpochSecond();
|
||||
|
||||
// 获取白名单玩家UUID集合
|
||||
Set<UUID> whitelistUuids = new HashSet<>();
|
||||
// UserCache 应该在 SERVER_STARTED 之后可用
|
||||
if (server != null && server.getPlayerManager() != null && server.getUserCache() != null) {
|
||||
for (String name : server.getPlayerManager().getWhitelist().getNames()) {
|
||||
server.getUserCache().findByName(name).ifPresent(profile -> {
|
||||
@ -153,18 +145,15 @@ public class PlayerTimeTracker {
|
||||
}
|
||||
} else {
|
||||
PlayerTimeMod.LOGGER.error("[在线时间] 尝试获取白名单玩家统计时 UserCache 或 PlayerManager 不可用!");
|
||||
return stats; // 返回空Map避免崩溃
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
// 遍历所有已记录玩家数据
|
||||
playerData.forEach((uuid, data) -> {
|
||||
// 只处理白名单玩家
|
||||
if (whitelistUuids.contains(uuid)) {
|
||||
String playerName = getPlayerName(uuid); // UserCache 此时应该可用
|
||||
String playerName = getPlayerName(uuid);
|
||||
long totalTime = data.totalTime;
|
||||
|
||||
// 检查玩家是否当前在线,只在在线时才计算
|
||||
if (data.lastLogin > 0 && server.getPlayerManager().getPlayer(uuid) != null) {
|
||||
long currentSessionTime = now - data.lastLogin;
|
||||
if (currentSessionTime > 0) {
|
||||
@ -187,13 +176,11 @@ public class PlayerTimeTracker {
|
||||
}
|
||||
|
||||
public String getPlayerName(UUID uuid) {
|
||||
// 这个方法现在只会在 SERVER_STARTED 之后被调用,UserCache 应该可用
|
||||
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
|
||||
if (player != null) {
|
||||
return player.getName().getString();
|
||||
}
|
||||
|
||||
// 确保 UserCache 不为 null 再使用
|
||||
if (server != null && server.getUserCache() != null) {
|
||||
Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid);
|
||||
if (profile.isPresent()) {
|
||||
@ -215,7 +202,6 @@ public class PlayerTimeTracker {
|
||||
});
|
||||
|
||||
try {
|
||||
// 确保父目录存在
|
||||
Files.createDirectories(dataFile.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(dataFile)) {
|
||||
GSON.toJson(root, writer);
|
||||
@ -255,21 +241,18 @@ public class PlayerTimeTracker {
|
||||
root.add(uuid.toString(), GSON.toJsonTree(data));
|
||||
|
||||
try {
|
||||
// 确保父目录存在
|
||||
Files.createDirectories(dataFile.getParent());
|
||||
try (Writer writer = Files.newBufferedWriter(dataFile)) {
|
||||
GSON.toJson(root, writer);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 异步保存失败时,尝试获取玩家名称可能会再次触发 UserCache 问题,直接使用 UUID
|
||||
String playerName = "UUID: " + uuid.toString();
|
||||
// 尝试获取玩家名称,但要小心 UserCache 是否可用
|
||||
try {
|
||||
if (server != null && server.getPlayerManager() != null) {
|
||||
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
|
||||
if (player != null) {
|
||||
playerName = player.getName().getString();
|
||||
} else if (server.getUserCache() != null) { // 只有 UserCache 可用时才尝试
|
||||
} else if (server.getUserCache() != null) {
|
||||
Optional<GameProfile> profile = server.getUserCache().getByUuid(uuid);
|
||||
if (profile.isPresent()) {
|
||||
playerName = profile.get().getName();
|
||||
@ -277,7 +260,6 @@ public class PlayerTimeTracker {
|
||||
}
|
||||
}
|
||||
} catch (Exception nameEx) {
|
||||
// Ignore error getting name during error logging
|
||||
}
|
||||
|
||||
PlayerTimeMod.LOGGER.error("[在线时间] 无法异步保存玩家的在线时间数据 " + playerName, e);
|
||||
@ -326,7 +308,6 @@ public class PlayerTimeTracker {
|
||||
|
||||
private void cleanUp(long currentTime) {
|
||||
long cutoff = currentTime - (days * 24 * 3600L);
|
||||
// 使用迭代器安全地移除元素
|
||||
entries.removeIf(entry -> entry.timestamp < cutoff);
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,6 @@ public class WebServer {
|
||||
|
||||
try {
|
||||
// 白名单
|
||||
// timeTracker.getWhitelistedPlayerStats() 内部已处理 UserCache null 检查
|
||||
Map<String, String> stats = timeTracker.getWhitelistedPlayerStats();
|
||||
String response = GSON.toJson(stats);
|
||||
sendResponse(exchange, 200, response.getBytes(StandardCharsets.UTF_8), "application/json");
|
||||
@ -109,8 +108,7 @@ public class WebServer {
|
||||
.orElse(null);
|
||||
|
||||
if (is == null) {
|
||||
// Fallback to default language if configured language file is not found
|
||||
langCode = "en_us"; // Default fallback
|
||||
langCode = "zh_cn";
|
||||
resourcePath = String.format("assets/playertime/lang/%s.json", langCode);
|
||||
String finalResourcePath = resourcePath;
|
||||
is = FabricLoader.getInstance().getModContainer("playertime")
|
||||
@ -244,7 +242,6 @@ public class WebServer {
|
||||
JsonArray whitelistPlayers = new JsonArray();
|
||||
|
||||
Set<UUID> whitelistUuids = new HashSet<>();
|
||||
// 增加 UserCache null 检查
|
||||
if (server != null && server.getUserCache() != null) {
|
||||
for (String name : playerManager.getWhitelist().getNames()) {
|
||||
server.getUserCache().findByName(name).ifPresent(profile -> {
|
||||
@ -272,11 +269,10 @@ public class WebServer {
|
||||
|
||||
JsonArray topPlayers = new JsonArray();
|
||||
timeTracker.getPlayerData().entrySet().stream()
|
||||
.filter(entry -> whitelistUuids.contains(entry.getKey())) // 只筛选白名单玩家
|
||||
.filter(entry -> whitelistUuids.contains(entry.getKey()))
|
||||
.sorted((a, b) -> Long.compare(b.getValue().totalTime, a.getValue().totalTime))
|
||||
.limit(3)
|
||||
.forEach(entry -> {
|
||||
// timeTracker.getPlayerName() 内部已处理 UserCache null 检查
|
||||
JsonObject playerJson = new JsonObject();
|
||||
playerJson.addProperty("name", timeTracker.getPlayerName(entry.getKey()));
|
||||
playerJson.addProperty("time", PlayerTimeTracker.formatTime(entry.getValue().totalTime)); // formatTime doesn't need localization
|
||||
@ -312,7 +308,6 @@ public class WebServer {
|
||||
|
||||
// 获取白名单玩家UUID集合
|
||||
Set<UUID> whitelistUuids = new HashSet<>();
|
||||
// 增加 UserCache null 检查
|
||||
if (minecraftServer != null && minecraftServer.getUserCache() != null) {
|
||||
for (String name : playerManager.getWhitelist().getNames()) {
|
||||
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
|
||||
@ -370,7 +365,6 @@ public class WebServer {
|
||||
|
||||
// 获取白名单玩家UUID集合
|
||||
Set<UUID> whitelistUuids = new HashSet<>();
|
||||
// 增加 UserCache null 检查
|
||||
if (minecraftServer != null && minecraftServer.getUserCache() != null) {
|
||||
for (String name : playerManager.getWhitelist().getNames()) {
|
||||
minecraftServer.getUserCache().findByName(name).ifPresent(profile -> {
|
||||
@ -409,7 +403,7 @@ public class WebServer {
|
||||
server.createContext("/api/whitelist", exchange -> {
|
||||
handleCors(exchange);
|
||||
if ("OPTIONS".equals(exchange.getRequestMethod())) {
|
||||
exchange.sendResponseHeaders(204, -1); // 204 No Content
|
||||
exchange.sendResponseHeaders(204, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -422,18 +416,15 @@ public class WebServer {
|
||||
PlayerManager playerManager = minecraftServer.getPlayerManager();
|
||||
JsonArray whitelist = new JsonArray();
|
||||
|
||||
// 增加 UserCache null 检查
|
||||
if (minecraftServer != null && minecraftServer.getUserCache() != null) {
|
||||
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 {
|
||||
@ -545,7 +536,7 @@ public class WebServer {
|
||||
}
|
||||
|
||||
try {
|
||||
Path dataFile = timeTracker.getDataFile(); // 从PlayerTimeTracker获取文件路径
|
||||
Path dataFile = timeTracker.getDataFile();
|
||||
|
||||
if (!Files.exists(dataFile)) {
|
||||
PlayerTimeMod.LOGGER.warn("[在线时间] 玩家数据文件未找到: {}", dataFile);
|
||||
|
@ -42,5 +42,15 @@
|
||||
|
||||
"playertime.web.refresh_button": "Refresh",
|
||||
|
||||
"playertime.web.error.load_failed": "Failed to load data, check console"
|
||||
"playertime.web.error.load_failed": "Failed to load data, check console",
|
||||
|
||||
"playertime.web.server_status.disk_usage": "Disk Usage",
|
||||
"playertime.web.server_status.disk_used": "Used",
|
||||
"playertime.web.server_status.disk_free": "Free",
|
||||
"playertime.web.server_status.disk_percent": "Usage",
|
||||
"playertime.web.server_status.processors": "Processors",
|
||||
"playertime.web.server_status.server_version": "Server Version",
|
||||
"playertime.web.server_status.disk_total": "Total",
|
||||
"playertime.web.chart.disk_used": "Used",
|
||||
"playertime.web.chart.disk_free": "Free"
|
||||
}
|
||||
|
@ -42,5 +42,15 @@
|
||||
|
||||
"playertime.web.refresh_button": "刷新数据",
|
||||
|
||||
"playertime.web.error.load_failed": "加载数据失败,请检查控制台"
|
||||
"playertime.web.error.load_failed": "加载数据失败,请检查控制台",
|
||||
|
||||
"playertime.web.server_status.disk_usage": "磁盘使用",
|
||||
"playertime.web.server_status.disk_used": "已用",
|
||||
"playertime.web.server_status.disk_free": "可用",
|
||||
"playertime.web.server_status.disk_percent": "使用率",
|
||||
"playertime.web.server_status.processors": "处理器核心",
|
||||
"playertime.web.server_status.server_version": "服务器版本",
|
||||
"playertime.web.server_status.disk_total": "总量",
|
||||
"playertime.web.chart.disk_used": "已用",
|
||||
"playertime.web.chart.disk_free": "可用"
|
||||
}
|
||||
|
@ -621,4 +621,93 @@ canvas {
|
||||
}
|
||||
}
|
||||
|
||||
.server-info-stats div {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.server-info-stats div:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-refresh-btn {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
z-index: 1000;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.refresh-btn.floating {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.refresh-btn.floating i {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.refresh-btn.floating:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.refresh-btn.floating.loading i {
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.floating-refresh-btn {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.refresh-btn.floating {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.floating-refresh-btn {
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.refresh-btn.floating {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.refresh-btn.floating i {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -90,6 +90,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<h3 data-lang-key="playertime.web.server_status.disk_usage">磁盘使用</h3>
|
||||
<canvas id="disk-chart"></canvas>
|
||||
<div class="memory-stats">
|
||||
<div><span data-lang-key="playertime.web.server_status.disk_total">总量</span>: <span id="disk-total">0</span> GB</div>
|
||||
<div><span data-lang-key="playertime.web.server_status.disk_used">已用</span>: <span id="disk-used">0</span> GB</div>
|
||||
<div><span data-lang-key="playertime.web.server_status.disk_free">可用</span>: <span id="disk-free">0</span> GB</div>
|
||||
<div><span data-lang-key="playertime.web.server_status.disk_percent">使用率</span>: <span id="disk-percent">0</span>%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<h3 data-lang-key="playertime.web.server_status.server_info">服务器信息</h3>
|
||||
<div class="server-info-stats">
|
||||
<div><span data-lang-key="playertime.web.server_status.server_version">服务器版本</span>: <span id="server-version">未知</span></div>
|
||||
<div><span data-lang-key="playertime.web.server_status.processors">处理器核心</span>: <span id="processors">0</span></div>
|
||||
<div><span data-lang-key="playertime.web.server_status.player_count">在线玩家</span>: <span id="server-player-count">0</span>/<span id="server-max-players">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-item full-width">
|
||||
@ -127,5 +147,10 @@
|
||||
<p>Copyright © 2025 <a href="https://git.branulf.top/BRanulf" target="_blank">BRanulf</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="floating-refresh-btn">
|
||||
<button id="floating-refresh-btn" class="refresh-btn floating" title="刷新数据">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,6 +5,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let memoryChart = null;
|
||||
let performanceChart = null;
|
||||
let lang = {};
|
||||
let diskChart = null;
|
||||
const floatingRefreshBtn = document.getElementById('floating-refresh-btn');
|
||||
|
||||
|
||||
// 获取语言
|
||||
const elements = {
|
||||
@ -93,6 +96,29 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
|
||||
if (floatingRefreshBtn) {
|
||||
floatingRefreshBtn.addEventListener('click', function() {
|
||||
if (this.classList.contains('loading')) return;
|
||||
|
||||
this.classList.add('loading');
|
||||
loadAllData().finally(() => {
|
||||
setTimeout(() => {
|
||||
this.classList.remove('loading');
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// window.addEventListener('scroll', function() {
|
||||
// if (!floatingRefreshBtn) return;
|
||||
//
|
||||
// if (window.scrollY > 100) {
|
||||
// floatingRefreshBtn.style.transform = 'scale(1)';
|
||||
// } else {
|
||||
// floatingRefreshBtn.style.transform = 'scale(0.9)';
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
// 图表
|
||||
function initCharts() {
|
||||
@ -221,6 +247,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const diskCtx = document.getElementById('disk-chart')?.getContext('2d');
|
||||
if (diskCtx) {
|
||||
diskChart = new Chart(diskCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: [getLangString('playertime.web.chart.disk_used'),
|
||||
getLangString('playertime.web.chart.disk_free')],
|
||||
datasets: [{
|
||||
data: [0, 100],
|
||||
backgroundColor: ['#4361ee', '#e9ecef']
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
@ -409,6 +459,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
}
|
||||
|
||||
if (data.disk) {
|
||||
const totalGB = Math.round(data.disk.total / (1024 * 1024 * 1024));
|
||||
const usedGB = Math.round(data.disk.used / (1024 * 1024 * 1024));
|
||||
const freeGB = Math.round(data.disk.free / (1024 * 1024 * 1024));
|
||||
const percent = Math.round(data.disk.usage_percentage);
|
||||
|
||||
if (diskChart) {
|
||||
diskChart.data.datasets[0].data = [usedGB, freeGB];
|
||||
diskChart.update();
|
||||
}
|
||||
|
||||
document.getElementById('disk-total').textContent = totalGB;
|
||||
document.getElementById('disk-used').textContent = usedGB;
|
||||
document.getElementById('disk-free').textContent = freeGB;
|
||||
document.getElementById('disk-percent').textContent = percent;
|
||||
}
|
||||
|
||||
// 新增服务器信息
|
||||
if (data.available_processors) {
|
||||
document.getElementById('processors').textContent = data.available_processors;
|
||||
}
|
||||
|
||||
if (data.server) {
|
||||
document.getElementById('server-version').textContent = data.server.version;
|
||||
document.getElementById('server-player-count').textContent = data.server.player_count;
|
||||
document.getElementById('server-max-players').textContent = data.server.max_players;
|
||||
}
|
||||
|
||||
if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0';
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user