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

2788 lines
82 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>