From 8f3dd3e43e4339f512bb1f7215a8758810fd15d6 Mon Sep 17 00:00:00 2001 From: zxr <271055687@qq.com> Date: Mon, 29 Jun 2026 21:47:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E6=8B=93=E6=89=91=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ops/businessSystem.ts | 46 +- src/api/ops/dcControl.ts | 23 +- src/locale/en-US.ts | 1 + src/locale/zh-CN.ts | 1 + src/router/local-menu-flat.ts | 16 + src/router/local-menu-items.ts | 17 + src/router/routes/modules/ops.ts | 11 + .../ops/pages/business-topology/index.vue | 1138 +++++++++++++++++ .../ops/pages/business-topology/transform.ts | 418 ++++++ .../ops/pages/business-topology/types.ts | 67 + .../ops/pages/resource-context/index.vue | 231 +++- 11 files changed, 1955 insertions(+), 14 deletions(-) create mode 100644 src/views/ops/pages/business-topology/index.vue create mode 100644 src/views/ops/pages/business-topology/transform.ts create mode 100644 src/views/ops/pages/business-topology/types.ts diff --git a/src/api/ops/businessSystem.ts b/src/api/ops/businessSystem.ts index 7bb6473..7940552 100644 --- a/src/api/ops/businessSystem.ts +++ b/src/api/ops/businessSystem.ts @@ -71,6 +71,27 @@ export interface BusinessResourceMember { description?: string } +/** 业务拓扑资源 */ +export interface BusinessTopologyResource { + id: number + resource_uid: string + resource_category: string + service_identity: string + display_name: string + description?: string + status: string + business_system_id?: number + owner_user_id?: number + department_id?: number + asset_id?: number + tags?: string + source_service?: string + source_table?: string + source_id?: number + created_at?: string + updated_at?: string +} + /** 业务拓扑节点 */ export interface BusinessTopologyNode { id?: number @@ -102,10 +123,10 @@ export interface BusinessDependency { /** 业务拓扑 */ export interface BusinessTopology { business_system_id: number - resources: unknown[] - dependencies: BusinessDependency[] - nodes: BusinessTopologyNode[] - links: BusinessTopologyEdge[] + resources: BusinessTopologyResource[] | null + dependencies: BusinessDependency[] | null + nodes: BusinessTopologyNode[] | null + links: BusinessTopologyEdge[] | null } /** 业务时间线项 */ @@ -146,7 +167,7 @@ export interface BusinessDocumentLink { /** 列表响应 */ export interface ListResult { - list: T[] + list: T[] | null count: number } @@ -166,6 +187,21 @@ export const fetchBusinessHealth = (id: number) => export const fetchBusinessMembers = (id: number) => request.get<{ code?: number; details?: ListResult; message?: string }>(`/DC-Control/v1/business-systems/${id}/resources`) +export interface BusinessResourceMemberPayload { + resource_uid?: string + resource_id?: number + role?: string + criticality?: string + weight?: number + description?: string +} + +export const createBusinessMember = (id: number, data: BusinessResourceMemberPayload) => + request.post<{ code?: number; details?: BusinessResourceMember; message?: string }>(`/DC-Control/v1/business-systems/${id}/resources`, data) + +export const deleteBusinessMember = (id: number, memberId: number) => + request.delete<{ code?: number; details?: { message: string }; message?: string }>(`/DC-Control/v1/business-systems/${id}/resources/${memberId}`) + /** 获取业务拓扑 */ export const fetchBusinessTopology = (id: number) => request.get<{ code?: number; details?: BusinessTopology; message?: string }>(`/DC-Control/v1/business-systems/${id}/topology`) diff --git a/src/api/ops/dcControl.ts b/src/api/ops/dcControl.ts index bfaf975..c4ee9e4 100644 --- a/src/api/ops/dcControl.ts +++ b/src/api/ops/dcControl.ts @@ -80,6 +80,25 @@ export interface ControlResource { updated_at?: string } +export interface ControlResourcePayload { + resource_category?: string + service_identity?: string + display_name?: string + description?: string + status?: string + business_system_id?: number | null + owner_user_id?: number | null + department_id?: number | null + asset_id?: number | null + datacenter_id?: number | null + room_id?: number | null + rack_id?: number | null + unit_start?: number | null + unit_end?: number | null + location_ref?: string | null + tags?: string +} + export interface ControlResourceType { id: number resource_category: string @@ -165,10 +184,10 @@ export const fetchControlResources = (params?: ResourceListParams) => export const fetchControlResourceOptions = (params?: { resource_category?: string }) => request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', { params }) -export const createControlResource = (data: Partial) => +export const createControlResource = (data: ControlResourcePayload) => request.post<{ code?: number; details?: ControlResource; message?: string }>('/DC-Control/v1/resources', data) -export const updateControlResource = (id: number, data: Partial) => +export const updateControlResource = (id: number, data: ControlResourcePayload) => request.put<{ code?: number; details?: ControlResource; message?: string }>(`/DC-Control/v1/resources/${id}`, data) export const deleteControlResource = (id: number) => diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index 7b7c509..42868d2 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -29,6 +29,7 @@ export default { 'menu.ops.netarch': 'Network Architecture', 'menu.ops.netarch.topoGroup': 'Topology Group Management', 'menu.ops.netarch.topo': 'Topology', + 'menu.ops.businessTopology': 'Business Topology', 'menu.ops.systemSettings': 'System Settings', 'menu.ops.systemSettings.menuManagement': 'Menu Management', 'menu.ops.systemSettings.systemLogs': 'System Logs', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index d73d41d..15b1064 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -29,6 +29,7 @@ export default { 'menu.ops.netarch': '网络架构', 'menu.ops.netarch.topoGroup': '拓扑组管理', 'menu.ops.netarch.topo': '拓扑图', + 'menu.ops.businessTopology': '业务拓扑', 'menu.ops.systemSettings': '系统设置', 'menu.ops.systemSettings.menuManagement': '菜单管理', 'menu.ops.systemSettings.systemLogs': '系统日志', diff --git a/src/router/local-menu-flat.ts b/src/router/local-menu-flat.ts index b2c46a9..ccf0870 100644 --- a/src/router/local-menu-flat.ts +++ b/src/router/local-menu-flat.ts @@ -108,6 +108,22 @@ export const localMenuFlatItems: MenuItem[] = [ hide_menu: true, created_at: '2026-06-25T10:00:00+08:00', }, + { + id: 12063, + identity: '019ca000-0001-7000-8000-000000000063', + title: '业务拓扑', + title_en: 'Business Topology', + code: 'ops:业务系统视图:业务拓扑', + description: '业务依赖、资源链路、影响范围和时间线拓扑', + app_id: 2, + parent_id: 12060, + menu_path: '/business-topology', + component: 'ops/pages/business-topology', + menu_icon: 'Cluster', + type: 1, + sort_key: 4.5, + created_at: '2026-06-29T10:00:00+08:00', + }, { id: 16, identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390', diff --git a/src/router/local-menu-items.ts b/src/router/local-menu-items.ts index 9917023..41ac35a 100644 --- a/src/router/local-menu-items.ts +++ b/src/router/local-menu-items.ts @@ -130,6 +130,23 @@ export const localMenuItems: MenuItem[] = [ created_at: '2026-06-25T10:00:00+08:00', children: [], }, + { + id: 12063, + identity: '019ca000-0001-7000-8000-000000000063', + title: '业务拓扑', + title_en: 'Business Topology', + code: 'ops:业务系统视图:业务拓扑', + description: '业务依赖、资源链路、影响范围和时间线拓扑', + app_id: 2, + parent_id: 12060, + menu_path: '/business-topology', + component: 'ops/pages/business-topology', + menu_icon: 'Cluster', + type: 1, + sort_key: 3, + created_at: '2026-06-29T10:00:00+08:00', + children: [], + }, { id: 12062, identity: '019ca000-0001-7000-8000-000000000062', diff --git a/src/router/routes/modules/ops.ts b/src/router/routes/modules/ops.ts index 40d7b8d..ba6805c 100644 --- a/src/router/routes/modules/ops.ts +++ b/src/router/routes/modules/ops.ts @@ -68,6 +68,17 @@ const OPS: AppRouteRecordRaw = { hideInMenu: true, }, }, + { + path: 'business-topology', + alias: ['/business-topology'], + name: 'OpsBusinessTopology', + component: () => import('@/views/ops/pages/business-topology/index.vue'), + meta: { + locale: 'menu.ops.businessTopology', + requiresAuth: true, + roles: ['*'], + }, + }, { path: 'big-screen', name: 'OpsBigScreen', diff --git a/src/views/ops/pages/business-topology/index.vue b/src/views/ops/pages/business-topology/index.vue new file mode 100644 index 0000000..0ea8009 --- /dev/null +++ b/src/views/ops/pages/business-topology/index.vue @@ -0,0 +1,1138 @@ + + + + + + + diff --git a/src/views/ops/pages/business-topology/transform.ts b/src/views/ops/pages/business-topology/transform.ts new file mode 100644 index 0000000..f995a65 --- /dev/null +++ b/src/views/ops/pages/business-topology/transform.ts @@ -0,0 +1,418 @@ +import type { + BusinessDependency, + BusinessHealth, + BusinessSystemItem, + BusinessTopology, + BusinessTopologyEdge, + BusinessTopologyNode, +} from '@/api/ops/businessSystem' +import type { + BusinessTopologyFlowEdge, + BusinessTopologyFlowNode, + BusinessTopologyGraph, + BusinessTopologyHealthFilter, + BusinessTopologyViewMode, +} from './types' + +const HEALTHY_THRESHOLD = 90 +const DEGRADED_THRESHOLD = 60 + +export function scoreValue(score?: number): number { + if (typeof score !== 'number' || Number.isNaN(score)) { + return 0 + } + + return Math.max(0, Math.min(100, Math.round(score))) +} + +export function healthLevelFromScore(score?: number): 'healthy' | 'degraded' | 'critical' { + const value = scoreValue(score) + + if (value >= HEALTHY_THRESHOLD) { + return 'healthy' + } + + if (value >= DEGRADED_THRESHOLD) { + return 'degraded' + } + + return 'critical' +} + +export function statusColor(status?: string): string { + const normalized = status?.trim().toLowerCase() + + if (!normalized) { + return '#8c8c8c' + } + + if (['healthy', 'normal', 'ok', 'online', 'active', 'running', 'enabled'].includes(normalized)) { + return '#52c41a' + } + + if (['degraded', 'warning', 'warn', 'pending', 'maintenance', 'medium'].includes(normalized)) { + return '#faad14' + } + + if (['critical', 'error', 'failed', 'failure', 'offline', 'inactive', 'disabled', 'high'].includes(normalized)) { + return '#ff4d4f' + } + + return '#1677ff' +} + +export function healthFilterMatch(item: BusinessSystemItem, filter: BusinessTopologyHealthFilter): boolean { + if (filter === 'all') { + return true + } + + const level = normalizedHealthLevel(item) + + if (filter === 'unknown') { + return level === 'unknown' + } + + return level === filter +} + +export function buildBusinessTopologyGraph(input: { + currentBusiness: BusinessSystemItem + topology: BusinessTopology + health?: BusinessHealth | null + mode: BusinessTopologyViewMode + keyword?: string +}): BusinessTopologyGraph { + const { currentBusiness, topology, health, mode } = input + const keyword = input.keyword?.trim().toLowerCase() + const currentBusinessId = businessNodeId(currentBusiness.id) + const includeResourceTopology = mode === 'resource' + const emphasizeDependencyEdges = mode === 'dependency' + const dependencies = topology.dependencies ?? [] + const topologyNodes = topology.nodes ?? [] + const topologyLinks = topology.links ?? [] + const nodes: BusinessTopologyFlowNode[] = [] + const edges: BusinessTopologyFlowEdge[] = [] + const businessNodeIds = new Set() + const resourceNodeIds = new Map() + const resourceFlowNodeIds = new Set() + const resourceContainmentEdgeIds = new Set() + const matchedNodeIds = new Set() + + const addBusinessNode = (node: BusinessTopologyFlowNode) => { + if (businessNodeIds.has(node.id)) { + return + } + + businessNodeIds.add(node.id) + nodes.push(node) + + if (keyword && nodeMatchesKeyword(node, keyword)) { + matchedNodeIds.add(node.id) + } + } + + addBusinessNode(createCurrentBusinessNode(currentBusiness, health)) + + dependencies.forEach((dependency, index) => { + const sourceBusinessId = dependency.business_system_id + const targetBusinessId = dependency.depends_on_business_system_id + const externalIds = [sourceBusinessId, targetBusinessId].filter((id) => id !== currentBusiness.id) + + externalIds.forEach((businessId, externalIndex) => { + addBusinessNode(createDependencyBusinessNode(businessId, dependency, index, externalIndex)) + }) + + const edge: BusinessTopologyFlowEdge = { + id: `business-dependency:${dependency.id}:${sourceBusinessId}->${targetBusinessId}`, + source: businessNodeId(sourceBusinessId), + target: businessNodeId(targetBusinessId), + animated: false, + data: { + kind: 'business_dependency', + label: dependency.dependency_type || '依赖', + status: dependency.criticality, + dependency, + }, + label: dependency.dependency_type || '依赖', + ...(emphasizeDependencyEdges ? { style: dependencyEdgeStyle(dependency) } : {}), + } + + edge.animated = shouldAnimateEdge(edge, matchedNodeIds, keyword) || (emphasizeDependencyEdges && !keyword) + edges.push(edge) + }) + + if (includeResourceTopology) { + topologyNodes.forEach((topologyNode, index) => { + const key = resourceKey(topologyNode) + + if (!key) { + return + } + + const id = resourceNodeId(key) + rememberResourceNodeIds(resourceNodeIds, topologyNode, id) + + if (resourceFlowNodeIds.has(id)) { + return + } + + resourceFlowNodeIds.add(id) + const node = createResourceNode(topologyNode, index) + nodes.push(node) + + if (keyword && nodeMatchesKeyword(node, keyword)) { + matchedNodeIds.add(node.id) + } + }) + + topologyNodes.forEach((topologyNode) => { + const target = resourceNodeIds.get(resourceKey(topologyNode)) + + if (!target) { + return + } + + const edgeId = `business-resource:${currentBusiness.id}->${target}` + + if (resourceContainmentEdgeIds.has(edgeId)) { + return + } + + resourceContainmentEdgeIds.add(edgeId) + const edge: BusinessTopologyFlowEdge = { + id: edgeId, + source: currentBusinessId, + target, + animated: false, + data: { + kind: 'resource_link', + label: '包含', + }, + label: '包含', + } + + edge.animated = shouldAnimateEdge(edge, matchedNodeIds, keyword) + edges.push(edge) + }) + + topologyLinks.forEach((link, index) => { + const source = resourceNodeIds.get(link.source_node_id) + const target = resourceNodeIds.get(link.target_node_id) + + if (!source || !target) { + return + } + + edges.push(createResourceLinkEdge(link, index, source, target, matchedNodeIds, keyword)) + }) + } + + return { nodes, edges } +} + +function normalizedHealthLevel(item: BusinessSystemItem): 'healthy' | 'degraded' | 'critical' | 'unknown' { + const level = item.health_level?.trim().toLowerCase() + + if (level === 'healthy' || level === 'degraded' || level === 'critical') { + return level + } + + if (typeof item.health_score === 'number' && !Number.isNaN(item.health_score)) { + return healthLevelFromScore(item.health_score) + } + + return 'unknown' +} + +function createCurrentBusinessNode(currentBusiness: BusinessSystemItem, health?: BusinessHealth | null): BusinessTopologyFlowNode { + const healthScore = health?.health.score ?? currentBusiness.health_score + const label = currentBusiness.name || currentBusiness.code || `业务系统 ${currentBusiness.id}` + + return { + id: businessNodeId(currentBusiness.id), + type: 'businessTopology', + position: { x: 0, y: 0 }, + data: { + kind: 'business', + label, + status: currentBusiness.status, + ...(typeof healthScore === 'number' ? { healthScore } : {}), + businessSystem: currentBusiness, + }, + } +} + +function createDependencyBusinessNode( + businessId: number, + dependency: BusinessDependency, + index: number, + externalIndex: number, +): BusinessTopologyFlowNode { + const isUpstream = dependency.depends_on_business_system_id === businessId + const x = isUpstream ? 320 : -320 + const y = (index + externalIndex) * 120 - 120 + + return { + id: businessNodeId(businessId), + type: 'businessTopology', + position: { x, y }, + data: { + kind: 'business', + label: `业务系统 ${businessId}`, + status: dependency.criticality || dependency.dependency_type || 'unknown', + dependency, + }, + } +} + +function createResourceNode(topologyNode: BusinessTopologyNode, index: number): BusinessTopologyFlowNode { + const key = resourceKey(topologyNode) + + return { + id: resourceNodeId(key), + type: 'businessTopology', + position: { + x: (index % 4) * 240 - 360, + y: 260 + Math.floor(index / 4) * 140, + }, + data: { + kind: 'resource', + label: topologyNode.label || topologyNode.resource_uid || topologyNode.node_id, + status: topologyNode.status, + ...(topologyNode.resource_uid ? { resourceUID: topologyNode.resource_uid } : {}), + ...(topologyNode.type ? { resourceType: topologyNode.type } : {}), + topologyNode, + }, + } +} + +function createResourceLinkEdge( + link: BusinessTopologyEdge, + index: number, + source: string, + target: string, + matchedNodeIds: Set, + keyword?: string, +): BusinessTopologyFlowEdge { + const edge: BusinessTopologyFlowEdge = { + id: `resource-link:${link.id ?? index}:${source}->${target}`, + source, + target, + animated: false, + data: { + kind: 'resource_link', + label: '关联', + topologyLink: link, + }, + label: '关联', + } + + edge.animated = shouldAnimateEdge(edge, matchedNodeIds, keyword) + return edge +} + +function dependencyEdgeStyle(dependency: BusinessDependency): NonNullable { + return { + stroke: dependencyCriticalityColor(dependency.criticality), + strokeWidth: 3, + } +} + +function dependencyCriticalityColor(criticality?: string): string { + const normalized = criticality?.trim().toLowerCase() + + if (!normalized) { + return '#f53f3f' + } + + if (['critical', 'high', 'error', 'failed', 'failure'].includes(normalized)) { + return '#f53f3f' + } + + if (['medium', 'warning', 'warn', 'degraded'].includes(normalized)) { + return '#faad14' + } + + if (['low', 'healthy', 'normal', 'ok'].includes(normalized)) { + return '#1677ff' + } + + return '#f53f3f' +} + +function businessNodeId(id: number): string { + return `business:${id}` +} + +function resourceNodeId(id: string): string { + return `resource:${id}` +} + +function resourceKey(node: BusinessTopologyNode): string { + return node.node_id || node.resource_uid +} + +function rememberResourceNodeIds(resourceNodeIds: Map, node: BusinessTopologyNode, id: string) { + const aliases = [resourceKey(node), node.node_id, node.resource_uid] + + aliases.forEach((alias) => { + if (alias && !resourceNodeIds.has(alias)) { + resourceNodeIds.set(alias, id) + } + }) +} + +function nodeMatchesKeyword(node: BusinessTopologyFlowNode, keyword: string): boolean { + const data = node.data + + if (!data) { + return node.id.toLowerCase().includes(keyword) + } + + const values: Array = [node.id, data.label, data.status] + + if (data.kind === 'business') { + values.push(data.businessSystem?.code, data.businessSystem?.name, data.dependency?.dependency_type, data.dependency?.description) + } else { + values.push(data.resourceUID, data.resourceType, data.topologyNode?.node_id) + } + + return values.some((value) => value?.toString().toLowerCase().includes(keyword)) +} + +function shouldAnimateEdge(edge: BusinessTopologyFlowEdge, matchedNodeIds: Set, keyword?: string): boolean { + if (!keyword) { + return false + } + + return matchedNodeIds.has(edge.source) || matchedNodeIds.has(edge.target) || edgeMatchesKeyword(edge, keyword) +} + +function edgeMatchesKeyword(edge: BusinessTopologyFlowEdge, keyword: string): boolean { + const data = edge.data + const values: Array = [ + edge.id, + edge.source, + edge.target, + typeof edge.label === 'string' ? edge.label : undefined, + data?.label, + data?.status, + ] + + if (data?.kind === 'business_dependency') { + values.push( + data.dependency?.id, + data.dependency?.business_system_id, + data.dependency?.depends_on_business_system_id, + data.dependency?.dependency_type, + data.dependency?.criticality, + data.dependency?.description, + ) + } + + if (data?.kind === 'resource_link') { + values.push(data.topologyLink?.id, data.topologyLink?.source_node_id, data.topologyLink?.target_node_id, data.topologyLink?.topology_id) + } + + return values.some((value) => value?.toString().toLowerCase().includes(keyword)) +} diff --git a/src/views/ops/pages/business-topology/types.ts b/src/views/ops/pages/business-topology/types.ts new file mode 100644 index 0000000..dd0e22a --- /dev/null +++ b/src/views/ops/pages/business-topology/types.ts @@ -0,0 +1,67 @@ +import type { Edge, Node } from '@vue-flow/core' +import type { BusinessDependency, BusinessSystemItem, BusinessTopologyEdge, BusinessTopologyNode } from '@/api/ops/businessSystem' + +export type BusinessTopologyViewMode = 'business' | 'resource' | 'dependency' + +export type BusinessTopologyHealthFilter = 'all' | 'healthy' | 'degraded' | 'critical' | 'unknown' + +export type BusinessTopologyNodeKind = 'business' | 'resource' + +export type BusinessTopologyEdgeKind = 'business_dependency' | 'resource_link' + +export interface BusinessTopologyBusinessNodeData { + kind: 'business' + label: string + status: string + healthScore?: number + businessSystem?: BusinessSystemItem + dependency?: BusinessDependency + resourceUID?: never + resourceType?: never + topologyNode?: never +} + +export interface BusinessTopologyResourceNodeData { + kind: 'resource' + label: string + status: string + resourceUID?: string + resourceType?: string + topologyNode?: BusinessTopologyNode + healthScore?: never + businessSystem?: never + dependency?: never +} + +export type BusinessTopologyGraphNodeData = BusinessTopologyBusinessNodeData | BusinessTopologyResourceNodeData + +export interface BusinessTopologyDependencyEdgeData { + kind: 'business_dependency' + label: string + status?: string + dependency?: BusinessDependency + topologyLink?: never +} + +export interface BusinessTopologyResourceLinkEdgeData { + kind: 'resource_link' + label: string + status?: string + topologyLink?: BusinessTopologyEdge + dependency?: never +} + +export type BusinessTopologyGraphEdgeData = BusinessTopologyDependencyEdgeData | BusinessTopologyResourceLinkEdgeData + +export type BusinessTopologyFlowNode = Node + +export type BusinessTopologyFlowEdge = Edge + +export interface BusinessTopologyGraph { + nodes: BusinessTopologyFlowNode[] + edges: BusinessTopologyFlowEdge[] +} + +export type BusinessTopologySelection = + | { type: 'node'; id: string; data: BusinessTopologyGraphNodeData } + | { type: 'edge'; id: string; data: BusinessTopologyGraphEdgeData } diff --git a/src/views/ops/pages/resource-context/index.vue b/src/views/ops/pages/resource-context/index.vue index eb15d87..8e1deb4 100644 --- a/src/views/ops/pages/resource-context/index.vue +++ b/src/views/ops/pages/resource-context/index.vue @@ -28,9 +28,13 @@ - + @@ -118,23 +130,71 @@ :pagination="false" /> + + + + + + + + + + + + + + + normal + online + offline + error + unknown + + + + + {{ item.name }} + + + + + + + + + + + + +