fix: 验收修正
This commit is contained in:
@@ -46,7 +46,7 @@ export const fetchIpScanList = (params?: { page?: number; size?: number; keyword
|
|||||||
export const fetchIpScanDetail = (id: number) =>
|
export const fetchIpScanDetail = (id: number) =>
|
||||||
request.get<{ code: number; details?: IpScanTask; message?: string }>(`/DC-Control/v1/ipscans/${id}`)
|
request.get<{ code: number; details?: IpScanTask; message?: string }>(`/DC-Control/v1/ipscans/${id}`)
|
||||||
|
|
||||||
/** 触发一次扫描,运行记录进入指定采集节点的等待队列。 */
|
/** 触发一次扫描,由独立网络扫描服务执行。 */
|
||||||
export const triggerIpScan = (id: number) =>
|
export const triggerIpScan = (id: number) =>
|
||||||
request.post<{ code: number; details?: { message?: string }; message?: string }>(`/DC-Control/v1/ipscans/${id}/trigger`)
|
request.post<{ code: number; details?: { message?: string }; message?: string }>(`/DC-Control/v1/ipscans/${id}/trigger`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,116 @@
|
|||||||
import { request } from '@/api/request'
|
import { request } from '@/api/request'
|
||||||
|
import type { NetworkDeviceService } from '@/api/ops/network-device'
|
||||||
|
|
||||||
export const fetchNetworkScreenOverview = () => request.get('/DC-Control/v1/network-screen/overview')
|
export interface NetworkScreenResponse<T> {
|
||||||
|
code?: number
|
||||||
|
details?: T
|
||||||
|
message?: string
|
||||||
|
timeseq?: number
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenHealthMatrix = (params?: { window?: string }) =>
|
export interface NetworkScreenOverview {
|
||||||
request.get('/DC-Control/v1/network-screen/health-matrix', { params })
|
total_devices: number
|
||||||
|
online_devices: number
|
||||||
|
abnormal_devices: number
|
||||||
|
avg_response_time_ms: number
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenFreshnessDistribution = () => request.get('/DC-Control/v1/network-screen/freshness-distribution')
|
export interface NetworkHealthMatrixItem {
|
||||||
|
error_bucket: string
|
||||||
|
latency_bucket: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenProtocolDistribution = () => request.get('/DC-Control/v1/network-screen/protocol-distribution')
|
export interface NetworkFreshnessItem {
|
||||||
|
bucket_label: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenResourceOverview = (params?: { window?: string }) =>
|
export interface NetworkProtocolItem {
|
||||||
request.get('/DC-Control/v1/network-screen/resource-overview', { params })
|
protocol: string
|
||||||
|
snmp_version: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenAlertsStream = (params?: { limit?: number; window?: string }) =>
|
export interface NetworkDistributionItem {
|
||||||
request.get('/DC-Control/v1/network-screen/alerts-stream', { params })
|
name: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchNetworkScreenDevices = (params?: {
|
export interface NetworkInterfaceSummary {
|
||||||
|
latest_timestamp: string
|
||||||
|
total: number
|
||||||
|
up: number
|
||||||
|
down: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkArpSummary {
|
||||||
|
latest_timestamp: string
|
||||||
|
total: number
|
||||||
|
unique_ip_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkRouteSummary {
|
||||||
|
latest_timestamp: string
|
||||||
|
total: number
|
||||||
|
protocol_share: NetworkDistributionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkFibSummary {
|
||||||
|
latest_timestamp: string
|
||||||
|
total: number
|
||||||
|
vendor_share: NetworkDistributionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkResourceOverview {
|
||||||
|
interface_summary: NetworkInterfaceSummary
|
||||||
|
arp_summary: NetworkArpSummary
|
||||||
|
route_summary: NetworkRouteSummary
|
||||||
|
fib_summary: NetworkFibSummary
|
||||||
|
meta: { count: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkContinuousErrorItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
status: string
|
||||||
|
continuous_errors: number
|
||||||
|
last_check_time: string
|
||||||
|
status_message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkOfflineItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
host: string
|
||||||
|
last_offline_time: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkStatusHotspotItem {
|
||||||
|
status_message: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkAlertsStream {
|
||||||
|
top_continuous_errors: NetworkContinuousErrorItem[]
|
||||||
|
recent_offline: NetworkOfflineItem[]
|
||||||
|
status_message_hotspots: NetworkStatusHotspotItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkScreenDevice extends NetworkDeviceService {
|
||||||
|
protocol: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkDevicePage {
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
size: number
|
||||||
|
data: NetworkScreenDevice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkDeviceQuery {
|
||||||
page?: number
|
page?: number
|
||||||
size?: number
|
size?: number
|
||||||
keyword?: string
|
keyword?: string
|
||||||
@@ -23,4 +118,27 @@ export const fetchNetworkScreenDevices = (params?: {
|
|||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
protocol?: string
|
protocol?: string
|
||||||
vendor?: string
|
vendor?: string
|
||||||
}) => request.get('/DC-Control/v1/network-screen/devices', { params })
|
}
|
||||||
|
|
||||||
|
export const fetchNetworkScreenOverview = () =>
|
||||||
|
request.get<NetworkScreenResponse<NetworkScreenOverview>>('/DC-Control/v1/network-screen/overview')
|
||||||
|
|
||||||
|
export const fetchNetworkScreenHealthMatrix = (params?: { window?: string }) =>
|
||||||
|
request.get<NetworkScreenResponse<{ window: string; data: NetworkHealthMatrixItem[] }>>('/DC-Control/v1/network-screen/health-matrix', {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const fetchNetworkScreenFreshnessDistribution = () =>
|
||||||
|
request.get<NetworkScreenResponse<{ data: NetworkFreshnessItem[] }>>('/DC-Control/v1/network-screen/freshness-distribution')
|
||||||
|
|
||||||
|
export const fetchNetworkScreenProtocolDistribution = () =>
|
||||||
|
request.get<NetworkScreenResponse<{ data: NetworkProtocolItem[] }>>('/DC-Control/v1/network-screen/protocol-distribution')
|
||||||
|
|
||||||
|
export const fetchNetworkScreenResourceOverview = (params?: { window?: string }) =>
|
||||||
|
request.get<NetworkScreenResponse<NetworkResourceOverview>>('/DC-Control/v1/network-screen/resource-overview', { params })
|
||||||
|
|
||||||
|
export const fetchNetworkScreenAlertsStream = (params?: { limit?: number; window?: string }) =>
|
||||||
|
request.get<NetworkScreenResponse<NetworkAlertsStream>>('/DC-Control/v1/network-screen/alerts-stream', { params })
|
||||||
|
|
||||||
|
export const fetchNetworkScreenDevices = (params?: NetworkDeviceQuery) =>
|
||||||
|
request.get<NetworkScreenResponse<NetworkDevicePage>>('/DC-Control/v1/network-screen/devices', { params })
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
<a-col v-for="(item, index) in metricsData.metrics" :key="`${item.metric_name}-${item.series_key || index}`" :span="8">
|
<a-col v-for="(item, index) in metricsData.metrics" :key="`${item.metric_name}-${item.series_key || index}`" :span="8">
|
||||||
<a-card class="metric-card" :bordered="false">
|
<a-card class="metric-card" :bordered="false">
|
||||||
<div class="metric-name">{{ item.metric_name }}</div>
|
<div class="metric-name">{{ metricDisplayName(item.metric_name) }}</div>
|
||||||
<div v-if="metricSeriesLabel(item)" class="metric-series">{{ metricSeriesLabel(item) }}</div>
|
<div v-if="metricSeriesLabel(item)" class="metric-series">{{ metricSeriesLabel(item) }}</div>
|
||||||
<div class="metric-value">{{ item.metric_value }} {{ item.metric_unit || '' }}</div>
|
<div class="metric-value">{{ item.metric_value }} {{ item.metric_unit || '' }}</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -117,6 +117,7 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import { IconEdit, IconDelete, IconDashboard } from '@arco-design/web-vue/es/icon'
|
import { IconEdit, IconDelete, IconDashboard } from '@arco-design/web-vue/es/icon'
|
||||||
import Chart from '@/components/chart/index.vue'
|
import Chart from '@/components/chart/index.vue'
|
||||||
import ImportantInterfacesPanel from '../../components/ImportantInterfacesPanel.vue'
|
import ImportantInterfacesPanel from '../../components/ImportantInterfacesPanel.vue'
|
||||||
|
import { fetchControlMetricDefinitionOptions } from '@/api/ops/dcControl'
|
||||||
import {
|
import {
|
||||||
SECURITY_TYPE_MAP,
|
SECURITY_TYPE_MAP,
|
||||||
STATUS_MAP,
|
STATUS_MAP,
|
||||||
@@ -139,6 +140,7 @@ defineEmits(['edit', 'delete'])
|
|||||||
const metricsVisible = ref(false)
|
const metricsVisible = ref(false)
|
||||||
const metricsLoading = ref(false)
|
const metricsLoading = ref(false)
|
||||||
const metricsData = ref<SecurityMetricsLatestData | null>(null)
|
const metricsData = ref<SecurityMetricsLatestData | null>(null)
|
||||||
|
const metricDefinitionNames = ref<Record<string, string>>({})
|
||||||
const selectedMetric = ref('')
|
const selectedMetric = ref('')
|
||||||
const trendLoading = ref(false)
|
const trendLoading = ref(false)
|
||||||
const trendRows = ref<SecurityMetric[]>([])
|
const trendRows = ref<SecurityMetric[]>([])
|
||||||
@@ -152,7 +154,7 @@ const accessPointColumns = [
|
|||||||
|
|
||||||
const metricOptions = computed(() => {
|
const metricOptions = computed(() => {
|
||||||
const names = new Set((metricsData.value?.metrics || []).map((item) => item.metric_name))
|
const names = new Set((metricsData.value?.metrics || []).map((item) => item.metric_name))
|
||||||
return [...names].sort().map((value) => ({ label: value, value }))
|
return [...names].sort().map((value) => ({ label: metricDisplayName(value), value }))
|
||||||
})
|
})
|
||||||
|
|
||||||
const trendChartOptions = computed(() => {
|
const trendChartOptions = computed(() => {
|
||||||
@@ -191,6 +193,7 @@ watch(
|
|||||||
async (id) => {
|
async (id) => {
|
||||||
accessPoints.value = []
|
accessPoints.value = []
|
||||||
metricsData.value = null
|
metricsData.value = null
|
||||||
|
metricDefinitionNames.value = {}
|
||||||
selectedMetric.value = ''
|
selectedMetric.value = ''
|
||||||
trendRows.value = []
|
trendRows.value = []
|
||||||
if (!id) return
|
if (!id) return
|
||||||
@@ -219,8 +222,17 @@ const handleViewMetrics = async () => {
|
|||||||
metricsVisible.value = true
|
metricsVisible.value = true
|
||||||
metricsLoading.value = true
|
metricsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response: any = await fetchSecurityMetricsLatest(props.record.resource_uid)
|
const [definitionsResponse, metricsResponse] = await Promise.all([
|
||||||
metricsData.value = response?.details || null
|
fetchControlMetricDefinitionOptions({ resource_category: 'security', data_source: 'dc-security' }),
|
||||||
|
fetchSecurityMetricsLatest(props.record.resource_uid),
|
||||||
|
])
|
||||||
|
if (definitionsResponse.code !== undefined && definitionsResponse.code !== 0) {
|
||||||
|
throw new Error(definitionsResponse.message || '获取安全设备指标定义失败')
|
||||||
|
}
|
||||||
|
metricDefinitionNames.value = Object.fromEntries(
|
||||||
|
(definitionsResponse.details?.list || []).map((item) => [item.metric_code, item.metric_name])
|
||||||
|
)
|
||||||
|
metricsData.value = metricsResponse.details || null
|
||||||
selectedMetric.value = metricOptions.value[0]?.value || ''
|
selectedMetric.value = metricOptions.value[0]?.value || ''
|
||||||
if (selectedMetric.value) await loadTrend()
|
if (selectedMetric.value) await loadTrend()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -273,9 +285,11 @@ const metricSeriesLabel = (metric: SecurityMetric) => {
|
|||||||
const trendSeriesName = (seriesKey: string, metric?: SecurityMetric) => {
|
const trendSeriesName = (seriesKey: string, metric?: SecurityMetric) => {
|
||||||
const label = metric ? metricSeriesLabel(metric) : ''
|
const label = metric ? metricSeriesLabel(metric) : ''
|
||||||
if (label) return label
|
if (label) return label
|
||||||
return seriesKey === 'default' ? selectedMetric.value : seriesKey.slice(0, 8)
|
return seriesKey === 'default' ? metricDisplayName(selectedMetric.value) : seriesKey.slice(0, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metricDisplayName = (metricName: string) => metricDefinitionNames.value[metricName]
|
||||||
|
|
||||||
const formatTime = (time?: string | null) => {
|
const formatTime = (time?: string | null) => {
|
||||||
if (!time) return '-'
|
if (!time) return '-'
|
||||||
if (time.startsWith('0001-01-01')) return '-'
|
if (time.startsWith('0001-01-01')) return '-'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -93,20 +93,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="12" :lg="4">
|
|
||||||
<a-card class="stats-card" :bordered="false">
|
|
||||||
<div class="stats-content">
|
|
||||||
<div class="stats-icon stats-icon-success">
|
|
||||||
<icon-folder />
|
|
||||||
</div>
|
|
||||||
<div class="stats-info">
|
|
||||||
<div class="stats-title">厂商适配</div>
|
|
||||||
<div class="stats-value">{{ monitorSummary?.current?.adapter_status === 'verified' ? '已验证' : '待联调' }}</div>
|
|
||||||
<div class="stats-desc">{{ monitorSummary?.current?.product_version || '版本待采集' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-card>
|
|
||||||
</a-col>
|
|
||||||
<a-col :xs="24" :sm="12" :lg="4">
|
<a-col :xs="24" :sm="12" :lg="4">
|
||||||
<a-card class="stats-card" :bordered="false">
|
<a-card class="stats-card" :bordered="false">
|
||||||
<div class="stats-content">
|
<div class="stats-content">
|
||||||
@@ -152,40 +138,92 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :lg="14" class="chart-pair-col">
|
<a-col :xs="24" :lg="14" class="chart-pair-col">
|
||||||
<a-card :bordered="false" class="chart-pair-card trend-side-card">
|
<div class="storage-metrics-stack">
|
||||||
<template #title>
|
<a-card :bordered="false" class="capacity-overview-card">
|
||||||
<div class="card-header">
|
<template #title>
|
||||||
<div class="card-title">存储指标趋势</div>
|
<div class="card-header">
|
||||||
<div class="card-subtitle">近 24 小时 · ECharts 折线图(原始样本按小时取 max 聚合展示)</div>
|
<div class="card-title">容量概览</div>
|
||||||
</div>
|
<div class="card-subtitle">总容量、已用容量、可用容量</div>
|
||||||
</template>
|
</div>
|
||||||
<template #extra>
|
</template>
|
||||||
<a-space size="small" wrap align="center">
|
<div v-if="capacitySummary" class="capacity-overview-content">
|
||||||
<span class="text-muted">指标</span>
|
<div class="capacity-gauge">
|
||||||
<a-select
|
<svg viewBox="0 0 120 120" aria-hidden="true">
|
||||||
v-model="trendSelectedMetric"
|
<circle class="capacity-gauge-track" cx="60" cy="60" r="48" />
|
||||||
class="trend-metric-select"
|
<circle
|
||||||
placeholder="选择指标"
|
class="capacity-gauge-value"
|
||||||
allow-search
|
cx="60"
|
||||||
:loading="metricsOptionsLoading"
|
cy="60"
|
||||||
:options="trendMetricOptions"
|
r="48"
|
||||||
:disabled="!trendMetricOptions.length"
|
pathLength="100"
|
||||||
@change="refreshTimeseries"
|
:stroke-dasharray="`${capacitySummary.usagePercent} ${100 - capacitySummary.usagePercent}`"
|
||||||
/>
|
/>
|
||||||
<a-button type="outline" size="small" @click="refreshTimeseries">刷新曲线</a-button>
|
</svg>
|
||||||
</a-space>
|
<div class="capacity-gauge-text">
|
||||||
</template>
|
<strong>{{ capacitySummary.usagePercent.toFixed(1) }}%</strong>
|
||||||
<a-spin :loading="chartLoading" class="chart-spin chart-spin--fill">
|
<span>已使用</span>
|
||||||
<div class="chart-content-wrapper">
|
</div>
|
||||||
<p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p>
|
</div>
|
||||||
<div class="chart-container chart-container--line">
|
<div class="capacity-values">
|
||||||
<!-- Chart 为 ECharts 封装;单序列 type: 'line' 折线 -->
|
<div class="capacity-value-item">
|
||||||
<Chart v-if="hasChartSeries" :options="ioChartOptions" />
|
<span class="capacity-value-label capacity-value-label--total">总容量</span>
|
||||||
<a-empty v-else description="暂无趋势数据或指标未上报" />
|
<strong>{{ formatBytes(capacitySummary.totalBytes) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="capacity-value-item">
|
||||||
|
<span class="capacity-value-label capacity-value-label--used">已使用</span>
|
||||||
|
<strong>{{ formatBytes(capacitySummary.usedBytes) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="capacity-value-item">
|
||||||
|
<span class="capacity-value-label capacity-value-label--free">可用容量</span>
|
||||||
|
<strong>{{ formatBytes(capacitySummary.freeBytes) }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
<a-empty v-else description="暂无完整容量指标数据" />
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
|
<a-card v-if="keyMetrics.length" :bordered="false" class="key-metrics-card">
|
||||||
|
<div class="key-metrics-list">
|
||||||
|
<div v-for="metric in keyMetrics" :key="metric.name" class="key-metric-item">
|
||||||
|
<span class="key-metric-name">{{ metric.label }}</span>
|
||||||
|
<strong>{{ formatMetricValue(metric.value, metric.unit) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<a-card :bordered="false" class="storage-trend-card">
|
||||||
|
<template #title>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-title">存储指标趋势</div>
|
||||||
|
<div class="card-subtitle">近 24 小时 · 原始样本按小时取最大值</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-space size="small" wrap align="center">
|
||||||
|
<span class="text-muted">指标</span>
|
||||||
|
<a-select
|
||||||
|
v-model="trendSelectedMetric"
|
||||||
|
class="trend-metric-select"
|
||||||
|
placeholder="选择指标"
|
||||||
|
:loading="metricsOptionsLoading"
|
||||||
|
:options="trendMetricOptions"
|
||||||
|
:disabled="!trendMetricOptions.length"
|
||||||
|
@change="refreshTimeseries"
|
||||||
|
/>
|
||||||
|
<a-button type="outline" size="small" @click="refreshTimeseries">刷新曲线</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-spin :loading="chartLoading" class="chart-spin chart-spin--fill">
|
||||||
|
<div class="chart-content-wrapper">
|
||||||
|
<p v-if="chartHint" class="chart-hint text-muted">{{ chartHint }}</p>
|
||||||
|
<div class="chart-container chart-container--line">
|
||||||
|
<Chart v-if="hasChartSeries" :options="ioChartOptions" />
|
||||||
|
<a-empty v-else description="暂无趋势数据或指标未上报" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</a-card>
|
||||||
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,23 +235,55 @@ import dayjs from 'dayjs'
|
|||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { getSystemParameter, SystemParameterCode } from '@/composables/systemParameters'
|
import { getSystemParameter, SystemParameterCode } from '@/composables/systemParameters'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import { IconDashboard, IconStorage, IconCodeSquare, IconDriveFile, IconFolder, IconCheckCircleFill } from '@arco-design/web-vue/es/icon'
|
import { IconDashboard, IconStorage, IconCodeSquare, IconDriveFile, IconCheckCircleFill } from '@arco-design/web-vue/es/icon'
|
||||||
import Chart from '@/components/chart/index.vue'
|
import Chart from '@/components/chart/index.vue'
|
||||||
import {
|
import {
|
||||||
fetchStorageList,
|
fetchStorageList,
|
||||||
fetchStorageMonitorList,
|
fetchStorageMonitorList,
|
||||||
fetchStorageMonitorSummary,
|
fetchStorageMonitorSummary,
|
||||||
fetchStorageMonitorCollectedMetrics,
|
|
||||||
fetchStorageMetricsLatest,
|
fetchStorageMetricsLatest,
|
||||||
fetchStorageMetricsTimeseries,
|
fetchStorageMetricsTimeseries,
|
||||||
type StorageItem,
|
type StorageItem,
|
||||||
|
type StorageMetricItem,
|
||||||
type StorageMonitorOptionItem,
|
type StorageMonitorOptionItem,
|
||||||
type StorageMonitorSummaryPayload,
|
type StorageMonitorSummaryPayload,
|
||||||
} from '@/api/ops/storage'
|
} from '@/api/ops/storage'
|
||||||
|
|
||||||
/** 趋势图当前选中的指标名(选项来自「按设备 id 最新一批」接口,无 id 时回退 latest) */
|
const storageMetricMeta = {
|
||||||
|
'storage.capacity.total_bytes': { label: '总容量', unit: 'B' },
|
||||||
|
'storage.capacity.used_bytes': { label: '已用容量', unit: 'B' },
|
||||||
|
'storage.capacity.free_bytes': { label: '可用容量', unit: 'B' },
|
||||||
|
'storage.cluster.host_count': { label: '存储集群主机数', unit: '台' },
|
||||||
|
'storage.pool.capacity_count': { label: '容量池数量', unit: '个' },
|
||||||
|
'storage.disk.ssd_used_count': { label: '已用 SSD 数量', unit: '个' },
|
||||||
|
'storage.network.manage_abnormal_count': { label: '管理网络异常数', unit: '个' },
|
||||||
|
'storage.object.read_bandwidth': { label: '对象存储读取带宽', unit: 'B/s' },
|
||||||
|
'storage.object.read_latency': { label: '对象存储读取延迟', unit: 'ms' },
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type StorageMetricName = keyof typeof storageMetricMeta
|
||||||
|
|
||||||
|
const capacityMetricNames: StorageMetricName[] = [
|
||||||
|
'storage.capacity.total_bytes',
|
||||||
|
'storage.capacity.used_bytes',
|
||||||
|
'storage.capacity.free_bytes',
|
||||||
|
]
|
||||||
|
const keyMetricNames: StorageMetricName[] = [
|
||||||
|
'storage.cluster.host_count',
|
||||||
|
'storage.pool.capacity_count',
|
||||||
|
'storage.disk.ssd_used_count',
|
||||||
|
'storage.network.manage_abnormal_count',
|
||||||
|
]
|
||||||
|
const trendMetricNames: StorageMetricName[] = [
|
||||||
|
'storage.capacity.free_bytes',
|
||||||
|
'storage.capacity.total_bytes',
|
||||||
|
'storage.capacity.used_bytes',
|
||||||
|
'storage.object.read_bandwidth',
|
||||||
|
'storage.object.read_latency',
|
||||||
|
]
|
||||||
|
|
||||||
const trendSelectedMetric = ref('')
|
const trendSelectedMetric = ref('')
|
||||||
const trendMetricOptions = ref<{ label: string; value: string }[]>([])
|
const trendMetricOptions = ref<{ label: string; value: string; unit: string }[]>([])
|
||||||
const metricsOptionsLoading = ref(false)
|
const metricsOptionsLoading = ref(false)
|
||||||
|
|
||||||
const dropdownLoading = ref(false)
|
const dropdownLoading = ref(false)
|
||||||
@@ -225,12 +295,53 @@ const storageOptions = ref<StorageMonitorOptionItem[]>([])
|
|||||||
const selectedResourceUID = ref<string | undefined>(undefined)
|
const selectedResourceUID = ref<string | undefined>(undefined)
|
||||||
|
|
||||||
const monitorSummary = ref<StorageMonitorSummaryPayload | null>(null)
|
const monitorSummary = ref<StorageMonitorSummaryPayload | null>(null)
|
||||||
|
const latestStorageMetrics = ref<StorageMetricItem[]>([])
|
||||||
|
|
||||||
const chartSeriesPoints = ref<{ time: string; value: number }[]>([])
|
const chartSeriesPoints = ref<{ time: string; value: number }[]>([])
|
||||||
|
|
||||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
const activeStorage = computed(() => storageList.value.find((s) => s.resource_uid === selectedResourceUID.value))
|
const activeStorage = computed(() => storageList.value.find((s) => s.resource_uid === selectedResourceUID.value))
|
||||||
|
const latestMetricByName = computed(() => new Map(latestStorageMetrics.value.map((metric) => [metric.metric_name, metric])))
|
||||||
|
|
||||||
|
const capacitySummary = computed(() => {
|
||||||
|
const totalBytes = latestMetricByName.value.get('storage.capacity.total_bytes')?.metric_value
|
||||||
|
const usedBytes = latestMetricByName.value.get('storage.capacity.used_bytes')?.metric_value
|
||||||
|
const freeBytes = latestMetricByName.value.get('storage.capacity.free_bytes')?.metric_value
|
||||||
|
if (
|
||||||
|
totalBytes === undefined ||
|
||||||
|
usedBytes === undefined ||
|
||||||
|
freeBytes === undefined ||
|
||||||
|
!Number.isFinite(totalBytes) ||
|
||||||
|
!Number.isFinite(usedBytes) ||
|
||||||
|
!Number.isFinite(freeBytes) ||
|
||||||
|
totalBytes <= 0
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
totalBytes,
|
||||||
|
usedBytes,
|
||||||
|
freeBytes,
|
||||||
|
usagePercent: (usedBytes / totalBytes) * 100,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyMetrics = computed(() => {
|
||||||
|
const metrics: Array<{ name: StorageMetricName; label: string; unit: string; value: number }> = []
|
||||||
|
for (const name of keyMetricNames) {
|
||||||
|
const metric = latestMetricByName.value.get(name)
|
||||||
|
if (!metric || !Number.isFinite(metric.metric_value)) continue
|
||||||
|
const meta = storageMetricMeta[name]
|
||||||
|
metrics.push({
|
||||||
|
name,
|
||||||
|
label: meta.label,
|
||||||
|
unit: metric.metric_unit || meta.unit,
|
||||||
|
value: metric.metric_value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return metrics
|
||||||
|
})
|
||||||
|
|
||||||
const globalHint = computed(() => {
|
const globalHint = computed(() => {
|
||||||
const m = monitorSummary.value
|
const m = monitorSummary.value
|
||||||
@@ -252,6 +363,27 @@ function formatDateTime(v: string | undefined) {
|
|||||||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : v
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number): string {
|
||||||
|
if (!Number.isFinite(value) || value < 0) return '-'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let unitIndex = 0
|
||||||
|
let result = value
|
||||||
|
while (result >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
result /= 1024
|
||||||
|
unitIndex += 1
|
||||||
|
}
|
||||||
|
const digits = unitIndex === 0 ? 0 : result >= 10 ? 1 : 2
|
||||||
|
return `${result.toFixed(digits)} ${units[unitIndex]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMetricValue(value: number, unit: string): string {
|
||||||
|
if (unit === 'B') return formatBytes(value)
|
||||||
|
if (unit === 'B/s') return `${formatBytes(value)}/s`
|
||||||
|
if (unit === '%') return `${value.toFixed(1)} %`
|
||||||
|
if (unit === 'ms') return `${value.toFixed(value >= 10 ? 0 : 1)} ms`
|
||||||
|
return `${Number.isInteger(value) ? value : value.toFixed(1)} ${unit}`
|
||||||
|
}
|
||||||
|
|
||||||
const controllerStatusText = computed(() => {
|
const controllerStatusText = computed(() => {
|
||||||
const s = monitorSummary.value?.current?.status ?? activeStorage.value?.status
|
const s = monitorSummary.value?.current?.status ?? activeStorage.value?.status
|
||||||
switch (s) {
|
switch (s) {
|
||||||
@@ -277,7 +409,7 @@ const controllerStatusColor = computed(() => {
|
|||||||
const chartHint = computed(() => {
|
const chartHint = computed(() => {
|
||||||
if (!selectedResourceUID.value) return ''
|
if (!selectedResourceUID.value) return ''
|
||||||
if (!trendMetricOptions.value.length) {
|
if (!trendMetricOptions.value.length) {
|
||||||
return '该设备暂无已上报的指标批次,无法选择指标;请确认采集已写入。'
|
return '该设备暂无可展示的标准存储指标。'
|
||||||
}
|
}
|
||||||
if (!trendSelectedMetric.value) return '请选择要展示的指标。'
|
if (!trendSelectedMetric.value) return '请选择要展示的指标。'
|
||||||
if (!chartSeriesPoints.value.length) {
|
if (!chartSeriesPoints.value.length) {
|
||||||
@@ -295,14 +427,35 @@ const trendSeriesDisplayName = computed(() => {
|
|||||||
return opt?.label ?? v
|
return opt?.label ?? v
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const trendSeriesUnit = computed(() => trendMetricOptions.value.find((o) => o.value === trendSelectedMetric.value)?.unit || '')
|
||||||
|
|
||||||
|
const trendYAxisName = computed(() => {
|
||||||
|
switch (trendSeriesUnit.value) {
|
||||||
|
case 'B':
|
||||||
|
return '容量'
|
||||||
|
case 'B/s':
|
||||||
|
return '带宽'
|
||||||
|
case '%':
|
||||||
|
return '使用率'
|
||||||
|
case 'ms':
|
||||||
|
return '延迟'
|
||||||
|
default:
|
||||||
|
return '指标值'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/** ECharts 折线图:单序列 type: 'line',由 @/components/chart 渲染 */
|
/** ECharts 折线图:单序列 type: 'line',由 @/components/chart 渲染 */
|
||||||
const ioChartOptions = computed(() => {
|
const ioChartOptions = computed(() => {
|
||||||
const pts = chartSeriesPoints.value
|
const pts = chartSeriesPoints.value
|
||||||
const labels = pts.map((p) => dayjs(p.time).format('MM-DD HH:mm'))
|
const labels = pts.map((p) => dayjs(p.time).format('MM-DD HH:mm'))
|
||||||
const values = pts.map((p) => p.value)
|
const values = pts.map((p) => p.value)
|
||||||
const name = trendSeriesDisplayName.value
|
const name = trendSeriesDisplayName.value
|
||||||
|
const unit = trendSeriesUnit.value
|
||||||
return {
|
return {
|
||||||
tooltip: { trigger: 'axis' },
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
valueFormatter: (value: number | string) => formatMetricValue(Number(value), unit),
|
||||||
|
},
|
||||||
legend: { show: true, data: [name] },
|
legend: { show: true, data: [name] },
|
||||||
grid: { left: '3%', right: '4%', bottom: '40px', containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '40px', containLabel: true },
|
||||||
xAxis: {
|
xAxis: {
|
||||||
@@ -314,7 +467,13 @@ const ioChartOptions = computed(() => {
|
|||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yAxis: { type: 'value', name: '指标值' },
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: trendYAxisName.value,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (value: number) => formatMetricValue(value, unit),
|
||||||
|
},
|
||||||
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
@@ -433,53 +592,39 @@ async function loadStorageList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCollectedMetricNames() {
|
async function refreshLatestStorageMetrics() {
|
||||||
trendMetricOptions.value = []
|
latestStorageMetrics.value = []
|
||||||
const resourceUID = selectedResourceUID.value?.trim()
|
const resourceUID = selectedResourceUID.value?.trim()
|
||||||
if (!resourceUID) {
|
if (!resourceUID) return
|
||||||
trendSelectedMetric.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
metricsOptionsLoading.value = true
|
metricsOptionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
type MRow = { metric_name: string; metric_unit?: string }
|
const res: any = await fetchStorageMetricsLatest(resourceUID)
|
||||||
let list: MRow[] = []
|
if (res.code === 0 && res.details?.metrics?.length) {
|
||||||
const devId = activeStorage.value?.id
|
latestStorageMetrics.value = res.details.metrics
|
||||||
if (devId && devId > 0) {
|
|
||||||
const res: any = await fetchStorageMonitorCollectedMetrics(devId)
|
|
||||||
if (res.code === 0 && res.details?.metrics?.length) {
|
|
||||||
list = res.details.metrics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!list.length) {
|
|
||||||
const res: any = await fetchStorageMetricsLatest(resourceUID)
|
|
||||||
if (res.code === 0 && res.details?.metrics?.length) {
|
|
||||||
list = res.details.metrics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trendMetricOptions.value = list.map((m) => ({
|
|
||||||
value: m.metric_name,
|
|
||||||
label: m.metric_unit ? `${m.metric_name} (${m.metric_unit})` : m.metric_name,
|
|
||||||
}))
|
|
||||||
const names = new Set(list.map((m) => m.metric_name))
|
|
||||||
const cur = trendSelectedMetric.value.trim()
|
|
||||||
if (cur && names.has(cur)) {
|
|
||||||
// 保持用户选择
|
|
||||||
} else if (names.has('read_iops')) {
|
|
||||||
trendSelectedMetric.value = 'read_iops'
|
|
||||||
} else if (list.length) {
|
|
||||||
trendSelectedMetric.value = list[0].metric_name
|
|
||||||
} else {
|
|
||||||
trendSelectedMetric.value = ''
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
trendMetricOptions.value = []
|
latestStorageMetrics.value = []
|
||||||
trendSelectedMetric.value = ''
|
|
||||||
} finally {
|
} finally {
|
||||||
metricsOptionsLoading.value = false
|
metricsOptionsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshTrendMetricOptions() {
|
||||||
|
trendMetricOptions.value = trendMetricNames.flatMap((name) => {
|
||||||
|
const metric = latestMetricByName.value.get(name)
|
||||||
|
if (!metric) return []
|
||||||
|
const meta = storageMetricMeta[name]
|
||||||
|
return [{
|
||||||
|
value: name,
|
||||||
|
label: meta.label,
|
||||||
|
unit: metric.metric_unit || meta.unit,
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
const current = trendSelectedMetric.value.trim()
|
||||||
|
if (trendMetricOptions.value.some((option) => option.value === current)) return
|
||||||
|
trendSelectedMetric.value = trendMetricOptions.value[0]?.value || ''
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshMonitorSummary() {
|
async function refreshMonitorSummary() {
|
||||||
summaryLoading.value = true
|
summaryLoading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -533,7 +678,8 @@ async function refreshTimeseries() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshSelectionDetails() {
|
async function refreshSelectionDetails() {
|
||||||
await refreshCollectedMetricNames()
|
await refreshLatestStorageMetrics()
|
||||||
|
refreshTrendMetricOptions()
|
||||||
await Promise.all([refreshMonitorSummary(), refreshTimeseries()])
|
await Promise.all([refreshMonitorSummary(), refreshTimeseries()])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,6 +782,13 @@ export default {
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.stats-row :deep(.arco-col) {
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -678,11 +831,6 @@ export default {
|
|||||||
color: #722ed1;
|
color: #722ed1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-success {
|
|
||||||
background-color: rgba(0, 180, 42, 0.12);
|
|
||||||
color: rgb(var(--success-6));
|
|
||||||
}
|
|
||||||
|
|
||||||
&-warning {
|
&-warning {
|
||||||
background-color: rgba(255, 125, 0, 0.12);
|
background-color: rgba(255, 125, 0, 0.12);
|
||||||
color: rgb(var(--warning-6));
|
color: rgb(var(--warning-6));
|
||||||
@@ -732,6 +880,160 @@ export default {
|
|||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storage-metrics-stack {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-overview-card {
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-overview-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-gauge {
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 132px;
|
||||||
|
height: 132px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
circle {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-gauge-track {
|
||||||
|
stroke: var(--color-fill-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-gauge-value {
|
||||||
|
stroke: rgb(var(--primary-6));
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-gauge-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-values {
|
||||||
|
display: grid;
|
||||||
|
flex: 1;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-value-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 20px;
|
||||||
|
border-left: 1px solid var(--color-border-2);
|
||||||
|
|
||||||
|
strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.capacity-value-label {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-3);
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&--used::before {
|
||||||
|
background: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
&--free::before {
|
||||||
|
background: var(--color-neutral-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-metrics-card {
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-metrics-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-metric-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-right: 1px solid var(--color-border-2);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-metric-name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-pair-card {
|
.chart-pair-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -747,6 +1049,20 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.storage-trend-card {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 400px;
|
||||||
|
|
||||||
|
:deep(.arco-card-body) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.detail-side-card :deep(.arco-card-body) {
|
.detail-side-card :deep(.arco-card-body) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,11 +72,11 @@
|
|||||||
<a-form-item label="目标范围" required>
|
<a-form-item label="目标范围" required>
|
||||||
<a-textarea v-model="form.target_range" placeholder="CIDR、IP 段等,按后端约定填写" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
<a-textarea v-model="form.target_range" placeholder="CIDR、IP 段等,按后端约定填写" :auto-size="{ minRows: 2, maxRows: 6 }" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="端口范围">
|
<a-form-item label="端口范围" :required="form.type === 'port' || form.type === 'full'">
|
||||||
<a-input v-model="form.port_range" placeholder="如 80,443 或 8080-8090,可选" allow-clear />
|
<a-input v-model="form.port_range" placeholder="如 80,443 或 8080-8090" allow-clear />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="执行采集节点" required>
|
<a-form-item label="发现后采集节点" required extra="IP 扫描由网络扫描服务执行;该节点用于发现设备后续自动纳管和监控采集。">
|
||||||
<a-select v-model="form.collector_node_id" placeholder="选择中心或区域采集节点" allow-search>
|
<a-select v-model="form.collector_node_id" placeholder="选择发现设备后使用的采集节点" allow-search>
|
||||||
<a-option v-for="node in nodes" :key="node.id" :value="node.id">{{ node.name }}({{ node.node_type === 'center' ? '中心' : node.region_code }})</a-option>
|
<a-option v-for="node in nodes" :key="node.id" :value="node.id">{{ node.name }}({{ node.node_type === 'center' ? '中心' : node.region_code }})</a-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item label="并发数">
|
<a-form-item label="并发数">
|
||||||
<a-input-number v-model="form.concurrency" :min="1" :max="5000" style="width: 100%" />
|
<a-input-number v-model="form.concurrency" :min="1" :max="100" style="width: 100%" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -316,7 +316,11 @@ const submitForm = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!form.collector_node_id) {
|
if (!form.collector_node_id) {
|
||||||
Message.warning('请选择执行采集节点')
|
Message.warning('请选择发现设备后使用的采集节点')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ((form.type === 'port' || form.type === 'full') && !form.port_range?.trim()) {
|
||||||
|
Message.warning('端口扫描和全量扫描必须填写端口范围')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!allowedProtocols.value.length) { Message.warning('请选择允许发现协议'); return }
|
if (!allowedProtocols.value.length) { Message.warning('请选择允许发现协议'); return }
|
||||||
@@ -440,7 +444,7 @@ const columns: TableColumnData[] = [
|
|||||||
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
|
{ title: '名称', dataIndex: 'name', ellipsis: true, tooltip: true },
|
||||||
{ title: '类型', dataIndex: 'type', width: 88 },
|
{ title: '类型', dataIndex: 'type', width: 88 },
|
||||||
{ title: '目标范围', slotName: 'target', minWidth: 200 },
|
{ title: '目标范围', slotName: 'target', minWidth: 200 },
|
||||||
{ title: '采集节点', slotName: 'node', width: 180, ellipsis: true, tooltip: true },
|
{ title: '发现后采集节点', slotName: 'node', width: 180, ellipsis: true, tooltip: true },
|
||||||
{ title: '状态', slotName: 'status', width: 100 },
|
{ title: '状态', slotName: 'status', width: 100 },
|
||||||
{ title: '最近错误', slotName: 'latestError', minWidth: 260, ellipsis: true, tooltip: true },
|
{ title: '最近错误', slotName: 'latestError', minWidth: 260, ellipsis: true, tooltip: true },
|
||||||
{ title: '操作', slotName: 'actions', width: 300, fixed: 'right' },
|
{ title: '操作', slotName: 'actions', width: 300, fixed: 'right' },
|
||||||
|
|||||||
Reference in New Issue
Block a user