fix
This commit is contained in:
@@ -1,143 +1,140 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-safe />
|
||||
</div>
|
||||
<div class="stats-icon stats-icon-primary"><icon-code-square /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">安全设备</div>
|
||||
<div class="stats-value">{{ stats.totalDevices }}</div>
|
||||
<div class="stats-desc">在线设备数</div>
|
||||
<div class="stats-title">CPU使用率</div>
|
||||
<div class="stats-value">{{ formatPercent(summary.cpuUsage) }}</div>
|
||||
<div class="stats-desc">安全设备平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-success">
|
||||
<icon-check-circle-fill />
|
||||
</div>
|
||||
<div class="stats-icon stats-icon-cyan"><icon-drive-file /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">威胁拦截</div>
|
||||
<div class="stats-value">{{ stats.threatsBlocked }}</div>
|
||||
<div class="stats-desc">今日拦截</div>
|
||||
<div class="stats-title">内存使用率</div>
|
||||
<div class="stats-value">{{ formatPercent(summary.memoryUsage) }}</div>
|
||||
<div class="stats-desc">安全设备平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-danger">
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-icon stats-icon-warning"><icon-storage /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">高危威胁</div>
|
||||
<div class="stats-value">{{ stats.highRiskThreats }}</div>
|
||||
<div class="stats-desc text-danger">需立即处理</div>
|
||||
<div class="stats-title">虚拟内存使用率</div>
|
||||
<div class="stats-value">{{ formatPercent(summary.swapUsage) }}</div>
|
||||
<div class="stats-desc">Swap 平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-muted">
|
||||
<icon-close-circle-fill />
|
||||
</div>
|
||||
<div class="stats-icon stats-icon-success"><icon-storage /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">离线设备</div>
|
||||
<div class="stats-value">{{ stats.offlineDevices }}</div>
|
||||
<div class="stats-desc text-danger">VPN网关异常</div>
|
||||
<div class="stats-title">硬盘空间使用率</div>
|
||||
<div class="stats-value">{{ formatPercent(summary.diskUsage) }}</div>
|
||||
<div class="stats-desc">安全设备平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary"><icon-thunderbolt /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">磁盘IO吞吐</div>
|
||||
<div class="stats-value">{{ formatThroughput(summary.diskIoThroughput) }}</div>
|
||||
<div class="stats-desc">平均值</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="8" :xl="4">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-success"><icon-check-circle-fill /></div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">可用率</div>
|
||||
<div class="stats-value">{{ formatPercent(summary.availability) }}</div>
|
||||
<div class="stats-desc">在线 {{ summary.onlineCount }}/{{ summary.totalCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="威胁趋势" :bordered="false">
|
||||
<a-col :xs="24" :xl="14">
|
||||
<a-card title="设备资源指标对比" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>检测</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-2"></span>
|
||||
<span>拦截</span>
|
||||
</span>
|
||||
</a-space>
|
||||
<span class="text-muted">最多展示 10 台设备</span>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="threatChartOptions" height="280px" />
|
||||
<Chart :options="metricsCompareChartOptions" height="300px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="威胁分类统计" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">今日威胁类型分布</span>
|
||||
</template>
|
||||
<div class="threat-category-list">
|
||||
<div v-for="item in threatCategories" :key="item.name" class="threat-category-item">
|
||||
<div class="threat-category-header">
|
||||
<div class="threat-category-icon" :style="{ backgroundColor: item.bgColor }">
|
||||
<component :is="item.icon" :style="{ color: item.color }" />
|
||||
</div>
|
||||
<div class="threat-category-info">
|
||||
<div class="threat-category-name">{{ item.name }}</div>
|
||||
<a-progress :percent="item.percent" :stroke-width="8" :show-text="false" :status="item.progressStatus" />
|
||||
</div>
|
||||
<span class="threat-category-value" :style="{ color: item.color }">{{ item.value }}</span>
|
||||
</div>
|
||||
<a-col :xs="24" :xl="10">
|
||||
<a-card title="控制器状态分布" :bordered="false" class="controller-card">
|
||||
<div class="controller-overview">
|
||||
<div class="controller-item">
|
||||
<div class="controller-label">在线控制器</div>
|
||||
<div class="controller-value text-success">{{ summary.onlineCount }}</div>
|
||||
</div>
|
||||
<div class="controller-item">
|
||||
<div class="controller-label">离线控制器</div>
|
||||
<div class="controller-value text-danger">{{ summary.offlineCount }}</div>
|
||||
</div>
|
||||
<div class="controller-item">
|
||||
<div class="controller-label">异常控制器</div>
|
||||
<div class="controller-value text-warning">{{ summary.errorCount }}</div>
|
||||
</div>
|
||||
<div class="controller-item">
|
||||
<div class="controller-label">未知状态</div>
|
||||
<div class="controller-value text-muted-strong">{{ summary.unknownCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card title="设备列表" :bordered="false">
|
||||
<a-card title="控制器运行状态" :bordered="false">
|
||||
<template #extra>
|
||||
<a-select v-model="filterType" placeholder="全部类型" style="width: 150px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="firewall">防火墙</a-option>
|
||||
<a-option value="ids">IDS</a-option>
|
||||
<a-option value="ips">IPS</a-option>
|
||||
<a-option value="waf">WAF</a-option>
|
||||
<a-option value="vpn">VPN</a-option>
|
||||
</a-select>
|
||||
<a-space>
|
||||
<a-select v-model="filterType" placeholder="全部类型" style="width: 140px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="firewall">防火墙</a-option>
|
||||
<a-option value="ids">IDS</a-option>
|
||||
<a-option value="ips">IPS</a-option>
|
||||
<a-option value="waf">WAF</a-option>
|
||||
<a-option value="vpn">VPN</a-option>
|
||||
<a-option value="other">其他</a-option>
|
||||
</a-select>
|
||||
<a-input-search v-model="keyword" placeholder="搜索名称/标识" style="width: 200px" allow-clear />
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table :data="filteredDevices" :columns="columns" :loading="loading" :pagination="false" row-key="id">
|
||||
<!-- 状态列 -->
|
||||
|
||||
<a-table :data="filteredDevices" :columns="columns" :loading="loading" :pagination="{ pageSize: 10 }" row-key="id">
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)" bordered>
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 响应时间列 -->
|
||||
<template #responseTime="{ record }">
|
||||
<span v-if="record.response_time">{{ record.response_time.toFixed(2) }} ms</span>
|
||||
<span v-else>-</span>
|
||||
<template #controllerStatus="{ record }">
|
||||
<a-tag :color="getControllerStatusColor(record.controller_status)">{{ record.controller_status_text }}</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 运行时长列 -->
|
||||
<template #uptime="{ record }">
|
||||
<span>{{ formatUptime(record.uptime) }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 最近检查时间列 -->
|
||||
<template #lastCheckTime="{ record }">
|
||||
<span>{{ formatTime(record.last_check_time) }}</span>
|
||||
{{ formatTime(record.last_check_time) }}
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -145,193 +142,220 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconSafe,
|
||||
IconCheckCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconCloseCircleFill,
|
||||
IconCodeSquare,
|
||||
IconDriveFile,
|
||||
IconStorage,
|
||||
IconThunderbolt,
|
||||
IconLock,
|
||||
IconCode,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { fetchSecurityServiceList, type SecurityServiceItem } from '@/api/ops/security'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import {
|
||||
fetchSecurityMetricsLatest,
|
||||
fetchSecurityServiceList,
|
||||
type SecurityMetric,
|
||||
type SecurityServiceItem,
|
||||
} from '@/api/ops/security'
|
||||
|
||||
type ControllerStatus = 'online' | 'offline' | 'warning' | 'unknown'
|
||||
|
||||
interface DeviceMetricRow extends SecurityServiceItem {
|
||||
cpu_usage: number | null
|
||||
memory_usage: number | null
|
||||
swap_usage: number | null
|
||||
disk_usage: number | null
|
||||
disk_io_throughput: number | null
|
||||
availability: number | null
|
||||
controller_status: ControllerStatus
|
||||
controller_status_text: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const filterType = ref('')
|
||||
const deviceData = ref<SecurityServiceItem[]>([])
|
||||
const keyword = ref('')
|
||||
const devices = ref<DeviceMetricRow[]>([])
|
||||
|
||||
const metricNameAliases = {
|
||||
cpu_usage: ['cpu_usage'],
|
||||
memory_usage: ['memory_used_percent', 'memory_usage', 'mem_usage', 'memory_percent', 'used_percent'],
|
||||
swap_usage: ['swap_used_percent', 'swap_usage', 'virtual_memory_usage', 'memory_virt'],
|
||||
disk_usage: ['disk_used_percent', 'disk_usage', 'disk_space_used_percent'],
|
||||
disk_io_throughput: ['disk_io_throughput', 'disk_io', 'disk_io_bps', 'disk_io_mb_s'],
|
||||
availability: ['availability', 'success_rate', 'available_percent'],
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{ title: '设备名称', dataIndex: 'name', width: 150 },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '服务标识', dataIndex: 'service_identity', width: 180 },
|
||||
{ title: '控制器状态', dataIndex: 'controller_status', slotName: 'controllerStatus', width: 120, align: 'center' },
|
||||
{ title: 'CPU使用率', dataIndex: 'cpu_usage', width: 110, render: ({ record }) => formatPercent(record.cpu_usage) },
|
||||
{ title: '内存使用率', dataIndex: 'memory_usage', width: 110, render: ({ record }) => formatPercent(record.memory_usage) },
|
||||
{ title: '虚拟内存使用率', dataIndex: 'swap_usage', width: 130, render: ({ record }) => formatPercent(record.swap_usage) },
|
||||
{ title: '硬盘空间', dataIndex: 'disk_usage', width: 110, render: ({ record }) => formatPercent(record.disk_usage) },
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
title: '磁盘IO吞吐',
|
||||
dataIndex: 'disk_io_throughput',
|
||||
width: 120,
|
||||
render: ({ record }) => formatThroughput(record.disk_io_throughput),
|
||||
},
|
||||
{
|
||||
title: '服务标识',
|
||||
dataIndex: 'service_identity',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'response_time',
|
||||
slotName: 'responseTime',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '运行时长',
|
||||
dataIndex: 'uptime',
|
||||
slotName: 'uptime',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '连续错误',
|
||||
dataIndex: 'continuous_errors',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '最近检查',
|
||||
dataIndex: 'last_check_time',
|
||||
slotName: 'lastCheckTime',
|
||||
width: 180,
|
||||
},
|
||||
{ title: '可用率', dataIndex: 'availability', width: 100, render: ({ record }) => formatPercent(record.availability) },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 90, align: 'center' },
|
||||
{ title: '最近检查', dataIndex: 'last_check_time', slotName: 'lastCheckTime', width: 170 },
|
||||
]
|
||||
|
||||
// 统计数据
|
||||
const stats = computed(() => {
|
||||
const total = deviceData.value.length
|
||||
const online = deviceData.value.filter((d) => d.status === 'online').length
|
||||
const offline = deviceData.value.filter((d) => d.status === 'offline').length
|
||||
const error = deviceData.value.filter((d) => d.status === 'error').length
|
||||
const filteredDevices = computed(() => {
|
||||
const typeFiltered = filterType.value ? devices.value.filter((d) => d.type === filterType.value) : devices.value
|
||||
const text = keyword.value.trim().toLowerCase()
|
||||
if (!text) return typeFiltered
|
||||
return typeFiltered.filter((d) => d.name.toLowerCase().includes(text) || d.service_identity.toLowerCase().includes(text))
|
||||
})
|
||||
|
||||
const summary = computed(() => {
|
||||
const totalCount = devices.value.length
|
||||
const onlineCount = devices.value.filter((d) => d.status === 'online').length
|
||||
const offlineCount = devices.value.filter((d) => d.status === 'offline').length
|
||||
const errorCount = devices.value.filter((d) => d.status === 'error').length
|
||||
const unknownCount = totalCount - onlineCount - offlineCount - errorCount
|
||||
return {
|
||||
totalDevices: online,
|
||||
threatsBlocked: '-',
|
||||
highRiskThreats: error,
|
||||
offlineDevices: offline,
|
||||
cpuUsage: avgMetric(devices.value.map((d) => d.cpu_usage)),
|
||||
memoryUsage: avgMetric(devices.value.map((d) => d.memory_usage)),
|
||||
swapUsage: avgMetric(devices.value.map((d) => d.swap_usage)),
|
||||
diskUsage: avgMetric(devices.value.map((d) => d.disk_usage)),
|
||||
diskIoThroughput: avgMetric(devices.value.map((d) => d.disk_io_throughput)),
|
||||
availability: avgMetric(devices.value.map((d) => d.availability)) ?? (totalCount ? (onlineCount / totalCount) * 100 : null),
|
||||
totalCount,
|
||||
onlineCount,
|
||||
offlineCount,
|
||||
errorCount,
|
||||
unknownCount,
|
||||
}
|
||||
})
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!filterType.value) return deviceData.value
|
||||
return deviceData.value.filter((device) => device.type === filterType.value)
|
||||
const metricsCompareChartOptions = computed(() => {
|
||||
const sample = filteredDevices.value.slice(0, 10)
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 4 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: sample.map((item) => item.name || item.service_identity),
|
||||
axisLabel: { interval: 0, rotate: 25 },
|
||||
},
|
||||
yAxis: { type: 'value', name: '%' },
|
||||
series: [
|
||||
{ name: 'CPU', type: 'bar', data: sample.map((item) => toNum(item.cpu_usage)), itemStyle: { color: '#165DFF' } },
|
||||
{ name: '内存', type: 'bar', data: sample.map((item) => toNum(item.memory_usage)), itemStyle: { color: '#14C9C9' } },
|
||||
{ name: '虚拟内存', type: 'bar', data: sample.map((item) => toNum(item.swap_usage)), itemStyle: { color: '#F7BA1E' } },
|
||||
{ name: '硬盘空间', type: 'bar', data: sample.map((item) => toNum(item.disk_usage)), itemStyle: { color: '#00B42A' } },
|
||||
{ name: '可用率', type: 'line', smooth: true, data: sample.map((item) => toNum(item.availability)), itemStyle: { color: '#722ED1' } },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
// 威胁分类数据
|
||||
const threatCategories = ref([
|
||||
{
|
||||
name: 'DDoS攻击',
|
||||
value: 456,
|
||||
percent: 45,
|
||||
icon: IconThunderbolt,
|
||||
color: '#F53F3F',
|
||||
bgColor: 'rgba(245, 63, 63, 0.1)',
|
||||
progressStatus: 'danger' as const,
|
||||
},
|
||||
{
|
||||
name: '暴力破解',
|
||||
value: 234,
|
||||
percent: 30,
|
||||
icon: IconLock,
|
||||
color: '#FF7D00',
|
||||
bgColor: 'rgba(255, 125, 0, 0.1)',
|
||||
progressStatus: 'warning' as const,
|
||||
},
|
||||
{
|
||||
name: 'SQL注入',
|
||||
value: 189,
|
||||
percent: 25,
|
||||
icon: IconCode,
|
||||
color: '#165DFF',
|
||||
bgColor: 'rgba(22, 93, 255, 0.1)',
|
||||
progressStatus: 'normal' as const,
|
||||
},
|
||||
{
|
||||
name: 'XSS攻击',
|
||||
value: 156,
|
||||
percent: 20,
|
||||
icon: IconCode,
|
||||
color: '#14C9C9',
|
||||
bgColor: 'rgba(20, 201, 201, 0.1)',
|
||||
progressStatus: 'normal' as const,
|
||||
},
|
||||
])
|
||||
const parseMetricsMap = (metrics: SecurityMetric[]) => {
|
||||
const map = new Map<string, number>()
|
||||
for (const metric of metrics || []) {
|
||||
map.set(metric.metric_name, Number(metric.metric_value))
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
// 威胁趋势图表配置
|
||||
const threatChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '数量',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '检测',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [45, 32, 89, 156, 123, 78, 56],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '拦截',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [12, 8, 25, 45, 38, 22, 15],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14C9C9',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#14C9C9',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const pickMetricValue = (metricsMap: Map<string, number>, key: keyof typeof metricNameAliases) => {
|
||||
const aliases = metricNameAliases[key]
|
||||
for (const alias of aliases) {
|
||||
if (metricsMap.has(alias)) return metricsMap.get(alias) ?? null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getControllerStatus = (item: SecurityServiceItem, availability: number | null): ControllerStatus => {
|
||||
if (item.status === 'online') return 'online'
|
||||
if (item.status === 'offline') return 'offline'
|
||||
if (item.status === 'error') return 'warning'
|
||||
if (availability !== null) return availability >= 95 ? 'online' : 'warning'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const controllerStatusTextMap: Record<ControllerStatus, string> = {
|
||||
online: '正常',
|
||||
offline: '离线',
|
||||
warning: '异常',
|
||||
unknown: '未知',
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await fetchSecurityServiceList({ page: 1, size: 100 })
|
||||
if (!response || response.code !== 0 || !response.details?.data) {
|
||||
Message.error(response?.message || '获取安全设备列表失败')
|
||||
devices.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const baseList = response.details.data
|
||||
const metricsRespList = await Promise.all(
|
||||
baseList.map((item) =>
|
||||
fetchSecurityMetricsLatest(item.service_identity).catch(() => ({
|
||||
code: -1,
|
||||
details: { metrics: [] as SecurityMetric[] },
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
devices.value = baseList.map((item, idx) => {
|
||||
const latest = metricsRespList[idx]
|
||||
const metricsMap = parseMetricsMap(latest?.details?.metrics || [])
|
||||
const availability = pickMetricValue(metricsMap, 'availability')
|
||||
const controllerStatus = getControllerStatus(item, availability)
|
||||
return {
|
||||
...item,
|
||||
cpu_usage: pickMetricValue(metricsMap, 'cpu_usage'),
|
||||
memory_usage: pickMetricValue(metricsMap, 'memory_usage'),
|
||||
swap_usage: pickMetricValue(metricsMap, 'swap_usage'),
|
||||
disk_usage: pickMetricValue(metricsMap, 'disk_usage'),
|
||||
disk_io_throughput: pickMetricValue(metricsMap, 'disk_io_throughput'),
|
||||
availability,
|
||||
controller_status: controllerStatus,
|
||||
controller_status_text: controllerStatusTextMap[controllerStatus],
|
||||
}
|
||||
})
|
||||
} catch (error: any) {
|
||||
Message.error(error?.message || '加载安全设备监控数据失败')
|
||||
devices.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const avgMetric = (values: Array<number | null>) => {
|
||||
const nums = values.filter((v): v is number => typeof v === 'number' && Number.isFinite(v))
|
||||
if (!nums.length) return null
|
||||
return nums.reduce((acc, cur) => acc + cur, 0) / nums.length
|
||||
}
|
||||
|
||||
const toNum = (val: number | null) => (typeof val === 'number' && Number.isFinite(val) ? Number(val.toFixed(2)) : 0)
|
||||
|
||||
const formatPercent = (val: number | null) => {
|
||||
if (val === null || !Number.isFinite(val)) return '-'
|
||||
return `${val.toFixed(2)}%`
|
||||
}
|
||||
|
||||
const formatThroughput = (val: number | null) => {
|
||||
if (val === null || !Number.isFinite(val)) return '-'
|
||||
return `${val.toFixed(2)}`
|
||||
}
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
@@ -343,7 +367,16 @@ const getStatusColor = (status: string) => {
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
/** 获取状态文本 */
|
||||
const getControllerStatusColor = (status: ControllerStatus) => {
|
||||
const colorMap: Record<ControllerStatus, string> = {
|
||||
online: 'green',
|
||||
offline: 'gray',
|
||||
warning: 'orange',
|
||||
unknown: 'gray',
|
||||
}
|
||||
return colorMap[status]
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
@@ -355,39 +388,6 @@ const getStatusText = (status: string) => {
|
||||
return textMap[status] || '未知'
|
||||
}
|
||||
|
||||
/** 格式化运行时长 */
|
||||
const formatUptime = (uptime: number) => {
|
||||
if (!uptime || uptime === 0) return '-'
|
||||
const days = Math.floor(uptime / 86400)
|
||||
const hours = Math.floor((uptime % 86400) / 3600)
|
||||
const minutes = Math.floor((uptime % 3600) / 60)
|
||||
if (days > 0) return `${days}天 ${hours}小时`
|
||||
if (hours > 0) return `${hours}小时 ${minutes}分钟`
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
|
||||
/** 格式化时间 */
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
/** 获取数据 */
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await fetchSecurityServiceList()
|
||||
if (response && response.code === 0 && response.details) {
|
||||
deviceData.value = response.details.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取安全设备列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
@@ -408,6 +408,10 @@ export default {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
@@ -440,14 +444,14 @@ export default {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-muted {
|
||||
background-color: rgba(134, 144, 156, 0.1);
|
||||
color: rgb(var(--gray-6));
|
||||
&-warning {
|
||||
background-color: rgba(247, 186, 30, 0.12);
|
||||
color: #f7ba1e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,115 +479,56 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
.controller-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
min-height: 300px;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.controller-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.controller-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165dff;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #14c9c9;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
.controller-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.threat-category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 280px;
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.threat-category-item {
|
||||
.threat-category-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.threat-category-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.threat-category-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.threat-category-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.threat-category-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
.text-warning {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
.threats-value {
|
||||
font-weight: 500;
|
||||
|
||||
&.danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&.normal {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.cpu-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.cpu-text {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
min-width: 36px;
|
||||
}
|
||||
.text-muted-strong {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user