diff --git a/src/api/admin/kvmService.js b/src/api/admin/kvmService.js
index 9cda5f4..97d4324 100644
--- a/src/api/admin/kvmService.js
+++ b/src/api/admin/kvmService.js
@@ -298,6 +298,30 @@ export const resetVmMac = (data) => {
})
}
+/** 断开虚拟机外部网络 */
+export const disconnectVmNetwork = (data) => {
+ return http2.post('/api/v1/admin/server/host_service/point/vm/disconnect_network', data, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ })
+}
+
+/** 恢复虚拟机外部网络 */
+export const connectVmNetwork = (data) => {
+ return http2.post('/api/v1/admin/server/host_service/point/vm/connect_network', data, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ })
+}
+
+/** 查询虚拟机每小时流量 */
+export const getVmTrafficHourly = (params) => {
+ return http2.get('/api/v1/admin/server/host_service/point/vm/traffic_hourly', { params })
+}
+
+/** 获取宿主机额度统计 */
+export const getHostQuotaStats = (params) => {
+ return http2.get('/api/v1/admin/server/host_service/point/host/quota_stats', { params })
+}
+
/**
* ================================
* 主控服务接口 - 数据卷管理
diff --git a/src/api/admin/userVm.js b/src/api/admin/userVm.js
index 9d1d14f..3a31848 100644
--- a/src/api/admin/userVm.js
+++ b/src/api/admin/userVm.js
@@ -90,6 +90,8 @@ export const getUserVmNetworkList = (params) => http2.get(`${BASE}/network/list`
export const getUserVmNetworkDetail = (params) => http2.get(`${BASE}/network/detail`, { params })
export const setUserVmNetworkPrimary = (data) => http2.post(`${BASE}/network/set_primary`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
export const resetUserVmMac = (params) => http2.post(`${BASE}/reset_mac`, null, { params })
+export const disconnectUserVmNetwork = (data) => http2.post(`${BASE}/disconnect_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
+export const connectUserVmNetwork = (data) => http2.post(`${BASE}/connect_network`, fd(data), { headers: { 'Content-Type': 'multipart/form-data' } })
// ========== 组网 ==========
export const getUserVmNetworkingList = (params) => http2.get(`${BASE}/networking/list`, { params })
diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue
index e0b1904..6e8f8e6 100644
--- a/src/views/user-vm/UserVmDetail.vue
+++ b/src/views/user-vm/UserVmDetail.vue
@@ -19,6 +19,7 @@
@@ -1606,7 +1637,8 @@ import {
dataMigrateVm, getDataMigrateProgress, abortDataMigrate,
getKvmServiceList, getMetricsHistory, getNetworkList,
getVmTrafficPolicy, updateVmTrafficPolicy, addVmFixedTraffic, addVmTemporaryTraffic,
- setNetworkPrimary, resetVmMac
+ setNetworkPrimary, resetVmMac,
+ disconnectVmNetwork, connectVmNetwork, getVmTrafficHourly
} from '@/api/admin/kvmService'
import { getUserInfo } from '@/api/admin/user'
import { extractApiError } from '@/utils/kvmErrorUtil'
@@ -1744,7 +1776,8 @@ const handleMoreCommand = (cmd) => {
const actionMap = {
editVm: handleEditVm, refactorVm: handleRefactorVm, updateTraffic: handleUpdateTraffic,
rebuild: handleRebuild, rescue: handleRescue, exitRescue: handleExitRescue,
- migrateVm: handleMigrateVm, resetMac: handleResetMac
+ migrateVm: handleMigrateVm, resetMac: handleResetMac,
+ disconnectNetwork: handleDisconnectNetwork, connectNetwork: handleConnectNetwork
}
if (actionMap[cmd]) actionMap[cmd]()
if (cmd === 'dataMigrateVm') handleDataMigrateVm()
@@ -2866,6 +2899,89 @@ const handleResetMac = () => {
}).catch(() => {})
}
+const handleDisconnectNetwork = () => {
+ ElMessageBox.confirm(
+ '断开虚拟机外部网络连接?断网后虚拟机将无法访问外部网络。',
+ '断网', { confirmButtonText: '确定断网', cancelButtonText: '取消', type: 'warning' }
+ ).then(async () => {
+ try {
+ const fd = new FormData()
+ fd.append('service_id', serviceId.value)
+ fd.append('vm_id', detail.value?.id)
+ const res = await disconnectVmNetwork(fd)
+ if (res?.data?.code === 200) { ElMessage.success('已断网'); loadDetail() }
+ else ElMessage.error(extractApiError(res?.data, '断网失败'))
+ } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '断网失败')) }
+ }).catch(() => {})
+}
+
+const handleConnectNetwork = () => {
+ ElMessageBox.confirm(
+ '恢复虚拟机外部网络连接?',
+ '恢复网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'info' }
+ ).then(async () => {
+ try {
+ const fd = new FormData()
+ fd.append('service_id', serviceId.value)
+ fd.append('vm_id', detail.value?.id)
+ const res = await connectVmNetwork(fd)
+ if (res?.data?.code === 200) { ElMessage.success('网络已恢复'); loadDetail() }
+ else ElMessage.error(extractApiError(res?.data, '恢复网络失败'))
+ } catch (e) { ElMessage.error(extractApiError(e?.response?.data, '恢复网络失败')) }
+ }).catch(() => {})
+}
+
+// ---- 每小时流量统计 ----
+const trafficHourlyRange = ref(null)
+const trafficHourlyData = ref([])
+const trafficHourlyLoading = ref(false)
+const trafficHourlyChartRef = ref(null)
+let trafficHourlyChart = null
+
+const loadTrafficHourly = async () => {
+ if (!serviceId.value || !detail.value?.host_id || !detail.value?.name) return
+ if (!trafficHourlyRange.value) {
+ const now = new Date()
+ trafficHourlyRange.value = [new Date(now.getTime() - 24 * 3600 * 1000), now]
+ }
+ trafficHourlyLoading.value = true
+ try {
+ const res = await getVmTrafficHourly({
+ service_id: serviceId.value,
+ host_id: detail.value.host_id,
+ vm_name: detail.value.name,
+ start: new Date(trafficHourlyRange.value[0]).toISOString(),
+ end_time: new Date(trafficHourlyRange.value[1]).toISOString()
+ })
+ const raw = res?.data?.data?.data
+ trafficHourlyData.value = typeof raw === 'string' ? JSON.parse(raw) : (Array.isArray(raw) ? raw : [])
+ nextTick(renderTrafficHourlyChart)
+ } catch { trafficHourlyData.value = [] } finally { trafficHourlyLoading.value = false }
+}
+
+const renderTrafficHourlyChart = () => {
+ if (!trafficHourlyChartRef.value || !trafficHourlyData.value.length) return
+ if (!trafficHourlyChart) trafficHourlyChart = echarts.init(trafficHourlyChartRef.value)
+ const data = trafficHourlyData.value
+ const times = data.map(d => {
+ const date = new Date(d.bucket)
+ return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
+ })
+ const toMB = (b) => +(b / 1048576).toFixed(2)
+ trafficHourlyChart.setOption({
+ tooltip: { trigger: 'axis', formatter: (params) => params.map(p => `${p.marker}${p.seriesName}: ${p.value} MB`).join('
') },
+ legend: { data: ['下行', '上行', '合计'], bottom: 0 },
+ grid: { top: 10, right: 16, bottom: 40, left: 50 },
+ xAxis: { type: 'category', data: times, boundaryGap: false, axisLabel: { fontSize: 10 } },
+ yAxis: { type: 'value', name: 'MB', axisLabel: { fontSize: 10 } },
+ series: [
+ { name: '下行', type: 'bar', stack: 'traffic', data: data.map(d => toMB(d.rx_bytes)), itemStyle: { color: '#409EFF' } },
+ { name: '上行', type: 'bar', stack: 'traffic', data: data.map(d => toMB(d.tx_bytes)), itemStyle: { color: '#67C23A' } },
+ { name: '合计', type: 'line', smooth: true, data: data.map(d => toMB(d.total_bytes)), itemStyle: { color: '#E6A23C' }, lineStyle: { width: 2 } }
+ ]
+ }, true)
+}
+
// ---- 数据卷操作(绑定/创建/调整/挂载/卸载/迁移/删除/详情) ----
const showVolSelector = ref(false)