Files
ApiServer-Web-admin_dashboa…/src/views/acs/nodes/containDetail.vue
T
lin b3ed406f84
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 1m9s
fix: 提交修改
2026-04-15 16:02:36 +08:00

1879 lines
47 KiB
Vue

<template>
<div class="container-detail-container">
<div class="page-header">
<div class="left">
<h2 class="title">容器详情</h2>
<el-tag
:type="getStatusType(containerInfo.state)"
effect="dark"
class="status-tag"
>
<span class="status-dot" :class="getStatusDotClass(containerInfo.state)"></span>
{{ getStatusText(containerInfo.state) }}
</el-tag>
</div>
<div class="actions">
<el-button @click="goBack" :icon="Back">返回</el-button>
<el-button type="primary" @click="refreshData" :icon="Refresh">刷新数据</el-button>
</div>
</div>
<!-- 容器信息卡片 -->
<div class="container-info">
<div class="info-card main-info">
<div class="card-title">
<el-icon><Box /></el-icon>
<span>基本信息</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">容器ID</div>
<div class="info-value">{{ containerInfo.container_id || '未知' }}</div>
</div>
<!-- <div class="info-item">
<div class="info-label">容器名称</div>
<div class="info-value">{{ containerInfo.name || '未知' }}</div>
</div> -->
<div class="info-item">
<div class="info-label">用户ID</div>
<div class="info-value">{{ containerInfo.user_id || '未知' }}</div>
</div>
<div class="info-item">
<div class="info-label">价格</div>
<div class="info-value highlight">{{ containerInfo.pay || '0' }} </div>
</div>
</div>
</div>
<div class="info-card server-info">
<div class="card-title">
<el-icon><Connection /></el-icon>
<span>服务器信息</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">服务器IP</div>
<div class="info-value">{{ containerInfo.server_ip || '未知' }}</div>
</div>
<div class="info-item">
<div class="info-label">创建时间</div>
<div class="info-value">{{ containerInfo.created_at || '未知' }}</div>
</div>
<div class="info-item">
<div class="info-label">到期时间</div>
<div class="info-value highlight">{{ containerInfo.become_time || '未知' }}</div>
</div>
<div class="info-item">
<div class="info-label">所属套餐</div>
<div class="info-value">{{ containerInfo.plan_name || '未知' }}</div>
</div>
</div>
</div>
<div class="info-card resource-info">
<div class="card-title">
<el-icon><CpuIcon /></el-icon>
<span>资源配置</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">内存限制</div>
<div class="info-value">{{ containerInfo.memory_limit || '0' }} MB</div>
</div>
<div class="info-item">
<div class="info-label">CPU限制</div>
<div class="info-value">{{ containerInfo.cpu_limit || '0' }} </div>
</div>
<div class="info-item">
<div class="info-label">存储限制</div>
<div class="info-value">{{ containerInfo.storage_limit || '0' }} GB</div>
</div>
<div class="info-item">
<div class="info-label">带宽</div>
<div class="info-value">{{ containerInfo.network_mode || 'bridge' }}</div>
</div>
</div>
</div>
<!-- <div class="info-card access-info">
<div class="card-title">
<el-icon><Key /></el-icon>
<span>访问信息</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">容器端口</div>
<div class="info-value">{{ containerInfo.container_port || '80' }}</div>
</div>
<div class="info-item">
<div class="info-label">主机端口</div>
<div class="info-value">{{ containerInfo.host_port || '8080' }}</div>
</div>
<div class="info-item">
<div class="info-label">访问地址</div>
<div class="info-value">
<span v-if="containerInfo.server_ip && containerInfo.host_port">
http://{{ containerInfo.server_ip }}:{{ containerInfo.host_port }}
</span>
<span v-else>未配置</span>
</div>
</div>
</div>
</div> -->
<div class="info-card system-info">
<div class="card-title">
<el-icon><Setting /></el-icon>
<span>系统信息</span>
</div>
<div class="info-content">
<div class="info-item">
<div class="info-label">容器镜像</div>
<div class="info-value">{{ containerInfo.image || 'nginx:latest' }}</div>
</div>
<div class="info-item">
<div class="info-label">容器IP</div>
<div class="info-value">{{ containerInfo.container_ip || '未分配' }}</div>
</div>
<div class="info-item">
<div class="info-label">状态</div>
<div class="info-value">
<el-tag :type="getStatusType(containerInfo.state)">{{ getStatusText(containerInfo.state) }}</el-tag>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button
type="primary"
:disabled="!buttonLogic.canOpen(containerInfo.state)"
@click="handleOpen"
>
<el-icon><VideoPlay /></el-icon>开通
</el-button>
<el-button
type="success"
:disabled="!buttonLogic.canStart(containerInfo.state)"
@click="handleStart"
>
<el-icon><VideoPlay /></el-icon>开机
</el-button>
<el-button
type="warning"
:disabled="!buttonLogic.canStop(containerInfo.state)"
@click="handleStop"
>
<el-icon><VideoPause /></el-icon>关机
</el-button>
<el-button
type="info"
:disabled="!buttonLogic.canPause(containerInfo.state)"
@click="handlePause"
>
<el-icon><VideoPause /></el-icon>暂停
</el-button>
<el-button
type="info"
:disabled="!buttonLogic.canRestart(containerInfo.state)"
@click="handleRestart"
>
<el-icon><RefreshRight /></el-icon>重启
</el-button>
<el-button
type="warning"
:disabled="!buttonLogic.canReinstall(containerInfo.state)"
@click="openReinstallDialog"
>
<el-icon><Refresh /></el-icon>重装
</el-button>
<el-button
type="danger"
:disabled="!buttonLogic.canRemove(containerInfo.state)"
@click="handleRemove"
>
<el-icon><Delete /></el-icon>删除
</el-button>
<el-button
:disabled="!buttonLogic.canConsole(containerInfo.state)"
@click="handleConsole"
>
<el-icon><Monitor /></el-icon>连接控制台
</el-button>
<el-button
type="primary"
:disabled="!buttonLogic.canFileManager(containerInfo.state)"
@click="handleFileManager"
>
<el-icon><Folder /></el-icon>文件管理
</el-button>
<el-button
type="warning"
:disabled="!buttonLogic.canClearTraffic(containerInfo.state)"
@click="handleClearTraffic"
>
<el-icon><DeleteFilled /></el-icon>清除流量
</el-button>
</div>
<!-- 主要内容区域 -->
<div class="content-wrapper">
<el-tabs type="border-card" class="main-tabs" v-model="containerActiveTabName" @tab-click="handleTabClick">
<!-- 容器操作日志 -->
<el-tab-pane label="容器操作日志" name="0">
<div class="tab-header">
<h3 class="tab-title">操作日志</h3>
</div>
<el-table
v-loading="logsLoading"
:data="logsList"
border
style="width: 100%"
>
<el-table-column prop="log_id" label="ID" width="80" />
<el-table-column prop="action_type" label="操作类型" width="150" />
<el-table-column prop="created_at" label="时间" min-width="180" />
<el-table-column prop="action_info" label="操作详情" min-width="300" />
</el-table>
<el-empty v-if="logsList.length === 0" description="暂无日志数据" />
<div class="pagination-container" v-if="logsList.length > 0">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="logsTotal"
:current-page="logsPage"
:page-size="logsPageSize"
:page-sizes="[10, 20, 50, 100]"
@size-change="handleLogsSizeChange"
@current-change="handleLogsPageChange"
/>
</div>
</el-tab-pane>
<!-- 网络信息 -->
<el-tab-pane label="网络信息" name="1">
<div class="tab-header">
<h3 class="tab-title">网络信息</h3>
<el-button
type="primary"
@click="showAddNetworkDialog = true"
:icon="Plus"
:disabled="!buttonLogic.canAddNetwork(containerInfo.state)"
>
添加网络
</el-button>
</div>
<el-table
v-loading="networkLoading"
:data="networkList"
border
style="width: 100%"
class="data-table"
>
<el-table-column prop="connect_id" label="ID" width="80" />
<el-table-column prop="connect_type" label="网络类型" width="120" />
<el-table-column prop="server_port" label="外部访问端口" width="140" />
<el-table-column prop="container_port" label="内部端口" width="120" />
<el-table-column prop="container_ip" label="容器IP" min-width="130" />
<el-table-column prop="floating_ip" label="浮动IP" min-width="130" />
<el-table-column prop="domain" label="域名" min-width="150" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" min-width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button
type="danger"
size="small"
@click="handleDeleteNetwork(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="networkList.length === 0" description="暂无数据" />
</el-tab-pane>
<!-- 数据卷信息 -->
<!-- <el-tab-pane label="数据卷信息" name="2">
<div class="tab-header">
<h3 class="tab-title">数据卷信息</h3>
</div>
<el-table
v-loading="volumeInfoLoading"
:data="volumeInfoList"
border
style="width: 100%"
class="data-table"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="size" label="空间大小(MB)" width="140">
<template #default="{ row }">
{{ row.size != null && row.size !== '' ? `${row.size} MB` : '-' }}
</template>
</el-table-column>
<el-table-column prop="mount_path" label="挂载路径" min-width="200" />
<el-table-column prop="created_at" label="创建时间" min-width="160" />
</el-table>
<el-empty v-if="volumeInfoList.length === 0" description="暂无数据" />
</el-tab-pane> -->
</el-tabs>
</div>
<!-- 添加网络对话框 -->
<el-dialog
v-model="showAddNetworkDialog"
title="添加网络配置"
width="600px"
destroy-on-close
>
<el-form :model="networkForm" label-width="120px" :rules="networkRules" ref="networkFormRef">
<el-form-item label="访问类型" prop="type">
<el-select v-model="networkForm.type" placeholder="请选择访问类型" @change="handleTypeChange">
<el-option label="端口转发 (port_forward)" value="port_forward" />
<el-option label="反向代理 (nginx)" value="nginx" />
<el-option label="浮动IP (floating_ip)" value="floating_ip" />
</el-select>
</el-form-item>
<!-- port_forward 类型需要容器内部端口 -->
<el-form-item
v-if="networkForm.type === 'port_forward'"
label="容器内部端口"
prop="container_port"
>
<el-input-number
v-model="networkForm.container_port"
:min="1"
:max="65535"
placeholder="请输入容器内部端口"
style="width: 100%"
/>
</el-form-item>
<!-- nginx 类型需要容器内部端口和域名 -->
<template v-if="networkForm.type === 'nginx'">
<el-form-item label="容器内部端口" prop="container_port">
<el-input-number
v-model="networkForm.container_port"
:min="1"
:max="65535"
placeholder="请输入容器内部端口"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="域名" prop="domain">
<el-input v-model="networkForm.domain" placeholder="请输入域名" />
</el-form-item>
</template>
<!-- floating_ip 类型不需要额外参数 -->
<el-alert
v-if="networkForm.type === 'floating_ip'"
title="浮动IP类型无需额外配置参数"
type="info"
:closable="false"
show-icon
/>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showAddNetworkDialog = false">取消</el-button>
<el-button type="primary" @click="submitAddNetwork" :loading="addingNetwork">
确认
</el-button>
</div>
</template>
</el-dialog>
<!-- 重装容器对话框 -->
<el-dialog
v-model="showReinstallDialog"
title="重装容器"
width="600px"
destroy-on-close
>
<div class="reinstall-warning">
<el-alert
title="警告:重装将清除容器内所有数据,请确保已备份重要文件!"
type="error"
:closable="false"
show-icon
/>
</div>
<el-form label-width="120px" style="margin-top: 20px;">
<el-form-item label="当前镜像">
<el-input
v-model="containerInfo.image"
readonly
placeholder="当前使用的镜像"
/>
</el-form-item>
<el-form-item label="选择新镜像" required>
<el-select
v-model="selectedImageId"
placeholder="请选择要重装的镜像"
style="width: 100%"
:loading="mirrorLoading"
filterable
>
<el-option
v-for="mirror in mirrorList"
:key="mirror.image_id"
:label="`${mirror.show_name}`"
:value="mirror.image_id"
>
<div class="mirror-option">
<div class="mirror-name">{{ mirror.show_name }}</div>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-if="selectedImageId">
<el-alert
:title="`选中镜像: ${getMirrorDisplayName(selectedImageId)}`"
type="info"
:closable="false"
show-icon
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="showReinstallDialog = false">取消</el-button>
<el-button
type="danger"
@click="confirmReinstall"
:disabled="!selectedImageId"
>
确认重装
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import {
Back,
Refresh,
Monitor,
Cpu as CpuIcon,
Setting,
VideoPlay,
VideoPause,
RefreshRight,
Delete,
Plus,
Key,
Connection,
Box,
Document,
Folder,
DeleteFilled,
Upload,
FolderAdd,
CloseBold,
DCaret,
CaretRight,
DocumentAdd,
DocumentChecked,
Download,
Grid,
List
} from '@element-plus/icons-vue';
import * as echarts from 'echarts';
import {
getOneContainer,
selectServerPlan,
selectServer,
getContainerStatus,
openContainer,
stopContainer,
startContainer,
pauseContainer,
reinstallC,
clearContainerTraffic,
restartContainer,
getContainerLog,
getNetworkList,
deleteContainerNetwork,
addNetwork,
deleteContainer,
getVolumeList,
connectConsole
} from '@/utils/acs/server'
import {
Mirrorinfo,
getMirrorList
} from '@/utils/acs/mirror'
import{
backUserContainer
} from '@/utils/acs/user'
const router = useRouter();
const route = useRoute();
const containerInfo = ref({});
const loading = ref(false);
// 标签页相关
const containerActiveTabName = ref('0'); // 默认选中第一个标签
// 网络信息管理
const networkList = ref([]);
const networkLoading = ref(false);
const showAddNetworkDialog = ref(false);
// 数据卷信息管理
const volumeInfoList = ref([]);
const volumeInfoLoading = ref(false);
// 重装对话框管理
const showReinstallDialog = ref(false);
const mirrorList = ref([]);
const mirrorLoading = ref(false);
const selectedImageId = ref('');
// 处理标签页点击
const handleTabClick = (tab) => {
localStorage.setItem('containerDetailActiveTab', tab.index);
};
// 网络配置管理
const networkForm = reactive({
type: 'port_forward',
container_port: null,
domain: ''
});
// 动态验证规则
const networkRules = computed(() => {
const rules = {
type: [
{ required: true, message: '请选择访问类型', trigger: 'change' }
]
};
if (networkForm.type === 'port_forward' || networkForm.type === 'nginx') {
rules.container_port = [
{ required: true, message: '请输入容器内部端口', trigger: 'blur' },
{ type: 'number', min: 1, max: 65535, message: '请输入有效的端口(1-65535)', trigger: 'blur' }
];
}
if (networkForm.type === 'nginx') {
rules.domain = [
{ required: true, message: '请输入域名', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$/, message: '请输入有效的域名', trigger: 'blur' }
];
}
return rules;
});
const networkFormRef = ref(null);
const addingNetwork = ref(false);
// 处理访问类型变化
const handleTypeChange = (newType) => {
// 清空表单数据
networkForm.container_port = null;
networkForm.domain = '';
// 清除验证错误
if (networkFormRef.value) {
networkFormRef.value.clearValidate();
}
};
// 日志管理
const logsList = ref([]);
const logsLoading = ref(false);
const logsTotal = ref(0);
const logsPage = ref(1);
const logsPageSize = ref(10);
// 初始化数据
onMounted(() => {
if (route.query.container_id) {
// 恢复上次选中的标签页
const savedTab = localStorage.getItem('containerDetailActiveTab');
if (savedTab) {
containerActiveTabName.value = savedTab;
}
fetchContainerInfo();
fetchLogsList();
fetchNetworkList();
fetchVolumeInfoList();
} else {
ElMessage.error('缺少容器ID参数');
goBack();
}
});
// 获取容器信息
const fetchContainerInfo = async () => {
loading.value = true;
try {
//获取单个容器详情信息
const res = await getOneContainer({container_id:route.query.container_id})
console.log("获取容器信息",res)
if(res.data.code == 200){
//通过套餐id获取套餐名称
const planRes = await selectServerPlan({plan_id:res.data.data.plan_id,server_type:'dockerContainer'})
//查询服务器
const serverRes = await selectServer({server_id:res.data.data.server_id})
//获取容器状态
const statusRes = await getContainerStatus({container_id:route.query.container_id})
console.log("获取容器状态",statusRes)
//获取容器镜像
const imageRes = await Mirrorinfo({image_id:res.data.data.image_id,server_type:'dockerContainer'})
containerInfo.value = {
...res.data.data,
server_ip:serverRes.data.data.server_ip,
plan_name:planRes.data.data.name,
cpu_limit:planRes.data.data.cpu,
memory_limit:planRes.data.data.memory,
storage_limit:planRes.data.data.disk,
state:statusRes.data.data.data.state == false ? 6 : 5 || res.data.data.container_state,
network_mode:planRes.data.data.threshold_rx+'mbps'+'/'+planRes.data.data.threshold_tx+'mbps',
image:imageRes.data.data.show_name
}
}
ElMessage.success('容器信息加载成功');
} catch (error) {
console.error('获取容器信息出错:', error);
ElMessage.error('获取容器信息出错');
} finally {
loading.value = false;
}
};
// 开通容器
const handleOpen = async () => {
const res = await openContainer({container_id:route.query.container_id})
console.log("开通容器",res)
if(res.data.code == 200){
ElMessage.success(res.data.data);
}else{
ElMessage.error(res.data.data);
}
}
// 获取网络信息
const fetchNetworkList = async () => {
networkLoading.value = true;
try {
const res = await getNetworkList(route.query.container_id);
console.log("获取网络信息",res);
if(res.status == 200){
networkList.value = res.data;
}
} catch (error) {
console.error('获取网络信息出错:', error);
ElMessage.error('获取网络信息出错');
} finally {
networkLoading.value = false;
}
};
// 获取数据卷信息
const fetchVolumeInfoList = async () => {
volumeInfoLoading.value = true;
try {
const res = await getVolumeList({instance_id:route.query.container_id,page:1,count:10});
console.log("获取数据卷信息",res);
if(res.data.code == 200){
volumeInfoList.value = res.data.data;
}
} catch (error) {
console.error('获取数据卷信息出错:', error);
ElMessage.error('获取数据卷信息出错');
} finally {
volumeInfoLoading.value = false;
}
};
// 获取镜像列表
const fetchMirrorList = async () => {
mirrorLoading.value = true;
console.log("获取镜像列表",containerInfo.value.server_id)
try {
const res = await getMirrorList(containerInfo.value.server_id);
console.log("获取镜像列表",res);
if(res.data.code == 200){
mirrorList.value = res.data.data;
// 默认选择当前镜像
// if (containerInfo.value.image_id) {
// selectedImageId.value = containerInfo.value.image_id;
// }
}
} catch (error) {
console.error('获取镜像列表出错:', error);
ElMessage.error('获取镜像列表出错');
} finally {
mirrorLoading.value = false;
}
};
// 获取日志列表
const fetchLogsList = async () => {
logsLoading.value = true;
try {
const res = await getContainerLog({container_id:route.query.container_id,page:logsPage.value,count:logsPageSize.value})
console.log("获取日志列表",res)
if(res.data.code == 200){
logsList.value = res.data.data
logsTotal.value = res.data.data.length
}
} catch (error) {
console.error('获取日志列表出错:', error);
ElMessage.error('获取日志列表出错');
} finally {
logsLoading.value = false;
}
};
// 处理日志分页
const handleLogsSizeChange = (size) => {
logsPageSize.value = size;
logsPage.value = 1;
fetchLogsList();
};
const handleLogsPageChange = (page) => {
logsPage.value = page;
fetchLogsList();
};
// 添加网络配置
const submitAddNetwork = async () => {
if (!networkFormRef.value) return;
await networkFormRef.value.validate(async (valid) => {
if (valid) {
addingNetwork.value = true;
try {
// 构建 proxy_data 参数
let proxy_data = [{
type: networkForm.type
}];
// 根据类型添加对应参数
if (networkForm.type === 'port_forward') {
proxy_data[0].container_port = networkForm.container_port;
proxy_data[0].container_id = route.query.container_id;
} else if (networkForm.type === 'nginx') {
proxy_data[0].container_port = networkForm.container_port;
proxy_data[0].container_id = route.query.container_id;
proxy_data[0].domain = networkForm.domain;
}
proxy_data = JSON.stringify(proxy_data)
// floating_ip 类型不需要额外参数
const res = await addNetwork({
container_id: route.query.container_id,
proxy_data: proxy_data
});
console.log("添加网络配置结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.msg || '添加网络配置成功');
showAddNetworkDialog.value = false;
fetchNetworkList(); // 重新获取网络列表
// 重置表单
resetNetworkForm();
} else {
ElMessage.error(res.data.msg || '添加网络配置失败');
}
} catch (error) {
console.error('添加网络配置出错:', error);
ElMessage.error('添加网络配置出错');
} finally {
addingNetwork.value = false;
}
}
});
};
// 重置网络表单
const resetNetworkForm = () => {
networkForm.type = 'port_forward';
networkForm.container_port = null;
networkForm.domain = '';
if (networkFormRef.value) {
networkFormRef.value.resetFields();
}
};
// 删除网络
const handleDeleteNetwork = async (network) => {
try {
ElMessageBox.confirm(`确定要删除该网络配置吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await deleteContainerNetwork({container_id: route.query.container_id,connect_id: String(network.connect_id)});
console.log("删除网络结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.msg || '删除成功');
fetchNetworkList();
} else {
ElMessage.error(res.data.msg || '删除失败');
}
}).catch(() => {});
} catch (error) {
console.error('删除网络出错:', error);
ElMessage.error('删除网络出错');
}
};
// 返回上一页
const goBack = () => {
router.go(-1);
};
// 刷新数据
const refreshData = () => {
fetchContainerInfo();
fetchLogsList();
fetchNetworkList();
fetchVolumeInfoList();
};
// 启动容器
const handleStart = async () => {
try {
const res = await startContainer({container_id: route.query.container_id});
console.log("启动容器结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.data || '启动指令已发送');
} else {
ElMessage.error(res.data.data || '启动失败');
}
setTimeout(() => {
fetchContainerInfo();
}, 2000);
} catch (error) {
console.error('启动容器出错:', error);
ElMessage.error('启动容器出错');
}
};
// 停止容器
const handleStop = async () => {
try {
ElMessageBox.confirm('确定要停止该容器吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await stopContainer({container_id: route.query.container_id});
console.log("停止容器结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.data || '停止指令已发送');
} else {
ElMessage.error(res.data.data || '停止失败');
}
setTimeout(() => {
fetchContainerInfo();
}, 2000);
}).catch(() => {});
} catch (error) {
console.error('停止容器出错:', error);
ElMessage.error('停止容器出错');
}
};
// 重启容器
const handleRestart = async () => {
try {
ElMessageBox.confirm('确定要重启该容器吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await restartContainer({container_id: route.query.container_id});
console.log("重启容器结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.data || '重启指令已发送');
} else {
ElMessage.error(res.data.data || '重启失败');
}
setTimeout(() => {
fetchContainerInfo();
}, 2000);
}).catch(() => {});
} catch (error) {
console.error('重启容器出错:', error);
ElMessage.error('重启容器出错');
}
};
// 暂停容器
const handlePause = async () => {
try {
ElMessageBox.confirm('确定要暂停该容器吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await pauseContainer({container_id: route.query.container_id});
console.log("暂停容器结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.data || '暂停指令已发送');
} else {
ElMessage.error(res.data.data || '暂停失败');
}
setTimeout(() => {
fetchContainerInfo();
}, 2000);
}).catch(() => {});
} catch (error) {
console.error('暂停容器出错:', error);
ElMessage.error('暂停容器出错');
}
};
// 打开重装对话框
const openReinstallDialog = async () => {
try {
// 获取镜像列表
await fetchMirrorList();
showReinstallDialog.value = true;
} catch (error) {
console.error('打开重装对话框出错:', error);
ElMessage.error('获取镜像列表失败');
}
};
// 确认重装
const confirmReinstall = async () => {
if (!selectedImageId.value) {
ElMessage.error('请选择要重装的镜像');
return;
}
try {
ElMessageBox.confirm('确定要重装该容器吗?此操作将清除容器内所有数据!', '警告', {
confirmButtonText: '确定重装',
cancelButtonText: '取消',
type: 'primary'
}).then(async () => {
await handleReinstall();
}).catch(() => {});
} catch (error) {
console.error('确认重装出错:', error);
ElMessage.error('操作失败');
}
};
// 重装容器
const handleReinstall = async () => {
try {
const res = await reinstallC({
container_id: route.query.container_id,
image_id: selectedImageId.value
});
console.log("重装容器结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.data.msg || '重装指令已发送');
showReinstallDialog.value = false;
setTimeout(() => {
fetchContainerInfo();
}, 3000);
} else {
ElMessage.error(res.data.msg || '重装失败');
}
} catch (error) {
console.error('重装容器出错:', error);
ElMessage.error('重装容器出错');
}
};
// 获取镜像显示名称
const getMirrorDisplayName = (imageId) => {
const mirror = mirrorList.value.find(m => m.image_id === imageId);
return mirror ? `${mirror.show_name}` : '未知镜像';
};
// 清除流量
const handleClearTraffic = async () => {
try {
ElMessageBox.confirm('确定要清除该容器的流量统计吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await clearContainerTraffic({container_id: route.query.container_id});
console.log("清除流量结果:", res);
if (res.data.code == 200) {
ElMessage.success(res.data.msg || '流量统计已清除');
} else {
ElMessage.error(res.data.msg || '清除流量失败');
}
}).catch(() => {});
} catch (error) {
console.error('清除流量出错:', error);
ElMessage.error('清除流量出错');
}
};
// 文件管理
const handleFileManager = async () => {
try {
window.open('/servers/container/files?container_id='+route.query.container_id,'_blank')
} catch (error) {
console.error('打开文件管理出错:', error);
ElMessage.error('打开文件管理出错');
}
};
// 删除容器
const handleRemove = async () => {
try {
ElMessageBox.confirm('确定要删除该容器吗?此操作不可恢复!', '警告', {
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 这里应该调用删除容器的API
//对接删除
const res = await deleteContainer({container_id: route.query.container_id});
console.log("删除容器结果:", res);
// if (res.data.code == 200) {
// ElMessage.success(res.data.msg || '删除成功');
// } else {
// ElMessage.error(res.data.data.msg || '删除失败');
// }
ElMessage.success('删除指令已发送');
setTimeout(() => {
goBack();
}, 2000);
}).catch(() => {});
} catch (error) {
console.error('删除容器出错:', error);
ElMessage.error('删除容器出错');
}
};
// 打开终端
const handleConsole = async () => {
const res = await connectConsole({container_id: route.query.container_id});
window.open(`/servers/container/console?token=${res.data.data.data.token}&server_addr=${res.data.data.data.console_port}`,`_blank`);
};
// 获取状态类型
const getStatusType = (state) => {
// 0未支付 1未构建 2已构建 3未知 4已删除 5运行中 6关机
switch (state) {
case 0:
return 'warning'; // 未支付 - 橙色警告
case 1:
return 'info'; // 未构建 - 蓝色信息
case 2:
return 'success'; // 已构建 - 绿色成功
case 3:
return 'info'; // 未知 - 蓝色信息
case 4:
return 'danger'; // 已删除 - 红色危险
case 5:
return 'success'; // 运行中 - 绿色成功
case 6:
return 'danger'; // 关机 - 红色危险
default:
return 'info';
}
};
// 按钮启用/禁用逻辑函数
const buttonLogic = {
// 开通按钮:未支付或未构建时可用
canOpen: (state) => state === 0 || state === 1,
// 开机按钮:已构建但未运行时可用(状态2已构建,状态6关机)
canStart: (state) => state === 2 || state === 6,
// 关机按钮:运行中时可用
canStop: (state) => state === 5,
// 暂停按钮:运行中时可用
canPause: (state) => state === 5,
// 重启按钮:运行中时可用
canRestart: (state) => state === 5,
// 重装按钮:已构建、运行中、关机状态时可用,未构建和已删除时不可用
canReinstall: (state) => state !== 1 && state !== 4,
// 删除按钮:已构建、运行中、关机状态时可用,未构建和已删除时不可用
canRemove: (state) => state !== 1 && state !== 4,
// 控制台按钮:运行中时可用
canConsole: (state) => state === 5,
// 文件管理按钮:运行中时可用
canFileManager: (state) => state === 5,
// 清除流量按钮:始终可用(除了已删除状态)
canClearTraffic: (state) => state !== 4,
// 网络配置按钮:运行中时可用
canAddNetwork: (state) => state === 5
};
// 获取状态文本
const getStatusText = (state) => {
// 0未支付 1未构建 2已构建 3未知 4已删除 5运行中 6关机
switch (state) {
case 0:
return '未支付';
case 1:
return '未构建';
case 2:
return '已构建';
case 3:
return '未知';
case 4:
return '已删除';
case 5:
return '开机';
case 6:
return '关机';
default:
return '未知状态';
}
};
// 获取状态点样式类
const getStatusDotClass = (state) => {
// 运行中状态显示为在线(绿色),其他状态显示为离线(红色)
return state === 5 ? 'online' : 'offline';
};
// 组件卸载前清理
onBeforeUnmount(() => {
// 移除事件监听
// window.removeEventListener('resize', resizeCharts); // 移除此行
// 销毁图表实例 // 移除此行
// cpuChartInstance && cpuChartInstance.dispose(); // 移除此行
// memoryChartInstance && memoryChartInstance.dispose(); // 移除此行
// diskChartInstance && diskChartInstance.dispose(); // 移除此行
// networkChartInstance && networkChartInstance.dispose(); // 移除此行
});
</script>
<style scoped>
.container-detail-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 120px);
}
/* 页面标题区域 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-header .left {
display: flex;
align-items: center;
gap: 12px;
}
.page-header .title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
.status-tag {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.status-dot.online {
background-color: #67C23A;
box-shadow: 0 0 6px rgba(103, 194, 58, 0.8);
}
.status-dot.offline {
background-color: #F56C6C;
box-shadow: 0 0 6px rgba(245, 108, 108, 0.8);
}
/* 容器信息卡片 */
.container-info {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.info-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
padding: 0;
overflow: hidden;
transition: all 0.3s;
border: 1px solid #ebeef5;
}
.info-card:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1);
}
.card-title {
background-color: #f5f7fa;
padding: 12px 16px;
border-bottom: 1px solid #ebeef5;
font-size: 16px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 8px;
}
.card-title .el-icon {
font-size: 18px;
color: #409EFF;
}
.info-content {
padding: 16px;
}
.info-item {
margin-bottom: 12px;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 13px;
color: #909399;
margin-bottom: 4px;
}
.info-value {
font-size: 15px;
color: #303133;
word-break: break-all;
}
.info-value.highlight {
font-weight: 600;
font-size: 16px;
color: #409EFF;
}
/* 操作按钮 */
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 24px;
}
.action-buttons .el-button {
margin: 0;
min-width: 120px;
}
/* 主要内容区域 */
.content-wrapper {
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
}
.tab-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.tab-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #303133;
}
/* 分页容器 */
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
/* 对话框样式 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 重装对话框样式 */
.reinstall-warning {
margin-bottom: 20px;
}
.mirror-option {
display: flex;
flex-direction: column;
}
.mirror-name {
font-weight: 600;
color: #303133;
}
.mirror-detail {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
/* 响应式设计 */
@media screen and (max-width: 1200px) {
.container-info {
grid-template-columns: repeat(2, 1fr);
}
.info-card.system-info {
grid-column: span 2;
}
}
@media screen and (max-width: 768px) {
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.container-info {
grid-template-columns: 1fr;
}
.info-card.system-info {
grid-column: auto;
}
.action-buttons {
flex-direction: column;
}
.action-buttons .el-button {
width: 100%;
min-width: auto;
}
}
/* 监控图表相关样式 */
.date-filter {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
}
.monitor-charts {
margin-top: 20px;
}
.chart-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart {
height: 300px;
}
/* 文件管理对话框样式 */
.file-manager-dialog .el-dialog__body {
padding: 0;
}
.file-manager-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.header-icon {
font-size: 24px;
}
.header-title {
font-size: 18px;
font-weight: 600;
}
.path-display {
margin-left: 20px;
opacity: 0.9;
}
.header-right {
display: flex;
gap: 8px;
}
.header-right .el-button {
color: white;
border-color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
}
.header-right .el-button:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
.file-manager-container {
height: 85vh;
display: flex;
background: #f8f9fa;
}
/* 左侧文件树 */
.file-sidebar {
width: 300px;
background: #ffffff;
border-right: 1px solid #e9ecef;
display: flex;
flex-direction: column;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
font-weight: 600;
color: #495057;
}
.sidebar-title {
font-size: 14px;
}
.sidebar-actions {
display: flex;
gap: 4px;
}
.file-tree-container {
flex: 1;
overflow: auto;
padding: 8px;
}
.file-tree .el-tree-node__content {
height: 36px;
line-height: 36px;
}
.file-tree .el-tree-node__content:hover {
background-color: #f8f9fa;
}
.file-tree .el-tree-node:focus > .el-tree-node__content {
background-color: #e3f2fd;
}
.tree-node-content {
display: flex;
align-items: center;
flex: 1;
padding-right: 8px;
}
.node-icon {
margin-right: 8px;
font-size: 16px;
}
.folder-icon {
color: #ffd54f;
}
.file-icon {
color: #90a4ae;
}
.node-label {
flex: 1;
font-size: 14px;
color: #495057;
}
.node-actions {
display: flex;
gap: 2px;
opacity: 0.8;
}
/* 右侧内容区域 */
.content-area {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff;
}
.content-tabs {
height: 100%;
}
.editor-tabs {
height: 100%;
}
.editor-tabs .el-tabs__content {
height: calc(100% - 40px);
overflow: hidden;
}
.editor-tabs .el-tab-pane {
height: 100%;
}
/* 文件编辑器 */
.file-editor {
height: 100%;
display: flex;
flex-direction: column;
}
.editor-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.file-info {
font-weight: 600;
color: #495057;
}
.toolbar-right {
display: flex;
gap: 8px;
}
.editor-content {
flex: 1;
padding: 0;
}
.code-editor {
height: 100%;
}
.code-editor .el-textarea__inner {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
border: none;
border-radius: 0;
resize: none;
background: #ffffff;
color: #212529;
}
.code-editor .el-textarea__inner:focus {
box-shadow: none;
}
/* 目录视图 */
.directory-view {
height: 100%;
display: flex;
flex-direction: column;
}
.directory-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.directory-header h3 {
margin: 0;
color: #495057;
font-size: 18px;
}
.view-actions {
display: flex;
gap: 4px;
}
/* 网格视图 */
.grid-view {
flex: 1;
padding: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
overflow: auto;
}
.file-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.file-item:hover {
background: #f8f9fa;
border-color: #dee2e6;
}
.file-item:active {
background: #e9ecef;
}
.file-icon-large {
margin-bottom: 8px;
}
.file-name {
text-align: center;
font-size: 12px;
color: #495057;
word-break: break-all;
line-height: 1.3;
max-width: 100%;
}
.file-details {
font-size: 11px;
color: #6c757d;
margin-top: 4px;
}
/* 列表视图 */
.list-view {
flex: 1;
padding: 0 20px 20px;
}
.directory-table .el-table__row {
cursor: pointer;
}
.directory-table .el-table__row:hover {
background: #f8f9fa;
}
/* 欢迎页 */
.welcome-view {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.welcome-content {
text-align: center;
max-width: 600px;
padding: 40px;
}
.welcome-header {
margin-bottom: 40px;
}
.welcome-header h2 {
margin: 16px 0 8px;
color: #495057;
font-size: 28px;
font-weight: 300;
}
.welcome-header p {
color: #6c757d;
font-size: 16px;
margin: 0;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.action-card {
cursor: pointer;
transition: all 0.3s;
border: 2px solid transparent;
}
.action-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #409EFF;
}
.action-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px;
}
.action-content span {
font-weight: 500;
color: #495057;
}
.breadcrumb-clickable {
cursor: pointer;
color: rgba(255, 255, 255, 0.9);
}
.breadcrumb-clickable:hover {
color: white;
text-decoration: underline;
}
/* 响应式设计 */
@media screen and (max-width: 1200px) {
.file-sidebar {
width: 250px;
}
}
@media screen and (max-width: 768px) {
.file-manager-container {
height: 75vh;
flex-direction: column;
}
.file-sidebar {
width: 100%;
height: 200px;
border-right: none;
border-bottom: 1px solid #e9ecef;
}
.content-area {
height: calc(100% - 200px);
}
.header-left {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.path-display {
margin-left: 0;
}
.header-right {
flex-wrap: wrap;
gap: 4px;
}
.grid-view {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 12px;
padding: 16px;
}
}
</style>