feat(admin+user): 虚拟机断网/恢复网络+每小时流量图表+宿主机额度统计 -- 缘由: 后端新增disconnect/connect_network,traffic_hourly,quota_stats接口,VM新增network_disabled字段 -- 预期: VmDetail/UserVmDetail/用户详情支持断网恢复操作并显示断网状态,VmDetail新增流量统计tab,HostDetail新增额度统计tab

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shiran
2026-05-15 16:29:18 +08:00
parent 564e6cc017
commit a5f8a9ef13
5 changed files with 249 additions and 5 deletions
+71 -1
View File
@@ -153,6 +153,42 @@
</div>
</el-tab-pane>
<el-tab-pane label="额度统计" name="quotaStats">
<div class="section-block">
<div class="section-header">
<h3 class="section-title">资源额度统计</h3>
<el-button size="small" :icon="Refresh" @click="loadQuotaStats" :loading="quotaStatsLoading">刷新</el-button>
</div>
<template v-if="quotaStats">
<el-descriptions :column="3" border size="small" style="margin-top:12px">
<el-descriptions-item label="虚拟机数量">{{ quotaStats.vm_count ?? '-' }}</el-descriptions-item>
<el-descriptions-item label="规划 CPU">{{ quotaStats.planned_cpu ?? '-' }} </el-descriptions-item>
<el-descriptions-item label="已分配 CPU">{{ quotaStats.allocated_cpu ?? '-' }} </el-descriptions-item>
<el-descriptions-item label="规划内存">{{ formatQuotaMem(quotaStats.planned_memory) }}</el-descriptions-item>
<el-descriptions-item label="已分配内存">{{ formatQuotaMem(quotaStats.allocated_memory) }}</el-descriptions-item>
<el-descriptions-item label="实时内存">{{ formatQuotaBytes(quotaStats.actual_memory_used) }} / {{ formatQuotaBytes(quotaStats.actual_memory_total) }}</el-descriptions-item>
<el-descriptions-item label="规划磁盘">{{ quotaStats.planned_disk ?? '-' }} GB</el-descriptions-item>
<el-descriptions-item label="已分配磁盘">{{ quotaStats.allocated_disk ?? '-' }} GB</el-descriptions-item>
<el-descriptions-item label="实时 CPU">{{ quotaStats.actual_cpu_percent != null ? quotaStats.actual_cpu_percent.toFixed(1) + '%' : '-' }}</el-descriptions-item>
<el-descriptions-item label="规划下行带宽">{{ quotaStats.planned_rx_bandwidth ?? '-' }} Mbps</el-descriptions-item>
<el-descriptions-item label="已分配下行带宽">{{ quotaStats.allocated_rx_bandwidth ?? '-' }} Mbps</el-descriptions-item>
<el-descriptions-item label="规划上行带宽">{{ quotaStats.planned_tx_bandwidth ?? '-' }} Mbps</el-descriptions-item>
<el-descriptions-item label="已分配上行带宽">{{ quotaStats.allocated_tx_bandwidth ?? '-' }} Mbps</el-descriptions-item>
</el-descriptions>
<template v-if="quotaStatsDisk.length">
<h4 style="margin:16px 0 8px;font-size:13px;color:#606266">磁盘使用</h4>
<el-table :data="quotaStatsDisk" size="small" stripe>
<el-table-column prop="path" label="路径" min-width="160" />
<el-table-column label="总量" width="100"><template #default="{row}">{{ formatQuotaBytes(row.total) }}</template></el-table-column>
<el-table-column label="已用" width="100"><template #default="{row}">{{ formatQuotaBytes(row.used) }}</template></el-table-column>
<el-table-column label="使用率" width="100"><template #default="{row}">{{ row.total ? ((row.used / row.total) * 100).toFixed(1) + '%' : '-' }}</template></el-table-column>
</el-table>
</template>
</template>
<el-empty v-else-if="!quotaStatsLoading" description="暂无额度统计数据" :image-size="60" />
</div>
</el-tab-pane>
<el-tab-pane label="监控" name="monitor">
<div class="section-block">
<div class="section-header">
@@ -613,7 +649,7 @@ import {
getRemoteHostDetail, updateRemoteHost, deleteRemoteHost,
getUserNetworkingList, getUserNetworkingDetail, createUserNetworking, deleteUserNetworking,
assignUserNetworking, removeUserNetworkingNetwork,
createHostToken, getMetricsHistory
createHostToken, getMetricsHistory, getHostQuotaStats
} from '@/api/admin/kvmService'
import { extractApiError } from '@/utils/kvmErrorUtil'
import { baseUrl } from '@/config/env'
@@ -662,6 +698,7 @@ watch(activeTab, (tab) => {
}
}
if (tab === 'networking') loadNetworkingList()
if (tab === 'quotaStats' && !quotaStats.value) loadQuotaStats()
})
const loading = ref(false)
@@ -822,6 +859,39 @@ const loadDetail = async () => {
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false }
}
// ---- 额度统计 ----
const quotaStats = ref(null)
const quotaStatsLoading = ref(false)
const quotaStatsDisk = computed(() => {
if (!quotaStats.value?.actual_disk_json) return []
try { return JSON.parse(quotaStats.value.actual_disk_json) } catch { return [] }
})
const loadQuotaStats = async () => {
if (!serviceId.value || !hostId.value) return
quotaStatsLoading.value = true
try {
const res = await getHostQuotaStats({ service_id: serviceId.value, host_id: hostId.value })
if (res?.data?.code === 200) quotaStats.value = res.data.data
else quotaStats.value = null
} catch { quotaStats.value = null } finally { quotaStatsLoading.value = false }
}
const formatQuotaMem = (mb) => {
if (!mb && mb !== 0) return '-'
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB'
return mb + ' MB'
}
const formatQuotaBytes = (bytes) => {
if (!bytes && bytes !== 0) return '-'
const n = Number(bytes)
if (n >= 1073741824) return (n / 1073741824).toFixed(2) + ' GB'
if (n >= 1048576) return (n / 1048576).toFixed(1) + ' MB'
if (n >= 1024) return (n / 1024).toFixed(0) + ' KB'
return n + ' B'
}
const cpuChartRef = ref(null)
const memChartRef = ref(null)
const netChartRef = ref(null)