+
@@ -1126,7 +1169,7 @@ import {
getUserVmPostGroupDetail,
getUserVmNetworkList, getUserVmNetworkDetail, getUserVmNetworkingList, createUserVmNetworking, assignUserVmNetworking, removeUserVmNetworkingNetwork, deleteUserVmNetworking,
getUserGoodsDetail,
- getUserVmMetricsHistory,
+ getUserVmMetricsHistory, getUserVmTrafficHourly,
getUserVmTrafficPolicy, updateUserVmTrafficPolicy, addUserVmFixedTraffic, addUserVmTemporaryTraffic,
setUserVmNetworkPrimary, resetUserVmMac,
disconnectUserVmNetwork, connectUserVmNetwork
@@ -1318,7 +1361,7 @@ const handleTabChange = (tab) => {
if (tab === 'security') loadSgLockInfo()
if (tab === 'networking') loadNetworkings()
if (tab === 'monitor' && !metricsData.value) loadMetricsHistory()
- if (tab === 'trafficPolicy') loadTrafficPolicy()
+ if (tab === 'trafficManage') { loadTrafficPolicy(); loadTrafficHourly() }
}
// 请求安全组详情补充 lock 字段(使用用户虚拟机安全组详情接口)
@@ -2215,6 +2258,55 @@ const submitAddTraffic = async () => {
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { trafficPolicyLoading.value = false }
}
+// ---- 每小时流量统计 ----
+const trafficHourlyRange = ref(null)
+const trafficHourlyData = ref([])
+const trafficHourlyLoading = ref(false)
+const trafficHourlyChartRef = ref(null)
+let trafficHourlyChart = null
+
+const loadTrafficHourly = async () => {
+ if (!userGoodsId.value) 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 getUserVmTrafficHourly({
+ user_goods_id: userGoodsId.value,
+ 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 transferVisible = ref(false)
const showTransferUserSelector = ref(false)
@@ -2267,11 +2359,15 @@ const submitEditGoods = async () => {
const cpuChartRef = ref(null)
const memChartRef = ref(null)
const diskChartRef = ref(null)
+const diskIopsChartRef = ref(null)
const netChartRef = ref(null)
+const trafficUsedChartRef = ref(null)
let cpuChart = null
let memChart = null
let diskChart = null
+let diskIopsChart = null
let netChart = null
+let trafficUsedChart = null
const metricsData = ref(null)
const metricsLoading = ref(false)
@@ -2398,9 +2494,28 @@ const renderMetricsCharts = () => {
})
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 memData = metrics.map(m => m.mem_used ?? 0)
+ const memTotal = Math.max(...metrics.map(m => m.mem_total ?? 0))
+ const memAxisFmt = memTotal >= 1048576
+ ? (v) => (v / 1048576).toFixed(1) + ' GB'
+ : memTotal >= 1024
+ ? (v) => (v / 1024).toFixed(0) + ' MB'
+ : (v) => v + ' KB'
+
const diskReadData = metrics.map(m => m.disk_read ?? 0)
const diskWriteData = metrics.map(m => m.disk_write ?? 0)
+
+ const diskReadRate = []
+ const diskWriteRate = []
+ for (let i = 0; i < metrics.length; i++) {
+ if (i === 0) { diskReadRate.push(0); diskWriteRate.push(0); continue }
+ const dt = (new Date(metrics[i].bucket) - new Date(metrics[i - 1].bucket)) / 1000
+ if (dt > 0) {
+ diskReadRate.push(+Math.max(0, ((metrics[i].disk_read ?? 0) - (metrics[i - 1].disk_read ?? 0)) / dt / 1024).toFixed(2))
+ diskWriteRate.push(+Math.max(0, ((metrics[i].disk_write ?? 0) - (metrics[i - 1].disk_write ?? 0)) / dt / 1024).toFixed(2))
+ } else { diskReadRate.push(0); diskWriteRate.push(0) }
+ }
+
const netRxData = metrics.map(m => m.net_rx ?? 0)
const netTxData = metrics.map(m => m.net_tx ?? 0)
@@ -2421,9 +2536,9 @@ const renderMetricsCharts = () => {
if (memChartRef.value) {
if (!memChart) memChart = echarts.init(memChartRef.value)
memChart.setOption({
- tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}
${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 + '%' } },
+ tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}
${p[0].marker} 内存: ${formatMemKB(p[0].value)}` },
+ grid: { ...baseGrid, left: 60 }, xAxis: makeXAxis(),
+ yAxis: { type: 'value', min: 0, max: memTotal || undefined, axisLabel: { fontSize: 10, formatter: memAxisFmt } },
series: [makeSeries('内存', memData, '#67c23a')]
}, true)
}
@@ -2442,6 +2557,20 @@ const renderMetricsCharts = () => {
}, true)
}
+ if (diskIopsChartRef.value) {
+ if (!diskIopsChart) diskIopsChart = echarts.init(diskIopsChartRef.value)
+ diskIopsChart.setOption({
+ tooltip: { trigger: 'axis', formatter: (params) => {
+ let s = params[0].axisValue
+ params.forEach(p => { s += `
${p.marker} ${p.seriesName}: ${formatBytesRaw(p.value)}/s` })
+ return s
+ }},
+ grid: baseGrid, xAxis: makeXAxis(),
+ yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => formatBytesRaw(v) + '/s' } },
+ series: [makeSeries('读取', diskReadRate, '#409eff'), makeSeries('写入', diskWriteRate, '#e6a23c')]
+ }, true)
+ }
+
if (netChartRef.value) {
if (!netChart) netChart = echarts.init(netChartRef.value)
netChart.setOption({
@@ -2455,13 +2584,32 @@ const renderMetricsCharts = () => {
series: [makeSeries('接收', netRxData, '#409eff'), makeSeries('发送', netTxData, '#e6a23c')]
}, true)
}
+
+ if (trafficUsedChartRef.value) {
+ if (!trafficUsedChart) trafficUsedChart = echarts.init(trafficUsedChartRef.value)
+ const trafficDelta = []
+ for (let i = 0; i < metrics.length; i++) {
+ if (i === 0) { trafficDelta.push(0); continue }
+ const delta = Math.max(0, (metrics[i].traffic_used_mb ?? 0) - (metrics[i - 1].traffic_used_mb ?? 0))
+ trafficDelta.push(+delta.toFixed(2))
+ }
+ trafficUsedChart.setOption({
+ tooltip: { trigger: 'axis', formatter: (p) => `${p[0].axisValue}
${p[0].marker} 流量增量: ${p[0].value} MB` },
+ grid: baseGrid, xAxis: makeXAxis(),
+ yAxis: { type: 'value', min: 0, axisLabel: { fontSize: 10, formatter: v => v + ' MB' } },
+ series: [makeSeries('流量增量', trafficDelta, '#722ed1')]
+ }, true)
+ }
}
const disposeCharts = () => {
cpuChart?.dispose(); cpuChart = null
memChart?.dispose(); memChart = null
diskChart?.dispose(); diskChart = null
+ diskIopsChart?.dispose(); diskIopsChart = null
netChart?.dispose(); netChart = null
+ trafficUsedChart?.dispose(); trafficUsedChart = null
+ trafficHourlyChart?.dispose(); trafficHourlyChart = null
}
onMounted(() => { loadDetail() })
diff --git a/src/views/virtualization/HostDetail.vue b/src/views/virtualization/HostDetail.vue
index abbc514..7273a14 100644
--- a/src/views/virtualization/HostDetail.vue
+++ b/src/views/virtualization/HostDetail.vue
@@ -151,6 +151,42 @@
+