fix: 提交修改
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<div class="name-row">
|
||||
<h2 class="vm-name">{{ vm?.name || userGoods.good?.name || `用户虚拟机 #${userGoodsId}` }}</h2>
|
||||
<el-tag v-if="vm?.status" :type="vmStatusType(vm.status)" size="small" style="margin-left:8px">{{ vmStatusLabel(vm.status) }}</el-tag>
|
||||
<el-tag v-if="vm?.rescue" size="small" type="danger" effect="dark" style="margin-left:4px">救援模式</el-tag>
|
||||
<el-tag v-if="userGoods.tag" size="small" type="info" style="margin-left:4px">{{ userGoods.tag }}</el-tag>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
@@ -43,8 +44,8 @@
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="suspend">暂停</el-dropdown-item>
|
||||
<el-dropdown-item command="resume">恢复</el-dropdown-item>
|
||||
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
|
||||
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
|
||||
<el-dropdown-item command="rescue" :disabled="!!vm?.rescue">救援模式</el-dropdown-item>
|
||||
<el-dropdown-item command="exitRescue" :disabled="!vm?.rescue">退出救援</el-dropdown-item>
|
||||
<el-dropdown-item divided command="rebuild">重装系统</el-dropdown-item>
|
||||
<el-dropdown-item command="updateVm">编辑虚拟机</el-dropdown-item>
|
||||
<el-dropdown-item command="refactorVm">重构虚拟机</el-dropdown-item>
|
||||
@@ -68,13 +69,57 @@
|
||||
<div class="config-cell"><span class="config-label">上行带宽</span><span class="config-value">{{ vm.tx_bandwidth || 0 }} Mbps</span></div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-cell"><span class="config-label">SSH端口</span><span class="config-value">{{ vm.ssh_port || 22 }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">流量上限</span><span class="config-value">{{ formatTraffic(vm.traffic_max) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">IP</span><span class="config-value" style="color:#165dff;font-weight:500">{{ vm.ips || '-' }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">续费价格</span><span class="config-value">¥{{ (userGoods.renewPrice / 100).toFixed(2) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">用户名</span><span class="config-value" style="font-weight:500">{{ isWindows ? 'Administrator' : 'root' }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">远程端口</span><span class="config-value">{{ isWindows ? (vm.ssh_port || 3389) : (vm.ssh_port || 22) }}</span></div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">外网IP</span>
|
||||
<span class="config-value" style="color:#165dff;font-weight:500" v-if="vmPublicIpList.length">
|
||||
{{ vmPublicIpList[0] }}
|
||||
<el-popover v-if="vmPublicIpList.length > 1" trigger="hover" placement="bottom-start" :width="280">
|
||||
<template #reference>
|
||||
<el-tag size="small" type="primary" style="margin-left:4px;cursor:pointer;vertical-align:middle">+{{ vmPublicIpList.length - 1 }}</el-tag>
|
||||
</template>
|
||||
<div class="ip-popover-list">
|
||||
<div v-for="(ip, idx) in vmPublicIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</span>
|
||||
<span class="config-value" v-else>-</span>
|
||||
</div>
|
||||
<div class="config-cell">
|
||||
<span class="config-label">内网IP</span>
|
||||
<span class="config-value" style="color:#67c23a;font-weight:500" v-if="vmPrivateIpList.length">
|
||||
{{ vmPrivateIpList[0] }}
|
||||
<el-popover v-if="vmPrivateIpList.length > 1" trigger="hover" placement="bottom-start" :width="280">
|
||||
<template #reference>
|
||||
<el-tag size="small" type="success" style="margin-left:4px;cursor:pointer;vertical-align:middle">+{{ vmPrivateIpList.length - 1 }}</el-tag>
|
||||
</template>
|
||||
<div class="ip-popover-list">
|
||||
<div v-for="(ip, idx) in vmPrivateIpList" :key="idx" class="ip-popover-item">{{ ip }}</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
</span>
|
||||
<span class="config-value" v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-cell">
|
||||
<span class="config-label">Root密码</span>
|
||||
<span class="config-value pwd-value">
|
||||
<code class="pwd-text">{{ showPassword ? (vm.root_password || '-') : '••••••••' }}</code>
|
||||
<el-button link size="small" @click="showPassword = !showPassword" class="pwd-btn">
|
||||
<el-icon :size="14"><View v-if="!showPassword" /><Hide v-else /></el-icon>
|
||||
</el-button>
|
||||
<el-button link size="small" type="primary" @click="copyPassword" class="pwd-btn">
|
||||
<el-icon :size="14"><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-cell"><span class="config-label">流量上限</span><span class="config-value">{{ formatTraffic(vm.traffic_max) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">续费价格</span><span class="config-value">¥{{ (userGoods.renewPrice / 100).toFixed(2) }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">基础价格</span><span class="config-value">¥{{ (userGoods.basePrice / 100).toFixed(2) }}</span></div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-cell"><span class="config-label">商品</span><span class="config-value">{{ userGoods.good?.name || '-' }}</span></div>
|
||||
<div class="config-cell"><span class="config-label">备注</span><span class="config-value">{{ userGoods.note || '-' }}</span></div>
|
||||
<div class="config-cell">
|
||||
@@ -84,8 +129,6 @@
|
||||
<span v-else style="color:#c0c4cc">未绑定</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<div class="config-cell">
|
||||
<span class="config-label">出站安全组</span>
|
||||
<span class="config-value">
|
||||
@@ -93,7 +136,7 @@
|
||||
<span v-else style="color:#c0c4cc">未绑定</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="config-cell" style="flex:3" v-if="vmImage">
|
||||
<div class="config-cell" v-if="vmImage">
|
||||
<span class="config-label">镜像</span>
|
||||
<span class="config-value">{{ vmImage.name }} <el-tag size="small" :type="vmImage.os_type === 'linux' ? 'success' : 'primary'" style="margin-left:4px">{{ vmImage.os_type }}</el-tag></span>
|
||||
</div>
|
||||
@@ -229,6 +272,7 @@
|
||||
<!-- 网络 -->
|
||||
<el-tab-pane v-if="isVmGoods" label="网络" name="network">
|
||||
<div class="tab-toolbar">
|
||||
<el-button size="small" type="primary" @click="showBindNetworkSelector = true">绑定网络</el-button>
|
||||
<el-button size="small" :icon="Refresh" @click="loadDetail">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="vmNetworks" stripe size="small">
|
||||
@@ -286,6 +330,79 @@
|
||||
@size-change="s => { networkingPageSize = s; networkingPage = 1; loadNetworkings() }" @current-change="p => { networkingPage = p; loadNetworkings() }" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 监控 -->
|
||||
<el-tab-pane v-if="isVmGoods" label="监控" name="monitor">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">监控指标</h3>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<el-select v-model="monitorInterval" size="small" style="width: 120px" @change="loadMetricsHistory">
|
||||
<el-option label="1分钟" value="1m" />
|
||||
<el-option label="5分钟" value="5m" />
|
||||
<el-option label="1小时" value="1h" />
|
||||
</el-select>
|
||||
<el-button size="small" :icon="Refresh" @click="loadMetricsHistory" :loading="metricsLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="latestMetrics">
|
||||
<div class="metric-summary-row">
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">CPU 使用率</div>
|
||||
<div class="metric-summary-value">{{ latestMetrics.cpu_usage?.toFixed(1) }}%</div>
|
||||
<div class="metric-summary-sub"> </div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">内存</div>
|
||||
<div class="metric-summary-value">{{ vmMemPercent(latestMetrics) }}%</div>
|
||||
<div class="metric-summary-sub">{{ formatMemKB(latestMetrics.mem_used) }} / {{ formatMemKB(latestMetrics.mem_total) }}</div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">磁盘 I/O</div>
|
||||
<div class="metric-summary-value">读 {{ formatBytesRaw(latestMetrics.disk_read) }}</div>
|
||||
<div class="metric-summary-sub">写 {{ formatBytesRaw(latestMetrics.disk_write) }}</div>
|
||||
</div>
|
||||
<div class="metric-summary-card">
|
||||
<div class="metric-summary-label">网络流量</div>
|
||||
<div class="metric-summary-value">↓{{ formatNetLabel(latestMetrics.net_rx) }}</div>
|
||||
<div class="metric-summary-sub">↑{{ formatNetLabel(latestMetrics.net_tx) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="metricsData">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> CPU 使用率</span></template>
|
||||
<div ref="cpuChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> 内存使用率</span></template>
|
||||
<div ref="memChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16" style="margin-top: 16px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> 磁盘 I/O</span></template>
|
||||
<div ref="diskChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="hover" class="metrics-card">
|
||||
<template #header><span class="metrics-title"><el-icon><Monitor /></el-icon> 网络流量</span></template>
|
||||
<div ref="netChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-empty v-else-if="!metricsLoading" description="暂无监控数据" :image-size="80" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -302,7 +419,7 @@
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="vncVisible = false">关闭</el-button>
|
||||
<el-button v-if="vncResult?.url" type="primary" @click="window.open(vncResult.url, '_blank')">打开连接</el-button>
|
||||
<el-button v-if="vncResult?.url" type="primary" @click="openVncUrl">打开连接</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@@ -328,7 +445,12 @@
|
||||
<el-dialog v-model="volCreateVisible" title="创建数据卷" width="440px" destroy-on-close>
|
||||
<el-form :model="volCreateForm" label-width="100px">
|
||||
<el-form-item label="名称" required><el-input v-model="volCreateForm.name" /></el-form-item>
|
||||
<el-form-item label="大小(GB)"><el-input-number v-model="volCreateForm.size" :min="1" controls-position="right" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="大小">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volCreateForm.size" :min="1" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">GB</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标设备名"><el-input v-model="volCreateForm.target_device" placeholder="不填自动生成" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -341,7 +463,12 @@
|
||||
<el-dialog v-model="volResizeVisible" title="扩容数据卷" width="400px" destroy-on-close>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="当前大小">{{ volResizeTarget?.size || 0 }} GB</el-form-item>
|
||||
<el-form-item label="新大小(GB)"><el-input-number v-model="volNewSize" :min="volResizeTarget?.size || 1" controls-position="right" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="新大小">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="volNewSize" :min="volResizeTarget?.size || 1" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">GB</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="volResizeVisible = false">取消</el-button>
|
||||
@@ -394,8 +521,8 @@
|
||||
<!-- 创建组网 -->
|
||||
<el-dialog v-model="networkingCreateVisible" title="创建组网" width="440px" destroy-on-close>
|
||||
<el-form :model="networkingCreateForm" label-width="90px">
|
||||
<el-form-item label="名称" required><el-input v-model="networkingCreateForm.name" /></el-form-item>
|
||||
<el-form-item label="网桥名称" required><el-input v-model="networkingCreateForm.bridge_name" /></el-form-item>
|
||||
<el-form-item label="名称"><el-input v-model="networkingCreateForm.name" placeholder="不填则随机生成" /></el-form-item>
|
||||
<el-form-item label="网桥名称"><el-input v-model="networkingCreateForm.bridge_name" placeholder="不填则随机生成" /></el-form-item>
|
||||
<el-form-item label="网关"><el-input v-model="networkingCreateForm.gateway" placeholder="可选" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="networkingCreateForm.description" /></el-form-item>
|
||||
</el-form>
|
||||
@@ -462,10 +589,31 @@
|
||||
|
||||
<!-- 修改带宽 -->
|
||||
<el-dialog v-model="trafficVisible" title="修改带宽" width="440px" destroy-on-close>
|
||||
<el-form :model="trafficForm" label-width="130px">
|
||||
<el-form-item label="下行带宽(Mbps)"><el-input-number v-model="trafficForm.rx_bandwidth" :min="0" controls-position="right" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="上行带宽(Mbps)"><el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="流量上限(GB)"><el-input-number v-model="trafficForm._trafficGB" :min="0" :precision="2" controls-position="right" style="width:100%" /></el-form-item>
|
||||
<el-form :model="trafficForm" label-width="100px">
|
||||
<el-form-item label="下行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="trafficForm.rx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="trafficForm._rxUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="上行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="trafficForm.tx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="trafficForm._txUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="流量上限">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="trafficForm._trafficValue" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="trafficForm._trafficUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="MB" value="MB" /><el-option label="GB" value="GB" /><el-option label="TB" value="TB" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="trafficVisible = false">取消</el-button>
|
||||
@@ -602,17 +750,27 @@
|
||||
@confirm="vol => handleMountVolume(vol)" />
|
||||
|
||||
<!-- 编辑虚拟机弹窗(对接 /user_vm/update) -->
|
||||
<el-dialog v-model="editVmVisible" title="编辑虚拟机配置" width="560px" destroy-on-close class="scrollable-dialog">
|
||||
<el-form :model="editVmForm" label-width="130px" v-loading="editVmLoading">
|
||||
<el-dialog v-model="editVmVisible" title="编辑虚拟机配置" width="680px" destroy-on-close class="scrollable-dialog">
|
||||
<el-form :model="editVmForm" label-width="90px" v-loading="editVmLoading">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下行带宽(Mbps)">
|
||||
<el-input-number v-model="editVmForm.rx_bandwidth" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="下行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editVmForm.rx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="editVmForm._rxUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上行带宽(Mbps)">
|
||||
<el-input-number v-model="editVmForm.tx_bandwidth" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="上行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editVmForm.tx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="editVmForm._txUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -666,30 +824,48 @@
|
||||
@confirm="net => { editVmForm.internet_network_id = net.id; editVmForm._networkName = net.name }" />
|
||||
|
||||
<!-- 重构虚拟机弹窗 -->
|
||||
<el-dialog v-model="refactorVisible" title="重构虚拟机" width="640px" destroy-on-close class="scrollable-dialog">
|
||||
<el-dialog v-model="refactorVisible" title="重构虚拟机" width="720px" destroy-on-close class="scrollable-dialog">
|
||||
<el-alert type="warning" :closable="false" style="margin-bottom:12px">重构会修改虚拟机底层配置,请谨慎操作!</el-alert>
|
||||
<el-form :model="refactorForm" label-width="120px">
|
||||
<el-form :model="refactorForm" label-width="90px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="内存(MB)">
|
||||
<el-input-number v-model="refactorForm._memoryMB" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="内存">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="refactorForm._memoryValue" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="refactorForm._memoryUnit" class="unit-select" style="width:85px">
|
||||
<el-option label="MB" value="MB" /><el-option label="GB" value="GB" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="vCPU">
|
||||
<el-input-number v-model="refactorForm.vcpu" :min="0" controls-position="right" style="width:100%" />
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="refactorForm.vcpu" :min="0" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">核</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下行带宽(Mbps)">
|
||||
<el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="下行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="refactorForm._rxUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="上行带宽(Mbps)">
|
||||
<el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="上行带宽">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" style="flex:1" />
|
||||
<el-select v-model="refactorForm._txUnit" class="unit-select" style="width:100px">
|
||||
<el-option label="Mbps" value="Mbps" /><el-option label="Gbps" value="Gbps" />
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -732,14 +908,24 @@
|
||||
<UserVmNetworkSelector v-model="showRefactorNetSelector" :user-goods-id="userGoodsId"
|
||||
@confirm="net => { refactorForm.internet_network_id = net.id; refactorForm._networkName = net.name }" />
|
||||
|
||||
<!-- 绑定网络选择器 -->
|
||||
<UserVmNetworkSelector v-model="showBindNetworkSelector" :user-goods-id="userGoodsId"
|
||||
:show-create-button="false" @confirm="handleBindNetworkConfirm" />
|
||||
|
||||
<!-- 编辑商品信息弹窗 --> <el-dialog v-model="editGoodsVisible" title="编辑商品信息" width="480px" destroy-on-close>
|
||||
<el-form :model="editGoodsForm" label-width="110px">
|
||||
<el-form-item label="备注"><el-input v-model="editGoodsForm.note" /></el-form-item>
|
||||
<el-form-item label="续费价格(元)">
|
||||
<el-input-number v-model="editGoodsForm.renew_price" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="续费价格">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editGoodsForm.renew_price" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="基础价格(元)">
|
||||
<el-input-number v-model="editGoodsForm.base_price" :min="0" :precision="2" controls-position="right" style="width:100%" />
|
||||
<el-form-item label="基础价格">
|
||||
<div class="unit-input-row">
|
||||
<el-input-number v-model="editGoodsForm.base_price" :min="0" :precision="2" controls-position="right" style="flex:1" />
|
||||
<span class="unit-text">元</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="到期时间">
|
||||
<el-date-picker v-model="editGoodsForm.expire_time" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
@@ -773,10 +959,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled } from '@element-plus/icons-vue'
|
||||
import { ArrowLeft, Refresh, ArrowDown, Monitor, WarningFilled, View, Hide, CopyDocument } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getUserVmDetail, getUserVmVnc, getUserVmHostImages,
|
||||
startUserVm, stopUserVm, rebootUserVm, suspendUserVm, resumeUserVm, rescueUserVm, exitRescueUserVm, rebuildUserVm, deleteUserVm,
|
||||
@@ -788,7 +974,8 @@ import {
|
||||
createUserVmPostGroupRule, updateUserVmPostGroupRule, deleteUserVmPostGroupRule,
|
||||
getUserVmPostGroupDetail,
|
||||
getUserVmNetworkList, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
|
||||
getUserGoodsDetail
|
||||
getUserGoodsDetail,
|
||||
getUserVmMetricsHistory
|
||||
} from '@/api/admin/userVm'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
import { vmStatusLabel as vmStatusLabelUtil, vmStatusType as vmStatusTypeUtil, volumeStatusLabel, volumeStatusType } from '@/utils/tool'
|
||||
@@ -797,10 +984,11 @@ import UserVmSecurityGroupSelector from '@/components/admin/UserVmSecurityGroupS
|
||||
import UserVmVolumeSelector from '@/components/admin/UserVmVolumeSelector.vue'
|
||||
import UserVmNetworkSelector from '@/components/admin/UserVmNetworkSelector.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userGoodsId = computed(() => parseInt(route.query.id) || 0)
|
||||
const userGoodsId = ref(parseInt(route.query.id) || 0)
|
||||
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
@@ -814,7 +1002,39 @@ const vmVolumes = ref([])
|
||||
const vmImage = ref(null)
|
||||
const inPortGroup = ref(null)
|
||||
const outPortGroup = ref(null)
|
||||
const isVmGoods = ref(false) // 是否为虚拟机类型商品
|
||||
const isVmGoods = ref(false)
|
||||
const showPassword = ref(false)
|
||||
|
||||
const copyPassword = async () => {
|
||||
const pwd = vm.value?.root_password
|
||||
if (!pwd) { ElMessage.warning('暂无密码'); return }
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(pwd)
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = pwd
|
||||
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'
|
||||
document.body.appendChild(ta)
|
||||
ta.focus()
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
ElMessage.success('密码已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
const isWindows = computed(() => vmImage.value?.os_type === 'windows')
|
||||
|
||||
const vmPublicIpList = computed(() => {
|
||||
return vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.address ? n.address.split('/')[0] : n.name).filter(Boolean)
|
||||
})
|
||||
const vmPrivateIpList = computed(() => {
|
||||
return vmNetworks.value.filter(n => n.type === 'nat').map(n => n.address ? n.address.split('/')[0] : n.name).filter(Boolean)
|
||||
})
|
||||
|
||||
// 安全组列表(来自详情接口,入站+出站)
|
||||
const inPortGroupList = computed(() => {
|
||||
@@ -906,6 +1126,7 @@ const handleTabChange = (tab) => {
|
||||
if (tab === 'backup') { loadBackups(); loadBackupQuota() }
|
||||
if (tab === 'security') loadSgLockInfo()
|
||||
if (tab === 'networking') loadNetworkings()
|
||||
if (tab === 'monitor' && !metricsData.value) loadMetricsHistory()
|
||||
}
|
||||
|
||||
// 请求安全组详情补充 lock 字段(使用用户虚拟机安全组详情接口)
|
||||
@@ -935,6 +1156,7 @@ const handleVnc = async () => {
|
||||
else ElMessage.error(extractApiError(res?.data, '获取VNC失败'))
|
||||
} catch { /* */ } finally { vncLoading.value = false }
|
||||
}
|
||||
const openVncUrl = () => { if (vncResult.value?.url) window.open(vncResult.value.url, '_blank') }
|
||||
|
||||
// ---- 电源 ----
|
||||
const powerVisible = ref(false)
|
||||
@@ -955,7 +1177,7 @@ const submitPower = async () => {
|
||||
const handleMoreCmd = (cmd) => {
|
||||
if (powerLabels[cmd]) { handlePower(cmd); return }
|
||||
if (cmd === 'rebuild') { rebuildImageId.value = 0; rebuildImageName.value = ''; rebuildImages.value = []; rebuildVisible.value = true; loadRebuildImages() }
|
||||
if (cmd === 'updateTraffic') { Object.assign(trafficForm, { rx_bandwidth: vm.value?.rx_bandwidth || 0, tx_bandwidth: vm.value?.tx_bandwidth || 0, _trafficGB: ((vm.value?.traffic_max || 0) / 1024).toFixed(2) * 1 }); trafficVisible.value = true }
|
||||
if (cmd === 'updateTraffic') { Object.assign(trafficForm, { rx_bandwidth: vm.value?.rx_bandwidth || 0, tx_bandwidth: vm.value?.tx_bandwidth || 0, _trafficValue: +((vm.value?.traffic_max || 0) / 1024).toFixed(2), _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB' }); trafficVisible.value = true }
|
||||
if (cmd === 'transfer') { Object.assign(transferForm, { target_user_id: 0, _userName: '' }); transferVisible.value = true }
|
||||
if (cmd === 'updateVm') openEditVm()
|
||||
if (cmd === 'refactorVm') openRefactorVm()
|
||||
@@ -1282,6 +1504,45 @@ const loadNetworks = async () => {
|
||||
} catch { /* */ } finally { networkLoading.value = false }
|
||||
}
|
||||
|
||||
// ---- 绑定网络 ----
|
||||
const showBindNetworkSelector = ref(false)
|
||||
|
||||
const handleBindNetworkConfirm = async (selectedNetwork) => {
|
||||
const existingIds = vmNetworks.value.map(n => n.id)
|
||||
if (existingIds.includes(selectedNetwork.id)) {
|
||||
ElMessage.warning('该网络已绑定')
|
||||
return
|
||||
}
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const payload = { user_goods_id: userGoodsId.value }
|
||||
const bridgeIds = vmNetworks.value.filter(n => n.type === 'bridge').map(n => n.id)
|
||||
const natNet = vmNetworks.value.find(n => n.type === 'nat')
|
||||
if (selectedNetwork.type === 'nat') {
|
||||
payload.internet_network_id = selectedNetwork.id
|
||||
if (bridgeIds.length) payload.network_ids = bridgeIds
|
||||
} else {
|
||||
payload.network_ids = [...bridgeIds, selectedNetwork.id]
|
||||
if (natNet) payload.internet_network_id = natNet.id
|
||||
}
|
||||
if (vm.value?.rx_bandwidth) payload.rx_bandwidth = vm.value.rx_bandwidth
|
||||
if (vm.value?.tx_bandwidth) payload.tx_bandwidth = vm.value.tx_bandwidth
|
||||
if (vm.value?.ssh_port) payload.ssh_port = vm.value.ssh_port
|
||||
if (inPortGroup.value?.id) payload.port_group_id = inPortGroup.value.id
|
||||
const res = await updateUserVm(payload)
|
||||
if (res?.data?.code === 200) {
|
||||
ElMessage.success('绑定网络成功')
|
||||
loadDetail()
|
||||
} else {
|
||||
ElMessage.error(extractApiError(res?.data, '绑定网络失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '绑定网络失败'))
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 组网 ----
|
||||
const networkings = ref([])
|
||||
const networkingLoading = ref(false)
|
||||
@@ -1318,7 +1579,7 @@ const loadNetworkings = async () => {
|
||||
}
|
||||
const handleCreateNetworking = () => { Object.assign(networkingCreateForm, { name: '', bridge_name: '', gateway: '', description: '' }); networkingCreateVisible.value = true }
|
||||
const submitCreateNetworking = async () => {
|
||||
if (!networkingCreateForm.name || !networkingCreateForm.bridge_name) { ElMessage.warning('请填写名称和网桥名称'); return }
|
||||
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const res = await createUserVmNetworking({ user_goods_id: userGoodsId.value, ...networkingCreateForm })
|
||||
@@ -1463,7 +1724,8 @@ const refactorVisible = ref(false)
|
||||
const showRefactorSgSelector = ref(false)
|
||||
const showRefactorNetSelector = ref(false)
|
||||
const refactorForm = reactive({
|
||||
_memoryMB: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
_memoryValue: 0, _memoryUnit: 'MB', vcpu: 0,
|
||||
rx_bandwidth: 0, tx_bandwidth: 0, _rxUnit: 'Mbps', _txUnit: 'Mbps',
|
||||
root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '',
|
||||
ssh_port: 0, vnc_port: 0, vnc_password: '',
|
||||
port_group_id: 0, _sgName: '', internet_network_id: 0, _networkName: ''
|
||||
@@ -1471,10 +1733,12 @@ const refactorForm = reactive({
|
||||
|
||||
const openRefactorVm = () => {
|
||||
Object.assign(refactorForm, {
|
||||
_memoryMB: vm.value ? Math.round((vm.value.memory || 0) / 1024) : 0,
|
||||
_memoryValue: vm.value ? Math.round((vm.value.memory || 0) / 1024) : 0,
|
||||
_memoryUnit: 'MB',
|
||||
vcpu: vm.value?.vcpu || 0,
|
||||
rx_bandwidth: vm.value?.rx_bandwidth || 0,
|
||||
tx_bandwidth: vm.value?.tx_bandwidth || 0,
|
||||
_rxUnit: 'Mbps', _txUnit: 'Mbps',
|
||||
root_password: '', uuid: vm.value?.uuid || '',
|
||||
mate_data_id: vm.value?.mate_data_id || '',
|
||||
physical_name: '', config_path: '',
|
||||
@@ -1491,10 +1755,15 @@ const submitRefactorVm = async () => {
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const payload = { user_goods_id: userGoodsId.value }
|
||||
if (refactorForm._memoryMB) payload.memory = Math.round(refactorForm._memoryMB * 1024) // MB → KB
|
||||
if (refactorForm._memoryValue) {
|
||||
const memKB = refactorForm._memoryUnit === 'GB' ? refactorForm._memoryValue * 1024 * 1024 : refactorForm._memoryValue * 1024
|
||||
payload.memory = Math.round(memKB)
|
||||
}
|
||||
if (refactorForm.vcpu) payload.vcpu = refactorForm.vcpu
|
||||
if (refactorForm.rx_bandwidth) payload.rx_bandwidth = refactorForm.rx_bandwidth
|
||||
if (refactorForm.tx_bandwidth) payload.tx_bandwidth = refactorForm.tx_bandwidth
|
||||
const refRx = refactorForm._rxUnit === 'Gbps' ? refactorForm.rx_bandwidth * 1000 : refactorForm.rx_bandwidth
|
||||
const refTx = refactorForm._txUnit === 'Gbps' ? refactorForm.tx_bandwidth * 1000 : refactorForm.tx_bandwidth
|
||||
if (refRx) payload.rx_bandwidth = Math.round(refRx)
|
||||
if (refTx) payload.tx_bandwidth = Math.round(refTx)
|
||||
if (refactorForm.root_password) payload.root_password = refactorForm.root_password
|
||||
if (refactorForm.uuid) payload.uuid = refactorForm.uuid
|
||||
if (refactorForm.mate_data_id) payload.mate_data_id = refactorForm.mate_data_id
|
||||
@@ -1518,6 +1787,7 @@ const showEditVmSgSelector = ref(false)
|
||||
const showEditVmNetSelector = ref(false)
|
||||
const editVmForm = reactive({
|
||||
rx_bandwidth: 0, tx_bandwidth: 0,
|
||||
_rxUnit: 'Mbps', _txUnit: 'Mbps',
|
||||
root_password: '',
|
||||
ssh_port: 22,
|
||||
port_group_id: 0, _sgName: '',
|
||||
@@ -1529,6 +1799,7 @@ const openEditVm = async () => {
|
||||
Object.assign(editVmForm, {
|
||||
rx_bandwidth: vm.value?.rx_bandwidth || 0,
|
||||
tx_bandwidth: vm.value?.tx_bandwidth || 0,
|
||||
_rxUnit: 'Mbps', _txUnit: 'Mbps',
|
||||
root_password: '',
|
||||
ssh_port: vm.value?.ssh_port || 22,
|
||||
port_group_id: inPortGroup.value?.id || 0,
|
||||
@@ -1547,8 +1818,10 @@ const submitEditVm = async () => {
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const payload = { user_goods_id: userGoodsId.value }
|
||||
if (editVmForm.rx_bandwidth) payload.rx_bandwidth = editVmForm.rx_bandwidth
|
||||
if (editVmForm.tx_bandwidth) payload.tx_bandwidth = editVmForm.tx_bandwidth
|
||||
const editRx = editVmForm._rxUnit === 'Gbps' ? editVmForm.rx_bandwidth * 1000 : editVmForm.rx_bandwidth
|
||||
const editTx = editVmForm._txUnit === 'Gbps' ? editVmForm.tx_bandwidth * 1000 : editVmForm.tx_bandwidth
|
||||
if (editRx) payload.rx_bandwidth = Math.round(editRx)
|
||||
if (editTx) payload.tx_bandwidth = Math.round(editTx)
|
||||
if (editVmForm.root_password) payload.root_password = editVmForm.root_password
|
||||
if (editVmForm.ssh_port) payload.ssh_port = editVmForm.ssh_port
|
||||
if (editVmForm.port_group_id) payload.port_group_id = editVmForm.port_group_id
|
||||
@@ -1563,15 +1836,20 @@ const submitEditVm = async () => {
|
||||
|
||||
// ---- 修改带宽 ----
|
||||
const trafficVisible = ref(false)
|
||||
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficGB: 0 })
|
||||
const trafficForm = reactive({ rx_bandwidth: 0, tx_bandwidth: 0, _trafficValue: 0, _rxUnit: 'Mbps', _txUnit: 'Mbps', _trafficUnit: 'GB' })
|
||||
const submitUpdateTraffic = async () => {
|
||||
actionLoading.value = true
|
||||
try {
|
||||
const rxBw = trafficForm._rxUnit === 'Gbps' ? trafficForm.rx_bandwidth * 1000 : trafficForm.rx_bandwidth
|
||||
const txBw = trafficForm._txUnit === 'Gbps' ? trafficForm.tx_bandwidth * 1000 : trafficForm.tx_bandwidth
|
||||
const trafficMb = trafficForm._trafficUnit === 'TB' ? (trafficForm._trafficValue || 0) * 1024 * 1024
|
||||
: trafficForm._trafficUnit === 'GB' ? (trafficForm._trafficValue || 0) * 1024
|
||||
: (trafficForm._trafficValue || 0)
|
||||
const payload = {
|
||||
user_goods_id: userGoodsId.value,
|
||||
rx_bandwidth: trafficForm.rx_bandwidth,
|
||||
tx_bandwidth: trafficForm.tx_bandwidth,
|
||||
traffic_max: Math.round((trafficForm._trafficGB || 0) * 1024) // GB → Mb
|
||||
rx_bandwidth: Math.round(rxBw),
|
||||
tx_bandwidth: Math.round(txBw),
|
||||
traffic_max: Math.round(trafficMb)
|
||||
}
|
||||
const res = await updateUserVmTraffic(payload)
|
||||
if (res?.data?.code === 200) { ElMessage.success('修改成功'); trafficVisible.value = false; loadDetail() }
|
||||
@@ -1627,12 +1905,179 @@ const submitEditGoods = async () => {
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '保存失败')) } finally { actionLoading.value = false }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDetail()
|
||||
// ---- 监控指标 ----
|
||||
const cpuChartRef = ref(null)
|
||||
const memChartRef = ref(null)
|
||||
const diskChartRef = ref(null)
|
||||
const netChartRef = ref(null)
|
||||
let cpuChart = null
|
||||
let memChart = null
|
||||
let diskChart = null
|
||||
let netChart = null
|
||||
|
||||
const metricsData = ref(null)
|
||||
const metricsLoading = ref(false)
|
||||
const monitorInterval = ref('1m')
|
||||
|
||||
const latestMetrics = computed(() => {
|
||||
const arr = metricsData.value
|
||||
if (!Array.isArray(arr) || !arr.length) return null
|
||||
return arr[arr.length - 1]
|
||||
})
|
||||
|
||||
watch(userGoodsId, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
const formatBytesRaw = (val) => {
|
||||
if (!val && val !== 0) return '-'
|
||||
val = Number(val)
|
||||
if (val >= 1099511627776) return (val / 1099511627776).toFixed(2) + ' TB'
|
||||
if (val >= 1073741824) return (val / 1073741824).toFixed(2) + ' GB'
|
||||
if (val >= 1048576) return (val / 1048576).toFixed(2) + ' MB'
|
||||
if (val >= 1024) return (val / 1024).toFixed(1) + ' KB'
|
||||
return val + ' B'
|
||||
}
|
||||
|
||||
const formatNetLabel = (v) => {
|
||||
if (!v) return '0 B/s'
|
||||
v = Number(v)
|
||||
if (v >= 1073741824) return (v / 1073741824).toFixed(1) + ' GB/s'
|
||||
if (v >= 1048576) return (v / 1048576).toFixed(1) + ' MB/s'
|
||||
if (v >= 1024) return (v / 1024).toFixed(1) + ' KB/s'
|
||||
return v + ' B/s'
|
||||
}
|
||||
|
||||
const formatMemKB = (kb) => {
|
||||
if (!kb && kb !== 0) return '-'
|
||||
kb = Number(kb)
|
||||
if (kb >= 1073741824) return (kb / 1073741824).toFixed(1) + ' TB'
|
||||
if (kb >= 1048576) return (kb / 1048576).toFixed(1) + ' GB'
|
||||
if (kb >= 1024) return (kb / 1024).toFixed(0) + ' MB'
|
||||
return kb + ' KB'
|
||||
}
|
||||
|
||||
const vmMemPercent = (m) => {
|
||||
if (!m || !m.mem_total) return '0.0'
|
||||
return ((m.mem_used / m.mem_total) * 100).toFixed(1)
|
||||
}
|
||||
|
||||
const loadMetricsHistory = async () => {
|
||||
if (!userGoodsId.value) return
|
||||
metricsLoading.value = true
|
||||
try {
|
||||
const now = new Date()
|
||||
let startTime = new Date(now)
|
||||
const interval = monitorInterval.value
|
||||
switch (interval) {
|
||||
case '1m': startTime.setHours(now.getHours() - 1); break
|
||||
case '5m': startTime.setHours(now.getHours() - 6); break
|
||||
case '1h': startTime.setDate(now.getDate() - 1); break
|
||||
}
|
||||
const params = {
|
||||
user_goods_id: userGoodsId.value,
|
||||
start: startTime.toISOString(),
|
||||
end_time: now.toISOString(),
|
||||
interval
|
||||
}
|
||||
const res = await getUserVmMetricsHistory(params)
|
||||
const body = res?.data
|
||||
if (body?.code === 200 && body?.data) {
|
||||
metricsData.value = Array.isArray(body.data) ? body.data : (body.data.data || [])
|
||||
await nextTick()
|
||||
renderMetricsCharts()
|
||||
} else {
|
||||
ElMessage.error(extractApiError(body, '加载监控数据失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(extractApiError(e?.response?.data, '加载监控数据失败'))
|
||||
} finally {
|
||||
metricsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const renderMetricsCharts = () => {
|
||||
const metrics = metricsData.value
|
||||
if (!Array.isArray(metrics) || !metrics.length) return
|
||||
|
||||
const showDate = monitorInterval.value === '1h'
|
||||
const labelRotate = showDate ? 45 : 0
|
||||
|
||||
const times = metrics.map(m => {
|
||||
const date = new Date(m.bucket)
|
||||
if (showDate) return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
|
||||
})
|
||||
|
||||
const cpuData = metrics.map(m => m.cpu_usage ?? 0)
|
||||
const memData = metrics.map(m => m.mem_total ? ((m.mem_used / m.mem_total) * 100) : 0)
|
||||
const diskReadData = metrics.map(m => m.disk_read ?? 0)
|
||||
const diskWriteData = metrics.map(m => m.disk_write ?? 0)
|
||||
const netRxData = metrics.map(m => m.net_rx ?? 0)
|
||||
const netTxData = metrics.map(m => m.net_tx ?? 0)
|
||||
|
||||
const baseGrid = { top: 10, right: 16, bottom: showDate ? 40 : 24, left: 50 }
|
||||
const makeXAxis = () => ({ type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10, rotate: labelRotate } })
|
||||
const makeSeries = (name, data, color) => ({ name, type: 'line', smooth: true, symbol: 'none', areaStyle: { opacity: 0.15 }, lineStyle: { width: 2, color }, itemStyle: { color }, data })
|
||||
|
||||
if (cpuChartRef.value) {
|
||||
if (!cpuChart) cpuChart = echarts.init(cpuChartRef.value)
|
||||
cpuChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} CPU: ${p[0].value.toFixed(1)}%` },
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
|
||||
series: [makeSeries('CPU', cpuData, '#409eff')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (memChartRef.value) {
|
||||
if (!memChart) memChart = echarts.init(memChartRef.value)
|
||||
memChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}<br/>${p[0].marker} 内存: ${p[0].value.toFixed(1)}%` },
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, max: 100, axisLabel: { fontSize: 10, formatter: v => v + '%' } },
|
||||
series: [makeSeries('内存', memData, '#67c23a')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (diskChartRef.value) {
|
||||
if (!diskChart) diskChart = echarts.init(diskChartRef.value)
|
||||
diskChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (params) => {
|
||||
let s = params[0].axisValue
|
||||
params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatBytesRaw(p.value)}` })
|
||||
return s
|
||||
}},
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatBytesRaw(v) } },
|
||||
series: [makeSeries('读取', diskReadData, '#409eff'), makeSeries('写入', diskWriteData, '#e6a23c')]
|
||||
}, true)
|
||||
}
|
||||
|
||||
if (netChartRef.value) {
|
||||
if (!netChart) netChart = echarts.init(netChartRef.value)
|
||||
netChart.setOption({
|
||||
tooltip: { trigger: 'axis', formatter: (params) => {
|
||||
let s = params[0].axisValue
|
||||
params.forEach(p => { s += `<br/>${p.marker} ${p.seriesName}: ${formatNetLabel(p.value)}` })
|
||||
return s
|
||||
}},
|
||||
grid: baseGrid, xAxis: makeXAxis(),
|
||||
yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatNetLabel(v) } },
|
||||
series: [makeSeries('接收', netRxData, '#409eff'), makeSeries('发送', netTxData, '#e6a23c')]
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
|
||||
const disposeCharts = () => {
|
||||
cpuChart?.dispose(); cpuChart = null
|
||||
memChart?.dispose(); memChart = null
|
||||
diskChart?.dispose(); diskChart = null
|
||||
netChart?.dispose(); netChart = null
|
||||
}
|
||||
|
||||
onMounted(() => { loadDetail() })
|
||||
|
||||
onActivated(() => {
|
||||
const newId = parseInt(route.query.id) || 0
|
||||
if (newId && newId !== userGoodsId.value) {
|
||||
userGoodsId.value = newId
|
||||
userGoods.value = null
|
||||
vm.value = null
|
||||
vmNetworks.value = []
|
||||
@@ -1640,9 +2085,13 @@ watch(userGoodsId, (newId, oldId) => {
|
||||
inPortGroup.value = null
|
||||
outPortGroup.value = null
|
||||
isVmGoods.value = false
|
||||
metricsData.value = null
|
||||
disposeCharts()
|
||||
loadDetail()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => { disposeCharts() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1661,9 +2110,7 @@ watch(userGoodsId, (newId, oldId) => {
|
||||
.overview-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.tabs-card { }
|
||||
.tab-toolbar { display: flex; gap: 8px; align-items: center; margin: 12px 0; }
|
||||
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.selector-row { display: flex; align-items: center; width: 100%; }
|
||||
.mono-text { font-family: 'Cascadia Code', Consolas, monospace; font-size: 12px; }
|
||||
|
||||
/* VM 配置网格 */
|
||||
.vm-config-grid { border: 1px solid #e8e8e8; border-radius: 4px; overflow: hidden; }
|
||||
@@ -1673,4 +2120,29 @@ watch(userGoodsId, (newId, oldId) => {
|
||||
.config-cell:last-child { border-right: none; }
|
||||
.config-label { font-size: 12px; color: #86909c; line-height: 1; }
|
||||
.config-value { font-size: 13px; color: #1d2129; line-height: 1.4; word-break: break-all; }
|
||||
.pwd-value { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.pwd-text { font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; background: #f5f7fa; padding: 2px 8px; border-radius: 3px; letter-spacing: .5px; user-select: all; }
|
||||
.pwd-btn { padding: 0 !important; height: auto !important; min-height: auto !important; }
|
||||
|
||||
/* 单位输入行 */
|
||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||
.unit-select :deep(.el-input__wrapper) { padding: 0 8px; }
|
||||
.unit-text { flex-shrink: 0; font-size: 13px; color: #606266; padding: 0 4px; min-width: 36px; text-align: center; line-height: 32px; background: #f5f7fa; border: 1px solid #dcdfe6; border-radius: 4px; }
|
||||
|
||||
/* 监控指标 */
|
||||
.section-block { padding: 0 4px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.section-title { margin: 0; font-size: 15px; font-weight: 600; color: #303133; }
|
||||
.metrics-card { margin-bottom: 0; }
|
||||
.metrics-title { font-weight: 600; font-size: 13px; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.metrics-title .el-icon { font-size: 16px; color: #409eff; }
|
||||
.chart-container { width: 100%; height: 220px; }
|
||||
.metric-summary-row { display: flex; gap: 16px; margin-bottom: 16px; }
|
||||
.metric-summary-card { flex: 1; min-width: 0; background: #f7f8fa; border-radius: 6px; padding: 14px 16px; border: 1px solid #e8e8e8; display: flex; flex-direction: column; }
|
||||
.metric-summary-label { font-size: 12px; color: #86909c; margin-bottom: 8px; }
|
||||
.metric-summary-value { font-size: 22px; font-weight: 600; color: #1d2129; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.metric-summary-sub { font-size: 12px; color: #86909c; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ip-popover-list { max-height: 200px; overflow-y: auto; }
|
||||
.ip-popover-item { padding: 4px 0; font-size: 13px; color: #303133; border-bottom: 1px dashed #ebeef5; word-break: break-all; }
|
||||
.ip-popover-item:last-child { border-bottom: none; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user