1879 lines
47 KiB
Vue
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>
|