From 0d4bd10afece2bd33432586d10125673b70ae852 Mon Sep 17 00:00:00 2001 From: dqzboy Date: Thu, 8 May 2025 23:40:00 +0800 Subject: [PATCH] fix: Resolve frontend redirection issues when using images, and improve the backend management interface. --- .gitignore | 1 + hubcmdui/documentation/1743543400369.json | 2 +- hubcmdui/package.json | 1 + hubcmdui/services/systemService.js | 149 +++++-- hubcmdui/users.json | 4 +- hubcmdui/web/admin.html | 321 +++++++++++++- hubcmdui/web/index.html | 252 +++++++---- hubcmdui/web/js/app.js | 74 ++-- hubcmdui/web/js/auth.js | 8 +- hubcmdui/web/js/core.js | 283 +++++++------ hubcmdui/web/js/documentManager.js | 288 ++++--------- hubcmdui/web/js/menuManager.js | 41 +- hubcmdui/web/js/nav-menu.js | 26 +- hubcmdui/web/js/networkTest.js | 159 +++++-- hubcmdui/web/js/systemStatus.js | 493 ++++++++++++---------- hubcmdui/web/js/userCenter.js | 18 +- hubcmdui/web/style.css | 155 +++---- 17 files changed, 1346 insertions(+), 929 deletions(-) diff --git a/.gitignore b/.gitignore index 5443028..cf8d3dd 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +node_modules \ No newline at end of file diff --git a/hubcmdui/documentation/1743543400369.json b/hubcmdui/documentation/1743543400369.json index b049198..8aa066f 100644 --- a/hubcmdui/documentation/1743543400369.json +++ b/hubcmdui/documentation/1743543400369.json @@ -3,5 +3,5 @@ "content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```", "published": true, "createdAt": "2025-04-01T21:36:40.369Z", - "updatedAt": "2025-04-01T21:36:41.977Z" + "updatedAt": "2025-05-08T15:16:47.900Z" } \ No newline at end of file diff --git a/hubcmdui/package.json b/hubcmdui/package.json index c604261..1d17120 100644 --- a/hubcmdui/package.json +++ b/hubcmdui/package.json @@ -30,6 +30,7 @@ "node-cache": "^5.1.2", "p-limit": "^4.0.0", "session-file-store": "^1.5.0", + "systeminformation": "^5.25.11", "validator": "^13.7.0", "ws": "^8.8.1" }, diff --git a/hubcmdui/services/systemService.js b/hubcmdui/services/systemService.js index 1fd65d3..53a8212 100644 --- a/hubcmdui/services/systemService.js +++ b/hubcmdui/services/systemService.js @@ -1,55 +1,118 @@ /** * 系统服务模块 - 处理系统级信息获取 + * 使用 systeminformation 库来提供跨平台的系统数据 */ -const { exec } = require('child_process'); -const os = require('os'); +const si = require('systeminformation'); const logger = require('../logger'); +const os = require('os'); // os模块仍可用于某些特定情况或日志记录 -// 获取磁盘空间信息 -async function getDiskSpace() { - try { - // 根据操作系统不同有不同的命令 - const isWindows = os.platform() === 'win32'; - - if (isWindows) { - // Windows实现(需要更复杂的逻辑) - return { - diskSpace: '未实现', - usagePercent: 0 - }; - } else { - // Linux/Mac实现 - const diskInfo = await execPromise('df -h | grep -E "/$|/home" | head -1'); - const diskParts = diskInfo.split(/\s+/); - - if (diskParts.length >= 5) { - return { - diskSpace: `${diskParts[2]}/${diskParts[1]}`, - usagePercent: parseInt(diskParts[4].replace('%', '')) - }; - } else { - throw new Error('磁盘信息格式不正确'); - } +// Helper function to format bytes into a more readable format +function formatBytes(bytes, decimals = 2) { + if (bytes === null || bytes === undefined || isNaN(bytes) || bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + try { + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; + } catch (e) { + return 'N/A'; // In case of Math.log error with very small numbers etc. } - } catch (error) { - logger.error('获取磁盘空间失败:', error); - throw error; - } } -// 辅助函数: 执行命令 -function execPromise(command) { - return new Promise((resolve, reject) => { - exec(command, (error, stdout, stderr) => { - if (error) { - reject(error); - return; - } - resolve(stdout.trim()); - }); - }); +// 获取核心系统资源信息 (CPU, Memory, Disk) +async function getSystemResources() { + try { + logger.info('Fetching system resources using systeminformation...'); + + // 并行获取数据以提高效率 + const [cpuInfo, memInfo, fsInfo, cpuLoadInfo, osInfo] = await Promise.all([ + si.cpu(), // For CPU model, cores, speed + si.mem(), // For memory details + si.fsSize(), // For filesystem details + si.currentLoad(), // For current CPU load percentage and per-core load + si.osInfo() // For OS type, mainly for specific disk selection if needed + ]); + + // --- CPU 信息处理 --- + let cpuUsage = parseFloat(cpuLoadInfo.currentLoad.toFixed(1)); + // Fallback if currentLoad is not a number (very unlikely with systeminformation) + if (isNaN(cpuUsage) && Array.isArray(cpuLoadInfo.cpus) && cpuLoadInfo.cpus.length > 0) { + // Calculate average from per-core loads if overall isn't good + const totalLoad = cpuLoadInfo.cpus.reduce((acc, core) => acc + core.load, 0); + cpuUsage = parseFloat((totalLoad / cpuLoadInfo.cpus.length).toFixed(1)); + } + if (isNaN(cpuUsage)) cpuUsage = null; // Final fallback to null if still NaN + + const cpuData = { + cores: cpuInfo.cores, + physicalCores: cpuInfo.physicalCores, + model: cpuInfo.manufacturer + ' ' + cpuInfo.brand, + speed: cpuInfo.speed, // in GHz + usage: cpuUsage, // Overall CPU usage percentage + loadAvg: osInfo.platform !== 'win32' ? os.loadavg().map(load => parseFloat(load.toFixed(1))) : null // os.loadavg() is not for Windows + }; + + // --- 内存信息处理 --- + // systeminformation already provides these in bytes + const memData = { + total: memInfo.total, + free: memInfo.free, // Truly free + used: memInfo.used, // total - free (includes buff/cache on Linux, PhysMem used on macOS) + active: memInfo.active, // More representative of app-used memory + available: memInfo.available, // Memory available to applications (often free + reclaimable buff/cache) + wired: memInfo.wired, // macOS specific: memory that cannot be paged out + // compressed: memInfo.compressed, // macOS specific, if systeminformation lib provides it directly + buffcache: memInfo.buffcache // Linux specific: buffer and cache + }; + + // --- 磁盘信息处理 --- + // Find the primary disk (e.g., mounted on '/' for Linux/macOS, or C: for Windows) + let mainDiskInfo = null; + if (osInfo.platform === 'win32') { + mainDiskInfo = fsInfo.find(d => d.fs.startsWith('C:')); + } else { + mainDiskInfo = fsInfo.find(d => d.mount === '/'); + } + if (!mainDiskInfo && fsInfo.length > 0) { + // Fallback to the first disk if the standard one isn't found + mainDiskInfo = fsInfo[0]; + } + + const diskData = mainDiskInfo ? { + mount: mainDiskInfo.mount, + size: formatBytes(mainDiskInfo.size), + used: formatBytes(mainDiskInfo.used), + available: formatBytes(mainDiskInfo.available), // systeminformation provides 'available' + percent: mainDiskInfo.use !== null && mainDiskInfo.use !== undefined ? mainDiskInfo.use.toFixed(0) + '%' : 'N/A' + } : { + mount: 'N/A', + size: 'N/A', + used: 'N/A', + available: 'N/A', + percent: 'N/A' + }; + + const resources = { + osType: osInfo.platform, // e.g., 'darwin', 'linux', 'win32' + osDistro: osInfo.distro, + cpu: cpuData, + memory: memData, + disk: diskData + }; + logger.info('Successfully fetched system resources:', /* JSON.stringify(resources, null, 2) */ resources.osType); + return resources; + + } catch (error) { + logger.error('获取系统资源失败 (services/systemService.js):', error); + // Return a structured error object or rethrow, + // so the API route can send an appropriate HTTP error + throw new Error(`Failed to get system resources: ${error.message}`); + } } module.exports = { - getDiskSpace + getSystemResources + // getDiskSpace, if it was previously exported and used by another route, can be kept + // or removed if getSystemResources now covers all disk info needs for /api/system-resources }; diff --git a/hubcmdui/users.json b/hubcmdui/users.json index f082238..2dbc255 100644 --- a/hubcmdui/users.json +++ b/hubcmdui/users.json @@ -3,8 +3,8 @@ { "username": "root", "password": "$2b$10$lh1kqJtq3shL2BhMD1LbVOThGeAlPXsDgME/he4ZyDMRupVtj0Hl.", - "loginCount": 1, - "lastLogin": "2025-04-01T22:45:10.184Z" + "loginCount": 0, + "lastLogin": "2025-05-08T14:59:22.166Z" } ] } \ No newline at end of file diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index c4b8ed6..370a24e 100644 --- a/hubcmdui/web/admin.html +++ b/hubcmdui/web/admin.html @@ -1518,6 +1518,272 @@ color: #333; margin: 0; } + + /* 菜单管理 - 新增行样式 */ + #new-menu-item-row input[type="text"], + #new-menu-item-row select { + /* 使用 Bootstrap 的 form-control-sm 已经减小了 padding 和 font-size */ + margin-bottom: 0; /* 移除下方外边距,使其在表格行内更紧凑 */ + } + + #new-menu-item-row .form-control-sm { + padding: 0.3rem 0.6rem; /* 微调内边距 */ + font-size: 0.875rem; /* 统一字体大小 */ + } + + #new-menu-item-row .form-select-sm { + padding: 0.3rem 1.5rem 0.3rem 0.6rem; /* 微调 select 内边距以适应箭头 */ + font-size: 0.875rem; + } + + .action-buttons-new-menu .btn { + margin-right: 5px; /* 按钮间距 */ + min-width: 80px; /* 给按钮一个最小宽度 */ + } + + .action-buttons-new-menu .btn:last-child { + margin-right: 0; + } + + .action-buttons-new-menu .btn i { + margin-right: 4px; /* 图标和文字间距 */ + } + + /* 使新行单元格垂直居中 */ + #new-menu-item-row td { + vertical-align: middle; + } + + /* 网络测试页面美化 */ + #network-test { + /* 可以考虑将整个 #network-test 作为一个卡片,如果它还没有被 .content-section 样式化为卡片的话 */ + } + + /* 直接覆盖#testResults.loading的样式,防止旋转 */ + #network-test #testResults.loading { + animation: none !important; + border: none !important; + display: block !important; + position: relative !important; + text-align: center !important; + padding: 15px !important; + color: var(--text-secondary, #6c757d) !important; + font-size: 1rem !important; + width: auto !important; + height: auto !important; + } + + /* 使用:before添加文本内容 */ + #network-test #testResults.loading:before { + content: "测试进行中..." !important; + animation: none !important; + border: none !important; + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + color: var(--text-light, #e9ecef) !important; + font-size: 1rem !important; + font-weight: 500 !important; + padding: 0 !important; + background-color: transparent !important; + z-index: 10 !important; + text-align: center !important; + } + + #network-test .form-row { + display: flex; + flex-wrap: wrap; /* 允许换行到小屏幕 */ + gap: 1.5rem; /* 各个元素之间的间距 */ + margin-bottom: 1.5rem; + align-items: flex-end; /* 使得按钮和选择框底部对齐 */ + } + + #network-test .form-group { + flex: 1; /* 让表单组占据可用空间 */ + min-width: 250px; /* 避免在小屏幕上过于挤压 */ + background-color: transparent; /* 移除之前 #network-test .input-group 的背景色 */ + padding: 0; /* 移除内边距 */ + box-shadow: none; /* 移除阴影 */ + border-radius: 0; + } + + #network-test label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-primary); + font-weight: 500; + font-size: 0.95rem; + } + + #network-test .form-control, + #network-test .form-select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background-color: var(--input-bg, var(--container-bg)); /* 允许自定义输入背景或使用容器背景 */ + color: var(--text-primary); + font-size: 0.95rem; + transition: all var(--transition-fast); + box-shadow: var(--shadow-sm); + } + + #network-test .form-control:focus, + #network-test .form-select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(var(--primary-rgb, 61, 124, 244), 0.25); /* 使用RGB变量 */ + outline: none; + } + + #network-test .test-controls-container { + /* 这个容器包裹选择器和按钮 */ + display: flex; + flex-wrap: wrap; /* 允许换行 */ + gap: 1rem; /* 控件之间的间距 */ + align-items: flex-end; /* 使得按钮和选择框底部对齐 */ + margin-bottom: 1.5rem; + } + + #network-test .test-controls-container .form-group { + flex-grow: 1; + flex-basis: 200px; /* 给每个控件一个基础宽度 */ + } + + #network-test .start-test-btn { + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-weight: 500; + white-space: nowrap; /* 防止按钮文字换行 */ + height: fit-content; /* 与调整后的 select 高度匹配 */ + flex-shrink: 0; /* 防止按钮在 flex 布局中被压缩 */ + } + + #network-test .start-test-btn i { + margin-right: 0.5rem; + } + + .results-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding: 0.5rem 0; + border-bottom: 1px solid var(--border-light); + } + + .results-header h3 { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-light, #e9ecef); /* Changed to light color for dark background */ + margin: 0; + } + + #clearTestResultsBtn { + font-size: 0.85rem; + padding: 0.3rem 0.8rem; + } + + #testResultsContainer { + background-color: var(--container-bg-dark, #1e2532); /* 使用变量或默认暗色 */ + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + margin-top: 1rem; + padding: 0; /* 移除外层padding,让头部和内容区自己控制 */ + position: relative; /* Add this to be a positioning context for absolute children if needed by headers etc. */ + } + + #testResults { + padding: 1rem; + color: var(--text-light, #e9ecef); + font-family: var(--font-mono, 'JetBrains Mono', monospace); + font-size: 0.9rem; + white-space: pre-wrap; + word-break: break-all; + min-height: 200px; + max-height: 400px; + overflow-y: auto; + background-color: transparent; + border-radius: 0 0 var(--radius-md) var(--radius-md); + box-shadow: none; + border: none; + margin-top: 0; + position: relative; + } + + /* Use higher specificity and !important to force override */ + #network-test #testResults.loading::before { + content: "测试进行中..."; /* Force text content */ + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* Center the text block */ + color: var(--text-secondary, #6c757d); + font-size: 1rem; + font-weight: 500; + padding: 0.75rem 1.25rem; + background-color: transparent !important; + border: none !important; /* 移除边框 */ + border-radius: var(--radius-md, 0.375rem); + z-index: 10; + text-align: center; + /* Force removal of spinner styles */ + animation: none !important; + border-top-color: transparent !important; /* Force override any spinner head */ + width: auto !important; /* Force width */ + height: auto !important; /* Force height */ + box-sizing: border-box; /* Ensure padding is included correctly */ + } + + /* Ensure keyframes for the spinner are removed or commented out */ + /* @keyframes testResultSpinner { to { transform: rotate(360deg); } } */ + + /* Style for the
 tag inside #testResults for consistent output formatting */
