首页接口重构
This commit is contained in:
@@ -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("查询详情失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,9 +39,9 @@ server:
|
|||||||
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
|
||||||
|
|||||||
@@ -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">
|
|
||||||
import { PageWrapper } from '@jeesite/core/components/Page';
|
|
||||||
|
|
||||||
|
<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 { 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user