首页接口重构

This commit is contained in:
2026-04-13 17:13:00 +08:00
parent 75de04086c
commit 91327f14fc
3 changed files with 1044 additions and 116 deletions

View File

@@ -5,93 +5,32 @@ import com.jeesite.modules.apps.Module.ContainerInfo;
import com.jeesite.modules.apps.Module.DockerResult; import com.jeesite.modules.apps.Module.DockerResult;
import com.jeesite.modules.biz.entity.MySftpAccounts; import com.jeesite.modules.biz.entity.MySftpAccounts;
import java.io.*; import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; import java.util.List;
/**
* Docker 容器管理工具
* <p>
* 通过 SSH 连接主机,基于 MySftpAccounts 凭证执行 docker 命令。
* 支持密码认证和私钥认证。
*/
public class DockerUtil { public class DockerUtil {
private static final int SSH_TIMEOUT = 30000; private static final int SSH_TIMEOUT = 30000;
public static DockerResult start(MySftpAccounts accounts, String containerId) { private static String runCommand(MySftpAccounts account, String cmd) {
return exec(accounts, "docker start " + containerId);
}
public static DockerResult stop(MySftpAccounts accounts, String containerId) {
return exec(accounts, "docker stop " + containerId);
}
public static DockerResult restart(MySftpAccounts accounts, String containerId) {
return exec(accounts, "docker restart " + containerId);
}
public static DockerResult getLogs(MySftpAccounts accounts, String containerId,
int tail, boolean timestamps) {
String cmd = "docker logs" + (timestamps ? " -t" : "")
+ " --tail " + tail + " " + containerId;
return exec(accounts, cmd);
}
public static DockerResult list(MySftpAccounts accounts, boolean all) {
return exec(accounts, all ? "docker ps -a" : "docker ps");
}
public static DockerResult inspect(MySftpAccounts accounts, String containerId) {
return exec(accounts, "docker inspect " + containerId);
}
public static List<ContainerInfo> listContainers(MySftpAccounts accounts, boolean all) {
List<ContainerInfo> list = new ArrayList<>();
String cmd = (all ? "docker ps -a" : "docker ps")
+ " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\"";
DockerResult r = exec(accounts, cmd);
if (!r.isSuccess()) {
return new ArrayList<>();
}
for (String line : r.getOutput().split("\n")) {
if (line.trim().isEmpty()) continue;
String[] p = line.split("\\|");
if (p.length < 2) continue;
ContainerInfo info = new ContainerInfo();
info.setContainerId(p[0].trim());
info.setImage(p[1].trim());
info.setCommand(p.length > 2 ? p[2].trim() : "");
info.setCreated(p.length > 3 ? p[3].trim() : "");
info.setStatus(p.length > 4 ? p[4].trim() : "");
info.setPorts(p.length > 5 ? p[5].trim() : "");
info.setNames(p.length > 6 ? p[6].trim() : "");
info.setAccountId(accounts.getAccountId());
list.add(info);
}
return list;
}
public static DockerResult exec(MySftpAccounts accounts, String command) {
JSch jsch = new JSch(); JSch jsch = new JSch();
Session session = null; Session session = null;
ChannelExec channel = null; ChannelExec channel = null;
try { try {
int port = accounts.getHostPort() != null ? accounts.getHostPort() : 22; int port = account.getHostPort() == null ? 22 : account.getHostPort();
session = jsch.getSession(accounts.getUsername(), accounts.getHostIp(), port); session = jsch.getSession(account.getUsername(), account.getHostIp(), port);
session.setTimeout(SSH_TIMEOUT); session.setTimeout(SSH_TIMEOUT);
if ("key".equalsIgnoreCase(accounts.getAuthType()) // 认证
&& accounts.getPrivateKey() != null && !accounts.getPrivateKey().isEmpty()) { if ("key".equalsIgnoreCase(account.getAuthType()) && account.getPrivateKey() != null) {
jsch.addIdentity("temp", jsch.addIdentity("temp", account.getPrivateKey().getBytes(StandardCharsets.UTF_8), null, null);
accounts.getPrivateKey().getBytes(StandardCharsets.UTF_8),
null, null);
} else if (accounts.getPassword() != null && !accounts.getPassword().isEmpty()) {
session.setPassword(accounts.getPassword());
} else { } else {
return DockerResult.fail("SSH 认证信息不完整:缺少密码或私钥"); session.setPassword(account.getPassword());
} }
Hashtable<String, String> config = new Hashtable<>(); Hashtable<String, String> config = new Hashtable<>();
@@ -99,46 +38,85 @@ public class DockerUtil {
session.setConfig(config); session.setConfig(config);
session.connect(SSH_TIMEOUT); session.connect(SSH_TIMEOUT);
// 执行命令
channel = (ChannelExec) session.openChannel("exec"); channel = (ChannelExec) session.openChannel("exec");
String finalCmd = (accounts.getRootPath() != null && !accounts.getRootPath().isEmpty()) String command = account.getRootPath() != null && !account.getRootPath().isEmpty()
? "cd " + accounts.getRootPath() + " && " + command ? "cd " + account.getRootPath() + " && " + cmd
: command; : cmd;
channel.setCommand(finalCmd);
channel.setInputStream(null);
channel.setCommand(command);
InputStream in = channel.getInputStream(); InputStream in = channel.getInputStream();
ByteArrayOutputStream errOut = new ByteArrayOutputStream();
channel.setExtOutputStream(errOut);
channel.connect(); channel.connect();
StringBuilder outBuilder = new StringBuilder(); // 读取输出
if (in != null) { ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[4096]; byte[] buf = new byte[4096];
int n; int len;
while ((n = in.read(buf)) != -1) { while ((len = in.read(buf)) != -1) {
outBuilder.append(new String(buf, 0, n, StandardCharsets.UTF_8)); out.write(buf, 0, len);
}
} }
String stdout = outBuilder.toString().trim(); return out.toString(StandardCharsets.UTF_8).trim();
String stderr = errOut.toString(StandardCharsets.UTF_8).trim();
int exitCode = channel.getExitStatus();
if (exitCode == 0) { } catch (Exception e) {
return DockerResult.ok(stdout); return null;
} else {
return DockerResult.fail("命令执行失败,退出码: " + exitCode,
stderr.isEmpty() ? stdout : stderr);
}
} catch (JSchException e) {
return DockerResult.fail("SSH 连接失败: " + e.getMessage());
} catch (IOException e) {
return DockerResult.fail("IO 异常: " + e.getMessage());
} finally { } finally {
if (channel != null && channel.isConnected()) channel.disconnect(); if (channel != null) channel.disconnect();
if (session != null && session.isConnected()) session.disconnect(); if (session != null) session.disconnect();
} }
} }
}
public static List<ContainerInfo> listContainers(MySftpAccounts accounts, boolean all) {
List<ContainerInfo> list = new ArrayList<>();
String cmd = (all ? "docker ps -a" : "docker ps")
+ " --format \"{{.ID}}|{{.Image}}|{{.Command}}|{{.CreatedAt}}|{{.Status}}|{{.Ports}}|{{.Names}}\"";
String output = runCommand(accounts, cmd);
if (output == null || output.isBlank()) return list;
for (String line : output.split("\\R")) {
if (line.isBlank()) continue;
String[] arr = line.split("\\|");
ContainerInfo info = new ContainerInfo();
info.setContainerId(arr.length > 0 ? arr[0].trim() : "");
info.setImage(arr.length > 1 ? arr[1].trim() : "");
info.setCommand(arr.length > 2 ? arr[2].trim() : "");
info.setCreated(arr.length > 3 ? arr[3].trim() : "");
info.setStatus(arr.length > 4 ? arr[4].trim() : "");
info.setPorts(arr.length > 5 ? arr[5].trim() : "");
info.setNames(arr.length > 6 ? arr[6].trim() : "");
info.setAccountId(accounts.getAccountId());
list.add(info);
}
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("执行失败");
}
public static DockerResult stop(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker stop " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败");
}
public static DockerResult restart(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker restart " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("执行失败");
}
public static DockerResult getLogs(MySftpAccounts accounts, String containerId, int tail, boolean timestamps) {
String cmd = "docker logs " + (timestamps ? "-t " : "") + "--tail " + tail + " " + containerId;
String res = runCommand(accounts, cmd);
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取日志失败");
}
public static DockerResult list(MySftpAccounts accounts, boolean all) {
String res = runCommand(accounts, all ? "docker ps -a" : "docker ps");
return res != null ? DockerResult.ok(res) : DockerResult.fail("获取列表失败");
}
public static DockerResult inspect(MySftpAccounts accounts, String containerId) {
String res = runCommand(accounts, "docker inspect " + containerId);
return res != null ? DockerResult.ok(res) : DockerResult.fail("查询详情失败");
}
}

