diff --git a/gradle.properties b/gradle.properties index 456764f..9b61fe8 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.128 +mod_version=1.14.514.129 maven_group=org.example1 archives_base_name=playerOnlineTimeTrackerMod # Dependencies diff --git a/src/main/java/com/example/playertime/ModConfig.java b/src/main/java/com/example/playertime/ModConfig.java index 9577d13..e59a1f5 100644 --- a/src/main/java/com/example/playertime/ModConfig.java +++ b/src/main/java/com/example/playertime/ModConfig.java @@ -9,6 +9,7 @@ public class ModConfig { private final Path configPath; private int webPort = 60048; private String language = "zh_cn"; + private long autoSaveSeconds = 300; public ModConfig(Path configDir) { this.configPath = configDir.resolve("playertime-config.json"); @@ -17,7 +18,7 @@ public class ModConfig { private void loadConfig() { if (!Files.exists(configPath)) { - PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置"); // 使用英文日志,或者也本地化日志?这里先用英文 + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置"); saveConfig(); return; } @@ -35,6 +36,9 @@ public class ModConfig { if (json.has("webPort")) { webPort = json.get("webPort").getAsInt(); + } else { + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“webPort”字段,添加默认值'%s'并保存", webPort); + saveConfig(); } if (json.has("language")) { language = json.get("language").getAsString(); @@ -42,6 +46,12 @@ public class ModConfig { PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“language”字段,添加默认值'%s'并保存", language); saveConfig(); } + if (json.has("autoSaveSeconds")) { + autoSaveSeconds = json.get("autoSaveSeconds").getAsLong(); + } else { + PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“autoSaveSeconds”字段,添加默认值'%s'并保存", autoSaveSeconds); + saveConfig(); + } } catch (Exception e) { PlayerTimeMod.LOGGER.error("[在线时间] 加载配置文件失败,使用默认配置", e); @@ -54,6 +64,7 @@ public class ModConfig { JsonObject json = new JsonObject(); json.addProperty("webPort", webPort); json.addProperty("language", language); + json.addProperty("autoSaveSeconds", autoSaveSeconds); try { Files.createDirectories(configPath.getParent()); @@ -75,4 +86,9 @@ public class ModConfig { public String getLanguage() { return language; } + + // 获取保存间隔 + public long getSeconds() { + return autoSaveSeconds; + } } diff --git a/src/main/java/com/example/playertime/PlayerTimeMod.java b/src/main/java/com/example/playertime/PlayerTimeMod.java index 04bc05d..cef7e4a 100644 --- a/src/main/java/com/example/playertime/PlayerTimeMod.java +++ b/src/main/java/com/example/playertime/PlayerTimeMod.java @@ -35,13 +35,13 @@ public class PlayerTimeMod implements ModInitializer { // TODO 定时保存配置文件没整,暂时硬编码 private ScheduledExecutorService scheduler; private ScheduledFuture saveTask; - private static final long AUTO_SAVE_INTERVAL_SECONDS = 5 * 60; + private static long AUTO_SAVE_INTERVAL_SECONDS; @Override public void onInitialize() { config = new ModConfig(FabricLoader.getInstance().getConfigDir()); - localizationManager = new LocalizationManager(config.getLanguage()); // 初始化本地化管理器 + localizationManager = new LocalizationManager(config.getLanguage()); scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -49,6 +49,8 @@ public class PlayerTimeMod implements ModInitializer { try { LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); + AUTO_SAVE_INTERVAL_SECONDS = config.getSeconds(); + ServerLifecycleEvents.SERVER_STARTING.register(server -> { timeTracker = new PlayerTimeTracker(server); try { @@ -102,7 +104,7 @@ public class PlayerTimeMod implements ModInitializer { } } catch (InterruptedException e) { LOGGER.error("[在线时间] 调度程序终止被中断", e); - Thread.currentThread().interrupt(); // Restore interrupted status + Thread.currentThread().interrupt(); } LOGGER.info("[在线时间] 调度程序关闭"); } diff --git a/src/main/resources/assets/playertime/web/js/app.js b/src/main/resources/assets/playertime/web/js/app.js index 1b74570..82c8602 100644 --- a/src/main/resources/assets/playertime/web/js/app.js +++ b/src/main/resources/assets/playertime/web/js/app.js @@ -1,17 +1,17 @@ document.addEventListener('DOMContentLoaded', function() { - // 1. 首先初始化所有变量 + // 初始化变量 const tpsHistory = Array(30).fill(20); const msptHistory = Array(30).fill(50); let memoryChart = null; let performanceChart = null; - let lang = {}; // 新增:存储语言文件内容 + let lang = {}; - // 2. 获取DOM元素(带安全检查) + // 获取语言 const elements = { refreshBtn: document.getElementById('refresh-btn'), themeToggle: document.getElementById('theme-toggle'), - statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], // Changed ID for clarity - statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0], // Added for header localization + statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], + statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0], whitelistPlayers: document.getElementById('whitelist-players'), nonWhitelistPlayers: document.getElementById('non-whitelist-players'), totalCount: document.getElementById('total-count'), @@ -20,30 +20,27 @@ document.addEventListener('DOMContentLoaded', function() { memoryUsed: document.getElementById('memory-used'), memoryFree: document.getElementById('memory-free'), memoryPercent: document.getElementById('memory-percent'), - avgTick: document.getElementById('avg-tick'), // This element seems unused in updateServerStatus now, can remove if not needed + 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. 初始加载语言文件,然后加载其他数据并初始化图表 + // 加载语言 loadLanguage().then(() => { - translatePage(); // 本地化页面静态文本 - initCharts(); // 初始化图表(需要语言文本) - loadAllData(); // 加载动态数据 - // 7. 设置定时刷新(10秒) + translatePage(); + initCharts(); + loadAllData(); + // 定时刷新 const refreshInterval = setInterval(loadAllData, 10000); }).catch(error => { console.error('Failed to load language or initial data:', error); - showError('Failed to load language or initial data.'); // Fallback error message + showError('Failed to load language or initial data.'); }); - - // 5. 事件监听器 if (elements.themeToggle) { elements.themeToggle.addEventListener('click', toggleTheme); } @@ -52,9 +49,8 @@ document.addEventListener('DOMContentLoaded', function() { elements.refreshBtn.addEventListener('click', handleRefresh); } - /*** 功能函数 ***/ - // 新增:加载语言文件 + // 后端加载语言 async function loadLanguage() { try { const response = await fetch('/api/lang'); @@ -63,18 +59,15 @@ document.addEventListener('DOMContentLoaded', function() { console.log('Language file loaded.'); } catch (error) { console.error('Failed to load language file:', error); - // Fallback to English keys if language file fails to load lang = {}; - throw error; // Propagate error to stop further loading if language is critical + throw error; } } - // 新增:根据data-lang-key属性本地化页面元素 function translatePage() { document.querySelectorAll('[data-lang-key]').forEach(element => { const key = element.getAttribute('data-lang-key'); if (lang[key]) { - // Special handling for title tag if (element.tagName === 'TITLE') { document.title = lang[key]; } else { @@ -86,11 +79,9 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 新增:获取本地化字符串 function getLangString(key, ...args) { - const pattern = lang[key] || key; // Use key as fallback + const pattern = lang[key] || key; try { - // Simple placeholder replacement {0}, {1}, etc. let result = pattern; args.forEach((arg, index) => { result = result.replace(new RegExp('\\{' + index + '\\}', 'g'), arg); @@ -98,19 +89,18 @@ document.addEventListener('DOMContentLoaded', function() { return result; } catch (e) { console.error(`Failed to format string for key: ${key}`, e); - return pattern; // Return unformatted pattern on error + return pattern; } } + // 图表 function initCharts() { - // 内存图表 const memoryCtx = document.getElementById('memory-chart')?.getContext('2d'); if (memoryCtx) { memoryChart = new Chart(memoryCtx, { type: 'doughnut', data: { - // Use localized labels labels: [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')], datasets: [{ data: [0, 100], @@ -129,7 +119,6 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // 性能图表 const perfCtx = document.getElementById('performance-chart')?.getContext('2d'); if (perfCtx) { performanceChart = new Chart(perfCtx, { @@ -138,7 +127,6 @@ document.addEventListener('DOMContentLoaded', function() { labels: Array(30).fill(''), datasets: [ { - // Use localized labels label: getLangString('playertime.web.chart.tps'), data: tpsHistory, borderColor: '#4CAF50', @@ -150,7 +138,6 @@ document.addEventListener('DOMContentLoaded', function() { yAxisID: 'y' }, { - // Use localized labels label: getLangString('playertime.web.chart.mspt'), data: msptHistory, borderColor: '#FF5722', @@ -203,7 +190,6 @@ document.addEventListener('DOMContentLoaded', function() { position: 'left', title: { display: true, - // Use localized labels text: getLangString('playertime.web.chart.tps'), font: { weight: 'bold' @@ -221,7 +207,6 @@ document.addEventListener('DOMContentLoaded', function() { position: 'right', title: { display: true, - // Use localized labels text: getLangString('playertime.web.chart.mspt'), font: { weight: 'bold' @@ -246,10 +231,10 @@ document.addEventListener('DOMContentLoaded', function() { } function handleRefresh() { - if (this.classList.contains('loading')) return; // Prevent multiple clicks + if (this.classList.contains('loading')) return; this.classList.add('loading'); - loadAllData().finally(() => { // Use finally to ensure loading class is removed - setTimeout(() => { // Add a small delay for visual feedback + loadAllData().finally(() => { + setTimeout(() => { this.classList.remove('loading'); }, 500); }); @@ -257,7 +242,6 @@ document.addEventListener('DOMContentLoaded', function() { async function loadAllData() { try { - // Fetch data concurrently const [statsData, onlinePlayersData, playerCountsData, serverStatusData] = await Promise.all([ fetchData('/api/stats'), fetchData('/api/online-players'), @@ -265,7 +249,6 @@ document.addEventListener('DOMContentLoaded', function() { fetchData('/api/server-status') ]); - // Update UI with fetched data updateTable(statsData); updateOnlinePlayers(onlinePlayersData); updatePlayerCounts(playerCountsData); @@ -273,14 +256,13 @@ document.addEventListener('DOMContentLoaded', function() { } catch (error) { console.error('加载数据出错:', error); - showError(getLangString('playertime.web.error.load_failed')); // Use localized error message + showError(getLangString('playertime.web.error.load_failed')); } } async function fetchData(url) { const response = await fetch(url); if (!response.ok) { - // Attempt to read error body if available const errorBody = await response.text().catch(() => 'Unknown Error'); throw new Error(`HTTP error! status: ${response.status}, URL: ${url}, Body: ${errorBody}`); } @@ -290,9 +272,7 @@ document.addEventListener('DOMContentLoaded', function() { function updateTable(data) { if (!elements.statsTableBody || !elements.statsTableHeader) return; - elements.statsTableBody.innerHTML = ''; // Clear existing rows - - // Update table headers using localization + elements.statsTableBody.innerHTML = ''; const headers = elements.statsTableHeader.querySelectorAll('th[data-lang-key]'); headers.forEach(th => { const key = th.getAttribute('data-lang-key'); @@ -304,25 +284,19 @@ document.addEventListener('DOMContentLoaded', function() { const sortedPlayers = Object.entries(data) .map(([name, statString]) => { - // Parse the localized stat string const stats = {}; - // Split by " | " first statString.split(" | ").forEach(part => { - // Find the first colon to split label and value const firstColonIndex = part.indexOf(':'); if (firstColonIndex > 0) { const label = part.substring(0, firstColonIndex).trim(); const value = part.substring(firstColonIndex + 1).trim(); stats[label] = value; } else { - // Handle cases without colon if necessary, or log a warning console.warn(`Could not parse stat part: ${part}`); } }); - // Map localized labels back to internal keys for sorting const internalStats = {}; - // This mapping assumes the order in the format string is consistent const formatParts = getLangString('playertime.stats.format').split(" | ").map(p => p.split(":")[0].trim()); if (formatParts.length >= 3) { @@ -331,7 +305,6 @@ document.addEventListener('DOMContentLoaded', function() { internalStats['7Days'] = stats[formatParts[2]]; } else { console.error("Unexpected format string structure:", getLangString('playertime.stats.format')); - // Fallback to using the raw statString if parsing fails internalStats.totalTime = statString.split(" | ")[0]?.split(": ")[1] || "0h 00m"; internalStats['30Days'] = statString.split(" | ")[1]?.split(": ")[1] || "0h 00m"; internalStats['7Days'] = statString.split(" | ")[2]?.split(": ")[1] || "0h 00m"; @@ -345,8 +318,8 @@ document.addEventListener('DOMContentLoaded', function() { if (sortedPlayers.length === 0) { const row = elements.statsTableBody.insertRow(); const cell = row.insertCell(0); - cell.colSpan = 4; // Span across all columns - cell.textContent = getLangString('playertime.web.stats_table.empty'); // Use localized empty message + cell.colSpan = 4; + cell.textContent = getLangString('playertime.web.stats_table.empty'); cell.style.textAlign = 'center'; cell.style.fontStyle = 'italic'; return; @@ -374,7 +347,6 @@ document.addEventListener('DOMContentLoaded', function() { element.appendChild(li); }); } else { - // Use localized empty message element.innerHTML = `
  • ${getLangString(emptyMessageKey)}
  • `; } }; @@ -390,7 +362,6 @@ document.addEventListener('DOMContentLoaded', function() { } function updateServerStatus(data) { - // Update memory info if (data.memory) { const usedMB = Math.round(data.memory.used / (1024 * 1024)); const freeMB = Math.round((data.memory.max - data.memory.used) / (1024 * 1024)); @@ -398,7 +369,6 @@ document.addEventListener('DOMContentLoaded', function() { if (memoryChart) { memoryChart.data.datasets[0].data = [usedMB, freeMB]; - // Update chart labels if they weren't set during initCharts (e.g., if lang loaded later) memoryChart.data.labels = [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')]; memoryChart.update(); } @@ -408,60 +378,49 @@ document.addEventListener('DOMContentLoaded', function() { if (elements.memoryPercent) elements.memoryPercent.textContent = percent; } - // Update performance info if (data.server) { - // Calculate TPS (limit between 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); - // Update display if (elements.tpsValue) elements.tpsValue.textContent = tps; if (elements.msptValue) elements.msptValue.textContent = mspt; - // Update chart data if (performanceChart) { - // Update history data tpsHistory.shift(); tpsHistory.push(parseFloat(tps)); msptHistory.shift(); msptHistory.push(parseFloat(mspt)); - // Update chart labels if they weren't set during initCharts performanceChart.data.datasets[0].label = getLangString('playertime.web.chart.tps'); performanceChart.data.datasets[1].label = getLangString('playertime.web.chart.mspt'); if (performanceChart.options.scales.y.title) performanceChart.options.scales.y.title.text = getLangString('playertime.web.chart.tps'); if (performanceChart.options.scales.y1.title) performanceChart.options.scales.y1.title.text = getLangString('playertime.web.chart.mspt'); - // Only update data (do not recreate chart) performanceChart.data.datasets[0].data = tpsHistory; performanceChart.data.datasets[1].data = msptHistory; - // Smooth update performanceChart.update('none'); - // Set color based on value - setMetricColor(elements.tpsValue, tps, 15, 10, false); // TPS: higher is better - setMetricColor(elements.msptValue, mspt, 50, 100, true); // MSPT: lower is better + setMetricColor(elements.tpsValue, tps, 15, 10, false); + setMetricColor(elements.msptValue, mspt, 50, 100, true); } - // if (elements.avgTick) elements.avgTick.textContent = data.server.average_tick_time_ms?.toFixed(2) || '0'; // This element seems unused } - if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0'; // uptime_formatted is already localized by server + if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0'; } function parseTimeToSeconds(timeStr) { if (!timeStr) return 0; - // Ensure parsing works even if labels are present, by only looking for h and m const parts = timeStr.match(/(\d+h)?\s*(\d+m)?/); let seconds = 0; if (parts) { - if (parts[1]) { // hours part + if (parts[1]) { // hour seconds += parseInt(parts[1].replace('h', '')) * 3600; } - if (parts[2]) { // minutes part + if (parts[2]) { // min seconds += parseInt(parts[2].replace('m', '')) * 60; } } @@ -480,18 +439,17 @@ document.addEventListener('DOMContentLoaded', function() { setTimeout(() => { errorEl.classList.remove('show'); setTimeout(() => document.body.removeChild(errorEl), 300); - }, 5000); // Display for 5 seconds - }, 10); // Small delay to allow CSS transition + }, 5000); + }, 10); } - // Helper function to set color based on metric value and thresholds function setMetricColor(element, value, goodThreshold, badThreshold, reverse = false) { if (!element) return; const numValue = parseFloat(value); let color = ''; - if (reverse) { // For MSPT: lower is better + if (reverse) { if (numValue <= goodThreshold) { color = '#4CAF50'; // Green } else if (numValue <= badThreshold) { @@ -499,7 +457,7 @@ document.addEventListener('DOMContentLoaded', function() { } else { color = '#FF5722'; // Red } - } else { // For TPS: higher is better + } else { if (numValue >= goodThreshold) { color = '#4CAF50'; // Green } else if (numValue >= badThreshold) { diff --git a/src/main/resources/config/playertime-default-config.json b/src/main/resources/config/playertime-default-config.json index 9963ca1..fe0250d 100644 --- a/src/main/resources/config/playertime-default-config.json +++ b/src/main/resources/config/playertime-default-config.json @@ -1,4 +1,5 @@ { "webPort": 60048, - "language": "zh_cn" + "language": "zh_cn", + "autoSaveSeconds": 60 }