This commit is contained in:
BRanulf 2025-06-05 12:04:24 +08:00
parent 23a0518ca7
commit 1ffa91a98a
5 changed files with 58 additions and 81 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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("[在线时间] 调度程序关闭");
}

View File

@ -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 = `<li>${getLangString(emptyMessageKey)}</li>`;
}
};
@ -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) {

View File

@ -1,4 +1,5 @@
{
"webPort": 60048,
"language": "zh_cn"
"language": "zh_cn",
"autoSaveSeconds": 60
}