This commit is contained in:
2025-07-20 15:32:17 -07:00
commit 9014dc87f1
7 changed files with 1195 additions and 0 deletions

141
.gitignore vendored Normal file
View File

@ -0,0 +1,141 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pdcc.login.token.txt
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Sveltekit cache directory
.svelte-kit/
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Firebase cache directory
.firebase/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

324
app.js Normal file
View File

@ -0,0 +1,324 @@
// 服务器管理相关代码
const servers = JSON.parse(localStorage.getItem("servers")) || [];
let currentServer = null;
let currentSocket = null;
// DOM 元素
const serverList = document.getElementById("server-list");
const noServerMessage = document.getElementById("no-server-message");
const serverStatus = document.getElementById("server-status");
const currentServerName = document.getElementById("current-server-name");
// 初始化UI
function initUI() {
updateServerList();
if (servers.length > 0) {
noServerMessage.style.display = "none";
serverStatus.style.display = "block";
connectToServer(servers[0]);
} else {
noServerMessage.style.display = "block";
serverStatus.style.display = "none";
}
}
// 更新服务器下拉列表
function updateServerList() {
serverList.innerHTML = "";
if (servers.length === 0) {
const emptyItem = document.createElement("div");
emptyItem.className = "dropdown-item";
emptyItem.textContent = "无服务器";
emptyItem.style.color = "#888";
emptyItem.style.cursor = "default";
serverList.appendChild(emptyItem);
return;
}
servers.forEach((server, index) => {
const serverItem = document.createElement("div");
serverItem.className = "dropdown-item";
serverItem.innerHTML = `
<div>${server.name}</div>
<small>${server.address}</small>
<div class="server-actions">
<button class="delete-server" data-index="${index}">×</button>
</div>
`;
serverItem.addEventListener("click", (e) => {
if (!e.target.classList.contains("delete-server")) {
connectToServer(server);
document.querySelector(".dropdown").classList.remove("open");
}
});
// 添加删除按钮事件
const deleteBtn = serverItem.querySelector(".delete-server");
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
deleteServer(index);
});
serverList.appendChild(serverItem);
});
}
// 连接到指定服务器
function connectToServer(server) {
// 断开现有连接
if (currentSocket) {
currentSocket.disconnect();
}
currentServer = server;
currentServerName.textContent = server.name;
// 显示连接状态
document.getElementById("cpu-details").textContent = "连接中...";
// 建立新连接
currentSocket = io(server.address, {
transports: ["websocket"],
reconnectionAttempts: 3,
timeout: 5000,
});
// 设置事件监听器
setupSocketListeners(currentSocket);
}
// 设置Socket.IO监听器
function setupSocketListeners(socket) {
socket.on("connect", () => {
console.log("已连接到服务器:", currentServer.name);
});
socket.on("connect_error", (error) => {
console.error("连接错误:", error);
document.getElementById("cpu-details").textContent = "连接失败";
});
socket.on("disconnect", (reason) => {
console.log("断开连接:", reason);
});
socket.on("status", (data) => {
updateMemory({
totalmem: data.totalmem,
freemem: data.freemem,
});
updateCpuLoad(data.perCpuUsage);
updateLoadAvg(data.loadavg);
if (data.uptime) {
updateUptime(data.uptime);
}
const cpuCoreUsages = data.perCpuUsage || [];
updateCpuCores(cpuCoreUsages);
});
}
// 添加新服务器
function addServer(name, address) {
// 验证地址格式
if (!address.startsWith("http://") && !address.startsWith("https://")) {
address = "http://" + address;
}
const newServer = {
name,
address: address.replace(/\/$/, ""), // 移除末尾的斜杠
};
servers.push(newServer);
localStorage.setItem("servers", JSON.stringify(servers));
if (servers.length === 1) {
// 如果是第一个服务器,自动连接
noServerMessage.style.display = "none";
serverStatus.style.display = "block";
connectToServer(newServer);
}
updateServerList();
}
// 删除服务器
function deleteServer(index) {
if (currentServer && servers[index].name === currentServer.name) {
if (currentSocket) {
currentSocket.disconnect();
currentSocket = null;
}
if (servers.length === 1) {
noServerMessage.style.display = "block";
serverStatus.style.display = "none";
}
}
servers.splice(index, 1);
localStorage.setItem("servers", JSON.stringify(servers));
updateServerList();
// 如果还有服务器,连接到第一个
if (servers.length > 0 && !currentServer) {
connectToServer(servers[0]);
}
}
// 初始化模态框
const openModal = document.getElementById("openModal");
const closeModal = document.getElementById("closeModal");
const modal = document.getElementById("addServerModal");
const addServerBtn = document.getElementById("add-server-btn");
openModal.addEventListener("click", () => {
modal.style.display = "flex";
document.getElementById("server-name").focus();
});
closeModal.addEventListener("click", () => {
modal.style.display = "none";
});
// 点击背景区域关闭弹窗
window.addEventListener("click", (e) => {
if (e.target === modal) {
modal.style.display = "none";
}
});
// 添加服务器按钮事件
addServerBtn.addEventListener("click", () => {
const name = document.getElementById("server-name").value.trim();
const address = document.getElementById("server-address").value.trim();
if (name && address) {
addServer(name, address);
modal.style.display = "none";
document.getElementById("server-name").value = "";
document.getElementById("server-address").value = "";
} else {
alert("请填写服务器名称和地址");
}
});
// 回车键提交表单
document.getElementById("server-address").addEventListener("keypress", (e) => {
if (e.key === "Enter") {
addServerBtn.click();
}
});
// 下拉菜单切换
const toggleBtn = document.querySelector(".dropdown-toggle");
const dropdown = document.querySelector(".dropdown");
toggleBtn.addEventListener("click", () => {
dropdown.classList.toggle("open");
});
// 点击外部关闭菜单
document.addEventListener("click", (e) => {
if (!dropdown.contains(e.target)) {
dropdown.classList.remove("open");
}
});
// 工具函数
function bytesToGB(bytes) {
return (bytes / 1024 / 1024 / 1024).toFixed(2);
}
function updateMemory(mem) {
const totalGB = bytesToGB(mem.totalmem);
const freeGB = bytesToGB(mem.freemem);
const usedGB = (totalGB - freeGB).toFixed(2);
const usedPercent = ((usedGB / totalGB) * 100).toFixed(1);
document.getElementById("mem-used").textContent = `${usedPercent}%`;
document.getElementById("mem-progress").style.width = usedPercent + "%";
document.getElementById(
"mem-details"
).textContent = `${usedGB} GB / ${totalGB} GB`;
}
function updateCpuLoad(perCpuUsage) {
if (!perCpuUsage || perCpuUsage.length === 0) {
document.getElementById("cpu-load").textContent = `N/A`;
document.getElementById("cpu-progress").style.width = "0%";
document.getElementById("cpu-details").textContent = `无 CPU 使用数据`;
return;
}
const sum = perCpuUsage.reduce((a, b) => a + b, 0);
const avg = (sum / perCpuUsage.length).toFixed(1);
document.getElementById("cpu-load").textContent = `${avg}%`;
document.getElementById("cpu-progress").style.width = avg + "%";
document.getElementById("cpu-details").textContent = `每核CPU平均使用率`;
}
function updateLoadAvg(loadavg) {
if (!loadavg || loadavg.length < 3) return;
document.getElementById(
"loadavg-values"
).textContent = `1min: ${loadavg[0].toFixed(2)}, 5min: ${loadavg[1].toFixed(
2
)}, 15min: ${loadavg[2].toFixed(2)}`;
}
function updateUptime(uptime) {
if (!uptime) return;
const days = Math.floor(uptime / 86400);
const hours = Math.floor((uptime % 86400) / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
let uptimeStr = "";
if (days > 0) uptimeStr += `${days}`;
if (hours > 0 || days > 0) uptimeStr += `${hours}小时 `;
if (minutes > 0 || hours > 0 || days > 0) uptimeStr += `${minutes}`;
uptimeStr += `${seconds}`;
document.getElementById("uptime-value").textContent = uptimeStr;
}
function updateCpuCores(usages) {
const container = document.getElementById("cpu-cores");
container.innerHTML = "";
usages.forEach((percent, idx) => {
const coreDiv = document.createElement("div");
coreDiv.className = "cpu-core";
// 创建进度条
const progressBar = document.createElement("div");
progressBar.className = "progress-bar";
progressBar.style.height = "6px";
progressBar.style.marginTop = "6px";
progressBar.style.backgroundColor = "#2a2a2a";
const progress = document.createElement("div");
progress.className = "progress";
progress.style.height = "100%";
progress.style.width = `${percent}%`;
progress.style.backgroundColor =
percent > 80 ? "#e91e63" : percent > 60 ? "#ff9800" : "#0bc";
progressBar.appendChild(progress);
coreDiv.textContent = `核心 ${idx + 1}: ${percent}%`;
coreDiv.appendChild(progressBar);
container.appendChild(coreDiv);
});
}
// 初始化应用
initUI();

107
index.html Normal file
View File

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>服务器状态面板</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="header-bar">
<h1>服务器状态面板</h1>
<div class="dropdown">
<button class="dropdown-toggle">
<span id="current-server-name">选择服务器</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2" fill="none" />
</svg>
</button>
<div class="dropdown-menu" id="server-list">
<!-- 服务器列表将在这里动态生成 -->
</div>
</div>
</div>
<div class="container">
<!-- 默认显示无服务器状态 -->
<div class="no-server" id="no-server-message">
<h2>无服务器</h2>
<p>请先添加服务器</p>
</div>
<!-- 服务器状态卡片 (初始隐藏) -->
<div id="server-status" style="display: none;">
<!-- 第一行卡片 -->
<div class="card-row">
<!-- 内存卡片 -->
<div class="card">
<h2>内存使用率</h2>
<div class="value" id="mem-used">0%</div>
<div class="progress-bar">
<div id="mem-progress" class="progress"></div>
</div>
<div class="small-text" id="mem-details">0 / 0 GB</div>
</div>
<!-- CPU 总负载卡片 -->
<div class="card">
<h2>CPU 总负载</h2>
<div class="value" id="cpu-load">0%</div>
<div class="progress-bar">
<div id="cpu-progress" class="progress"></div>
</div>
<div class="small-text" id="cpu-details">每核CPU平均使用率</div>
</div>
</div>
<!-- 第二行卡片 -->
<div class="card-row">
<!-- 系统负载平均卡片 -->
<div class="card">
<h2>系统负载平均</h2>
<div class="small-text" id="loadavg-values">1min: 0, 5min: 0, 15min: 0</div>
</div>
<!-- 系统运行时间卡片 -->
<div class="card">
<h2>系统运行时间</h2>
<div class="small-text" id="uptime-value">加载中...</div>
</div>
</div>
<!-- 每核CPU使用率卡片 -->
<div class="card cpu-cores-card">
<h2>每核CPU使用率</h2>
<div id="cpu-cores" class="cpu-grid">
<!-- 动态生成 -->
</div>
</div>
</div>
</div>
<!-- 悬浮按钮 -->
<div class="floating-btn" id="openModal">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#00bcd4" />
<line x1="12" y1="6" x2="12" y2="18" stroke="white" stroke-width="2" />
<line x1="6" y1="12" x2="18" y2="12" stroke="white" stroke-width="2" />
</svg>
</div>
<!-- 添加服务器弹窗 -->
<div class="modal" id="addServerModal">
<div class="modal-content">
<span class="close-modal" id="closeModal">&times;</span>
<h2>添加服务器</h2>
<input type="text" id="server-name" placeholder="服务器名称" />
<input type="text" id="server-address" placeholder="IP地址或域名 (如: http://localhost:3001)" />
<button class="submit-btn" id="add-server-btn">添加</button>
</div>
</div>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script src="app.js"></script>
</body>
</html>

248
package-lock.json generated Normal file
View File

@ -0,0 +1,248 @@
{
"name": "PDCC",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"socket.io": "^4.8.1"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/undici-types": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
"license": "MIT"
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"socket.io": "^4.8.1"
}
}

