feat
This commit is contained in:
@@ -28,8 +28,12 @@
|
||||
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item field="server_identity" label="服务器标识">
|
||||
<a-input v-model="formData.server_identity" placeholder="请输入服务器标识" />
|
||||
<a-form-item field="server_identity" label="服务器">
|
||||
<a-select v-model="formData.server_identity" placeholder="请选择服务器" allow-search allow-clear>
|
||||
<a-option v-for="server in serverOptions" :key="server.server_identity" :value="server.server_identity">
|
||||
{{ server.name }} ({{ server.server_identity }})
|
||||
</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@@ -102,6 +106,7 @@ import { Message } from '@arco-design/web-vue'
|
||||
import type { FormInstance } from '@arco-design/web-vue'
|
||||
import { createSecurityService, updateSecurityService, SECURITY_TYPE_OPTIONS, type SecurityServiceFormData } from '@/api/ops/security'
|
||||
import { fetchPolicyOptions, type PolicyOptionItem } from '@/api/ops/alertPolicy'
|
||||
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
@@ -117,6 +122,7 @@ const emit = defineEmits(['update:visible', 'success'])
|
||||
const formRef = ref<FormInstance>()
|
||||
const confirmLoading = ref(false)
|
||||
const policyOptions = ref<PolicyOptionItem[]>([])
|
||||
const serverOptions = ref<ServerItem[]>([])
|
||||
|
||||
const isEdit = computed(() => !!props.record?.id)
|
||||
|
||||
@@ -158,6 +164,20 @@ const loadPolicyOptions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadServerOptions = async () => {
|
||||
try {
|
||||
const response: any = await fetchServerList({ page: 1, size: 1000 })
|
||||
if (response && response.details) {
|
||||
serverOptions.value = response.details.data || []
|
||||
} else {
|
||||
serverOptions.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error)
|
||||
serverOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
@@ -253,5 +273,6 @@ const handleCancel = () => {
|
||||
|
||||
onMounted(() => {
|
||||
loadPolicyOptions()
|
||||
loadServerOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -30,10 +30,9 @@ export const columns = [
|
||||
},
|
||||
{
|
||||
dataIndex: 'server_identity',
|
||||
title: '服务器标识',
|
||||
title: '服务器',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
slotName: 'serverIdentity',
|
||||
},
|
||||
{
|
||||
dataIndex: 'enabled',
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template #serverIdentity="{ record }">
|
||||
<span>{{ getServerName(record.server_identity) }}</span>
|
||||
</template>
|
||||
|
||||
<template #enabled="{ record }">
|
||||
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||
{{ record.enabled ? '已启用' : '已禁用' }}
|
||||
@@ -124,9 +128,11 @@ import {
|
||||
type SecurityServiceItem,
|
||||
type SecurityServiceListParams,
|
||||
} from '@/api/ops/security'
|
||||
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<SecurityServiceItem[]>([])
|
||||
const serverOptions = ref<ServerItem[]>([])
|
||||
const formDialogVisible = ref(false)
|
||||
const quickConfigVisible = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
@@ -277,6 +283,26 @@ const getStatusColor = (status: string) => {
|
||||
return colorMap[status] || 'gray'
|
||||
}
|
||||
|
||||
const getServerName = (serverIdentity: string) => {
|
||||
if (!serverIdentity) return '-'
|
||||
const server = serverOptions.value.find((s) => s.server_identity === serverIdentity)
|
||||
return server ? `${server.name}` : serverIdentity
|
||||
}
|
||||
|
||||
const loadServerOptions = async () => {
|
||||
try {
|
||||
const response: any = await fetchServerList({ page: 1, size: 1000 })
|
||||
if (response && response.details) {
|
||||
serverOptions.value = response.details.data || []
|
||||
} else {
|
||||
serverOptions.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载服务器列表失败:', error)
|
||||
serverOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (time?: string) => {
|
||||
if (!time) return '-'
|
||||
const date = new Date(time)
|
||||
@@ -289,6 +315,7 @@ const formatTime = (time?: string) => {
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
loadServerOptions()
|
||||
fetchSecurityServiceData()
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,19 +30,10 @@
|
||||
<a-doption value="server">服务器</a-doption>
|
||||
<a-doption value="switch">交换机</a-doption>
|
||||
<a-doption value="router">路由器</a-doption>
|
||||
<a-doption value="firewall">防火墙</a-doption>
|
||||
<a-doption value="storage">存储</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-button-group>
|
||||
|
||||
<a-tooltip v-if="props.onBatchImportAssets" content="按资产 ID 批量导入(绑定 ref_type=asset)">
|
||||
<a-button type="outline" size="small" @click="props.onBatchImportAssets">
|
||||
<icon-import :size="18" />
|
||||
<span class="btn-text">资产导入</span>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
|
||||
<!-- 布局 -->
|
||||
<a-button-group type="outline" size="small">
|
||||
<a-dropdown trigger="click" @select="props.onLayout">
|
||||
@@ -108,27 +99,23 @@ import {
|
||||
IconRefresh,
|
||||
IconDownload,
|
||||
IconRotateLeft,
|
||||
IconImport,
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
|
||||
interface Props {
|
||||
onZoomIn: () => void;
|
||||
onZoomOut: () => void;
|
||||
onFitView: () => void;
|
||||
onAddDevice: (value: string | number | Record<string, any> | undefined) => void;
|
||||
onLayout: (value: string | number | Record<string, any> | undefined) => void;
|
||||
onEdgeStyle: (value: string | number | Record<string, any> | undefined) => void;
|
||||
onRefresh: () => void;
|
||||
onExport: () => void;
|
||||
onReset?: () => void;
|
||||
/** 打开「批量导入资产」对话框(无拓扑 ID 时不传) */
|
||||
onBatchImportAssets?: () => void;
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
onFitView: () => void
|
||||
onAddDevice: (value: string | number | Record<string, any> | undefined) => void
|
||||
onLayout: (value: string | number | Record<string, any> | undefined) => void
|
||||
onEdgeStyle: (value: string | number | Record<string, any> | undefined) => void
|
||||
onRefresh: () => void
|
||||
onExport: () => void
|
||||
onReset?: () => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
onReset: undefined,
|
||||
onBatchImportAssets: undefined,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@@ -25,8 +25,6 @@ export const DEVICE_TYPE_CONFIG: Record<DeviceType, { icon: any; label: string;
|
||||
/** 扩展设备类型配置(包含更多设备) */
|
||||
export const EXTENDED_DEVICE_CONFIG = {
|
||||
...DEVICE_TYPE_CONFIG,
|
||||
firewall: { icon: IconShield, label: '防火墙', color: '#DC2626' },
|
||||
storage: { icon: IconDatabase, label: '存储设备', color: '#7C3AED' },
|
||||
mobile: { icon: IconDeviceMobile, label: '移动设备', color: '#EC4899' },
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
@refresh="refreshTopology"
|
||||
@export="exportTopology"
|
||||
@reset="resetTopology"
|
||||
:on-batch-import-assets="currentTopologyId ? openBatchImportAssets : undefined"
|
||||
/>
|
||||
|
||||
<!-- Vue Flow 画布 -->
|
||||
@@ -46,7 +45,7 @@
|
||||
>
|
||||
<background pattern-color="#aaa" :gap="16" />
|
||||
<mini-map :node-color="getNodeColor" node-stroke-color="#555" />
|
||||
<controls />
|
||||
<!-- <controls /> -->
|
||||
</vue-flow>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,26 +85,6 @@
|
||||
/>
|
||||
|
||||
<delete-confirm-dialog v-model:visible="deleteEdgeDialogOpen" node-name="链路" @confirm="handleDeleteEdgeConfirm" />
|
||||
|
||||
<a-modal
|
||||
v-model:visible="batchImportAssetsOpen"
|
||||
title="批量导入资产节点"
|
||||
@ok="handleBatchImportAssetsConfirm"
|
||||
@cancel="batchImportAssetsOpen = false"
|
||||
>
|
||||
<p class="text-muted" style="margin-bottom: 8px; font-size: 12px; color: var(--color-text-3)">
|
||||
调用 DC-Control
|
||||
<code>/topologies/:id/nodes/batch-import</code>
|
||||
,节点将绑定
|
||||
<code>ref_type=asset</code>
|
||||
。
|
||||
</p>
|
||||
<a-textarea
|
||||
v-model="batchImportAssetIdsText"
|
||||
placeholder="请输入资产 ID,逗号或换行分隔,例如:1,2,3"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -180,45 +159,6 @@ const edgeActionDialogOpen = ref(false)
|
||||
const edgeEditDialogOpen = ref(false)
|
||||
const deleteEdgeDialogOpen = ref(false)
|
||||
|
||||
const batchImportAssetsOpen = ref(false)
|
||||
const batchImportAssetIdsText = ref('')
|
||||
|
||||
const openBatchImportAssets = () => {
|
||||
batchImportAssetIdsText.value = ''
|
||||
batchImportAssetsOpen.value = true
|
||||
}
|
||||
|
||||
const handleBatchImportAssetsConfirm = async () => {
|
||||
const id = currentTopologyId.value
|
||||
if (!id) {
|
||||
Message.warning('请先通过路由选择拓扑(?id=)')
|
||||
batchImportAssetsOpen.value = false
|
||||
return
|
||||
}
|
||||
const raw = batchImportAssetIdsText.value
|
||||
.split(/[\s,,;;]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const assetIds = [...new Set(raw.map((x) => parseInt(x, 10)).filter((n) => !Number.isNaN(n)))]
|
||||
if (assetIds.length === 0) {
|
||||
Message.warning('请输入至少一个有效的资产 ID')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res: any = await TopoAPI.batchImportAssetNodes(id, assetIds)
|
||||
if (res?.code === 0) {
|
||||
Message.success(`已导入 ${res.details?.imported ?? assetIds.length} 个节点请求已提交`)
|
||||
batchImportAssetsOpen.value = false
|
||||
await refreshTopology()
|
||||
} else {
|
||||
Message.error(res?.message || '导入失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Message.error('导入请求失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 布局钩子
|
||||
const { applyLayout } = useTopoLayout()
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user