awa
This commit is contained in:
parent
23a0518ca7
commit
1ffa91a98a
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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("[在线时间] 调度程序关闭");
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"webPort": 60048,
|
||||
"language": "zh_cn"
|
||||
"language": "zh_cn",
|
||||
"autoSaveSeconds": 60
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user