From 86ea4702089978cc52ed2cfbf15f60774aedb618 Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Mon, 13 Apr 2026 19:43:12 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A6=96=E9=A1=B5=E6=8E=A5=E5=8F=A3=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/apps/Module/SystemInfo.java | 13 + .../web/docker/myContainerController.java | 13 + .../apps/web/docker/myServerController.java | 2 + .../com/jeesite/modules/utils/DockerUtil.java | 46 ++- web-vue/packages/biz/api/biz/myDocker.ts | 10 + .../packages/biz/views/biz/myDocker/index.vue | 317 ++++++++++++++---- 6 files changed, 312 insertions(+), 89 deletions(-) create mode 100644 web-api/src/main/java/com/jeesite/modules/apps/Module/SystemInfo.java diff --git a/web-api/src/main/java/com/jeesite/modules/apps/Module/SystemInfo.java b/web-api/src/main/java/com/jeesite/modules/apps/Module/SystemInfo.java new file mode 100644 index 0000000..7ebd142 --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/apps/Module/SystemInfo.java @@ -0,0 +1,13 @@ +package com.jeesite.modules.apps.Module; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class SystemInfo implements Serializable { + private String cpu; // CPU使用率 + private String memory; // 内存 + private String disk; // 磁盘 + private String usage; // 使用率 +} diff --git a/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myContainerController.java b/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myContainerController.java index fb72ac3..9446709 100644 --- a/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myContainerController.java +++ b/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myContainerController.java @@ -2,6 +2,7 @@ package com.jeesite.modules.apps.web.docker; import com.jeesite.modules.apps.Module.ContainerInfo; import com.jeesite.modules.apps.Module.DockerResult; +import com.jeesite.modules.apps.Module.SystemInfo; import com.jeesite.modules.biz.dao.MySftpAccountsDao; import com.jeesite.modules.biz.entity.MySftpAccounts; import com.jeesite.modules.utils.DockerUtil; @@ -90,4 +91,16 @@ public class myContainerController { MySftpAccounts accounts = mySftpAccountsDao.get(mySftpAccounts); return DockerUtil.listContainers(accounts, true); } + + /** + * 获取CPU列表 + */ + @RequestMapping(value = "systemInfo") + @ResponseBody + public SystemInfo systemInfo(ContainerInfo containerInfo){ + MySftpAccounts mySftpAccounts = new MySftpAccounts(); + mySftpAccounts.setAccountId(containerInfo.getAccountId()); + MySftpAccounts accounts = mySftpAccountsDao.get(mySftpAccounts); + return DockerUtil.systemInfo(accounts); + } } diff --git a/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myServerController.java b/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myServerController.java index 311cb98..0c51fdd 100644 --- a/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myServerController.java +++ b/web-api/src/main/java/com/jeesite/modules/apps/web/docker/myServerController.java @@ -1,8 +1,10 @@ package com.jeesite.modules.apps.web.docker; import com.jeesite.modules.apps.Module.ServerInfo; +import com.jeesite.modules.apps.Module.SystemInfo; import com.jeesite.modules.biz.dao.MySftpAccountsDao; import com.jeesite.modules.biz.entity.MySftpAccounts; +import com.jeesite.modules.utils.DockerUtil; import jakarta.annotation.Resource; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java index 5c9df61..ac8f554 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/DockerUtil.java @@ -3,6 +3,7 @@ package com.jeesite.modules.utils; import com.jcraft.jsch.*; import com.jeesite.modules.apps.Module.ContainerInfo; import com.jeesite.modules.apps.Module.DockerResult; +import com.jeesite.modules.apps.Module.SystemInfo; import com.jeesite.modules.biz.entity.MySftpAccounts; import java.io.ByteArrayOutputStream; @@ -14,7 +15,7 @@ import java.util.List; public class DockerUtil { - private static final int SSH_TIMEOUT = 30000; + private static final int SSH_TIMEOUT = 3000; private static String runCommand(MySftpAccounts account, String cmd) { JSch jsch = new JSch(); @@ -89,6 +90,7 @@ public class DockerUtil { } return list; } + public static DockerResult start(MySftpAccounts accounts, String containerId) { String res = runCommand(accounts, "docker start " + containerId); return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败"); @@ -120,36 +122,30 @@ public class DockerUtil { return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败"); } - /** - * 获取CPU使用率 - */ + // 获取 CPU 使用率 public static String getCpuUsage(MySftpAccounts accounts) { - try { - return runCommand(accounts, "top -bn1 | grep Cpu | awk '{print 100 - $8}'"); - } catch (Exception e) { - return null; - } + // 1秒采样,输出纯数字百分比 + return runCommand(accounts, + "top -bn1 | grep 'Cpu(s)' | sed -n '1p' | awk '{printf \"%.1f\", 100 - $8}'"); } - /** - * 获取内存使用情况 - */ + // 获取内存使用率 public static String getMemoryUsage(MySftpAccounts accounts) { - try { - return runCommand(accounts, "free -m | grep Mem | awk '{print $3\"MB / \"$2\"MB\"}'"); - } catch (Exception e) { - return null; - } + return runCommand(accounts, + "free | grep Mem | awk '{printf \"%.1f\", $3/$2*100}'"); } - /** - * 获取磁盘使用情况 - */ + // 获取磁盘使用率 public static String getDiskUsage(MySftpAccounts accounts) { - try { - return runCommand(accounts, "df -h / | grep / | awk '{print $3\" / \"$2}\" \"$5}'"); - } catch (Exception e) { - return null; - } + return runCommand(accounts, + "df -h / | grep / | awk '{gsub(/%/,\"\"); print $5}'"); + } + + public static SystemInfo systemInfo(MySftpAccounts accounts) { + SystemInfo systemInfo = new SystemInfo(); + systemInfo.setCpu(getCpuUsage(accounts)); + systemInfo.setMemory(getMemoryUsage(accounts)); + systemInfo.setDisk(getDiskUsage(accounts)); + return systemInfo; } } \ No newline at end of file diff --git a/web-vue/packages/biz/api/biz/myDocker.ts b/web-vue/packages/biz/api/biz/myDocker.ts index 8082cc3..5c3403b 100644 --- a/web-vue/packages/biz/api/biz/myDocker.ts +++ b/web-vue/packages/biz/api/biz/myDocker.ts @@ -18,6 +18,13 @@ export interface ServerInfo extends BasicModel { containerId: string; // 容器ID } +export interface SystemInfo extends BasicModel { + cpu: string; // CPU使用率 + memory: string; // 内存 + disk: string; // 磁盘 +} + + export interface ContainerInfo extends BasicModel { image: string; // 镜像名称 command: string; // 启动命令 @@ -42,6 +49,9 @@ defHttp.get({ url: adminPath + '/docker/myServer/listAll' }); export const myContainerInfo = (params?: ContainerInfo | any) => defHttp.get({ url: adminPath + '/docker/myContainer/listAll', params }); +export const mySystemInfo = (params?: ContainerInfo | any) => + defHttp.get({ url: adminPath + '/docker/myContainer/systemInfo', params }); + export const myDockerStop = (params?: ContainerInfo | any) => defHttp.get({ url: adminPath + '/docker/myContainer/stop', params }); diff --git a/web-vue/packages/biz/views/biz/myDocker/index.vue b/web-vue/packages/biz/views/biz/myDocker/index.vue index 287e106..12bfd7b 100644 --- a/web-vue/packages/biz/views/biz/myDocker/index.vue +++ b/web-vue/packages/biz/views/biz/myDocker/index.vue @@ -18,15 +18,11 @@ >
{{ item.hostName }}
- - {{ getServerStatus(item).label }} -
{{ item.hostIp }}:{{ item.hostPort }}
{{ item.username }}
@@ -41,55 +37,60 @@
-
-
-
{{ currentServer.hostName }}
-
{{ currentServer.hostIp }}:{{ currentServer.hostPort }}
-
账号:{{ currentServer.username }}
+ +
+
+
+
+
+
+
+
+
 
+
+
+
+
-
-
{{ getServerStatus(currentServer).label }}
-
-
+
+
+
+
{{ currentServer.hostName }}
+
{{ currentServer.hostIp }}:{{ currentServer.hostPort }}
+
账号:{{ currentServer.username }}
+
+
+
+ {{ systemOnline ? '在线' : '离线' }} +
+
+
-
-
-
CPU
-
--
-
- +
+
+
CPU
+
{{ formatPercentText(systemInfo?.cpu) }}
+
+ +
+
+
+
内存
+
{{ formatPercentText(systemInfo?.memory) }}
+
+ +
+
+
+
磁盘
+
{{ formatPercentText(systemInfo?.disk) }}
+
+ +
+
-
-
内存
-
--
-
- -
-
-
-
磁盘
-
--
-
- -
-
-
- -
-
-
账号标识
-
{{ currentServer.accountId }}
-
-
-
容器总数
-
{{ currentContainers.length }}
-
-
-
运行中
-
{{ runningCount }}
-
-
+
@@ -228,6 +229,7 @@ ContainerInfo, DockerResult, ServerInfo, + SystemInfo, myContainerInfo, myDockerInspect, myDockerLogs, @@ -235,12 +237,16 @@ myDockerStart, myDockerStop, myServerInfo, + mySystemInfo, } from '@jeesite/biz/api/biz/myDocker'; const loading = ref(false); + const metricLoading = ref(false); + const detailLoading = ref(false); const serverOptions = ref([]); const sourceData = ref([]); const selectedAccountId = ref(''); + const systemInfo = ref(null); const resultVisible = ref(false); const resultTitle = ref(''); const resultContent = ref(''); @@ -259,25 +265,22 @@ () => serverOptions.value.find((item) => item.accountId === selectedAccountId.value) || null, ); const currentContainers = computed(() => sourceData.value); + const systemOnline = computed(() => + Boolean(systemInfo.value && (systemInfo.value.cpu || systemInfo.value.memory || systemInfo.value.disk)), + ); const runningCount = computed( () => sourceData.value.filter((item) => getStatusTagType(item.status) === 'success').length, ); function handleServerSelect(item: ServerInfo) { const accountId = item.accountId || ''; + detailLoading.value = true; selectedAccountId.value = accountId; containerQueryParams.accountId = accountId; + getSystemInfo(accountId); getContainerList(); } - function getServerStatus(item?: ServerInfo | null) { - const online = Boolean(item?.accountId); - return { - label: online ? '在线' : '离线', - type: online ? 'success' : 'danger', - } as const; - } - function getStatusTagType(status?: string) { const value = String(status || '').toLowerCase(); if (value.includes('up') || value.includes('running')) return 'success'; @@ -286,6 +289,22 @@ return 'info'; } + function normalizePercent(value?: string) { + const numeric = Number( + String(value || '') + .replace('%', '') + .trim(), + ); + if (Number.isNaN(numeric)) return '0%'; + return `${Math.max(0, Math.min(100, numeric))}%`; + } + + function formatPercentText(value?: string) { + const text = String(value || '').trim(); + if (!text) return '0'; + return text.includes('%') ? text : `${text}%`; + } + function isContainerStopped(status?: string) { const value = String(status || '').toLowerCase(); return value.includes('exit') || value.includes('stop') || value.includes('dead'); @@ -297,7 +316,9 @@ if (!selectedAccountId.value && serverOptions.value.length) { selectedAccountId.value = serverOptions.value[0].accountId || ''; if (selectedAccountId.value) { + detailLoading.value = true; containerQueryParams.accountId = selectedAccountId.value; + getSystemInfo(selectedAccountId.value); getContainerList(); } } @@ -306,6 +327,30 @@ } } + async function getSystemInfo(accountId = containerQueryParams.accountId) { + if (!accountId) { + return; + } + metricLoading.value = true; + detailLoading.value = true; + try { + const params = { + accountId, + }; + const res = await mySystemInfo(params); + if (accountId === selectedAccountId.value) { + systemInfo.value = res; + } + } catch (error) { + console.log(error); + } finally { + if (accountId === selectedAccountId.value) { + metricLoading.value = false; + detailLoading.value = false; + } + } + } + async function getContainerList() { if (!containerQueryParams.accountId) { sourceData.value = []; @@ -313,7 +358,10 @@ } loading.value = true; try { - sourceData.value = (await myContainerInfo(containerQueryParams)) || []; + const params = { + accountId: containerQueryParams.accountId, + }; + sourceData.value = (await myContainerInfo(params)) || []; } catch (error) { sourceData.value = []; } finally { @@ -503,7 +551,7 @@ .docker-main { display: grid; - grid-template-rows: 360px minmax(0, 1fr); + grid-template-rows: 270px minmax(0, 1fr); gap: 12px; min-width: 0; min-height: 0; @@ -556,11 +604,16 @@ height: 32px; padding: 0 12px; border-radius: 999px; - background: rgb(30 64 175); - color: rgb(239 246 255); + background: rgb(220 252 231); + color: rgb(21 128 61); font-size: 12px; font-weight: 700; letter-spacing: 1px; + + &--offline { + background: rgb(254 226 226); + color: rgb(185 28 28); + } } } @@ -591,6 +644,29 @@ color: rgb(30 41 59); } + &__skeleton { + margin-top: 10px; + } + + &__skeleton-value, + &__skeleton-bar { + border-radius: 999px; + background: linear-gradient(90deg, rgb(226 232 240) 25%, rgb(241 245 249) 50%, rgb(226 232 240) 75%); + background-size: 200% 100%; + animation: metric-skeleton 1.2s ease-in-out infinite; + } + + &__skeleton-value { + width: 56px; + height: 24px; + } + + &__skeleton-bar { + width: 100%; + height: 8px; + margin-top: 12px; + } + &__progress { height: 8px; margin-top: 12px; @@ -625,6 +701,25 @@ margin-top: 12px; } + .metric-fade-enter-active, + .metric-fade-leave-active { + transition: opacity 0.2s ease; + } + + .metric-fade-enter-from, + .metric-fade-leave-to { + opacity: 0; + } + + @keyframes metric-skeleton { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } + } + .detail-card { padding: 12px; border-radius: 10px; @@ -643,7 +738,82 @@ line-height: 20px; color: rgb(30 41 59); word-break: break-all; + + &--status { + display: inline-flex; + align-items: center; + gap: 8px; + } } + + &__dot { + width: 8px; + height: 8px; + flex-shrink: 0; + border-radius: 999px; + background: rgb(34 197 94); + + &--offline { + background: rgb(239 68 68); + } + } + } + + .detail-skeleton { + display: flex; + flex-direction: column; + gap: 12px; + } + + .hero-card--skeleton { + display: flex; + flex-direction: column; + gap: 10px; + } + + .hero-card__skeleton-title, + .hero-card__skeleton-meta, + .detail-card__skeleton-label, + .detail-card__skeleton-value { + border-radius: 999px; + background: linear-gradient(90deg, rgb(226 232 240) 25%, rgb(241 245 249) 50%, rgb(226 232 240) 75%); + background-size: 200% 100%; + animation: metric-skeleton 1.2s ease-in-out infinite; + } + + .hero-card__skeleton-title { + width: 160px; + height: 22px; + } + + .hero-card__skeleton-meta { + width: 220px; + height: 14px; + + &--short { + width: 120px; + } + } + + .detail-card__skeleton-label { + width: 80px; + height: 12px; + } + + .detail-card__skeleton-value { + width: 70%; + height: 18px; + margin-top: 10px; + } + + .detail-fade-enter-active, + .detail-fade-leave-active { + transition: opacity 0.25s ease; + } + + .detail-fade-enter-from, + .detail-fade-leave-to { + opacity: 0; } .docker-table { @@ -875,8 +1045,13 @@ } &__badge { - background: rgb(30 64 175 / 80%); - color: rgb(219 234 254); + background: rgb(20 83 45 / 72%); + color: rgb(187 247 208); + + &--offline { + background: rgb(127 29 29 / 72%); + color: rgb(254 202 202); + } } } @@ -895,6 +1070,12 @@ color: rgb(226 232 240); } + &__skeleton-value, + &__skeleton-bar { + background: linear-gradient(90deg, rgb(51 65 85) 25%, rgb(71 85 105) 50%, rgb(51 65 85) 75%); + background-size: 200% 100%; + } + &__progress { background: rgb(51 65 85); } @@ -910,6 +1091,14 @@ } } + .hero-card__skeleton-title, + .hero-card__skeleton-meta, + .detail-card__skeleton-label, + .detail-card__skeleton-value { + background: linear-gradient(90deg, rgb(51 65 85) 25%, rgb(71 85 105) 50%, rgb(51 65 85) 75%); + background-size: 200% 100%; + } + .docker-table { .el-table { --el-table-header-bg-color: rgb(20, 20, 20); @@ -1041,7 +1230,7 @@ } .docker-main { - grid-template-rows: 400px minmax(0, 1fr); + grid-template-rows: 300px minmax(0, 1fr); } }