feat: add shared snmp oid editor

This commit is contained in:
zxr
2026-07-05 22:13:39 +08:00
parent 7a20329594
commit f5d0bae89b

View File

@@ -0,0 +1,338 @@
<template>
<a-form-item field="snmp_oids">
<template #label>
<div class="snmp-oid-label">
<span>SNMP OID配置</span>
<div class="snmp-oid-actions">
<a-button size="mini" type="text" @click="switchEditMode">
{{ editMode === 'table' ? 'JSON' : '表格' }}
</a-button>
<a-button v-if="editMode === 'table'" size="mini" type="primary" @click="addRow">添加</a-button>
</div>
</div>
</template>
<template v-if="editMode === 'table'">
<a-table :data="rows" :pagination="false" size="small" class="snmp-oid-table">
<template #columns>
<a-table-column title="OID" data-index="oid" :width="220">
<template #cell="{ record }">
<a-input v-model="record.oid" @input="syncJsonFromRows" />
</template>
</a-table-column>
<a-table-column title="指标名称" data-index="metric_name" :width="150">
<template #cell="{ record }">
<a-input v-model="record.metric_name" @input="syncJsonFromRows" />
</template>
</a-table-column>
<a-table-column title="指标unit" data-index="metric_unit" :width="120">
<template #cell="{ record }">
<a-input v-model="record.metric_unit" @input="syncJsonFromRows" />
</template>
</a-table-column>
<a-table-column title="类型" data-index="type" :width="100">
<template #cell="{ record }">
<a-input v-model="record.type" @input="syncJsonFromRows" />
</template>
</a-table-column>
<a-table-column title="操作" :width="80">
<template #cell="{ rowIndex }">
<a-button size="mini" type="text" status="danger" @click="removeRow(rowIndex)">删除</a-button>
</template>
</a-table-column>
</template>
</a-table>
</template>
<a-textarea
v-else
v-model="jsonText"
:rows="5"
placeholder='可留空使用默认模板,或填写如 [{"oid":"1.3.6.1.2.1.1.3.0","metric_name":"sys_uptime"}]'
@input="emitJsonText"
/>
</a-form-item>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { onMounted, ref, watch } from 'vue'
interface Props {
modelValue?: string
resetKey?: number
}
type EditMode = 'table' | 'json'
interface SnmpOidRow {
oid: string
metric_name: string
metric_unit: string
type: string
}
type JsonRecord = Record<string, unknown>
const props = defineProps<Props>()
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const tableFields = ['oid', 'metric_name', 'metric_unit', 'type']
const editMode = ref<EditMode>('table')
const rows = ref<SnmpOidRow[]>([])
const jsonText = ref('')
const createEmptyRow = (): SnmpOidRow => ({
oid: '',
metric_name: '',
metric_unit: '',
type: '',
})
const isJsonRecord = (value: unknown): value is JsonRecord =>
!!value && typeof value === 'object' && !Array.isArray(value)
const parseJsonArray = (raw: string): unknown[] => {
const text = raw.trim()
if (!text) {
return []
}
const parsed = JSON.parse(text)
if (!Array.isArray(parsed)) {
throw new Error('SNMP OID 配置必须是合法 JSON 数组')
}
return parsed
}
const hasUnsupportedFields = (items: unknown[]) =>
items.some((item) => {
if (!isJsonRecord(item)) {
return true
}
return Object.keys(item).some((key) => !tableFields.includes(key))
})
const normalizeRows = (sourceRows: SnmpOidRow[]) =>
sourceRows
.map((row) => ({
oid: row.oid.trim(),
metric_name: row.metric_name.trim(),
metric_unit: row.metric_unit.trim(),
type: row.type.trim(),
}))
.filter((row) => row.oid || row.metric_name || row.metric_unit || row.type)
const findIncompleteRowIndex = (sourceRows: SnmpOidRow[]) =>
sourceRows.findIndex((row) => {
const normalizedRow = {
oid: row.oid.trim(),
metric_name: row.metric_name.trim(),
metric_unit: row.metric_unit.trim(),
type: row.type.trim(),
}
const hasAnyValue =
normalizedRow.oid || normalizedRow.metric_name || normalizedRow.metric_unit || normalizedRow.type
return !!hasAnyValue && (!normalizedRow.oid || !normalizedRow.metric_name)
})
const findInvalidJsonRowIndex = (items: unknown[]) =>
items.findIndex((item) => {
if (!isJsonRecord(item)) {
return true
}
return !String(item.oid ?? '').trim() || !String(item.metric_name ?? '').trim()
})
const normalizeJsonItems = (items: unknown[]) =>
items.map((item) => {
const record = item as JsonRecord
return {
...record,
oid: String(record.oid ?? '').trim(),
metric_name: String(record.metric_name ?? '').trim(),
metric_unit: typeof record.metric_unit === 'string' ? record.metric_unit.trim() : record.metric_unit,
type: typeof record.type === 'string' ? record.type.trim() : record.type,
}
})
const parseRows = (raw: string): SnmpOidRow[] =>
parseJsonArray(raw).map((item) => {
const record = isJsonRecord(item) ? item : {}
return {
oid: String(record.oid ?? '').trim(),
metric_name: String(record.metric_name ?? '').trim(),
metric_unit: String(record.metric_unit ?? '').trim(),
type: String(record.type ?? '').trim(),
}
})
const stringifyRows = (sourceRows: SnmpOidRow[]) => {
const normalizedRows = normalizeRows(sourceRows)
return normalizedRows.length ? JSON.stringify(normalizedRows, null, 2) : ''
}
const emitValue = (value: string) => {
jsonText.value = value
emit('update:modelValue', value)
}
const syncRowsFromJson = () => {
rows.value = parseRows(jsonText.value)
}
const syncJsonFromRows = () => {
emitValue(stringifyRows(rows.value))
}
const emitJsonText = () => {
emit('update:modelValue', jsonText.value)
}
const addRow = () => {
if (editMode.value !== 'table') {
return
}
rows.value.push(createEmptyRow())
syncJsonFromRows()
}
const removeRow = (index: number) => {
rows.value.splice(index, 1)
syncJsonFromRows()
}
const switchEditMode = () => {
if (editMode.value === 'table') {
syncJsonFromRows()
editMode.value = 'json'
return
}
let items: unknown[]
try {
items = parseJsonArray(jsonText.value)
} catch {
Message.warning('SNMP OID 配置必须是合法 JSON 数组,修正后才能切换到表格模式')
return
}
if (hasUnsupportedFields(items)) {
Message.warning('当前 SNMP OID JSON 包含表格不支持的字段,请在 JSON 模式下编辑')
return
}
syncRowsFromJson()
editMode.value = 'table'
}
const validate = () => {
if (editMode.value === 'table') {
const invalidIndex = findIncompleteRowIndex(rows.value)
if (invalidIndex >= 0) {
Message.warning(`SNMP OID 第 ${invalidIndex + 1} 行请填写 OID 和指标名称`)
return false
}
syncJsonFromRows()
return true
}
if (!jsonText.value.trim()) {
rows.value = []
emitValue('')
return true
}
let parsedItems: unknown[]
try {
parsedItems = parseJsonArray(jsonText.value)
} catch {
Message.warning('SNMP OID 配置必须是合法 JSON 数组')
return false
}
const invalidIndex = findInvalidJsonRowIndex(parsedItems)
if (invalidIndex >= 0) {
Message.warning(`SNMP OID 第 ${invalidIndex + 1} 行请填写 OID 和指标名称`)
return false
}
const normalizedItems = normalizeJsonItems(parsedItems)
rows.value = parseRows(JSON.stringify(normalizedItems))
emitValue(normalizedItems.length ? JSON.stringify(normalizedItems, null, 2) : '')
return true
}
const resetFromModelValue = () => {
jsonText.value = props.modelValue ?? ''
if (!jsonText.value.trim()) {
editMode.value = 'table'
rows.value = []
return
}
try {
const items = parseJsonArray(jsonText.value)
if (hasUnsupportedFields(items)) {
editMode.value = 'json'
rows.value = []
return
}
rows.value = parseRows(jsonText.value)
editMode.value = 'table'
} catch {
editMode.value = 'json'
rows.value = []
}
}
watch(
() => props.modelValue,
(value) => {
if ((value ?? '') !== jsonText.value) {
resetFromModelValue()
}
}
)
watch(
() => props.resetKey,
() => {
resetFromModelValue()
}
)
onMounted(() => {
resetFromModelValue()
})
defineExpose({
validate,
resetFromModelValue,
})
</script>
<style scoped>
.snmp-oid-label {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.snmp-oid-actions {
display: flex;
gap: 8px;
}
.snmp-oid-table {
width: 100%;
}
</style>