首页接口重构

This commit is contained in:
2026-04-13 19:43:12 +08:00
parent c66d754c77
commit 86ea470208
6 changed files with 312 additions and 89 deletions

View File

@@ -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; // 使用率
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -18,6 +18,13 @@ export interface ServerInfo extends BasicModel<ServerInfo> {
containerId: string; // 容器ID
}
export interface SystemInfo extends BasicModel<SystemInfo> {
cpu: string; // CPU使用率
memory: string; // 内存
disk: string; // 磁盘
}
export interface ContainerInfo extends BasicModel<ContainerInfo> {
image: string; // 镜像名称
command: string; // 启动命令
@@ -42,6 +49,9 @@ defHttp.get<ServerInfo[]>({ url: adminPath + '/docker/myServer/listAll' });
export const myContainerInfo = (params?: ContainerInfo | any) =>
defHttp.get<ContainerInfo[]>({ url: adminPath + '/docker/myContainer/listAll', params });
export const mySystemInfo = (params?: ContainerInfo | any) =>
defHttp.get<SystemInfo>({ url: adminPath + '/docker/myContainer/systemInfo', params });
export const myDockerStop = (params?: ContainerInfo | any) =>
defHttp.get<DockerResult>({ url: adminPath + '/docker/myContainer/stop', params });

View File

@@ -18,15 +18,11 @@
>
<div class="host-item__header">
<div class="host-item__name">{{ item.hostName }}</div>
<el-tag size="small" :type="getServerStatus(item).type">
{{ getServerStatus(item).label }}
</el-tag>
</div>
<div class="host-item__meta">{{ item.hostIp }}:{{ item.hostPort }}</div>
<div class="host-item__user">{{ item.username }}</div>
<div class="host-item__footer">
<span>账号 {{ item.accountId }}</span>
<span>{{ getServerStatus(item).label }}</span>
</div>
</button>
</div>
@@ -41,55 +37,60 @@
</div>
<div v-if="currentServer" class="docker-detail__body">
<div class="hero-card">
<div class="hero-card__left">
<div class="hero-card__title">{{ currentServer.hostName }}</div>
<div class="hero-card__meta">{{ currentServer.hostIp }}:{{ currentServer.hostPort }}</div>
<div class="hero-card__meta">账号{{ currentServer.username }}</div>
<transition name="detail-fade" mode="out-in">
<div v-if="detailLoading" key="detail-skeleton" class="detail-skeleton">
<div class="hero-card hero-card--skeleton">
<div class="hero-card__skeleton-title"></div>
<div class="hero-card__skeleton-meta"></div>
<div class="hero-card__skeleton-meta hero-card__skeleton-meta--short"></div>
</div>
<div class="metric-grid">
<div v-for="item in 3" :key="item" class="metric-card metric-card--skeleton">
<div class="metric-card__label">&nbsp;</div>
<div class="metric-card__skeleton-value"></div>
<div class="metric-card__skeleton-bar"></div>
</div>
</div>
</div>
<div class="hero-card__right">
<div class="hero-card__badge">{{ getServerStatus(currentServer).label }}</div>
</div>
</div>
<div v-else key="detail-content">
<div class="hero-card">
<div class="hero-card__left">
<div class="hero-card__title">{{ currentServer.hostName }}</div>
<div class="hero-card__meta">{{ currentServer.hostIp }}:{{ currentServer.hostPort }}</div>
<div class="hero-card__meta">账号{{ currentServer.username }}</div>
</div>
<div class="hero-card__right">
<div :class="['hero-card__badge', { 'hero-card__badge--offline': !systemOnline }]">
{{ systemOnline ? '在线' : '离线' }}
</div>
</div>
</div>
<div class="metric-grid">
<div class="metric-card metric-card--cpu">
<div class="metric-card__label">CPU</div>
<div class="metric-card__value">--</div>
<div class="metric-card__progress">
<span :style="{ width: '0%' }"></span>
<div class="metric-grid">
<div class="metric-card metric-card--cpu">
<div class="metric-card__label">CPU</div>
<div class="metric-card__value">{{ formatPercentText(systemInfo?.cpu) }}</div>
<div class="metric-card__progress">
<span :style="{ width: normalizePercent(systemInfo?.cpu) }"></span>
</div>
</div>
<div class="metric-card metric-card--memory">
<div class="metric-card__label">内存</div>
<div class="metric-card__value">{{ formatPercentText(systemInfo?.memory) }}</div>
<div class="metric-card__progress">
<span :style="{ width: normalizePercent(systemInfo?.memory) }"></span>
</div>
</div>
<div class="metric-card metric-card--disk">
<div class="metric-card__label">磁盘</div>
<div class="metric-card__value">{{ formatPercentText(systemInfo?.disk) }}</div>
<div class="metric-card__progress">
<span :style="{ width: normalizePercent(systemInfo?.disk) }"></span>
</div>
</div>
</div>
</div>
<div class="metric-card metric-card--memory">
<div class="metric-card__label">内存</div>
<div class="metric-card__value">--</div>
<div class="metric-card__progress">
<span :style="{ width: '0%' }"></span>
</div>
</div>
<div class="metric-card metric-card--disk">
<div class="metric-card__label">磁盘</div>
<div class="metric-card__value">--</div>
<div class="metric-card__progress">
<span :style="{ width: '0%' }"></span>
</div>
</div>
</div>
<div class="detail-grid">
<div class="detail-card">
<div class="detail-card__label">账号标识</div>
<div class="detail-card__value">{{ currentServer.accountId }}</div>
</div>
<div class="detail-card">
<div class="detail-card__label">容器总数</div>
<div class="detail-card__value">{{ currentContainers.length }}</div>
</div>
<div class="detail-card">
<div class="detail-card__label">运行中</div>
<div class="detail-card__value">{{ runningCount }}</div>
</div>
</div>
</transition>
</div>
</section>
@@ -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<ServerInfo[]>([]);
const sourceData = ref<ContainerInfo[]>([]);
const selectedAccountId = ref('');
const systemInfo = ref<SystemInfo | null>(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);
}
}