2788 lines
82 KiB
Vue
2788 lines
82 KiB
Vue
<template>
|
||
<div class="vm-detail-container">
|
||
<div class="page-header">
|
||
<div class="left">
|
||
<h2 class="title">虚拟机详情</h2>
|
||
<el-tag
|
||
:type="getStatusType(vmInfo.state)"
|
||
effect="dark"
|
||
class="status-tag"
|
||
>
|
||
<span class="status-dot" :class="vmInfo.state == 2 ? 'online' : 'offline'"></span>
|
||
{{ getStatusText(vmInfo.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>
|
||
<el-button type="info" @click="clearAllCache" plain>清除缓存</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 虚拟机信息卡片 -->
|
||
<div class="vm-info">
|
||
<div class="info-card main-info">
|
||
<div class="card-title">
|
||
<el-icon><Monitor /></el-icon>
|
||
<span>基本信息</span>
|
||
</div>
|
||
<div class="info-content">
|
||
<div class="info-item">
|
||
<div class="info-label">虚拟机ID</div>
|
||
<div class="info-value">{{ vmInfo.id || '未知' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">用户ID</div>
|
||
<div class="info-value">{{ vmInfo.user_id || '未知' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">价格</div>
|
||
<div class="info-value highlight">{{ vmInfo.pay || '0' }} 元</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">所属服务器</div>
|
||
<div class="info-value">{{ vmInfo.server_name || '未知' }}</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">{{ vmInfo.server_ip || '未知' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">创建时间</div>
|
||
<div class="info-value">{{ vmInfo.created_at || '未知' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">到期时间</div>
|
||
<div class="info-value highlight">{{ vmInfo.become_time || '未知' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">所属套餐</div>
|
||
<div class="info-value">{{ vmInfo.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">{{ vmInfo.memory_size || '0' }} MB</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">硬盘</div>
|
||
<div class="info-value">{{ vmInfo.disk_size || '0' }} GB</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">CPU</div>
|
||
<div class="info-value">{{ vmInfo.cpu_count || '0' }} 核</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">{{ vmInfo.node_port || (portsList.length > 0 ? portsList[0].node_port : '10006') }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">虚拟机用户名</div>
|
||
<div class="info-value">{{ vmInfo.username || 'administrator' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">虚拟机密码</div>
|
||
<div class="info-value password-field">
|
||
<span>{{ vmInfo.password || '******' }}</span>
|
||
<!-- <el-button link type="primary" size="small" @click="confirmPassword">确认</el-button> -->
|
||
</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">{{ vmInfo.image_name || 'Windows10-cn' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">虚拟机IP</div>
|
||
<div class="info-value">{{ vmInfo.network_ip || '未分配' }}</div>
|
||
</div>
|
||
<div class="info-item">
|
||
<div class="info-label">状态</div>
|
||
<div class="info-value">
|
||
<el-tag :type="getStatusType(vmInfo.state)">{{ getStatusText(vmInfo.state) }}</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons">
|
||
|
||
<el-button
|
||
type="primary" @click="handleOpen">
|
||
<el-icon><VideoPlay /></el-icon>开通
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
:disabled="vmInfo.state == 2 || vmInfo.state == 0 || vmInfo.state == 1 || vmInfo.state == 4 || vmInfo.state == 5 || vmInfo.state == 6"
|
||
@click="handleStart"
|
||
>
|
||
<el-icon><VideoPlay /></el-icon>开机
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
:disabled="vmInfo.state != 2"
|
||
@click="handleStop"
|
||
>
|
||
<el-icon><VideoPause /></el-icon>关机
|
||
</el-button>
|
||
<el-button
|
||
type="info"
|
||
:disabled="vmInfo.state != 2"
|
||
@click="handlePause"
|
||
>
|
||
<el-icon><VideoPause /></el-icon>暂停
|
||
</el-button>
|
||
<el-button
|
||
type="success"
|
||
:disabled="vmInfo.state != 7"
|
||
@click="handleUnpause"
|
||
>
|
||
<el-icon><VideoPlay /></el-icon>恢复
|
||
</el-button>
|
||
<el-button
|
||
type="info"
|
||
:disabled="vmInfo.state != 2"
|
||
@click="handleRestart"
|
||
>
|
||
<el-icon><RefreshRight /></el-icon>重启
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
:disabled="vmInfo.state == 0 || vmInfo.state == 1 || vmInfo.state == 4 || vmInfo.state == 5 || vmInfo.state == 6"
|
||
@click="handleReinstall"
|
||
>
|
||
<el-icon><Delete /></el-icon>重装系统
|
||
</el-button>
|
||
<el-button
|
||
:disabled="vmInfo.state == 0 || vmInfo.state == 1"
|
||
@click="handleConsole"
|
||
>
|
||
<el-icon><Monitor /></el-icon>控制台
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:disabled="vmInfo.state == 0 || vmInfo.state == 1 || vmInfo.state == 4 || vmInfo.state == 5 || vmInfo.state == 6"
|
||
@click="handleEnterRescue()"
|
||
>
|
||
<el-icon><Warning /></el-icon>进入救援模式
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
:disabled="vmInfo.state == 0 || vmInfo.state == 1 || vmInfo.state == 4 || vmInfo.state == 5 || vmInfo.state == 6"
|
||
@click="handleExitRescue()"
|
||
>
|
||
<el-icon><Warning /></el-icon>退出救援模式
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
:disabled="vmInfo.state == 0 || vmInfo.state == 1 || vmInfo.state == 4 || vmInfo.state == 5 || vmInfo.state == 6"
|
||
@click="handleDelete"
|
||
>
|
||
<el-icon><Delete /></el-icon>删除虚拟机
|
||
</el-button>
|
||
</div>
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="content-wrapper">
|
||
<el-tabs type="border-card" class="main-tabs" v-model="activeTabName" @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>
|
||
<div class="date-filter">
|
||
<el-date-picker
|
||
v-model="monitorDateRange"
|
||
type="datetimerange"
|
||
range-separator="至"
|
||
start-placeholder="开始时间"
|
||
end-placeholder="结束时间"
|
||
:shortcuts="dateRangeShortcuts"
|
||
@change="handleDateRangeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="monitor-charts" v-loading="monitorLoading">
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-card class="chart-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>实时监控</span>
|
||
<div class="monitor-stats">
|
||
<span class="stat-item">CPU: {{ latestCpuUsage }}%</span>
|
||
<span class="stat-item">内存: {{ latestMemoryUsage }}MB</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div ref="realTimeChart" class="chart"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 虚拟机网络管理 -->
|
||
<el-tab-pane label="虚拟机网络管理" name="2">
|
||
<div class="tab-header">
|
||
<h3 class="tab-title">网络管理</h3>
|
||
<el-button
|
||
type="primary"
|
||
@click="showAddNetworkRuleDialog = true"
|
||
:icon="Plus"
|
||
:disabled="vmInfo.state != 2"
|
||
>
|
||
添加网络规则
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="networkRulesLoading"
|
||
:data="networkRulesList"
|
||
border
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="id" label="ID" width="60" />
|
||
<el-table-column prop="direction" label="网络方向" width="100">
|
||
<template #default="scope">
|
||
{{ scope.row.direction === 'Inbound' ? '入站' : '出站' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="protocol" label="协议类型" width="100" />
|
||
<el-table-column prop="local_port_range" label="本地端口范围" width="120" />
|
||
<el-table-column prop="local_ip_address" label="本地IP地址" width="140" />
|
||
<el-table-column prop="remote_port_range" label="远程端口范围" width="120" />
|
||
<el-table-column prop="remote_ip_address" label="远程IP地址" width="140" />
|
||
<el-table-column prop="action" label="状态" width="100" >
|
||
<template #default="scope">
|
||
{{ scope.row.action === 'Allow' ? '允许' : '拒绝' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="100" fixed="right">
|
||
<template #default="scope">
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleDeleteNetworkRule(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-empty v-if="networkRulesList.length === 0" description="暂无网络规则" />
|
||
|
||
<div class="pagination-container" v-if="networkRulesList.length > 0">
|
||
<el-pagination
|
||
background
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="networkRulesTotal"
|
||
:current-page="networkRulesPage"
|
||
:page-size="networkRulesPageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
@size-change="handleNetworkRulesSizeChange"
|
||
@current-change="handleNetworkRulesPageChange"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 端口管理 -->
|
||
<el-tab-pane label="端口管理" name="3">
|
||
<div class="tab-header">
|
||
<h3 class="tab-title">端口列表</h3>
|
||
<el-button
|
||
type="primary"
|
||
@click="showAddPortDialog = true"
|
||
:icon="Plus"
|
||
:disabled="vmInfo.state != 2"
|
||
>
|
||
添加端口
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="portsLoading"
|
||
:data="portsList"
|
||
border
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column prop="internal_port" label="内部端口" width="120" />
|
||
<el-table-column prop="node_port" label="外部端口" width="120" />
|
||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||
<el-table-column label="操作" width="120" fixed="right">
|
||
<template #default="scope">
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleDeletePort(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-empty v-if="portsList.length === 0" description="暂无端口数据" />
|
||
|
||
<div class="pagination-container" v-if="portsList.length > 0">
|
||
<el-pagination
|
||
background
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="portsTotal"
|
||
:current-page="portsPage"
|
||
:page-size="portsPageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
@size-change="handlePortsSizeChange"
|
||
@current-change="handlePortsPageChange"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<!-- 快照列表 -->
|
||
<el-tab-pane label="快照列表" name="4">
|
||
<div class="tab-header">
|
||
<h3 class="tab-title">快照管理</h3>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleCreateSnapshot"
|
||
:icon="Plus"
|
||
:disabled="vmInfo.state != 2"
|
||
>
|
||
创建快照
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="snapshotsLoading"
|
||
:data="snapshotsList"
|
||
border
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column prop="note" label="名称" width="150" />
|
||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||
<el-table-column prop="description" label="描述" min-width="200" />
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template #default="scope">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handleRestoreSnapshot(scope.row)"
|
||
>
|
||
恢复
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleDeleteSnapshot(scope.row)"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-empty v-if="snapshotsList.length === 0" description="暂无快照数据" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 数据卷信息 -->
|
||
<el-tab-pane label="数据卷信息" name="5">
|
||
<div class="tab-header">
|
||
<h3 class="tab-title">数据卷列表</h3>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleAddVolume"
|
||
:icon="Plus"
|
||
:disabled="vmInfo.state != 2"
|
||
>
|
||
添加数据卷
|
||
</el-button>
|
||
</div>
|
||
|
||
<el-table
|
||
v-loading="volumesLoading"
|
||
:data="dataVolumes"
|
||
border
|
||
style="width: 100%"
|
||
>
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column prop="size" label="大小" width="100">
|
||
<template #default="scope">
|
||
{{ scope.row.size }} GB
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||
<el-table-column prop="state" label="状态" width="100" >
|
||
<template #default="scope">
|
||
{{ volumeStatus(scope.row.state) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="280" fixed="right">
|
||
<template #default="scope">
|
||
<el-button
|
||
type="primary"
|
||
link
|
||
size="small"
|
||
@click="handleEditVolume(scope.row)"
|
||
:disabled="scope.row.type === 'system'"
|
||
>
|
||
修改
|
||
</el-button>
|
||
<el-button
|
||
type="warning"
|
||
link
|
||
size="small"
|
||
@click="handleMigrateVolume(scope.row)"
|
||
:disabled="scope.row.type === 'system'"
|
||
>
|
||
迁移
|
||
</el-button>
|
||
<el-button
|
||
type="danger"
|
||
link
|
||
size="small"
|
||
@click="handleDeleteVolume(scope.row)"
|
||
:disabled="scope.row.type === 'system'"
|
||
>
|
||
删除
|
||
</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-empty v-if="dataVolumes.length === 0" description="暂无数据卷" />
|
||
|
||
<div class="pagination-container" v-if="dataVolumes.length > 0">
|
||
<el-pagination
|
||
background
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
:total="volumesTotal"
|
||
:current-page="volumesPage"
|
||
:page-size="volumesPageSize"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
/>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
|
||
<!-- 添加端口对话框 -->
|
||
<el-dialog
|
||
v-model="showAddPortDialog"
|
||
title="添加端口"
|
||
width="500px"
|
||
>
|
||
<el-form :model="portForm" label-width="120px" :rules="portRules" ref="portFormRef">
|
||
<el-form-item label="内部端口" prop="internal_port">
|
||
<el-input v-model="portForm.internal_port" :min="1" :max="65535" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showAddPortDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitAddPort" :loading="addingPort">
|
||
确认
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 重装系统对话框 -->
|
||
<el-dialog
|
||
v-model="showReinstallDialog"
|
||
title="重装系统"
|
||
width="500px"
|
||
>
|
||
<el-form :model="reinstallForm" label-width="120px" :rules="reinstallRules" ref="reinstallFormRef">
|
||
<el-form-item label="选择镜像" prop="image_id">
|
||
<el-select v-model="reinstallForm.image_id" placeholder="请选择系统镜像" filterable>
|
||
<el-option
|
||
v-for="item in imagesList"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showReinstallDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitReinstall" :loading="reinstalling">
|
||
确认重装
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 添加访问控制对话框 -->
|
||
<el-dialog
|
||
v-model="showAddAccessControlDialog"
|
||
title="添加访问控制"
|
||
width="500px"
|
||
>
|
||
<el-form :model="accessControlForm" label-width="120px" :rules="accessControlRules" ref="accessControlFormRef">
|
||
<el-form-item label="来源IP" prop="source_ip">
|
||
<el-input v-model="accessControlForm.source_ip" placeholder="例如: 192.168.1.1 或 192.168.1.0/24" />
|
||
</el-form-item>
|
||
<el-form-item label="描述" prop="description">
|
||
<el-input v-model="accessControlForm.description" type="textarea" :rows="2" placeholder="请输入描述" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showAddAccessControlDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitAddAccessControl" :loading="addingAccessControl">
|
||
确认
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 添加网络规则对话框 -->
|
||
<el-dialog
|
||
v-model="showAddNetworkRuleDialog"
|
||
title="添加网络规则"
|
||
width="600px"
|
||
>
|
||
<el-form :model="networkRuleForm" label-width="120px" :rules="networkRuleRules" ref="networkRuleFormRef">
|
||
<el-form-item label="网络方向" prop="direction">
|
||
<el-radio-group v-model="networkRuleForm.direction">
|
||
<el-radio label="Inbound">入站</el-radio>
|
||
<el-radio label="Outbound">出站</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="协议类型" prop="protocol">
|
||
<el-select v-model="networkRuleForm.protocol" placeholder="请选择协议类型">
|
||
<el-option label="TCP" value="tcp" />
|
||
<el-option label="UDP" value="udp" />
|
||
<el-option label="ALL" value="all" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="本地端口范围" prop="local_port_range">
|
||
<el-input v-model="networkRuleForm.local_port_range" placeholder="一个端口则填端口,多个则表示为 80-100" />
|
||
</el-form-item>
|
||
<el-form-item label="本地IP地址" prop="local_ip_address">
|
||
<el-input v-model="networkRuleForm.local_ip_address" placeholder="例如: 192.168.1.1 或留空表示任意" />
|
||
</el-form-item>
|
||
<el-form-item label="远程端口范围" prop="remote_port_range">
|
||
<el-input v-model="networkRuleForm.remote_port_range" placeholder="一个端口则填端口,多个则表示为 80-100" />
|
||
</el-form-item>
|
||
<el-form-item label="远程IP地址" prop="remote_ip_address">
|
||
<el-input v-model="networkRuleForm.remote_ip_address" placeholder="例如: 192.168.1.1 或留空表示任意" />
|
||
</el-form-item>
|
||
<el-form-item label="操作" prop="action">
|
||
<el-select v-model="networkRuleForm.action" placeholder="请选择操作">
|
||
<el-option label="允许" value="Allow" />
|
||
<el-option label="拒绝" value="Deny" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showAddNetworkRuleDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitAddNetworkRule" :loading="addingNetworkRule">
|
||
确认
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 添加数据卷对话框 -->
|
||
<el-dialog
|
||
v-model="showAddVolumeDialog"
|
||
title="添加数据卷"
|
||
width="500px"
|
||
>
|
||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||
<el-form-item label="大小" prop="size">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showAddVolumeDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitAddVolume" :loading="addingVolume">
|
||
确认
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 编辑数据卷对话框 -->
|
||
<el-dialog
|
||
v-model="showEditVolumeDialog"
|
||
title="编辑数据卷"
|
||
width="500px"
|
||
>
|
||
<el-form :model="volumeForm" label-width="120px" :rules="volumeRules" ref="volumeFormRef">
|
||
|
||
<el-form-item label="大小" prop="size">
|
||
<div class="unit-input-row">
|
||
<el-input-number v-model="volumeForm.size" :min="1" :max="1000" style="flex:1" />
|
||
<el-select v-model="volumeForm._sizeUnit" class="unit-select"><el-option label="GB" value="GB" /><el-option label="TB" value="TB" /></el-select>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="showEditVolumeDialog = false">取消</el-button>
|
||
<el-button type="primary" @click="submitEditVolume" :loading="editingVolume">
|
||
确认
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 迁移数据卷对话框 -->
|
||
<el-dialog
|
||
v-model="showMigrateVolumeDialog"
|
||
title="迁移数据卷"
|
||
width="500px"
|
||
>
|
||
<el-table :data="migrateForm" style="width: 100%" >
|
||
<el-table-column prop="id" label="ID" width="100">
|
||
</el-table-column>
|
||
<el-table-column prop="created_at" label="创建时间" width="100">
|
||
</el-table-column>
|
||
<el-table-column prop="state" label="状态" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="statusColor(scope.row.state)">{{ statusMap(scope.row.state) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column width="100">
|
||
<template #default="scope">
|
||
<el-button type="primary" @click="outerVisible(scope.row.id)">迁移</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||
import { useRouter, useRoute } from 'vue-router';
|
||
import {getUserInfo, userLogin} from "@/api/login.js";
|
||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||
import {
|
||
Back,
|
||
Refresh,
|
||
Monitor,
|
||
Cpu as CpuIcon,
|
||
Setting,
|
||
VideoPlay,
|
||
VideoPause,
|
||
RefreshRight,
|
||
Delete,
|
||
Camera,
|
||
Warning,
|
||
Plus,
|
||
Key,
|
||
Connection
|
||
} from '@element-plus/icons-vue';
|
||
import {
|
||
getVmAdminContainer,
|
||
startInstance,
|
||
stopInstance,
|
||
restartInstance,
|
||
reinstallI,
|
||
rescueInstance,
|
||
exitRescueInstance,
|
||
getInstancePortList,
|
||
addPort,
|
||
deletePort,
|
||
getInstanceLog,
|
||
selectServer,
|
||
selectServerPlan,
|
||
getInstanceConsole,
|
||
getInstanceVolumeList,
|
||
addVolume,
|
||
deleteVolume,
|
||
updateVolume,
|
||
getInstanceList,
|
||
openInstance,
|
||
pauseInstance,
|
||
unpauseInstance,
|
||
deleteInstance,
|
||
getInstanceStatus
|
||
} from '@/utils/acs/server';
|
||
import {
|
||
Mirrorinfo,
|
||
getMirrorList
|
||
} from '@/utils/acs/mirror';
|
||
import * as echarts from 'echarts';
|
||
import {
|
||
getVirtualLog,
|
||
getVirtualAccessList,
|
||
createAccessControl,
|
||
deleteAccessControl,
|
||
getSnapshotList,
|
||
createSnapshot,
|
||
recoverSnapshot,
|
||
deleteSnapshot,
|
||
migrate_disk
|
||
} from '@/utils/acs/virtual';
|
||
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
const vmInfo = ref({});
|
||
const loading = ref(false);
|
||
|
||
// 缓存相关
|
||
const dataCache = ref(new Map()); // 缓存不同instance_id的数据
|
||
const currentInstanceId = ref(route.query.instance_id);
|
||
const isFromNavigation = ref(false); // 标记是否来自导航返回
|
||
|
||
// 缓存管理函数
|
||
const getCacheKey = (instanceId) => `vm_${instanceId}`;
|
||
|
||
const getCachedData = (instanceId) => {
|
||
const cacheKey = getCacheKey(instanceId);
|
||
return dataCache.value.get(cacheKey);
|
||
};
|
||
|
||
const setCachedData = (instanceId, data) => {
|
||
const cacheKey = getCacheKey(instanceId);
|
||
const cacheData = {
|
||
...data,
|
||
timestamp: Date.now(),
|
||
instanceId: instanceId
|
||
};
|
||
dataCache.value.set(cacheKey, cacheData);
|
||
console.log(`缓存虚拟机数据: ${instanceId}`, cacheData);
|
||
};
|
||
|
||
const isCacheValid = (cachedData, maxAge = 10 * 60 * 1000) => { // 默认10分钟有效期
|
||
if (!cachedData || !cachedData.timestamp) return false;
|
||
return (Date.now() - cachedData.timestamp) < maxAge;
|
||
};
|
||
|
||
const shouldUseCache = (instanceId) => {
|
||
// 检查是否来自列表页面的新进入
|
||
const fromSource = sessionStorage.getItem('vmDetailFrom');
|
||
const fromTimestamp = sessionStorage.getItem('vmDetailTimestamp');
|
||
|
||
// 如果是从列表页面新进入的,不使用缓存
|
||
if (fromSource === 'list' && fromTimestamp) {
|
||
const timeDiff = Date.now() - parseInt(fromTimestamp);
|
||
if (timeDiff < 2000) { // 2秒内的新进入
|
||
console.log('从列表页面新进入,不使用缓存');
|
||
// 清除标记
|
||
sessionStorage.removeItem('vmDetailFrom');
|
||
sessionStorage.removeItem('vmDetailTimestamp');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// 如果是相同的instance_id且来自导航返回,优先使用缓存
|
||
if (isFromNavigation.value && instanceId === currentInstanceId.value) {
|
||
const cachedData = getCachedData(instanceId);
|
||
const isValid = cachedData && isCacheValid(cachedData);
|
||
console.log(`缓存检查结果: instanceId=${instanceId}, isValid=${isValid}`);
|
||
return isValid;
|
||
}
|
||
|
||
return false;
|
||
};
|
||
|
||
// 保存当前数据到缓存
|
||
const saveDataToCache = (instanceId = null) => {
|
||
const targetInstanceId = instanceId || route.query.instance_id;
|
||
if (!targetInstanceId) return;
|
||
|
||
const dataToCache = {
|
||
vmInfo: { ...vmInfo.value },
|
||
logsList: [...logsList.value],
|
||
portsList: [...portsList.value],
|
||
networkRulesList: [...networkRulesList.value],
|
||
snapshotsList: [...snapshotsList.value],
|
||
dataVolumes: [...dataVolumes.value],
|
||
// 分页状态
|
||
logsPage: logsPage.value,
|
||
logsPageSize: logsPageSize.value,
|
||
portsPage: portsPage.value,
|
||
portsPageSize: portsPageSize.value,
|
||
networkRulesPage: networkRulesPage.value,
|
||
networkRulesPageSize: networkRulesPageSize.value,
|
||
// 总数
|
||
logsTotal: logsTotal.value,
|
||
portsTotal: portsTotal.value,
|
||
networkRulesTotal: networkRulesTotal.value,
|
||
volumesTotal: volumesTotal.value
|
||
};
|
||
|
||
setCachedData(targetInstanceId, dataToCache);
|
||
};
|
||
|
||
// 标签页相关
|
||
const activeTabName = ref('0'); // 默认选中第一个标签
|
||
|
||
// 处理标签页点击
|
||
const handleTabClick = (tab) => {
|
||
localStorage.setItem('vmDetailActiveTab', tab.index);
|
||
};
|
||
|
||
// 端口管理
|
||
const portsList = ref([]);
|
||
const portsLoading = ref(false);
|
||
const portsTotal = ref(0);
|
||
const portsPage = ref(1);
|
||
const portsPageSize = ref(10);
|
||
const showAddPortDialog = ref(false);
|
||
const portFormRef = ref(null);
|
||
const portForm = reactive({
|
||
internal_port: null,
|
||
});
|
||
const portRules = {
|
||
internal_port: [
|
||
{ required: true, message: '请输入内部端口', trigger: 'blur' },
|
||
]
|
||
};
|
||
const addingPort = ref(false);
|
||
|
||
// 日志管理
|
||
const logsList = ref([]);
|
||
const logsLoading = ref(false);
|
||
const logsTotal = ref(0);
|
||
const logsPage = ref(1);
|
||
const logsPageSize = ref(10);
|
||
|
||
// 监控相关
|
||
const monitorLoading = ref(false);
|
||
const monitorDateRange = ref([
|
||
new Date(new Date().getTime() - 24 * 60 * 60 * 1000), // 默认24小时前
|
||
new Date() // 当前时间
|
||
]);
|
||
const realTimeChart = ref(null);
|
||
let realTimeChartInstance = null;
|
||
|
||
// 最新的监控数据
|
||
const latestCpuUsage = ref(0);
|
||
const latestMemoryUsage = ref(0);
|
||
|
||
// 日期选择器快捷选项
|
||
const dateRangeShortcuts = [
|
||
{
|
||
text: '最近1小时',
|
||
value: () => {
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setTime(start.getTime() - 3600 * 1000);
|
||
return [start, end];
|
||
},
|
||
},
|
||
{
|
||
text: '最近6小时',
|
||
value: () => {
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setTime(start.getTime() - 6 * 3600 * 1000);
|
||
return [start, end];
|
||
},
|
||
},
|
||
{
|
||
text: '最近12小时',
|
||
value: () => {
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setTime(start.getTime() - 12 * 3600 * 1000);
|
||
return [start, end];
|
||
},
|
||
},
|
||
{
|
||
text: '最近24小时',
|
||
value: () => {
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setTime(start.getTime() - 24 * 3600 * 1000);
|
||
return [start, end];
|
||
},
|
||
},
|
||
];
|
||
|
||
// 访问控制相关
|
||
const accessControlList = ref([]);
|
||
const accessControlLoading = ref(false);
|
||
const accessControlTotal = ref(0);
|
||
const accessControlPage = ref(1);
|
||
const accessControlPageSize = ref(10);
|
||
const showAddAccessControlDialog = ref(false);
|
||
const accessControlForm = reactive({
|
||
source_ip: '',
|
||
description: ''
|
||
});
|
||
const accessControlFormRef = ref(null);
|
||
const accessControlRules = {
|
||
source_ip: [
|
||
{ required: true, message: '请输入IP地址', trigger: 'blur' },
|
||
{ pattern: /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/, message: 'IP地址格式不正确', trigger: 'blur' }
|
||
],
|
||
description: [
|
||
{ max: 100, message: '描述不能超过100个字符', trigger: 'blur' }
|
||
]
|
||
};
|
||
const addingAccessControl = ref(false);
|
||
|
||
// 网络规则管理
|
||
const networkRulesList = ref([]);
|
||
const networkRulesLoading = ref(false);
|
||
const networkRulesTotal = ref(0);
|
||
const networkRulesPage = ref(1);
|
||
const networkRulesPageSize = ref(10);
|
||
const showAddNetworkRuleDialog = ref(false);
|
||
const networkRuleForm = reactive({
|
||
direction: 'Inbound',
|
||
protocol: 'tcp',
|
||
local_port_range: '',
|
||
local_ip_address: '',
|
||
remote_port_range: '',
|
||
remote_ip_address: '',
|
||
action: 'Allow'
|
||
});
|
||
const networkRuleFormRef = ref(null);
|
||
const networkRuleRules = {
|
||
direction: [
|
||
{ required: true, message: '请选择网络方向', trigger: 'change' }
|
||
],
|
||
protocol: [
|
||
{ required: true, message: '请选择协议类型', trigger: 'change' }
|
||
],
|
||
local_port_range: [
|
||
{ required: true, message: '请输入本地端口范围', trigger: 'blur' }
|
||
],
|
||
local_ip_address: [
|
||
{ pattern: /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/, message: '本地IP地址格式不正确', trigger: 'blur' }
|
||
],
|
||
remote_port_range: [
|
||
{ required: true, message: '请输入远程端口范围', trigger: 'blur' }
|
||
],
|
||
remote_ip_address: [
|
||
{ pattern: /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/, message: '远程IP地址格式不正确', trigger: 'blur' }
|
||
]
|
||
};
|
||
const addingNetworkRule = ref(false);
|
||
|
||
// 确认密码
|
||
const confirmPassword = () => {
|
||
ElMessageBox.confirm('确认查看虚拟机密码?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
// 这里可以调用获取真实密码的API
|
||
ElMessage.success('密码已显示');
|
||
}).catch(() => {});
|
||
};
|
||
|
||
// 快照管理
|
||
const snapshotsList = ref([]);
|
||
const snapshotsLoading = ref(false);
|
||
|
||
// 数据卷
|
||
const dataVolumes = ref([
|
||
{ id: 1, name: '系统盘', size: 50, type: 'system', created_at: '2023-06-09 21:05:10' },
|
||
{ id: 2, name: '数据盘1', size: 100, type: 'data', created_at: '2023-06-10 13:22:45' }
|
||
]);
|
||
const volumesLoading = ref(false);
|
||
const volumesTotal = ref(0);
|
||
const volumesPage = ref(1);
|
||
const volumesPageSize = ref(10);
|
||
const showAddVolumeDialog = ref(false);
|
||
const showEditVolumeDialog = ref(false);
|
||
const showMigrateVolumeDialog = ref(false);
|
||
const currentVolumeToEdit = ref(null);
|
||
const volumeForm = reactive({
|
||
size: 10,
|
||
_sizeUnit: 'GB'
|
||
});
|
||
const volumeFormRef = ref(null);
|
||
const volumeRules = {
|
||
|
||
size: [
|
||
{ required: true, message: '请输入数据卷大小', trigger: 'blur' },
|
||
{ type: 'number', min: 1, max: 1000, message: '大小范围为1-1000GB', trigger: 'blur' }
|
||
]
|
||
};
|
||
const addingVolume = ref(false);
|
||
const editingVolume = ref(false);
|
||
const migratingVolume = ref(false);
|
||
|
||
const migrateForm = ref([]);
|
||
const migrateFormRef = ref(null);
|
||
const migrateRules = {
|
||
};
|
||
const serversList = ref([]);
|
||
|
||
// 重装系统
|
||
const showReinstallDialog = ref(false);
|
||
const reinstallFormRef = ref(null);
|
||
const reinstallForm = reactive({
|
||
image_id: ''
|
||
});
|
||
const reinstallRules = {
|
||
image_id: [
|
||
{ required: true, message: '请选择系统镜像', trigger: 'change' }
|
||
]
|
||
};
|
||
const reinstalling = ref(false);
|
||
const imagesList = ref([]);
|
||
|
||
//开通虚拟机
|
||
const handleOpen = async () => {
|
||
const res = await openInstance(route.query.instance_id)
|
||
console.log("开通虚拟机",res)
|
||
if (res.data.code === 200) {
|
||
ElMessage.success(res.data.msg);
|
||
} else {
|
||
ElMessage.error(res.data.msg);
|
||
}
|
||
}
|
||
|
||
// 获取镜像列表
|
||
const fetchImagesList = async () => {
|
||
try {
|
||
// 获取虚拟机信息中的server_id
|
||
const serverId = vmInfo.value.server_id;
|
||
if (!serverId) {
|
||
ElMessage.error('无法获取服务器ID');
|
||
return;
|
||
}
|
||
|
||
const res = await getMirrorList(serverId);
|
||
if (res && res.data && res.data.code === 200) {
|
||
imagesList.value = res.data.data || [];
|
||
} else {
|
||
ElMessage.error('获取镜像列表失败');
|
||
imagesList.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取镜像列表出错:', error);
|
||
ElMessage.error('获取镜像列表出错');
|
||
imagesList.value = [];
|
||
}
|
||
};
|
||
|
||
// 重装系统
|
||
const handleReinstall = () => {
|
||
fetchImagesList();
|
||
showReinstallDialog.value = true;
|
||
};
|
||
// 获取快照列表
|
||
const fetchSnapshotsList = async () => {
|
||
try {
|
||
const res = await getSnapshotList({instance_id:route.query.instance_id});
|
||
if (res && res.data && res.data.code === 200) {
|
||
snapshotsList.value = res.data.data || [];
|
||
} else {
|
||
ElMessage.error('获取快照列表失败');
|
||
snapshotsList.value = [];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取快照列表出错:', error);
|
||
ElMessage.error('获取快照列表出错');
|
||
}
|
||
};
|
||
|
||
|
||
// 提交重装系统
|
||
const submitReinstall = async () => {
|
||
if (!reinstallFormRef.value) return;
|
||
|
||
await reinstallFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
reinstalling.value = true;
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('image_id', reinstallForm.image_id);
|
||
|
||
const res = await reinstallI(formData, route.query.instance_id);
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success(res.data.data);
|
||
showReinstallDialog.value = false;
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error(res.data.message || '重装系统失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('重装系统出错:', error);
|
||
ElMessage.error('重装系统出错');
|
||
} finally {
|
||
reinstalling.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
const fetchDataVolumesList = async () => {
|
||
volumesLoading.value = true;
|
||
try {
|
||
const res = await getInstanceVolumeList({
|
||
instance_id: route.query.instance_id,
|
||
page: volumesPage.value,
|
||
count: volumesPageSize.value
|
||
});
|
||
console.log("获取数据卷列表",res)
|
||
if (res && res.data && res.data.code === 200) {
|
||
dataVolumes.value = res.data.data || [];
|
||
volumesTotal.value = res.data.count || 0;
|
||
} else {
|
||
ElMessage.error('获取数据卷列表失败');
|
||
dataVolumes.value = [];
|
||
volumesTotal.value = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取数据卷列表出错:', error);
|
||
ElMessage.error('获取数据卷列表出错');
|
||
dataVolumes.value = [];
|
||
volumesTotal.value = 0;
|
||
} finally {
|
||
volumesLoading.value = false;
|
||
}
|
||
};
|
||
const fetchInstanceStatus = async () => {
|
||
const res = await getInstanceStatus(route.query.instance_id);
|
||
console.log("获取虚拟机状态",res)
|
||
if (res && res.data && res.data.data.data.state === 200) {
|
||
vmInfo.value.state = res.data.data;
|
||
}
|
||
};
|
||
|
||
// 初始化数据
|
||
// 加载所有数据的统一函数
|
||
const loadAllData = async (instanceId = null, useCache = true) => {
|
||
const targetInstanceId = instanceId || route.query.instance_id;
|
||
|
||
// 检查是否使用缓存
|
||
if (useCache && shouldUseCache(targetInstanceId)) {
|
||
const cachedData = getCachedData(targetInstanceId);
|
||
console.log(`使用缓存数据加载所有信息: ${targetInstanceId}`);
|
||
|
||
// 从缓存恢复所有数据
|
||
vmInfo.value = cachedData.vmInfo || {};
|
||
logsList.value = cachedData.logsList || [];
|
||
portsList.value = cachedData.portsList || [];
|
||
networkRulesList.value = cachedData.networkRulesList || [];
|
||
snapshotsList.value = cachedData.snapshotsList || [];
|
||
dataVolumes.value = cachedData.dataVolumes || [];
|
||
|
||
// 恢复分页状态
|
||
if (cachedData.logsPage) logsPage.value = cachedData.logsPage;
|
||
if (cachedData.logsPageSize) logsPageSize.value = cachedData.logsPageSize;
|
||
if (cachedData.portsPage) portsPage.value = cachedData.portsPage;
|
||
if (cachedData.portsPageSize) portsPageSize.value = cachedData.portsPageSize;
|
||
if (cachedData.networkRulesPage) networkRulesPage.value = cachedData.networkRulesPage;
|
||
if (cachedData.networkRulesPageSize) networkRulesPageSize.value = cachedData.networkRulesPageSize;
|
||
|
||
// 恢复总数
|
||
if (cachedData.logsTotal) logsTotal.value = cachedData.logsTotal;
|
||
if (cachedData.portsTotal) portsTotal.value = cachedData.portsTotal;
|
||
if (cachedData.networkRulesTotal) networkRulesTotal.value = cachedData.networkRulesTotal;
|
||
if (cachedData.volumesTotal) volumesTotal.value = cachedData.volumesTotal;
|
||
|
||
// 重置loading状态
|
||
loading.value = false;
|
||
logsLoading.value = false;
|
||
portsLoading.value = false;
|
||
networkRulesLoading.value = false;
|
||
snapshotsLoading.value = false;
|
||
volumesLoading.value = false;
|
||
|
||
// 延迟初始化图表
|
||
setTimeout(() => {
|
||
initCharts();
|
||
fetchMonitorData();
|
||
}, 500);
|
||
|
||
return;
|
||
}
|
||
|
||
// 重新请求所有数据
|
||
console.log(`重新请求所有数据: ${targetInstanceId}`);
|
||
await Promise.all([
|
||
fetchVmInfo(targetInstanceId, false),
|
||
fetchPortsList(),
|
||
fetchLogsList(),
|
||
fetchAccessControlList(),
|
||
fetchNetworkRulesList(),
|
||
fetchSnapshotsList(),
|
||
fetchDataVolumesList(),
|
||
fetchInstanceStatus()
|
||
]);
|
||
|
||
// 延迟初始化图表,确保DOM已经渲染
|
||
setTimeout(() => {
|
||
initCharts();
|
||
fetchMonitorData();
|
||
}, 500);
|
||
|
||
// 保存到缓存
|
||
saveDataToCache(targetInstanceId);
|
||
};
|
||
|
||
// 监听路由参数变化
|
||
watch(() => route.query.instance_id, async (newInstanceId, oldInstanceId) => {
|
||
if (!newInstanceId) return;
|
||
|
||
// 保存旧数据到缓存(如果存在)
|
||
if (oldInstanceId && oldInstanceId !== newInstanceId) {
|
||
console.log(`保存旧虚拟机数据到缓存: ${oldInstanceId}`);
|
||
saveDataToCache(oldInstanceId);
|
||
}
|
||
|
||
// 检测是否是instance_id变化
|
||
const isInstanceIdChanged = newInstanceId !== currentInstanceId.value;
|
||
|
||
if (isInstanceIdChanged) {
|
||
console.log(`虚拟机ID变化: ${currentInstanceId.value} -> ${newInstanceId}`);
|
||
currentInstanceId.value = newInstanceId;
|
||
isFromNavigation.value = false; // 重置导航标记
|
||
|
||
// 加载新的虚拟机数据(不使用缓存)
|
||
await loadAllData(newInstanceId, false);
|
||
} else {
|
||
// 相同的instance_id,可能是从其他页面返回
|
||
isFromNavigation.value = true;
|
||
console.log(`返回相同虚拟机: ${newInstanceId}`);
|
||
|
||
// 尝试使用缓存
|
||
await loadAllData(newInstanceId, true);
|
||
}
|
||
}, { immediate: false });
|
||
|
||
onMounted(() => {
|
||
if (route.query.instance_id) {
|
||
// 恢复上次选中的标签页
|
||
const savedTab = localStorage.getItem('vmDetailActiveTab');
|
||
if (savedTab) {
|
||
activeTabName.value = savedTab;
|
||
}
|
||
|
||
// 设置当前instance_id
|
||
currentInstanceId.value = route.query.instance_id;
|
||
isFromNavigation.value = false;
|
||
|
||
// 加载数据
|
||
loadAllData();
|
||
} else {
|
||
ElMessage.error('缺少虚拟机ID参数');
|
||
goBack();
|
||
}
|
||
});
|
||
|
||
// 获取虚拟机信息
|
||
const fetchVmInfo = async (instanceId = null, useCache = true) => {
|
||
const targetInstanceId = instanceId || route.query.instance_id;
|
||
|
||
// 检查是否应该使用缓存
|
||
if (useCache && shouldUseCache(targetInstanceId)) {
|
||
const cachedData = getCachedData(targetInstanceId);
|
||
console.log(`使用缓存数据加载虚拟机: ${targetInstanceId}`);
|
||
|
||
// 从缓存恢复数据
|
||
vmInfo.value = cachedData.vmInfo || {};
|
||
logsList.value = cachedData.logsList || [];
|
||
portsList.value = cachedData.portsList || [];
|
||
networkRulesList.value = cachedData.networkRulesList || [];
|
||
snapshotsList.value = cachedData.snapshotsList || [];
|
||
dataVolumes.value = cachedData.dataVolumes || [];
|
||
|
||
// 重置loading状态
|
||
loading.value = false;
|
||
logsLoading.value = false;
|
||
portsLoading.value = false;
|
||
networkRulesLoading.value = false;
|
||
snapshotsLoading.value = false;
|
||
volumesLoading.value = false;
|
||
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
try {
|
||
console.log(`重新请求虚拟机数据: ${targetInstanceId}`);
|
||
|
||
const res = await getVmAdminContainer(targetInstanceId);
|
||
const serverRes = await selectServer({server_id:res.data.data.server_id})
|
||
const planRes = await selectServerPlan({plan_id:res.data.data.plan_id,server_type:"hyperV"})
|
||
const imageRes= await Mirrorinfo({image_id:res.data.data.image_id,server_type:"hyperV"})
|
||
|
||
// 如果端口列表已加载,获取第一个端口号
|
||
const firstPort = portsList.value.length > 0 ? portsList.value[0].node_port : null;
|
||
|
||
const data = {
|
||
...res.data.data,
|
||
server_name:serverRes.data.data.name,
|
||
server_ip:serverRes.data.data.server_ip,
|
||
plan_name:planRes.data.data.name,
|
||
image_name:imageRes.data.data.name,
|
||
node_port: res.data.data.node_port || firstPort // 优先使用API返回的,否则用端口列表的第一个
|
||
}
|
||
if (res && res.data && res.data.code === 200) {
|
||
vmInfo.value = data || {};
|
||
} else {
|
||
ElMessage.error('获取虚拟机信息失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取虚拟机信息出错:', error);
|
||
ElMessage.error('获取虚拟机信息出错');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取端口列表
|
||
const fetchPortsList = async () => {
|
||
portsLoading.value = true;
|
||
try {
|
||
const res = await getInstancePortList({
|
||
instance_id: route.query.instance_id,
|
||
page: portsPage.value,
|
||
count: portsPageSize.value
|
||
});
|
||
if (res && res.data && res.data.code === 200) {
|
||
portsList.value = res.data.data || [];
|
||
portsTotal.value = res.data.count || 0;
|
||
} else {
|
||
ElMessage.error('获取端口列表失败');
|
||
portsList.value = [];
|
||
portsTotal.value = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取端口列表出错:', error);
|
||
ElMessage.error('获取端口列表出错');
|
||
portsList.value = [];
|
||
portsTotal.value = 0;
|
||
} finally {
|
||
portsLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 获取日志列表
|
||
const fetchLogsList = async () => {
|
||
logsLoading.value = true;
|
||
try {
|
||
const res = await getInstanceLog(route.query.instance_id, {
|
||
page: logsPage.value,
|
||
count: logsPageSize.value
|
||
});
|
||
if (res && res.data && res.data.code === 200) {
|
||
logsList.value = res.data.data || [];
|
||
logsTotal.value = res.data.count || 0;
|
||
} else {
|
||
ElMessage.error('获取日志列表失败');
|
||
logsList.value = [];
|
||
logsTotal.value = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取日志列表出错:', error);
|
||
ElMessage.error('获取日志列表出错');
|
||
logsList.value = [];
|
||
logsTotal.value = 0;
|
||
} finally {
|
||
logsLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 处理端口分页
|
||
const handlePortsSizeChange = (size) => {
|
||
portsPageSize.value = size;
|
||
portsPage.value = 1;
|
||
fetchPortsList();
|
||
};
|
||
|
||
const handlePortsPageChange = (page) => {
|
||
portsPage.value = page;
|
||
fetchPortsList();
|
||
};
|
||
|
||
// 处理日志分页
|
||
const handleLogsSizeChange = (size) => {
|
||
logsPageSize.value = size;
|
||
logsPage.value = 1;
|
||
fetchLogsList();
|
||
};
|
||
|
||
const handleLogsPageChange = (page) => {
|
||
logsPage.value = page;
|
||
fetchLogsList();
|
||
};
|
||
|
||
// 添加端口
|
||
const submitAddPort = async () => {
|
||
if (!portFormRef.value) return;
|
||
|
||
await portFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
addingPort.value = true;
|
||
try {
|
||
const res = await addPort({
|
||
instance_id: route.query.instance_id,
|
||
internal_port: portForm.internal_port,
|
||
protocol: portForm.protocol
|
||
});
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('添加端口成功');
|
||
showAddPortDialog.value = false;
|
||
await fetchPortsList();
|
||
// 更新缓存
|
||
saveDataToCache();
|
||
} else {
|
||
ElMessage.error(res.data.message || '添加端口失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('添加端口出错:', error);
|
||
ElMessage.error('添加端口出错');
|
||
} finally {
|
||
addingPort.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除端口
|
||
const handleDeletePort = async (port) => {
|
||
try {
|
||
ElMessageBox.confirm('确定要删除该端口吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await deletePort({ port_forward_id: port.id });
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('删除成功');
|
||
await fetchPortsList();
|
||
// 更新缓存
|
||
saveDataToCache();
|
||
} else {
|
||
ElMessage.error(res.data.message || '删除失败');
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('删除端口出错:', error);
|
||
ElMessage.error('删除端口出错');
|
||
}
|
||
};
|
||
|
||
// 返回上一页
|
||
const goBack = () => {
|
||
// 标记这是返回操作,为了后续可能的缓存使用
|
||
sessionStorage.setItem('vmDetailFrom', 'back');
|
||
sessionStorage.setItem('vmDetailTimestamp', Date.now().toString());
|
||
router.go(-1);
|
||
};
|
||
|
||
// 刷新数据
|
||
const refreshData = () => {
|
||
const instanceId = route.query.instance_id;
|
||
console.log(`手动刷新数据,清除缓存: ${instanceId}`);
|
||
|
||
// 清除当前虚拟机的缓存
|
||
const cacheKey = getCacheKey(instanceId);
|
||
dataCache.value.delete(cacheKey);
|
||
|
||
// 重新加载数据(不使用缓存)
|
||
loadAllData(instanceId, false);
|
||
};
|
||
|
||
// 清除所有缓存
|
||
const clearAllCache = () => {
|
||
console.log('清除所有虚拟机缓存');
|
||
dataCache.value.clear();
|
||
ElMessage.success('已清除所有缓存');
|
||
};
|
||
|
||
// 启动虚拟机
|
||
const handleStart = async () => {
|
||
try {
|
||
const res = await startInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('启动指令已发送');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('启动失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
console.error('启动虚拟机出错:', error);
|
||
ElMessage.error('启动虚拟机出错');
|
||
}
|
||
};
|
||
|
||
// 停止虚拟机
|
||
const handleStop = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要停止该虚拟机吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await stopInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('停止指令已发送');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('停止失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('停止虚拟机出错:', error);
|
||
ElMessage.error('停止虚拟机出错');
|
||
}
|
||
};
|
||
|
||
// 重启虚拟机
|
||
const handleRestart = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要重启该虚拟机吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await restartInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('重启指令已发送');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('重启失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('重启虚拟机出错:', error);
|
||
ElMessage.error('重启虚拟机出错');
|
||
}
|
||
};
|
||
|
||
// 打开控制台
|
||
const handleConsole = async () => {
|
||
const res = await getInstanceConsole(route.query.instance_id)
|
||
console.log("打开控制台结果:",res)
|
||
if(res.data.code === 200){
|
||
let url = res.data.data.base_url + '/?token=' + res.data.data.data
|
||
window.open(url);
|
||
}
|
||
// window.open(`/console?instance_id=${route.query.instance_id}`, '_blank');
|
||
else{
|
||
ElMessage.error('打开控制台失败');
|
||
}
|
||
};
|
||
|
||
|
||
// 进入救援模式
|
||
const handleEnterRescue = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要进入救援模式吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await rescueInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('已进入救援模式');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('操作失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('进入救援模式出错:', error);
|
||
ElMessage.error('进入救援模式出错');
|
||
}
|
||
};
|
||
|
||
// 退出救援模式
|
||
const handleExitRescue = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要退出救援模式吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await exitRescueInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('已退出救援模式');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('操作失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('退出救援模式出错:', error);
|
||
ElMessage.error('退出救援模式出错');
|
||
}
|
||
};
|
||
|
||
// 暂停虚拟机
|
||
const handlePause = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要暂停该虚拟机吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const formData = new FormData();
|
||
const res = await pauseInstance(formData, route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('暂停指令已发送');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('暂停失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('暂停虚拟机出错:', error);
|
||
ElMessage.error('暂停虚拟机出错');
|
||
}
|
||
};
|
||
|
||
// 恢复虚拟机
|
||
const handleUnpause = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要恢复该虚拟机吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await unpauseInstance(route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('恢复指令已发送');
|
||
setTimeout(() => {
|
||
fetchVmInfo();
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('恢复失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('恢复虚拟机出错:', error);
|
||
ElMessage.error('恢复虚拟机出错');
|
||
}
|
||
};
|
||
|
||
// 删除虚拟机
|
||
const handleDelete = async () => {
|
||
try {
|
||
ElMessageBox.confirm('确定要删除该虚拟机吗?此操作不可恢复!', '警告', {
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'error',
|
||
dangerouslyUseHTMLString: true,
|
||
message: '<div style="color: red; font-weight: bold;">⚠️ 警告:删除虚拟机将永久删除所有数据,此操作不可恢复!</div>'
|
||
}).then(async () => {
|
||
const formData = new FormData();
|
||
const res = await deleteInstance(route.query.instance_id, formData);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('虚拟机已删除');
|
||
// 删除成功后返回虚拟机列表页面
|
||
setTimeout(() => {
|
||
router.push('/acs/nodes');
|
||
}, 2000);
|
||
} else {
|
||
ElMessage.error('删除失败: ' + (res.data.message || '未知错误'));
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('删除虚拟机出错:', error);
|
||
ElMessage.error('删除虚拟机出错');
|
||
}
|
||
};
|
||
|
||
function statusMap(data) {
|
||
const statusMap = {
|
||
0: "未支付",
|
||
1: "未创建",
|
||
2: "已启动",
|
||
3: "关机",
|
||
4: "重装中",
|
||
5: "正在创建快照",
|
||
6: "正在恢复快照",
|
||
7: "已暂停"
|
||
};
|
||
return statusMap[data] || "未知状态";
|
||
}
|
||
|
||
function volumeStatus(data) {
|
||
const statusMap = {
|
||
0: "未创建",
|
||
1: "未链接",
|
||
2: "已链接"
|
||
};
|
||
return statusMap[data] || "未知状态";
|
||
}
|
||
|
||
function statusColor(data) {
|
||
const statusMap = {
|
||
0: "danger", // 未支付
|
||
1: "warning", // 未创建
|
||
2: "success", // 已启动
|
||
3: "secondary", // 关机
|
||
4: "info", // 重装中
|
||
5: "primary", // 正在创建快照
|
||
6: "info", // 正在恢复快照
|
||
7: "warning" // 已暂停
|
||
};
|
||
return statusMap[data] || "default"; // 默认颜色类名
|
||
}
|
||
|
||
|
||
// 创建快照
|
||
const handleCreateSnapshot = () => {
|
||
ElMessageBox.prompt('请输入快照名称', '创建快照', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
inputPattern: /^.{1,50}$/,
|
||
inputErrorMessage: '快照名称不能为空且不超过50个字符'
|
||
}).then(async ({ value }) => {
|
||
// 这里调用创建快照API
|
||
const res = await createSnapshot({note:value},route.query.instance_id);
|
||
console.log("创建快照结果:",res)
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success(`快照 "${value}" 创建中,请稍后查看`);
|
||
} else {
|
||
ElMessage.error('创建快照失败');
|
||
}
|
||
}).catch(() => {});
|
||
};
|
||
|
||
// 恢复快照
|
||
const handleRestoreSnapshot = (snapshot) => {
|
||
ElMessageBox.confirm(`确定要恢复到快照 "${snapshot.note}" 吗?这将会覆盖当前虚拟机的所有数据!`, '警告', {
|
||
confirmButtonText: '确定恢复',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
// 这里调用恢复快照API
|
||
|
||
const res = await recoverSnapshot({snapshot_id:snapshot.id},route.query.instance_id)
|
||
console.log("恢复快照结果:",res)
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success(res.data.data);
|
||
fetchSnapshotsList();
|
||
} else {
|
||
ElMessage.error('恢复快照失败');
|
||
}
|
||
}).catch(() => {});
|
||
};
|
||
// 删除快照
|
||
const handleDeleteSnapshot = (snapshot) => {
|
||
ElMessageBox.confirm(`确定要删除快照 "${snapshot.note}" 吗?`, '警告', {
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await deleteSnapshot({snapshot_id:snapshot.id},route.query.instance_id)
|
||
console.log("删除快照结果:",res)
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('快照已删除');
|
||
fetchSnapshotsList();
|
||
} else {
|
||
ElMessage.error('删除快照失败');
|
||
}
|
||
}).catch(() => {});
|
||
};
|
||
|
||
|
||
//迁移数据卷
|
||
const outerVisible = async (id) => {
|
||
console.log("迁移数据卷id:",id)
|
||
const data = {
|
||
volume_id: id,
|
||
server_id: route.query.instance_id
|
||
}
|
||
try {
|
||
const res = await migrate_disk(data)
|
||
console.log("迁移数据卷结果:",res)
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('数据卷迁移成功');
|
||
showMigrateVolumeDialog.value = false;
|
||
fetchDataVolumesList();
|
||
} else {
|
||
ElMessage.error(res.data.msg || '迁移数据卷失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('迁移数据卷出错:', error);
|
||
ElMessage.error('迁移数据卷出错');
|
||
}
|
||
}
|
||
|
||
// 删除数据卷
|
||
const handleDeleteVolume = async (volume) => {
|
||
try {
|
||
ElMessageBox.confirm(`确定要删除 ${volume.size}GB 的数据卷吗?`, '警告', {
|
||
confirmButtonText: '确定删除',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await deleteVolume({volume_id:volume.id},route.query.instance_id);
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success(res.data.msg);
|
||
fetchDataVolumesList();
|
||
} else {
|
||
ElMessage.error(res.data.msg || '删除失败');
|
||
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('删除数据卷出错:', error);
|
||
ElMessage.error('删除数据卷出错');
|
||
}
|
||
};
|
||
|
||
// 获取状态类型
|
||
const getStatusType = (state) => {
|
||
if (state == 0 || state == 1) {
|
||
return 'info';
|
||
} else if (state == 2) {
|
||
return 'success';
|
||
} else if (state == 4 || state == 5 || state == 6) {
|
||
return 'warning';
|
||
} else if (state == 7) {
|
||
return 'info'; // 暂停状态
|
||
} else {
|
||
return 'danger';
|
||
}
|
||
};
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (state) => {
|
||
switch (state) {
|
||
case 0:
|
||
return '未支付';
|
||
case 1:
|
||
return '未创建';
|
||
case 2:
|
||
return '已启动';
|
||
case 3:
|
||
return '已关机';
|
||
case 4:
|
||
return '重装中';
|
||
case 5:
|
||
return '创建快照中';
|
||
case 6:
|
||
return '恢复快照中';
|
||
case 7:
|
||
return '已暂停';
|
||
default:
|
||
return '未知状态';
|
||
}
|
||
};
|
||
|
||
// 初始化图表
|
||
const initCharts = () => {
|
||
if (!realTimeChart.value) return;
|
||
|
||
// 初始化实时监控图表
|
||
realTimeChartInstance = echarts.init(realTimeChart.value);
|
||
const realTimeOption = {
|
||
title: {
|
||
text: '实时监控数据',
|
||
left: 'center',
|
||
textStyle: {
|
||
fontSize: 16,
|
||
color: '#303133'
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function (params) {
|
||
let result = params[0].axisValueLabel + '<br/>';
|
||
params.forEach(param => {
|
||
const unit = param.seriesName === 'CPU使用率' ? '%' : 'MB';
|
||
result += `${param.marker}${param.seriesName}: ${param.value}${unit}<br/>`;
|
||
});
|
||
return result;
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['CPU使用率', '内存使用量'],
|
||
top: '10%'
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: []
|
||
},
|
||
yAxis: [
|
||
{
|
||
type: 'value',
|
||
name: 'CPU使用率(%)',
|
||
position: 'left',
|
||
axisLabel: {
|
||
formatter: '{value}%'
|
||
}
|
||
},
|
||
{
|
||
type: 'value',
|
||
name: '内存使用量(MB)',
|
||
position: 'right',
|
||
axisLabel: {
|
||
formatter: '{value}MB'
|
||
}
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: 'CPU使用率',
|
||
type: 'line',
|
||
yAxisIndex: 0,
|
||
data: [],
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#409EFF'
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [{
|
||
offset: 0, color: 'rgba(64, 158, 255, 0.3)'
|
||
}, {
|
||
offset: 1, color: 'rgba(64, 158, 255, 0.1)'
|
||
}]
|
||
}
|
||
}
|
||
},
|
||
{
|
||
name: '内存使用量',
|
||
type: 'line',
|
||
yAxisIndex: 1,
|
||
data: [],
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#67C23A'
|
||
},
|
||
areaStyle: {
|
||
color: {
|
||
type: 'linear',
|
||
x: 0,
|
||
y: 0,
|
||
x2: 0,
|
||
y2: 1,
|
||
colorStops: [{
|
||
offset: 0, color: 'rgba(103, 194, 58, 0.3)'
|
||
}, {
|
||
offset: 1, color: 'rgba(103, 194, 58, 0.1)'
|
||
}]
|
||
}
|
||
}
|
||
}
|
||
]
|
||
};
|
||
realTimeChartInstance.setOption(realTimeOption);
|
||
};
|
||
|
||
|
||
const formatDateTime = (date) => {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
};
|
||
|
||
|
||
|
||
|
||
|
||
// 获取监控数据
|
||
const fetchMonitorData = async () => {
|
||
if (!monitorDateRange.value || !monitorDateRange.value[0] || !monitorDateRange.value[1]) return;
|
||
|
||
monitorLoading.value = true;
|
||
try {
|
||
// 将日期转换为 YYYY-MM-DD HH:MM:ss 格式
|
||
|
||
|
||
const startTime = formatDateTime(monitorDateRange.value[0]);
|
||
const endTime = formatDateTime(monitorDateRange.value[1]);
|
||
|
||
console.log('监控数据时间范围:', startTime, 'to', endTime);
|
||
|
||
const res = await getVirtualLog({
|
||
id: route.query.instance_id,
|
||
start_time: startTime,
|
||
end_time: endTime
|
||
});
|
||
console.log("获取监控数据",res)
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
const monitorData = res.data.data;
|
||
|
||
// 更新图表数据
|
||
updateCharts(monitorData);
|
||
} else {
|
||
ElMessage.error('获取监控数据失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('获取监控数据出错:', error);
|
||
ElMessage.error('获取监控数据出错');
|
||
} finally {
|
||
monitorLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 更新图表数据
|
||
const updateCharts = (data) => {
|
||
if (!data || !Array.isArray(data) || data.length === 0) return;
|
||
|
||
if (!realTimeChartInstance) return;
|
||
|
||
// 处理时间轴数据
|
||
const timeLabels = data.map(item => {
|
||
const date = new Date(item.time);
|
||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||
});
|
||
|
||
// 处理CPU数据
|
||
const cpuData = data.map(item => parseFloat(item.cpu_usage || 0).toFixed(2));
|
||
|
||
// 处理内存数据
|
||
const memoryData = data.map(item => parseInt(item.memory_usage || 0));
|
||
|
||
// 更新最新数据显示
|
||
if (data.length > 0) {
|
||
const latestData = data[data.length - 1];
|
||
latestCpuUsage.value = parseFloat(latestData.cpu_usage || 0).toFixed(2);
|
||
latestMemoryUsage.value = parseInt(latestData.memory_usage || 0);
|
||
}
|
||
|
||
// 更新图表
|
||
realTimeChartInstance.setOption({
|
||
xAxis: {
|
||
data: timeLabels
|
||
},
|
||
series: [
|
||
{
|
||
name: 'CPU使用率',
|
||
data: cpuData
|
||
},
|
||
{
|
||
name: '内存使用量',
|
||
data: memoryData
|
||
}
|
||
]
|
||
});
|
||
};
|
||
|
||
// 处理日期范围变化
|
||
const handleDateRangeChange = () => {
|
||
fetchMonitorData();
|
||
};
|
||
|
||
// 获取访问控制列表
|
||
const fetchAccessControlList = async () => {
|
||
accessControlLoading.value = true;
|
||
try {
|
||
const res = await getVirtualAccessList({
|
||
instance_id: route.query.instance_id,
|
||
page: accessControlPage.value,
|
||
count: accessControlPageSize.value
|
||
});
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
accessControlList.value = res.data.data || [];
|
||
accessControlTotal.value = res.data.count || 0;
|
||
} else {
|
||
ElMessage.error('获取访问控制列表失败');
|
||
accessControlList.value = [];
|
||
accessControlTotal.value = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取访问控制列表出错:', error);
|
||
ElMessage.error('获取访问控制列表出错');
|
||
accessControlList.value = [];
|
||
accessControlTotal.value = 0;
|
||
} finally {
|
||
accessControlLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 处理访问控制分页
|
||
const handleAccessControlSizeChange = (size) => {
|
||
accessControlPageSize.value = size;
|
||
accessControlPage.value = 1;
|
||
fetchAccessControlList();
|
||
};
|
||
|
||
const handleAccessControlPageChange = (page) => {
|
||
accessControlPage.value = page;
|
||
fetchAccessControlList();
|
||
};
|
||
|
||
// 添加访问控制
|
||
const submitAddAccessControl = async () => {
|
||
if (!accessControlFormRef.value) return;
|
||
|
||
await accessControlFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
addingAccessControl.value = true;
|
||
try {
|
||
const res = await createAccessControl({
|
||
instance_id: route.query.instance_id,
|
||
source_ip: accessControlForm.source_ip,
|
||
description: accessControlForm.description
|
||
});
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('添加访问控制成功');
|
||
showAddAccessControlDialog.value = false;
|
||
fetchAccessControlList();
|
||
// 重置表单
|
||
accessControlForm.source_ip = '';
|
||
accessControlForm.description = '';
|
||
} else {
|
||
ElMessage.error(res.data.message || '添加访问控制失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('添加访问控制出错:', error);
|
||
ElMessage.error('添加访问控制出错');
|
||
} finally {
|
||
addingAccessControl.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除访问控制
|
||
const handleDeleteAccessControl = async (accessControl) => {
|
||
try {
|
||
ElMessageBox.confirm('确定要删除该访问控制规则吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await deleteAccessControl({ access_control_id: accessControl.id });
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('删除成功');
|
||
fetchAccessControlList();
|
||
} else {
|
||
ElMessage.error(res.data.message || '删除失败');
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('删除访问控制出错:', error);
|
||
ElMessage.error('删除访问控制出错');
|
||
}
|
||
};
|
||
|
||
// 网络规则管理
|
||
const fetchNetworkRulesList = async () => {
|
||
networkRulesLoading.value = true;
|
||
try {
|
||
const res = await getVirtualAccessList({
|
||
instance_id: route.query.instance_id,
|
||
page: networkRulesPage.value,
|
||
count: networkRulesPageSize.value
|
||
});
|
||
if (res && res.data && res.data.code === 200) {
|
||
networkRulesList.value = res.data.data || [];
|
||
networkRulesTotal.value = res.data.count || 0;
|
||
} else {
|
||
ElMessage.error('获取网络规则列表失败');
|
||
networkRulesList.value = [];
|
||
networkRulesTotal.value = 0;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取网络规则列表出错:', error);
|
||
ElMessage.error('获取网络规则列表出错');
|
||
networkRulesList.value = [];
|
||
networkRulesTotal.value = 0;
|
||
} finally {
|
||
networkRulesLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 处理网络规则分页
|
||
const handleNetworkRulesSizeChange = (size) => {
|
||
networkRulesPageSize.value = size;
|
||
networkRulesPage.value = 1;
|
||
fetchNetworkRulesList();
|
||
};
|
||
|
||
const handleNetworkRulesPageChange = (page) => {
|
||
networkRulesPage.value = page;
|
||
fetchNetworkRulesList();
|
||
};
|
||
|
||
// 添加网络规则
|
||
const submitAddNetworkRule = async () => {
|
||
if (!networkRuleFormRef.value) return;
|
||
|
||
await networkRuleFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
addingNetworkRule.value = true;
|
||
try {
|
||
const res = await createAccessControl({
|
||
instance_id: route.query.instance_id,
|
||
direction: networkRuleForm.direction,
|
||
protocol: networkRuleForm.protocol,
|
||
local_port_range: networkRuleForm.local_port_range,
|
||
local_ip_address: networkRuleForm.local_ip_address,
|
||
remote_port_range: networkRuleForm.remote_port_range,
|
||
remote_ip_address: networkRuleForm.remote_ip_address,
|
||
action: networkRuleForm.action
|
||
});
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('添加网络规则成功');
|
||
showAddNetworkRuleDialog.value = false;
|
||
fetchNetworkRulesList();
|
||
// 重置表单
|
||
networkRuleForm.direction = 'Inbound';
|
||
networkRuleForm.protocol = 'tcp';
|
||
networkRuleForm.local_port_range = '';
|
||
networkRuleForm.local_ip_address = '';
|
||
networkRuleForm.remote_port_range = '';
|
||
networkRuleForm.remote_ip_address = '';
|
||
networkRuleForm.action = 'Allow';
|
||
} else {
|
||
ElMessage.error(res.data.message || '添加网络规则失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('添加网络规则出错:', error);
|
||
ElMessage.error('添加网络规则出错');
|
||
} finally {
|
||
addingNetworkRule.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 删除网络规则
|
||
const handleDeleteNetworkRule = async (rule) => {
|
||
try {
|
||
ElMessageBox.confirm(`确定要删除网络规则 ${rule.id} 吗?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(async () => {
|
||
const res = await deleteAccessControl({ access_control_id: rule.id });
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('删除成功');
|
||
fetchNetworkRulesList();
|
||
} else {
|
||
ElMessage.error(res.data.message || '删除失败');
|
||
}
|
||
}).catch(() => {});
|
||
} catch (error) {
|
||
console.error('删除网络规则出错:', error);
|
||
ElMessage.error('删除网络规则出错');
|
||
}
|
||
};
|
||
|
||
// 添加数据卷
|
||
const handleAddVolume = () => {
|
||
showAddVolumeDialog.value = true;
|
||
// 重置表单
|
||
volumeForm.size = 10;
|
||
volumeForm._sizeUnit = 'GB';
|
||
};
|
||
|
||
// 编辑数据卷
|
||
const handleEditVolume = (volume) => {
|
||
currentVolumeToEdit.value = volume;
|
||
// 填充表单
|
||
volumeForm.size = volume.size;
|
||
volumeForm._sizeUnit = 'GB';
|
||
showEditVolumeDialog.value = true;
|
||
};
|
||
|
||
// 迁移数据卷
|
||
const handleMigrateVolume = (volume) => {
|
||
console.log("迁移数据卷",volume)
|
||
// 获取服务器列表
|
||
fetchServersList();
|
||
// 重置表单
|
||
showMigrateVolumeDialog.value = true;
|
||
};
|
||
|
||
let user_id = ref(null)
|
||
|
||
// 提交添加数据卷
|
||
const submitAddVolume = async () => {
|
||
if (!volumeFormRef.value) return;
|
||
const userInfoRes = await getUserInfo();
|
||
console.log("获取用户信息",userInfoRes)
|
||
user_id.value = userInfoRes.data.user_id;
|
||
console.log("user_id",user_id.value)
|
||
console.log("size",volumeForm.size)
|
||
await volumeFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
addingVolume.value = true;
|
||
try {
|
||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||
const res = await addVolume({
|
||
instance_id: route.query.instance_id,
|
||
size: String(sizeGb),
|
||
user_id: user_id.value
|
||
});
|
||
console.log("添加数据卷112",res)
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('添加数据卷成功');
|
||
showAddVolumeDialog.value = false;
|
||
fetchDataVolumesList();
|
||
} else {
|
||
ElMessage.error(res.data.data.msg || '添加数据卷失败');
|
||
showAddVolumeDialog.value = false;
|
||
}
|
||
} catch (error) {
|
||
console.error('添加数据卷出错:', error);
|
||
ElMessage.error('添加数据卷出错');
|
||
} finally {
|
||
addingVolume.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 提交编辑数据卷
|
||
const submitEditVolume = async () => {
|
||
if (!volumeFormRef.value) return;
|
||
|
||
await volumeFormRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
editingVolume.value = true;
|
||
try {
|
||
// 这里应该调用修改数据卷的API
|
||
const sizeGb = volumeForm._sizeUnit === 'TB' ? volumeForm.size * 1024 : volumeForm.size
|
||
const res = await updateVolume({
|
||
volume_id: currentVolumeToEdit.value.id,
|
||
size: sizeGb
|
||
});
|
||
console.log("编辑数据卷数据:",res)
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElMessage.success('编辑数据卷成功');
|
||
showEditVolumeDialog.value = false;
|
||
fetchDataVolumesList();
|
||
} else {
|
||
ElMessage.error(res.data.msg || '编辑数据卷失败');
|
||
showEditVolumeDialog.value = false;
|
||
}
|
||
} catch (error) {
|
||
console.error('编辑数据卷出错:', error);
|
||
ElMessage.error('编辑数据卷出错');
|
||
} finally {
|
||
editingVolume.value = false;
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 调整图表大小
|
||
const resizeCharts = () => {
|
||
realTimeChartInstance && realTimeChartInstance.resize();
|
||
};
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', resizeCharts);
|
||
|
||
// 组件卸载前清理
|
||
onBeforeUnmount(() => {
|
||
// 保存当前数据到缓存
|
||
if (route.query.instance_id) {
|
||
console.log(`组件卸载,保存数据到缓存: ${route.query.instance_id}`);
|
||
saveDataToCache();
|
||
}
|
||
|
||
// 移除事件监听
|
||
window.removeEventListener('resize', resizeCharts);
|
||
|
||
// 销毁图表实例
|
||
realTimeChartInstance && realTimeChartInstance.dispose();
|
||
});
|
||
|
||
// 获取服务器列表
|
||
const fetchServersList = async () => {
|
||
try {
|
||
const userInfoRes = await getUserInfo();
|
||
user_id.value = userInfoRes.data.user_id;
|
||
|
||
const data = {
|
||
server_id: route.query.instance_id,
|
||
user_id: user_id.value,
|
||
page: 1,
|
||
count: 10
|
||
}
|
||
const res = await getInstanceList(data)
|
||
console.log("获取服务器列表123",res)
|
||
if(res && res.data && res.data.code === 200){
|
||
migrateForm.value = res.data.data
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('获取服务器列表出错:', error);
|
||
ElMessage.error('获取服务器列表出错');
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.vm-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);
|
||
}
|
||
|
||
/* 虚拟机信息卡片 */
|
||
.vm-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;
|
||
}
|
||
|
||
.password-field {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.password-field span {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 操作按钮 */
|
||
.action-buttons {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
/* 主要内容区域 */
|
||
.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;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media screen and (max-width: 1200px) {
|
||
.vm-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;
|
||
}
|
||
|
||
.vm-info {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.info-card.system-info {
|
||
grid-column: auto;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.action-buttons .el-button {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* 监控图表相关样式 */
|
||
.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;
|
||
}
|
||
|
||
.monitor-stats {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-item {
|
||
padding: 4px 12px;
|
||
background-color: #f0f2f5;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #606266;
|
||
}
|
||
|
||
.chart {
|
||
height: 300px;
|
||
}
|
||
|
||
/* 网络管理相关样式 */
|
||
.tab-subtitle {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||
.unit-select { width: 90px; flex-shrink: 0; }
|
||
</style>
|