70
server.js Normal file
View File

@ -0,0 +1,70 @@
const { Server } = require("socket.io");
const http = require("http");
const os = require("os");
const server = http.createServer();
const io = new Server(server, {
cors: {
origin: "*", // 允许所有来源访问
},
});
function calculateCpuUsage(startCpus, endCpus) {
const usagePercentages = [];
for (let i = 0; i < startCpus.length; i++) {
const start = startCpus[i].times;
const end = endCpus[i].times;
const idleDiff = end.idle - start.idle;
const totalStart = Object.values(start).reduce((a, b) => a + b, 0);
const totalEnd = Object.values(end).reduce((a, b) => a + b, 0);
const totalDiff = totalEnd - totalStart;
const usage = totalDiff === 0 ? 0 : (1 - idleDiff / totalDiff) * 100;
usagePercentages.push(+usage.toFixed(1));
}
return usagePercentages;
}
function getSystemInfo(prevCpuInfo) {
const currentCpuInfo = os.cpus();
const perCpuUsage = calculateCpuUsage(prevCpuInfo, currentCpuInfo);
return {
totalmem: os.totalmem(),
freemem: os.freemem(),
loadavg: os.loadavg(),
uptime: os.uptime(),
perCpuUsage,
currentCpuInfo,
};
}
let prevCpuInfo = os.cpus();
io.on("connection", (socket) => {
console.log("客户端连接:", socket.id);
// 先发一次状态信息
const initInfo = getSystemInfo(prevCpuInfo);
prevCpuInfo = initInfo.currentCpuInfo;
socket.emit("status", initInfo);
// 定时发送状态信息
const interval = setInterval(() => {
const info = getSystemInfo(prevCpuInfo);
prevCpuInfo = info.currentCpuInfo;
socket.emit("status", info);
}, 3000);
socket.on("disconnect", () => {
clearInterval(interval);
console.log("客户端断开:", socket.id);
});
});
server.listen(3001, () => {
console.log("Socket.IO 服务运行在 http://localhost:3001");
});

