新增 业务拓扑相关
This commit is contained in:
@@ -71,6 +71,27 @@ export interface BusinessResourceMember {
|
|||||||
description?: string
|
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 {
|
export interface BusinessTopologyNode {
|
||||||
id?: number
|
id?: number
|
||||||
@@ -102,10 +123,10 @@ export interface BusinessDependency {
|
|||||||
/** 业务拓扑 */
|
/** 业务拓扑 */
|
||||||
export interface BusinessTopology {
|
export interface BusinessTopology {
|
||||||
business_system_id: number
|
business_system_id: number
|
||||||
resources: unknown[]
|
resources: BusinessTopologyResource[] | null
|
||||||
dependencies: BusinessDependency[]
|
dependencies: BusinessDependency[] | null
|
||||||
nodes: BusinessTopologyNode[]
|
nodes: BusinessTopologyNode[] | null
|
||||||
links: BusinessTopologyEdge[]
|
links: BusinessTopologyEdge[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 业务时间线项 */
|
/** 业务时间线项 */
|
||||||
@@ -146,7 +167,7 @@ export interface BusinessDocumentLink {
|
|||||||
|
|
||||||
/** 列表响应 */
|
/** 列表响应 */
|
||||||
export interface ListResult<T> {
|
export interface ListResult<T> {
|
||||||
list: T[]
|
list: T[] | null
|
||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +187,21 @@ export const fetchBusinessHealth = (id: number) =>
|
|||||||
export const fetchBusinessMembers = (id: number) =>
|
export const fetchBusinessMembers = (id: number) =>
|
||||||
request.get<{ code?: number; details?: ListResult<BusinessResourceMember>; message?: string }>(`/DC-Control/v1/business-systems/${id}/resources`)
|
request.get<{ code?: number; details?: ListResult<BusinessResourceMember>; 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) =>
|
export const fetchBusinessTopology = (id: number) =>
|
||||||
request.get<{ code?: number; details?: BusinessTopology; message?: string }>(`/DC-Control/v1/business-systems/${id}/topology`)
|
request.get<{ code?: number; details?: BusinessTopology; message?: string }>(`/DC-Control/v1/business-systems/${id}/topology`)
|
||||||
|
|||||||
@@ -80,6 +80,25 @@ export interface ControlResource {
|
|||||||
updated_at?: string
|
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 {
|
export interface ControlResourceType {
|
||||||
id: number
|
id: number
|
||||||
resource_category: string
|
resource_category: string
|
||||||
@@ -165,10 +184,10 @@ export const fetchControlResources = (params?: ResourceListParams) =>
|
|||||||
export const fetchControlResourceOptions = (params?: { resource_category?: string }) =>
|
export const fetchControlResourceOptions = (params?: { resource_category?: string }) =>
|
||||||
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', { params })
|
request.get<{ code?: number; details?: { list: OptionItem[]; count: number }; message?: string }>('/DC-Control/v1/resources/options', { params })
|
||||||
|
|
||||||
export const createControlResource = (data: Partial<ControlResource>) =>
|
export const createControlResource = (data: ControlResourcePayload) =>
|
||||||
request.post<{ code?: number; details?: ControlResource; message?: string }>('/DC-Control/v1/resources', data)
|
request.post<{ code?: number; details?: ControlResource; message?: string }>('/DC-Control/v1/resources', data)
|
||||||
|
|
||||||
export const updateControlResource = (id: number, data: Partial<ControlResource>) =>
|
export const updateControlResource = (id: number, data: ControlResourcePayload) =>
|
||||||
request.put<{ code?: number; details?: ControlResource; message?: string }>(`/DC-Control/v1/resources/${id}`, data)
|
request.put<{ code?: number; details?: ControlResource; message?: string }>(`/DC-Control/v1/resources/${id}`, data)
|
||||||
|
|
||||||
export const deleteControlResource = (id: number) =>
|
export const deleteControlResource = (id: number) =>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'menu.ops.netarch': 'Network Architecture',
|
'menu.ops.netarch': 'Network Architecture',
|
||||||
'menu.ops.netarch.topoGroup': 'Topology Group Management',
|
'menu.ops.netarch.topoGroup': 'Topology Group Management',
|
||||||
'menu.ops.netarch.topo': 'Topology',
|
'menu.ops.netarch.topo': 'Topology',
|
||||||
|
'menu.ops.businessTopology': 'Business Topology',
|
||||||
'menu.ops.systemSettings': 'System Settings',
|
'menu.ops.systemSettings': 'System Settings',
|
||||||
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
'menu.ops.systemSettings.menuManagement': 'Menu Management',
|
||||||
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
'menu.ops.systemSettings.systemLogs': 'System Logs',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default {
|
|||||||
'menu.ops.netarch': '网络架构',
|
'menu.ops.netarch': '网络架构',
|
||||||
'menu.ops.netarch.topoGroup': '拓扑组管理',
|
'menu.ops.netarch.topoGroup': '拓扑组管理',
|
||||||
'menu.ops.netarch.topo': '拓扑图',
|
'menu.ops.netarch.topo': '拓扑图',
|
||||||
|
'menu.ops.businessTopology': '业务拓扑',
|
||||||
'menu.ops.systemSettings': '系统设置',
|
'menu.ops.systemSettings': '系统设置',
|
||||||
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
'menu.ops.systemSettings.menuManagement': '菜单管理',
|
||||||
'menu.ops.systemSettings.systemLogs': '系统日志',
|
'menu.ops.systemSettings.systemLogs': '系统日志',
|
||||||
|
|||||||
@@ -108,6 +108,22 @@ export const localMenuFlatItems: MenuItem[] = [
|
|||||||
hide_menu: true,
|
hide_menu: true,
|
||||||
created_at: '2026-06-25T10:00:00+08:00',
|
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,
|
id: 16,
|
||||||
identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390',
|
identity: '019b591d-00f4-73a0-bbdb-aa7da79ed390',
|
||||||
|
|||||||
@@ -130,6 +130,23 @@ export const localMenuItems: MenuItem[] = [
|
|||||||
created_at: '2026-06-25T10:00:00+08:00',
|
created_at: '2026-06-25T10:00:00+08:00',
|
||||||
children: [],
|
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,
|
id: 12062,
|
||||||
identity: '019ca000-0001-7000-8000-000000000062',
|
identity: '019ca000-0001-7000-8000-000000000062',
|
||||||
|
|||||||
@@ -68,6 +68,17 @@ const OPS: AppRouteRecordRaw = {
|
|||||||
hideInMenu: true,
|
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',
|
path: 'big-screen',
|
||||||
name: 'OpsBigScreen',
|
name: 'OpsBigScreen',
|
||||||
|
|||||||
1138
src/views/ops/pages/business-topology/index.vue
Normal file
1138
src/views/ops/pages/business-topology/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
418
src/views/ops/pages/business-topology/transform.ts
Normal file
418
src/views/ops/pages/business-topology/transform.ts
Normal file
@@ -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<string>()
|
||||||
|
const resourceNodeIds = new Map<string, string>()
|
||||||
|
const resourceFlowNodeIds = new Set<string>()
|
||||||
|
const resourceContainmentEdgeIds = new Set<string>()
|
||||||
|
const matchedNodeIds = new Set<string>()
|
||||||
|
|
||||||
|
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<string>,
|
||||||
|
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<BusinessTopologyFlowEdge['style']> {
|
||||||
|
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<string, string>, 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<string | number | undefined> = [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<string>, 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<string | number | undefined> = [
|
||||||
|
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))
|
||||||
|
}
|
||||||
67
src/views/ops/pages/business-topology/types.ts
Normal file
67
src/views/ops/pages/business-topology/types.ts
Normal file
@@ -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<BusinessTopologyGraphNodeData, any, 'businessTopology'>
|
||||||
|
|
||||||
|
export type BusinessTopologyFlowEdge = Edge<BusinessTopologyGraphEdgeData>
|
||||||
|
|
||||||
|
export interface BusinessTopologyGraph {
|
||||||
|
nodes: BusinessTopologyFlowNode[]
|
||||||
|
edges: BusinessTopologyFlowEdge[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BusinessTopologySelection =
|
||||||
|
| { type: 'node'; id: string; data: BusinessTopologyGraphNodeData }
|
||||||
|
| { type: 'edge'; id: string; data: BusinessTopologyGraphEdgeData }
|
||||||
@@ -28,9 +28,13 @@
|
|||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<a-card :bordered="false">
|
<a-card :bordered="false">
|
||||||
<template #title>资源上下文</template>
|
<template #title>资源库</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-button type="primary" :disabled="!canOperate || loading" @click="openCreateResource">
|
||||||
|
<template #icon><icon-plus /></template>
|
||||||
|
新增资源
|
||||||
|
</a-button>
|
||||||
<a-button :disabled="!canOperate || loading" @click="handleBackfill">
|
<a-button :disabled="!canOperate || loading" @click="handleBackfill">
|
||||||
<template #icon><icon-refresh /></template>
|
<template #icon><icon-refresh /></template>
|
||||||
回填资源
|
回填资源
|
||||||
@@ -87,9 +91,17 @@
|
|||||||
<a-tag :color="statusColor(record.status)">{{ record.status || 'unknown' }}</a-tag>
|
<a-tag :color="statusColor(record.status)">{{ record.status || 'unknown' }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<a-button type="text" size="small" :disabled="pageUnauthorized" @click="openDetail(record)">
|
<a-space>
|
||||||
<template #icon><icon-eye /></template>
|
<a-button type="text" size="small" :disabled="pageUnauthorized" @click="openDetail(record)">
|
||||||
</a-button>
|
<template #icon><icon-eye /></template>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" size="small" :disabled="!canOperate" @click="openEditResource(record)">
|
||||||
|
<template #icon><icon-edit /></template>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="text" status="danger" size="small" :disabled="!canOperate" @click="handleDeleteResource(record)">
|
||||||
|
<template #icon><icon-delete /></template>
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</PageState>
|
</PageState>
|
||||||
@@ -118,23 +130,71 @@
|
|||||||
:pagination="false"
|
:pagination="false"
|
||||||
/>
|
/>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="resourceFormVisible"
|
||||||
|
:title="resourceFormTitle"
|
||||||
|
:confirm-loading="resourceFormSubmitting"
|
||||||
|
:on-before-ok="submitResourceForm"
|
||||||
|
unmount-on-close
|
||||||
|
@cancel="resetResourceForm"
|
||||||
|
>
|
||||||
|
<a-form :model="resourceForm" layout="vertical">
|
||||||
|
<a-form-item field="resource_category" label="资源分类" required>
|
||||||
|
<a-select v-model="resourceForm.resource_category" allow-search placeholder="请选择资源分类" :options="resourceTypeOptions" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="service_identity" label="服务标识" required>
|
||||||
|
<a-input v-model="resourceForm.service_identity" placeholder="例如 his-app-01" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="display_name" label="展示名称" required>
|
||||||
|
<a-input v-model="resourceForm.display_name" placeholder="例如 HIS 应用服务 01" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="status" label="状态" required>
|
||||||
|
<a-select v-model="resourceForm.status" placeholder="请选择状态">
|
||||||
|
<a-option value="normal">normal</a-option>
|
||||||
|
<a-option value="online">online</a-option>
|
||||||
|
<a-option value="offline">offline</a-option>
|
||||||
|
<a-option value="error">error</a-option>
|
||||||
|
<a-option value="unknown">unknown</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="business_system_id" label="业务系统">
|
||||||
|
<a-select v-model="resourceForm.business_system_id" allow-clear allow-search placeholder="可选">
|
||||||
|
<a-option v-for="item in businessSystems" :key="item.id" :value="item.id">{{ item.name }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="asset_id" label="资产 ID">
|
||||||
|
<a-input-number v-model="resourceForm.asset_id" :min="1" :precision="0" placeholder="可选" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="tags" label="标签">
|
||||||
|
<a-input v-model="resourceForm.tags" placeholder="多个标签用逗号分隔" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item field="description" label="描述">
|
||||||
|
<a-textarea v-model="resourceForm.description" :auto-size="{ minRows: 3, maxRows: 6 }" placeholder="请输入资源说明" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import { IconEye, IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
|
import { IconDelete, IconEdit, IconEye, IconPlus, IconRefresh, IconSearch } from '@arco-design/web-vue/es/icon'
|
||||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'
|
||||||
import {
|
import {
|
||||||
backfillControlResources,
|
backfillControlResources,
|
||||||
|
createControlResource,
|
||||||
|
deleteControlResource,
|
||||||
fetchControlBusinessSystems,
|
fetchControlBusinessSystems,
|
||||||
fetchControlMetricSeries,
|
fetchControlMetricSeries,
|
||||||
fetchControlResources,
|
fetchControlResources,
|
||||||
fetchControlResourceTypes,
|
fetchControlResourceTypes,
|
||||||
|
updateControlResource,
|
||||||
type ControlBusinessSystem,
|
type ControlBusinessSystem,
|
||||||
type ControlMetricSeries,
|
type ControlMetricSeries,
|
||||||
type ControlResource,
|
type ControlResource,
|
||||||
|
type ControlResourcePayload,
|
||||||
type ControlResourceType,
|
type ControlResourceType,
|
||||||
} from '@/api/ops/dcControl'
|
} from '@/api/ops/dcControl'
|
||||||
|
|
||||||
@@ -150,6 +210,28 @@ const resourceTypes = ref<ControlResourceType[]>([])
|
|||||||
const businessSystems = ref<ControlBusinessSystem[]>([])
|
const businessSystems = ref<ControlBusinessSystem[]>([])
|
||||||
const currentResource = ref<ControlResource | null>(null)
|
const currentResource = ref<ControlResource | null>(null)
|
||||||
const selectedMetricTotal = ref(0)
|
const selectedMetricTotal = ref(0)
|
||||||
|
const resourceFormVisible = ref(false)
|
||||||
|
const resourceFormSubmitting = ref(false)
|
||||||
|
const editingResource = ref<ControlResource | null>(null)
|
||||||
|
|
||||||
|
const resourceForm = reactive<ControlResourcePayload>({
|
||||||
|
resource_category: '',
|
||||||
|
service_identity: '',
|
||||||
|
display_name: '',
|
||||||
|
status: 'normal',
|
||||||
|
business_system_id: undefined,
|
||||||
|
owner_user_id: undefined,
|
||||||
|
department_id: undefined,
|
||||||
|
asset_id: undefined,
|
||||||
|
datacenter_id: undefined,
|
||||||
|
room_id: undefined,
|
||||||
|
rack_id: undefined,
|
||||||
|
unit_start: undefined,
|
||||||
|
unit_end: undefined,
|
||||||
|
location_ref: '',
|
||||||
|
tags: '',
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@@ -173,7 +255,7 @@ const resourceColumns: TableColumnData[] = [
|
|||||||
{ title: '责任人', dataIndex: 'owner_user_id', slotName: 'owner_user_id', width: 100 },
|
{ title: '责任人', dataIndex: 'owner_user_id', slotName: 'owner_user_id', width: 100 },
|
||||||
{ title: '资产', dataIndex: 'asset_id', slotName: 'asset_id', width: 100 },
|
{ title: '资产', dataIndex: 'asset_id', slotName: 'asset_id', width: 100 },
|
||||||
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 110 },
|
{ title: '状态', dataIndex: 'status', slotName: 'status', width: 110 },
|
||||||
{ title: '操作', dataIndex: 'actions', slotName: 'actions', width: 80, fixed: 'right' },
|
{ title: '操作', dataIndex: 'actions', slotName: 'actions', width: 150, fixed: 'right' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const metricColumns: TableColumnData[] = [
|
const metricColumns: TableColumnData[] = [
|
||||||
@@ -194,6 +276,62 @@ const resourceTypeOptions = computed(() =>
|
|||||||
|
|
||||||
const canOperate = computed(() => !pageUnauthorized.value)
|
const canOperate = computed(() => !pageUnauthorized.value)
|
||||||
|
|
||||||
|
const resourceFormTitle = computed(() => (editingResource.value ? '编辑资源' : '新增资源'))
|
||||||
|
|
||||||
|
const resetResourceForm = () => {
|
||||||
|
editingResource.value = null
|
||||||
|
Object.assign(resourceForm, {
|
||||||
|
resource_category: '',
|
||||||
|
service_identity: '',
|
||||||
|
display_name: '',
|
||||||
|
status: 'normal',
|
||||||
|
business_system_id: undefined,
|
||||||
|
owner_user_id: undefined,
|
||||||
|
department_id: undefined,
|
||||||
|
asset_id: undefined,
|
||||||
|
datacenter_id: undefined,
|
||||||
|
room_id: undefined,
|
||||||
|
rack_id: undefined,
|
||||||
|
unit_start: undefined,
|
||||||
|
unit_end: undefined,
|
||||||
|
location_ref: '',
|
||||||
|
tags: '',
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeNullableNumber = (value: unknown) => {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildResourcePayload = (): ControlResourcePayload => {
|
||||||
|
const locationRef = resourceForm.location_ref?.trim()
|
||||||
|
const payload: ControlResourcePayload = {
|
||||||
|
resource_category: resourceForm.resource_category?.trim(),
|
||||||
|
service_identity: resourceForm.service_identity?.trim(),
|
||||||
|
display_name: resourceForm.display_name?.trim(),
|
||||||
|
status: resourceForm.status?.trim() || 'normal',
|
||||||
|
business_system_id: normalizeNullableNumber(resourceForm.business_system_id),
|
||||||
|
owner_user_id: normalizeNullableNumber(resourceForm.owner_user_id),
|
||||||
|
department_id: normalizeNullableNumber(resourceForm.department_id),
|
||||||
|
asset_id: normalizeNullableNumber(resourceForm.asset_id),
|
||||||
|
datacenter_id: normalizeNullableNumber(resourceForm.datacenter_id),
|
||||||
|
room_id: normalizeNullableNumber(resourceForm.room_id),
|
||||||
|
rack_id: normalizeNullableNumber(resourceForm.rack_id),
|
||||||
|
unit_start: normalizeNullableNumber(resourceForm.unit_start),
|
||||||
|
unit_end: normalizeNullableNumber(resourceForm.unit_end),
|
||||||
|
tags: resourceForm.tags?.trim(),
|
||||||
|
description: resourceForm.description?.trim(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locationRef) {
|
||||||
|
payload.location_ref = locationRef
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
const pageStatus = computed(() => {
|
const pageStatus = computed(() => {
|
||||||
if (pageUnauthorized.value) return 'unauthorized'
|
if (pageUnauthorized.value) return 'unauthorized'
|
||||||
if (loading.value) return 'loading'
|
if (loading.value) return 'loading'
|
||||||
@@ -313,6 +451,85 @@ const openDetail = (record: ControlResource) => {
|
|||||||
fetchMetricSeries(record.resource_uid)
|
fetchMetricSeries(record.resource_uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openCreateResource = () => {
|
||||||
|
resetResourceForm()
|
||||||
|
resourceFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const openEditResource = (record: ControlResource) => {
|
||||||
|
editingResource.value = record
|
||||||
|
Object.assign(resourceForm, {
|
||||||
|
resource_category: record.resource_category || '',
|
||||||
|
service_identity: record.service_identity || '',
|
||||||
|
display_name: record.display_name || '',
|
||||||
|
status: record.status || 'normal',
|
||||||
|
business_system_id: record.business_system_id,
|
||||||
|
owner_user_id: record.owner_user_id,
|
||||||
|
department_id: record.department_id,
|
||||||
|
asset_id: record.asset_id,
|
||||||
|
datacenter_id: record.datacenter_id,
|
||||||
|
room_id: record.room_id,
|
||||||
|
rack_id: record.rack_id,
|
||||||
|
unit_start: record.unit_start,
|
||||||
|
unit_end: record.unit_end,
|
||||||
|
location_ref: record.location_ref || '',
|
||||||
|
tags: record.tags || '',
|
||||||
|
description: record.description || '',
|
||||||
|
})
|
||||||
|
resourceFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitResourceForm = async () => {
|
||||||
|
const payload = buildResourcePayload()
|
||||||
|
if (!payload.resource_category || !payload.service_identity || !payload.display_name) {
|
||||||
|
Message.warning('请填写资源分类、服务标识和展示名称')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceFormSubmitting.value = true
|
||||||
|
try {
|
||||||
|
if (editingResource.value) {
|
||||||
|
await updateControlResource(editingResource.value.id, payload)
|
||||||
|
Message.success('资源已更新')
|
||||||
|
} else {
|
||||||
|
await createControlResource(payload)
|
||||||
|
Message.success('资源已新增')
|
||||||
|
}
|
||||||
|
await fetchResources()
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存资源失败:', error)
|
||||||
|
Message.error('保存资源失败')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
resourceFormSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteResource = (record: ControlResource) => {
|
||||||
|
Modal.warning({
|
||||||
|
title: '删除资源',
|
||||||
|
content: `确认删除资源「${record.display_name || '-'} / ${record.resource_uid} / ${resourceTypeName(record.resource_category)}」?这会删除资源库中的全局资源,可能影响业务拓扑、告警和指标序列。`,
|
||||||
|
hideCancel: false,
|
||||||
|
okText: '删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteControlResource(record.id)
|
||||||
|
Message.success('资源已删除')
|
||||||
|
if (currentResource.value?.id === record.id) {
|
||||||
|
detailVisible.value = false
|
||||||
|
currentResource.value = null
|
||||||
|
}
|
||||||
|
await fetchResources()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除资源失败:', error)
|
||||||
|
Message.error('删除资源失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleBackfill = async () => {
|
const handleBackfill = async () => {
|
||||||
try {
|
try {
|
||||||
await backfillControlResources()
|
await backfillControlResources()
|
||||||
|
|||||||
Reference in New Issue
Block a user