This commit is contained in:
zxr
2026-04-14 17:42:31 +08:00
parent 8db158c390
commit 55170bceb0
3 changed files with 221 additions and 258 deletions

View File

@@ -119,6 +119,11 @@ export interface HostMetricsDiskMount {
export interface HostMetricsCpuCard {
usage_percent: number
logical_cores_total: number
socket_count?: number
physical_cores_total?: number
physical_threads_total?: number
cores_per_socket?: number
threads_per_core?: number
}
export interface HostMetricsSummary {
@@ -163,3 +168,82 @@ export const fetchServerNetworkTraffic = (serverIdentity: string, hours = 6) =>
{ params: { server_identity: serverIdentity, hours } },
)
}
/** 虚拟机资产概览 */
export interface VirtualOverviewPayload {
vm_total: number
asset_group_total: number
online_total: number
latest_timestamp?: string
latest_cpu_usage_avg?: number
latest_mem_usage_avg?: number
}
/** 物理机 24h 趋势点 */
export interface PhysicalUsagePoint {
hour: string
cpu_used_percent_avg: number | null
mem_used_percent_avg: number | null
}
export interface PhysicalUsageTrendPayload {
hours: number
points: PhysicalUsagePoint[]
}
/** 资源总量项 */
export interface ResourceTotalGroup {
server_type: 'physical' | 'virtual'
server_count: number
total_vcpu: number
total_mem_bytes: number
}
/** 最新资源总量汇总 */
export interface LatestResourceSummaryPayload {
physical: ResourceTotalGroup
virtual: ResourceTotalGroup
}
/** 按资产聚合项 */
export interface AssetMixedSummaryItem {
asset_id: number
virtual_count: number
physical_count: number
physical_latest_cpu_usage?: number | null
physical_latest_mem_usage?: number | null
}
export interface AssetMixedSummaryPayload {
total: number
data: AssetMixedSummaryItem[]
}
/** 接口1虚拟机资产概览支持分类筛选 */
export const fetchVirtualOverview = (category?: string) => {
return request.get<{ code: number; details?: VirtualOverviewPayload; message?: string }>(
'/DC-Control/v1/servers/assets/virtual/overview',
{ params: { category } },
)
}
/** 接口2physical 近24小时 CPU/内存趋势 */
export const fetchPhysicalUsageTrend24h = () => {
return request.get<{ code: number; details?: PhysicalUsageTrendPayload; message?: string }>(
'/DC-Control/v1/servers/physical/usage/trend-24h',
)
}
/** 接口3physical/virtual 最新资源总量 */
export const fetchLatestResourceSummary = () => {
return request.get<{ code: number; details?: LatestResourceSummaryPayload; message?: string }>(
'/DC-Control/v1/servers/resources/latest/summary',
)
}
/** 接口4按资产汇总 virtual 数量与 physical 最新指标 */
export const fetchAssetMixedSummary = () => {
return request.get<{ code: number; details?: AssetMixedSummaryPayload; message?: string }>(
'/DC-Control/v1/servers/assets/mixed/summary',
)
}

View File

