fix
This commit is contained in:
@@ -18,7 +18,15 @@ export interface RoomDeviceItem {
|
||||
collect_method: 'api' | 'snmp'
|
||||
snmp_target: string
|
||||
snmp_port: number
|
||||
snmp_version: 'v2c' | 'v3'
|
||||
snmp_community: string
|
||||
snmp_v3_security_level: string
|
||||
snmp_v3_security_name: string
|
||||
snmp_v3_auth_protocol: string
|
||||
snmp_v3_auth_password: string
|
||||
snmp_v3_priv_protocol: string
|
||||
snmp_v3_priv_password: string
|
||||
snmp_v3_context_name: string
|
||||
snmp_timeout_ms: number
|
||||
snmp_retries: number
|
||||
snmp_oids: string
|
||||
@@ -67,7 +75,15 @@ export interface RoomDeviceCreateData {
|
||||
collect_method?: 'api' | 'snmp'
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
@@ -87,7 +103,15 @@ export interface RoomDeviceUpdateData {
|
||||
collect_method?: 'api' | 'snmp'
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
@@ -103,7 +127,15 @@ export interface RoomDeviceCollectData {
|
||||
agent_config?: string
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
|
||||
@@ -22,7 +22,15 @@ export interface SecurityServiceItem {
|
||||
collect_method: 'api' | 'snmp'
|
||||
snmp_target: string
|
||||
snmp_port: number
|
||||
snmp_version: 'v2c' | 'v3'
|
||||
snmp_community: string
|
||||
snmp_v3_security_level: string
|
||||
snmp_v3_security_name: string
|
||||
snmp_v3_auth_protocol: string
|
||||
snmp_v3_auth_password: string
|
||||
snmp_v3_priv_protocol: string
|
||||
snmp_v3_priv_password: string
|
||||
snmp_v3_context_name: string
|
||||
snmp_timeout_ms: number
|
||||
snmp_retries: number
|
||||
snmp_oids: string
|
||||
@@ -75,7 +83,15 @@ export interface SecurityServiceFormData {
|
||||
collect_method?: 'api' | 'snmp'
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
@@ -138,7 +154,15 @@ export interface SecurityServicePatchData {
|
||||
agent_config?: string
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
|
||||
@@ -22,7 +22,15 @@ export interface StorageItem {
|
||||
collect_method: 'api' | 'snmp'
|
||||
snmp_target: string
|
||||
snmp_port: number
|
||||
snmp_version: 'v2c' | 'v3'
|
||||
snmp_community: string
|
||||
snmp_v3_security_level: string
|
||||
snmp_v3_security_name: string
|
||||
snmp_v3_auth_protocol: string
|
||||
snmp_v3_auth_password: string
|
||||
snmp_v3_priv_protocol: string
|
||||
snmp_v3_priv_password: string
|
||||
snmp_v3_context_name: string
|
||||
snmp_timeout_ms: number
|
||||
snmp_retries: number
|
||||
snmp_oids: string
|
||||
@@ -75,7 +83,15 @@ export interface StorageCreateData {
|
||||
collect_method?: 'api' | 'snmp'
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
@@ -103,7 +119,15 @@ export interface StorageUpdateData {
|
||||
collect_method?: 'api' | 'snmp'
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
@@ -316,7 +340,15 @@ export interface StoragePatchData {
|
||||
agent_config?: string
|
||||
snmp_target?: string
|
||||
snmp_port?: number
|
||||
snmp_version?: 'v2c' | 'v3'
|
||||
snmp_community?: string
|
||||
snmp_v3_security_level?: string
|
||||
snmp_v3_security_name?: string
|
||||
snmp_v3_auth_protocol?: string
|
||||
snmp_v3_auth_password?: string
|
||||
snmp_v3_priv_protocol?: string
|
||||
snmp_v3_priv_password?: string
|
||||
snmp_v3_context_name?: string
|
||||
snmp_timeout_ms?: number
|
||||
snmp_retries?: number
|
||||
snmp_oids?: string
|
||||
|
||||
21
src/api/ops/traffic-runtime.ts
Normal file
21
src/api/ops/traffic-runtime.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
export interface TrafficRuntimeStatus {
|
||||
status?: 'running' | 'stopped' | string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export const fetchTrafficRuntimeStatus = () =>
|
||||
request.get<{ code: number; details?: TrafficRuntimeStatus; message?: string }>(
|
||||
'/dc-network/v1/api/traffic/status',
|
||||
)
|
||||
|
||||
export const startTrafficRuntime = () =>
|
||||
request.post<{ code: number; details?: TrafficRuntimeStatus; message?: string }>(
|
||||
'/dc-network/v1/api/traffic/start',
|
||||
)
|
||||
|
||||
export const stopTrafficRuntime = () =>
|
||||
request.post<{ code: number; details?: TrafficRuntimeStatus; message?: string }>(
|
||||
'/dc-network/v1/api/traffic/stop',
|
||||
)
|
||||
120
src/api/ops/traffic.ts
Normal file
120
src/api/ops/traffic.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { request } from '@/api/request'
|
||||
|
||||
export interface TrafficListItem {
|
||||
id: number
|
||||
topology_id: number
|
||||
link_id: number
|
||||
node_id: string
|
||||
time_granularity: string
|
||||
time_point: string
|
||||
in_bytes: number
|
||||
out_bytes: number
|
||||
total_bytes: number
|
||||
in_packets: number
|
||||
out_packets: number
|
||||
total_packets: number
|
||||
connections: number
|
||||
avg_latency: number
|
||||
packet_loss: number
|
||||
bandwidth: number
|
||||
bandwidth_peak: number
|
||||
protocol_stats?: string
|
||||
top_sources?: string
|
||||
}
|
||||
|
||||
export interface TrafficListResponse {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
data: TrafficListItem[]
|
||||
}
|
||||
|
||||
export interface TrafficSummary {
|
||||
total_in_bytes: number
|
||||
total_out_bytes: number
|
||||
total_bytes: number
|
||||
avg_bandwidth: number
|
||||
peak_bandwidth: number
|
||||
total_connections: number
|
||||
avg_packet_loss: number
|
||||
}
|
||||
|
||||
export interface TrafficTrendPoint {
|
||||
time: string
|
||||
in_bytes: number
|
||||
out_bytes: number
|
||||
total_bytes: number
|
||||
}
|
||||
|
||||
export interface TrafficTopItem {
|
||||
node_id: string
|
||||
total_in_bytes: number
|
||||
total_out_bytes: number
|
||||
total_bytes: number
|
||||
total_packets: number
|
||||
}
|
||||
|
||||
export interface TrafficDashboardResponse {
|
||||
direction: {
|
||||
inbound_percent: number
|
||||
outbound_percent: number
|
||||
}
|
||||
protocols: Array<{
|
||||
name: string
|
||||
percent: number
|
||||
}>
|
||||
top_applications: Array<{
|
||||
name: string
|
||||
in_bytes: number
|
||||
out_bytes: number
|
||||
total_bytes: number
|
||||
share_percent: number
|
||||
sessions: number
|
||||
port: string
|
||||
}>
|
||||
top_sources: Array<{
|
||||
ip: string
|
||||
name: string
|
||||
total_bytes: number
|
||||
total_packets: number
|
||||
}>
|
||||
bandwidth: Array<{
|
||||
name: string
|
||||
percent: number
|
||||
}>
|
||||
}
|
||||
|
||||
export const fetchTrafficList = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: TrafficListResponse; message?: string }>('/DC-Control/v1/traffic', {
|
||||
params,
|
||||
})
|
||||
|
||||
export const fetchTrafficSummary = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: TrafficSummary; message?: string }>(
|
||||
'/DC-Control/v1/traffic/summary',
|
||||
{ params },
|
||||
)
|
||||
|
||||
export const fetchTrafficTrend = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: { data?: TrafficTrendPoint[] }; message?: string }>(
|
||||
'/DC-Control/v1/traffic/trend',
|
||||
{ params },
|
||||
)
|
||||
|
||||
export const fetchTopTraffic = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: { data?: TrafficTopItem[] }; message?: string }>(
|
||||
'/DC-Control/v1/traffic/top',
|
||||
{ params },
|
||||
)
|
||||
|
||||
export const fetchRealtimeTraffic = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: { bandwidth?: number; connections?: number }; message?: string }>(
|
||||
'/DC-Control/v1/traffic/realtime',
|
||||
{ params },
|
||||
)
|
||||
|
||||
export const fetchTrafficDashboard = (params?: Record<string, any>) =>
|
||||
request.get<{ code: number; details?: TrafficDashboardResponse; message?: string }>(
|
||||
'/DC-Control/v1/traffic/dashboard',
|
||||
{ params },
|
||||
)
|
||||
@@ -1,139 +1,123 @@
|
||||
<template>
|
||||
<a-modal
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
title="数据库服务详情"
|
||||
title="数据库采集详情"
|
||||
placement="right"
|
||||
:width="860"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleUpdateVisible"
|
||||
:footer="false"
|
||||
width="700px"
|
||||
>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions
|
||||
:column="2"
|
||||
bordered
|
||||
size="medium"
|
||||
:label-style="{ width: '140px' }"
|
||||
>
|
||||
<a-descriptions-item label="服务ID">
|
||||
{{ detailData?.id }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务唯一标识">
|
||||
{{ detailData?.service_identity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务名称">
|
||||
{{ detailData?.name || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="数据库类型">
|
||||
<a-tag :color="getTypeColor(detailData?.type)">
|
||||
{{ detailData?.type || '-' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="主机地址">
|
||||
{{ detailData?.host || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="端口">
|
||||
{{ detailData?.port || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户名">
|
||||
{{ detailData?.username || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="数据库名">
|
||||
{{ detailData?.database || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器标识">
|
||||
{{ detailData?.server_identity || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="分类">
|
||||
{{ detailData?.category || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="启用监控">
|
||||
<a-tag :color="detailData?.enabled ? 'green' : 'red'">
|
||||
{{ detailData?.enabled ? '已启用' : '已禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">
|
||||
{{ detailData?.interval ? `${detailData.interval}秒` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="进程内采集">
|
||||
<a-tag :color="detailData?.collect_on ? 'green' : 'red'">
|
||||
{{ detailData?.collect_on ? '已启用' : '已禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">
|
||||
{{ detailData?.collect_interval ? `${detailData.collect_interval}秒` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行状态" :span="2">
|
||||
<a-tag :color="getStatusColor(detailData?.status)">
|
||||
{{ getStatusText(detailData?.status) }}
|
||||
</a-tag>
|
||||
<span v-if="detailData?.status_message" style="margin-left: 8px; color: rgb(var(--text-3))">
|
||||
{{ detailData.status_message }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态码">
|
||||
{{ detailData?.status_code || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="响应时间">
|
||||
{{ detailData?.response_time ? `${detailData.response_time.toFixed(2)}ms` : '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="连续错误次数">
|
||||
<span :style="{ color: (detailData?.continuous_errors ?? 0) > 0 ? 'rgb(var(--danger-6))' : '' }">
|
||||
{{ detailData?.continuous_errors ?? 0 }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="运行时长">
|
||||
{{ formatUptime(detailData?.uptime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后检查时间">
|
||||
{{ formatTime(detailData?.last_check_time) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后在线时间">
|
||||
{{ formatTime(detailData?.last_online_time) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最后离线时间">
|
||||
{{ formatTime(detailData?.last_offline_time) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ formatTime(detailData?.created_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">
|
||||
{{ formatTime(detailData?.updated_at) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="标签" :span="2">
|
||||
<a-tag v-for="(tag, index) in parseTags(detailData?.tags)" :key="index" style="margin-right: 4px">
|
||||
{{ tag }}
|
||||
</a-tag>
|
||||
<span v-if="!parseTags(detailData?.tags).length">-</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">
|
||||
{{ detailData?.description || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集参数" :span="2">
|
||||
{{ detailData?.collect_args || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="最近采集结果" :span="2">
|
||||
<div v-if="detailData?.collect_last_result" class="collect-result">
|
||||
{{ detailData.collect_last_result }}
|
||||
<div v-if="detailData" class="database-detail">
|
||||
<div class="detail-header-card">
|
||||
<div class="header-left">
|
||||
<div class="service-icon">{{ getTypeAbbr(detailData.type) }}</div>
|
||||
<div class="service-info">
|
||||
<div class="service-title">
|
||||
<h2>{{ detailData.name || '数据库服务' }}</h2>
|
||||
<div class="status-tags">
|
||||
<a-tag :color="getStatusColor(detailData.status)" size="large">{{ getStatusText(detailData.status) }}</a-tag>
|
||||
<a-tag :color="detailData.enabled ? 'green' : 'gray'" size="large">{{ detailData.enabled ? '已启用' : '已禁用' }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-meta">
|
||||
<span class="meta-item">标识: {{ detailData.service_identity || '-' }}</span>
|
||||
<span class="meta-item">类型: <a-tag :color="getTypeColor(detailData.type)" size="small">{{ detailData.type || '-' }}</a-tag></span>
|
||||
<span class="meta-item">地址: {{ detailData.host || '-' }}:{{ detailData.port || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="额外配置" :span="2">
|
||||
<div v-if="detailData?.extra" class="extra-config">
|
||||
<a-button size="small" @click="showExtraConfig = !showExtraConfig">
|
||||
{{ showExtraConfig ? '隐藏' : '展开' }} JSON
|
||||
</a-button>
|
||||
<pre v-if="showExtraConfig" class="json-display">{{ formatJson(detailData?.extra) }}</pre>
|
||||
<div class="header-actions">
|
||||
<a-button size="small" @click="emit('edit', detailData)"><template #icon><icon-edit /></template>编辑</a-button>
|
||||
<a-button size="small" @click="emit('collect', detailData)"><template #icon><icon-sync /></template>触发采集</a-button>
|
||||
<a-button size="small" status="danger" @click="emit('delete', detailData)"><template #icon><icon-delete /></template>删除</a-button>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">响应时间</div>
|
||||
<div class="metric-value">{{ detailData.response_time ? detailData.response_time.toFixed(2) : '-' }}<span class="unit">ms</span></div>
|
||||
<div class="metric-footer">最后检查: {{ formatTime(detailData.last_check_time) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">状态码</div>
|
||||
<div class="metric-value">{{ formatStatusCode(detailData.status_code) }}</div>
|
||||
<div class="metric-footer">{{ detailData.status_message || '-' }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">连续错误</div>
|
||||
<div class="metric-value">{{ detailData.continuous_errors ?? 0 }}<span class="unit">次</span></div>
|
||||
<div class="metric-footer">运行时长: {{ formatUptime(detailData.uptime) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-title">采集状态</div>
|
||||
<div class="metric-value">{{ detailData.collect_on ? '运行中' : '已停用' }}</div>
|
||||
<div class="metric-footer">间隔: {{ detailData.collect_interval || detailData.interval || 60 }} 秒</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tabs v-model:active-tab="activeTab" class="detail-tabs">
|
||||
<a-tab-pane key="basic" title="基本信息">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="服务ID">{{ detailData.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务名称">{{ detailData.name || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="数据库">{{ detailData.database || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户名">{{ detailData.username || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务器标识">{{ detailData.server_identity || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="分类">{{ detailData.category || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签" :span="2">
|
||||
<a-tag v-for="(tag, index) in parseTags(detailData.tags)" :key="index" style="margin-right: 4px">{{ tag }}</a-tag>
|
||||
<span v-if="!parseTags(detailData.tags).length">-</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">{{ detailData.description || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="collect" title="采集配置">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="启用监控">
|
||||
<a-tag :color="detailData.enabled ? 'green' : 'red'">{{ detailData.enabled ? '已启用' : '已禁用' }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="检查间隔">{{ detailData.interval ? `${detailData.interval}秒` : '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="进程内采集">
|
||||
<a-tag :color="detailData.collect_on ? 'green' : 'red'">{{ detailData.collect_on ? '已启用' : '已禁用' }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ detailData.collect_interval ? `${detailData.collect_interval}秒` : '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="采集参数" :span="2">{{ detailData.collect_args || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最近采集结果" :span="2">{{ detailData.collect_last_result || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="额外配置" :span="2">
|
||||
<pre class="json-display">{{ formatJson(detailData.extra) || '-' }}</pre>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="status" title="运行状态">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="运行状态">
|
||||
<a-tag :color="getStatusColor(detailData.status)">{{ getStatusText(detailData.status) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态码">{{ formatStatusCode(detailData.status_code) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态消息" :span="2">{{ detailData.status_message || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最后检查时间">{{ formatTime(detailData.last_check_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最后在线时间">{{ formatTime(detailData.last_online_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="最后离线时间">{{ formatTime(detailData.last_offline_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(detailData.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(detailData.updated_at) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { IconEdit, IconDelete, IconSync } from '@arco-design/web-vue/es/icon'
|
||||
import { fetchDatabaseDetail, type DatabaseService } from '@/api/ops/database'
|
||||
|
||||
interface Props {
|
||||
@@ -145,18 +129,18 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
recordId: 0,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
const emit = defineEmits(['update:visible', 'edit', 'delete', 'collect'])
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<DatabaseService | null>(null)
|
||||
const showExtraConfig = ref(false)
|
||||
const activeTab = ref('basic')
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.recordId) {
|
||||
fetchDetail()
|
||||
showExtraConfig.value = false
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -164,8 +148,8 @@ watch(
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDatabaseDetail(props.recordId)
|
||||
detailData.value = res
|
||||
const res: any = await fetchDatabaseDetail(props.recordId)
|
||||
detailData.value = res?.details || null
|
||||
} catch (error) {
|
||||
console.error('获取详情失败:', error)
|
||||
Message.error('获取详情失败')
|
||||
@@ -196,6 +180,18 @@ const getTypeColor = (type?: string) => {
|
||||
return colorMap[type || ''] || 'gray'
|
||||
}
|
||||
|
||||
const getTypeAbbr = (type?: string) => {
|
||||
const abbrMap: Record<string, string> = {
|
||||
MySQL: 'MY',
|
||||
PostgreSQL: 'PG',
|
||||
Redis: 'RD',
|
||||
MongoDB: 'MG',
|
||||
DM: 'DM',
|
||||
KingBase: 'KB',
|
||||
}
|
||||
return abbrMap[type || ''] || 'DB'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
@@ -216,6 +212,11 @@ const getStatusText = (status?: string) => {
|
||||
return textMap[status || ''] || '-'
|
||||
}
|
||||
|
||||
const formatStatusCode = (code?: number | null) => {
|
||||
if (code === null || code === undefined) return '-'
|
||||
return code
|
||||
}
|
||||
|
||||
// 格式化运行时长
|
||||
const formatUptime = (uptime?: number) => {
|
||||
if (!uptime) return '-'
|
||||
@@ -265,24 +266,124 @@ const formatJson = (json?: string) => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.collect-result {
|
||||
max-height: 100px;
|
||||
.database-detail {
|
||||
padding: 16px;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
.detail-header-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #165dff, #4080ff);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.service-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.service-meta {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
color: #86909c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.metric-footer {
|
||||
margin-top: 8px;
|
||||
color: #86909c;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background: rgb(var(--gray-2));
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.extra-config {
|
||||
.json-display {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: rgb(var(--gray-2));
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -103,20 +103,32 @@
|
||||
<!-- 操作栏 -->
|
||||
<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-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>
|
||||
</search-table>
|
||||
@@ -132,6 +144,9 @@
|
||||
<DetailDialog
|
||||
v-model:visible="detailDialogVisible"
|
||||
:record-id="currentRecordId"
|
||||
@edit="handleDetailEdit"
|
||||
@delete="handleDetailDelete"
|
||||
@collect="handleDetailCollect"
|
||||
/>
|
||||
|
||||
<!-- 指标采集对话框 -->
|
||||
@@ -149,6 +164,7 @@ import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconPlus,
|
||||
IconSync,
|
||||
IconDown,
|
||||
IconEye,
|
||||
IconEdit,
|
||||
IconDelete,
|
||||
@@ -360,6 +376,7 @@ const handleAdd = () => {
|
||||
|
||||
// 查看详情
|
||||
const handleDetail = (record: DatabaseService) => {
|
||||
currentRecord.value = record
|
||||
currentRecordId.value = record.id
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
@@ -375,6 +392,22 @@ const handleCollect = () => {
|
||||
collectDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailEdit = (record: DatabaseService) => {
|
||||
detailDialogVisible.value = false
|
||||
currentRecord.value = record
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailDelete = (record: DatabaseService) => {
|
||||
detailDialogVisible.value = false
|
||||
handleDelete(record)
|
||||
}
|
||||
|
||||
const handleDetailCollect = () => {
|
||||
detailDialogVisible.value = false
|
||||
collectDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 表单提交成功
|
||||
const handleFormSuccess = () => {
|
||||
fetchList()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="detail-container">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions title="基础信息" :column="2" bordered>
|
||||
<a-descriptions-item label="ID">{{ record.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务标识">{{ record.service_identity }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务名称">{{ record.name }}</a-descriptions-item>
|
||||
@@ -9,7 +9,11 @@
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="机房ID">{{ record.room_id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions title="采集配置" :column="2" bordered class="section-block">
|
||||
<a-descriptions-item label="采集方式">
|
||||
<a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
|
||||
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
|
||||
@@ -37,12 +41,11 @@
|
||||
{{ record.collect_on ? '已启用' : '未启用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="采集间隔">{{ record.collect_interval }}秒</a-descriptions-item>
|
||||
<a-descriptions-item label="采集结果摘要" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
|
||||
<a-descriptions title="运行状态" :column="2" bordered class="section-block">
|
||||
<a-descriptions-item label="采集结果摘要" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="action-bar">
|
||||
@@ -66,19 +69,23 @@
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-drawer v-model:visible="metricsVisible" :width="800" title="最新指标数据" :footer="false" unmount-on-close>
|
||||
<a-drawer v-model:visible="metricsVisible" :width="860" title="最新指标数据" :footer="false" unmount-on-close>
|
||||
<a-spin :loading="metricsLoading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered style="margin-bottom: 12px">
|
||||
<a-descriptions-item label="数据时间">{{ formatTime(metricsLatestTimestamp) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="指标数量">{{ metricsCount }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div v-if="metricsData.length > 0">
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="(item, index) in metricsData" :key="index">
|
||||
<a-descriptions :column="3" size="small">
|
||||
<a-descriptions-item label="指标名称">{{ item.metric_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="指标值">{{ item.metric_value }} {{ item.metric_unit || '' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="类型">{{ item.type || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="时间">{{ formatTime(item.timestamp) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<div class="metrics-grid">
|
||||
<div v-for="(item, index) in metricsData" :key="index" class="metric-item">
|
||||
<div class="metric-name">{{ item.metric_name }}</div>
|
||||
<div class="metric-value">{{ item.metric_value }} {{ item.metric_unit || '' }}</div>
|
||||
<div class="metric-meta">
|
||||
<span>类型: {{ item.type || '-' }}</span>
|
||||
<span>时间: {{ formatTime(item.timestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="暂无指标数据" />
|
||||
</a-spin>
|
||||
@@ -102,12 +109,16 @@ defineEmits(['edit', 'quick-config', 'delete'])
|
||||
const metricsVisible = ref(false)
|
||||
const metricsLoading = ref(false)
|
||||
const metricsData = ref<MetricItem[]>([])
|
||||
const metricsLatestTimestamp = ref<string | undefined>('')
|
||||
const metricsCount = ref(0)
|
||||
|
||||
const handleViewMetrics = async () => {
|
||||
metricsVisible.value = true
|
||||
metricsLoading.value = true
|
||||
try {
|
||||
const response = await fetchLatestMetrics(props.record.service_identity)
|
||||
metricsLatestTimestamp.value = response?.details?.latest_timestamp || ''
|
||||
metricsCount.value = response?.details?.count || 0
|
||||
const metrics =
|
||||
response?.code === 0 && response.details?.metrics && Array.isArray(response.details.metrics)
|
||||
? response.details.metrics
|
||||
@@ -117,6 +128,8 @@ const handleViewMetrics = async () => {
|
||||
console.error('获取最新指标失败:', error)
|
||||
Message.error('获取最新指标失败')
|
||||
metricsData.value = []
|
||||
metricsLatestTimestamp.value = ''
|
||||
metricsCount.value = 0
|
||||
} finally {
|
||||
metricsLoading.value = false
|
||||
}
|
||||
@@ -143,9 +156,55 @@ const formatTime = (time?: string) => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
border-radius: 4px;
|
||||
background: var(--color-fill-1);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--color-text-1);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.metric-meta {
|
||||
margin-top: 6px;
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -68,12 +68,32 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_version" label="SNMP版本">
|
||||
<a-radio-group v-model="formData.snmp_version" type="button">
|
||||
<a-radio value="v2c">v2c</a-radio>
|
||||
<a-radio value="v3">v3</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-col v-if="formData.snmp_version !== 'v3'" :span="12">
|
||||
<a-form-item field="snmp_community" label="Community">
|
||||
<a-input v-model="formData.snmp_community" placeholder="SNMP v2c community" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-else :span="12">
|
||||
<a-form-item field="snmp_v3_security_name" label="Security Name">
|
||||
<a-input v-model="formData.snmp_v3_security_name" placeholder="SNMP v3 用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="formData.snmp_version === 'v3'" :span="12">
|
||||
<a-form-item field="snmp_v3_security_level" label="Security Level">
|
||||
<a-select v-model="formData.snmp_v3_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-col :span="6">
|
||||
<a-form-item field="snmp_timeout_ms" label="超时(ms)">
|
||||
<a-input-number v-model="formData.snmp_timeout_ms" :min="1" :max="60000" style="width: 100%" />
|
||||
@@ -85,6 +105,41 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_auth_protocol" label="Auth协议">
|
||||
<a-select v-model="formData.snmp_v3_auth_protocol">
|
||||
<a-option value="SHA">SHA</a-option>
|
||||
<a-option value="MD5">MD5</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-form-item field="snmp_v3_auth_password" label="Auth密码">
|
||||
<a-input-password v-model="formData.snmp_v3_auth_password" placeholder="authNoPriv/authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3' && formData.snmp_v3_security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_protocol" label="Priv协议">
|
||||
<a-select v-model="formData.snmp_v3_priv_protocol">
|
||||
<a-option value="AES">AES</a-option>
|
||||
<a-option value="DES">DES</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_password" label="Priv密码">
|
||||
<a-input-password v-model="formData.snmp_v3_priv_password" placeholder="authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_context_name" label="Context">
|
||||
<a-input v-model="formData.snmp_v3_context_name" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_oids" label="SNMP OID配置(JSON数组)">
|
||||
<a-textarea
|
||||
v-model="formData.snmp_oids"
|
||||
@@ -167,7 +222,15 @@ const formData = reactive({
|
||||
collect_method: 'api' as 'api' | 'snmp',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c' as 'v2c' | 'v3',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -229,7 +292,15 @@ watch(
|
||||
collect_method: props.record.collect_method || 'api',
|
||||
snmp_target: props.record.snmp_target || '',
|
||||
snmp_port: props.record.snmp_port || 161,
|
||||
snmp_version: props.record.snmp_version || 'v2c',
|
||||
snmp_community: props.record.snmp_community || '',
|
||||
snmp_v3_security_level: props.record.snmp_v3_security_level || 'noAuthNoPriv',
|
||||
snmp_v3_security_name: props.record.snmp_v3_security_name || '',
|
||||
snmp_v3_auth_protocol: props.record.snmp_v3_auth_protocol || 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: props.record.snmp_v3_priv_protocol || 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: props.record.snmp_v3_context_name || '',
|
||||
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
|
||||
snmp_retries: props.record.snmp_retries ?? 1,
|
||||
snmp_oids: props.record.snmp_oids || '',
|
||||
@@ -248,7 +319,15 @@ watch(
|
||||
collect_method: 'api',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -270,8 +349,25 @@ const handleOk = async () => {
|
||||
return
|
||||
}
|
||||
if (formData.collect_method === 'snmp') {
|
||||
if (!formData.snmp_target?.trim() || !formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址和 community')
|
||||
if (!formData.snmp_target?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_version === 'v3') {
|
||||
if (!formData.snmp_v3_security_name?.trim()) {
|
||||
Message.warning('SNMP v3 请填写 Security Name')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level !== 'noAuthNoPriv' && !formData.snmp_v3_auth_password?.trim()) {
|
||||
Message.warning('SNMP v3 认证级别请填写 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level === 'authPriv' && !formData.snmp_v3_priv_password?.trim()) {
|
||||
Message.warning('SNMP v3 authPriv 请填写 Priv 密码')
|
||||
return
|
||||
}
|
||||
} else if (!formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP v2c 模式下请填写 community')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_oids?.trim()) {
|
||||
@@ -296,7 +392,15 @@ const handleOk = async () => {
|
||||
collect_method: formData.collect_method,
|
||||
snmp_target: formData.snmp_target,
|
||||
snmp_port: formData.snmp_port,
|
||||
snmp_version: formData.snmp_version,
|
||||
snmp_community: formData.snmp_community,
|
||||
snmp_v3_security_level: formData.snmp_v3_security_level,
|
||||
snmp_v3_security_name: formData.snmp_v3_security_name,
|
||||
snmp_v3_auth_protocol: formData.snmp_v3_auth_protocol,
|
||||
snmp_v3_auth_password: formData.snmp_v3_auth_password,
|
||||
snmp_v3_priv_protocol: formData.snmp_v3_priv_protocol,
|
||||
snmp_v3_priv_password: formData.snmp_v3_priv_password,
|
||||
snmp_v3_context_name: formData.snmp_v3_context_name,
|
||||
snmp_timeout_ms: formData.snmp_timeout_ms,
|
||||
snmp_retries: formData.snmp_retries,
|
||||
snmp_oids: formData.snmp_oids,
|
||||
@@ -321,7 +425,15 @@ const handleOk = async () => {
|
||||
collect_method: formData.collect_method,
|
||||
snmp_target: formData.snmp_target,
|
||||
snmp_port: formData.snmp_port,
|
||||
snmp_version: formData.snmp_version,
|
||||
snmp_community: formData.snmp_community,
|
||||
snmp_v3_security_level: formData.snmp_v3_security_level,
|
||||
snmp_v3_security_name: formData.snmp_v3_security_name,
|
||||
snmp_v3_auth_protocol: formData.snmp_v3_auth_protocol,
|
||||
snmp_v3_auth_password: formData.snmp_v3_auth_password,
|
||||
snmp_v3_priv_protocol: formData.snmp_v3_priv_protocol,
|
||||
snmp_v3_priv_password: formData.snmp_v3_priv_password,
|
||||
snmp_v3_context_name: formData.snmp_v3_context_name,
|
||||
snmp_timeout_ms: formData.snmp_timeout_ms,
|
||||
snmp_retries: formData.snmp_retries,
|
||||
snmp_oids: formData.snmp_oids,
|
||||
|
||||
@@ -17,12 +17,90 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label="SNMP目标地址">
|
||||
<a-input v-model="form.snmp_target" placeholder="例如 192.168.1.10" />
|
||||
</a-form-item>
|
||||
<a-form-item label="Community">
|
||||
<a-input v-model="form.snmp_community" placeholder="SNMP v2c community" />
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="SNMP目标地址">
|
||||
<a-input v-model="form.snmp_target" placeholder="例如 192.168.1.10" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="SNMP端口">
|
||||
<a-input-number v-model="form.snmp_port" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="SNMP版本">
|
||||
<a-radio-group v-model="form.snmp_version" type="button">
|
||||
<a-radio value="v2c">v2c</a-radio>
|
||||
<a-radio value="v3">v3</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-row :gutter="20">
|
||||
<a-col v-if="form.snmp_version !== 'v3'" :span="12">
|
||||
<a-form-item label="Community">
|
||||
<a-input v-model="form.snmp_community" placeholder="SNMP v2c community" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-else :span="12">
|
||||
<a-form-item label="Security Name">
|
||||
<a-input v-model="form.snmp_v3_security_name" placeholder="SNMP v3 用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="form.snmp_version === 'v3'" :span="12">
|
||||
<a-form-item label="Security Level">
|
||||
<a-select v-model="form.snmp_v3_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-col :span="6">
|
||||
<a-form-item label="超时(ms)">
|
||||
<a-input-number v-model="form.snmp_timeout_ms" :min="1" :max="60000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item label="重试次数">
|
||||
<a-input-number v-model="form.snmp_retries" :min="0" :max="10" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="form.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="Auth协议">
|
||||
<a-select v-model="form.snmp_v3_auth_protocol">
|
||||
<a-option value="SHA">SHA</a-option>
|
||||
<a-option value="MD5">MD5</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-form-item label="Auth密码">
|
||||
<a-input-password v-model="form.snmp_v3_auth_password" placeholder="authNoPriv/authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="form.snmp_version === 'v3' && form.snmp_v3_security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="Priv协议">
|
||||
<a-select v-model="form.snmp_v3_priv_protocol">
|
||||
<a-option value="AES">AES</a-option>
|
||||
<a-option value="DES">DES</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="Priv密码">
|
||||
<a-input-password v-model="form.snmp_v3_priv_password" placeholder="authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="Context">
|
||||
<a-input v-model="form.snmp_v3_context_name" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<a-form-item label="参与周期采集">
|
||||
@@ -61,7 +139,18 @@ const form = ref({
|
||||
collect_interval: 60,
|
||||
agent_config: '',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c' as 'v2c' | 'v3',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -73,7 +162,18 @@ watch(
|
||||
form.value.collect_interval = props.record.collect_interval || 60
|
||||
form.value.agent_config = props.record.agent_config || ''
|
||||
form.value.snmp_target = props.record.snmp_target || ''
|
||||
form.value.snmp_port = props.record.snmp_port || 161
|
||||
form.value.snmp_version = props.record.snmp_version || 'v2c'
|
||||
form.value.snmp_community = props.record.snmp_community || ''
|
||||
form.value.snmp_v3_security_level = props.record.snmp_v3_security_level || 'noAuthNoPriv'
|
||||
form.value.snmp_v3_security_name = props.record.snmp_v3_security_name || ''
|
||||
form.value.snmp_v3_auth_protocol = props.record.snmp_v3_auth_protocol || 'SHA'
|
||||
form.value.snmp_v3_auth_password = ''
|
||||
form.value.snmp_v3_priv_protocol = props.record.snmp_v3_priv_protocol || 'AES'
|
||||
form.value.snmp_v3_priv_password = ''
|
||||
form.value.snmp_v3_context_name = props.record.snmp_v3_context_name || ''
|
||||
form.value.snmp_timeout_ms = props.record.snmp_timeout_ms || 3000
|
||||
form.value.snmp_retries = props.record.snmp_retries ?? 1
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -81,13 +181,51 @@ watch(
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
if (form.value.collect_method === 'api' && !form.value.agent_config?.trim()) {
|
||||
Message.warning('API 模式下请填写采集地址')
|
||||
return
|
||||
}
|
||||
if (form.value.collect_method === 'snmp') {
|
||||
if (!form.value.snmp_target?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址')
|
||||
return
|
||||
}
|
||||
if (form.value.snmp_version === 'v3') {
|
||||
if (!form.value.snmp_v3_security_name?.trim()) {
|
||||
Message.warning('SNMP v3 请填写 Security Name')
|
||||
return
|
||||
}
|
||||
if (form.value.snmp_v3_security_level !== 'noAuthNoPriv' && !form.value.snmp_v3_auth_password?.trim()) {
|
||||
Message.warning('SNMP v3 认证级别请填写 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (form.value.snmp_v3_security_level === 'authPriv' && !form.value.snmp_v3_priv_password?.trim()) {
|
||||
Message.warning('SNMP v3 authPriv 请填写 Priv 密码')
|
||||
return
|
||||
}
|
||||
} else if (!form.value.snmp_community?.trim()) {
|
||||
Message.warning('SNMP v2c 模式下请填写 community')
|
||||
return
|
||||
}
|
||||
}
|
||||
const data: RoomDeviceCollectData = {
|
||||
collect_method: form.value.collect_method,
|
||||
collect_on: form.value.collect_on,
|
||||
collect_interval: form.value.collect_interval,
|
||||
agent_config: form.value.collect_method === 'api' ? form.value.agent_config : undefined,
|
||||
snmp_target: form.value.collect_method === 'snmp' ? form.value.snmp_target : undefined,
|
||||
snmp_port: form.value.collect_method === 'snmp' ? form.value.snmp_port : undefined,
|
||||
snmp_version: form.value.collect_method === 'snmp' ? form.value.snmp_version : undefined,
|
||||
snmp_community: form.value.collect_method === 'snmp' ? form.value.snmp_community : undefined,
|
||||
snmp_v3_security_level: form.value.collect_method === 'snmp' ? form.value.snmp_v3_security_level : undefined,
|
||||
snmp_v3_security_name: form.value.collect_method === 'snmp' ? form.value.snmp_v3_security_name : undefined,
|
||||
snmp_v3_auth_protocol: form.value.collect_method === 'snmp' ? form.value.snmp_v3_auth_protocol : undefined,
|
||||
snmp_v3_auth_password: form.value.collect_method === 'snmp' ? form.value.snmp_v3_auth_password : undefined,
|
||||
snmp_v3_priv_protocol: form.value.collect_method === 'snmp' ? form.value.snmp_v3_priv_protocol : undefined,
|
||||
snmp_v3_priv_password: form.value.collect_method === 'snmp' ? form.value.snmp_v3_priv_password : undefined,
|
||||
snmp_v3_context_name: form.value.collect_method === 'snmp' ? form.value.snmp_v3_context_name : undefined,
|
||||
snmp_timeout_ms: form.value.collect_method === 'snmp' ? form.value.snmp_timeout_ms : undefined,
|
||||
snmp_retries: form.value.collect_method === 'snmp' ? form.value.snmp_retries : undefined,
|
||||
}
|
||||
|
||||
await patchRoomDeviceCollect(props.record.id, data)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
@@ -66,12 +66,6 @@
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleQuickConfig(record)">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
采集配置
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
@@ -88,7 +82,7 @@
|
||||
|
||||
<QuickConfigDialog v-model:visible="quickConfigVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||
|
||||
<a-drawer v-model:visible="detailVisible" :width="800" title="机房设备详情" :footer="false" unmount-on-close>
|
||||
<a-drawer v-model:visible="detailVisible" :width="860" title="机房设备采集详情" :footer="false" unmount-on-close>
|
||||
<Detail
|
||||
v-if="currentRecord"
|
||||
:record="currentRecord"
|
||||
@@ -103,7 +97,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye } 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'
|
||||
@@ -266,3 +260,5 @@ export default {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -71,17 +71,6 @@
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
@@ -100,12 +89,12 @@
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<!-- <a-doption @click="handleQuickConfig(record)">
|
||||
<a-doption @click="handleQuickConfig(record)">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
采集配置
|
||||
</a-doption> -->
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:visible="visible"
|
||||
:title="isEdit ? '编辑网络设备采集' : '新增网络设备采集'"
|
||||
:confirm-loading="confirmLoading"
|
||||
:mask-closable="false"
|
||||
:mask-closable="true"
|
||||
width="980px"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
@@ -38,13 +38,24 @@
|
||||
<a-divider orientation="left">SNMP 配置(必填)</a-divider>
|
||||
<a-row :gutter="20">
|
||||
<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-col :span="12"><a-form-item field="snmp_version" label="SNMP版本"><a-radio-group v-model="formData.snmp_version" type="button"><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-row v-if="formData.snmp_version === 'v3' && formData.snmpv3.security_level !== 'noAuthNoPriv'" :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="snmpv3_auth_protocol" label="Auth协议"><a-select v-model="formData.snmpv3.auth_protocol"><a-option value="SHA">SHA</a-option><a-option value="MD5">MD5</a-option></a-select></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="snmpv3_auth_password" label="Auth密码" required><a-input-password v-model="formData.snmpv3.auth_password" placeholder="authNoPriv/authPriv 必填" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3' && formData.snmpv3.security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="snmpv3_priv_protocol" label="Priv协议"><a-select v-model="formData.snmpv3.priv_protocol"><a-option value="AES">AES</a-option><a-option value="DES">DES</a-option></a-select></a-form-item></a-col>
|
||||
<a-col :span="12"><a-form-item field="snmpv3_priv_password" label="Priv密码" required><a-input-password v-model="formData.snmpv3.priv_password" placeholder="authPriv 必填" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="12"><a-form-item field="snmpv3_context_name" label="Context Name"><a-input v-model="formData.snmpv3.context_name" placeholder="可选" /></a-form-item></a-col>
|
||||
</a-row>
|
||||
|
||||
<a-divider orientation="left">SSH 配置(必填)</a-divider>
|
||||
<a-row :gutter="20">
|
||||
@@ -183,6 +194,20 @@ const handleOk = async () => {
|
||||
Message.warning('请填写 SNMP community')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_version === 'v3') {
|
||||
if (!formData.snmpv3.security_name.trim()) {
|
||||
Message.warning('请填写 SNMPv3 Security Name')
|
||||
return
|
||||
}
|
||||
if (formData.snmpv3.security_level !== 'noAuthNoPriv' && !formData.snmpv3.auth_password.trim()) {
|
||||
Message.warning('请填写 SNMPv3 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (formData.snmpv3.security_level === 'authPriv' && !formData.snmpv3.priv_password.trim()) {
|
||||
Message.warning('请填写 SNMPv3 Priv 密码')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!formData.ssh.username.trim()) {
|
||||
Message.warning('请填写 SSH 用户名')
|
||||
return
|
||||
|
||||
@@ -26,7 +26,36 @@
|
||||
<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>
|
||||
<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>
|
||||
</search-table>
|
||||
|
||||
<FormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||
@@ -42,6 +71,9 @@ import { Message, Modal } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconPlus,
|
||||
IconDelete,
|
||||
IconDown,
|
||||
IconEdit,
|
||||
IconEye,
|
||||
IconRefresh,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { FormItem } from '@/components/search-form/types'
|
||||
|
||||
@@ -85,12 +85,32 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_version" label="SNMP版本">
|
||||
<a-radio-group v-model="formData.snmp_version" type="button">
|
||||
<a-radio value="v2c">v2c</a-radio>
|
||||
<a-radio value="v3">v3</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-col v-if="formData.snmp_version !== 'v3'" :span="12">
|
||||
<a-form-item field="snmp_community" label="Community">
|
||||
<a-input v-model="formData.snmp_community" placeholder="SNMP v2c community" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-else :span="12">
|
||||
<a-form-item field="snmp_v3_security_name" label="Security Name">
|
||||
<a-input v-model="formData.snmp_v3_security_name" placeholder="SNMP v3 用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="formData.snmp_version === 'v3'" :span="12">
|
||||
<a-form-item field="snmp_v3_security_level" label="Security Level">
|
||||
<a-select v-model="formData.snmp_v3_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-col :span="6">
|
||||
<a-form-item field="snmp_timeout_ms" label="超时(ms)">
|
||||
<a-input-number v-model="formData.snmp_timeout_ms" :min="1" :max="60000" style="width: 100%" />
|
||||
@@ -102,6 +122,41 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_auth_protocol" label="Auth协议">
|
||||
<a-select v-model="formData.snmp_v3_auth_protocol">
|
||||
<a-option value="SHA">SHA</a-option>
|
||||
<a-option value="MD5">MD5</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-form-item field="snmp_v3_auth_password" label="Auth密码">
|
||||
<a-input-password v-model="formData.snmp_v3_auth_password" placeholder="authNoPriv/authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3' && formData.snmp_v3_security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_protocol" label="Priv协议">
|
||||
<a-select v-model="formData.snmp_v3_priv_protocol">
|
||||
<a-option value="AES">AES</a-option>
|
||||
<a-option value="DES">DES</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_password" label="Priv密码">
|
||||
<a-input-password v-model="formData.snmp_v3_priv_password" placeholder="authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_context_name" label="Context">
|
||||
<a-input v-model="formData.snmp_v3_context_name" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_oids" label="SNMP OID配置(JSON数组)">
|
||||
<a-textarea
|
||||
v-model="formData.snmp_oids"
|
||||
@@ -190,7 +245,15 @@ const formData = reactive({
|
||||
collect_method: 'api' as 'api' | 'snmp',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c' as 'v2c' | 'v3',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -254,7 +317,15 @@ watch(
|
||||
collect_method: props.record.collect_method || 'api',
|
||||
snmp_target: props.record.snmp_target || '',
|
||||
snmp_port: props.record.snmp_port || 161,
|
||||
snmp_version: props.record.snmp_version || 'v2c',
|
||||
snmp_community: props.record.snmp_community || '',
|
||||
snmp_v3_security_level: props.record.snmp_v3_security_level || 'noAuthNoPriv',
|
||||
snmp_v3_security_name: props.record.snmp_v3_security_name || '',
|
||||
snmp_v3_auth_protocol: props.record.snmp_v3_auth_protocol || 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: props.record.snmp_v3_priv_protocol || 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: props.record.snmp_v3_context_name || '',
|
||||
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
|
||||
snmp_retries: props.record.snmp_retries ?? 1,
|
||||
snmp_oids: props.record.snmp_oids || '',
|
||||
@@ -278,7 +349,15 @@ watch(
|
||||
collect_method: 'api',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -306,8 +385,21 @@ const handleOk = async () => {
|
||||
Message.warning('SNMP 模式下请填写目标地址')
|
||||
return
|
||||
}
|
||||
if (!formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写 community')
|
||||
if (formData.snmp_version === 'v3') {
|
||||
if (!formData.snmp_v3_security_name?.trim()) {
|
||||
Message.warning('SNMP v3 请填写 Security Name')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level !== 'noAuthNoPriv' && !formData.snmp_v3_auth_password?.trim()) {
|
||||
Message.warning('SNMP v3 认证级别请填写 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level === 'authPriv' && !formData.snmp_v3_priv_password?.trim()) {
|
||||
Message.warning('SNMP v3 authPriv 请填写 Priv 密码')
|
||||
return
|
||||
}
|
||||
} else if (!formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP v2c 模式下请填写 community')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_oids?.trim()) {
|
||||
@@ -334,7 +426,15 @@ const handleOk = async () => {
|
||||
collect_method: formData.collect_method,
|
||||
snmp_target: formData.snmp_target,
|
||||
snmp_port: formData.snmp_port,
|
||||
snmp_version: formData.snmp_version,
|
||||
snmp_community: formData.snmp_community,
|
||||
snmp_v3_security_level: formData.snmp_v3_security_level,
|
||||
snmp_v3_security_name: formData.snmp_v3_security_name,
|
||||
snmp_v3_auth_protocol: formData.snmp_v3_auth_protocol,
|
||||
snmp_v3_auth_password: formData.snmp_v3_auth_password,
|
||||
snmp_v3_priv_protocol: formData.snmp_v3_priv_protocol,
|
||||
snmp_v3_priv_password: formData.snmp_v3_priv_password,
|
||||
snmp_v3_context_name: formData.snmp_v3_context_name,
|
||||
snmp_timeout_ms: formData.snmp_timeout_ms,
|
||||
snmp_retries: formData.snmp_retries,
|
||||
snmp_oids: formData.snmp_oids,
|
||||
|
||||
@@ -22,12 +22,90 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item field="snmp_target" label="SNMP目标地址">
|
||||
<a-input v-model="formData.snmp_target" placeholder="例如 192.168.1.10" />
|
||||
</a-form-item>
|
||||
<a-form-item field="snmp_community" label="Community">
|
||||
<a-input v-model="formData.snmp_community" placeholder="SNMP v2c community" />
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="snmp_target" label="SNMP目标地址">
|
||||
<a-input v-model="formData.snmp_target" placeholder="例如 192.168.1.10" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item field="snmp_port" label="SNMP端口">
|
||||
<a-input-number v-model="formData.snmp_port" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_version" label="SNMP版本">
|
||||
<a-radio-group v-model="formData.snmp_version" type="button">
|
||||
<a-radio value="v2c">v2c</a-radio>
|
||||
<a-radio value="v3">v3</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-row :gutter="20">
|
||||
<a-col v-if="formData.snmp_version !== 'v3'" :span="12">
|
||||
<a-form-item field="snmp_community" label="Community">
|
||||
<a-input v-model="formData.snmp_community" placeholder="SNMP v2c community" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-else :span="12">
|
||||
<a-form-item field="snmp_v3_security_name" label="Security Name">
|
||||
<a-input v-model="formData.snmp_v3_security_name" placeholder="SNMP v3 用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="formData.snmp_version === 'v3'" :span="12">
|
||||
<a-form-item field="snmp_v3_security_level" label="Security Level">
|
||||
<a-select v-model="formData.snmp_v3_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-col :span="6">
|
||||
<a-form-item field="snmp_timeout_ms" label="超时(ms)">
|
||||
<a-input-number v-model="formData.snmp_timeout_ms" :min="1" :max="60000" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-form-item field="snmp_retries" label="重试次数">
|
||||
<a-input-number v-model="formData.snmp_retries" :min="0" :max="10" style="width: 100%" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_auth_protocol" label="Auth协议">
|
||||
<a-select v-model="formData.snmp_v3_auth_protocol">
|
||||
<a-option value="SHA">SHA</a-option>
|
||||
<a-option value="MD5">MD5</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-form-item field="snmp_v3_auth_password" label="Auth密码">
|
||||
<a-input-password v-model="formData.snmp_v3_auth_password" placeholder="authNoPriv/authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3' && formData.snmp_v3_security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_protocol" label="Priv协议">
|
||||
<a-select v-model="formData.snmp_v3_priv_protocol">
|
||||
<a-option value="AES">AES</a-option>
|
||||
<a-option value="DES">DES</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_password" label="Priv密码">
|
||||
<a-input-password v-model="formData.snmp_v3_priv_password" placeholder="authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_context_name" label="Context">
|
||||
<a-input v-model="formData.snmp_v3_context_name" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<a-form-item field="collect_on" label="启用周期采集">
|
||||
@@ -67,7 +145,18 @@ const formData = reactive({
|
||||
collect_method: 'api' as 'api' | 'snmp',
|
||||
agent_config: '',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c' as 'v2c' | 'v3',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
collect_on: true,
|
||||
collect_interval: 60,
|
||||
})
|
||||
@@ -79,7 +168,18 @@ watch(
|
||||
formData.collect_method = props.record.collect_method || 'api'
|
||||
formData.agent_config = props.record.agent_config || ''
|
||||
formData.snmp_target = props.record.snmp_target || ''
|
||||
formData.snmp_port = props.record.snmp_port || 161
|
||||
formData.snmp_version = props.record.snmp_version || 'v2c'
|
||||
formData.snmp_community = props.record.snmp_community || ''
|
||||
formData.snmp_v3_security_level = props.record.snmp_v3_security_level || 'noAuthNoPriv'
|
||||
formData.snmp_v3_security_name = props.record.snmp_v3_security_name || ''
|
||||
formData.snmp_v3_auth_protocol = props.record.snmp_v3_auth_protocol || 'SHA'
|
||||
formData.snmp_v3_auth_password = ''
|
||||
formData.snmp_v3_priv_protocol = props.record.snmp_v3_priv_protocol || 'AES'
|
||||
formData.snmp_v3_priv_password = ''
|
||||
formData.snmp_v3_context_name = props.record.snmp_v3_context_name || ''
|
||||
formData.snmp_timeout_ms = props.record.snmp_timeout_ms || 3000
|
||||
formData.snmp_retries = props.record.snmp_retries ?? 1
|
||||
formData.collect_on = props.record.collect_on ?? true
|
||||
formData.collect_interval = props.record.collect_interval || 60
|
||||
}
|
||||
@@ -88,13 +188,51 @@ watch(
|
||||
|
||||
const handleOk = async () => {
|
||||
try {
|
||||
if (formData.collect_method === 'api' && !formData.agent_config?.trim()) {
|
||||
Message.warning('API 模式下请填写采集地址')
|
||||
return
|
||||
}
|
||||
if (formData.collect_method === 'snmp') {
|
||||
if (!formData.snmp_target?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_version === 'v3') {
|
||||
if (!formData.snmp_v3_security_name?.trim()) {
|
||||
Message.warning('SNMP v3 请填写 Security Name')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level !== 'noAuthNoPriv' && !formData.snmp_v3_auth_password?.trim()) {
|
||||
Message.warning('SNMP v3 认证级别请填写 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level === 'authPriv' && !formData.snmp_v3_priv_password?.trim()) {
|
||||
Message.warning('SNMP v3 authPriv 请填写 Priv 密码')
|
||||
return
|
||||
}
|
||||
} else if (!formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP v2c 模式下请填写 community')
|
||||
return
|
||||
}
|
||||
}
|
||||
confirmLoading.value = true
|
||||
|
||||
const patchData: SecurityServicePatchData = {
|
||||
collect_method: formData.collect_method,
|
||||
agent_config: formData.collect_method === 'api' ? formData.agent_config : undefined,
|
||||
snmp_target: formData.collect_method === 'snmp' ? formData.snmp_target : undefined,
|
||||
snmp_port: formData.collect_method === 'snmp' ? formData.snmp_port : undefined,
|
||||
snmp_version: formData.collect_method === 'snmp' ? formData.snmp_version : undefined,
|
||||
snmp_community: formData.collect_method === 'snmp' ? formData.snmp_community : undefined,
|
||||
snmp_v3_security_level: formData.collect_method === 'snmp' ? formData.snmp_v3_security_level : undefined,
|
||||
snmp_v3_security_name: formData.collect_method === 'snmp' ? formData.snmp_v3_security_name : undefined,
|
||||
snmp_v3_auth_protocol: formData.collect_method === 'snmp' ? formData.snmp_v3_auth_protocol : undefined,
|
||||
snmp_v3_auth_password: formData.collect_method === 'snmp' ? formData.snmp_v3_auth_password : undefined,
|
||||
snmp_v3_priv_protocol: formData.collect_method === 'snmp' ? formData.snmp_v3_priv_protocol : undefined,
|
||||
snmp_v3_priv_password: formData.collect_method === 'snmp' ? formData.snmp_v3_priv_password : undefined,
|
||||
snmp_v3_context_name: formData.collect_method === 'snmp' ? formData.snmp_v3_context_name : undefined,
|
||||
snmp_timeout_ms: formData.collect_method === 'snmp' ? formData.snmp_timeout_ms : undefined,
|
||||
snmp_retries: formData.collect_method === 'snmp' ? formData.snmp_retries : undefined,
|
||||
collect_on: formData.collect_on,
|
||||
collect_interval: formData.collect_interval,
|
||||
}
|
||||
|
||||
@@ -114,23 +114,18 @@
|
||||
<!-- 操作栏 - 下拉菜单 -->
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button
|
||||
v-if="!record.agent_config"
|
||||
type="outline"
|
||||
size="small"
|
||||
@click="handleQuickConfig(record)"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-button>
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="primary" size="small">
|
||||
管理
|
||||
<icon-down />
|
||||
</a-button>
|
||||
<template #content>
|
||||
<a-doption @click="handleQuickConfig(record)">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
快捷配置
|
||||
</a-doption>
|
||||
<!-- <a-doption @click="handleRestart(record)">
|
||||
<template #icon>
|
||||
<icon-refresh />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
title="存储设备详情"
|
||||
title="存储设备采集详情"
|
||||
placement="right"
|
||||
width="600px"
|
||||
width="860px"
|
||||
@cancel="handleCancel"
|
||||
@update:visible="handleUpdateVisible"
|
||||
>
|
||||
@@ -67,16 +67,31 @@
|
||||
<a-descriptions-item label="运行时长">{{ detailData?.uptime ? `${detailData?.uptime}秒` : '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions title="最新指标" :column="2" bordered style="margin-top: 20px" v-if="metricsData?.metrics?.length">
|
||||
<a-descriptions-item label="数据时间">{{ formatDateTime(metricsData?.latest_timestamp) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="指标数量">{{ metricsData?.count || 0 }}</a-descriptions-item>
|
||||
<a-descriptions-item v-for="metric in metricsData?.metrics" :key="metric.metric_name" :label="metric.metric_name">
|
||||
{{ metric.metric_value }} {{ metric.metric_unit }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-empty v-else-if="!loading && !metricsData?.metrics?.length" description="暂无指标数据" style="margin-top: 20px" />
|
||||
<div class="action-bar">
|
||||
<a-space>
|
||||
<a-button type="outline" @click="handleViewMetrics">查看最新指标</a-button>
|
||||
<a-button type="primary" @click="$emit('edit')">编辑</a-button>
|
||||
<a-button type="outline" status="danger" @click="$emit('delete')">删除</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<a-drawer v-model:visible="metricsVisible" :width="860" title="最新指标数据" :footer="false" unmount-on-close>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="数据时间">{{ formatDateTime(metricsData?.latest_timestamp) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="指标数量">{{ metricsData?.count || 0 }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div v-if="metricsData?.metrics?.length" class="metrics-grid">
|
||||
<div v-for="metric in metricsData?.metrics" :key="metric.metric_name" class="metric-item">
|
||||
<div class="metric-name">{{ metric.metric_name }}</div>
|
||||
<div class="metric-value">{{ metric.metric_value }} {{ metric.metric_unit }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else-if="!loading" description="暂无指标数据" style="margin-top: 20px" />
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@@ -95,11 +110,12 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
record: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
const emit = defineEmits(['update:visible', 'edit', 'delete'])
|
||||
|
||||
const loading = ref(false)
|
||||
const detailData = ref<StorageItem | null>(null)
|
||||
const metricsData = ref<StorageMetricsLatestResponse | null>(null)
|
||||
const metricsVisible = ref(false)
|
||||
|
||||
const getStatusColor = (status?: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
@@ -187,10 +203,55 @@ const handleUpdateVisible = (value: boolean) => {
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleViewMetrics = () => {
|
||||
metricsVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.detail-container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
border: 1px solid var(--color-neutral-3);
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
min-height: 56px;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
.metric-name {
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
color: var(--color-text-1);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.metrics-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -99,13 +99,33 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item field="snmp_version" label="SNMP版本">
|
||||
<a-radio-group v-model="formData.snmp_version" type="button">
|
||||
<a-radio value="v2c">v2c</a-radio>
|
||||
<a-radio value="v3">v3</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-col v-if="formData.snmp_version !== 'v3'" :span="12">
|
||||
<a-form-item field="snmp_community" label="SNMP Community">
|
||||
<a-input v-model="formData.snmp_community" placeholder="请输入SNMP community" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-else :span="12">
|
||||
<a-form-item field="snmp_v3_security_name" label="Security Name">
|
||||
<a-input v-model="formData.snmp_v3_security_name" placeholder="SNMP v3 用户名" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="formData.snmp_version === 'v3'" :span="12">
|
||||
<a-form-item field="snmp_v3_security_level" label="Security Level">
|
||||
<a-select v-model="formData.snmp_v3_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-col :span="6">
|
||||
<a-form-item field="snmp_timeout_ms" label="超时(毫秒)">
|
||||
<a-input-number v-model="formData.snmp_timeout_ms" :min="1" :max="60000" placeholder="默认3000" style="width: 100%" />
|
||||
@@ -117,6 +137,41 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_auth_protocol" label="Auth协议">
|
||||
<a-select v-model="formData.snmp_v3_auth_protocol">
|
||||
<a-option value="SHA">SHA</a-option>
|
||||
<a-option value="MD5">MD5</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<a-form-item field="snmp_v3_auth_password" label="Auth密码">
|
||||
<a-input-password v-model="formData.snmp_v3_auth_password" placeholder="authNoPriv/authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row v-if="formData.snmp_version === 'v3' && formData.snmp_v3_security_level === 'authPriv'" :gutter="20">
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_protocol" label="Priv协议">
|
||||
<a-select v-model="formData.snmp_v3_priv_protocol">
|
||||
<a-option value="AES">AES</a-option>
|
||||
<a-option value="DES">DES</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_priv_password" label="Priv密码">
|
||||
<a-input-password v-model="formData.snmp_v3_priv_password" placeholder="authPriv 必填" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item field="snmp_v3_context_name" label="Context">
|
||||
<a-input v-model="formData.snmp_v3_context_name" placeholder="可选" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item field="snmp_oids" label="SNMP OID配置">
|
||||
<a-textarea
|
||||
@@ -185,7 +240,15 @@ const formData = reactive<StorageCreateData>({
|
||||
collect_method: 'api' as 'api' | 'snmp',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c' as 'v2c' | 'v3',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -218,7 +281,15 @@ watch(
|
||||
collect_method: props.record.collect_method || 'api',
|
||||
snmp_target: props.record.snmp_target || '',
|
||||
snmp_port: props.record.snmp_port || 161,
|
||||
snmp_version: props.record.snmp_version || 'v2c',
|
||||
snmp_community: props.record.snmp_community || '',
|
||||
snmp_v3_security_level: props.record.snmp_v3_security_level || 'noAuthNoPriv',
|
||||
snmp_v3_security_name: props.record.snmp_v3_security_name || '',
|
||||
snmp_v3_auth_protocol: props.record.snmp_v3_auth_protocol || 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: props.record.snmp_v3_priv_protocol || 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: props.record.snmp_v3_context_name || '',
|
||||
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
|
||||
snmp_retries: props.record.snmp_retries ?? 1,
|
||||
snmp_oids: props.record.snmp_oids || '',
|
||||
@@ -241,7 +312,15 @@ watch(
|
||||
collect_method: 'api',
|
||||
snmp_target: '',
|
||||
snmp_port: 161,
|
||||
snmp_version: 'v2c',
|
||||
snmp_community: '',
|
||||
snmp_v3_security_level: 'noAuthNoPriv',
|
||||
snmp_v3_security_name: '',
|
||||
snmp_v3_auth_protocol: 'SHA',
|
||||
snmp_v3_auth_password: '',
|
||||
snmp_v3_priv_protocol: 'AES',
|
||||
snmp_v3_priv_password: '',
|
||||
snmp_v3_context_name: '',
|
||||
snmp_timeout_ms: 3000,
|
||||
snmp_retries: 1,
|
||||
snmp_oids: '',
|
||||
@@ -262,8 +341,25 @@ const handleOk = async () => {
|
||||
return
|
||||
}
|
||||
if (formData.collect_method === 'snmp') {
|
||||
if (!formData.snmp_target?.trim() || !formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址和 community')
|
||||
if (!formData.snmp_target?.trim()) {
|
||||
Message.warning('SNMP 模式下请填写目标地址')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_version === 'v3') {
|
||||
if (!formData.snmp_v3_security_name?.trim()) {
|
||||
Message.warning('SNMP v3 请填写 Security Name')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level !== 'noAuthNoPriv' && !formData.snmp_v3_auth_password?.trim()) {
|
||||
Message.warning('SNMP v3 认证级别请填写 Auth 密码')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_v3_security_level === 'authPriv' && !formData.snmp_v3_priv_password?.trim()) {
|
||||
Message.warning('SNMP v3 authPriv 请填写 Priv 密码')
|
||||
return
|
||||
}
|
||||
} else if (!formData.snmp_community?.trim()) {
|
||||
Message.warning('SNMP v2c 模式下请填写 community')
|
||||
return
|
||||
}
|
||||
if (formData.snmp_oids?.trim()) {
|
||||
@@ -292,7 +388,15 @@ const handleOk = async () => {
|
||||
collect_method: formData.collect_method,
|
||||
snmp_target: formData.snmp_target,
|
||||
snmp_port: formData.snmp_port,
|
||||
snmp_version: formData.snmp_version,
|
||||
snmp_community: formData.snmp_community,
|
||||
snmp_v3_security_level: formData.snmp_v3_security_level,
|
||||
snmp_v3_security_name: formData.snmp_v3_security_name,
|
||||
snmp_v3_auth_protocol: formData.snmp_v3_auth_protocol,
|
||||
snmp_v3_auth_password: formData.snmp_v3_auth_password,
|
||||
snmp_v3_priv_protocol: formData.snmp_v3_priv_protocol,
|
||||
snmp_v3_priv_password: formData.snmp_v3_priv_password,
|
||||
snmp_v3_context_name: formData.snmp_v3_context_name,
|
||||
snmp_timeout_ms: formData.snmp_timeout_ms,
|
||||
snmp_retries: formData.snmp_retries,
|
||||
snmp_oids: formData.snmp_oids,
|
||||
|
||||
@@ -58,18 +58,18 @@
|
||||
|
||||
<template #actions="{ record }">
|
||||
<a-space>
|
||||
<a-button type="outline" size="small" @click="handleDetail(record)">
|
||||
<template #icon>
|
||||
<icon-eye />
|
||||
</template>
|
||||
详情
|
||||
</a-button>
|
||||
<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 />
|
||||
@@ -90,7 +90,12 @@
|
||||
|
||||
<StorageFormDialog v-model:visible="formDialogVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||
|
||||
<StorageDetail v-model:visible="detailVisible" :record="currentRecord" />
|
||||
<StorageDetail
|
||||
v-model:visible="detailVisible"
|
||||
:record="currentRecord"
|
||||
@edit="handleDetailEdit"
|
||||
@delete="handleDetailDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -248,6 +253,18 @@ const handleDetail = (record: any) => {
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailEdit = () => {
|
||||
detailVisible.value = false
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDetailDelete = () => {
|
||||
detailVisible.value = false
|
||||
if (currentRecord.value) {
|
||||
handleDelete(currentRecord.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
fetchStorages()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="detail-container">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions title="基础信息" :column="2" bordered>
|
||||
<a-descriptions-item label="ID">{{ record.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务标识">{{ record.service_identity }}</a-descriptions-item>
|
||||
<a-descriptions-item label="服务名称">{{ record.name }}</a-descriptions-item>
|
||||
@@ -10,21 +10,29 @@
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="标签" :span="2">{{ record.tags || '-' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions title="采集配置" :column="2" bordered class="section-block">
|
||||
<a-descriptions-item label="启用状态">
|
||||
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||
{{ record.enabled ? '已启用' : '已禁用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="采集间隔">{{ record.interval }}秒</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="参与周期采集">
|
||||
<a-tag :color="record.collect_on ? 'green' : 'gray'">
|
||||
{{ record.collect_on ? '已启用' : '未启用' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="周期采集间隔">{{ record.collect_interval }}秒</a-descriptions-item>
|
||||
<a-descriptions-item label="额外配置" :span="2">
|
||||
<a-textarea :model-value="record.extra" :auto-size="{ minRows: 2, maxRows: 6 }" read-only />
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-descriptions title="运行状态" :column="2" bordered class="section-block">
|
||||
<a-descriptions-item label="运行状态">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
@@ -40,13 +48,6 @@
|
||||
|
||||
<a-descriptions-item label="最后离线时间">{{ formatTime(record.last_offline_time) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="采集结果摘要" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="额外配置" :span="2">
|
||||
<a-textarea :model-value="record.extra" :auto-size="{ minRows: 2, maxRows: 6 }" read-only />
|
||||
</a-descriptions-item>
|
||||
|
||||
<a-descriptions-item label="创建时间">{{ formatTime(record.created_at) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="更新时间">{{ formatTime(record.updated_at) }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="action-bar">
|
||||
@@ -116,9 +117,13 @@ const formatTime = (time?: string | null) => {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="container">
|
||||
<search-table
|
||||
:form-model="formModel"
|
||||
@@ -71,12 +71,6 @@
|
||||
</template>
|
||||
编辑
|
||||
</a-doption>
|
||||
<a-doption @click="handleQuickConfig(record)">
|
||||
<template #icon>
|
||||
<icon-settings />
|
||||
</template>
|
||||
采集配置
|
||||
</a-doption>
|
||||
<a-doption @click="handleDelete(record)" style="color: rgb(var(--danger-6))">
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
@@ -93,7 +87,7 @@
|
||||
|
||||
<QuickConfigDialog v-model:visible="quickConfigVisible" :record="currentRecord" @success="handleFormSuccess" />
|
||||
|
||||
<a-drawer v-model:visible="detailVisible" :width="800" title="URL监控设备详情" :footer="false" unmount-on-close>
|
||||
<a-drawer v-model:visible="detailVisible" :width="860" title="URL采集详情" :footer="false" unmount-on-close>
|
||||
<Detail
|
||||
v-if="currentRecord"
|
||||
:record="currentRecord"
|
||||
@@ -108,7 +102,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye, IconSettings } from '@arco-design/web-vue/es/icon'
|
||||
import { IconPlus, IconDown, IconEdit, IconDelete, IconEye } 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'
|
||||
@@ -299,3 +293,5 @@ export default {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h2>自动感知拓扑图</h2>
|
||||
<p class="page-subtitle">自动发现网络设备;数据来自 DC-Control /discovery 与 /ipscans</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a-select
|
||||
@@ -34,7 +33,7 @@
|
||||
|
||||
<a-card :bordered="false" class="mb-6" title="自动感知 · 主扫描范围">
|
||||
<p class="config-hint">
|
||||
仅将所选扫描任务纳入「当前视图」统计与下方设备列表(对应 DC-Control PUT /discovery/config)。
|
||||
仅将所选扫描任务纳入「当前视图」统计与下方设备列表。
|
||||
</p>
|
||||
<div class="primary-scan-row">
|
||||
<a-select
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h2>IP 扫描任务</h2>
|
||||
<p class="page-subtitle">创建与维护发现任务,供自动感知页触发扫描;数据来自 DC-Control /ipscans</p>
|
||||
</div>
|
||||
<a-space>
|
||||
<a-button @click="goAutoTopology">自动感知</a-button>
|
||||
<a-button type="primary" @click="openCreate">
|
||||
<template #icon><icon-plus /></template>
|
||||
新建任务
|
||||
|
||||
@@ -7,6 +7,30 @@
|
||||
<p class="page-subtitle">实时监控和分析网络流量,识别异常流量模式</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<a-tag :color="trafficRuntimeStatus === 'running' ? 'green' : 'gray'" bordered style="margin-right: 12px">
|
||||
流量采集器:{{ trafficRuntimeStatus === 'running' ? '运行中' : '已停止' }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="runtimeActionLoading"
|
||||
:disabled="trafficRuntimeStatus === 'running'"
|
||||
style="margin-right: 8px"
|
||||
@click="handleStartRuntime"
|
||||
>
|
||||
启动采集
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
status="warning"
|
||||
:loading="runtimeActionLoading"
|
||||
:disabled="trafficRuntimeStatus !== 'running'"
|
||||
style="margin-right: 12px"
|
||||
@click="handleStopRuntime"
|
||||
>
|
||||
停止采集
|
||||
</a-button>
|
||||
<a-button size="small" style="margin-right: 12px" @click="loadRuntimeStatus">刷新状态</a-button>
|
||||
<a-select v-model="timeRange" style="width: 140px; margin-right: 12px">
|
||||
<a-option value="24h">最近24小时</a-option>
|
||||
<a-option value="7d">最近7天</a-option>
|
||||
@@ -26,7 +50,7 @@
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">当前带宽</div>
|
||||
<div class="stats-value">12.4 Gbps</div>
|
||||
<div class="stats-value">{{ currentBandwidth }}</div>
|
||||
<div class="stats-desc">峰值流量</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +64,7 @@
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">入站流量</div>
|
||||
<div class="stats-value">8.5 TB</div>
|
||||
<div class="stats-value">{{ inboundTraffic }}</div>
|
||||
<div class="stats-desc">今日累计</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +78,7 @@
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">出站流量</div>
|
||||
<div class="stats-value">6.2 TB</div>
|
||||
<div class="stats-value">{{ outboundTraffic }}</div>
|
||||
<div class="stats-desc">今日累计</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,12 +92,9 @@
|
||||
</div>
|
||||
<div class="stats-info">
|
||||
<div class="stats-title">活跃会话</div>
|
||||
<div class="stats-value">256K</div>
|
||||
<div class="stats-value">{{ activeSessions }}</div>
|
||||
<div class="stats-desc">
|
||||
<span class="text-success">
|
||||
<IconTrendingUp />
|
||||
较昨日 +12%
|
||||
</span>
|
||||
<span>当前实时连接数</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,9 +169,9 @@
|
||||
<div class="direction-info">
|
||||
<div class="direction-header">
|
||||
<span class="direction-name">外网流量</span>
|
||||
<span class="direction-percent">65%</span>
|
||||
<span class="direction-percent">{{ outboundPercent }}%</span>
|
||||
</div>
|
||||
<a-progress :percent="65" :stroke-width="8" :show-text="false" color="#165DFF" />
|
||||
<a-progress :percent="outboundPercent" :stroke-width="8" :show-text="false" color="#165DFF" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="direction-item">
|
||||
@@ -160,9 +181,9 @@
|
||||
<div class="direction-info">
|
||||
<div class="direction-header">
|
||||
<span class="direction-name">内网流量</span>
|
||||
<span class="direction-percent">35%</span>
|
||||
<span class="direction-percent">{{ inboundPercent }}%</span>
|
||||
</div>
|
||||
<a-progress :percent="35" :stroke-width="8" :show-text="false" color="#14C9C9" />
|
||||
<a-progress :percent="inboundPercent" :stroke-width="8" :show-text="false" color="#14C9C9" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,21 +192,13 @@
|
||||
<a-col :span="12">
|
||||
<div class="direction-stat">
|
||||
<div class="stat-label">外网入站</div>
|
||||
<div class="stat-value">5.2 TB</div>
|
||||
<div class="stat-change text-success">
|
||||
<IconTrendingUp />
|
||||
+8.5%
|
||||
</div>
|
||||
<div class="stat-value">{{ inboundTraffic }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="direction-stat">
|
||||
<div class="stat-label">外网出站</div>
|
||||
<div class="stat-value">3.8 TB</div>
|
||||
<div class="stat-change text-success">
|
||||
<IconTrendingUp />
|
||||
+5.2%
|
||||
</div>
|
||||
<div class="stat-value">{{ outboundTraffic }}</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -269,39 +282,44 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
IconActivity,
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconWorld,
|
||||
IconTrendingUp,
|
||||
IconServer,
|
||||
IconDownload,
|
||||
IconChartLine,
|
||||
} from '@tabler/icons-vue'
|
||||
import Chart from '@/components/chart/index.vue'
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
fetchTrafficDashboard,
|
||||
fetchRealtimeTraffic,
|
||||
fetchTopTraffic,
|
||||
fetchTrafficList,
|
||||
fetchTrafficSummary,
|
||||
fetchTrafficTrend,
|
||||
type TrafficTrendPoint,
|
||||
} from '@/api/ops/traffic'
|
||||
import {
|
||||
fetchTrafficRuntimeStatus,
|
||||
startTrafficRuntime,
|
||||
stopTrafficRuntime,
|
||||
} from '@/api/ops/traffic-runtime'
|
||||
|
||||
// 时间范围
|
||||
const timeRange = ref('24h')
|
||||
|
||||
// 流量趋势数据
|
||||
const trafficData = [
|
||||
{ time: '00:00', inbound: 2.5, outbound: 1.8 },
|
||||
{ time: '02:00', inbound: 1.8, outbound: 1.2 },
|
||||
{ time: '04:00', inbound: 1.2, outbound: 0.8 },
|
||||
{ time: '06:00', inbound: 2.0, outbound: 1.5 },
|
||||
{ time: '08:00', inbound: 8.5, outbound: 6.2 },
|
||||
{ time: '10:00', inbound: 10.2, outbound: 7.8 },
|
||||
{ time: '12:00', inbound: 12.4, outbound: 9.8 },
|
||||
{ time: '14:00', inbound: 11.5, outbound: 8.5 },
|
||||
{ time: '16:00', inbound: 10.8, outbound: 8.2 },
|
||||
{ time: '18:00', inbound: 8.2, outbound: 6.5 },
|
||||
{ time: '20:00', inbound: 5.6, outbound: 4.2 },
|
||||
{ time: '22:00', inbound: 4.2, outbound: 3.2 },
|
||||
{ time: '24:00', inbound: 3.2, outbound: 2.4 },
|
||||
]
|
||||
const currentBandwidth = ref('0 bps')
|
||||
const inboundTraffic = ref('0 B')
|
||||
const outboundTraffic = ref('0 B')
|
||||
const activeSessions = ref('0')
|
||||
const inboundPercent = ref(0)
|
||||
const outboundPercent = ref(0)
|
||||
const trafficTrendData = ref<TrafficTrendPoint[]>([])
|
||||
const trafficRuntimeStatus = ref<'running' | 'stopped' | string>('stopped')
|
||||
const runtimeActionLoading = ref(false)
|
||||
|
||||
// 流量趋势图表配置
|
||||
const trafficChartOptions = ref({
|
||||
@@ -317,7 +335,7 @@ const trafficChartOptions = ref({
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: trafficData.map((item) => item.time),
|
||||
data: [],
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
@@ -328,7 +346,7 @@ const trafficChartOptions = ref({
|
||||
name: '入站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trafficData.map((item) => item.inbound),
|
||||
data: [],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: '#165DFF',
|
||||
@@ -345,7 +363,7 @@ const trafficChartOptions = ref({
|
||||
name: '出站',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: trafficData.map((item) => item.outbound),
|
||||
data: [],
|
||||
areaStyle: {
|
||||
opacity: 0.1,
|
||||
color: '#14C9C9',
|
||||
@@ -362,20 +380,10 @@ const trafficChartOptions = ref({
|
||||
})
|
||||
|
||||
// 协议分布数据
|
||||
const protocolData = [
|
||||
{ name: 'TCP', percent: 78, color: '#165DFF' },
|
||||
{ name: 'UDP', percent: 18, color: '#14C9C9' },
|
||||
{ name: 'ICMP', percent: 2, color: '#F7BA1E' },
|
||||
{ name: '其他', percent: 2, color: '#86909C' },
|
||||
]
|
||||
const protocolData = ref<{ name: string; percent: number; color: string }[]>([])
|
||||
|
||||
// 带宽利用率数据
|
||||
const bandwidthData = [
|
||||
{ name: '上行链路 1', percent: 85 },
|
||||
{ name: '上行链路 2', percent: 62 },
|
||||
{ name: '核心链路', percent: 45 },
|
||||
{ name: '备用链路', percent: 12 },
|
||||
]
|
||||
const bandwidthData = ref<{ name: string; percent: number }[]>([])
|
||||
|
||||
// 获取带宽颜色
|
||||
const getBandwidthColor = (percent: number) => {
|
||||
@@ -385,48 +393,7 @@ const getBandwidthColor = (percent: number) => {
|
||||
}
|
||||
|
||||
// Top 应用流量数据
|
||||
const topApplications = ref([
|
||||
{
|
||||
name: 'HTTPS',
|
||||
port: '443',
|
||||
inbound: '4.2 Gbps',
|
||||
outbound: '3.1 Gbps',
|
||||
sessions: '125,456',
|
||||
shareValue: 45,
|
||||
},
|
||||
{
|
||||
name: 'HTTP',
|
||||
port: '80',
|
||||
inbound: '1.8 Gbps',
|
||||
outbound: '1.2 Gbps',
|
||||
sessions: '45,678',
|
||||
shareValue: 22,
|
||||
},
|
||||
{
|
||||
name: 'SSH',
|
||||
port: '22',
|
||||
inbound: '0.5 Gbps',
|
||||
outbound: '0.3 Gbps',
|
||||
sessions: '2,345',
|
||||
shareValue: 8,
|
||||
},
|
||||
{
|
||||
name: 'MySQL',
|
||||
port: '3306',
|
||||
inbound: '0.8 Gbps',
|
||||
outbound: '1.5 Gbps',
|
||||
sessions: '1,234',
|
||||
shareValue: 12,
|
||||
},
|
||||
{
|
||||
name: 'DNS',
|
||||
port: '53',
|
||||
inbound: '0.2 Gbps',
|
||||
outbound: '0.2 Gbps',
|
||||
sessions: '89,012',
|
||||
shareValue: 5,
|
||||
},
|
||||
])
|
||||
const topApplications = ref<any[]>([])
|
||||
|
||||
// 应用流量表格列
|
||||
const applicationColumns: TableColumnData[] = [
|
||||
@@ -439,48 +406,7 @@ const applicationColumns: TableColumnData[] = [
|
||||
]
|
||||
|
||||
// Top 流量源数据
|
||||
const topSources = ref([
|
||||
{
|
||||
ip: '192.168.1.100',
|
||||
name: 'Web-Server-01',
|
||||
traffic: '2.8 Gbps',
|
||||
sessions: '45,678',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.101',
|
||||
name: 'Web-Server-02',
|
||||
traffic: '2.4 Gbps',
|
||||
sessions: '38,456',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.2.50',
|
||||
name: 'API-Gateway',
|
||||
traffic: '1.8 Gbps',
|
||||
sessions: '28,901',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
{
|
||||
ip: '192.168.3.10',
|
||||
name: 'DB-Master',
|
||||
traffic: '1.5 Gbps',
|
||||
sessions: '12,345',
|
||||
statusColor: 'orange',
|
||||
statusText: '高负载',
|
||||
},
|
||||
{
|
||||
ip: '192.168.1.200',
|
||||
name: 'Cache-Server',
|
||||
traffic: '1.2 Gbps',
|
||||
sessions: '56,789',
|
||||
statusColor: 'green',
|
||||
statusText: '在线',
|
||||
},
|
||||
])
|
||||
const topSources = ref<any[]>([])
|
||||
|
||||
// 流量源表格列
|
||||
const sourceColumns: TableColumnData[] = [
|
||||
@@ -490,6 +416,262 @@ const sourceColumns: TableColumnData[] = [
|
||||
{ title: '会话数', dataIndex: 'sessions', width: 90 },
|
||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 80, align: 'center' },
|
||||
]
|
||||
|
||||
const toDateTime = (date: Date) => {
|
||||
const pad = (v: number) => `${v}`.padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
const getGranularity = () => {
|
||||
if (timeRange.value === '24h') return 'minute'
|
||||
if (timeRange.value === '7d') return 'hour'
|
||||
return 'day'
|
||||
}
|
||||
|
||||
const getRangeParams = () => {
|
||||
const now = new Date()
|
||||
const start = new Date(now)
|
||||
if (timeRange.value === '7d') start.setDate(start.getDate() - 7)
|
||||
else if (timeRange.value === '30d') start.setDate(start.getDate() - 30)
|
||||
else start.setDate(start.getDate() - 1)
|
||||
return { start_time: toDateTime(start), end_time: toDateTime(now) }
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes) return '0 B'
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let value = bytes
|
||||
let index = 0
|
||||
while (value >= 1024 && index < units.length - 1) {
|
||||
value /= 1024
|
||||
index += 1
|
||||
}
|
||||
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[index]}`
|
||||
}
|
||||
|
||||
const formatBps = (bps: number) => {
|
||||
if (!bps) return '0 bps'
|
||||
const units = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps']
|
||||
let value = bps
|
||||
let index = 0
|
||||
while (value >= 1000 && index < units.length - 1) {
|
||||
value /= 1000
|
||||
index += 1
|
||||
}
|
||||
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[index]}`
|
||||
}
|
||||
|
||||
const updateTrendChart = () => {
|
||||
const labels = trafficTrendData.value.map((item: any) => {
|
||||
const raw = item?.time || item?.time_point || ''
|
||||
if (!raw) return ''
|
||||
const normalized = String(raw).replace('T', ' ')
|
||||
if (timeRange.value === '24h') return normalized.slice(11, 16)
|
||||
return normalized.slice(5, 16)
|
||||
})
|
||||
const inbound = trafficTrendData.value.map((item) => Number(item.in_bytes || 0))
|
||||
const outbound = trafficTrendData.value.map((item) => Number(item.out_bytes || 0))
|
||||
trafficChartOptions.value = {
|
||||
...trafficChartOptions.value,
|
||||
xAxis: { ...(trafficChartOptions.value as any).xAxis, data: labels },
|
||||
series: [
|
||||
{ ...(trafficChartOptions.value as any).series[0], data: inbound },
|
||||
{ ...(trafficChartOptions.value as any).series[1], data: outbound },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonObject = (raw?: string) => {
|
||||
if (!raw) return null
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const loadTrafficData = async () => {
|
||||
const params = getRangeParams()
|
||||
const granularity = getGranularity()
|
||||
try {
|
||||
const [summaryRes, realtimeRes, trendRes, topRes, listRes, dashboardRes] = await Promise.all([
|
||||
fetchTrafficSummary(params),
|
||||
fetchRealtimeTraffic(params),
|
||||
fetchTrafficTrend({ ...params, granularity }),
|
||||
fetchTopTraffic({ ...params, granularity, order_by: 'total_bytes', limit: 5 }),
|
||||
fetchTrafficList({ ...params, page: 1, size: 1 }),
|
||||
fetchTrafficDashboard({ ...params, granularity, limit: 5 }),
|
||||
])
|
||||
|
||||
const summary = summaryRes?.details
|
||||
const realtime = realtimeRes?.details
|
||||
const trendList = trendRes?.details?.data || []
|
||||
const topList = topRes?.details?.data || []
|
||||
const latest = listRes?.details?.data?.[0]
|
||||
const dashboard = dashboardRes?.details
|
||||
|
||||
currentBandwidth.value = formatBps(Number(realtime?.bandwidth || summary?.peak_bandwidth || 0))
|
||||
inboundTraffic.value = formatBytes(Number(summary?.total_in_bytes || 0))
|
||||
outboundTraffic.value = formatBytes(Number(summary?.total_out_bytes || 0))
|
||||
activeSessions.value = `${Number(realtime?.connections || summary?.total_connections || 0).toLocaleString()}`
|
||||
const inBytes = Number(summary?.total_in_bytes || 0)
|
||||
const outBytes = Number(summary?.total_out_bytes || 0)
|
||||
const allBytes = inBytes + outBytes
|
||||
const summaryInboundPercent = allBytes > 0 ? Math.round((inBytes / allBytes) * 100) : 0
|
||||
const summaryOutboundPercent = allBytes > 0 ? Math.round((outBytes / allBytes) * 100) : 0
|
||||
const dashboardInPercent = Number(dashboard?.direction?.inbound_percent || 0)
|
||||
const dashboardOutPercent = Number(dashboard?.direction?.outbound_percent || 0)
|
||||
if (dashboardInPercent + dashboardOutPercent > 0) {
|
||||
inboundPercent.value = dashboardInPercent
|
||||
outboundPercent.value = dashboardOutPercent
|
||||
} else {
|
||||
inboundPercent.value = summaryInboundPercent
|
||||
outboundPercent.value = summaryOutboundPercent
|
||||
}
|
||||
|
||||
trafficTrendData.value = trendList
|
||||
updateTrendChart()
|
||||
|
||||
if (dashboard?.top_sources?.length) {
|
||||
topSources.value = dashboard.top_sources.map((item) => ({
|
||||
ip: item.ip || '-',
|
||||
name: item.name || item.ip || '-',
|
||||
traffic: formatBytes(Number(item.total_bytes || 0)),
|
||||
sessions: Number(item.total_packets || 0).toLocaleString(),
|
||||
statusColor: Number(item.total_bytes || 0) > 0 ? 'green' : 'gray',
|
||||
statusText: Number(item.total_bytes || 0) > 0 ? '活跃' : '空闲',
|
||||
}))
|
||||
} else {
|
||||
topSources.value = topList.map((item: any) => ({
|
||||
ip: item.node_id || '-',
|
||||
name: item.node_id || '-',
|
||||
traffic: formatBytes(Number(item.total_bytes || 0)),
|
||||
sessions: Number(item.total_packets || 0).toLocaleString(),
|
||||
statusColor: Number(item.total_bytes || 0) > 0 ? 'green' : 'gray',
|
||||
statusText: Number(item.total_bytes || 0) > 0 ? '活跃' : '空闲',
|
||||
}))
|
||||
}
|
||||
|
||||
if (dashboard?.bandwidth?.length) {
|
||||
bandwidthData.value = dashboard.bandwidth.map((item) => ({
|
||||
name: item.name || '-',
|
||||
percent: Number(item.percent || 0),
|
||||
}))
|
||||
} else {
|
||||
const maxTraffic = Math.max(...topList.map((item: any) => Number(item.total_bytes || 0)), 0)
|
||||
bandwidthData.value = topList.map((item: any) => {
|
||||
const bytes = Number(item.total_bytes || 0)
|
||||
return {
|
||||
name: item.node_id || '-',
|
||||
percent: maxTraffic > 0 ? Math.round((bytes / maxTraffic) * 100) : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (dashboard?.protocols?.length) {
|
||||
protocolData.value = dashboard.protocols.map((item, index) => ({
|
||||
name: item.name,
|
||||
percent: Number(item.percent || 0),
|
||||
color: ['#165DFF', '#14C9C9', '#F7BA1E', '#86909C'][index % 4],
|
||||
}))
|
||||
}
|
||||
|
||||
if (dashboard?.top_applications?.length) {
|
||||
topApplications.value = dashboard.top_applications.map((item) => ({
|
||||
name: item.name,
|
||||
port: item.port || '-',
|
||||
inbound: formatBytes(Number(item.in_bytes || 0)),
|
||||
outbound: formatBytes(Number(item.out_bytes || 0)),
|
||||
sessions: Number(item.sessions || 0).toLocaleString(),
|
||||
shareValue: Number(item.share_percent || 0),
|
||||
}))
|
||||
}
|
||||
|
||||
if (!dashboard?.protocols?.length && latest?.protocol_stats) {
|
||||
const protocol = parseJsonObject(latest.protocol_stats)
|
||||
if (protocol && typeof protocol === 'object') {
|
||||
const total = Object.values(protocol).reduce((sum, v) => sum + Number(v || 0), 0)
|
||||
if (total > 0) {
|
||||
protocolData.value = Object.entries(protocol).map(([name, value], index) => ({
|
||||
name,
|
||||
percent: Math.round((Number(value) / total) * 100),
|
||||
color: ['#165DFF', '#14C9C9', '#F7BA1E', '#86909C'][index % 4],
|
||||
}))
|
||||
topApplications.value = protocolData.value.map((item) => {
|
||||
const bytes = Math.round((allBytes * item.percent) / 100)
|
||||
return {
|
||||
name: item.name,
|
||||
port: '-',
|
||||
inbound: formatBytes(Math.round((bytes * inboundPercent.value) / 100)),
|
||||
outbound: formatBytes(Math.round((bytes * outboundPercent.value) / 100)),
|
||||
sessions: activeSessions.value,
|
||||
shareValue: item.percent,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('流量数据加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadRuntimeStatus = async () => {
|
||||
try {
|
||||
const res = await fetchTrafficRuntimeStatus()
|
||||
const status = String(res?.details?.status || 'stopped')
|
||||
trafficRuntimeStatus.value = status === 'running' ? 'running' : 'stopped'
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
trafficRuntimeStatus.value = 'stopped'
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartRuntime = async () => {
|
||||
runtimeActionLoading.value = true
|
||||
try {
|
||||
const res = await startTrafficRuntime()
|
||||
if (res?.code === 0 || res?.code === 200) {
|
||||
Message.success(res?.details?.message || '流量采集器已启动')
|
||||
} else {
|
||||
Message.error(res?.message || '启动失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('启动失败')
|
||||
} finally {
|
||||
runtimeActionLoading.value = false
|
||||
await loadRuntimeStatus()
|
||||
}
|
||||
}
|
||||
|
||||
const handleStopRuntime = async () => {
|
||||
runtimeActionLoading.value = true
|
||||
try {
|
||||
const res = await stopTrafficRuntime()
|
||||
if (res?.code === 0 || res?.code === 200) {
|
||||
Message.success(res?.details?.message || '流量采集器已停止')
|
||||
} else {
|
||||
Message.error(res?.message || '停止失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Message.error('停止失败')
|
||||
} finally {
|
||||
runtimeActionLoading.value = false
|
||||
await loadRuntimeStatus()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRuntimeStatus()
|
||||
loadTrafficData()
|
||||
})
|
||||
|
||||
watch(timeRange, () => {
|
||||
loadTrafficData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -675,6 +857,7 @@ export default {
|
||||
:deep(.arco-card-body) {
|
||||
padding: 16px;
|
||||
height: calc(320px - 60px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
@@ -707,8 +890,9 @@ export default {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.protocol-item {
|
||||
@@ -737,8 +921,9 @@ export default {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.direction-item {
|
||||
@@ -820,8 +1005,9 @@ export default {
|
||||
padding: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.bandwidth-item {
|
||||
|
||||
Reference in New Issue
Block a user