小更新

This commit is contained in:
BRanulf 2025-04-19 22:29:36 +08:00
parent ef3f3dd1b8
commit 387f1a83de
6 changed files with 482 additions and 75 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.120
mod_version=1.14.514.121
maven_group=org.example1
archives_base_name=playerOnlineTimeTrackerMod
# Dependencies

View File

@ -1,12 +1,26 @@
package com.example.playertime;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.ClickEvent;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class PlayerTimeMod implements ModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("PlayerTimeTracker");
private static PlayerTimeTracker timeTracker;
@ -60,6 +74,7 @@ public class PlayerTimeMod implements ModInitializer {
LOGGER.error("[在线时间] Mod 初始化失败!", e);
throw new RuntimeException("[在线时间] Mod 初始化失败", e);
}
registerCommands();
}
public static ModConfig getConfig() {
@ -71,4 +86,107 @@ public class PlayerTimeMod implements ModInitializer {
public static PlayerTimeTracker getTimeTracker() {
return timeTracker;
}
public static void registerCommands() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
dispatcher.register(CommandManager.literal("onlineTime")
.requires(source -> source.hasPermissionLevel(0))
.executes(context -> showOnlineTime(context.getSource(), 1)) // 默认第一页
.then(CommandManager.argument("page", IntegerArgumentType.integer(1))
.executes(context -> showOnlineTime(
context.getSource(),
IntegerArgumentType.getInteger(context, "page")
))
)
);
});
}
private static int showOnlineTime(ServerCommandSource source, int requestedPage) {
ServerPlayerEntity player = source.getPlayer();
if (player == null) return 0;
CompletableFuture.runAsync(() -> {
PlayerTimeTracker tracker = getTimeTracker();
if (tracker != null) {
Map<String, String> stats = tracker.getWhitelistedPlayerStats();
List<String> sorted = stats.entrySet().stream()
.sorted((a, b) -> comparePlayTime(b.getValue(), a.getValue()))
.map(entry -> formatPlayerTime(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
sendPaginatedMessage(player, sorted, requestedPage);
}
}, Util.getMainWorkerExecutor());
return 1;
}
private static int comparePlayTime(String a, String b) {
return Long.compare(parseTimeToSeconds(a), parseTimeToSeconds(b));
}
private static long parseTimeToSeconds(String timeStr) {
long seconds = 0;
String[] parts = timeStr.split(" ");
for (String part : parts) {
if (part.contains("h")) {
seconds += Integer.parseInt(part.replace("h", "")) * 3600;
} else if (part.contains("m")) {
seconds += Integer.parseInt(part.replace("m", "")) * 60;
}
}
return seconds;
}
private static String formatPlayerTime(String name, String timeStr) {
return String.format("§e%s§r: %s", name, timeStr.split(" \\| ")[0]); // 只显示总时长
}
private static void sendPaginatedMessage(ServerPlayerEntity player, List<String> lines, int page) {
int pageSize = 10;
int totalPages = (lines.size() + pageSize - 1) / pageSize;
page = Math.max(1, Math.min(page, totalPages));
int from = (page - 1) * pageSize;
int to = Math.min(from + pageSize, lines.size());
// 标题
player.sendMessage(Text.literal("§6===== 玩家在线时长 (第 " + page + "/" + totalPages + " 页) ====="), false);
// 内容
for (int i = from; i < to; i++) {
player.sendMessage(Text.literal(lines.get(i)), false);
}
// 翻页按钮
MutableText footer = Text.literal("");
if (page > 1) {
int finalPage = page;
footer.append(Text.literal("§a[上一页]")
.styled(style -> style.withClickEvent(new ClickEvent(
ClickEvent.Action.RUN_COMMAND,
"/onlineTime " + (finalPage - 1)
)))
.append(" "));
}
footer.append(Text.literal("§7共 " + lines.size() + " 位玩家"));
if (page < totalPages) {
int finalPage1 = page;
footer.append(" ").append(Text.literal("§a[下一页]")
.styled(style -> style.withClickEvent(new ClickEvent(
ClickEvent.Action.RUN_COMMAND,
"/onlineTime " + (finalPage1 + 1)
))));
}
player.sendMessage(footer, false);
}
}

View File

@ -345,7 +345,6 @@ public class WebServer {
Runtime runtime = Runtime.getRuntime();
JsonObject status = new JsonObject();
// 内存使用情况
long maxMemory = runtime.maxMemory();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();

View File

@ -532,3 +532,93 @@ canvas {
opacity: 1;
}
.performance-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 15px;
}
.metric {
background: var(--card-bg);
padding: 10px;
border-radius: 8px;
box-shadow: var(--shadow);
}
.metric-label {
font-weight: bold;
display: block;
margin-bottom: 5px;
color: var(--primary-color-c);
}
.metric-value {
font-size: 1.5rem;
font-family: monospace;
}
@media (max-width: 600px) {
.performance-metrics {
grid-template-columns: 1fr;
}
}
.chart-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.chart-wrapper {
position: relative;
height: 300px; /* 固定高度 */
width: 100%;
}
.metric-display {
display: flex;
gap: 20px;
justify-content: center;
}
.metric {
background: var(--card-bg);
padding: 12px 20px;
border-radius: 8px;
box-shadow: var(--shadow);
min-width: 120px;
text-align: center;
}
.metric-label {
font-weight: bold;
display: block;
margin-bottom: 5px;
color: var(--primary-color-c);
font-size: 0.9rem;
}
.metric-value {
font-size: 1.8rem;
font-family: monospace;
font-weight: bold;
}
@media (max-width: 768px) {
.chart-wrapper {
height: 250px;
}
.metric-display {
flex-direction: column;
gap: 10px;
}
.metric {
padding: 10px 15px;
}
}

View File

@ -69,14 +69,26 @@
<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>
<h3>实时性能</h3>
<div class="chart-container">
<div class="metric-display">
<div class="metric">
<span class="metric-label">TPS:</span>
<span class="metric-value" id="tps-value">0.0</span>
</div>
<div class="metric">
<span class="metric-label">MSPT:</span>
<span class="metric-value" id="mspt-value">0.0</span>
</div>
</div>
<div class="chart-wrapper">
<canvas id="performance-chart"></canvas>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,11 @@
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素带安全检查
// 1. 首先初始化所有变量
const tpsHistory = Array(30).fill(20);
const msptHistory = Array(30).fill(50);
let memoryChart = null;
let performanceChart = null;
// 2. 获取DOM元素带安全检查
const elements = {
refreshBtn: document.getElementById('refresh-btn'),
themeToggle: document.getElementById('theme-toggle'),
@ -13,34 +19,107 @@ document.addEventListener('DOMContentLoaded', function() {
memoryFree: document.getElementById('memory-free'),
memoryPercent: document.getElementById('memory-percent'),
avgTick: document.getElementById('avg-tick'),
uptime: document.getElementById('uptime')
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. 初始化图表(必须在其他函数之前)
initCharts();
// 5. 事件监听器
if (elements.themeToggle) {
elements.themeToggle.addEventListener('click', toggleTheme);
}
// 刷新按钮
if (elements.refreshBtn) {
elements.refreshBtn.addEventListener('click', handleRefresh);
}
// 初始化图表
const memoryChart = initMemoryChart();
const performanceChart = initPerformanceChart();
// 初始加载数据
// 6. 初始加载数据
loadAllData();
// 设置定时刷新10秒
setInterval(loadAllData, 10000);
// 7. 设置定时刷新10秒
const refreshInterval = setInterval(loadAllData, 10000);
/*** 功能函数 ***/
function initCharts() {
// 内存图表
const memoryCtx = document.getElementById('memory-chart')?.getContext('2d');
if (memoryCtx) {
memoryChart = new Chart(memoryCtx, {
type: 'doughnut',
data: {
labels: ['已使用', '未使用'],
datasets: [{
data: [0, 100],
backgroundColor: ['#4361ee', '#e9ecef']
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
// 性能图表
const perfCtx = document.getElementById('performance-chart')?.getContext('2d');
if (perfCtx) {
performanceChart = new Chart(perfCtx, {
type: 'line',
data: {
labels: Array(30).fill(''),
datasets: [
{
label: 'TPS',
data: tpsHistory,
borderColor: '#4CAF50',
tension: 0.1,
yAxisID: 'y'
},
{
label: 'MSPT',
data: msptHistory,
borderColor: '#FF5722',
tension: 0.1,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
interaction: {
mode: 'index',
intersect: false
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
title: { display: true, text: 'TPS' },
min: 0,
max: 20
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: 'MSPT (ms)' },
min: 0,
grid: { drawOnChartArea: false }
}
}
}
});
}
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
@ -56,65 +135,13 @@ document.addEventListener('DOMContentLoaded', function() {
}, 1000);
}
function initMemoryChart() {
const ctx = document.getElementById('memory-chart')?.getContext('2d');
if (!ctx) return null;
return new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['已使用', '未使用'],
datasets: [{
data: [0, 100],
backgroundColor: ['#4361ee', '#e9ecef']
}]
},
options: {
responsive: true,
maintainAspectRatio: false
}
});
}
function initPerformanceChart() {
const ctx = document.getElementById('performance-chart')?.getContext('2d');
if (!ctx) return null;
return new Chart(ctx, {
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: '毫秒'
}
}
}
}
});
}
async function loadAllData() {
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))
fetchData('/api/server-status', updateServerStatus)
]);
} catch (error) {
console.error('加载数据出错:', error);
@ -184,7 +211,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (elements.nonWhitelistCount) elements.nonWhitelistCount.textContent = data.non_whitelisted || 0;
}
function updateServerStatus(data, memoryChart, performanceChart) {
function updateServerStatus(data) {
// 更新内存信息
if (data.memory) {
const usedMB = Math.round(data.memory.used / (1024 * 1024));
@ -203,14 +230,59 @@ document.addEventListener('DOMContentLoaded', function() {
// 更新性能信息
if (data.server) {
if (performanceChart && data.server.recent_tick_samples_ms) {
performanceChart.data.datasets[0].data = data.server.recent_tick_samples_ms;
// 计算TPS (限制在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);
// 更新显示
if (elements.tpsValue) elements.tpsValue.textContent = tps;
if (elements.msptValue) elements.msptValue.textContent = mspt;
// 更新图表数据
if (performanceChart) {
// 更新历史数据
tpsHistory.shift();
tpsHistory.push(parseFloat(tps));
msptHistory.shift();
msptHistory.push(parseFloat(mspt));
// 只更新数据不重新创建图表
performanceChart.data.datasets[0].data = tpsHistory;
performanceChart.data.datasets[1].data = msptHistory;
performanceChart.update();
}
if (elements.avgTick) elements.avgTick.textContent = data.server.average_tick_time_ms?.toFixed(2) || '0';
}
// 更新性能图表
if (performanceChart && data.server) {
// 计算TPS和MSPT
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);
// 更新历史数据
tpsHistory.shift();
tpsHistory.push(parseFloat(tps));
msptHistory.shift();
msptHistory.push(parseFloat(mspt));
// 只更新数据(不重新创建图表)
performanceChart.data.datasets[0].data = tpsHistory;
performanceChart.data.datasets[1].data = msptHistory;
// 平滑更新
performanceChart.update('none');
// 更新数值显示
if (elements.tpsValue) elements.tpsValue.textContent = tps;
if (elements.msptValue) elements.msptValue.textContent = mspt;
// 根据数值设置颜色
setMetricColor(elements.tpsValue, tps, 15, 10);
setMetricColor(elements.msptValue, mspt, 50, 100, true);
}
if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0';
}
@ -237,4 +309,120 @@ document.addEventListener('DOMContentLoaded', function() {
}, 5000);
}, 10);
}
function initPerformanceChart() {
const perfCtx = document.getElementById('performance-chart')?.getContext('2d');
if (!perfCtx) return null;
return new Chart(perfCtx, {
type: 'line',
data: {
labels: Array(30).fill(''),
datasets: [
{
label: 'TPS',
data: tpsHistory,
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
tension: 0.3,
yAxisID: 'y'
},
{
label: 'MSPT',
data: msptHistory,
borderColor: '#FF5722',
backgroundColor: 'rgba(255, 87, 34, 0.1)',
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 5,
tension: 0.3,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 500
},
plugins: {
legend: {
position: 'top',
labels: {
boxWidth: 12,
padding: 20,
font: {
size: 12
}
}
},
tooltip: {
mode: 'index',
intersect: false,
bodySpacing: 8
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
maxRotation: 0,
autoSkip: true,
maxTicksLimit: 10
}
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'TPS',
font: {
weight: 'bold'
}
},
min: 0,
max: 20,
grid: {
color: 'rgba(0, 0, 0, 0.05)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'MSPT (ms)',
font: {
weight: 'bold'
}
},
min: 0,
grid: {
drawOnChartArea: false
}
}
}
}
});
}
function setMetricColor(element, value, warnThreshold, dangerThreshold, reverse = false) {
if (!element) return;
const numValue = parseFloat(value);
element.style.color =
reverse ?
(numValue > dangerThreshold ? '#FF5722' : numValue > warnThreshold ? '#FFC107' : '#4CAF50') :
(numValue < dangerThreshold ? '#FF5722' : numValue < warnThreshold ? '#FFC107' : '#4CAF50');
}
});