首页接口重构

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

@@ -1,21 +1,347 @@
<template>
<PageWrapper :contentFullHeight="true" :dense="true" title="false" contentClass="docker-page-wrapper">
<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>
</PageWrapper>
</template>
<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>
<style lang="less">
@dark-bg: #141414;
@desktop-page-gap: 12px;
@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);
@analysis-dark-bg: rgb(20, 20, 20);
@analysis-dark-shadow: 0 10px 24px rgb(0 0 0 / 24%);
.docker-page-wrapper {
display: flex;
@@ -42,4 +368,628 @@
overflow: hidden;
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>