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:
@@ -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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* ================================
|
||||
* 主控服务接口 - 数据卷管理
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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?.network_disabled" size="small" type="danger" effect="dark" style="margin-left:4px">已断网</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>
|
||||
@@ -47,6 +48,8 @@
|
||||
<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 command="resetMac">重置MAC地址</el-dropdown-item>
|
||||
<el-dropdown-item command="disconnectNetwork" :disabled="vm?.network_disabled">断网</el-dropdown-item>
|
||||
<el-dropdown-item command="connectNetwork" :disabled="!vm?.network_disabled">恢复网络</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>
|
||||
@@ -1125,7 +1128,8 @@ import {
|
||||
getUserGoodsDetail,
|
||||
getUserVmMetricsHistory,
|
||||
getUserVmTrafficPolicy, updateUserVmTrafficPolicy, addUserVmFixedTraffic, addUserVmTemporaryTraffic,
|
||||
setUserVmNetworkPrimary, resetUserVmMac
|
||||
setUserVmNetworkPrimary, resetUserVmMac,
|
||||
disconnectUserVmNetwork, connectUserVmNetwork
|
||||
} from '@/api/admin/userVm'
|
||||
import { deleteNetwork as deletePointNetwork } from '@/api/admin/kvmService'
|
||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||
@@ -1372,6 +1376,8 @@ const handleMoreCmd = (cmd) => {
|
||||
if (cmd === 'migrate') openMigrateVm()
|
||||
if (cmd === 'editGoods') openEditGoods()
|
||||
if (cmd === 'resetMac') handleResetVmMac()
|
||||
if (cmd === 'disconnectNetwork') handleDisconnectVmNetwork()
|
||||
if (cmd === 'connectNetwork') handleConnectVmNetwork()
|
||||
if (cmd === 'delete') {
|
||||
ElMessageBox.confirm('确定删除该用户虚拟机吗?', '删除确认', { type: 'error' }).then(async () => {
|
||||
try {
|
||||
@@ -1746,7 +1752,7 @@ const handleSetVmNetworkPrimary = (row) => {
|
||||
'设置主IP', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await setUserVmNetworkPrimary({ user_good_id: userGoodsId.value, network_id: row.id })
|
||||
const res = await setUserVmNetworkPrimary({ user_goods_id: userGoodsId.value, network_id: row.id })
|
||||
if (res?.data?.code === 200) { ElMessage.success('设置主IP成功,虚拟机将重启'); loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '设置主IP失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '设置主IP失败')) }
|
||||
@@ -1766,6 +1772,32 @@ const handleResetVmMac = () => {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleDisconnectVmNetwork = () => {
|
||||
ElMessageBox.confirm(
|
||||
'断开虚拟机外部网络连接?断网后虚拟机将无法访问外部网络。',
|
||||
'断网', { confirmButtonText: '确定断网', cancelButtonText: '取消', type: 'warning' }
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await disconnectUserVmNetwork({ user_goods_id: userGoodsId.value })
|
||||
if (res?.data?.code === 200) { ElMessage.success('已断网'); loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '断网失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '断网失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleConnectVmNetwork = () => {
|
||||
ElMessageBox.confirm(
|
||||
'恢复虚拟机外部网络连接?',
|
||||
'恢复网络', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'info' }
|
||||
).then(async () => {
|
||||
try {
|
||||
const res = await connectUserVmNetwork({ user_goods_id: userGoodsId.value })
|
||||
if (res?.data?.code === 200) { ElMessage.success('网络已恢复'); loadDetail() }
|
||||
else ElMessage.error(extractApiError(res?.data, '恢复网络失败'))
|
||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '恢复网络失败')) }
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// ---- 绑定网络 ----
|
||||
const showBindNetworkSelector = ref(false)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
<el-dropdown-item command="refactorVm" :disabled="isMigrating">重构虚拟机</el-dropdown-item>
|
||||
<el-dropdown-item command="updateTraffic" :disabled="isMigrating">修改带宽</el-dropdown-item>
|
||||
<el-dropdown-item command="resetMac" :disabled="isMigrating">重置MAC地址</el-dropdown-item>
|
||||
<el-dropdown-item command="disconnectNetwork" :disabled="isMigrating || detail.network_disabled">断网</el-dropdown-item>
|
||||
<el-dropdown-item command="connectNetwork" :disabled="isMigrating || !detail.network_disabled">恢复网络</el-dropdown-item>
|
||||
<el-dropdown-item divided command="rebuild" :disabled="isMigrating">重装虚拟机</el-dropdown-item>
|
||||
<el-dropdown-item command="rescue">救援模式</el-dropdown-item>
|
||||
<el-dropdown-item command="exitRescue">退出救援</el-dropdown-item>
|
||||
@@ -51,6 +53,7 @@
|
||||
<span class="status-value">
|
||||
<span class="status-dot" :class="detail.status === 'running' ? 'dot-running' : 'dot-other'"></span>
|
||||
{{ vmStatusLabel(detail.status) }}
|
||||
<el-tag v-if="detail.network_disabled" type="danger" size="small" effect="dark" style="margin-left:8px">已断网</el-tag>
|
||||
<el-tag v-if="isMigrating" type="warning" size="small" effect="dark" style="margin-left:8px">迁移中</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
@@ -664,6 +667,34 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 每小时流量 -->
|
||||
<el-tab-pane label="流量统计" name="trafficHourly">
|
||||
<div class="section-block">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">每小时流量</h3>
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<el-date-picker
|
||||
v-model="trafficHourlyRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
size="small"
|
||||
style="width: 360px"
|
||||
:shortcuts="monitorShortcuts"
|
||||
@change="loadTrafficHourly"
|
||||
/>
|
||||
<el-button size="small" :icon="Refresh" @click="loadTrafficHourly" :loading="trafficHourlyLoading">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-card v-if="trafficHourlyData.length" shadow="hover" class="metrics-card" style="margin-top:12px">
|
||||
<template #header><span class="metrics-title"><el-icon><Refresh /></el-icon> 每小时流量(MB)</span></template>
|
||||
<div ref="trafficHourlyChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
<el-empty v-else-if="!trafficHourlyLoading" description="暂无流量统计数据" :image-size="60" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 流量策略 -->
|
||||
<el-tab-pane label="流量策略" name="vmTrafficPolicy">
|
||||
<div class="section-block">
|
||||
@@ -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('<br/>') },
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user