@@ -492,7 +492,7 @@ const resourcePanels = computed<OsResourcePanel[]>(() => {
sysPanel = { ...defaultResourcePanels[1] }
}
const cores = d.cpu?.logical_cores_total ?? 0
const cores = d.cpu?.physical_threads_total ?? d.cpu?.logical_cores_total ?? 0
const usage = d.cpu?.usage_percent ?? 0
const usedCores = cores > 0 ? (usage / 100) * cores : 0
const freeCores = cores > 0 ? Math.max(0, cores - usedCores) : 0

View File

@@ -11,7 +11,7 @@
<div class="stats-info">
<div class="stats-title">虚拟机总数</div>
<div class="stats-value">{{ stats.total }}</div>
<div class="stats-desc"> 8 宿主机</div>
<div class="stats-desc">宿主机 {{ stats.assetGroups }}</div>
</div>
</div>
</a-card>
@@ -25,7 +25,7 @@
<div class="stats-info">
<div class="stats-title">运行中</div>
<div class="stats-value">{{ stats.running }}</div>
<div class="stats-desc text-success">90.7%</div>
<div class="stats-desc text-success">{{ runningRateText }}</div>
</div>
</div>
</a-card>
@@ -38,8 +38,8 @@
</div>
<div class="stats-info">
<div class="stats-title">CPU使用率</div>
<div class="stats-value">{{ stats.cpuUsage }}%</div>
<div class="stats-desc">集群平均</div>
<div class="stats-value">{{ statsCpuUsageText }}%</div>
<div class="stats-desc">平均</div>
</div>
</div>
</a-card>
@@ -52,8 +52,8 @@
</div>
<div class="stats-info">
<div class="stats-title">内存使用率</div>
<div class="stats-value">{{ stats.memoryUsage }}%</div>
<div class="stats-desc">集群平均</div>
<div class="stats-value">{{ statsMemoryUsageText }}%</div>
<div class="stats-desc">平均</div>
</div>
</div>
</a-card>
@@ -85,7 +85,7 @@
<a-col :xs="24" :lg="8">
<a-card title="CPU资源分配" :bordered="false">
<template #extra>
<span class="text-muted">集群总计 256 vCPU</span>
<span class="text-muted">总计 {{ cpuTotalText }} vCPU</span>
</template>
<div class="chart-container">
<Chart :options="cpuChartOptions" height="240px" />
@@ -93,11 +93,11 @@
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot legend-dot-1"></span>
<span>分配 174 vCPU</span>
<span>使用 {{ cpuVirtualText }} vCPU</span>
</div>
<div class="legend-item">
<span class="legend-dot legend-dot-gray"></span>
<span>可用 82 vCPU</span>
<span>总计 {{ cpuPhysicalText }} vCPU</span>
</div>
</div>
</a-card>
@@ -105,7 +105,7 @@
<a-col :xs="24" :lg="8">
<a-card title="内存资源分配" :bordered="false">
<template #extra>
<span class="text-muted">集群总计 512 GB</span>
<span class="text-muted">总计 {{ memoryTotalText }} GB</span>
</template>
<div class="chart-container">
<Chart :options="memoryChartOptions" height="240px" />
@@ -113,11 +113,11 @@
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot legend-dot-2"></span>
<span>已使用 384 GB</span>
<span>已使用 {{ memoryVirtualText }} GB</span>
</div>
<div class="legend-item">
<span class="legend-dot legend-dot-gray"></span>
<span>可用 128 GB</span>
<span>总计 {{ memoryPhysicalText }} GB</span>
</div>
</div>
</a-card>
@@ -130,7 +130,9 @@
<a-card class="host-card" :bordered="false">
<div class="host-header">
<span class="host-name">{{ host.name }}</span>
<a-tag color="green" size="small">在线</a-tag>
<a-tag :color="host.physicalCount > 0 ? 'green' : 'gray'" size="small">
{{ host.physicalCount > 0 ? '有物理机' : '仅虚拟机' }}
</a-tag>
</div>
<div class="host-metrics">
<div class="metric-item">
@@ -151,253 +153,58 @@
<span class="metric-label">虚拟机数</span>
<span class="metric-value-right">{{ host.vmCount }}</span>
</div>
<div class="metric-item metric-vm">
<span class="metric-label">物理机数</span>
<span class="metric-value-right">{{ host.physicalCount }}</span>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 虚拟机列表 -->
<a-card title="虚拟机列表" :bordered="false">
<template #extra>
<a-space>
<a-select v-model="filterHost" placeholder="全部宿主机" style="width: 130px">
<a-option value="">全部宿主机</a-option>
<a-option value="ESXi-01">ESXi-01</a-option>
<a-option value="ESXi-02">ESXi-02</a-option>
<a-option value="ESXi-03">ESXi-03</a-option>
<a-option value="ESXi-04">ESXi-04</a-option>
</a-select>
<a-select v-model="filterStatus" placeholder="全部状态" style="width: 120px">
<a-option value="">全部状态</a-option>
<a-option value="running">运行中</a-option>
<a-option value="stopped">已停止</a-option>
<a-option value="warning">异常</a-option>
</a-select>
</a-space>
</template>
<a-table
:data="filteredVMs"
:columns="columns"
:loading="loading"
:pagination="false"
row-key="name"
>
<!-- 状态列 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.statusValue)" bordered>
{{ record.statusText }}
</a-tag>
</template>
<!-- CPU列 -->
<template #cpu="{ record }">
<div class="progress-cell">
<a-progress
:percent="record.cpu / 100"
:stroke-width="6"
:show-text="false"
:status="getProgressStatus(record.cpu)"
/>
<span class="progress-text">{{ record.cpu }}%</span>
</div>
</template>
<!-- 内存列 -->
<template #memory="{ record }">
<div class="progress-cell">
<a-progress
:percent="record.memory / 100"
:stroke-width="6"
:show-text="false"
:status="getProgressStatus(record.memory)"
/>
<span class="progress-text">{{ record.memory }}%</span>
</div>
</template>
<!-- 存储列 -->
<template #storage="{ record }">
<div class="progress-cell">
<a-progress
:percent="record.storage / 100"
:stroke-width="6"
:show-text="false"
:status="getProgressStatus(record.storage)"
/>
<span class="progress-text">{{ record.storage }}%</span>
</div>
</template>
</a-table>
</a-card>
<!-- 虚拟机列表模块按需求下线暂不渲染 -->
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconStorage,
IconCheckCircleFill,
IconCodeSquare,
IconDriveFile,
} from '@arco-design/web-vue/es/icon'
import Breadcrumb from '@/components/breadcrumb/index.vue'
import Chart from '@/components/chart/index.vue'
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
import {
fetchAssetMixedSummary,
fetchLatestResourceSummary,
fetchPhysicalUsageTrend24h,
fetchVirtualOverview,
type AssetMixedSummaryItem,
} from '@/api/ops/server'
// 统计数据
const stats = ref({
total: 86,
running: 78,
cpuUsage: 68,
memoryUsage: 75,
total: 0,
assetGroups: 0,
running: 0,
cpuUsage: 0,
memoryUsage: 0,
})
const loading = ref(false)
const filterHost = ref('')
const filterStatus = ref('')
// 表格列配置
const columns: TableColumnData[] = [
{
title: '虚拟机名称',
dataIndex: 'name',
width: 130,
},
{
title: '宿主机',
dataIndex: 'host',
width: 100,
},
{
title: '操作系统',
dataIndex: 'os',
width: 150,
},
{
title: '状态',
dataIndex: 'status',
slotName: 'status',
width: 100,
align: 'center',
},
{
title: 'CPU使用率',
dataIndex: 'cpu',
slotName: 'cpu',
width: 150,
},
{
title: '内存使用率',
dataIndex: 'memory',
slotName: 'memory',
width: 150,
},
{
title: '存储使用率',
dataIndex: 'storage',
slotName: 'storage',
width: 150,
},
{
title: '网络流量',
dataIndex: 'network',
width: 100,
align: 'center',
},
]
// 虚拟机数据
const vmData = ref([
{
name: 'VM-Web-01',
host: 'ESXi-01',
os: 'CentOS 7.9',
statusValue: 'running',
statusText: '运行中',
cpu: 65,
memory: 72,
storage: 45,
network: '1.2 Gbps',
},
{
name: 'VM-DB-01',
host: 'ESXi-01',
os: 'Ubuntu 22.04',
statusValue: 'running',
statusText: '运行中',
cpu: 85,
memory: 88,
storage: 78,
network: '850 Mbps',
},
{
name: 'VM-App-01',
host: 'ESXi-02',
os: 'Windows Server 2022',
statusValue: 'running',
statusText: '运行中',
cpu: 42,
memory: 55,
storage: 35,
network: '450 Mbps',
},
{
name: 'VM-Cache-01',
host: 'ESXi-02',
os: 'CentOS 8',
statusValue: 'warning',
statusText: '高负载',
cpu: 92,
memory: 95,
storage: 60,
network: '2.1 Gbps',
},
{
name: 'VM-Dev-01',
host: 'ESXi-03',
os: 'Ubuntu 20.04',
statusValue: 'stopped',
statusText: '已停止',
cpu: 0,
memory: 0,
storage: 25,
network: '-',
},
{
name: 'VM-Test-01',
host: 'ESXi-03',
os: 'Debian 11',
statusValue: 'running',
statusText: '运行中',
cpu: 28,
memory: 35,
storage: 42,
network: '320 Mbps',
},
])
// 过滤后的虚拟机列表
const filteredVMs = computed(() => {
let result = vmData.value
if (filterHost.value) {
result = result.filter((vm) => vm.host === filterHost.value)
}
if (filterStatus.value) {
result = result.filter((vm) => vm.statusValue === filterStatus.value)
}
return result
})
// 宿主机状态
const hostStatus = ref([
{ name: 'ESXi-01', cpu: 65, memory: 78, vmCount: 24 },
{ name: 'ESXi-02', cpu: 72, memory: 85, vmCount: 22 },
{ name: 'ESXi-03', cpu: 45, memory: 52, vmCount: 18 },
{ name: 'ESXi-04', cpu: 58, memory: 68, vmCount: 22 },
])
const hostStatus = ref<
Array<{
name: string
cpu: number
memory: number
vmCount: number
physicalCount: number
}>
>([])
// 资源使用趋势图表配置
const performanceChartOptions = ref({
@@ -413,7 +220,7 @@ const performanceChartOptions = ref({
xAxis: {
type: 'category',
boundaryGap: false,
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
data: [] as string[],
},
yAxis: {
type: 'value',
@@ -425,7 +232,7 @@ const performanceChartOptions = ref({
name: 'CPU',
type: 'line',
smooth: true,
data: [45, 38, 72, 85, 78, 55, 42],
data: [] as number[],
lineStyle: {
width: 2,
color: '#165DFF',
@@ -438,7 +245,7 @@ const performanceChartOptions = ref({
name: '内存',
type: 'line',
smooth: true,
data: [62, 58, 75, 82, 79, 65, 60],
data: [] as number[],
lineStyle: {
width: 2,
color: '#F7BA1E',
@@ -464,8 +271,8 @@ const cpuChartOptions = ref({
show: false,
},
data: [
{ value: 68, name: '已分配', itemStyle: { color: '#165DFF' } },
{ value: 32, name: '可用', itemStyle: { color: '#E5E6EB' } },
{ value: 0, name: 'virtual', itemStyle: { color: '#165DFF' } },
{ value: 0, name: 'physical', itemStyle: { color: '#E5E6EB' } },
],
},
],
@@ -485,34 +292,106 @@ const memoryChartOptions = ref({
show: false,
},
data: [
{ value: 75, name: '已使用', itemStyle: { color: '#14C9C9' } },
{ value: 25, name: '可用', itemStyle: { color: '#E5E6EB' } },
{ value: 0, name: 'virtual', itemStyle: { color: '#14C9C9' } },
{ value: 0, name: 'physical', itemStyle: { color: '#E5E6EB' } },
],
},
],
})
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
running: 'green',
stopped: 'gray',
warning: 'orange',
}
return colorMap[status] || 'gray'
const cpuVirtual = ref(0)
const cpuPhysical = ref(0)
const memVirtualGb = ref(0)
const memPhysicalGb = ref(0)
const roundToTwo = (value: number) => Math.round(value * 100) / 100
const formatMaxTwoDecimals = (value: number) => {
const rounded = roundToTwo(value)
return Number.isInteger(rounded) ? String(rounded) : String(rounded)
}
// 获取进度条状态
const getProgressStatus = (value: number) => {
if (value >= 90) return 'danger'
if (value >= 70) return 'warning'
return 'normal'
const runningRateText = computed(() => {
if (!stats.value.total) return '0%'
return `${formatMaxTwoDecimals((stats.value.running / stats.value.total) * 100)}%`
})
const statsCpuUsageText = computed(() => formatMaxTwoDecimals(stats.value.cpuUsage))
const statsMemoryUsageText = computed(() => formatMaxTwoDecimals(stats.value.memoryUsage))
const cpuTotalText = computed(() => formatMaxTwoDecimals(cpuVirtual.value + cpuPhysical.value))
const cpuVirtualText = computed(() => formatMaxTwoDecimals(cpuVirtual.value))
const cpuPhysicalText = computed(() => formatMaxTwoDecimals(cpuPhysical.value))
const memoryTotalText = computed(() => formatMaxTwoDecimals(memVirtualGb.value + memPhysicalGb.value))
const memoryVirtualText = computed(() => formatMaxTwoDecimals(memVirtualGb.value))
const memoryPhysicalText = computed(() => formatMaxTwoDecimals(memPhysicalGb.value))
const toGb = (bytes: number) => bytes / 1024 / 1024 / 1024
const safeNum = (v: number | null | undefined) => (typeof v === 'number' ? v : 0)
const hourLabel = (v: string) => {
const d = new Date(v)
if (Number.isNaN(d.getTime())) return v
return `${String(d.getHours()).padStart(2, '0')}:00`
}
// 获取数据
const fetchData = async () => {
// TODO: 从API获取数据
loading.value = false
loading.value = true
try {
const [overviewRes, trendRes, resourceRes, mixedRes] = await Promise.all([
fetchVirtualOverview(),
fetchPhysicalUsageTrend24h(),
fetchLatestResourceSummary(),
fetchAssetMixedSummary(),
])
if (overviewRes.code !== 0 || !overviewRes.details) {
Message.error(overviewRes.message || '加载虚拟机概览失败')
return
}
const overview = overviewRes.details
stats.value.total = overview.vm_total || 0
stats.value.assetGroups = overview.asset_group_total || 0
stats.value.running = overview.online_total || 0
stats.value.cpuUsage = roundToTwo(safeNum(overview.latest_cpu_usage_avg))
stats.value.memoryUsage = roundToTwo(safeNum(overview.latest_mem_usage_avg))
if (trendRes.code === 0 && trendRes.details?.points) {
const labels = trendRes.details.points.map((p) => hourLabel(p.hour))
const cpuSeries = trendRes.details.points.map((p) => roundToTwo(safeNum(p.cpu_used_percent_avg)))
const memSeries = trendRes.details.points.map((p) => roundToTwo(safeNum(p.mem_used_percent_avg)))
;(performanceChartOptions.value.xAxis as { data: string[] }).data = labels
;(performanceChartOptions.value.series[0] as { data: number[] }).data = cpuSeries
;(performanceChartOptions.value.series[1] as { data: number[] }).data = memSeries
}
if (resourceRes.code === 0 && resourceRes.details) {
cpuVirtual.value = roundToTwo(safeNum(resourceRes.details.virtual?.total_vcpu))
cpuPhysical.value = roundToTwo(safeNum(resourceRes.details.physical?.total_vcpu))
memVirtualGb.value = roundToTwo(toGb(safeNum(resourceRes.details.virtual?.total_mem_bytes)))
memPhysicalGb.value = roundToTwo(toGb(safeNum(resourceRes.details.physical?.total_mem_bytes)))
;(cpuChartOptions.value.series[0] as { data: Array<{ value: number; name: string }> }).data = [
{ value: cpuVirtual.value, name: 'virtual' },
{ value: cpuPhysical.value, name: 'physical' },
]
;(memoryChartOptions.value.series[0] as { data: Array<{ value: number; name: string }> }).data = [
{ value: memVirtualGb.value, name: 'virtual' },
{ value: memPhysicalGb.value, name: 'physical' },
]
}
if (mixedRes.code === 0 && mixedRes.details?.data) {
hostStatus.value = (mixedRes.details.data as AssetMixedSummaryItem[]).map((item) => ({
name: `Asset-${item.asset_id}`,
cpu: Number(safeNum(item.physical_latest_cpu_usage).toFixed(2)),
memory: Number(safeNum(item.physical_latest_mem_usage).toFixed(2)),
vmCount: item.virtual_count || 0,
physicalCount: item.physical_count || 0,
}))
} else {
hostStatus.value = []
}
} catch (e: any) {
Message.error(e?.message || '加载虚拟化监控数据失败')
} finally {
loading.value = false
}
}
// 初始化