View File

@@ -36,12 +36,12 @@ server:
#========= Database settings ==========# #========= Database settings ==========#
#======================================# #======================================#
jdbc: jdbc:
type: mysql type: mysql
driver: com.mysql.cj.jdbc.Driver driver: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.182:13306/system?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true url: jdbc:mysql://127.0.0.1:13306/my_app?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: dream username: root
password: info_dream password: root_dream
testSql: SELECT 1 testSql: SELECT 1
encrypt: encrypt:
username: false username: false
@@ -156,7 +156,7 @@ web:
addPathPatterns: > addPathPatterns: >
${frontPath}/** ${frontPath}/**
excludePathPatterns: ~ excludePathPatterns: ~
core: core:
enabled: true enabled: true
# 在线API文档 # 在线API文档

View File

@@ -1,21 +1,347 @@
<template> <template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="docker-page-wrapper"> <PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="docker-page-wrapper">
<div class="docker-page"> <div class="docker-page">
<div class="docker-layout">
<section class="docker-panel docker-hosts">
<div class="panel-title">
<span>主机列表</span>
<span class="panel-title__sub">{{ serverOptions.length }} </span>
</div>
<div class="docker-hosts__body">
<div class="host-list">
<button
v-for="item in serverOptions"
:key="item.accountId"
:class="['host-item', { 'host-item--active': selectedAccountId === item.accountId }]"
type="button"
@click="handleServerSelect(item)"
>
<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>
</div>
</section>
<section class="docker-main">
<section class="docker-panel docker-detail">
<div class="panel-title">
<span>主机详情</span>
<span class="panel-title__sub">{{ currentServer?.hostName || '-' }}</span>
</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>
</div>
<div class="hero-card__right">
<div class="hero-card__badge">{{ getServerStatus(currentServer).label }}</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>
</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>
</div>
</section>
<section v-loading="loading" class="docker-panel docker-table">
<div class="panel-title">
<span>容器列表</span>
<span class="panel-title__sub">{{ currentContainers.length }} 个容器</span>
</div>
<div class="docker-table__body">
<el-table :data="currentContainers" height="100%" :border="false" :show-header="true">
<el-table-column prop="created" label="创建时间" width="150" show-overflow-tooltip />
<el-table-column prop="containerId" label="容器ID" min-width="100" show-overflow-tooltip />
<el-table-column prop="names" label="容器名称" min-width="100" show-overflow-tooltip />
<el-table-column prop="image" label="镜像名称" min-width="100" show-overflow-tooltip />
<el-table-column prop="status" label="运行状态" width="135" show-overflow-tooltip>
<template #default="{ row }">
<el-tag :type="getStatusTagType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="ports" label="端口映射" min-width="145" show-overflow-tooltip />
<el-table-column prop="command" label="启动命令" min-width="125" show-overflow-tooltip />
<el-table-column label="操作" width="200" align="center">
<template #default="{ row }">
<div class="action-group">
<el-tooltip
v-if="isContainerStopped(row.status)"
content="启动"
placement="top"
:show-after="200"
>
<el-button
link
type="primary"
:icon="VideoPlay"
:loading="actionLoadingMap[row.containerId] === 'start'"
@click="handleDockerAction('start', row)"
/>
</el-tooltip>
<template v-else>
<el-tooltip content="重启" placement="top" :show-after="200">
<el-button
link
type="warning"
:icon="RefreshRight"
:loading="actionLoadingMap[row.containerId] === 'restart'"
@click="handleDockerAction('restart', row)"
/>
</el-tooltip>
<el-tooltip content="停止" placement="top" :show-after="200">
<el-button
link
type="danger"
:icon="SwitchButton"
:loading="actionLoadingMap[row.containerId] === 'stop'"
@click="handleDockerAction('stop', row)"
/>
</el-tooltip>
</template>
<el-tooltip content="详情" placement="top" :show-after="200">
<el-button
link
type="info"
:icon="View"
:loading="detailLoadingMap[row.containerId] === 'inspect'"
@click="handleDockerDetail('inspect', row)"
/>
</el-tooltip>
<el-tooltip content="日志" placement="top" :show-after="200">
<el-button
link
type="info"
:icon="Tickets"
:loading="detailLoadingMap[row.containerId] === 'logs'"
@click="handleDockerDetail('logs', row)"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
</section>
</section>
</div>
<el-dialog v-model="resultVisible" class="docker-info-dialog" :title="resultTitle" width="60%" destroy-on-close>
<div class="result-dialog">
<div class="result-dialog__header">
<div class="result-dialog__title">{{ currentContainer?.names || '-' }}</div>
<div class="result-dialog__header-divider"></div>
<div class="result-dialog__time">
<div class="result-dialog__time-left">
<span>主机{{ currentServer?.hostName || '-' }}</span>
<span>容器ID{{ currentContainer?.containerId || '-' }}</span>
</div>
<div class="result-dialog__time-right">{{ resultTitle }}</div>
</div>
</div>
<el-divider class="result-dialog__divider" />
<div class="result-dialog__content-panel">
<div class="result-dialog__content">{{ resultContent || '-' }}</div>
</div>
</div>
</el-dialog>
</div> </div>
</PageWrapper> </PageWrapper>
</template> </template>
<script lang="ts" setup name="Docker"> <script lang="ts" setup name="Docker">
import { computed, onMounted, reactive, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { PageWrapper } from '@jeesite/core/components/Page'; import { PageWrapper } from '@jeesite/core/components/Page';
import { RefreshRight, SwitchButton, Tickets, VideoPlay, View } from '@element-plus/icons-vue';
import {
ContainerInfo,
DockerResult,
ServerInfo,
myContainerInfo,
myDockerInspect,
myDockerLogs,
myDockerRestart,
myDockerStart,
myDockerStop,
myServerInfo,
} from '@jeesite/biz/api/biz/myDocker';
const loading = ref(false);
const serverOptions = ref<ServerInfo[]>([]);
const sourceData = ref<ContainerInfo[]>([]);
const selectedAccountId = ref('');
const resultVisible = ref(false);
const resultTitle = ref('');
const resultContent = ref('');
const currentContainer = ref<ContainerInfo | null>(null);
const actionLoadingMap = ref<Record<string, 'start' | 'stop' | 'restart' | ''>>({});
const detailLoadingMap = ref<Record<string, 'logs' | 'inspect' | ''>>({});
const containerQueryParams = reactive({
accountId: '',
});
const containerActionParams = reactive({
accountId: '',
containerId: '',
});
const currentServer = computed(
() => serverOptions.value.find((item) => item.accountId === selectedAccountId.value) || null,
);
const currentContainers = computed(() => sourceData.value);
const runningCount = computed(
() => sourceData.value.filter((item) => getStatusTagType(item.status) === 'success').length,
);
function handleServerSelect(item: ServerInfo) {
const accountId = item.accountId || '';
selectedAccountId.value = accountId;
containerQueryParams.accountId = 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';
if (value.includes('created') || value.includes('restart')) return 'warning';
if (value.includes('exit') || value.includes('stop')) return 'danger';
return 'info';
}
function isContainerStopped(status?: string) {
const value = String(status || '').toLowerCase();
return value.includes('exit') || value.includes('stop') || value.includes('dead');
}
async function getServerList() {
try {
serverOptions.value = (await myServerInfo()) || [];
if (!selectedAccountId.value && serverOptions.value.length) {
selectedAccountId.value = serverOptions.value[0].accountId || '';
if (selectedAccountId.value) {
containerQueryParams.accountId = selectedAccountId.value;
getContainerList();
}
}
} catch (error) {
serverOptions.value = [];
}
}
async function getContainerList() {
if (!containerQueryParams.accountId) {
sourceData.value = [];
return;
}
loading.value = true;
try {
sourceData.value = (await myContainerInfo(containerQueryParams)) || [];
} catch (error) {
sourceData.value = [];
} finally {
loading.value = false;
}
}
async function handleDockerAction(action: 'start' | 'stop' | 'restart', row: ContainerInfo) {
actionLoadingMap.value[row.containerId] = action;
try {
containerActionParams.accountId = row.accountId || '';
containerActionParams.containerId = row.containerId || '';
const api = action === 'start' ? myDockerStart : action === 'stop' ? myDockerStop : myDockerRestart;
const res = (await api(containerActionParams)) as DockerResult;
ElMessage.success(res?.message || '操作成功');
await getContainerList();
} catch (error) {
ElMessage.error('操作失败,请稍后重试');
} finally {
actionLoadingMap.value[row.containerId] = '';
}
}
async function handleDockerDetail(type: 'logs' | 'inspect', row: ContainerInfo) {
currentContainer.value = row;
resultTitle.value = type === 'logs' ? '容器日志' : '容器详情';
detailLoadingMap.value[row.containerId] = type;
try {
containerActionParams.accountId = row.accountId || '';
containerActionParams.containerId = row.containerId || '';
const api = type === 'logs' ? myDockerLogs : myDockerInspect;
const res = await api(containerActionParams);
resultContent.value = res?.output || res?.message || res?.error || '-';
} catch (error) {
resultContent.value = '获取内容失败,请稍后重试';
}
detailLoadingMap.value[row.containerId] = '';
resultVisible.value = true;
}
onMounted(async () => {
await getServerList();
});
</script> </script>
<style lang="less"> <style lang="less">
@dark-bg: #141414; @analysis-dark-bg: rgb(20, 20, 20);
@desktop-page-gap: 12px; @analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
@desktop-page-padding: 0;
@desktop-card-radius: 10px;
@desktop-card-border: 1px solid rgb(226 232 240);
@desktop-card-shadow: 0 1px 3px rgb(15 23 42 / 0.06);
@desktop-dark-border: rgb(51 65 85);
.docker-page-wrapper { .docker-page-wrapper {
display: flex; display: flex;
@@ -42,4 +368,628 @@
overflow: hidden; overflow: hidden;
background: transparent; background: transparent;
} }
.docker-layout {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
gap: 12px;
width: 100%;
height: 100%;
min-height: 0;
}
.docker-panel {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
border-radius: 10px;
background: rgb(255, 255, 255);
box-shadow: 0 8px 24px rgb(148 163 184 / 14%);
}
.panel-title {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 37px;
padding: 8px 16px;
border-bottom: 1px solid rgb(226 232 240);
color: rgb(51 65 85);
font-size: 14px;
font-weight: 500;
line-height: 20px;
&__sub {
font-size: 12px;
font-weight: 400;
color: rgb(100 116 139);
}
}
.docker-hosts {
&__body {
flex: 1;
min-height: 0;
padding: 10px;
overflow-y: auto;
}
}
.host-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.host-item {
padding: 12px;
border: 1px solid rgb(226 232 240);
border-radius: 12px;
background: radial-gradient(circle at top right, rgb(219 234 254 / 45%), transparent 38%), rgb(255, 255, 255);
text-align: left;
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease,
transform 0.2s ease,
background-color 0.2s ease;
&:hover {
transform: translateY(-2px);
border-color: rgb(147 197 253);
box-shadow: 0 12px 26px rgb(96 165 250 / 18%);
}
&--active {
border-color: rgb(191 219 254);
background: radial-gradient(circle at top right, rgb(191 219 254 / 65%), transparent 42%), rgb(239 246 255);
box-shadow: 0 14px 30px rgb(59 130 246 / 14%);
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
&__name {
font-size: 14px;
font-weight: 600;
color: rgb(30 41 59);
}
&__meta,
&__user,
&__footer {
margin-top: 6px;
font-size: 12px;
line-height: 16px;
color: rgb(100 116 139);
word-break: break-all;
}
&__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid rgb(226 232 240);
}
}
.docker-main {
display: grid;
grid-template-rows: 360px minmax(0, 1fr);
gap: 12px;
min-width: 0;
min-height: 0;
}
.docker-detail {
&__body {
flex: 1;
min-height: 0;
padding: 12px;
overflow: hidden;
}
}
.hero-card {
display: flex;
align-items: stretch;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border-radius: 12px;
background:
radial-gradient(circle at top left, rgb(191 219 254 / 75%), transparent 34%),
linear-gradient(135deg, rgb(248 250 252) 0%, rgb(255 255 255) 100%);
box-shadow: inset 0 0 0 1px rgb(226 232 240);
&__left {
min-width: 0;
}
&__title {
font-size: 18px;
font-weight: 700;
line-height: 24px;
color: rgb(30 41 59);
}
&__meta {
margin-top: 6px;
font-size: 12px;
line-height: 16px;
color: rgb(100 116 139);
}
&__badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 82px;
height: 32px;
padding: 0 12px;
border-radius: 999px;
background: rgb(30 64 175);
color: rgb(239 246 255);
font-size: 12px;
font-weight: 700;
letter-spacing: 1px;
}
}
.metric-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 12px;
}
.metric-card {
padding: 12px;
border-radius: 12px;
background: rgb(248 250 252);
box-shadow: inset 0 0 0 1px rgb(226 232 240);
&__label {
font-size: 12px;
line-height: 16px;
color: rgb(100 116 139);
}
&__value {
margin-top: 10px;
font-size: 24px;
line-height: 1;
font-weight: 700;
color: rgb(30 41 59);
}
&__progress {
height: 8px;
margin-top: 12px;
border-radius: 999px;
background: rgb(226 232 240);
overflow: hidden;
span {
display: block;
height: 100%;
border-radius: 999px;
}
}
&--cpu .metric-card__progress span {
background: linear-gradient(90deg, rgb(59 130 246), rgb(96 165 250));
}
&--memory .metric-card__progress span {
background: linear-gradient(90deg, rgb(16 185 129), rgb(52 211 153));
}
&--disk .metric-card__progress span {
background: linear-gradient(90deg, rgb(249 115 22), rgb(251 146 60));
}
}
.detail-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 12px;
}
.detail-card {
padding: 12px;
border-radius: 10px;
background: rgb(248 250 252);
box-shadow: inset 0 0 0 1px rgb(226 232 240);
&__label {
font-size: 12px;
line-height: 16px;
color: rgb(100 116 139);
}
&__value {
margin-top: 8px;
font-size: 14px;
line-height: 20px;
color: rgb(30 41 59);
word-break: break-all;
}
}
.docker-table {
&__body {
flex: 1;
min-height: 0;
padding: 8px 12px 0;
overflow: hidden;
}
.el-table {
width: 100%;
--el-table-border-color: transparent;
--el-table-header-bg-color: rgb(255, 255, 255);
--el-table-tr-bg-color: transparent;
--el-table-row-hover-bg-color: rgb(241 245 249);
--el-table-text-color: rgb(71 85 105);
--el-table-header-text-color: rgb(51 65 85);
background: transparent;
&::before,
&::after,
.el-table__inner-wrapper::before,
.el-table__border-left-patch {
display: none;
}
th.el-table__cell,
td.el-table__cell {
border-right: none;
border-left: none;
border-bottom: 1px solid rgb(226 232 240);
}
th.el-table__cell {
background: rgb(255, 255, 255);
}
td.el-table__cell,
th.el-table__cell {
padding: 8px 0;
}
.cell {
line-height: 20px;
}
}
}
.action-group {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 4px 6px;
.el-button {
padding: 0;
font-size: 16px;
}
}
.result-dialog {
display: flex;
flex-direction: column;
gap: 0;
&__header {
display: flex;
flex-direction: column;
gap: 8px;
}
&__title {
font-size: 18px;
font-weight: 600;
color: rgb(30 41 59);
text-align: center;
}
&__header-divider {
width: 100%;
height: 1px;
background: rgb(226 232 240);
}
&__time {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
color: rgb(100 116 139);
}
&__time-left {
display: flex;
align-items: center;
gap: 16px;
min-width: 0;
}
&__time-right {
flex-shrink: 0;
text-align: right;
}
&__divider {
margin: 12px 0;
}
&__content-panel {
padding: 4px;
border: none;
border-radius: 10px;
background: rgb(255, 255, 255);
box-sizing: border-box;
}
&__content {
min-height: 220px;
max-height: 320px;
padding: 14px 16px;
overflow-y: auto;
border: 1px solid rgb(191 219 254);
border-radius: 10px;
background: rgb(255 255 255);
color: rgb(51 65 85);
font-size: 14px;
line-height: 24px;
scrollbar-width: thin;
scrollbar-color: rgb(191 219 254) transparent;
white-space: pre-wrap;
word-break: break-all;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgb(191 219 254);
}
}
}
.docker-info-dialog {
.el-dialog {
background: rgb(255 255 255) !important;
--el-dialog-bg-color: rgb(255 255 255);
--el-bg-color: rgb(255 255 255);
box-shadow: 0 12px 32px rgb(15 23 42 / 18%);
}
.el-dialog__header {
margin-right: 0;
padding: 20px 24px 16px;
border-bottom: 1px solid rgb(226 232 240) !important;
background: rgb(255 255 255) !important;
}
.el-dialog__body {
padding: 20px 24px 24px;
background: rgb(255 255 255) !important;
}
}
html[data-theme='dark'] {
.docker-panel {
background: @analysis-dark-bg;
box-shadow: @analysis-dark-shadow;
}
.panel-title {
color: rgb(203 213 225);
border-bottom-color: rgb(51 65 85);
&__sub {
color: rgb(148 163 184);
}
}
.host-item {
border-color: rgb(51 65 85);
background: radial-gradient(circle at top right, rgb(37 99 235 / 22%), transparent 36%), rgb(20, 20, 20);
&:hover {
border-color: rgb(96 165 250);
box-shadow: 0 10px 24px rgb(37 99 235 / 18%);
}
&--active {
border-color: rgb(59 130 246 / 45%);
background: radial-gradient(circle at top right, rgb(37 99 235 / 30%), transparent 40%), rgb(20, 20, 20);
box-shadow: 0 12px 26px rgb(37 99 235 / 16%);
}
&__name {
color: rgb(226 232 240);
}
&__meta,
&__user,
&__footer {
color: rgb(148 163 184);
}
&__footer {
border-top-color: rgb(51 65 85);
}
}
.hero-card {
background:
radial-gradient(circle at top left, rgb(37 99 235 / 26%), transparent 34%),
linear-gradient(135deg, rgb(20, 20, 20) 0%, rgb(28 28 28) 100%);
box-shadow: inset 0 0 0 1px rgb(51 65 85);
&__title {
color: rgb(226 232 240);
}
&__meta {
color: rgb(148 163 184);
}
&__badge {
background: rgb(30 64 175 / 80%);
color: rgb(219 234 254);
}
}
.metric-card,
.detail-card {
background: rgb(20, 20, 20);
box-shadow: inset 0 0 0 1px rgb(51 65 85);
}
.metric-card {
&__label {
color: rgb(148 163 184);
}
&__value {
color: rgb(226 232 240);
}
&__progress {
background: rgb(51 65 85);
}
}
.detail-card {
&__label {
color: rgb(148 163 184);
}
&__value {
color: rgb(226 232 240);
}
}
.docker-table {
.el-table {
--el-table-header-bg-color: rgb(20, 20, 20);
--el-table-row-hover-bg-color: rgb(30 41 59);
--el-table-text-color: rgb(148 163 184);
--el-table-header-text-color: rgb(226 232 240);
background: transparent;
th.el-table__cell,
td.el-table__cell {
border-bottom-color: rgb(51 65 85);
}
th.el-table__cell {
background: rgb(20, 20, 20);
}
}
}
.result-dialog {
&__title {
color: rgb(226 232 240);
}
&__time {
color: rgb(148 163 184);
}
&__header-divider {
background: rgb(51 65 85);
}
&__content-panel {
background: rgb(20, 20, 20);
}
&__content {
background: rgb(20, 20, 20);
color: rgb(203 213 225);
border-color: rgb(51 65 85);
scrollbar-color: rgb(71 85 105) transparent;
}
&__content::-webkit-scrollbar-thumb {
background: rgb(71 85 105);
}
}
}
html[data-theme='dark'] .docker-info-dialog {
--el-bg-color: rgb(20, 20, 20);
--el-dialog-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
.el-dialog {
background: rgb(20, 20, 20) !important;
--el-dialog-bg-color: rgb(20, 20, 20);
--el-bg-color: rgb(20, 20, 20);
--el-fill-color-blank: rgb(20, 20, 20);
box-shadow: 0 14px 36px rgb(0 0 0 / 42%);
}
.el-dialog__wrapper,
.el-overlay-dialog,
.el-dialog__content {
background: rgb(20, 20, 20) !important;
}
.el-dialog__header {
border-bottom: 1px solid rgb(51 65 85) !important;
background: rgb(20, 20, 20) !important;
}
.el-dialog__title {
color: rgb(226 232 240);
}
.el-dialog__body {
background: rgb(20, 20, 20) !important;
}
.el-dialog__headerbtn .el-dialog__close {
color: rgb(148 163 184);
}
}
@media (max-width: 1100px) {
.docker-layout {
grid-template-columns: 1fr;
}
.docker-main {
grid-template-rows: 400px minmax(0, 1fr);
}
}
@media (max-width: 768px) {
.detail-grid,
.metric-grid {
grid-template-columns: 1fr;
}
}
</style> </style>