feat
This commit is contained in:
@@ -28,8 +28,12 @@
|
|||||||
|
|
||||||
<a-row :gutter="20">
|
<a-row :gutter="20">
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<a-form-item field="server_identity" label="服务器标识">
|
<a-form-item field="server_identity" label="服务器">
|
||||||
<a-input v-model="formData.server_identity" placeholder="请输入服务器标识" />
|
<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-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
@@ -102,6 +106,7 @@ import { Message } from '@arco-design/web-vue'
|
|||||||
import type { FormInstance } 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 { createSecurityService, updateSecurityService, SECURITY_TYPE_OPTIONS, type SecurityServiceFormData } from '@/api/ops/security'
|
||||||
import { fetchPolicyOptions, type PolicyOptionItem } from '@/api/ops/alertPolicy'
|
import { fetchPolicyOptions, type PolicyOptionItem } from '@/api/ops/alertPolicy'
|
||||||
|
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@@ -117,6 +122,7 @@ const emit = defineEmits(['update:visible', 'success'])
|
|||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const confirmLoading = ref(false)
|
const confirmLoading = ref(false)
|
||||||
const policyOptions = ref<PolicyOptionItem[]>([])
|
const policyOptions = ref<PolicyOptionItem[]>([])
|
||||||
|
const serverOptions = ref<ServerItem[]>([])
|
||||||
|
|
||||||
const isEdit = computed(() => !!props.record?.id)
|
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(
|
watch(
|
||||||
() => props.visible,
|
() => props.visible,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -253,5 +273,6 @@ const handleCancel = () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPolicyOptions()
|
loadPolicyOptions()
|
||||||
|
loadServerOptions()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export const columns = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
dataIndex: 'server_identity',
|
dataIndex: 'server_identity',
|
||||||
title: '服务器标识',
|
title: '服务器',
|
||||||
width: 150,
|
width: 150,
|
||||||
ellipsis: true,
|
slotName: 'serverIdentity',
|
||||||
tooltip: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
dataIndex: 'enabled',
|
dataIndex: 'enabled',
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #serverIdentity="{ record }">
|
||||||
|
<span>{{ getServerName(record.server_identity) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #enabled="{ record }">
|
<template #enabled="{ record }">
|
||||||
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
<a-tag :color="record.enabled ? 'green' : 'gray'">
|
||||||
{{ record.enabled ? '已启用' : '已禁用' }}
|
{{ record.enabled ? '已启用' : '已禁用' }}
|
||||||
@@ -124,9 +128,11 @@ import {
|
|||||||
type SecurityServiceItem,
|
type SecurityServiceItem,
|
||||||
type SecurityServiceListParams,
|
type SecurityServiceListParams,
|
||||||
} from '@/api/ops/security'
|
} from '@/api/ops/security'
|
||||||
|
import { fetchServerList, type ServerItem } from '@/api/ops/server'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const tableData = ref<SecurityServiceItem[]>([])
|
const tableData = ref<SecurityServiceItem[]>([])
|
||||||
|
const serverOptions = ref<ServerItem[]>([])
|
||||||
const formDialogVisible = ref(false)
|
const formDialogVisible = ref(false)
|
||||||
const quickConfigVisible = ref(false)
|
const quickConfigVisible = ref(false)
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
@@ -277,6 +283,26 @@ const getStatusColor = (status: string) => {
|
|||||||
return colorMap[status] || 'gray'
|
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) => {
|
const formatTime = (time?: string) => {
|
||||||
if (!time) return '-'
|
if (!time) return '-'
|
||||||
const date = new Date(time)
|
const date = new Date(time)
|
||||||
@@ -289,6 +315,7 @@ const formatTime = (time?: string) => {
|
|||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadServerOptions()
|
||||||
fetchSecurityServiceData()
|
fetchSecurityServiceData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -30,19 +30,10 @@
|
|||||||
<a-doption value="server">服务器</a-doption>
|
<a-doption value="server">服务器</a-doption>
|
||||||
<a-doption value="switch">交换机</a-doption>
|
<a-doption value="switch">交换机</a-doption>
|
||||||
<a-doption value="router">路由器</a-doption>
|
<a-doption value="router">路由器</a-doption>
|
||||||
<a-doption value="firewall">防火墙</a-doption>
|
|
||||||
<a-doption value="storage">存储</a-doption>
|
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-button-group>
|
</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-button-group type="outline" size="small">
|
||||||
<a-dropdown trigger="click" @select="props.onLayout">
|
<a-dropdown trigger="click" @select="props.onLayout">
|
||||||
@@ -108,27 +99,23 @@ import {
|
|||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconRotateLeft,
|
IconRotateLeft,
|
||||||
IconImport,
|
} from '@arco-design/web-vue/es/icon'
|
||||||
} from '@arco-design/web-vue/es/icon';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onZoomIn: () => void;
|
onZoomIn: () => void
|
||||||
onZoomOut: () => void;
|
onZoomOut: () => void
|
||||||
onFitView: () => void;
|
onFitView: () => void
|
||||||
onAddDevice: (value: string | number | Record<string, any> | undefined) => void;
|
onAddDevice: (value: string | number | Record<string, any> | undefined) => void
|
||||||
onLayout: (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;
|
onEdgeStyle: (value: string | number | Record<string, any> | undefined) => void
|
||||||
onRefresh: () => void;
|
onRefresh: () => void
|
||||||
onExport: () => void;
|
onExport: () => void
|
||||||
onReset?: () => void;
|
onReset?: () => void
|
||||||
/** 打开「批量导入资产」对话框(无拓扑 ID 时不传) */
|
|
||||||
onBatchImportAssets?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
onReset: undefined,
|
onReset: undefined,
|
||||||
onBatchImportAssets: undefined,
|
})
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ export const DEVICE_TYPE_CONFIG: Record<DeviceType, { icon: any; label: string;
|
|||||||
/** 扩展设备类型配置(包含更多设备) */
|
/** 扩展设备类型配置(包含更多设备) */
|
||||||
export const EXTENDED_DEVICE_CONFIG = {
|
export const EXTENDED_DEVICE_CONFIG = {
|
||||||
...DEVICE_TYPE_CONFIG,
|
...DEVICE_TYPE_CONFIG,
|
||||||
firewall: { icon: IconShield, label: '防火墙', color: '#DC2626' },
|
|
||||||
storage: { icon: IconDatabase, label: '存储设备', color: '#7C3AED' },
|
|
||||||
mobile: { icon: IconDeviceMobile, label: '移动设备', color: '#EC4899' },
|
mobile: { icon: IconDeviceMobile, label: '移动设备', color: '#EC4899' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
@refresh="refreshTopology"
|
@refresh="refreshTopology"
|
||||||
@export="exportTopology"
|
@export="exportTopology"
|
||||||
@reset="resetTopology"
|
@reset="resetTopology"
|
||||||
:on-batch-import-assets="currentTopologyId ? openBatchImportAssets : undefined"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Vue Flow 画布 -->
|
<!-- Vue Flow 画布 -->
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
>
|
>
|
||||||
<background pattern-color="#aaa" :gap="16" />
|
<background pattern-color="#aaa" :gap="16" />
|
||||||
<mini-map :node-color="getNodeColor" node-stroke-color="#555" />
|
<mini-map :node-color="getNodeColor" node-stroke-color="#555" />
|
||||||
<controls />
|
<!-- <controls /> -->
|
||||||
</vue-flow>
|
</vue-flow>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,26 +85,6 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<delete-confirm-dialog v-model:visible="deleteEdgeDialogOpen" node-name="链路" @confirm="handleDeleteEdgeConfirm" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -180,45 +159,6 @@ const edgeActionDialogOpen = ref(false)
|
|||||||
const edgeEditDialogOpen = ref(false)
|
const edgeEditDialogOpen = ref(false)
|
||||||
const deleteEdgeDialogOpen = 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()
|
const { applyLayout } = useTopoLayout()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user