This commit is contained in:
zxr
2026-04-15 21:41:47 +08:00
parent 55170bceb0
commit cb8bd05ff7

View File

@@ -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>