This commit is contained in:
zxr
2026-04-16 18:59:33 +08:00
parent cb8bd05ff7
commit 01139f2874
19 changed files with 522 additions and 22 deletions

View File

@@ -12,6 +12,13 @@ export interface RoomDeviceItem {
room_id: string room_id: string
device_category: string device_category: string
agent_config: string agent_config: string
collect_method: 'api' | 'snmp'
snmp_target: string
snmp_port: number
snmp_community: string
snmp_timeout_ms: number
snmp_retries: number
snmp_oids: string
enabled: boolean enabled: boolean
collect_on: boolean collect_on: boolean
collect_interval: number collect_interval: number
@@ -45,6 +52,13 @@ export interface RoomDeviceCreateData {
room_id: string room_id: string
device_category: string device_category: string
agent_config?: string agent_config?: string
collect_method?: 'api' | 'snmp'
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
enabled?: boolean enabled?: boolean
collect_on?: boolean collect_on?: boolean
collect_interval?: number collect_interval?: number
@@ -58,6 +72,13 @@ export interface RoomDeviceUpdateData {
room_id?: string room_id?: string
device_category?: string device_category?: string
agent_config?: string agent_config?: string
collect_method?: 'api' | 'snmp'
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
enabled?: boolean enabled?: boolean
collect_on?: boolean collect_on?: boolean
collect_interval?: number collect_interval?: number
@@ -66,9 +87,16 @@ export interface RoomDeviceUpdateData {
/** 机房设备采集配置数据 */ /** 机房设备采集配置数据 */
export interface RoomDeviceCollectData { export interface RoomDeviceCollectData {
collect_method?: 'api' | 'snmp'
agent_config?: string
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_interval?: number collect_interval?: number
agent_config?: string
} }
/** 指标数据项 */ /** 指标数据项 */

View File

@@ -19,6 +19,13 @@ export interface SecurityServiceItem {
tags: string tags: string
status_url: string status_url: string
agent_config: string agent_config: string
collect_method: 'api' | 'snmp'
snmp_target: string
snmp_port: number
snmp_community: string
snmp_timeout_ms: number
snmp_retries: number
snmp_oids: string
collect_on: boolean collect_on: boolean
collect_args: string collect_args: string
collect_interval: number collect_interval: number
@@ -65,6 +72,13 @@ export interface SecurityServiceFormData {
tags?: string tags?: string
status_url?: string status_url?: string
agent_config?: string agent_config?: string
collect_method?: 'api' | 'snmp'
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_args?: string collect_args?: string
collect_interval?: number collect_interval?: number
@@ -120,6 +134,14 @@ export const fetchSecurityMetricsLatest = (serviceIdentity: string) => {
/** 采集配置补丁参数 */ /** 采集配置补丁参数 */
export interface SecurityServicePatchData { export interface SecurityServicePatchData {
collect_method?: 'api' | 'snmp'
agent_config?: string
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_interval?: number collect_interval?: number
} }

View File

@@ -19,6 +19,13 @@ export interface StorageItem {
tags: string tags: string
status_url: string status_url: string
agent_config: string agent_config: string
collect_method: 'api' | 'snmp'
snmp_target: string
snmp_port: number
snmp_community: string
snmp_timeout_ms: number
snmp_retries: number
snmp_oids: string
collect_on: boolean collect_on: boolean
collect_args: string collect_args: string
collect_interval: number collect_interval: number
@@ -65,6 +72,13 @@ export interface StorageCreateData {
tags?: string tags?: string
status_url?: string status_url?: string
agent_config?: string agent_config?: string
collect_method?: 'api' | 'snmp'
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_args?: string collect_args?: string
collect_interval?: number collect_interval?: number
@@ -86,6 +100,13 @@ export interface StorageUpdateData {
tags?: string tags?: string
status_url?: string status_url?: string
agent_config?: string agent_config?: string
collect_method?: 'api' | 'snmp'
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_args?: string collect_args?: string
collect_interval?: number collect_interval?: number
@@ -151,6 +172,14 @@ export const fetchStorageMetricsLatest = (serviceIdentity: string) => {
/** 采集配置补丁 */ /** 采集配置补丁 */
export interface StoragePatchData { export interface StoragePatchData {
collect_method?: 'api' | 'snmp'
agent_config?: string
snmp_target?: string
snmp_port?: number
snmp_community?: string
snmp_timeout_ms?: number
snmp_retries?: number
snmp_oids?: string
collect_on?: boolean collect_on?: boolean
collect_interval?: number collect_interval?: number
} }

View File

@@ -10,10 +10,22 @@
<a-descriptions-item label="机房ID">{{ record.room_id }}</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="描述信息" :span="2">{{ record.description || '-' }}</a-descriptions-item>
<a-descriptions-item label="采集地址" :span="2"> <a-descriptions-item label="采集方式">
<a-link v-if="record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link> <a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="采集地址/目标" :span="2">
<a-link v-if="record.collect_method !== 'snmp' && record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link>
<span v-else-if="record.collect_method === 'snmp'">{{ record.snmp_target || '-' }}</span>
<span v-else>-</span> <span v-else>-</span>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item v-if="record.collect_method === 'snmp'" label="SNMP连接">
{{ (record.snmp_community || '-') + ' @ ' + (record.snmp_port || 161) }}
</a-descriptions-item>
<a-descriptions-item v-if="record.collect_method === 'snmp'" label="SNMP超时/重试">
{{ (record.snmp_timeout_ms || 3000) + 'ms / ' + (record.snmp_retries ?? 1) }}
</a-descriptions-item>
<a-descriptions-item label="启用状态"> <a-descriptions-item label="启用状态">
<a-tag :color="record.enabled ? 'green' : 'gray'"> <a-tag :color="record.enabled ? 'green' : 'gray'">

View File

@@ -38,10 +38,58 @@
<a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" /> <a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" />
</a-form-item> </a-form-item>
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)"> <a-divider orientation="left">采集协议与连接</a-divider>
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" /> <a-form-item field="collect_method" label="采集方式">
<a-radio-group v-model="formData.collect_method" type="button">
<a-radio value="api">API</a-radio>
<a-radio value="snmp">SNMP</a-radio>
</a-radio-group>
</a-form-item> </a-form-item>
<template v-if="formData.collect_method === 'api'">
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
</a-form-item>
</template>
<template v-else>
<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-row :gutter="20">
<a-col :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 :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-form-item field="snmp_oids" label="SNMP OID配置(JSON数组)">
<a-textarea
v-model="formData.snmp_oids"
:rows="3"
placeholder='可留空使用默认模板,或填写如 [{"oid":"1.3.6.1.2.1.1.3.0","metric_name":"sys_uptime"}]'
/>
</a-form-item>
</template>
<a-row :gutter="20"> <a-row :gutter="20">
<a-col :span="12"> <a-col :span="12">
<a-form-item field="enabled" label="启用设备"> <a-form-item field="enabled" label="启用设备">
@@ -110,6 +158,13 @@ const formData = reactive({
room_id: '', room_id: '',
device_category: '', device_category: '',
agent_config: '', agent_config: '',
collect_method: 'api' as 'api' | 'snmp',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
enabled: true, enabled: true,
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
@@ -149,6 +204,13 @@ watch(
room_id: props.record.room_id || '', room_id: props.record.room_id || '',
device_category: props.record.device_category || '', device_category: props.record.device_category || '',
agent_config: props.record.agent_config || '', agent_config: props.record.agent_config || '',
collect_method: props.record.collect_method || 'api',
snmp_target: props.record.snmp_target || '',
snmp_port: props.record.snmp_port || 161,
snmp_community: props.record.snmp_community || '',
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
snmp_retries: props.record.snmp_retries ?? 1,
snmp_oids: props.record.snmp_oids || '',
enabled: props.record.enabled ?? true, enabled: props.record.enabled ?? true,
collect_on: props.record.collect_on ?? true, collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60, collect_interval: props.record.collect_interval || 60,
@@ -161,6 +223,13 @@ watch(
room_id: '', room_id: '',
device_category: '', device_category: '',
agent_config: '', agent_config: '',
collect_method: 'api',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
enabled: true, enabled: true,
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
@@ -174,6 +243,24 @@ watch(
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value?.validate() await formRef.value?.validate()
if (formData.collect_method === 'api' && !formData.agent_config?.trim()) {
Message.warning('API 模式下请填写采集地址')
return
}
if (formData.collect_method === 'snmp') {
if (!formData.snmp_target?.trim() || !formData.snmp_community?.trim()) {
Message.warning('SNMP 模式下请填写目标地址和 community')
return
}
if (formData.snmp_oids?.trim()) {
try {
JSON.parse(formData.snmp_oids)
} catch {
Message.warning('SNMP OID 配置必须是合法 JSON')
return
}
}
}
confirmLoading.value = true confirmLoading.value = true
@@ -184,6 +271,13 @@ const handleOk = async () => {
room_id: formData.room_id, room_id: formData.room_id,
device_category: formData.device_category, device_category: formData.device_category,
agent_config: formData.agent_config, agent_config: formData.agent_config,
collect_method: formData.collect_method,
snmp_target: formData.snmp_target,
snmp_port: formData.snmp_port,
snmp_community: formData.snmp_community,
snmp_timeout_ms: formData.snmp_timeout_ms,
snmp_retries: formData.snmp_retries,
snmp_oids: formData.snmp_oids,
enabled: formData.enabled, enabled: formData.enabled,
collect_on: formData.collect_on, collect_on: formData.collect_on,
collect_interval: formData.collect_interval, collect_interval: formData.collect_interval,
@@ -198,6 +292,13 @@ const handleOk = async () => {
room_id: formData.room_id, room_id: formData.room_id,
device_category: formData.device_category, device_category: formData.device_category,
agent_config: formData.agent_config, agent_config: formData.agent_config,
collect_method: formData.collect_method,
snmp_target: formData.snmp_target,
snmp_port: formData.snmp_port,
snmp_community: formData.snmp_community,
snmp_timeout_ms: formData.snmp_timeout_ms,
snmp_retries: formData.snmp_retries,
snmp_oids: formData.snmp_oids,
enabled: formData.enabled, enabled: formData.enabled,
collect_on: formData.collect_on, collect_on: formData.collect_on,
collect_interval: formData.collect_interval, collect_interval: formData.collect_interval,

View File

@@ -1,6 +1,30 @@
<template> <template>
<a-modal :visible="visible" title="采集配置" :mask-closable="false" :ok-loading="loading" @ok="handleSubmit" @cancel="handleCancel"> <a-modal :visible="visible" title="采集配置" :mask-closable="false" :ok-loading="loading" @ok="handleSubmit" @cancel="handleCancel">
<a-form :model="form" layout="vertical"> <a-form :model="form" layout="vertical">
<a-form-item label="采集方式">
<a-radio-group v-model="form.collect_method" type="button">
<a-radio value="api">API</a-radio>
<a-radio value="snmp">SNMP</a-radio>
</a-radio-group>
</a-form-item>
<template v-if="form.collect_method === 'api'">
<a-form-item label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="form.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
<template #extra>
<span style="color: #86909c">采集地址应返回 JSON 格式的指标数据支持 {"metrics":[...]} 或数组形式</span>
</template>
</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-form-item>
</template>
<a-form-item label="参与周期采集"> <a-form-item label="参与周期采集">
<a-switch v-model="form.collect_on" /> <a-switch v-model="form.collect_on" />
</a-form-item> </a-form-item>
@@ -12,12 +36,6 @@
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="form.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
<template #extra>
<span style="color: #86909c">采集地址应返回 JSON 格式的指标数据支持 {"metrics":[...]} 或数组形式</span>
</template>
</a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
</template> </template>
@@ -38,18 +56,24 @@ const emit = defineEmits(['update:visible', 'success'])
const loading = ref(false) const loading = ref(false)
const form = ref({ const form = ref({
collect_method: 'api' as 'api' | 'snmp',
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
agent_config: '', agent_config: '',
snmp_target: '',
snmp_community: '',
}) })
watch( watch(
() => props.visible, () => props.visible,
(val) => { (val) => {
if (val && props.record) { if (val && props.record) {
form.value.collect_method = props.record.collect_method || 'api'
form.value.collect_on = props.record.collect_on ?? true form.value.collect_on = props.record.collect_on ?? true
form.value.collect_interval = props.record.collect_interval || 60 form.value.collect_interval = props.record.collect_interval || 60
form.value.agent_config = props.record.agent_config || '' form.value.agent_config = props.record.agent_config || ''
form.value.snmp_target = props.record.snmp_target || ''
form.value.snmp_community = props.record.snmp_community || ''
} }
} }
) )
@@ -58,9 +82,12 @@ const handleSubmit = async () => {
loading.value = true loading.value = true
try { try {
const data: RoomDeviceCollectData = { const data: RoomDeviceCollectData = {
collect_method: form.value.collect_method,
collect_on: form.value.collect_on, collect_on: form.value.collect_on,
collect_interval: form.value.collect_interval, collect_interval: form.value.collect_interval,
agent_config: form.value.agent_config, 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_community: form.value.collect_method === 'snmp' ? form.value.snmp_community : undefined,
} }
await patchRoomDeviceCollect(props.record.id, data) await patchRoomDeviceCollect(props.record.id, data)

View File

@@ -38,6 +38,12 @@ export const columns = [
width: 100, width: 100,
slotName: 'enabled', slotName: 'enabled',
}, },
{
dataIndex: 'collect_method',
title: '采集方式',
width: 100,
slotName: 'collect_method',
},
{ {
dataIndex: 'collect_on', dataIndex: 'collect_on',
title: '数据采集', title: '数据采集',

View File

@@ -40,6 +40,11 @@
{{ record.collect_on ? '已启用' : '未启用' }} {{ record.collect_on ? '已启用' : '未启用' }}
</a-tag> </a-tag>
</template> </template>
<template #collect_method="{ record }">
<a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</template>
<template #actions="{ record }"> <template #actions="{ record }">
<a-space> <a-space>

View File

@@ -17,16 +17,28 @@
<a-divider orientation="left">采集配置</a-divider> <a-divider orientation="left">采集配置</a-divider>
<a-descriptions :column="2" bordered> <a-descriptions :column="2" bordered>
<a-descriptions-item label="采集方式">
<a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="采集间隔">{{ record.collect_interval }}</a-descriptions-item>
<a-descriptions-item label="采集地址" :span="2"> <a-descriptions-item label="采集地址" :span="2">
<a-link v-if="record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link> <a-link v-if="record.collect_method !== 'snmp' && record.agent_config" :href="record.agent_config" target="_blank">{{ record.agent_config }}</a-link>
<span v-else-if="record.collect_method === 'snmp'">{{ record.snmp_target || '-' }}</span>
<span v-else>-</span> <span v-else>-</span>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item v-if="record.collect_method === 'snmp'" label="SNMP连接">
{{ (record.snmp_community || '-') + ' @ ' + (record.snmp_port || 161) }}
</a-descriptions-item>
<a-descriptions-item v-if="record.collect_method === 'snmp'" label="SNMP超时/重试">
{{ (record.snmp_timeout_ms || 3000) + 'ms / ' + (record.snmp_retries ?? 1) }}
</a-descriptions-item>
<a-descriptions-item label="启用周期采集"> <a-descriptions-item label="启用周期采集">
<a-tag :color="record.collect_on ? 'green' : 'gray'"> <a-tag :color="record.collect_on ? 'green' : 'gray'">
{{ record.collect_on ? '已启用' : '未启用' }} {{ record.collect_on ? '已启用' : '未启用' }}
</a-tag> </a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="采集间隔">{{ record.collect_interval }}</a-descriptions-item>
<a-descriptions-item label="采集参数" :span="2">{{ record.collect_args || '-' }}</a-descriptions-item> <a-descriptions-item label="采集参数" :span="2">{{ record.collect_args || '-' }}</a-descriptions-item>
<a-descriptions-item label="采集结果" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item> <a-descriptions-item label="采集结果" :span="2">{{ record.collect_last_result || '-' }}</a-descriptions-item>
</a-descriptions> </a-descriptions>

View File

@@ -55,10 +55,62 @@
<a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" /> <a-textarea v-model="formData.description" placeholder="请输入描述信息" :rows="2" />
</a-form-item> </a-form-item>
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)"> <a-divider orientation="left">采集协议与连接</a-divider>
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" /> <a-form-item field="collect_method" label="采集方式">
<a-radio-group v-model="formData.collect_method" type="button">
<a-radio value="api">API</a-radio>
<a-radio value="snmp">SNMP</a-radio>
</a-radio-group>
<template #extra>
<span class="form-extra">推荐优先 API设备无 API 时可切到 SNMP v2c</span>
</template>
</a-form-item> </a-form-item>
<template v-if="formData.collect_method === 'api'">
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
</a-form-item>
</template>
<template v-else>
<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-row :gutter="20">
<a-col :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 :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-form-item field="snmp_oids" label="SNMP OID配置(JSON数组)">
<a-textarea
v-model="formData.snmp_oids"
:rows="3"
placeholder='可留空使用默认模板,或填写如 [{"oid":"1.3.6.1.2.1.1.3.0","metric_name":"sys_uptime","metric_unit":"timeticks"}]'
/>
</a-form-item>
</template>
<a-row :gutter="20"> <a-row :gutter="20">
<a-col :span="12"> <a-col :span="12">
<a-form-item field="enabled" label="启用监控"> <a-form-item field="enabled" label="启用监控">
@@ -135,6 +187,13 @@ const formData = reactive({
interval: 60, interval: 60,
description: '', description: '',
agent_config: '', agent_config: '',
collect_method: 'api' as 'api' | 'snmp',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
enabled: true, enabled: true,
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
@@ -192,6 +251,13 @@ watch(
interval: props.record.interval || 60, interval: props.record.interval || 60,
description: props.record.description || '', description: props.record.description || '',
agent_config: props.record.agent_config || '', agent_config: props.record.agent_config || '',
collect_method: props.record.collect_method || 'api',
snmp_target: props.record.snmp_target || '',
snmp_port: props.record.snmp_port || 161,
snmp_community: props.record.snmp_community || '',
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
snmp_retries: props.record.snmp_retries ?? 1,
snmp_oids: props.record.snmp_oids || '',
enabled: props.record.enabled ?? true, enabled: props.record.enabled ?? true,
collect_on: props.record.collect_on ?? true, collect_on: props.record.collect_on ?? true,
collect_interval: props.record.collect_interval || 60, collect_interval: props.record.collect_interval || 60,
@@ -209,6 +275,13 @@ watch(
interval: 60, interval: 60,
description: '', description: '',
agent_config: '', agent_config: '',
collect_method: 'api',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
enabled: true, enabled: true,
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
@@ -224,6 +297,28 @@ watch(
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value?.validate() await formRef.value?.validate()
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_community?.trim()) {
Message.warning('SNMP 模式下请填写 community')
return
}
if (formData.snmp_oids?.trim()) {
try {
JSON.parse(formData.snmp_oids)
} catch {
Message.warning('SNMP OID配置必须是合法 JSON')
return
}
}
}
confirmLoading.value = true confirmLoading.value = true
@@ -236,6 +331,13 @@ const handleOk = async () => {
interval: formData.interval, interval: formData.interval,
description: formData.description, description: formData.description,
agent_config: formData.agent_config, agent_config: formData.agent_config,
collect_method: formData.collect_method,
snmp_target: formData.snmp_target,
snmp_port: formData.snmp_port,
snmp_community: formData.snmp_community,
snmp_timeout_ms: formData.snmp_timeout_ms,
snmp_retries: formData.snmp_retries,
snmp_oids: formData.snmp_oids,
enabled: formData.enabled, enabled: formData.enabled,
collect_on: formData.collect_on, collect_on: formData.collect_on,
collect_interval: formData.collect_interval, collect_interval: formData.collect_interval,
@@ -276,3 +378,10 @@ onMounted(() => {
loadServerOptions() loadServerOptions()
}) })
</script> </script>
<style scoped lang="less">
.form-extra {
color: var(--color-text-3);
font-size: 12px;
}
</style>

View File

@@ -9,6 +9,27 @@
width="500px" width="500px"
> >
<a-form ref="formRef" :model="formData" layout="vertical"> <a-form ref="formRef" :model="formData" layout="vertical">
<a-form-item field="collect_method" label="采集方式">
<a-radio-group v-model="formData.collect_method" type="button">
<a-radio value="api">API</a-radio>
<a-radio value="snmp">SNMP</a-radio>
</a-radio-group>
</a-form-item>
<template v-if="formData.collect_method === 'api'">
<a-form-item field="agent_config" label="采集地址(HTTP/HTTPS GET)">
<a-input v-model="formData.agent_config" placeholder="周期采集地址,返回 JSON 格式指标数组" />
</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-form-item>
</template>
<a-form-item field="collect_on" label="启用周期采集"> <a-form-item field="collect_on" label="启用周期采集">
<a-switch v-model="formData.collect_on" /> <a-switch v-model="formData.collect_on" />
</a-form-item> </a-form-item>
@@ -43,6 +64,10 @@ const formRef = ref<FormInstance>()
const confirmLoading = ref(false) const confirmLoading = ref(false)
const formData = reactive({ const formData = reactive({
collect_method: 'api' as 'api' | 'snmp',
agent_config: '',
snmp_target: '',
snmp_community: '',
collect_on: true, collect_on: true,
collect_interval: 60, collect_interval: 60,
}) })
@@ -51,6 +76,10 @@ watch(
() => props.visible, () => props.visible,
(val) => { (val) => {
if (val && props.record) { if (val && props.record) {
formData.collect_method = props.record.collect_method || 'api'
formData.agent_config = props.record.agent_config || ''
formData.snmp_target = props.record.snmp_target || ''
formData.snmp_community = props.record.snmp_community || ''
formData.collect_on = props.record.collect_on ?? true formData.collect_on = props.record.collect_on ?? true
formData.collect_interval = props.record.collect_interval || 60 formData.collect_interval = props.record.collect_interval || 60
} }
@@ -62,6 +91,10 @@ const handleOk = async () => {
confirmLoading.value = true confirmLoading.value = true
const patchData: SecurityServicePatchData = { 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_community: formData.collect_method === 'snmp' ? formData.snmp_community : undefined,
collect_on: formData.collect_on, collect_on: formData.collect_on,
collect_interval: formData.collect_interval, collect_interval: formData.collect_interval,
} }

View File

@@ -40,6 +40,12 @@ export const columns = [
width: 100, width: 100,
slotName: 'enabled', slotName: 'enabled',
}, },
{
dataIndex: 'collect_method',
title: '采集方式',
width: 100,
slotName: 'collectMethod',
},
{ {
dataIndex: 'collect_on', dataIndex: 'collect_on',
title: '采集状态', title: '采集状态',

View File

@@ -40,6 +40,11 @@
{{ record.collect_on ? '已启用' : '未启用' }} {{ record.collect_on ? '已启用' : '未启用' }}
</a-tag> </a-tag>
</template> </template>
<template #collectMethod="{ record }">
<a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</template>
<template #status="{ record }"> <template #status="{ record }">
<a-tag :color="getStatusColor(record.status)"> <a-tag :color="getStatusColor(record.status)">

View File

@@ -22,7 +22,21 @@
</a-descriptions> </a-descriptions>
<a-descriptions title="采集配置" :column="2" bordered style="margin-top: 20px"> <a-descriptions title="采集配置" :column="2" bordered style="margin-top: 20px">
<a-descriptions-item label="Agent配置">{{ detailData?.agent_config || '-' }}</a-descriptions-item> <a-descriptions-item label="采集方式">
<a-tag :color="detailData?.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ detailData?.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="连接配置">
<span v-if="detailData?.collect_method === 'snmp'">{{ detailData?.snmp_target || '-' }}</span>
<span v-else>{{ detailData?.agent_config || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item v-if="detailData?.collect_method === 'snmp'" label="SNMP连接">
{{ (detailData?.snmp_community || '-') + ' @ ' + (detailData?.snmp_port || 161) }}
</a-descriptions-item>
<a-descriptions-item v-if="detailData?.collect_method === 'snmp'" label="SNMP超时/重试">
{{ (detailData?.snmp_timeout_ms || 3000) + 'ms / ' + (detailData?.snmp_retries ?? 1) }}
</a-descriptions-item>
<a-descriptions-item label="启用采集"> <a-descriptions-item label="启用采集">
<a-tag :color="detailData?.collect_on ? 'green' : 'gray'"> <a-tag :color="detailData?.collect_on ? 'green' : 'gray'">
{{ detailData?.collect_on ? '已启用' : '未启用' }} {{ detailData?.collect_on ? '已启用' : '未启用' }}

View File

@@ -68,7 +68,39 @@
</a-row> </a-row>
<a-form-item field="agent_config" label="Agent配置URL"> <a-form-item field="agent_config" label="Agent配置URL">
<a-input v-model="formData.agent_config" placeholder="请输入Agent配置地址" /> <a-radio-group v-model="formData.collect_method" type="button" style="margin-bottom: 12px">
<a-radio value="api">API</a-radio>
<a-radio value="snmp">SNMP</a-radio>
</a-radio-group>
<template v-if="formData.collect_method === 'api'">
<a-input v-model="formData.agent_config" placeholder="请输入Agent配置地址" />
</template>
<template v-else>
<a-row :gutter="12">
<a-col :span="12">
<a-input v-model="formData.snmp_target" placeholder="SNMP目标地址" />
</a-col>
<a-col :span="12">
<a-input-number v-model="formData.snmp_port" :min="1" :max="65535" placeholder="SNMP端口" style="width: 100%" />
</a-col>
<a-col :span="12">
<a-input v-model="formData.snmp_community" placeholder="SNMP community" />
</a-col>
<a-col :span="6">
<a-input-number v-model="formData.snmp_timeout_ms" :min="1" :max="60000" placeholder="超时(ms)" style="width: 100%" />
</a-col>
<a-col :span="6">
<a-input-number v-model="formData.snmp_retries" :min="0" :max="10" placeholder="重试次数" style="width: 100%" />
</a-col>
<a-col :span="24">
<a-textarea
v-model="formData.snmp_oids"
:rows="3"
placeholder='可留空用默认模板,或填写 JSON 数组如 [{"oid":"1.3.6.1.2.1.1.3.0","metric_name":"sys_uptime"}]'
/>
</a-col>
</a-row>
</template>
</a-form-item> </a-form-item>
<a-form-item field="collect_args" label="采集参数"> <a-form-item field="collect_args" label="采集参数">
@@ -124,6 +156,13 @@ const formData = reactive<StorageCreateData>({
extra: '', extra: '',
tags: '', tags: '',
agent_config: '', agent_config: '',
collect_method: 'api' as 'api' | 'snmp',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
collect_on: true, collect_on: true,
collect_args: '', collect_args: '',
collect_interval: 60, collect_interval: 60,
@@ -150,6 +189,13 @@ watch(
extra: props.record.extra || '', extra: props.record.extra || '',
tags: props.record.tags || '', tags: props.record.tags || '',
agent_config: props.record.agent_config || '', agent_config: props.record.agent_config || '',
collect_method: props.record.collect_method || 'api',
snmp_target: props.record.snmp_target || '',
snmp_port: props.record.snmp_port || 161,
snmp_community: props.record.snmp_community || '',
snmp_timeout_ms: props.record.snmp_timeout_ms || 3000,
snmp_retries: props.record.snmp_retries ?? 1,
snmp_oids: props.record.snmp_oids || '',
collect_on: props.record.collect_on ?? true, collect_on: props.record.collect_on ?? true,
collect_args: props.record.collect_args || '', collect_args: props.record.collect_args || '',
collect_interval: props.record.collect_interval || 60, collect_interval: props.record.collect_interval || 60,
@@ -166,6 +212,13 @@ watch(
extra: '', extra: '',
tags: '', tags: '',
agent_config: '', agent_config: '',
collect_method: 'api',
snmp_target: '',
snmp_port: 161,
snmp_community: '',
snmp_timeout_ms: 3000,
snmp_retries: 1,
snmp_oids: '',
collect_on: true, collect_on: true,
collect_args: '', collect_args: '',
collect_interval: 60, collect_interval: 60,
@@ -178,6 +231,24 @@ watch(
const handleOk = async () => { const handleOk = async () => {
try { try {
await formRef.value?.validate() await formRef.value?.validate()
if (formData.collect_method === 'api' && !formData.agent_config?.trim()) {
Message.warning('API 模式下请填写 Agent 配置 URL')
return
}
if (formData.collect_method === 'snmp') {
if (!formData.snmp_target?.trim() || !formData.snmp_community?.trim()) {
Message.warning('SNMP 模式下请填写目标地址和 community')
return
}
if (formData.snmp_oids?.trim()) {
try {
JSON.parse(formData.snmp_oids)
} catch {
Message.warning('SNMP OID 配置必须是合法 JSON')
return
}
}
}
confirmLoading.value = true confirmLoading.value = true
@@ -192,6 +263,13 @@ const handleOk = async () => {
extra: formData.extra, extra: formData.extra,
tags: formData.tags, tags: formData.tags,
agent_config: formData.agent_config, agent_config: formData.agent_config,
collect_method: formData.collect_method,
snmp_target: formData.snmp_target,
snmp_port: formData.snmp_port,
snmp_community: formData.snmp_community,
snmp_timeout_ms: formData.snmp_timeout_ms,
snmp_retries: formData.snmp_retries,
snmp_oids: formData.snmp_oids,
collect_on: formData.collect_on, collect_on: formData.collect_on,
collect_args: formData.collect_args, collect_args: formData.collect_args,
collect_interval: formData.collect_interval, collect_interval: formData.collect_interval,

View File

@@ -31,6 +31,12 @@ export const columns = [
width: 100, width: 100,
slotName: 'enabled', slotName: 'enabled',
}, },
{
dataIndex: 'collect_method',
title: '采集方式',
width: 100,
slotName: 'collect_method',
},
{ {
dataIndex: 'collect_on', dataIndex: 'collect_on',
title: '数据采集', title: '数据采集',

View File

@@ -40,6 +40,11 @@
{{ record.collect_on ? '已启用' : '未启用' }} {{ record.collect_on ? '已启用' : '未启用' }}
</a-tag> </a-tag>
</template> </template>
<template #collect_method="{ record }">
<a-tag :color="record.collect_method === 'snmp' ? 'purple' : 'arcoblue'">
{{ record.collect_method === 'snmp' ? 'SNMP' : 'API' }}
</a-tag>
</template>
<template #status="{ record }"> <template #status="{ record }">
<a-tag :color="getStatusColor(record.status)"> <a-tag :color="getStatusColor(record.status)">

View File

@@ -97,9 +97,10 @@ const getStatusText = (status?: string) => {
return textMap[status || ''] || '-' return textMap[status || ''] || '-'
} }
const formatTime = (time?: string) => { const formatTime = (time?: string | null) => {
if (!time) return '-' if (!time || time.startsWith('0001-01-01')) return '-'
const date = new Date(time) const date = new Date(time)
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1) return '-'
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0')

View File

@@ -156,9 +156,10 @@ const getStatusText = (status?: string) => {
return textMap[status || ''] || '-' return textMap[status || ''] || '-'
} }
const formatTime = (time?: string) => { const formatTime = (time?: string | null) => {
if (!time) return '-' if (!time || time.startsWith('0001-01-01')) return '-'
const date = new Date(time) const date = new Date(time)
if (Number.isNaN(date.getTime()) || date.getFullYear() <= 1) return '-'
const year = date.getFullYear() const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0')