v1.0.0
This commit is contained in:
141
.gitignore
vendored
Normal file
141
.gitignore
vendored
Normal 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
324
app.js
Normal 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
107
index.html
Normal 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">×</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
248
package-lock.json
generated
Normal 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
5
package.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"socket.io": "^4.8.1"
|
||||
}
|
||||
}
|
70
server.js
Normal file
70
server.js
Normal 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
300
style.css
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user