+        #testResults pre {
+            margin: 0; /* Remove default pre margin */
+            font-family: inherit; /* Inherit from #testResults (monospace) */
+            font-size: inherit;   /* Inherit from #testResults */
+            color: inherit;       /* Inherit from #testResults */
+            white-space: pre-wrap; /* Ensure wrapping */
+            word-break: break-all;   /* Ensure long lines break */
+            background-color: transparent; /* Ensure no pre background interferes */
+            padding: 0; /* No extra padding for pre, parent div handles it */
+        }
+
+        #testResults pre.text-danger {
+            color: var(--danger-color, #dc3545); /* Ensure error text is colored */
+        }
+
+        /* Placeholder text styling (if using 

) */ + #testResults .text-muted.text-center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: calc(100% - 2rem); /* Consider padding of #testResults */ + } + + /* 保持容器监控页面已停止容器列表的表头在鼠标悬停时背景色不变 */ + #docker-monitoring .container-table th:hover { + background-color: var(--primary-color); + color: white; + } + + /* SweetAlert2 弹窗内容文本居中 */ + #swal2-html-container, /* 使用 ID 提高特异性 */ + .swal2-html-container { /* 保留类选择器作为备用 */ + text-align: center !important; + } + + #swal2-html-container p, + .swal2-html-container p { + text-align: center !important; + } + + #swal2-html-container div, + .swal2-html-container div { + text-align: center !important; + } @@ -1636,11 +1902,6 @@

-
- -
@@ -1650,6 +1911,12 @@
+ +
+ +