This commit is contained in:
2026-04-11 22:22:07 +08:00
parent c72af0bfa7
commit 5f4111aeb1
7 changed files with 65 additions and 93 deletions

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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">

View File

@@ -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' },
} }

View File

@@ -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