This commit is contained in:
ygx
2026-03-22 16:40:20 +08:00
parent 3d97443707
commit d42823142e
12 changed files with 7660 additions and 11 deletions

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

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

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

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

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

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

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

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

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

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

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

View File

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