300
style.css Normal file
View File

@ -0,0 +1,300 @@
body {
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: #121212;
color: #ddd;
margin: 0;
padding: 20px;
min-height: 100vh;
}
h1 {
margin-bottom: 20px;
color: #0bc;
font-weight: 600;
font-size: 1.8rem;
}
.container {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.card-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.card {
background: #1e1e1e;
border: 1px solid #0bc;
border-radius: 8px;
padding: 20px;
flex: 1;
min-width: 280px;
display: flex;
flex-direction: column;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
.card:hover {
border-color: #33d6ff;
}
.card h2 {
margin: 0 0 12px 0;
color: #0bc;
font-weight: 600;
font-size: 1.25rem;
}
.value {
font-size: 2rem;
font-weight: 700;
margin-bottom: 12px;
color: #e91e63;
}
.progress-bar {
width: 100%;
height: 18px;
background: #2a2a2a;
border-radius: 8px;
overflow: hidden;
margin-bottom: 10px;
}
.progress {
height: 100%;
background: #0bc;
width: 0;
transition: width 0.6s ease;
}
.small-text {
font-size: 0.85rem;
color: #888;
margin-top: auto;
}
.cpu-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
margin-top: 10px;
}
.cpu-core {
background: #2a2a2a;
border-radius: 6px;
padding: 12px 8px;
text-align: center;
font-weight: 600;
color: #0cf;
font-size: 0.9rem;
}
.cpu-cores-card {
min-height: 180px;
}
.no-server {
text-align: center;
padding: 3rem;
color: #888;
background: #1e1e1e;
border-radius: 8px;
border: 1px dashed #444;
margin-top: 2rem;
}
.no-server h2 {
margin-bottom: 1rem;
color: #666;
}
.floating-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 56px;
height: 56px;
background-color: transparent;
cursor: pointer;
z-index: 1000;
transition: transform 0.2s ease;
}
.floating-btn:hover {
transform: scale(1.1);
}
.modal {
display: none;
position: fixed;
z-index: 999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #1e1e1e;
padding: 25px 30px;
border-radius: 10px;
width: 350px;
max-width: 90%;
box-shadow: 0 0 15px #00bcd4aa;
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
}
.modal-content h2 {
margin: 0 0 10px 0;
color: #00bcd4;
text-align: center;
}
.modal-content input {
padding: 12px;
border: 1px solid #444;
background: #2a2a2a;
color: #fff;
border-radius: 5px;
font-size: 1rem;
width: 100%;
box-sizing: border-box;
}
.submit-btn {
background-color: #00bcd4;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 10px;
}
.submit-btn:hover {
background-color: #00acc1;
}
.close-modal {
position: absolute;
top: 15px;
right: 20px;
color: #aaa;
font-size: 24px;
font-weight: bold;
cursor: pointer;
}
.close-modal:hover {
color: #fff;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 900px;
margin: 0 auto 20px;
padding: 0 10px;
flex-wrap: wrap;
gap: 15px;
}
.dropdown {
position: relative;
}
.dropdown-toggle {
background-color: #1e1e1e;
color: #00bcd4;
border: 1px solid #00bcd4;
padding: 10px 15px;
font-size: 1rem;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
min-width: 180px;
justify-content: space-between;
}
.dropdown-toggle:hover {
background-color: #00bcd410;
}
.dropdown-menu {
display: none;
position: absolute;
right: 0;
top: 110%;
background: #1e1e1e;
border: 1px solid #00bcd4;
border-radius: 6px;
padding: 8px 0;
z-index: 100;
min-width: 200px;
box-shadow: 0 0 10px #00bcd4aa;
max-height: 300px;
overflow-y: auto;
}
.dropdown-item {
padding: 10px 15px;
color: #e0e0e0;
cursor: pointer;
transition: background-color 0.2s;
}
.dropdown-item:hover {
background-color: #00bcd420;
}
.dropdown.open .dropdown-menu {
display: block;
}
/* 响应式调整 */
@media (max-width: 768px) {
.header-bar {
flex-direction: column;
align-items: flex-start;
}
.dropdown {
width: 100%;
}
.dropdown-toggle {
width: 100%;
}
.dropdown-menu {
width: 100%;
}
.card {
min-width: 100%;
}
}
.card {
margin: 8px;
}