fix
This commit is contained in:
94
src/api/ops/network-device.ts
Normal file
94
src/api/ops/network-device.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
export type NetworkDeviceProtocol = 'snmp' | 'ssh' | 'telnet'
|
||||
export type NetworkDeviceSnmpVersion = 'v1' | 'v2c' | 'v3'
|
||||
export type NetworkDeviceStatus = 'online' | 'offline' | 'error' | 'unknown'
|
||||
|
||||
export interface NetworkDeviceService {
|
||||
id: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
deleted_at: string | null
|
||||
service_identity: string
|
||||
server_identity: string
|
||||
name: string
|
||||
category: string
|
||||
type: string
|
||||
host: string
|
||||
port: number
|
||||
protocol: NetworkDeviceProtocol
|
||||
community: string
|
||||
snmp_version: NetworkDeviceSnmpVersion
|
||||
username: string
|
||||
vendor: string
|
||||
model: string
|
||||
location: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
interval: number
|
||||
extra: string
|
||||
tags: string
|
||||
status: NetworkDeviceStatus
|
||||
status_code: number
|
||||
status_message: string
|
||||
response_time: number
|
||||
last_check_time: string
|
||||
last_online_time: string | null
|
||||
last_offline_time: string | null
|
||||
continuous_errors: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
export interface NetworkDeviceListResponse {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
data: NetworkDeviceService[]
|
||||
}
|
||||
|
||||
export interface NetworkDeviceQueryParams {
|
||||
page?: number
|
||||
size?: number
|
||||
keyword?: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export interface CreateNetworkDeviceParams {
|
||||
service_identity?: string
|
||||
server_identity?: string
|
||||
name: string
|
||||
category: string
|
||||
type?: string
|
||||
host: string
|
||||
port?: number
|
||||
protocol?: NetworkDeviceProtocol
|
||||
community?: string
|
||||
snmp_version?: NetworkDeviceSnmpVersion
|
||||
username?: string
|
||||
password?: string
|
||||
vendor?: string
|
||||
model?: string
|
||||
location?: string
|
||||
description?: string
|
||||
enabled?: boolean
|
||||
interval?: number
|
||||
extra?: string
|
||||
tags?: string
|
||||
}
|
||||
|
||||
export type UpdateNetworkDeviceParams = Partial<CreateNetworkDeviceParams>
|
||||
|
||||
export const fetchNetworkDeviceList = (params: NetworkDeviceQueryParams) =>
|
||||
request.get<NetworkDeviceListResponse>('/DC-Control/v1/network-device-services', { params })
|
||||
|
||||
export const fetchNetworkDeviceDetail = (id: number) =>
|
||||
request.get<NetworkDeviceService>(`/DC-Control/v1/network-device-services/${id}`)
|
||||
|
||||
export const createNetworkDevice = (data: CreateNetworkDeviceParams) =>
|
||||
request.post<{ message: string; id: number }>('/DC-Control/v1/network-device-services', data)
|
||||
|
||||
export const updateNetworkDevice = (id: number, data: UpdateNetworkDeviceParams) =>
|
||||
request.put<{ message: string }>(`/DC-Control/v1/network-device-services/${id}`, data)
|
||||
|
||||
export const deleteNetworkDevice = (id: number) =>
|
||||
request.delete<{ message: string }>(`/DC-Control/v1/network-device-services/${id}`)
|
||||
29
src/api/ops/network-screen.ts
Normal file
29
src/api/ops/network-screen.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
export const fetchNetworkScreenOverview = () =>
|
||||
request.get('/DC-Control/v1/network-screen/overview')
|
||||
|
||||
export const fetchNetworkScreenHealthMatrix = (params?: { window?: string }) =>
|
||||
request.get('/DC-Control/v1/network-screen/health-matrix', { params })
|
||||
|
||||
export const fetchNetworkScreenFreshnessDistribution = () =>
|
||||
request.get('/DC-Control/v1/network-screen/freshness-distribution')
|
||||
|
||||
export const fetchNetworkScreenProtocolDistribution = () =>
|
||||
request.get('/DC-Control/v1/network-screen/protocol-distribution')
|
||||
|
||||
export const fetchNetworkScreenResourceOverview = (params?: { window?: string }) =>
|
||||
request.get('/DC-Control/v1/network-screen/resource-overview', { params })
|
||||
|
||||
export const fetchNetworkScreenAlertsStream = (params?: { limit?: number; window?: string }) =>
|
||||
request.get('/DC-Control/v1/network-screen/alerts-stream', { params })
|
||||
|
||||
export const fetchNetworkScreenDevices = (params?: {
|
||||
page?: number
|
||||
size?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
enabled?: boolean
|
||||
protocol?: string
|
||||
vendor?: string
|
||||
}) => request.get('/DC-Control/v1/network-screen/devices', { params })
|
||||
@@ -154,8 +154,8 @@ export const localMenuFlatItems: MenuItem[] = [
|
||||
{
|
||||
id: 22,
|
||||
identity: '019b591d-0159-7e46-a5e0-fb69c6b62a25',
|
||||
title: '网络设备采集管理',
|
||||
title_en: 'Network Device Collection Management',
|
||||
title: '网络设备数据采集管理',
|
||||
title_en: 'Network Device Data Collection Management',
|
||||
code: 'ops:集群采集控制中心:网络设备采集管理',
|
||||
description: '集群采集控制中心 - 网络设备采集管理',
|
||||
app_id: 2,
|
||||
|
||||
@@ -164,8 +164,8 @@ export const localMenuItems: MenuItem[] = [
|
||||
{
|
||||
id: 22,
|
||||
identity: '019b591d-0159-7e46-a5e0-fb69c6b62a25',
|
||||
title: '网络设备采集管理',
|
||||
title_en: 'Network Device Collection Management',
|
||||
title: '网络设备数据采集管理',
|
||||
title_en: 'Network Device Data Collection Management',
|
||||
code: 'ops:集群采集控制中心:网络设备采集管理',
|
||||
description: '集群采集控制中心 - 网络设备采集管理',
|
||||
app_id: 2,
|
||||
|
||||
@@ -177,7 +177,10 @@ const getQuietHoursDisplay = () => {
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return ''
|
||||
try {
|
||||
if (time.startsWith('0001-01-01')) return ''
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
if (date.getFullYear() <= 1970) return ''
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
|
||||
@@ -154,7 +154,10 @@ const rules = {
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -234,7 +234,10 @@ const formatUptime = (uptime?: number) => {
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string | null) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -253,7 +253,10 @@ const formatUptime = (uptime: number) => {
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -124,7 +124,10 @@ const handleViewMetrics = async () => {
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -530,7 +530,11 @@ const formatUptime = (uptime?: number) => {
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string | null) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化额外配置
|
||||
|
||||
@@ -241,7 +241,10 @@ const formatUptime = (uptime?: number) => {
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -1,705 +1,120 @@
|
||||
<template>
|
||||
<div class="server-detail">
|
||||
<div class="detail-header">
|
||||
<div class="server-info">
|
||||
<h2>{{ record.name || '办公PC详情' }}</h2>
|
||||
<div class="info-tags">
|
||||
<a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag>
|
||||
<a-tag color="blue">{{ record.server_type || '未知类型' }}</a-tag>
|
||||
<a-tag color="cyan">{{ record.os || '未知系统' }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button @click="handleRemoteControl">
|
||||
<template #icon>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
远程登录
|
||||
</a-button>
|
||||
<a-button type="primary" @click="handleRestart">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
</template>
|
||||
重启
|
||||
</a-button>
|
||||
<div class="detail">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="title">{{ record.name || '-' }}</div>
|
||||
<a-space>
|
||||
<a-tag :color="statusColor">{{ statusText }}</a-tag>
|
||||
<a-tag color="arcoblue">{{ formatDeviceCategory(record.category) }}</a-tag>
|
||||
<a-tag>{{ (record.vendor || '-') + (record.model ? ` / ${record.model}` : '') }}</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="$emit('edit')">编辑</a-button>
|
||||
<a-button status="danger" @click="$emit('delete')">删除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:active-tab="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="overview" title="实例详情">
|
||||
<a-descriptions :column="2" bordered class="info-descriptions">
|
||||
<a-descriptions-item label="唯一标识">{{ record.unique_id || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="办公PC名称">{{ record.name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作系统">{{ record.os || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="位置信息">{{ record.location || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签">{{ record.tags || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">{{ record.ip || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="远程端口">{{ record.remote_port || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Agent URL">{{ record.agent_url || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="数据采集">
|
||||
<a-tag :color="record.data_collection ? 'green' : 'gray'">
|
||||
{{ record.data_collection ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ record.collection_interval ? `${record.collection_interval}分钟` : '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="备注信息" :span="2">{{ record.remark || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="monitor" title="监控">
|
||||
<div class="monitor-section">
|
||||
<div class="time-selector">
|
||||
<a-radio-group v-model="timeRange" type="button">
|
||||
<a-radio value="1h">1小时</a-radio>
|
||||
<a-radio value="3h">3小时</a-radio>
|
||||
<a-radio value="6h">6小时</a-radio>
|
||||
<a-radio value="12h">12小时</a-radio>
|
||||
<a-radio value="1d">1天</a-radio>
|
||||
<a-radio value="3d">3天</a-radio>
|
||||
<a-radio value="7d">7天</a-radio>
|
||||
<a-radio value="14d">14天</a-radio>
|
||||
<a-radio value="custom">自定义</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">CPU使用率_宿主机视角 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="cpuMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
<a-option value="min">最小值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="cpuOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)CPU使用率_平均值</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)CPU使用率_最小值</span>
|
||||
<span class="legend-item"><span class="legend-dot purple"></span>(ECS)CPU使用率_最大值</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">总带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot green"></span>内网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>内网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网带宽 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="publicBandwidthMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="publicBandwidthOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>公网流入带宽</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>公网流出带宽</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">公网流出带宽使用率 (%)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="bandwidthUsageMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="bandwidthUsageOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)公网流出带宽使用率</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入BPS (bytes/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskBpsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘读取BPS</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘写入BPS</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">磁盘读取写入IOPS (Count/Second)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="diskIopsMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="diskIopsOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>(ECS)所有磁盘每秒读取次数</span>
|
||||
<span class="legend-item"><span class="legend-dot green"></span>(ECS)所有磁盘每秒写入次数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20" class="charts-row">
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">同时连接数 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="connectionMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="connectionOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"><span class="legend-dot blue"></span>同时连接数</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA流量使用信息 (bit/s)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<a-col :span="8" class="chart-col">
|
||||
<div class="chart-card">
|
||||
<div class="chart-header">
|
||||
<span class="chart-title">RDMA资源使用信息 (Count)</span>
|
||||
<div class="chart-actions">
|
||||
<a-select v-model="rdmaResourceMetric" size="small" style="width: 120px">
|
||||
<a-option value="average">平均值</a-option>
|
||||
<a-option value="max">最大值</a-option>
|
||||
</a-select>
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
<v-chart :option="rdmaResourceOption" class="chart" autoresize />
|
||||
<div class="chart-legend">
|
||||
<span class="legend-item"></span>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="operation" title="操作记录">
|
||||
<div class="operation-records">
|
||||
<a-empty description="暂无操作记录" />
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="服务标识">{{ record.service_identity || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="执行节点">{{ record.server_identity || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="采集地址">{{ record.host || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="端口">{{ record.port || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="协议">{{ String(record.protocol || '-').toUpperCase() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="SNMP版本">{{ String(record.snmp_version || '-').toUpperCase() }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户名">{{ record.username || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="密码">已设置(不回显)</a-descriptions-item>
|
||||
<a-descriptions-item label="采集开关">
|
||||
<a-tag :color="record.enabled ? 'green' : 'gray'">{{ record.enabled ? '启用' : '停用' }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ record.interval || 0 }} 秒</a-descriptions-item>
|
||||
<a-descriptions-item label="状态消息" :span="2">{{ record.status_message || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最近检查">{{ formatTime(record.last_check_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="响应时间">{{ formatResponseTime(record.response_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最近上线">{{ formatTime(record.last_online_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最近离线">{{ formatTime(record.last_offline_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="连续失败次数">{{ record.continuous_errors ?? 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item label="运行时长">{{ formatUptime(record.uptime) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签" :span="2">{{ record.tags || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="位置">{{ record.location || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="描述">{{ record.description || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconDesktop, IconRefresh, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart } from 'echarts/charts'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
|
||||
use([
|
||||
CanvasRenderer,
|
||||
LineChart,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
GridComponent,
|
||||
DataZoomComponent,
|
||||
])
|
||||
import { computed } from 'vue'
|
||||
import type { NetworkDeviceService } from '@/api/ops/network-device'
|
||||
|
||||
interface Props {
|
||||
record: any
|
||||
record: Partial<NetworkDeviceService>
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
defineEmits(['edit', 'delete'])
|
||||
|
||||
const emit = defineEmits(['remote-control', 'restart', 'close'])
|
||||
const statusMap: Record<string, { text: string; color: string }> = {
|
||||
online: { text: '在线', color: 'green' },
|
||||
offline: { text: '离线', color: 'red' },
|
||||
error: { text: '异常', color: 'orange' },
|
||||
unknown: { text: '未知', color: 'gray' },
|
||||
}
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const timeRange = ref('1h')
|
||||
const cpuMetric = ref('average')
|
||||
const bandwidthMetric = ref('average')
|
||||
const publicBandwidthMetric = ref('average')
|
||||
const bandwidthUsageMetric = ref('average')
|
||||
const diskMetric = ref('average')
|
||||
const diskIopsMetric = ref('average')
|
||||
const connectionMetric = ref('average')
|
||||
const rdmaMetric = ref('average')
|
||||
const rdmaResourceMetric = ref('average')
|
||||
const statusText = computed(() => statusMap[props.record.status || 'unknown']?.text || '未知')
|
||||
const statusColor = computed(() => statusMap[props.record.status || 'unknown']?.color || 'gray')
|
||||
|
||||
const generateTimeData = (count: number) => {
|
||||
const now = Date.now()
|
||||
const data = []
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
data.push(new Date(now - i * 60000).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }))
|
||||
const formatDeviceCategory = (category?: string) => {
|
||||
const map: Record<string, string> = {
|
||||
router: '路由器',
|
||||
switch: '交换机',
|
||||
firewall: '防火墙',
|
||||
access_point: '接入点',
|
||||
load_balancer: '负载均衡',
|
||||
other: '其他',
|
||||
}
|
||||
return data
|
||||
if (!category) return '-'
|
||||
return map[category] || category
|
||||
}
|
||||
|
||||
const generateRandomData = (count: number, min: number, max: number) => {
|
||||
return Array.from({ length: count }, () => Math.floor(Math.random() * (max - min + 1)) + min)
|
||||
const formatTime = (time?: string | null) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const d = new Date(time)
|
||||
if (Number.isNaN(d.getTime())) return '-'
|
||||
if (d.getFullYear() <= 1970) return '-'
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(
|
||||
d.getHours()
|
||||
).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const timeData = generateTimeData(60)
|
||||
const formatResponseTime = (v?: number) => (typeof v === 'number' && v >= 0 ? `${v.toFixed(2)} ms` : '-')
|
||||
|
||||
const baseOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
borderColor: '#e5e6eb',
|
||||
textStyle: {
|
||||
color: '#1d2129',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: timeData,
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
},
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'inside',
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const cpuOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '平均值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20, 80),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(0, 180, 42, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(0, 180, 42, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '最小值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10, 40),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '最大值',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 95),
|
||||
lineStyle: { color: '#722ed1' },
|
||||
itemStyle: { color: '#722ed1' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '内网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100000, 500000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
{
|
||||
name: '内网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 300000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const publicBandwidthOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流入带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50000, 200000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '公网流出带宽',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 30000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const bandwidthUsageOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '公网流出带宽使用率',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 5, 30),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(22, 93, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(22, 93, 255, 0.05)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskBpsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 10000, 100000),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入BPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 20000, 150000),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const diskIopsOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '磁盘读取IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 50, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
{
|
||||
name: '磁盘写入IOPS',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 800),
|
||||
lineStyle: { color: '#00b42a' },
|
||||
itemStyle: { color: '#00b42a' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const connectionOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [
|
||||
{
|
||||
name: '同时连接数',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: generateRandomData(60, 100, 500),
|
||||
lineStyle: { color: '#165dff' },
|
||||
itemStyle: { color: '#165dff' },
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const rdmaOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const rdmaResourceOption = computed(() => ({
|
||||
...baseOption,
|
||||
series: [],
|
||||
}))
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
|
||||
const handleRemoteControl = () => {
|
||||
emit('remote-control')
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
emit('restart')
|
||||
const formatUptime = (uptime?: number) => {
|
||||
if (!uptime || uptime <= 0) return '-'
|
||||
const days = Math.floor(uptime / 86400)
|
||||
const hours = Math.floor((uptime % 86400) / 3600)
|
||||
const minutes = Math.floor((uptime % 3600) / 60)
|
||||
if (days > 0) return `${days}天 ${hours}小时`
|
||||
if (hours > 0) return `${hours}小时 ${minutes}分钟`
|
||||
return `${minutes}分钟`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.server-detail {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
.detail {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e5e6eb;
|
||||
|
||||
.server-info {
|
||||
h2 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.info-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
:deep(.arco-tabs-header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-descriptions {
|
||||
:deep(.arco-descriptions-item-label) {
|
||||
width: 140px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
.monitor-section {
|
||||
.time-selector {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-col {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.chart-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.chart-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e6eb;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
|
||||
.legend-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
||||
&.green {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background: #165dff;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background: #722ed1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-records {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,182 +1,254 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑网络设备' : '新增网络设备'"
|
||||
:title="isEdit ? '编辑网络设备采集' : '新增网络设备采集'"
|
||||
:confirm-loading="confirmLoading"
|
||||
:mask-closable="false"
|
||||
width="980px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleUpdateVisible"
|
||||
:confirm-loading="confirmLoading"
|
||||
width="800px"
|
||||
>
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical">
|
||||
<a-form ref="formRef" :model="formData" :rules="rules" layout="vertical" auto-label-width>
|
||||
<a-alert type="info" show-icon class="mb-12">
|
||||
采集协议固定为 SNMP + SSH 双协议,请同时完成两部分配置。编辑时密码不回显,留空表示不修改。
|
||||
</a-alert>
|
||||
<a-divider orientation="left">基础信息</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="name" label="设备名称">
|
||||
<a-input v-model="formData.name" placeholder="请输入设备名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="type" label="设备类型">
|
||||
<a-select v-model="formData.type" placeholder="请选择设备类型">
|
||||
<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>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12"><a-form-item field="name" label="设备名称" required><a-input v-model="formData.name" placeholder="例如:核心路由器-01" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="category" label="设备类型" required><a-select v-model="formData.category" allow-search><a-option value="router">路由器</a-option><a-option value="switch">交换机</a-option><a-option value="firewall">防火墙</a-option><a-option value="access_point">接入点</a-option><a-option value="load_balancer">负载均衡</a-option><a-option value="other">其他</a-option></a-select></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="server_identity" label="执行节点"><a-select v-model="formData.server_identity" allow-search allow-clear><a-option v-for="server in serverOptions" :key="server.server_identity" :value="server.server_identity">{{ server.name }} ({{ server.server_identity }})</a-option></a-select></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="vendor" label="厂商"><a-select v-model="formData.vendor" allow-search allow-create allow-clear><a-option value="Cisco">Cisco</a-option><a-option value="Huawei">Huawei</a-option><a-option value="H3C">H3C</a-option><a-option value="Juniper">Juniper</a-option></a-select></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="model" label="型号"><a-input v-model="formData.model" placeholder="例如:ASR1001-X" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="location" label="位置信息"><a-input v-model="formData.location" placeholder="例如:A机房-R03-12U" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-form-item field="description" label="描述"><a-textarea v-model="formData.description" :rows="2" /></a-form-item>
|
||||
<a-form-item field="tags" label="标签"><a-input-tag v-model="tagList" placeholder="回车添加标签" /></a-form-item>
|
||||
|
||||
<a-divider orientation="left">连接信息</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="host" label="设备地址" required><a-input v-model="formData.host" placeholder="例如:192.168.1.10" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item label="采集协议"><a-tag color="purple">SNMP + SSH(固定)</a-tag></a-form-item></a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="left">SNMP 配置(必填)</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="manu" label="厂商">
|
||||
<a-select v-model="formData.manu" placeholder="请选择厂商">
|
||||
<a-option value="华为">华为</a-option>
|
||||
<a-option value="华三">华三</a-option>
|
||||
<a-option value="思科">思科</a-option>
|
||||
<a-option value="深信服">深信服</a-option>
|
||||
<a-option value="F5">F5</a-option>
|
||||
<a-option value="锐捷">锐捷</a-option>
|
||||
<a-option value="TP-LINK">TP-LINK</a-option>
|
||||
<a-option value="其它">其它</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12"><a-form-item field="snmp_port" label="SNMP端口" required><a-input-number v-model="formData.port" :min="1" :max="65535" style="width: 100%" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="snmp_version" label="SNMP版本"><a-radio-group v-model="formData.snmp_version" type="button"><a-radio value="v1">v1</a-radio><a-radio value="v2c">v2c</a-radio><a-radio value="v3">v3</a-radio></a-radio-group></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-form-item v-if="formData.snmp_version !== 'v3'" field="community" label="Community" required><a-input v-model="formData.community" placeholder="SNMP v1/v2c 必填" /></a-form-item>
|
||||
<a-row v-else :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="snmpv3_security_name" label="Security Name" required><a-input v-model="formData.snmpv3.security_name" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="snmpv3_security_level" label="Security Level"><a-select v-model="formData.snmpv3.security_level"><a-option value="noAuthNoPriv">noAuthNoPriv</a-option><a-option value="authNoPriv">authNoPriv</a-option><a-option value="authPriv">authPriv</a-option></a-select></a-form-item></a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="left">SSH 配置(必填)</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="model_num" label="型号">
|
||||
<a-input v-model="formData.model_num" placeholder="请输入设备型号" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="protocol" label="通讯协议">
|
||||
<a-select v-model="formData.protocol" placeholder="请选择通讯协议">
|
||||
<a-option value="SNMP">SNMP</a-option>
|
||||
<a-option value="TELNET">TELNET</a-option>
|
||||
<a-option value="SSH">SSH</a-option>
|
||||
<a-option value="HTTP">HTTP</a-option>
|
||||
<a-option value="HTTPS">HTTPS</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12"><a-form-item field="ssh_port" label="SSH端口" required><a-input-number v-model="formData.ssh.port" :min="1" :max="65535" style="width: 100%" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="ssh_username" label="SSH用户名" required><a-input v-model="formData.ssh.username" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="ip" label="IP 地址">
|
||||
<a-input v-model="formData.ip" placeholder="可以输入多个 IP,逗号做分隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="tags" label="标签">
|
||||
<a-input v-model="formData.tags" placeholder="多个标签,逗号间隔" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12"><a-form-item field="ssh_password" label="SSH密码" required><a-input-password v-model="formData.ssh.password" :placeholder="isEdit ? '留空表示不修改' : '请输入 SSH 密码'" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="ssh_command_fib" label="FIB命令覆盖"><a-input v-model="formData.ssh.command_fib" placeholder="可选:覆盖厂商默认命令" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-form-item field="ssh_private_key" label="SSH私钥"><a-textarea v-model="formData.ssh.private_key" :rows="2" placeholder="可选,支持粘贴私钥" /></a-form-item>
|
||||
|
||||
<a-form-item field="location" label="位置信息">
|
||||
<a-input v-model="formData.location" placeholder="请输入位置信息" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item field="remark" label="备注信息">
|
||||
<a-textarea v-model="formData.remark" placeholder="请输入备注信息" :rows="4" />
|
||||
</a-form-item>
|
||||
<a-divider orientation="left">采集策略</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="enabled" label="启用采集"><a-switch v-model="formData.enabled" /></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="interval" label="采集间隔(秒)"><a-input-number v-model="formData.interval" :min="1" :max="86400" style="width: 100%" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="20"><a-col :span="12"><a-checkbox v-model="formData.collect.interface_enabled">采集接口信息</a-checkbox></a-col><a-col :span="12"><a-checkbox v-model="formData.collect.arp_enabled">采集ARP信息</a-checkbox></a-col></a-row>
|
||||
<a-row :gutter="20" class="mt-8"><a-col :span="12"><a-checkbox v-model="formData.collect.route_enabled">采集路由信息</a-checkbox></a-col><a-col :span="12"><a-checkbox v-model="formData.collect.fib_enabled">采集FIB信息</a-checkbox></a-col></a-row>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||
import { createNetworkDevice, updateNetworkDevice } from '@/api/ops/network-device'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
record?: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
record: () => ({}),
|
||||
})
|
||||
|
||||
interface Props { visible: boolean; record?: any }
|
||||
const props = withDefaults(defineProps<Props>(), { record: () => ({}) })
|
||||
const emit = defineEmits(['update:visible', 'success'])
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const confirmLoading = ref(false)
|
||||
|
||||
const serverOptions = ref<ServerItem[]>([])
|
||||
const tagList = ref<string[]>([])
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
|
||||
const formData = reactive({
|
||||
unique_id: '',
|
||||
const defaults = () => ({
|
||||
service_identity: '',
|
||||
server_identity: '',
|
||||
name: '',
|
||||
type: '',
|
||||
manu: '',
|
||||
model_num: '',
|
||||
protocol: '',
|
||||
ip: '',
|
||||
tags: '',
|
||||
category: 'router',
|
||||
type: 'network',
|
||||
host: '',
|
||||
port: 161,
|
||||
protocol: 'snmp',
|
||||
community: 'public',
|
||||
snmp_version: 'v2c',
|
||||
vendor: '',
|
||||
model: '',
|
||||
location: '',
|
||||
remark: '',
|
||||
description: '',
|
||||
enabled: true,
|
||||
interval: 60,
|
||||
tags: '',
|
||||
snmpv3: {
|
||||
security_level: 'noAuthNoPriv',
|
||||
security_name: '',
|
||||
auth_protocol: 'SHA',
|
||||
auth_password: '',
|
||||
priv_protocol: 'AES',
|
||||
priv_password: '',
|
||||
context_name: '',
|
||||
},
|
||||
ssh: {
|
||||
port: 22,
|
||||
username: '',
|
||||
password: '',
|
||||
private_key: '',
|
||||
command_fib: '',
|
||||
},
|
||||
collect: { interface_enabled: true, arp_enabled: true, route_enabled: true, fib_enabled: true, timeout_sec: 5, retries: 1 },
|
||||
})
|
||||
|
||||
const formData = reactive(defaults())
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入设备名称' }],
|
||||
type: [{ required: true, message: '请选择设备类型' }],
|
||||
manu: [{ required: true, message: '请选择厂商' }],
|
||||
category: [{ required: true, message: '请选择设备类型' }],
|
||||
host: [{ required: true, message: '请输入设备地址' }],
|
||||
community: [{ required: true, message: '请输入 SNMP community' }],
|
||||
ssh_username: [{ required: true, message: '请输入 SSH 用户名' }],
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
if (isEdit.value && props.record) {
|
||||
Object.assign(formData, props.record)
|
||||
} else {
|
||||
Object.assign(formData, {
|
||||
unique_id: '',
|
||||
name: '',
|
||||
type: '',
|
||||
manu: '',
|
||||
model_num: '',
|
||||
protocol: '',
|
||||
ip: '',
|
||||
tags: '',
|
||||
location: '',
|
||||
remark: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
watch(() => props.visible, (val) => {
|
||||
if (!val) return
|
||||
if (isEdit.value && props.record) {
|
||||
let parsedExtra: any = {}
|
||||
try { parsedExtra = props.record.extra ? JSON.parse(props.record.extra) : {} } catch {}
|
||||
const deviceCategory = (props.record.category && props.record.category !== 'network_device')
|
||||
? props.record.category
|
||||
: (props.record.type || 'other')
|
||||
Object.assign(formData, defaults(), {
|
||||
...props.record,
|
||||
category: deviceCategory,
|
||||
type: 'network',
|
||||
collect: {
|
||||
interface_enabled: parsedExtra.collect?.interface_enabled ?? true,
|
||||
arp_enabled: parsedExtra.collect?.arp_enabled ?? true,
|
||||
route_enabled: parsedExtra.collect?.route_enabled ?? true,
|
||||
fib_enabled: parsedExtra.collect?.fib_enabled ?? true,
|
||||
timeout_sec: parsedExtra.collect?.timeout_sec ?? 5,
|
||||
retries: parsedExtra.collect?.retries ?? 1,
|
||||
},
|
||||
snmpv3: {
|
||||
security_level: parsedExtra.snmp_v3?.security_level || 'noAuthNoPriv',
|
||||
security_name: parsedExtra.snmp_v3?.security_name || '',
|
||||
auth_protocol: parsedExtra.snmp_v3?.auth_protocol || 'SHA',
|
||||
auth_password: '',
|
||||
priv_protocol: parsedExtra.snmp_v3?.priv_protocol || 'AES',
|
||||
priv_password: '',
|
||||
context_name: parsedExtra.snmp_v3?.context_name || '',
|
||||
},
|
||||
ssh: {
|
||||
port: parsedExtra.ssh?.port || 22,
|
||||
username: parsedExtra.ssh?.username || props.record.username || '',
|
||||
password: '',
|
||||
private_key: parsedExtra.ssh?.private_key || '',
|
||||
command_fib: parsedExtra.ssh?.command_fib || '',
|
||||
},
|
||||
})
|
||||
tagList.value = (props.record.tags || '').split(',').map((i: string) => i.trim()).filter(Boolean)
|
||||
} else {
|
||||
Object.assign(formData, defaults())
|
||||
tagList.value = []
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
watch(tagList, (tags) => { formData.tags = tags.join(',') })
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
|
||||
if (formData.snmp_version !== 'v3' && !formData.community.trim()) {
|
||||
Message.warning('请填写 SNMP community')
|
||||
return
|
||||
}
|
||||
if (!formData.ssh.username.trim()) {
|
||||
Message.warning('请填写 SSH 用户名')
|
||||
return
|
||||
}
|
||||
if (!isEdit.value && !formData.ssh.password.trim()) {
|
||||
Message.warning('请填写 SSH 密码')
|
||||
return
|
||||
}
|
||||
confirmLoading.value = true
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
const payload: any = {
|
||||
...formData,
|
||||
type: 'network',
|
||||
protocol: 'snmp',
|
||||
username: formData.ssh.username,
|
||||
extra: JSON.stringify({
|
||||
schema_version: '1.0',
|
||||
collect: formData.collect,
|
||||
snmp_v2c: { port: formData.port, community: formData.community },
|
||||
snmp_v3: {
|
||||
port: formData.port,
|
||||
security_level: formData.snmpv3.security_level,
|
||||
security_name: formData.snmpv3.security_name,
|
||||
auth_protocol: formData.snmpv3.auth_protocol,
|
||||
auth_password: formData.snmpv3.auth_password,
|
||||
priv_protocol: formData.snmpv3.priv_protocol,
|
||||
priv_password: formData.snmpv3.priv_password,
|
||||
context_name: formData.snmpv3.context_name,
|
||||
},
|
||||
ssh: formData.ssh,
|
||||
}),
|
||||
}
|
||||
delete payload.snmpv3
|
||||
delete payload.ssh
|
||||
delete payload.collect
|
||||
if (formData.ssh.password.trim()) {
|
||||
payload.password = formData.ssh.password
|
||||
} else {
|
||||
delete payload.password
|
||||
}
|
||||
if (isEdit.value) await updateNetworkDevice(props.record.id, payload)
|
||||
else await createNetworkDevice(payload)
|
||||
Message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
emit('success')
|
||||
handleCancel()
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error)
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) Message.error(error.response.data.message)
|
||||
else Message.error('保存失败,请检查输入后重试')
|
||||
} finally {
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateVisible = (value: boolean) => {
|
||||
emit('update:visible', value)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
formRef.value?.resetFields()
|
||||
const handleUpdateVisible = (value: boolean) => emit('update:visible', value)
|
||||
const handleCancel = () => { emit('update:visible', false); formRef.value?.resetFields() }
|
||||
const loadServerOptions = async () => {
|
||||
try {
|
||||
const response: any = await fetchServerList({ page: 1, size: 1000 })
|
||||
serverOptions.value = response?.details?.data || []
|
||||
} catch {
|
||||
serverOptions.value = []
|
||||
}
|
||||
}
|
||||
onMounted(loadServerOptions)
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.mb-12 { margin-bottom: 12px; }
|
||||
.mt-8 { margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -1,60 +1,48 @@
|
||||
export const columns = [
|
||||
{
|
||||
dataIndex: 'id',
|
||||
title: 'ID',
|
||||
width: 80,
|
||||
slotName: 'id',
|
||||
},
|
||||
{
|
||||
dataIndex: 'unique_id',
|
||||
title: 'OID',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: '名称',
|
||||
width: 150,
|
||||
title: '设备名称',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
dataIndex: 'type',
|
||||
title: '类型',
|
||||
dataIndex: 'category',
|
||||
title: '设备类型',
|
||||
width: 120,
|
||||
slotName: 'category',
|
||||
},
|
||||
{
|
||||
dataIndex: 'vendor',
|
||||
title: '厂商',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'manu',
|
||||
title: '厂商',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'model_num',
|
||||
dataIndex: 'model',
|
||||
title: '型号',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'location',
|
||||
title: '位置信息',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
dataIndex: 'tags',
|
||||
title: '标签',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
dataIndex: 'ip',
|
||||
title: 'IP地址',
|
||||
width: 150,
|
||||
dataIndex: 'host',
|
||||
title: 'IP:端口',
|
||||
width: 170,
|
||||
slotName: 'host_port',
|
||||
},
|
||||
{
|
||||
dataIndex: 'protocol',
|
||||
title: '通讯协议',
|
||||
width: 100,
|
||||
title: '协议',
|
||||
width: 130,
|
||||
slotName: 'protocol',
|
||||
},
|
||||
{
|
||||
dataIndex: 'indicator',
|
||||
title: '网络设备指标',
|
||||
width: 150,
|
||||
slotName: 'indicator',
|
||||
dataIndex: 'interval',
|
||||
title: '采集间隔',
|
||||
width: 110,
|
||||
slotName: 'interval',
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
title: '启用状态',
|
||||
width: 100,
|
||||
slotName: 'enabled',
|
||||
},
|
||||
{
|
||||
dataIndex: 'status',
|
||||
@@ -62,10 +50,28 @@ export const columns = [
|
||||
width: 100,
|
||||
slotName: 'status',
|
||||
},
|
||||
{
|
||||
dataIndex: 'response_time',
|
||||
title: '响应时间',
|
||||
width: 110,
|
||||
slotName: 'response_time',
|
||||
},
|
||||
{
|
||||
dataIndex: 'last_check_time',
|
||||
title: '最近检查',
|
||||
width: 180,
|
||||
slotName: 'last_check_time',
|
||||
},
|
||||
{
|
||||
dataIndex: 'tags',
|
||||
title: '标签',
|
||||
width: 180,
|
||||
slotName: 'tags',
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: '操作',
|
||||
width: 180,
|
||||
width: 200,
|
||||
fixed: 'right' as const,
|
||||
slotName: 'actions',
|
||||
},
|
||||
|
||||
@@ -5,35 +5,42 @@ export const searchFormConfig: FormItem[] = [
|
||||
field: 'keyword',
|
||||
label: '关键词',
|
||||
type: 'input',
|
||||
placeholder: '请输入服务器名称、编码或IP',
|
||||
placeholder: '请输入名称/IP/厂商/型号',
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'datacenter_id',
|
||||
label: '数据中心',
|
||||
field: 'enabled',
|
||||
label: '启用状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择数据中心',
|
||||
options: [], // 需要动态加载
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'rack_id',
|
||||
label: '机柜',
|
||||
type: 'select',
|
||||
placeholder: '请选择机柜',
|
||||
options: [], // 需要动态加载
|
||||
placeholder: '全部',
|
||||
options: [
|
||||
{ label: '已启用', value: true },
|
||||
{ label: '已停用', value: false },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
label: '状态',
|
||||
label: '设备状态',
|
||||
type: 'select',
|
||||
placeholder: '请选择状态',
|
||||
placeholder: '全部',
|
||||
options: [
|
||||
{ label: '在线', value: 'online' },
|
||||
{ label: '离线', value: 'offline' },
|
||||
{ label: '维护中', value: 'maintenance' },
|
||||
{ label: '已退役', value: 'retired' },
|
||||
{ label: '异常', value: 'error' },
|
||||
{ label: '未知', value: 'unknown' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
{
|
||||
field: 'protocol',
|
||||
label: '采集协议',
|
||||
type: 'select',
|
||||
placeholder: '全部',
|
||||
options: [
|
||||
{ label: 'SNMP', value: 'snmp' },
|
||||
{ label: 'SSH', value: 'ssh' },
|
||||
{ label: 'Telnet', value: 'telnet' },
|
||||
],
|
||||
span: 6,
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
title="网络设备管理"
|
||||
title="网络设备数据采集管理"
|
||||
search-button-text="查询"
|
||||
reset-button-text="重置"
|
||||
@update:form-model="handleFormModelUpdate"
|
||||
@@ -16,298 +16,58 @@
|
||||
@page-change="handlePageChange"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<template #toolbar-left>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
新增网络设备
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<!-- ID -->
|
||||
<template #id="{ record }">
|
||||
{{ record.id }}
|
||||
</template>
|
||||
|
||||
<!-- 远程访问 -->
|
||||
<template #remote_access="{ record }">
|
||||
<a-tag :color="record.remote_access ? 'green' : 'gray'">
|
||||
{{ record.remote_access ? '已开启' : '未开启' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- Agent配置 -->
|
||||
<template #agent_config="{ record }">
|
||||
<a-tag :color="record.agent_config ? 'green' : 'gray'">
|
||||
{{ record.agent_config ? '已配置' : '未配置' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- CPU -->
|
||||
<template #cpu="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">CPU</span>
|
||||
<span class="resource-value">{{ record.cpu_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.cpu_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.cpu_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 内存 -->
|
||||
<template #memory="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-laebl">内存</span>
|
||||
<span class="resource-value">{{ record.memory_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.memory_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.memory_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 硬盘 -->
|
||||
<template #disk="{ record }">
|
||||
<div class="resource-display">
|
||||
<div class="resource-info">
|
||||
<span class="resource-label">硬盘</span>
|
||||
<span class="resource-value">{{ record.disk_info?.value || 0 }}%</span>
|
||||
</div>
|
||||
<a-progress
|
||||
:percent="(record.disk_info?.value || 0) / 100"
|
||||
:color="getProgressColor(record.disk_info?.value || 0)"
|
||||
size="small"
|
||||
:show-text="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 数据采集 -->
|
||||
<template #data_collection="{ record }">
|
||||
<a-tag :color="record.data_collection ? 'green' : 'gray'">
|
||||
{{ record.data_collection ? '已启用' : '未启用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-doption>
|
||||
<a-doption @click="handleEdit(record)">
|
||||
<template #icon>
|
||||
<icon-edit />
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #toolbar-left><a-space><a-button type="primary" @click="handleAdd"><template #icon><icon-plus /></template>新增设备</a-button><a-button type="outline" @click="handleRefresh"><template #icon><icon-refresh /></template>刷新状态</a-button></a-space></template>
|
||||
<template #category="{ record }"><a-tag color="arcoblue">{{ formatDeviceCategory(record.category) }}</a-tag></template>
|
||||
<template #host_port="{ record }">{{ record.host || '-' }}:{{ record.port || '-' }}</template>
|
||||
<template #protocol="{ record }"><a-space><a-tag color="purple">{{ String(record.protocol || '-').toUpperCase() }}</a-tag><a-tag v-if="record.protocol === 'snmp'" color="orangered">{{ String(record.snmp_version || '-').toUpperCase() }}</a-tag></a-space></template>
|
||||
<template #interval="{ record }">{{ record.interval || 0 }} 秒</template>
|
||||
<template #enabled="{ record }"><a-tag :color="record.enabled ? 'green' : 'gray'">{{ record.enabled ? '已启用' : '已停用' }}</a-tag></template>
|
||||
<template #status="{ record }"><a-tooltip :content="statusHint(record.status)"><a-tag :color="getStatusColor(record.status)">{{ getStatusText(record.status) }}</a-tag></a-tooltip></template>
|
||||
<template #response_time="{ record }"><span :style="{ color: getResponseTimeColor(record.response_time) }">{{ formatResponseTime(record.response_time) }}</span></template>
|
||||
<template #last_check_time="{ record }">{{ formatTime(record.last_check_time) }}</template>
|
||||
<template #tags="{ record }"><a-tag v-for="(tag, idx) in parseTags(record.tags)" :key="idx" style="margin-right: 4px">{{ tag }}</a-tag><span v-if="!parseTags(record.tags).length">-</span></template>
|
||||
<template #actions="{ record }"><a-space><a-button type="text" size="small" @click="handleDetail(record)">详情</a-button><a-button type="text" size="small" @click="handleEdit(record)">编辑</a-button><a-button type="text" size="small" status="danger" @click="handleDelete(record)">删除</a-button></a-space></template>
|
||||
</search-table>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<FormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
|
||||
<!-- 快捷配置对话框 -->
|
||||
<QuickConfigDialog
|
||||
v-model:visible="quickConfigVisible"
|
||||
:record="currentRecord"
|
||||
@success="handleFormSuccess"
|
||||
/>
|
||||
<FormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||
<a-drawer v-model:visible="detailVisible" :width="860" title="网络设备采集详情" :footer="false" unmount-on-close>
|
||||
<Detail v-if="currentRecord" :record="currentRecord" @edit="handleDetailEdit" @delete="handleDetailDelete" />
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconPlus,
|
||||
IconDown,
|
||||
IconEdit,
|
||||
IconDesktop,
|
||||
IconDelete,
|
||||
IconRefresh,
|
||||
IconEye,
|
||||
IconSettings
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
import SearchTable from '@/components/search-table/index.vue'
|
||||
import { searchFormConfig } from './config/search-form'
|
||||
import FormDialog from './components/FormDialog.vue'
|
||||
import Detail from './components/Detail.vue'
|
||||
import { columns as columnsConfig } from './config/columns'
|
||||
import {
|
||||
fetchServerList,
|
||||
deleteServer,
|
||||
} from '@/api/ops/server'
|
||||
fetchNetworkDeviceList,
|
||||
fetchNetworkDeviceDetail,
|
||||
deleteNetworkDevice,
|
||||
type NetworkDeviceService,
|
||||
type NetworkDeviceQueryParams,
|
||||
} from '@/api/ops/network-device'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Mock 假数据
|
||||
const mockServerData = [
|
||||
{
|
||||
id: 1,
|
||||
unique_id: 'NET-2024-0001',
|
||||
name: '核心交换机 -01',
|
||||
type: '交换机',
|
||||
manu: '华为',
|
||||
model_num: 'CE6857-48S6CQ',
|
||||
location: '数据中心 A-3 层 -24 机柜 -5U 位',
|
||||
tags: '核心,生产',
|
||||
ip: '192.168.1.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:45%,内存:62%',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
unique_id: 'NET-2024-0002',
|
||||
name: '汇聚路由器 -01',
|
||||
type: '路由器',
|
||||
manu: '思科',
|
||||
model_num: 'ISR4451-X/K9',
|
||||
location: '数据中心 A-3 层 -24 机柜 -6U 位',
|
||||
tags: '汇聚,生产',
|
||||
ip: '192.168.1.2',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:78%,内存:85%',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
unique_id: 'NET-2024-0003',
|
||||
name: '接入交换机 -01',
|
||||
type: '交换机',
|
||||
manu: '华三',
|
||||
model_num: 'S5560X-54C-EI',
|
||||
location: '数据中心 B-1 层 -12 机柜 -1U 位',
|
||||
tags: '接入,办公',
|
||||
ip: '192.168.2.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: '-',
|
||||
status: 'offline',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
unique_id: 'NET-2024-0004',
|
||||
name: '防火墙 -01',
|
||||
type: '防火墙',
|
||||
manu: '深信服',
|
||||
model_num: 'AF-2000',
|
||||
location: '数据中心 B-1 层 -12 机柜 -2U 位',
|
||||
tags: '安全,边界',
|
||||
ip: '192.168.1.254',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:35%,内存:68%',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
unique_id: 'NET-2024-0005',
|
||||
name: '负载均衡器 -01',
|
||||
type: '负载均衡',
|
||||
manu: 'F5',
|
||||
model_num: 'BIG-IP i2800',
|
||||
location: '数据中心 C-2 层 -8 机柜 -3U 位',
|
||||
tags: '负载,应用',
|
||||
ip: '192.168.3.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:28%,内存:45%',
|
||||
status: 'maintenance',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
unique_id: 'NET-2024-0006',
|
||||
name: '无线控制器 -01',
|
||||
type: '无线控制器',
|
||||
manu: '锐捷',
|
||||
model_num: 'WS6008',
|
||||
location: '数据中心 A-1 层 -05 机柜 -8U 位',
|
||||
tags: '无线,办公',
|
||||
ip: '192.168.4.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: '-',
|
||||
status: 'retired',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
unique_id: 'NET-2024-0007',
|
||||
name: '光模块交换机 -01',
|
||||
type: '交换机',
|
||||
manu: '华为',
|
||||
model_num: 'CE8850-64CQ-EI',
|
||||
location: '数据中心 A-2 层 -15 机柜 -10U 位',
|
||||
tags: '高速,存储网络',
|
||||
ip: '192.168.5.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:55%,内存:72%',
|
||||
status: 'online',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
unique_id: 'NET-2024-0008',
|
||||
name: '管理交换机 -01',
|
||||
type: '交换机',
|
||||
manu: 'TP-LINK',
|
||||
model_num: 'TL-SG3428',
|
||||
location: '数据中心 B-2 层 -18 机柜 -12U 位',
|
||||
tags: '管理,带外',
|
||||
ip: '192.168.100.1',
|
||||
protocol: 'SNMP',
|
||||
indicator: 'CPU:42%,内存:38%',
|
||||
status: 'online',
|
||||
},
|
||||
]
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const tableData = ref<NetworkDeviceService[]>([])
|
||||
const formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const currentRecord = ref<any>(null)
|
||||
const detailVisible = ref(false)
|
||||
const currentRecord = ref<NetworkDeviceService | null>(null)
|
||||
const formModel = ref({
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
enabled: undefined as boolean | undefined,
|
||||
status: undefined as string | undefined,
|
||||
protocol: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
@@ -322,70 +82,90 @@ const formItems = computed<FormItem[]>(() => searchFormConfig)
|
||||
// 表格列配置
|
||||
const columns = computed(() => columnsConfig)
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
online: 'green',
|
||||
offline: 'red',
|
||||
maintenance: 'orange',
|
||||
retired: 'gray',
|
||||
error: 'orange',
|
||||
unknown: 'gray',
|
||||
}
|
||||
return colorMap[status || ''] || 'gray'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status?: string) => {
|
||||
const textMap: Record<string, string> = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
maintenance: '维护中',
|
||||
retired: '已退役',
|
||||
error: '异常',
|
||||
unknown: '未知',
|
||||
}
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
const getProgressColor = (value: number) => {
|
||||
if (value >= 90) return '#F53F3F' // 红色
|
||||
if (value >= 70) return '#FF7D00' // 橙色
|
||||
if (value >= 50) return '#FFD00B' // 黄色
|
||||
return '#00B42A' // 绿色
|
||||
const statusHint = (status?: string) => {
|
||||
const hintMap: Record<string, string> = {
|
||||
online: '在线:设备采集正常',
|
||||
offline: '离线:采集任务未拿到设备响应',
|
||||
error: '异常:采集发生错误,请检查配置',
|
||||
unknown: '未知:等待首次采集',
|
||||
}
|
||||
return hintMap[status || 'unknown']
|
||||
}
|
||||
|
||||
// 获取网络设备列表(使用 Mock 数据)
|
||||
const fetchServers = async () => {
|
||||
const formatResponseTime = (value?: number) => {
|
||||
if (typeof value !== 'number' || value <= 0) return '-'
|
||||
return `${value.toFixed(2)} ms`
|
||||
}
|
||||
|
||||
const getResponseTimeColor = (value?: number) => {
|
||||
if (!value) return ''
|
||||
if (value >= 1000) return 'rgb(var(--danger-6))'
|
||||
if (value >= 500) return 'rgb(var(--warning-6))'
|
||||
return 'rgb(var(--success-6))'
|
||||
}
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(
|
||||
date.getHours()
|
||||
).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const parseTags = (tags?: string) => {
|
||||
if (!tags) return []
|
||||
return tags.split(',').map((i) => i.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
const formatDeviceCategory = (category?: string) => {
|
||||
const map: Record<string, string> = {
|
||||
router: '路由器',
|
||||
switch: '交换机',
|
||||
firewall: '防火墙',
|
||||
access_point: '接入点',
|
||||
load_balancer: '负载均衡',
|
||||
other: '其他',
|
||||
}
|
||||
if (!category) return '-'
|
||||
return map[category] || category
|
||||
}
|
||||
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 使用 Mock 数据
|
||||
tableData.value = mockServerData
|
||||
pagination.total = mockServerData.length
|
||||
|
||||
// 如果有搜索条件,进行过滤
|
||||
if (formModel.value.keyword || formModel.value.status) {
|
||||
let filteredData = [...mockServerData]
|
||||
|
||||
if (formModel.value.keyword) {
|
||||
const keyword = formModel.value.keyword.toLowerCase()
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.name.toLowerCase().includes(keyword) ||
|
||||
item.unique_id.toLowerCase().includes(keyword) ||
|
||||
item.ip.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
|
||||
if (formModel.value.status) {
|
||||
filteredData = filteredData.filter(item => item.status === formModel.value.status)
|
||||
}
|
||||
|
||||
tableData.value = filteredData
|
||||
pagination.total = filteredData.length
|
||||
}
|
||||
const params: NetworkDeviceQueryParams = { page: pagination.current, size: pagination.pageSize }
|
||||
if (formModel.value.keyword) params.keyword = formModel.value.keyword
|
||||
if (formModel.value.enabled !== undefined) params.enabled = formModel.value.enabled
|
||||
const res: any = await fetchNetworkDeviceList(params)
|
||||
let data: NetworkDeviceService[] = res?.details?.data || []
|
||||
if (formModel.value.status) data = data.filter((item) => item.status === formModel.value.status)
|
||||
if (formModel.value.protocol) data = data.filter((item) => item.protocol === formModel.value.protocol)
|
||||
tableData.value = data
|
||||
pagination.total = formModel.value.status || formModel.value.protocol ? data.length : (res?.details?.total || 0)
|
||||
} catch (error) {
|
||||
console.error('获取网络设备列表失败:', error)
|
||||
Message.error('获取网络设备列表失败')
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
@@ -394,132 +174,94 @@ const fetchServers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchServers()
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 处理表单模型更新
|
||||
const handleFormModelUpdate = (value: any) => {
|
||||
formModel.value = value
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
formModel.value = {
|
||||
keyword: '',
|
||||
datacenter_id: undefined,
|
||||
rack_id: undefined,
|
||||
status: undefined,
|
||||
enabled: undefined,
|
||||
status: undefined as string | undefined,
|
||||
protocol: undefined as string | undefined,
|
||||
}
|
||||
pagination.current = 1
|
||||
fetchServers()
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (current: number) => {
|
||||
pagination.current = current
|
||||
fetchServers()
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const handleRefresh = () => {
|
||||
fetchServers()
|
||||
fetchList()
|
||||
Message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 新增网络设备
|
||||
const handleAdd = () => {
|
||||
currentRecord.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 快捷配置
|
||||
const handleQuickConfig = (record: any) => {
|
||||
currentRecord.value = record
|
||||
quickConfigVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑网络设备
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (record: NetworkDeviceService) => {
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchServers()
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重启网络设备
|
||||
const handleRestart = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '确认重启',
|
||||
content: `确认重启网络设备 ${record.name} 吗?`,
|
||||
onOk: () => {
|
||||
Message.info('正在发送重启指令...')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情 - 在当前窗口打开
|
||||
const handleDetail = (record: any) => {
|
||||
router.push({
|
||||
path: '/dc/detail',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 远程控制 - 在新窗口打开
|
||||
const handleRemoteControl = (record: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/dc/remote',
|
||||
query: {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
ip: record.ip,
|
||||
status: record.status,
|
||||
},
|
||||
}).href
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 删除网络设备
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDetail = async (record: NetworkDeviceService) => {
|
||||
try {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除网络设备 ${record.name} 吗?`,
|
||||
onOk: async () => {
|
||||
// Mock 删除操作
|
||||
const index = mockServerData.findIndex(item => item.id === record.id)
|
||||
if (index > -1) {
|
||||
mockServerData.splice(index, 1)
|
||||
Message.success('删除成功')
|
||||
fetchServers()
|
||||
} else {
|
||||
Message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('删除网络设备失败:', error)
|
||||
const res: any = await fetchNetworkDeviceDetail(record.id)
|
||||
currentRecord.value = (res?.details || record) as NetworkDeviceService
|
||||
detailVisible.value = true
|
||||
} catch {
|
||||
currentRecord.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化加载数据
|
||||
fetchServers()
|
||||
const handleDetailEdit = () => {
|
||||
detailVisible.value = false
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailDelete = () => {
|
||||
detailVisible.value = false
|
||||
if (currentRecord.value) handleDelete(currentRecord.value)
|
||||
}
|
||||
|
||||
const handleDelete = (record: NetworkDeviceService) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确认删除设备「${record.name}」吗?删除为软删除,建议先停用后再执行。`,
|
||||
okButtonProps: { status: 'danger' },
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteNetworkDevice(record.id)
|
||||
Message.success('删除成功')
|
||||
fetchList()
|
||||
} catch (error: any) {
|
||||
Message.error(error?.response?.data?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(fetchList)
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'DataCenterServer',
|
||||
name: 'NetworkDeviceCollectionManagement',
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -527,38 +269,4 @@ export default {
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.resource-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0;
|
||||
|
||||
.resource-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
.resource-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgb(var(--text-1));
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-progress) {
|
||||
margin: 0;
|
||||
|
||||
.arco-progress-bar-bg {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.arco-progress-bar {
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -160,7 +160,10 @@ const handleViewMetrics = async () => {
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -310,7 +310,10 @@ const loadServerOptions = async () => {
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -578,6 +578,8 @@ const formatTime = (time?: string | Date) => {
|
||||
if (!time) return '-'
|
||||
try {
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
|
||||
@@ -100,7 +100,7 @@ const getStatusText = (status?: string) => {
|
||||
const formatTime = (time?: string | null) => {
|
||||
if (!time || time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1) return '-'
|
||||
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -159,7 +159,7 @@ const getStatusText = (status?: string) => {
|
||||
const formatTime = (time?: string | null) => {
|
||||
if (!time || time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1) return '-'
|
||||
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1970) return '-'
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
@@ -1,218 +1,108 @@
|
||||
<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>
|
||||
<div class="stats-title">受管设备</div>
|
||||
<div class="stats-value">{{ overview.total_devices ?? 0 }}</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>
|
||||
<div class="stats-title">在线设备</div>
|
||||
<div class="stats-value text-success">{{ overview.online_devices ?? 0 }}</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>
|
||||
<div class="stats-title">离线/异常</div>
|
||||
<div class="stats-value text-danger">{{ overview.abnormal_devices ?? 0 }}</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 class="stats-title">平均响应时延</div>
|
||||
<div class="stats-value">{{ Number(overview.avg_response_time_ms || 0).toFixed(1) }} ms</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" class="section-row">
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="设备健康矩阵" :bordered="false">
|
||||
<div class="matrix-list">
|
||||
<div v-for="item in healthMatrix" :key="`${item.error_bucket}-${item.latency_bucket}`" class="matrix-item">
|
||||
<span>{{ item.error_bucket }} / {{ item.latency_bucket }}</span>
|
||||
<a-tag>{{ item.count }}</a-tag>
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">告警设备</div>
|
||||
<div class="stats-value">2</div>
|
||||
<div class="stats-desc text-danger">需要关注</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="采集新鲜度分布" :bordered="false">
|
||||
<div class="simple-list">
|
||||
<div v-for="item in freshness" :key="item.bucket_label" class="simple-item">
|
||||
<span>{{ item.bucket_label }}</span>
|
||||
<b>{{ item.count }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :xs="24" :lg="8">
|
||||
<a-card title="协议/版本分布" :bordered="false">
|
||||
<div class="simple-list">
|
||||
<div v-for="item in protocols" :key="`${item.protocol}-${item.snmp_version}`" class="simple-item">
|
||||
<span>{{ item.protocol || '-' }} / {{ item.snmp_version || '-' }}</span>
|
||||
<b>{{ item.count }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<a-row :gutter="16" class="chart-row">
|
||||
<a-row :gutter="16" class="section-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 title="资源概览" :bordered="false">
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="接口总量">{{ resource.interface_summary?.total || 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item label="接口Up/Down">
|
||||
{{ resource.interface_summary?.up || 0 }} / {{ resource.interface_summary?.down || 0 }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="ARP总量">{{ resource.arp_summary?.total || 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item label="ARP去重IP">{{ resource.arp_summary?.unique_ip_count || 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item label="路由总量">{{ resource.route_summary?.total || 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item label="FIB总量">{{ resource.fib_summary?.total || 0 }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</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)"
|
||||
/>
|
||||
<a-card title="异常事件带" :bordered="false">
|
||||
<div class="simple-list">
|
||||
<div v-for="item in alerts.top_continuous_errors || []" :key="item.id" class="simple-item">
|
||||
<span>{{ item.name }} ({{ item.host }})</span>
|
||||
<a-tag color="red">连续错误 {{ item.continuous_errors }}</a-tag>
|
||||
</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">
|
||||
<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>
|
||||
<a-space>
|
||||
<a-input-search v-model="filters.keyword" placeholder="设备名/IP/型号" style="width: 220px" @search="fetchDevices" />
|
||||
<a-select v-model="filters.status" placeholder="状态" allow-clear style="width: 120px" @change="fetchDevices">
|
||||
<a-option value="online">online</a-option>
|
||||
<a-option value="offline">offline</a-option>
|
||||
<a-option value="error">error</a-option>
|
||||
<a-option value="unknown">unknown</a-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="fetchAll">刷新</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-table
|
||||
:data="filteredDevices"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="name"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<a-table :data="devices" :columns="columns" :loading="loading" row-key="id" :pagination="pagination">
|
||||
<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>
|
||||
<a-tag :color="statusColor(record.status)">{{ record.status }}</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -220,259 +110,122 @@
|
||||
</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 { onMounted, onBeforeUnmount, reactive, ref } from 'vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchNetworkScreenAlertsStream,
|
||||
fetchNetworkScreenDevices,
|
||||
fetchNetworkScreenFreshnessDistribution,
|
||||
fetchNetworkScreenHealthMatrix,
|
||||
fetchNetworkScreenOverview,
|
||||
fetchNetworkScreenProtocolDistribution,
|
||||
fetchNetworkScreenResourceOverview,
|
||||
} from '@/api/ops/network-screen'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const selectedType = ref('')
|
||||
const overview = reactive<Record<string, any>>({})
|
||||
const healthMatrix = ref<any[]>([])
|
||||
const freshness = ref<any[]>([])
|
||||
const protocols = ref<any[]>([])
|
||||
const resource = reactive<Record<string, any>>({})
|
||||
const alerts = reactive<Record<string, any>>({})
|
||||
const devices = ref<any[]>([])
|
||||
const pollTimer = ref<number | null>(null)
|
||||
|
||||
// 统计数据
|
||||
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 filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
onChange: (page: number) => {
|
||||
pagination.current = page
|
||||
fetchDevices()
|
||||
},
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
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,
|
||||
},
|
||||
{ title: '设备', dataIndex: 'name' },
|
||||
{ title: '管理IP', dataIndex: 'host' },
|
||||
{ title: '协议', dataIndex: 'protocol' },
|
||||
{ title: '厂商', dataIndex: 'vendor' },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status' },
|
||||
{ title: '响应时延(ms)', dataIndex: 'response_time' },
|
||||
{ title: '最后检查时间', dataIndex: 'last_check_time' },
|
||||
{ title: '连续错误', dataIndex: 'continuous_errors' },
|
||||
]
|
||||
|
||||
// 设备数据
|
||||
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 statusColor = (status: string) => {
|
||||
if (status === 'online') return 'green'
|
||||
if (status === 'offline') return 'red'
|
||||
if (status === 'error') return 'orangered'
|
||||
return 'orange'
|
||||
}
|
||||
|
||||
// 过滤后的设备列表
|
||||
const filteredDevices = computed(() => {
|
||||
if (!selectedType.value) {
|
||||
return networkDevices.value
|
||||
const fetchDevices = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await fetchNetworkScreenDevices({
|
||||
page: pagination.current,
|
||||
size: pagination.pageSize,
|
||||
keyword: filters.keyword || undefined,
|
||||
status: filters.status || undefined,
|
||||
})
|
||||
devices.value = res?.details?.data || []
|
||||
pagination.total = res?.details?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
return networkDevices.value.filter((device) => device.type === selectedType.value)
|
||||
}
|
||||
|
||||
const fetchAll = async () => {
|
||||
try {
|
||||
const [a, b, c, d, e, f] = await Promise.all([
|
||||
fetchNetworkScreenOverview(),
|
||||
fetchNetworkScreenHealthMatrix({ window: '15m' }),
|
||||
fetchNetworkScreenFreshnessDistribution(),
|
||||
fetchNetworkScreenProtocolDistribution(),
|
||||
fetchNetworkScreenResourceOverview({ window: '1h' }),
|
||||
fetchNetworkScreenAlertsStream({ limit: 10 }),
|
||||
])
|
||||
Object.assign(overview, a?.details || {})
|
||||
healthMatrix.value = b?.details?.data || []
|
||||
freshness.value = c?.details?.data || []
|
||||
protocols.value = d?.details?.data || []
|
||||
Object.assign(resource, e?.details || {})
|
||||
Object.assign(alerts, f?.details || {})
|
||||
await fetchDevices()
|
||||
} catch (e) {
|
||||
Message.error('网络设备大屏数据加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAll()
|
||||
pollTimer.value = window.setInterval(fetchAll, 30000)
|
||||
})
|
||||
|
||||
// 流量趋势图表配置
|
||||
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()
|
||||
onBeforeUnmount(() => {
|
||||
if (pollTimer.value) window.clearInterval(pollTimer.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'NetworkMonitor',
|
||||
name: 'NetworkMonitorScreen',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
min-height: calc(100vh - 120px);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
@@ -480,230 +233,30 @@ export default {
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
.stats-title { color: var(--color-text-2); margin-bottom: 8px; }
|
||||
.stats-value { font-size: 28px; font-weight: 700; }
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
.section-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 280px;
|
||||
.matrix-list,
|
||||
.simple-list {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.matrix-item,
|
||||
.simple-item {
|
||||
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;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: rgb(var(--success-6));
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
.text-success { color: rgb(var(--success-6)); }
|
||||
</style>
|
||||
@@ -353,7 +353,11 @@ const formatThroughput = (val: number | null) => {
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString('zh-CN')
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const date = new Date(time)
|
||||
if (Number.isNaN(date.getTime())) return '-'
|
||||
if (date.getFullYear() <= 1970) return '-'
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
|
||||
@@ -327,7 +327,11 @@ const getTypeColor = (type?: string) => {
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time?: string) => {
|
||||
return time ? dayjs(time).format('YYYY-MM-DD HH:mm') : '-'
|
||||
if (!time) return '-'
|
||||
if (time.startsWith('0001-01-01')) return '-'
|
||||
const d = dayjs(time)
|
||||
if (!d.isValid() || d.year() <= 1970) return '-'
|
||||
return d.format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user