feat
This commit is contained in:
870
src/views/ops/pages/monitor/environment/index.vue
Normal file
870
src/views/ops/pages/monitor/environment/index.vue
Normal file
@@ -0,0 +1,870 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<IconThermometer :size="24" stroke="1.5" />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">平均温度</div>
|
||||
<div class="stats-value">24.2°C</div>
|
||||
<div class="stats-desc text-success">正常范围</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-cyan">
|
||||
<IconDroplet :size="24" stroke="1.5" />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">平均湿度</div>
|
||||
<div class="stats-value">46%</div>
|
||||
<div class="stats-desc text-success">正常范围</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-green">
|
||||
<IconPlug :size="24" stroke="1.5" />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">总功耗</div>
|
||||
<div class="stats-value">156 kW</div>
|
||||
<div class="stats-desc">PUE 1.45</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-purple">
|
||||
<IconAirConditioning :size="24" stroke="1.5" />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">空调状态</div>
|
||||
<div class="stats-value">3/4</div>
|
||||
<div class="stats-desc">1台维护中</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 环境概览 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="温湿度趋势" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>温度°C</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-2"></span>
|
||||
<span>湿度%</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="tempChartOptions" height="280px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="UPS状态" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">不间断电源系统</span>
|
||||
</template>
|
||||
<div class="ups-list">
|
||||
<div v-for="ups in upsList" :key="ups.name" class="ups-item">
|
||||
<div class="ups-icon">
|
||||
<IconCircleCheck :size="24" stroke="1.5" class="battery-icon" />
|
||||
</div>
|
||||
<div class="ups-info">
|
||||
<div class="ups-header">
|
||||
<span class="ups-name">{{ ups.name }}</span>
|
||||
<a-tag color="green">在线</a-tag>
|
||||
</div>
|
||||
<div class="ups-details">
|
||||
<span>负载: {{ ups.load }}</span>
|
||||
<span>电池: {{ ups.battery }}</span>
|
||||
<span>后备: {{ ups.backup }}</span>
|
||||
</div>
|
||||
<div class="ups-progress">
|
||||
<a-progress
|
||||
:percent="ups.loadPercent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
color="#165DFF"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="精密空调" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">制冷系统状态</span>
|
||||
</template>
|
||||
<div class="ac-list">
|
||||
<div v-for="ac in acList" :key="ac.name" class="ac-item">
|
||||
<div class="ac-left">
|
||||
<IconAirConditioning :size="16" stroke="1.5" :class="['ac-icon', { 'ac-icon-offline': ac.status === 'offline' }]" />
|
||||
<span class="ac-name">{{ ac.name }}</span>
|
||||
</div>
|
||||
<div class="ac-right">
|
||||
<span class="ac-temp">{{ ac.temp }}</span>
|
||||
<span class="ac-humidity">{{ ac.humidity }}</span>
|
||||
<a-tag :color="ac.status === 'online' ? 'green' : 'gray'">{{ ac.mode }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 电力分布 -->
|
||||
<a-row :gutter="16" class="power-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="power-card" :bordered="false">
|
||||
<div class="power-header">
|
||||
<div class="power-icon power-icon-primary">
|
||||
<IconPlug :size="20" stroke="1.5" />
|
||||
</div>
|
||||
<div class="power-info">
|
||||
<div class="power-title">A路市电</div>
|
||||
<div class="power-desc">主供电</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="power-stats">
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">电压</span>
|
||||
<span class="power-stat-value text-success">380V</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">电流</span>
|
||||
<span class="power-stat-value">245A</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">功率</span>
|
||||
<span class="power-stat-value">82 kW</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="power-card" :bordered="false">
|
||||
<div class="power-header">
|
||||
<div class="power-icon power-icon-primary">
|
||||
<IconPlug :size="20" stroke="1.5" />
|
||||
</div>
|
||||
<div class="power-info">
|
||||
<div class="power-title">B路市电</div>
|
||||
<div class="power-desc">备用供电</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="power-stats">
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">电压</span>
|
||||
<span class="power-stat-value text-success">380V</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">电流</span>
|
||||
<span class="power-stat-value">218A</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">功率</span>
|
||||
<span class="power-stat-value">74 kW</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="power-card" :bordered="false">
|
||||
<div class="power-header">
|
||||
<div class="power-icon power-icon-success">
|
||||
<IconBolt :size="20" stroke="1.5" />
|
||||
</div>
|
||||
<div class="power-info">
|
||||
<div class="power-title">发电机</div>
|
||||
<div class="power-desc">备用电源</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="power-stats">
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">状态</span>
|
||||
<a-tag color="green">待机</a-tag>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">燃油</span>
|
||||
<span class="power-stat-value">95%</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">上次测试</span>
|
||||
<span class="power-stat-value">3天前</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="power-card" :bordered="false">
|
||||
<div class="power-header">
|
||||
<div class="power-icon power-icon-primary">
|
||||
<IconChartBar :size="20" stroke="1.5" />
|
||||
</div>
|
||||
<div class="power-info">
|
||||
<div class="power-title">能效统计</div>
|
||||
<div class="power-desc">本月数据</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="power-stats">
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">PUE</span>
|
||||
<span class="power-stat-value text-success">1.45</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">月耗电</span>
|
||||
<span class="power-stat-value">112,000 kWh</span>
|
||||
</div>
|
||||
<div class="power-stat-item">
|
||||
<span class="power-stat-label">碳排放</span>
|
||||
<span class="power-stat-value">78.4 吨</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card title="设备列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-select v-model="selectedType" placeholder="全部类型" style="width: 150px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="温度">温度传感器</a-option>
|
||||
<a-option value="湿度">湿度传感器</a-option>
|
||||
<a-option value="UPS">UPS</a-option>
|
||||
<a-option value="空调">空调</a-option>
|
||||
<a-option value="配电">配电</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredDevices"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconThermometer,
|
||||
IconDroplet,
|
||||
IconPlug,
|
||||
IconAirConditioning,
|
||||
IconCircleCheck,
|
||||
IconBolt,
|
||||
IconChartBar,
|
||||
} from '@tabler/icons-vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const selectedType = ref('')
|
||||
|
||||
// UPS数据
|
||||
const upsList = ref([
|
||||
{
|
||||
name: 'UPS-01',
|
||||
load: '62%',
|
||||
loadPercent: 62,
|
||||
battery: '100%',
|
||||
backup: '45分钟',
|
||||
},
|
||||
{
|
||||
name: 'UPS-02',
|
||||
load: '58%',
|
||||
loadPercent: 58,
|
||||
battery: '100%',
|
||||
backup: '52分钟',
|
||||
},
|
||||
])
|
||||
|
||||
// 空调数据
|
||||
const acList = ref([
|
||||
{ name: '空调-01', status: 'online', temp: '22°C', humidity: '45%', mode: '制冷' },
|
||||
{ name: '空调-02', status: 'offline', temp: '-', humidity: '-', mode: '维护' },
|
||||
{ name: '空调-03', status: 'online', temp: '21°C', humidity: '46%', mode: '制冷' },
|
||||
{ name: '空调-04', status: 'online', temp: '22°C', humidity: '44%', mode: '待机' },
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
dataIndex: 'location',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '阈值',
|
||||
dataIndex: 'threshold',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '趋势/备注',
|
||||
dataIndex: 'trend',
|
||||
width: 150,
|
||||
},
|
||||
]
|
||||
|
||||
// 设备数据
|
||||
const environmentDevices = ref([
|
||||
{
|
||||
name: '机房A-温度传感器-01',
|
||||
type: '温度',
|
||||
location: '机柜A1-顶部',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '23.5°C',
|
||||
threshold: '18-28°C',
|
||||
trend: '+0.2°C/h',
|
||||
},
|
||||
{
|
||||
name: '机房A-温度传感器-02',
|
||||
type: '温度',
|
||||
location: '机柜A2-顶部',
|
||||
statusValue: 'warning',
|
||||
statusText: '偏高',
|
||||
value: '27.8°C',
|
||||
threshold: '18-28°C',
|
||||
trend: '+0.5°C/h',
|
||||
},
|
||||
{
|
||||
name: '机房A-湿度传感器-01',
|
||||
type: '湿度',
|
||||
location: '机柜A1-中部',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '45%',
|
||||
threshold: '40-60%',
|
||||
trend: '稳定',
|
||||
},
|
||||
{
|
||||
name: 'UPS-01',
|
||||
type: 'UPS',
|
||||
location: '机房A-配电间',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
value: '负载 62%',
|
||||
threshold: '< 80%',
|
||||
trend: '电池 100%',
|
||||
},
|
||||
{
|
||||
name: 'UPS-02',
|
||||
type: 'UPS',
|
||||
location: '机房B-配电间',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
value: '负载 58%',
|
||||
threshold: '< 80%',
|
||||
trend: '电池 100%',
|
||||
},
|
||||
{
|
||||
name: '精密空调-01',
|
||||
type: '空调',
|
||||
location: '机房A-西侧',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
value: '制冷中',
|
||||
threshold: '-',
|
||||
trend: '送风 22°C',
|
||||
},
|
||||
{
|
||||
name: '精密空调-02',
|
||||
type: '空调',
|
||||
location: '机房A-东侧',
|
||||
statusValue: 'offline',
|
||||
statusText: '维护',
|
||||
value: '-',
|
||||
threshold: '-',
|
||||
trend: '-',
|
||||
},
|
||||
{
|
||||
name: 'PDU-01',
|
||||
type: '配电',
|
||||
location: '机柜A1',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '12.5 kW',
|
||||
threshold: '< 20 kW',
|
||||
trend: '电流 18.2A',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!selectedType.value) {
|
||||
return environmentDevices.value
|
||||
}
|
||||
return environmentDevices.value.filter((device) => device.type === selectedType.value)
|
||||
})
|
||||
|
||||
// 温湿度趋势图表配置
|
||||
const tempChartOptions = 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: '温度°C',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '湿度%',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '温度',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [22.5, 22.0, 23.5, 25.0, 26.5, 24.5, 23.0],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '湿度',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
yAxisIndex: 1,
|
||||
data: [45, 44, 46, 48, 50, 47, 45],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14C9C9',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#14C9C9',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'gray',
|
||||
warning: 'orange',
|
||||
success: 'green',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'EnvironmentMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-green {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-purple {
|
||||
background-color: rgba(114, 46, 209, 0.1);
|
||||
color: #722ed1;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.arco-card) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: calc(100% - 57px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165dff;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #14c9c9;
|
||||
}
|
||||
}
|
||||
|
||||
.ups-list {
|
||||
padding: 8px 0;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ups-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ups-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.battery-icon {
|
||||
font-size: 24px;
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
}
|
||||
|
||||
.ups-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ups-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ups-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.ups-details {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ups-progress {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ac-list {
|
||||
padding: 8px 0;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ac-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ac-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ac-icon {
|
||||
font-size: 16px;
|
||||
color: rgb(var(--success-6));
|
||||
|
||||
&.ac-icon-offline {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.ac-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.ac-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ac-temp,
|
||||
.ac-humidity {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.power-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.power-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.power-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.power-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
}
|
||||
|
||||
.power-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.power-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.power-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.power-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.power-stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.power-stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.power-stat-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
542
src/views/ops/pages/monitor/log/index.vue
Normal file
542
src/views/ops/pages/monitor/log/index.vue
Normal file
@@ -0,0 +1,542 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-file />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">今日日志</div>
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-desc">总采集量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-danger">
|
||||
<icon-close-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">错误日志</div>
|
||||
<div class="stats-value">{{ stats.error }}</div>
|
||||
<div class="stats-desc text-danger">较昨日 +12%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">警告日志</div>
|
||||
<div class="stats-value">{{ stats.warning }}</div>
|
||||
<div class="stats-desc text-success">较昨日 -5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-info-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">信息日志</div>
|
||||
<div class="stats-value">{{ stats.info }}</div>
|
||||
<div class="stats-desc">正常范围</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">
|
||||
<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>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="logTrendChartOptions" height="280px" />
|
||||
</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="source-list">
|
||||
<div v-for="item in logSources" :key="item.name" class="source-item">
|
||||
<div class="source-header">
|
||||
<span class="source-name">{{ item.name }}</span>
|
||||
<span class="source-value">{{ item.value }} ({{ item.percent }}%)</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="item.color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<a-card title="实时日志" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-input-search
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索日志..."
|
||||
style="width: 250px"
|
||||
allow-clear
|
||||
/>
|
||||
<a-select v-model="selectedLevel" placeholder="全部级别" style="width: 120px">
|
||||
<a-option value="">全部级别</a-option>
|
||||
<a-option value="error">ERROR</a-option>
|
||||
<a-option value="warn">WARN</a-option>
|
||||
<a-option value="info">INFO</a-option>
|
||||
<a-option value="debug">DEBUG</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredLogs"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="timestamp"
|
||||
>
|
||||
<!-- 级别列 -->
|
||||
<template #level="{ record }">
|
||||
<a-tag :color="getLevelColor(record.levelValue)" bordered>
|
||||
{{ record.levelText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 消息列 -->
|
||||
<template #message="{ record }">
|
||||
<span class="log-message">{{ record.message }}</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconFile,
|
||||
IconCloseCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconInfoCircleFill,
|
||||
} 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'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: '2.4M',
|
||||
error: '1,234',
|
||||
warning: '3,567',
|
||||
info: '2.39M',
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const selectedLevel = ref('')
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '级别',
|
||||
dataIndex: 'level',
|
||||
slotName: 'level',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
dataIndex: 'source',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '消息内容',
|
||||
dataIndex: 'message',
|
||||
slotName: 'message',
|
||||
},
|
||||
{
|
||||
title: '次数',
|
||||
dataIndex: 'count',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
// 日志数据
|
||||
const logData = ref([
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Web-Server-01',
|
||||
message: 'Connection refused: Unable to connect to database server',
|
||||
count: '156',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:32:12',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'API-Gateway',
|
||||
message: 'Rate limit exceeded for IP 192.168.1.100',
|
||||
count: '89',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:58',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Auth-Service',
|
||||
message: 'User login successful: admin@example.com',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:45',
|
||||
levelValue: 'error',
|
||||
levelText: 'ERROR',
|
||||
source: 'Payment-Service',
|
||||
message: 'Transaction failed: Insufficient funds',
|
||||
count: '23',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:30',
|
||||
levelValue: 'warn',
|
||||
levelText: 'WARN',
|
||||
source: 'Storage-Server',
|
||||
message: 'Disk usage exceeded 85% threshold',
|
||||
count: '5',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:15',
|
||||
levelValue: 'info',
|
||||
levelText: 'INFO',
|
||||
source: 'Scheduler',
|
||||
message: 'Backup job completed successfully',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:31:00',
|
||||
levelValue: 'critical',
|
||||
levelText: 'CRITICAL',
|
||||
source: 'Core-Router',
|
||||
message: 'Interface GigabitEthernet0/1 down',
|
||||
count: '1',
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-15 14:30:45',
|
||||
levelValue: 'debug',
|
||||
levelText: 'DEBUG',
|
||||
source: 'Cache-Server',
|
||||
message: 'Cache hit ratio: 94.5%',
|
||||
count: '1',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的日志列表
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logData.value
|
||||
|
||||
if (selectedLevel.value) {
|
||||
result = result.filter((log) => log.levelValue === selectedLevel.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(log) =>
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
log.source.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 日志来源分布数据
|
||||
const logSources = ref([
|
||||
{ name: 'Web服务器', value: '856K', percent: 35, color: '#165DFF' },
|
||||
{ name: '数据库', value: '624K', percent: 26, color: '#14C9C9' },
|
||||
{ name: '应用服务', value: '480K', percent: 20, color: '#F7BA1E' },
|
||||
{ name: '网络设备', value: '288K', percent: 12, color: '#722ED1' },
|
||||
{ name: '安全设备', value: '168K', percent: 7, color: '#F53F3F' },
|
||||
])
|
||||
|
||||
// 日志趋势图表配置
|
||||
const logTrendChartOptions = 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: [1200, 800, 2800, 4500, 3800, 2200, 1500],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '异常',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [89, 45, 156, 234, 189, 112, 78],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#F53F3F',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F53F3F',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取级别颜色
|
||||
const getLevelColor = (level: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
error: 'red',
|
||||
critical: 'red',
|
||||
warn: 'orange',
|
||||
info: 'green',
|
||||
debug: 'gray',
|
||||
}
|
||||
return colorMap[level] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'LogMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background-color: rgba(255, 125, 0, 0.1);
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&-muted {
|
||||
background-color: rgba(134, 144, 156, 0.1);
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #F53F3F;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.source-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.source-item {
|
||||
.source-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.source-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.log-message {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
709
src/views/ops/pages/monitor/network/index.vue
Normal file
709
src/views/ops/pages/monitor/network/index.vue
Normal file
@@ -0,0 +1,709 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-storage />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">网络设备</div>
|
||||
<div class="stats-value">32</div>
|
||||
<div class="stats-desc">在线 31 / 离线 1</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-cyan">
|
||||
<icon-drive-file />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">端口总数</div>
|
||||
<div class="stats-value">1,024</div>
|
||||
<div class="stats-desc">使用率 78%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-green">
|
||||
<icon-link />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">总带宽</div>
|
||||
<div class="stats-value">45.6 Gbps</div>
|
||||
<div class="stats-desc">峰值流量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">告警设备</div>
|
||||
<div class="stats-value">2</div>
|
||||
<div class="stats-desc text-danger">需要关注</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">
|
||||
<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>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="trafficChartOptions" height="280px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card title="Top 5 端口流量" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">当前流量最大的端口</span>
|
||||
</template>
|
||||
<div class="port-list">
|
||||
<div v-for="item in topPorts" :key="item.name" class="port-item">
|
||||
<div class="port-header">
|
||||
<span class="port-name">{{ item.name }}</span>
|
||||
<span class="port-value">{{ item.value }}</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="getPortColor(item.percent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 快速统计 -->
|
||||
<a-row :gutter="16" class="quick-stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="quick-stats-card" :bordered="false">
|
||||
<div class="quick-stats-header">
|
||||
<div class="quick-stats-icon quick-stats-icon-primary">
|
||||
<icon-storage />
|
||||
</div>
|
||||
<div class="quick-stats-info">
|
||||
<div class="quick-stats-title">核心层</div>
|
||||
<div class="quick-stats-desc">2 设备在线</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats-footer">
|
||||
<span class="quick-stats-value">99.9%</span>
|
||||
<a-tag color="green">正常</a-tag>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="quick-stats-card" :bordered="false">
|
||||
<div class="quick-stats-header">
|
||||
<div class="quick-stats-icon quick-stats-icon-cyan">
|
||||
<icon-apps />
|
||||
</div>
|
||||
<div class="quick-stats-info">
|
||||
<div class="quick-stats-title">汇聚层</div>
|
||||
<div class="quick-stats-desc">4 设备在线</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats-footer">
|
||||
<span class="quick-stats-value">100%</span>
|
||||
<a-tag color="green">正常</a-tag>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="quick-stats-card" :bordered="false">
|
||||
<div class="quick-stats-header">
|
||||
<div class="quick-stats-icon quick-stats-icon-purple">
|
||||
<icon-drive-file />
|
||||
</div>
|
||||
<div class="quick-stats-info">
|
||||
<div class="quick-stats-title">接入层</div>
|
||||
<div class="quick-stats-desc">24 设备在线</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats-footer">
|
||||
<span class="quick-stats-value">95.8%</span>
|
||||
<a-tag color="orange">告警</a-tag>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="quick-stats-card" :bordered="false">
|
||||
<div class="quick-stats-header">
|
||||
<div class="quick-stats-icon quick-stats-icon-green">
|
||||
<icon-wifi />
|
||||
</div>
|
||||
<div class="quick-stats-info">
|
||||
<div class="quick-stats-title">无线网络</div>
|
||||
<div class="quick-stats-desc">1 控制器离线</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-stats-footer">
|
||||
<span class="quick-stats-value">0%</span>
|
||||
<a-tag color="red">异常</a-tag>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card title="设备列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-select v-model="selectedType" placeholder="全部类型" style="width: 150px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="核心交换机">核心交换机</a-option>
|
||||
<a-option value="汇聚交换机">汇聚交换机</a-option>
|
||||
<a-option value="接入交换机">接入交换机</a-option>
|
||||
<a-option value="边界路由">路由器</a-option>
|
||||
<a-option value="无线控制器">无线设备</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredDevices"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 带宽使用列 -->
|
||||
<template #traffic="{ record }">
|
||||
<div class="traffic-cell">
|
||||
<a-progress
|
||||
:percent="record.trafficPercent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="getTrafficColor(record.trafficPercent)"
|
||||
/>
|
||||
<span class="traffic-text">{{ record.trafficPercent }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconStorage,
|
||||
IconDriveFile,
|
||||
IconLink,
|
||||
IconExclamationCircle,
|
||||
IconApps,
|
||||
IconWifi,
|
||||
} 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'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const selectedType = ref('')
|
||||
|
||||
// 统计数据
|
||||
const topPorts = ref([
|
||||
{ name: 'Core-SW-01 Port 1', value: '8.5 Gbps', percent: 85 },
|
||||
{ name: 'Core-SW-02 Port 1', value: '7.2 Gbps', percent: 72 },
|
||||
{ name: 'Router-01 Port 1', value: '6.8 Gbps', percent: 68 },
|
||||
{ name: 'Dist-SW-01 Port 24', value: '4.5 Gbps', percent: 45 },
|
||||
{ name: 'Access-SW-01 Port 48', value: '3.2 Gbps', percent: 32 },
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '型号',
|
||||
dataIndex: 'model',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '管理IP',
|
||||
dataIndex: 'ip',
|
||||
width: 130,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '运行时间',
|
||||
dataIndex: 'uptime',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '端口使用',
|
||||
dataIndex: 'ports',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '带宽使用',
|
||||
dataIndex: 'traffic',
|
||||
slotName: 'traffic',
|
||||
width: 200,
|
||||
},
|
||||
]
|
||||
|
||||
// 设备数据
|
||||
const networkDevices = ref([
|
||||
{
|
||||
name: 'Core-SW-01',
|
||||
type: '核心交换机',
|
||||
model: 'Cisco Nexus 9336C',
|
||||
ip: '10.0.0.1',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
uptime: '365天 12小时',
|
||||
ports: '36/36',
|
||||
trafficPercent: 45,
|
||||
},
|
||||
{
|
||||
name: 'Core-SW-02',
|
||||
type: '核心交换机',
|
||||
model: 'Cisco Nexus 9336C',
|
||||
ip: '10.0.0.2',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
uptime: '365天 12小时',
|
||||
ports: '34/36',
|
||||
trafficPercent: 52,
|
||||
},
|
||||
{
|
||||
name: 'Dist-SW-01',
|
||||
type: '汇聚交换机',
|
||||
model: 'Cisco Catalyst 9500',
|
||||
ip: '10.0.1.1',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
uptime: '180天 8小时',
|
||||
ports: '24/48',
|
||||
trafficPercent: 38,
|
||||
},
|
||||
{
|
||||
name: 'Access-SW-01',
|
||||
type: '接入交换机',
|
||||
model: 'Cisco Catalyst 9300',
|
||||
ip: '10.0.2.1',
|
||||
statusValue: 'warning',
|
||||
statusText: '高负载',
|
||||
uptime: '90天 4小时',
|
||||
ports: '45/48',
|
||||
trafficPercent: 88,
|
||||
},
|
||||
{
|
||||
name: 'Router-01',
|
||||
type: '边界路由',
|
||||
model: 'Cisco ASR 1002-HX',
|
||||
ip: '10.0.0.254',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
uptime: '425天 6小时',
|
||||
ports: '4/4',
|
||||
trafficPercent: 62,
|
||||
},
|
||||
{
|
||||
name: 'AP-Controller',
|
||||
type: '无线控制器',
|
||||
model: 'Cisco 9800-40',
|
||||
ip: '10.0.3.1',
|
||||
statusValue: 'offline',
|
||||
statusText: '离线',
|
||||
uptime: '-',
|
||||
ports: '-',
|
||||
trafficPercent: 0,
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!selectedType.value) {
|
||||
return networkDevices.value
|
||||
}
|
||||
return networkDevices.value.filter((device) => device.type === selectedType.value)
|
||||
})
|
||||
|
||||
// 流量趋势图表配置
|
||||
const trafficChartOptions = 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: 'Gbps',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '入站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [2.5, 1.2, 8.5, 12.4, 10.8, 5.6, 3.2],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '出站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [1.8, 0.8, 6.2, 9.8, 8.2, 4.2, 2.4],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14C9C9',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#14C9C9',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
warning: 'orange',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取端口流量颜色
|
||||
const getPortColor = (percent: number) => {
|
||||
if (percent >= 80) return '#F53F3F'
|
||||
if (percent >= 60) return '#FF7D00'
|
||||
return '#165DFF'
|
||||
}
|
||||
|
||||
// 获取带宽使用颜色
|
||||
const getTrafficColor = (percent: number) => {
|
||||
if (percent >= 80) return '#F53F3F'
|
||||
if (percent >= 60) return '#FF7D00'
|
||||
return '#165DFF'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NetworkMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-green {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background-color: rgba(255, 125, 0, 0.1);
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165dff;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #14c9c9;
|
||||
}
|
||||
}
|
||||
|
||||
.port-list {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.port-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.port-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.port-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.port-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.quick-stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.quick-stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.quick-stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.quick-stats-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-purple {
|
||||
background-color: rgba(114, 46, 209, 0.1);
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
&-green {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
}
|
||||
|
||||
.quick-stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.quick-stats-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.quick-stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.quick-stats-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quick-stats-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.traffic-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.traffic-text {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
725
src/views/ops/pages/monitor/safety/index.vue
Normal file
725
src/views/ops/pages/monitor/safety/index.vue
Normal file
@@ -0,0 +1,725 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-danger">
|
||||
<icon-fire />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">消防系统</div>
|
||||
<div class="stats-value">正常</div>
|
||||
<div class="stats-desc">12 个探测器在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-lock />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">门禁系统</div>
|
||||
<div class="stats-value">8/8</div>
|
||||
<div class="stats-desc text-success">全部在线</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-cyan">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">漏水检测</div>
|
||||
<div class="stats-value">1</div>
|
||||
<div class="stats-desc text-warning">需要关注</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-success">
|
||||
<icon-cloud />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">气体检测</div>
|
||||
<div class="stats-value">正常</div>
|
||||
<div class="stats-desc text-success">所有区域安全</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 安全概览 -->
|
||||
<a-row :gutter="16" class="overview-row">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="消防系统状态" :bordered="false">
|
||||
<template #extra>
|
||||
<a-tag color="green">正常</a-tag>
|
||||
</template>
|
||||
<div class="fire-list">
|
||||
<div v-for="item in fireSystemList" :key="item.name" class="fire-item">
|
||||
<div class="fire-icon">
|
||||
<icon-fire v-if="item.icon === 'IconFire'" />
|
||||
<icon-notification v-else-if="item.icon === 'IconNotification'" />
|
||||
<icon-cloud v-else-if="item.icon === 'IconCloud'" />
|
||||
</div>
|
||||
<div class="fire-info">
|
||||
<div class="fire-name">{{ item.name }}</div>
|
||||
<div class="fire-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
<icon-check-circle class="fire-status" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="门禁出入记录" :bordered="false">
|
||||
<template #extra>
|
||||
<a-link>查看全部</a-link>
|
||||
</template>
|
||||
<div class="access-list">
|
||||
<div v-for="(log, index) in accessLogs" :key="index" class="access-item">
|
||||
<div class="access-left">
|
||||
<div class="access-user">{{ log.user }} - {{ log.action }}</div>
|
||||
<div class="access-location">{{ log.location }}</div>
|
||||
</div>
|
||||
<div class="access-right">
|
||||
<div class="access-time">{{ log.time }}</div>
|
||||
<a-tag :color="getStatusColor(log.status)" size="small">
|
||||
{{ getStatusText(log.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="告警记录" :bordered="false">
|
||||
<template #extra>
|
||||
<a-tag color="orangered">1 条待处理</a-tag>
|
||||
</template>
|
||||
<div class="alarm-list">
|
||||
<div class="alarm-item alarm-item-warning">
|
||||
<div class="alarm-header">
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title">漏水检测告警</div>
|
||||
<div class="alarm-desc">机房A-空调下方检测到微量水汽</div>
|
||||
</div>
|
||||
<span class="alarm-time">5分钟前</span>
|
||||
</div>
|
||||
<div class="alarm-actions">
|
||||
<a-button type="primary" size="small" status="warning">处理</a-button>
|
||||
<a-button size="small">忽略</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alarm-item alarm-item-muted">
|
||||
<div class="alarm-header">
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title text-muted">门禁异常</div>
|
||||
<div class="alarm-desc">王五刷卡失败 - 已处理</div>
|
||||
</div>
|
||||
<span class="alarm-time">1小时前</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alarm-item alarm-item-muted">
|
||||
<div class="alarm-header">
|
||||
<div class="alarm-content">
|
||||
<div class="alarm-title text-muted">设备巡检</div>
|
||||
<div class="alarm-desc">消防系统月度巡检完成</div>
|
||||
</div>
|
||||
<span class="alarm-time">2小时前</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 区域状态 -->
|
||||
<a-row :gutter="16" class="area-row">
|
||||
<a-col v-for="area in areaStatusList" :key="area.name" :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="area-card" :bordered="false">
|
||||
<div class="area-header">
|
||||
<span class="area-name">{{ area.name }}</span>
|
||||
<icon-camera class="area-camera" />
|
||||
</div>
|
||||
<div class="area-status-list">
|
||||
<div class="area-status-item">
|
||||
<span class="area-status-label">消防</span>
|
||||
<a-tag :color="getStatusColor('success')" size="small">{{ area.fire }}</a-tag>
|
||||
</div>
|
||||
<div class="area-status-item">
|
||||
<span class="area-status-label">门禁</span>
|
||||
<a-tag :color="area.access === '关闭' ? 'green' : 'orange'" size="small">{{ area.access }}</a-tag>
|
||||
</div>
|
||||
<div class="area-status-item">
|
||||
<span class="area-status-label">漏水</span>
|
||||
<a-tag :color="area.water === '正常' ? 'green' : 'orange'" size="small">{{ area.water }}</a-tag>
|
||||
</div>
|
||||
<div class="area-status-item">
|
||||
<span class="area-status-label">气体</span>
|
||||
<a-tag :color="getStatusColor('success')" size="small">{{ area.gas }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card title="设备列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-select v-model="selectedType" placeholder="全部类型" style="width: 150px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="消防">消防</a-option>
|
||||
<a-option value="门禁">门禁</a-option>
|
||||
<a-option value="漏水">漏水</a-option>
|
||||
<a-option value="气体">气体</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredDevices"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconFire,
|
||||
IconLock,
|
||||
IconThunderbolt,
|
||||
IconCloud,
|
||||
IconCheckCircle,
|
||||
IconCamera,
|
||||
IconNotification,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const selectedType = ref('')
|
||||
|
||||
// 消防系统列表
|
||||
const fireSystemList = ref([
|
||||
{ name: '烟感探测器', desc: '8 个设备全部正常', icon: 'IconFire' },
|
||||
{ name: '温感探测器', desc: '4 个设备全部正常', icon: 'IconFire' },
|
||||
{ name: '报警系统', desc: '待机状态', icon: 'IconNotification' },
|
||||
{ name: '气体灭火', desc: '药剂充足 100%', icon: 'IconCloud' },
|
||||
])
|
||||
|
||||
// 出入记录
|
||||
const accessLogs = ref([
|
||||
{ time: '14:32:15', user: '张三', action: '刷卡进入', location: '机房A-主入口', status: 'success' },
|
||||
{ time: '14:28:45', user: '李四', action: '刷卡进入', location: '机房B-主入口', status: 'success' },
|
||||
{ time: '14:15:20', user: '王五', action: '刷卡失败', location: '机房A-主入口', status: 'error' },
|
||||
{ time: '13:58:30', user: '张三', action: '刷卡离开', location: '机房A-主入口', status: 'success' },
|
||||
{ time: '13:45:10', user: '系统', action: '自动解锁', location: '机房A-应急出口', status: 'warning' },
|
||||
])
|
||||
|
||||
// 区域状态
|
||||
const areaStatusList = ref([
|
||||
{ name: '机房A', fire: '正常', access: '关闭', water: '正常', gas: '正常' },
|
||||
{ name: '机房B', fire: '正常', access: '关闭', water: '正常', gas: '正常' },
|
||||
{ name: '配电间', fire: '正常', access: '关闭', water: '正常', gas: '正常' },
|
||||
{ name: 'UPS室', fire: '正常', access: '开启', water: '告警', gas: '正常' },
|
||||
])
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '位置',
|
||||
dataIndex: 'location',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
dataIndex: 'value',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '最后检查',
|
||||
dataIndex: 'lastCheck',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '电池',
|
||||
dataIndex: 'battery',
|
||||
width: 100,
|
||||
},
|
||||
]
|
||||
|
||||
// 设备数据
|
||||
const safetyDevices = ref([
|
||||
{
|
||||
name: '烟感探测器-A01',
|
||||
type: '消防',
|
||||
location: '机房A-顶部',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '正常',
|
||||
lastCheck: '5分钟前',
|
||||
battery: '100%',
|
||||
},
|
||||
{
|
||||
name: '烟感探测器-A02',
|
||||
type: '消防',
|
||||
location: '机房A-走廊',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '正常',
|
||||
lastCheck: '5分钟前',
|
||||
battery: '95%',
|
||||
},
|
||||
{
|
||||
name: '温感探测器-A01',
|
||||
type: '消防',
|
||||
location: '机房A-配电间',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '32°C',
|
||||
lastCheck: '5分钟前',
|
||||
battery: '-',
|
||||
},
|
||||
{
|
||||
name: '门禁-A01',
|
||||
type: '门禁',
|
||||
location: '机房A-主入口',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
value: '关闭',
|
||||
lastCheck: '实时',
|
||||
battery: '-',
|
||||
},
|
||||
{
|
||||
name: '门禁-A02',
|
||||
type: '门禁',
|
||||
location: '机房A-应急出口',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
value: '关闭',
|
||||
lastCheck: '实时',
|
||||
battery: '-',
|
||||
},
|
||||
{
|
||||
name: '漏水检测-A01',
|
||||
type: '漏水',
|
||||
location: '机房A-地板下',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '正常',
|
||||
lastCheck: '1分钟前',
|
||||
battery: '-',
|
||||
},
|
||||
{
|
||||
name: '漏水检测-A02',
|
||||
type: '漏水',
|
||||
location: '机房A-空调下方',
|
||||
statusValue: 'warning',
|
||||
statusText: '检测中',
|
||||
value: '微量水汽',
|
||||
lastCheck: '1分钟前',
|
||||
battery: '-',
|
||||
},
|
||||
{
|
||||
name: '气体检测-A01',
|
||||
type: '气体',
|
||||
location: '机房A-UPS区域',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
value: '正常',
|
||||
lastCheck: '10分钟前',
|
||||
battery: '88%',
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!selectedType.value) {
|
||||
return safetyDevices.value
|
||||
}
|
||||
return safetyDevices.value.filter((device) => device.type === selectedType.value)
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'gray',
|
||||
warning: 'orange',
|
||||
success: 'green',
|
||||
error: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
success: '成功',
|
||||
error: '失败',
|
||||
warning: '警告',
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SafetyMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.overview-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.arco-card) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.fire-list {
|
||||
.fire-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fire-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgb(var(--success-6));
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.fire-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fire-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.fire-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fire-status {
|
||||
color: rgb(var(--success-6));
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.access-list {
|
||||
.access-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.access-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.access-user {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.access-location {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.access-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.access-time {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
.alarm-item {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.alarm-item-warning {
|
||||
background-color: rgba(255, 125, 0, 0.05);
|
||||
border-left: 3px solid rgb(var(--warning-6));
|
||||
|
||||
.alarm-title {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
}
|
||||
|
||||
&.alarm-item-muted {
|
||||
background-color: var(--color-fill-1);
|
||||
border-left: 3px solid var(--color-border-2);
|
||||
}
|
||||
}
|
||||
|
||||
.alarm-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.alarm-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.alarm-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
white-space: nowrap;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.alarm-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.area-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.area-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.area-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.area-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.area-camera {
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.area-status-list {
|
||||
.area-status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.area-status-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
637
src/views/ops/pages/monitor/security/index.vue
Normal file
637
src/views/ops/pages/monitor/security/index.vue
Normal file
@@ -0,0 +1,637 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-safe />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">安全设备</div>
|
||||
<div class="stats-value">{{ stats.totalDevices }}</div>
|
||||
<div class="stats-desc">在线设备数</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<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">{{ stats.threatsBlocked }}</div>
|
||||
<div class="stats-desc">今日拦截</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<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-info">
|
||||
<div class="stats-title">高危威胁</div>
|
||||
<div class="stats-value">{{ stats.highRiskThreats }}</div>
|
||||
<div class="stats-desc text-danger">需立即处理</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<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-info">
|
||||
<div class="stats-title">离线设备</div>
|
||||
<div class="stats-value">{{ stats.offlineDevices }}</div>
|
||||
<div class="stats-desc text-danger">VPN网关异常</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">
|
||||
<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>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="threatChartOptions" height="280px" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card title="设备列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-select v-model="filterType" placeholder="全部类型" style="width: 150px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="防火墙">防火墙</a-option>
|
||||
<a-option value="IDS/IPS">IDS/IPS</a-option>
|
||||
<a-option value="WAF">WAF</a-option>
|
||||
<a-option value="VPN">VPN</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredDevices"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 威胁列 -->
|
||||
<template #threats="{ record }">
|
||||
<span :class="['threats-value', record.threatsLevel]">{{ record.threats }}</span>
|
||||
</template>
|
||||
|
||||
<!-- CPU列 -->
|
||||
<template #cpu="{ record }">
|
||||
<div class="cpu-cell">
|
||||
<a-progress
|
||||
:percent="record.cpu"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
:status="getCpuStatus(record.cpu)"
|
||||
/>
|
||||
<span class="cpu-text">{{ record.cpu }}%</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconSafe,
|
||||
IconCheckCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconCloseCircleFill,
|
||||
IconThunderbolt,
|
||||
IconLock,
|
||||
IconCode,
|
||||
} 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'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
totalDevices: 24,
|
||||
threatsBlocked: '2,456',
|
||||
highRiskThreats: 12,
|
||||
offlineDevices: 1,
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const filterType = ref('')
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '设备名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'IP地址',
|
||||
dataIndex: 'ip',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '吞吐量',
|
||||
dataIndex: 'throughput',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '连接数',
|
||||
dataIndex: 'connections',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '今日威胁',
|
||||
dataIndex: 'threats',
|
||||
slotName: 'threats',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'CPU使用',
|
||||
dataIndex: 'cpu',
|
||||
slotName: 'cpu',
|
||||
width: 180,
|
||||
},
|
||||
]
|
||||
|
||||
// 设备数据
|
||||
const deviceData = ref([
|
||||
{
|
||||
name: '防火墙-主',
|
||||
type: '防火墙',
|
||||
ip: '10.0.0.1',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
throughput: '2.4 Gbps',
|
||||
connections: '12,456',
|
||||
threats: '156',
|
||||
threatsLevel: 'danger',
|
||||
cpu: 45,
|
||||
},
|
||||
{
|
||||
name: '防火墙-备',
|
||||
type: '防火墙',
|
||||
ip: '10.0.0.2',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
throughput: '1.8 Gbps',
|
||||
connections: '8,234',
|
||||
threats: '89',
|
||||
threatsLevel: 'danger',
|
||||
cpu: 38,
|
||||
},
|
||||
{
|
||||
name: 'IDS-01',
|
||||
type: 'IDS/IPS',
|
||||
ip: '10.0.0.10',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
throughput: '3.2 Gbps',
|
||||
connections: '-',
|
||||
threats: '23',
|
||||
threatsLevel: 'warning',
|
||||
cpu: 62,
|
||||
},
|
||||
{
|
||||
name: 'IPS-01',
|
||||
type: 'IDS/IPS',
|
||||
ip: '10.0.0.11',
|
||||
statusValue: 'warning',
|
||||
statusText: '负载高',
|
||||
throughput: '2.8 Gbps',
|
||||
connections: '-',
|
||||
threats: '45',
|
||||
threatsLevel: 'danger',
|
||||
cpu: 85,
|
||||
},
|
||||
{
|
||||
name: 'WAF-01',
|
||||
type: 'WAF',
|
||||
ip: '10.0.0.20',
|
||||
statusValue: 'online',
|
||||
statusText: '在线',
|
||||
throughput: '1.5 Gbps',
|
||||
connections: '5,678',
|
||||
threats: '78',
|
||||
threatsLevel: 'warning',
|
||||
cpu: 55,
|
||||
},
|
||||
{
|
||||
name: 'VPN网关',
|
||||
type: 'VPN',
|
||||
ip: '10.0.0.30',
|
||||
statusValue: 'offline',
|
||||
statusText: '离线',
|
||||
throughput: '-',
|
||||
connections: '-',
|
||||
threats: '-',
|
||||
threatsLevel: 'normal',
|
||||
cpu: 0,
|
||||
},
|
||||
])
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!filterType.value) return deviceData.value
|
||||
return deviceData.value.filter((device) => device.type === filterType.value)
|
||||
})
|
||||
|
||||
// 威胁分类数据
|
||||
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 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 getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'gray',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取CPU状态
|
||||
const getCpuStatus = (cpu: number) => {
|
||||
if (cpu >= 90) return 'danger'
|
||||
if (cpu >= 70) return 'warning'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SecurityMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: rgba(245, 63, 63, 0.1);
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&-muted {
|
||||
background-color: rgba(134, 144, 156, 0.1);
|
||||
color: rgb(var(--gray-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.threat-category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.threats-value {
|
||||
font-weight: 500;
|
||||
|
||||
&.danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&.normal {
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
626
src/views/ops/pages/monitor/storage/index.vue
Normal file
626
src/views/ops/pages/monitor/storage/index.vue
Normal file
@@ -0,0 +1,626 @@
|
||||
<template>
|
||||
<div class="storage-monitor">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon">
|
||||
<IconStorage />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">存储设备</div>
|
||||
<div class="stats-value">16</div>
|
||||
<div class="stats-desc">在线 14 / 离线 2</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon icon-blue">
|
||||
<IconDriveFile />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">总容量</div>
|
||||
<div class="stats-value">1.2 PB</div>
|
||||
<div class="stats-desc">已使用 756 TB</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon icon-green">
|
||||
<IconFile />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">使用率</div>
|
||||
<div class="stats-value">63%</div>
|
||||
<div class="stats-desc">较上月 +5%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon icon-orange">
|
||||
<IconExclamationCircle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">容量告警</div>
|
||||
<div class="stats-value">1</div>
|
||||
<div class="stats-desc stats-warning">NAS-01 需扩容</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :span="12">
|
||||
<a-card class="chart-card">
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<div class="card-title">I/O 吞吐量趋势</div>
|
||||
<div class="card-subtitle">过去24小时 (IOPS)</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<div class="chart-legend">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot dot-blue"></span>
|
||||
<span>读取</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot dot-green"></span>
|
||||
<span>写入</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="ioChartOptions" height="280px" />
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card class="chart-card">
|
||||
<template #title>
|
||||
<div class="card-header">
|
||||
<div class="card-title">存储池状态</div>
|
||||
<div class="card-subtitle">各存储池容量使用情况</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="storage-pool-list">
|
||||
<div class="pool-item">
|
||||
<div class="pool-header">
|
||||
<span class="pool-name">生产数据池</span>
|
||||
<span class="pool-value">420 TB / 600 TB</span>
|
||||
</div>
|
||||
<a-progress :percent="0.7" :stroke-width="8" :show-text="false" />
|
||||
</div>
|
||||
<div class="pool-item">
|
||||
<div class="pool-header">
|
||||
<span class="pool-name">开发测试池</span>
|
||||
<span class="pool-value">85 TB / 200 TB</span>
|
||||
</div>
|
||||
<a-progress :percent="0.425" :stroke-width="8" :show-text="false" />
|
||||
</div>
|
||||
<div class="pool-item">
|
||||
<div class="pool-header">
|
||||
<span class="pool-name">备份归档池</span>
|
||||
<span class="pool-value">180 TB / 400 TB</span>
|
||||
</div>
|
||||
<a-progress :percent="0.45" :stroke-width="8" :show-text="false" />
|
||||
</div>
|
||||
<div class="pool-item">
|
||||
<div class="pool-header">
|
||||
<span class="pool-name">对象存储池</span>
|
||||
<span class="pool-value pool-warning">475 TB / 500 TB</span>
|
||||
</div>
|
||||
<a-progress :percent="0.95" :stroke-width="8" :show-text="false" status="warning" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 卷列表 -->
|
||||
<a-row :gutter="16" class="info-row">
|
||||
<a-col :span="8">
|
||||
<a-card class="info-card">
|
||||
<div class="info-header">
|
||||
<span class="info-title">LUN使用情况</span>
|
||||
<IconArrowRise class="info-icon success" />
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">总LUN数</span>
|
||||
<span class="info-value">128</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">已分配</span>
|
||||
<span class="info-value">98</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">可用</span>
|
||||
<span class="info-value success">30</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card class="info-card">
|
||||
<div class="info-header">
|
||||
<span class="info-title">磁盘组状态</span>
|
||||
<a-tag color="green">健康</a-tag>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">RAID组</span>
|
||||
<span class="info-value">12</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">热备盘</span>
|
||||
<span class="info-value">8</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">故障磁盘</span>
|
||||
<span class="info-value success">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-card class="info-card">
|
||||
<div class="info-header">
|
||||
<span class="info-title">复制任务</span>
|
||||
<a-tag color="blue">同步中</a-tag>
|
||||
</div>
|
||||
<div class="info-list">
|
||||
<div class="info-item">
|
||||
<span class="info-label">本地快照</span>
|
||||
<span class="info-value">256</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">远程复制</span>
|
||||
<span class="info-value">12 任务</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">RPO达成</span>
|
||||
<span class="info-value success">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<a-card class="table-card">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span class="table-title">存储设备列表</span>
|
||||
<a-select v-model="filterType" style="width: 120px" placeholder="全部类型">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="SAN">SAN存储</a-option>
|
||||
<a-option value="NAS">NAS存储</a-option>
|
||||
<a-option value="backup">备份存储</a-option>
|
||||
<a-option value="object">对象存储</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data="filteredDevices" :pagination="false">
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 'online'" color="green">在线</a-tag>
|
||||
<a-tag v-else-if="record.status === 'warning'" color="orange">容量告警</a-tag>
|
||||
<a-tag v-else-if="record.status === 'offline'" color="red">维护中</a-tag>
|
||||
</template>
|
||||
<template #used="{ record }">
|
||||
<a-progress :percent="record.usedPercent / 100" :stroke-width="6" :show-text="false" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
IconStorage,
|
||||
IconDriveFile,
|
||||
IconFile,
|
||||
IconExclamationCircle,
|
||||
IconArrowRise
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import Breadcrumb from '@/components/breadcrumb/index.vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
|
||||
// 面包屑
|
||||
const breadcrumbItems = ['运维管理', '监控中心', '存储设备监控']
|
||||
|
||||
// 筛选类型
|
||||
const filterType = ref('')
|
||||
|
||||
// 存储设备数据
|
||||
const storageDevices = ref([
|
||||
{
|
||||
name: 'SAN-Storage-01',
|
||||
type: 'SAN存储',
|
||||
model: 'Dell EMC Unity 500',
|
||||
status: 'online',
|
||||
capacity: '120 TB',
|
||||
usedPercent: 68,
|
||||
iops: '45,000',
|
||||
latency: '0.8ms'
|
||||
},
|
||||
{
|
||||
name: 'SAN-Storage-02',
|
||||
type: 'SAN存储',
|
||||
model: 'Dell EMC Unity 500',
|
||||
status: 'online',
|
||||
capacity: '120 TB',
|
||||
usedPercent: 72,
|
||||
iops: '52,000',
|
||||
latency: '0.9ms'
|
||||
},
|
||||
{
|
||||
name: 'NAS-01',
|
||||
type: 'NAS存储',
|
||||
model: 'Synology RS3621xs+',
|
||||
status: 'warning',
|
||||
capacity: '96 TB',
|
||||
usedPercent: 92,
|
||||
iops: '8,500',
|
||||
latency: '2.1ms'
|
||||
},
|
||||
{
|
||||
name: 'NAS-02',
|
||||
type: 'NAS存储',
|
||||
model: 'Synology RS3621xs+',
|
||||
status: 'online',
|
||||
capacity: '96 TB',
|
||||
usedPercent: 55,
|
||||
iops: '6,200',
|
||||
latency: '1.8ms'
|
||||
},
|
||||
{
|
||||
name: 'Backup-01',
|
||||
type: '备份存储',
|
||||
model: 'HPE StoreOnce',
|
||||
status: 'online',
|
||||
capacity: '200 TB',
|
||||
usedPercent: 45,
|
||||
iops: '3,200',
|
||||
latency: '5.2ms'
|
||||
},
|
||||
{
|
||||
name: 'Object-Store',
|
||||
type: '对象存储',
|
||||
model: 'MinIO Cluster',
|
||||
status: 'offline',
|
||||
capacity: '500 TB',
|
||||
usedPercent: 38,
|
||||
iops: '-',
|
||||
latency: '-'
|
||||
}
|
||||
])
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!filterType.value) return storageDevices.value
|
||||
const typeMap: Record<string, string> = {
|
||||
SAN: 'SAN存储',
|
||||
NAS: 'NAS存储',
|
||||
backup: '备份存储',
|
||||
object: '对象存储'
|
||||
}
|
||||
return storageDevices.value.filter(d => d.type === typeMap[filterType.value])
|
||||
})
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{ title: '存储名称', dataIndex: 'name', width: 140 },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '型号', dataIndex: 'model', width: 160 },
|
||||
{ title: '状态', slotName: 'status', width: 100 },
|
||||
{ title: '总容量', dataIndex: 'capacity', width: 100 },
|
||||
{ title: '使用率', slotName: 'used', width: 160 },
|
||||
{ title: 'IOPS', dataIndex: 'iops', width: 100 },
|
||||
{ title: '延迟', dataIndex: 'latency', width: 100 }
|
||||
]
|
||||
|
||||
// I/O 吞吐量图表配置
|
||||
const ioChartOptions = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
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: 'IOPS'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '读取',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
opacity: 0.3
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#3b82f6'
|
||||
},
|
||||
data: [25000, 15000, 52000, 68000, 58000, 35000, 22000]
|
||||
},
|
||||
{
|
||||
name: '写入',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
opacity: 0.3
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#22c55e'
|
||||
},
|
||||
data: [18000, 12000, 38000, 45000, 42000, 28000, 16000]
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.storage-monitor {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background: #f0f5ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #165dff;
|
||||
|
||||
&.icon-blue {
|
||||
background: #e8fffb;
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&.icon-green {
|
||||
background: #e8ffea;
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
&.icon-orange {
|
||||
background: #fff7e8;
|
||||
color: #ff7d00;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
|
||||
&.stats-warning {
|
||||
color: #ff7d00;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.dot-blue {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
&.dot-green {
|
||||
background: #22c55e;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
.storage-pool-list {
|
||||
padding: 8px 0;
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pool-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pool-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
font-size: 14px;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.pool-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
|
||||
&.pool-warning {
|
||||
color: #ff7d00;
|
||||
}
|
||||
}
|
||||
|
||||
.info-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 16px;
|
||||
|
||||
&.success {
|
||||
color: #00b42a;
|
||||
}
|
||||
}
|
||||
|
||||
.info-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 14px;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
|
||||
&.success {
|
||||
color: #00b42a;
|
||||
}
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
571
src/views/ops/pages/monitor/url/index.vue
Normal file
571
src/views/ops/pages/monitor/url/index.vue
Normal file
@@ -0,0 +1,571 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-eye />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">监控总数</div>
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-desc">活跃监控项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<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">{{ stats.normal }}</div>
|
||||
<div class="stats-desc">
|
||||
<a-tag color="green" size="small">{{ stats.normalPercent }}%</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle-fill />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">异常告警</div>
|
||||
<div class="stats-value">{{ stats.warning }}</div>
|
||||
<div class="stats-desc text-success">较昨日 -2</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-clock-circle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">平均响应</div>
|
||||
<div class="stats-value">{{ stats.avgResponse }}<span class="stats-unit">ms</span></div>
|
||||
<div class="stats-desc text-success">较昨日 -15ms</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">
|
||||
<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>P95</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="responseTimeChartOptions" height="280px" />
|
||||
</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="availability-list">
|
||||
<div v-for="item in availabilityData" :key="item.name" class="availability-item">
|
||||
<div class="availability-header">
|
||||
<span class="availability-name">{{ item.name }}</span>
|
||||
<span :class="['availability-value', `status-${item.status}`]">{{ item.uptime }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.uptime"
|
||||
:status="item.progressStatus"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 监控列表 -->
|
||||
<a-card title="监控列表" :bordered="false">
|
||||
<a-table
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.statusValue)" bordered>
|
||||
{{ record.statusText }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- URL列 -->
|
||||
<template #url="{ record }">
|
||||
<a :href="record.url" target="_blank" class="url-link">
|
||||
{{ record.url }}
|
||||
<icon-launch class="link-icon" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 趋势列 -->
|
||||
<template #trend="{ record }">
|
||||
<span :class="['trend-icon', record.trend > 0 ? 'trend-up' : 'trend-down']">
|
||||
<icon-arrow-rise v-if="record.trend > 0" />
|
||||
<icon-arrow-fall v-else />
|
||||
</span>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
IconEye,
|
||||
IconCheckCircleFill,
|
||||
IconExclamationCircleFill,
|
||||
IconClockCircle,
|
||||
IconPlus,
|
||||
IconArrowRise,
|
||||
IconArrowFall,
|
||||
IconLaunch,
|
||||
} 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'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: 48,
|
||||
normal: 45,
|
||||
normalPercent: 94,
|
||||
warning: 3,
|
||||
avgResponse: 123,
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnData[] = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'url',
|
||||
slotName: 'url',
|
||||
width: 280,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '响应时间',
|
||||
dataIndex: 'responseTime',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '可用率',
|
||||
dataIndex: 'uptime',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '最后检查',
|
||||
dataIndex: 'lastCheck',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '趋势',
|
||||
dataIndex: 'trend',
|
||||
slotName: 'trend',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([
|
||||
{
|
||||
name: '官网首页',
|
||||
url: 'https://www.example.com',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
responseTime: '89ms',
|
||||
uptime: '99.98%',
|
||||
lastCheck: '1分钟前',
|
||||
trend: 1,
|
||||
},
|
||||
{
|
||||
name: 'API服务',
|
||||
url: 'https://api.example.com/health',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
responseTime: '45ms',
|
||||
uptime: '99.99%',
|
||||
lastCheck: '1分钟前',
|
||||
trend: 1,
|
||||
},
|
||||
{
|
||||
name: '用户中心',
|
||||
url: 'https://user.example.com',
|
||||
statusValue: 'warning',
|
||||
statusText: '响应慢',
|
||||
responseTime: '2.3s',
|
||||
uptime: '98.5%',
|
||||
lastCheck: '1分钟前',
|
||||
trend: -1,
|
||||
},
|
||||
{
|
||||
name: '支付网关',
|
||||
url: 'https://pay.example.com',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
responseTime: '120ms',
|
||||
uptime: '99.95%',
|
||||
lastCheck: '1分钟前',
|
||||
trend: 1,
|
||||
},
|
||||
{
|
||||
name: '管理后台',
|
||||
url: 'https://admin.example.com',
|
||||
statusValue: 'error',
|
||||
statusText: '不可达',
|
||||
responseTime: '超时',
|
||||
uptime: '95.2%',
|
||||
lastCheck: '2分钟前',
|
||||
trend: -1,
|
||||
},
|
||||
{
|
||||
name: '文件服务',
|
||||
url: 'https://files.example.com',
|
||||
statusValue: 'success',
|
||||
statusText: '正常',
|
||||
responseTime: '156ms',
|
||||
uptime: '99.8%',
|
||||
lastCheck: '1分钟前',
|
||||
trend: 1,
|
||||
},
|
||||
])
|
||||
|
||||
// 可用性数据
|
||||
const availabilityData = ref([
|
||||
{ name: '官网首页', uptime: 99.98, status: 'success', progressStatus: 'success' as const },
|
||||
{ name: 'API服务', uptime: 99.99, status: 'success', progressStatus: 'success' as const },
|
||||
{ name: '用户中心', uptime: 98.5, status: 'warning', progressStatus: 'warning' as const },
|
||||
{ name: '管理后台', uptime: 95.2, status: 'error', progressStatus: 'danger' as const },
|
||||
])
|
||||
|
||||
// 响应时间图表配置
|
||||
const responseTimeChartOptions = 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: 'ms',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '平均',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [95, 88, 145, 220, 180, 130, 92],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF', // ArcoDesign primary color
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'P95',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [120, 105, 180, 250, 210, 155, 115],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#FF7D00', // ArcoDesign warning color
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#FF7D00',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
success: 'green',
|
||||
warning: 'orange',
|
||||
error: 'red',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'URLMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.stats-icon-primary {
|
||||
background-color: rgba(var(--primary-1), 1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&.stats-icon-success {
|
||||
background-color: rgba(var(--green-1), 1);
|
||||
color: rgb(var(--green-6));
|
||||
}
|
||||
|
||||
&.stats-icon-warning {
|
||||
background-color: rgba(var(--orange-1), 1);
|
||||
color: rgb(var(--orange-6));
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-unit {
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.arco-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-left: 16px;
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
|
||||
&.legend-dot-1 {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
&.legend-dot-2 {
|
||||
background-color: #FF7D00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.availability-list {
|
||||
height: 280px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
|
||||
.availability-item {
|
||||
.availability-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.availability-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.availability-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&.status-warning {
|
||||
color: rgb(var(--warning-6));
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.url-link {
|
||||
color: rgb(var(--primary-6));
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
&:hover .link-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-icon {
|
||||
font-size: 16px;
|
||||
|
||||
&.trend-up {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&.trend-down {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
730
src/views/ops/pages/monitor/virtualization/index.vue
Normal file
730
src/views/ops/pages/monitor/virtualization/index.vue
Normal file
@@ -0,0 +1,730 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-storage />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">虚拟机总数</div>
|
||||
<div class="stats-value">{{ stats.total }}</div>
|
||||
<div class="stats-desc">跨 8 台宿主机</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<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">{{ stats.running }}</div>
|
||||
<div class="stats-desc text-success">90.7%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-code-square />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">CPU使用率</div>
|
||||
<div class="stats-value">{{ stats.cpuUsage }}%</div>
|
||||
<div class="stats-desc">集群平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<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.memoryUsage }}%</div>
|
||||
<div class="stats-desc">集群平均</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="资源使用趋势" :bordered="false">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>CPU</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-3"></span>
|
||||
<span>内存</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="performanceChartOptions" height="240px" />
|
||||
</div>
|
||||
<div class="chart-legend-placeholder"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="CPU资源分配" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">集群总计 256 vCPU</span>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="cpuChartOptions" height="240px" />
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot legend-dot-1"></span>
|
||||
<span>已分配 174 vCPU</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot legend-dot-gray"></span>
|
||||
<span>可用 82 vCPU</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="内存资源分配" :bordered="false">
|
||||
<template #extra>
|
||||
<span class="text-muted">集群总计 512 GB</span>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="memoryChartOptions" height="240px" />
|
||||
</div>
|
||||
<div class="chart-legend">
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot legend-dot-2"></span>
|
||||
<span>已使用 384 GB</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<span class="legend-dot legend-dot-gray"></span>
|
||||
<span>可用 128 GB</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 宿主机状态 -->
|
||||
<a-row :gutter="16" class="host-row">
|
||||
<a-col v-for="host in hostStatus" :key="host.name" :xs="12" :sm="6">
|
||||
<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>
|
||||
</div>
|
||||
<div class="host-metrics">
|
||||
<div class="metric-item">
|
||||
<div class="metric-header">
|
||||
<span class="metric-label">CPU</span>
|
||||
<span class="metric-value">{{ host.cpu }}%</span>
|
||||
</div>
|
||||
<a-progress :percent="host.cpu / 100" :stroke-width="6" :show-text="false" />
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-header">
|
||||
<span class="metric-label">内存</span>
|
||||
<span class="metric-value">{{ host.memory }}%</span>
|
||||
</div>
|
||||
<a-progress :percent="host.memory / 100" :stroke-width="6" :show-text="false" />
|
||||
</div>
|
||||
<div class="metric-item metric-vm">
|
||||
<span class="metric-label">虚拟机数</span>
|
||||
<span class="metric-value-right">{{ host.vmCount }}</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 {
|
||||
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'
|
||||
|
||||
// 统计数据
|
||||
const stats = ref({
|
||||
total: 86,
|
||||
running: 78,
|
||||
cpuUsage: 68,
|
||||
memoryUsage: 75,
|
||||
})
|
||||
|
||||
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 performanceChartOptions = 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: '%',
|
||||
max: 100,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'CPU',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [45, 38, 72, 85, 78, 55, 42],
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '内存',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [62, 58, 75, 82, 79, 65, 60],
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#F7BA1E',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F7BA1E',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// CPU资源分配图表配置
|
||||
const cpuChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ value: 68, name: '已分配', itemStyle: { color: '#165DFF' } },
|
||||
{ value: 32, name: '可用', itemStyle: { color: '#E5E6EB' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 内存资源分配图表配置
|
||||
const memoryChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{ value: 75, name: '已使用', itemStyle: { color: '#14C9C9' } },
|
||||
{ value: 25, name: '可用', itemStyle: { color: '#E5E6EB' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
running: 'green',
|
||||
stopped: 'gray',
|
||||
warning: 'orange',
|
||||
}
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
// 获取进度条状态
|
||||
const getProgressStatus = (value: number) => {
|
||||
if (value >= 90) return 'danger'
|
||||
if (value >= 70) return 'warning'
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
// TODO: 从API获取数据
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'VirtualizationMonitor',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-success {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14C9C9;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.chart-legend-placeholder {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-1 {
|
||||
background-color: #165DFF;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background-color: #14C9C9;
|
||||
}
|
||||
|
||||
&-3 {
|
||||
background-color: #F7BA1E;
|
||||
}
|
||||
|
||||
&-gray {
|
||||
background-color: #E5E6EB;
|
||||
}
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.host-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.host-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.host-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.host-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.host-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
.metric-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.metric-vm {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-value-right {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
min-width: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
650
src/views/ops/pages/netarch/auto-topology/index.vue
Normal file
650
src/views/ops/pages/netarch/auto-topology/index.vue
Normal file
@@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h2>自动感知拓扑图</h2>
|
||||
<p class="page-subtitle">自动发现网络设备,构建实时网络拓扑</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a-button type="primary" :loading="isScanning" @click="handleScan">
|
||||
<template #icon>
|
||||
<icon-refresh v-if="isScanning" class="animate-spin" />
|
||||
<icon-play-circle v-else />
|
||||
</template>
|
||||
{{ isScanning ? '扫描中...' : '启动扫描' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-apps />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">发现设备</div>
|
||||
<div class="stats-value">156</div>
|
||||
<div class="stats-desc text-success">
|
||||
<icon-arrow-rise />
|
||||
较上次 +2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-success">
|
||||
<icon-wifi />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">在线设备</div>
|
||||
<div class="stats-value">148</div>
|
||||
<div class="stats-desc">在线率 94.8%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-warning">
|
||||
<icon-exclamation-circle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">新增设备</div>
|
||||
<div class="stats-value">2</div>
|
||||
<div class="stats-desc">待确认</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-default">
|
||||
<icon-clock-circle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">上次扫描</div>
|
||||
<div class="stats-value">14:30</div>
|
||||
<div class="stats-desc">12分钟前</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 扫描进度和配置 -->
|
||||
<a-row :gutter="16" class="mt-6 scan-row">
|
||||
<a-col :xs="24" :lg="16" class="scan-col">
|
||||
<a-card :bordered="false" class="scan-card">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>扫描进度</span>
|
||||
<a-tag v-if="isScanning" color="orange" bordered>扫描中</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="isScanning" class="scanning-content">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<icon-refresh class="h-5 w-5 animate-spin text-primary" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">正在扫描网段 10.0.0.0/16</p>
|
||||
<p class="text-xs text-muted">已发现 89 个设备</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-progress :percent="45" :stroke-width="8" />
|
||||
<a-row :gutter="16" class="mt-4 text-center">
|
||||
<a-col :span="6">
|
||||
<div class="text-lg font-semibold">45%</div>
|
||||
<div class="text-xs text-muted">扫描进度</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="text-lg font-semibold">89</div>
|
||||
<div class="text-xs text-muted">已发现</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="text-lg font-semibold">2</div>
|
||||
<div class="text-xs text-muted">新设备</div>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<div class="text-lg font-semibold">5:23</div>
|
||||
<div class="text-xs text-muted">预计剩余</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div v-else class="scan-complete-content">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<icon-check-circle class="h-5 w-5 text-success" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium">上次全网扫描已完成</p>
|
||||
<p class="text-xs text-muted">2024-01-15 14:30:00 - 耗时 12分钟</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="16" class="stats-grid">
|
||||
<a-col :span="6" class="text-center">
|
||||
<div class="text-lg font-semibold">156</div>
|
||||
<div class="text-xs text-muted">总设备</div>
|
||||
</a-col>
|
||||
<a-col :span="6" class="text-center">
|
||||
<div class="text-lg font-semibold text-success">148</div>
|
||||
<div class="text-xs text-muted">在线</div>
|
||||
</a-col>
|
||||
<a-col :span="6" class="text-center">
|
||||
<div class="text-lg font-semibold text-muted">6</div>
|
||||
<div class="text-xs text-muted">离线</div>
|
||||
</a-col>
|
||||
<a-col :span="6" class="text-center">
|
||||
<div class="text-lg font-semibold text-warning">2</div>
|
||||
<div class="text-xs text-muted">新发现</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8" class="scan-col">
|
||||
<a-card :bordered="false" title="扫描配置" class="scan-card">
|
||||
<div class="config-section">
|
||||
<div class="config-item">
|
||||
<p class="config-label">扫描网段</p>
|
||||
<div class="config-value-list">
|
||||
<a-tag color="arcoblue" bordered>10.0.0.0/16</a-tag>
|
||||
<a-tag color="arcoblue" bordered>192.168.0.0/16</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<p class="config-label">发现协议</p>
|
||||
<div class="config-tags">
|
||||
<a-tag color="arcoblue" bordered>SNMP</a-tag>
|
||||
<a-tag color="arcoblue" bordered>ICMP</a-tag>
|
||||
<a-tag color="arcoblue" bordered>ARP</a-tag>
|
||||
<a-tag color="arcoblue" bordered>CDP</a-tag>
|
||||
<a-tag color="arcoblue" bordered>LLDP</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<p class="config-label">自动扫描</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm">每 6 小时</span>
|
||||
<a-switch :model-value="true" type="round" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 扫描历史 -->
|
||||
<a-card class="mt-6" :bordered="false" title="扫描历史">
|
||||
<a-table
|
||||
:data="scanHistory"
|
||||
:columns="historyColumns"
|
||||
:pagination="false"
|
||||
row-key="time"
|
||||
>
|
||||
<template #newDevices="{ record }">
|
||||
<span v-if="record.newDevices > 0" class="text-warning">+{{ record.newDevices }}</span>
|
||||
<span v-else class="text-muted">0</span>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status === 'success' ? 'green' : 'orange'" bordered>
|
||||
{{ record.status === 'success' ? '完成' : '部分完成' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 发现的设备 -->
|
||||
<a-card class="mt-6" :bordered="false">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>发现的设备</span>
|
||||
<a-space>
|
||||
<a-select v-model="deviceTypeFilter" placeholder="全部类型" style="width: 120px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="switch">交换机</a-option>
|
||||
<a-option value="router">路由器</a-option>
|
||||
<a-option value="server">服务器</a-option>
|
||||
<a-option value="unknown">未知设备</a-option>
|
||||
</a-select>
|
||||
<a-select v-model="deviceStatusFilter" placeholder="全部状态" style="width: 120px">
|
||||
<a-option value="">全部状态</a-option>
|
||||
<a-option value="online">在线</a-option>
|
||||
<a-option value="offline">离线</a-option>
|
||||
<a-option value="new">新发现</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:data="discoveredDevices"
|
||||
:columns="deviceColumns"
|
||||
row-key="ip"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 'online'" color="green" bordered>在线</a-tag>
|
||||
<a-tag v-else-if="record.status === 'offline'" color="red" bordered>离线</a-tag>
|
||||
<a-tag v-else color="orange" bordered>新发现</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
IconRefresh,
|
||||
IconCheckCircle,
|
||||
IconSettings,
|
||||
IconDesktop,
|
||||
IconWifi,
|
||||
IconExclamationCircle,
|
||||
IconPlayCircle,
|
||||
IconSafe,
|
||||
IconApps,
|
||||
IconClockCircle,
|
||||
IconArrowRise,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 扫描状态
|
||||
const isScanning = ref(false)
|
||||
|
||||
// 设备筛选
|
||||
const deviceTypeFilter = ref('')
|
||||
const deviceStatusFilter = ref('')
|
||||
|
||||
// 启动扫描
|
||||
const handleScan = () => {
|
||||
isScanning.value = true
|
||||
setTimeout(() => {
|
||||
isScanning.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
// 发现的设备数据
|
||||
const discoveredDevices = ref([
|
||||
{
|
||||
ip: '10.0.0.1',
|
||||
mac: '00:1A:2B:3C:4D:01',
|
||||
hostname: 'Core-SW-01',
|
||||
vendor: 'Cisco Systems',
|
||||
type: '交换机',
|
||||
status: 'online',
|
||||
lastSeen: '刚刚',
|
||||
method: 'SNMP',
|
||||
},
|
||||
{
|
||||
ip: '10.0.0.2',
|
||||
mac: '00:1A:2B:3C:4D:02',
|
||||
hostname: 'Core-SW-02',
|
||||
vendor: 'Cisco Systems',
|
||||
type: '交换机',
|
||||
status: 'online',
|
||||
lastSeen: '刚刚',
|
||||
method: 'SNMP',
|
||||
},
|
||||
{
|
||||
ip: '10.0.1.1',
|
||||
mac: '00:1A:2B:3C:4D:10',
|
||||
hostname: 'Router-01',
|
||||
vendor: 'Cisco Systems',
|
||||
type: '路由器',
|
||||
status: 'online',
|
||||
lastSeen: '1分钟前',
|
||||
method: 'ICMP+ARP',
|
||||
},
|
||||
{
|
||||
ip: '10.0.2.50',
|
||||
mac: '00:50:56:AB:CD:01',
|
||||
hostname: 'ESXi-01',
|
||||
vendor: 'VMware, Inc.',
|
||||
type: '服务器',
|
||||
status: 'online',
|
||||
lastSeen: '刚刚',
|
||||
method: 'SNMP',
|
||||
},
|
||||
{
|
||||
ip: '10.0.3.100',
|
||||
mac: '00:00:5E:00:01:01',
|
||||
hostname: 'Unknown',
|
||||
vendor: 'Dell Technologies',
|
||||
type: '未知',
|
||||
status: 'new',
|
||||
lastSeen: '5分钟前',
|
||||
method: 'ARP',
|
||||
},
|
||||
{
|
||||
ip: '10.0.2.200',
|
||||
mac: '00:1A:2B:3C:4D:FF',
|
||||
hostname: 'Printer-01',
|
||||
vendor: 'HP Inc.',
|
||||
type: '打印机',
|
||||
status: 'offline',
|
||||
lastSeen: '2小时前',
|
||||
method: 'ICMP',
|
||||
},
|
||||
])
|
||||
|
||||
// 设备表格列
|
||||
const deviceColumns: TableColumnData[] = [
|
||||
{ title: 'IP地址', dataIndex: 'ip', width: 120 },
|
||||
{ title: 'MAC地址', dataIndex: 'mac', width: 150 },
|
||||
{ title: '主机名', dataIndex: 'hostname', width: 120 },
|
||||
{ title: '厂商', dataIndex: 'vendor', width: 140 },
|
||||
{ title: '设备类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100, align: 'center' },
|
||||
{ title: '最后发现', dataIndex: 'lastSeen', width: 100 },
|
||||
{ title: '发现方式', dataIndex: 'method', width: 100 },
|
||||
]
|
||||
|
||||
// 扫描历史数据
|
||||
const scanHistory = ref([
|
||||
{ time: '2024-01-15 14:30:00', type: '全网扫描', duration: '12分钟', devices: 156, newDevices: 2, status: 'success' },
|
||||
{ time: '2024-01-15 12:00:00', type: '增量扫描', duration: '3分钟', devices: 154, newDevices: 0, status: 'success' },
|
||||
{ time: '2024-01-15 08:00:00', type: '全网扫描', duration: '15分钟', devices: 154, newDevices: 1, status: 'success' },
|
||||
{ time: '2024-01-14 20:00:00', type: '增量扫描', duration: '2分钟', devices: 153, newDevices: 0, status: 'success' },
|
||||
{ time: '2024-01-14 14:30:00', type: '全网扫描', duration: '18分钟', devices: 153, newDevices: 3, status: 'warning' },
|
||||
])
|
||||
|
||||
// 扫描历史表格列
|
||||
const historyColumns: TableColumnData[] = [
|
||||
{ title: '时间', dataIndex: 'time', width: 160 },
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '耗时', dataIndex: 'duration', width: 80 },
|
||||
{ title: '设备数', dataIndex: 'devices', width: 80, align: 'center' },
|
||||
{ title: '新增', dataIndex: 'newDevices', slotName: 'newDevices', width: 80, align: 'center' },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100, align: 'center' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AutoTopology',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background: rgba(22, 93, 255, 0.1);
|
||||
color: #165dff;
|
||||
}
|
||||
|
||||
&-success {
|
||||
background: rgba(0, 180, 42, 0.1);
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background: rgba(255, 125, 0, 0.1);
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
&-default {
|
||||
background: rgba(134, 144, 156, 0.1);
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&.text-success {
|
||||
color: #00b42a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.scan-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.scan-col {
|
||||
display: flex;
|
||||
|
||||
:deep(.arco-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.arco-card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scan-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scanning-content,
|
||||
.scan-complete-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
background: var(--color-fill-2);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
.config-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.config-value-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.config-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #165dff;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
695
src/views/ops/pages/netarch/ip/index.vue
Normal file
695
src/views/ops/pages/netarch/ip/index.vue
Normal file
@@ -0,0 +1,695 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<!-- <div class="page-header">
|
||||
<div class="page-title">
|
||||
<h2>IP地址管理</h2>
|
||||
<p class="page-subtitle">管理IP地址分配,监控地址池使用情况</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a-button type="secondary" class="mr-2">
|
||||
<template #icon><icon-download /></template>
|
||||
导出
|
||||
</a-button>
|
||||
<a-button type="primary">
|
||||
<template #icon><icon-plus /></template>
|
||||
添加子网
|
||||
</a-button>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<icon-wifi />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">IP地址池</div>
|
||||
<div class="stats-value">6</div>
|
||||
<div class="stats-desc">管理中的子网</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-cyan">
|
||||
<icon-apps />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">总IP数</div>
|
||||
<div class="stats-value">1,524</div>
|
||||
<div class="stats-desc">可分配地址</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-blue">
|
||||
<icon-storage />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">已分配</div>
|
||||
<div class="stats-value">630</div>
|
||||
<div class="stats-desc">使用率 41.3%</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-success">
|
||||
<icon-exclamation-circle />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">冲突告警</div>
|
||||
<div class="stats-value">0</div>
|
||||
<div class="stats-desc text-success">无IP冲突</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 地址池概览和快速查询 -->
|
||||
<a-row :gutter="16" class="mt-6 overview-row">
|
||||
<a-col :xs="24" :lg="16" class="overview-col">
|
||||
<a-card :bordered="false" class="overview-card">
|
||||
<template #title>
|
||||
<div class="card-title-row">
|
||||
<span class="card-title">地址池概览</span>
|
||||
<span class="card-subtitle">各子网使用情况可视化</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="subnet-list">
|
||||
<div
|
||||
v-for="subnet in ipSubnets"
|
||||
:key="subnet.subnet"
|
||||
:class="['subnet-item', { active: selectedSubnet === subnet.subnet }]"
|
||||
@click="selectedSubnet = selectedSubnet === subnet.subnet ? '' : subnet.subnet"
|
||||
>
|
||||
<div class="subnet-header">
|
||||
<div>
|
||||
<p class="subnet-name">{{ subnet.name }}</p>
|
||||
<p class="subnet-info">{{ subnet.subnet }} · {{ subnet.vlan }}</p>
|
||||
</div>
|
||||
<div class="subnet-usage">
|
||||
<p class="usage-value">{{ subnet.used }} / {{ subnet.total }}</p>
|
||||
<p class="usage-label">已用 / 总数</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subnet-progress">
|
||||
<a-progress
|
||||
:percent="Math.round((subnet.used / subnet.total) * 100)"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="getUsageColor((subnet.used / subnet.total) * 100)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8" class="overview-col">
|
||||
<a-card :bordered="false" class="overview-card">
|
||||
<template #title>
|
||||
<div class="card-title-row">
|
||||
<span class="card-title">快速查询</span>
|
||||
<span class="card-subtitle">搜索IP地址或主机名</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="search-section">
|
||||
<a-input v-model="searchQuery" placeholder="输入IP或主机名..." allow-clear>
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="stats-section">
|
||||
<h4 class="section-title">分配类型统计</h4>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
<span class="color-dot color-dot-blue"></span>
|
||||
<span>静态分配</span>
|
||||
</div>
|
||||
<span class="stat-value">245 (39%)</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">
|
||||
<span class="color-dot color-dot-cyan"></span>
|
||||
<span>DHCP分配</span>
|
||||
</div>
|
||||
<span class="stat-value">385 (61%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="stats-section">
|
||||
<h4 class="section-title">DHCP服务器</h4>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">主DHCP</span>
|
||||
<a-tag color="green" bordered>在线</a-tag>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">备DHCP</span>
|
||||
<a-tag color="green" bordered>在线</a-tag>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">租约数</span>
|
||||
<span class="stat-value">385</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">平均租期</span>
|
||||
<span class="stat-value">8小时</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 子网列表 -->
|
||||
<a-card class="mt-6" :bordered="false">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>子网列表</span>
|
||||
<a-select v-model="subnetFilter" placeholder="全部子网" style="width: 140px">
|
||||
<a-option value="">全部子网</a-option>
|
||||
<a-option value="server">服务器网段</a-option>
|
||||
<a-option value="office">办公网段</a-option>
|
||||
<a-option value="guest">访客网段</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:data="ipSubnets"
|
||||
:columns="subnetColumns"
|
||||
:pagination="false"
|
||||
row-key="subnet"
|
||||
>
|
||||
<template #usage="{ record }">
|
||||
<a-progress
|
||||
:percent="Math.round((record.used / record.total) * 100)"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="getUsageColor((record.used / record.total) * 100)"
|
||||
/>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag
|
||||
v-if="record.usageStatus === 'warning'"
|
||||
color="orange"
|
||||
bordered
|
||||
>
|
||||
使用率高
|
||||
</a-tag>
|
||||
<a-tag v-else color="green" bordered>正常</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- IP分配记录 -->
|
||||
<a-card class="mt-6" :bordered="false">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>IP分配记录</span>
|
||||
<a-space>
|
||||
<a-select v-model="typeFilter" placeholder="全部类型" style="width: 120px">
|
||||
<a-option value="">全部类型</a-option>
|
||||
<a-option value="static">静态</a-option>
|
||||
<a-option value="dhcp">DHCP</a-option>
|
||||
</a-select>
|
||||
<a-select v-model="statusFilter" placeholder="全部状态" style="width: 120px">
|
||||
<a-option value="">全部状态</a-option>
|
||||
<a-option value="online">在线</a-option>
|
||||
<a-option value="offline">离线</a-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:data="ipAllocations"
|
||||
:columns="allocationColumns"
|
||||
:pagination="false"
|
||||
row-key="ip"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.status === 'online' ? 'green' : 'red'" bordered>
|
||||
{{ record.status === 'online' ? '在线' : '离线' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
IconWifi,
|
||||
IconApps,
|
||||
IconStorage,
|
||||
IconExclamationCircle,
|
||||
IconDownload,
|
||||
IconPlus,
|
||||
IconSearch,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 搜索和筛选
|
||||
const searchQuery = ref('')
|
||||
const selectedSubnet = ref('')
|
||||
const subnetFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const statusFilter = ref('')
|
||||
|
||||
// 获取使用率颜色
|
||||
const getUsageColor = (percent: number) => {
|
||||
if (percent >= 70) return '#FF7D00'
|
||||
return '#165DFF'
|
||||
}
|
||||
|
||||
// 子网数据
|
||||
const ipSubnets = ref([
|
||||
{
|
||||
subnet: '10.0.0.0/24',
|
||||
name: '核心网络',
|
||||
vlan: 'VLAN 1',
|
||||
gateway: '10.0.0.1',
|
||||
total: 254,
|
||||
used: 45,
|
||||
available: 209,
|
||||
usageStatus: 'success',
|
||||
},
|
||||
{
|
||||
subnet: '10.0.1.0/24',
|
||||
name: '服务器网段',
|
||||
vlan: 'VLAN 10',
|
||||
gateway: '10.0.1.1',
|
||||
total: 254,
|
||||
used: 186,
|
||||
available: 68,
|
||||
usageStatus: 'warning',
|
||||
},
|
||||
{
|
||||
subnet: '10.0.2.0/24',
|
||||
name: '虚拟化网段',
|
||||
vlan: 'VLAN 20',
|
||||
gateway: '10.0.2.1',
|
||||
total: 254,
|
||||
used: 124,
|
||||
available: 130,
|
||||
usageStatus: 'success',
|
||||
},
|
||||
{
|
||||
subnet: '10.0.3.0/24',
|
||||
name: '存储网段',
|
||||
vlan: 'VLAN 30',
|
||||
gateway: '10.0.3.1',
|
||||
total: 254,
|
||||
used: 32,
|
||||
available: 222,
|
||||
usageStatus: 'success',
|
||||
},
|
||||
{
|
||||
subnet: '192.168.1.0/24',
|
||||
name: '办公网络',
|
||||
vlan: 'VLAN 100',
|
||||
gateway: '192.168.1.1',
|
||||
total: 254,
|
||||
used: 198,
|
||||
available: 56,
|
||||
usageStatus: 'warning',
|
||||
},
|
||||
{
|
||||
subnet: '192.168.2.0/24',
|
||||
name: '访客网络',
|
||||
vlan: 'VLAN 200',
|
||||
gateway: '192.168.2.1',
|
||||
total: 254,
|
||||
used: 45,
|
||||
available: 209,
|
||||
usageStatus: 'success',
|
||||
},
|
||||
])
|
||||
|
||||
// IP分配数据
|
||||
const ipAllocations = ref([
|
||||
{
|
||||
ip: '10.0.1.10',
|
||||
hostname: 'Web-Server-01',
|
||||
mac: '00:50:56:AB:01:10',
|
||||
type: '静态',
|
||||
status: 'online',
|
||||
lastSeen: '在线',
|
||||
},
|
||||
{
|
||||
ip: '10.0.1.11',
|
||||
hostname: 'Web-Server-02',
|
||||
mac: '00:50:56:AB:01:11',
|
||||
type: '静态',
|
||||
status: 'online',
|
||||
lastSeen: '在线',
|
||||
},
|
||||
{
|
||||
ip: '10.0.1.50',
|
||||
hostname: 'DB-Master',
|
||||
mac: '00:50:56:AB:01:50',
|
||||
type: '静态',
|
||||
status: 'online',
|
||||
lastSeen: '在线',
|
||||
},
|
||||
{
|
||||
ip: '10.0.1.51',
|
||||
hostname: 'DB-Slave',
|
||||
mac: '00:50:56:AB:01:51',
|
||||
type: '静态',
|
||||
status: 'online',
|
||||
lastSeen: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.100',
|
||||
hostname: 'PC-Zhang',
|
||||
mac: '00:1A:2B:3C:4D:10',
|
||||
type: 'DHCP',
|
||||
status: 'online',
|
||||
lastSeen: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.101',
|
||||
hostname: 'PC-Li',
|
||||
mac: '00:1A:2B:3C:4D:11',
|
||||
type: 'DHCP',
|
||||
status: 'offline',
|
||||
lastSeen: '2小时前',
|
||||
},
|
||||
])
|
||||
|
||||
// 子网表格列
|
||||
const subnetColumns: TableColumnData[] = [
|
||||
{ title: '子网', dataIndex: 'subnet', width: 140 },
|
||||
{ title: '名称', dataIndex: 'name', width: 120 },
|
||||
{ title: 'VLAN', dataIndex: 'vlan', width: 100 },
|
||||
{ title: '网关', dataIndex: 'gateway', width: 120 },
|
||||
{ title: '总数', dataIndex: 'total', width: 80, align: 'center' },
|
||||
{ title: '已用', dataIndex: 'used', width: 80, align: 'center' },
|
||||
{ title: '可用', dataIndex: 'available', width: 80, align: 'center' },
|
||||
{ title: '使用率', dataIndex: 'usage', slotName: 'usage', width: 160 },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100, align: 'center' },
|
||||
]
|
||||
|
||||
// IP分配表格列
|
||||
const allocationColumns: TableColumnData[] = [
|
||||
{ title: 'IP地址', dataIndex: 'ip', width: 150 },
|
||||
{ title: '主机名', dataIndex: 'hostname', width: 200 },
|
||||
{ title: 'MAC地址', dataIndex: 'mac', width: 180 },
|
||||
{ title: '分配类型', dataIndex: 'type', width: 100, align: 'center' },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 100, align: 'center' },
|
||||
{ title: '最后在线', dataIndex: 'lastSeen', width: 120 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'IPManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background: rgba(22, 93, 255, 0.1);
|
||||
color: #165dff;
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-blue {
|
||||
background: rgba(22, 93, 255, 0.1);
|
||||
color: #165dff;
|
||||
}
|
||||
|
||||
&-success {
|
||||
background: rgba(0, 180, 42, 0.1);
|
||||
color: #00b42a;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
&.text-success {
|
||||
color: #00b42a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.overview-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.overview-col {
|
||||
display: flex;
|
||||
|
||||
:deep(.arco-card) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.arco-card-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
height: 100%;
|
||||
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.subnet-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.subnet-item {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(22, 93, 255, 0.5);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #165dff;
|
||||
background: rgba(22, 93, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.subnet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.subnet-name {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subnet-info {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.subnet-usage {
|
||||
text-align: right;
|
||||
|
||||
.usage-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subnet-progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
|
||||
&-blue {
|
||||
background: #165dff;
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background: #14c9c9;
|
||||
}
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
875
src/views/ops/pages/netarch/traffic/index.vue
Normal file
875
src/views/ops/pages/netarch/traffic/index.vue
Normal file
@@ -0,0 +1,875 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h2>流量分析管理</h2>
|
||||
<p class="page-subtitle">实时监控和分析网络流量,识别异常流量模式</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a-select v-model="timeRange" style="width: 140px; margin-right: 12px">
|
||||
<a-option value="24h">最近24小时</a-option>
|
||||
<a-option value="7d">最近7天</a-option>
|
||||
<a-option value="30d">最近30天</a-option>
|
||||
<a-option value="custom">自定义</a-option>
|
||||
</a-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<a-row :gutter="16" class="stats-row">
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-primary">
|
||||
<IconActivity />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">当前带宽</div>
|
||||
<div class="stats-value">12.4 Gbps</div>
|
||||
<div class="stats-desc">峰值流量</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-cyan">
|
||||
<IconArrowDown />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">入站流量</div>
|
||||
<div class="stats-value">8.5 TB</div>
|
||||
<div class="stats-desc">今日累计</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-green">
|
||||
<IconArrowUp />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">出站流量</div>
|
||||
<div class="stats-value">6.2 TB</div>
|
||||
<div class="stats-desc">今日累计</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12" :lg="6">
|
||||
<a-card class="stats-card" :bordered="false">
|
||||
<div class="stats-content">
|
||||
<div class="stats-icon stats-icon-purple">
|
||||
<IconWorld />
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">活跃会话</div>
|
||||
<div class="stats-value">256K</div>
|
||||
<div class="stats-desc">
|
||||
<span class="text-success">
|
||||
<IconTrendingUp />
|
||||
较昨日 +12%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 流量趋势图表 -->
|
||||
<a-card class="chart-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>流量趋势</span>
|
||||
<span class="card-subtitle">过去24小时网络流量 (Gbps)</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-inbound"></span>
|
||||
<span>入站</span>
|
||||
</span>
|
||||
<span class="legend-item">
|
||||
<span class="legend-dot legend-dot-outbound"></span>
|
||||
<span>出站</span>
|
||||
</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<div class="chart-container">
|
||||
<Chart :options="trafficChartOptions" height="250px" />
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 流量分布 -->
|
||||
<a-row :gutter="16" class="distribution-row">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card class="distribution-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>协议分布</span>
|
||||
<span class="card-subtitle">按协议类型统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="protocol-list">
|
||||
<div v-for="item in protocolData" :key="item.name" class="protocol-item">
|
||||
<div class="protocol-header">
|
||||
<span class="protocol-name">{{ item.name }}</span>
|
||||
<span class="protocol-value">{{ item.percent }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="item.color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card class="distribution-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>流量方向</span>
|
||||
<span class="card-subtitle">内外网流量占比</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="direction-list">
|
||||
<div class="direction-item">
|
||||
<div class="direction-icon direction-icon-external">
|
||||
<IconWorld />
|
||||
</div>
|
||||
<div class="direction-info">
|
||||
<div class="direction-header">
|
||||
<span class="direction-name">外网流量</span>
|
||||
<span class="direction-percent">65%</span>
|
||||
</div>
|
||||
<a-progress :percent="65" :stroke-width="8" :show-text="false" color="#165DFF" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="direction-item">
|
||||
<div class="direction-icon direction-icon-internal">
|
||||
<IconServer />
|
||||
</div>
|
||||
<div class="direction-info">
|
||||
<div class="direction-header">
|
||||
<span class="direction-name">内网流量</span>
|
||||
<span class="direction-percent">35%</span>
|
||||
</div>
|
||||
<a-progress :percent="35" :stroke-width="8" :show-text="false" color="#14C9C9" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<a-row :gutter="16" class="direction-stats">
|
||||
<a-col :span="12">
|
||||
<div class="direction-stat">
|
||||
<div class="stat-label">外网入站</div>
|
||||
<div class="stat-value">5.2 TB</div>
|
||||
<div class="stat-change text-success">
|
||||
<IconTrendingUp />
|
||||
+8.5%
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="direction-stat">
|
||||
<div class="stat-label">外网出站</div>
|
||||
<div class="stat-value">3.8 TB</div>
|
||||
<div class="stat-change text-success">
|
||||
<IconTrendingUp />
|
||||
+5.2%
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card class="distribution-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>带宽利用率</span>
|
||||
<span class="card-subtitle">各链路使用情况</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="bandwidth-list">
|
||||
<div v-for="item in bandwidthData" :key="item.name" class="bandwidth-item">
|
||||
<div class="bandwidth-header">
|
||||
<span class="bandwidth-name">{{ item.name }}</span>
|
||||
<span class="bandwidth-value">{{ item.percent }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="item.percent"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
:color="getBandwidthColor(item.percent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-row :gutter="16" class="tables-row">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>Top 应用流量</span>
|
||||
<a-link>查看全部</a-link>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:data="topApplications"
|
||||
:columns="applicationColumns"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<template #share="{ record }">
|
||||
<a-progress
|
||||
:percent="record.shareValue"
|
||||
:stroke-width="8"
|
||||
:show-text="false"
|
||||
color="#165DFF"
|
||||
/>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card class="table-card" :bordered="false">
|
||||
<template #title>
|
||||
<div class="table-header">
|
||||
<span>Top 流量源</span>
|
||||
<a-link>查看全部</a-link>
|
||||
</div>
|
||||
</template>
|
||||
<a-table
|
||||
:data="topSources"
|
||||
:columns="sourceColumns"
|
||||
:pagination="false"
|
||||
row-key="ip"
|
||||
>
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="record.statusColor" bordered>{{ record.statusText }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import {
|
||||
IconActivity,
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconWorld,
|
||||
IconTrendingUp,
|
||||
IconServer,
|
||||
IconDownload,
|
||||
IconChartLine,
|
||||
} from '@tabler/icons-vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
|
||||
// 时间范围
|
||||
const timeRange = ref('24h')
|
||||
|
||||
// 流量趋势数据
|
||||
const trafficData = [
|
||||
{ time: '00:00', inbound: 2.5, outbound: 1.8 },
|
||||
{ time: '02:00', inbound: 1.8, outbound: 1.2 },
|
||||
{ time: '04:00', inbound: 1.2, outbound: 0.8 },
|
||||
{ time: '06:00', inbound: 2.0, outbound: 1.5 },
|
||||
{ time: '08:00', inbound: 8.5, outbound: 6.2 },
|
||||
{ time: '10:00', inbound: 10.2, outbound: 7.8 },
|
||||
{ time: '12:00', inbound: 12.4, outbound: 9.8 },
|
||||
{ time: '14:00', inbound: 11.5, outbound: 8.5 },
|
||||
{ time: '16:00', inbound: 10.8, outbound: 8.2 },
|
||||
{ time: '18:00', inbound: 8.2, outbound: 6.5 },
|
||||
{ time: '20:00', inbound: 5.6, outbound: 4.2 },
|
||||
{ time: '22:00', inbound: 4.2, outbound: 3.2 },
|
||||
{ time: '24:00', inbound: 3.2, outbound: 2.4 },
|
||||
]
|
||||
|
||||
// 流量趋势图表配置
|
||||
const trafficChartOptions = ref({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trafficData.map((item) => item.time),
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: 'Gbps',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '入站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trafficData.map((item) => item.inbound),
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: '#165DFF',
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#165DFF',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#165DFF',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '出站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trafficData.map((item) => item.outbound),
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: '#14C9C9',
|
||||
},
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
color: '#14C9C9',
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#14C9C9',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// 协议分布数据
|
||||
const protocolData = [
|
||||
{ name: 'TCP', percent: 78, color: '#165DFF' },
|
||||
{ name: 'UDP', percent: 18, color: '#14C9C9' },
|
||||
{ name: 'ICMP', percent: 2, color: '#F7BA1E' },
|
||||
{ name: '其他', percent: 2, color: '#86909C' },
|
||||
]
|
||||
|
||||
// 带宽利用率数据
|
||||
const bandwidthData = [
|
||||
{ name: '上行链路 1', percent: 85 },
|
||||
{ name: '上行链路 2', percent: 62 },
|
||||
{ name: '核心链路', percent: 45 },
|
||||
{ name: '备用链路', percent: 12 },
|
||||
]
|
||||
|
||||
// 获取带宽颜色
|
||||
const getBandwidthColor = (percent: number) => {
|
||||
if (percent >= 80) return '#F53F3F'
|
||||
if (percent >= 60) return '#FF7D00'
|
||||
return '#165DFF'
|
||||
}
|
||||
|
||||
// Top 应用流量数据
|
||||
const topApplications = ref([
|
||||
{
|
||||
name: 'HTTPS',
|
||||
port: '443',
|
||||
inbound: '4.2 Gbps',
|
||||
outbound: '3.1 Gbps',
|
||||
sessions: '125,456',
|
||||
shareValue: 45,
|
||||
},
|
||||
{
|
||||
name: 'HTTP',
|
||||
port: '80',
|
||||
inbound: '1.8 Gbps',
|
||||
outbound: '1.2 Gbps',
|
||||
sessions: '45,678',
|
||||
shareValue: 22,
|
||||
},
|
||||
{
|
||||
name: 'SSH',
|
||||
port: '22',
|
||||
inbound: '0.5 Gbps',
|
||||
outbound: '0.3 Gbps',
|
||||
sessions: '2,345',
|
||||
shareValue: 8,
|
||||
},
|
||||
{
|
||||
name: 'MySQL',
|
||||
port: '3306',
|
||||
inbound: '0.8 Gbps',
|
||||
outbound: '1.5 Gbps',
|
||||
sessions: '1,234',
|
||||
shareValue: 12,
|
||||
},
|
||||
{
|
||||
name: 'DNS',
|
||||
port: '53',
|
||||
inbound: '0.2 Gbps',
|
||||
outbound: '0.2 Gbps',
|
||||
sessions: '89,012',
|
||||
shareValue: 5,
|
||||
},
|
||||
])
|
||||
|
||||
// 应用流量表格列
|
||||
const applicationColumns: TableColumnData[] = [
|
||||
{ title: '应用', dataIndex: 'name', width: 80 },
|
||||
{ title: '端口', dataIndex: 'port', width: 70, align: 'center' },
|
||||
{ title: '入站流量', dataIndex: 'inbound', width: 100 },
|
||||
{ title: '出站流量', dataIndex: 'outbound', width: 100 },
|
||||
{ title: '会话数', dataIndex: 'sessions', width: 90 },
|
||||
{ title: '占比', dataIndex: 'share', slotName: 'share', width: 100 },
|
||||
]
|
||||
|
||||
// Top 流量源数据
|
||||
const topSources = ref([
|
||||
{
|
||||
ip: '192.168.1.100',
|
||||
name: 'Web-Server-01',
|
||||
traffic: '2.8 Gbps',
|
||||
sessions: '45,678',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.101',
|
||||
name: 'Web-Server-02',
|
||||
traffic: '2.4 Gbps',
|
||||
sessions: '38,456',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.2.50',
|
||||
name: 'API-Gateway',
|
||||
traffic: '1.8 Gbps',
|
||||
sessions: '28,901',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.3.10',
|
||||
name: 'DB-Master',
|
||||
traffic: '1.5 Gbps',
|
||||
sessions: '12,345',
|
||||
statusColor: 'orange',
|
||||
statusText: '高负载',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.200',
|
||||
name: 'Cache-Server',
|
||||
traffic: '1.2 Gbps',
|
||||
sessions: '56,789',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
])
|
||||
|
||||
// 流量源表格列
|
||||
const sourceColumns: TableColumnData[] = [
|
||||
{ title: 'IP地址', dataIndex: 'ip', width: 120 },
|
||||
{ title: '设备名称', dataIndex: 'name', width: 130 },
|
||||
{ title: '流量', dataIndex: 'traffic', width: 90 },
|
||||
{ title: '会话数', dataIndex: 'sessions', width: 90 },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 80, align: 'center' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TrafficAnalysis',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.page-title {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
height: 100px;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-primary {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-cyan {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
|
||||
&-green {
|
||||
background-color: rgba(0, 180, 42, 0.1);
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
&-purple {
|
||||
background-color: rgba(114, 46, 209, 0.1);
|
||||
color: #722ed1;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stats-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
span:first-child {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&-inbound {
|
||||
background-color: #165dff;
|
||||
}
|
||||
|
||||
&-outbound {
|
||||
background-color: #14c9c9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-row {
|
||||
margin-bottom: 16px;
|
||||
|
||||
> .arco-col {
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (min-width: 992px) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-card {
|
||||
height: 320px;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
height: calc(320px - 60px);
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
height: 60px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
span:first-child {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-list {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.protocol-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.protocol-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.protocol-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.protocol-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.direction-list {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.direction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.direction-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
|
||||
&-external {
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
|
||||
&-internal {
|
||||
background-color: rgba(20, 201, 201, 0.1);
|
||||
color: #14c9c9;
|
||||
}
|
||||
}
|
||||
|
||||
.direction-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.direction-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.direction-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.direction-percent {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.direction-stats {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.direction-stat {
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bandwidth-list {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bandwidth-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.bandwidth-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bandwidth-name {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.bandwidth-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.tables-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
height: 100%;
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
.text-success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
</style>
|
||||
@@ -311,19 +311,38 @@ const handleDrop = async (data: { dragNode: MenuNode; targetNode: MenuNode; posi
|
||||
.filter(item => item.parent_id === targetNode.parent_id)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
|
||||
siblings.forEach((item) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
// 找到拖拽节点和目标节点的索引
|
||||
const dragIndex = siblings.findIndex(item => item.id === dragNode.id)
|
||||
const targetIndex = siblings.findIndex(item => item.id === targetNode.id)
|
||||
|
||||
// 检查是否需要移动
|
||||
const isSameParent = dragNode.parent_id === targetNode.parent_id
|
||||
const isAlreadyBefore = isSameParent && dragIndex === targetIndex - 1
|
||||
const isAlreadyAfter = isSameParent && dragIndex === targetIndex + 1
|
||||
|
||||
// 如果已经在目标位置,不需要更新排序
|
||||
const needSort = !(isSameParent && (
|
||||
(position === 'before' && dragIndex === targetIndex) ||
|
||||
(position === 'before' && isAlreadyBefore) ||
|
||||
(position === 'after' && dragIndex === targetIndex) ||
|
||||
(position === 'after' && isAlreadyAfter)
|
||||
))
|
||||
|
||||
if (needSort) {
|
||||
siblings.forEach((item) => {
|
||||
if (item.id === targetNode.id) {
|
||||
if (position === 'before') {
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
} else if (item.id !== dragNode.id) {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
} else {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
sortList.push({ pmn_id: dragNode.id!, sort_key: sortKey++ })
|
||||
}
|
||||
} else if (item.id !== dragNode.id) {
|
||||
sortList.push({ pmn_id: item.id!, sort_key: sortKey++ })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 更新拖拽节点的 parent_id
|
||||
const { children, ...dragData } = dragNode
|
||||
|
||||
Reference in New Issue
Block a user