| | |
| | | <template> |
| | | <div v-loading="firstLoading" class="h-full w-full bg-white relative"> |
| | | <!-- <div class="absolute right-4 top-4 z-10"> |
| | | |
| | | |
| | | <span class="ywifont ywicon-zishiying cursor-pointer" title="自适应" @click="selfAdaptClick"></span> |
| | | </div> --> |
| | | <div class="h-full w-full" ref="graphRef"></div> |
| | | |
| | | <div |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { DagreLayout } from '@antv/layout'; |
| | | import { Graph } from '@antv/x6'; |
| | | import { Graph, GraphOptions, treeToGraphData } from '@antv/g6'; |
| | | import { defaultsDeep } from 'lodash'; |
| | | import { onMounted, ref } from 'vue'; |
| | | import { routeMap } from './routeMap'; |
| | | import { circleShape, edgeShape, ellipseShape, rectShape } from './shape'; |
| | | import { getMetricAgentListByPost, getMetricNameListByPost } from '/@/api/metrics'; |
| | | import router from '/@/router'; |
| | | import { travelTree } from '/@/utils/util'; |
| | | import { DeepPartial } from '/@/utils/types'; |
| | | const graphRef = ref<HTMLDivElement>(null); |
| | | |
| | | let graph: Graph; |
| | | const firstLoading = ref(false); |
| | | |
| | | const agentId = router.currentRoute.value.query.id as string; |
| | | type OrgTreeItem = { |
| | | treeId: string; |
| | | logicId: string; |
| | | model: any; |
| | | type: 'agent' | 'metrics' | 'dimension'; |
| | | label: string; |
| | | level: number; |
| | | children?: OrgTreeItem[]; |
| | | }; |
| | | |
| | | const initGraph = () => { |
| | | // 初始化画布 |
| | | graph = new Graph({ |
| | | const convertOrgTreeToTreeNode = (orgTreeData: OrgTreeItem) => { |
| | | const treeData = { |
| | | id: orgTreeData.treeId, |
| | | label: orgTreeData.label, |
| | | data: orgTreeData, |
| | | children: orgTreeData.children?.length > 0 ? orgTreeData.children.map((item) => convertOrgTreeToTreeNode(item)) : [], |
| | | }; |
| | | return treeData; |
| | | }; |
| | | const enum CustomLayout { |
| | | /** @description 紧凑树布局 n <= 30*/ |
| | | NormalTree, |
| | | /** @description 垂直紧凑树,根节点在中间 30 < n <= 50*/ |
| | | Vertical, |
| | | /** @description 辐射紧凑树,> 50 */ |
| | | Radial, |
| | | } |
| | | |
| | | const getLayoutType = (maxLeafLen: number) => { |
| | | if (maxLeafLen > 50) { |
| | | return CustomLayout.Radial; |
| | | } else if (30 < maxLeafLen) { |
| | | return CustomLayout.Vertical; |
| | | } else { |
| | | return CustomLayout.NormalTree; |
| | | } |
| | | }; |
| | | const getLayoutOption = (layoutType: CustomLayout) => { |
| | | let option: DeepPartial<GraphOptions> = {}; |
| | | switch (layoutType) { |
| | | case CustomLayout.Radial: |
| | | option = { |
| | | autoFit: 'view', |
| | | edge: { |
| | | type: 'cubic-radial', |
| | | }, |
| | | layout: [ |
| | | { |
| | | radial: true, |
| | | direction: 'RL', |
| | | }, |
| | | ], |
| | | }; |
| | | break; |
| | | case CustomLayout.Vertical: |
| | | option = { |
| | | edge: { |
| | | type: 'line', |
| | | }, |
| | | layout: [ |
| | | { |
| | | direction: 'V', |
| | | // getHeight: () => { |
| | | // return 20; |
| | | // }, |
| | | // getWidth: () => { |
| | | // return 20; |
| | | // }, |
| | | // 垂直间隙 |
| | | getVGap: () => { |
| | | return 40; |
| | | }, |
| | | // 水平间隙 |
| | | getHGap: () => { |
| | | return 10; |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | break; |
| | | default: |
| | | option = { |
| | | edge: { |
| | | type: 'line', |
| | | }, |
| | | layout: [ |
| | | { |
| | | direction: 'TB', |
| | | // 垂直间隙 |
| | | getVGap: () => { |
| | | return 60; |
| | | }, |
| | | // 水平间隙 |
| | | getHGap: () => { |
| | | return 10; |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | break; |
| | | } |
| | | return option; |
| | | }; |
| | | const initGraph = async (orgTreeData: OrgTreeItem, layoutType: CustomLayout) => { |
| | | const treeNodeData = convertOrgTreeToTreeNode(orgTreeData); |
| | | |
| | | const commonOption: GraphOptions = { |
| | | container: graphRef.value, |
| | | async: true, |
| | | interacting: false, |
| | | connecting: { |
| | | anchor: 'orth', |
| | | connector: 'rounded', |
| | | connectionPoint: 'boundary', |
| | | router: { |
| | | name: 'er', |
| | | args: { |
| | | offset: 24, |
| | | direction: 'H', |
| | | data: treeToGraphData(treeNodeData), |
| | | node: { |
| | | style: { |
| | | size: 20, |
| | | labelText: (d) => d.label as string, |
| | | labelBackground: true, |
| | | }, |
| | | state: { |
| | | active: { |
| | | fill: '#00C9C9', |
| | | }, |
| | | }, |
| | | palette: { |
| | | field: 'type', |
| | | color: 'tableau', |
| | | }, |
| | | }, |
| | | edge: { |
| | | type: 'cubic-radial', |
| | | state: { |
| | | active: { |
| | | lineWidth: 3, |
| | | stroke: '#009999', |
| | | }, |
| | | }, |
| | | }, |
| | | mousewheel: { |
| | | enabled: true, |
| | | }, |
| | | panning: { |
| | | enabled: true, |
| | | }, |
| | | }); |
| | | layout: [ |
| | | { |
| | | type: 'compact-box', |
| | | |
| | | getHeight: () => { |
| | | return 20; |
| | | }, |
| | | getWidth: () => { |
| | | return 20; |
| | | }, |
| | | // 垂直间隙 |
| | | getVGap: () => { |
| | | return 2; |
| | | }, |
| | | // 水平间隙 |
| | | getHGap: () => { |
| | | return 120; |
| | | }, |
| | | }, |
| | | ], |
| | | behaviors: [ |
| | | 'drag-canvas', |
| | | 'zoom-canvas', |
| | | // 'drag-element', |
| | | { |
| | | key: 'hover-activate', |
| | | type: 'hover-activate', |
| | | degree: 5, |
| | | direction: 'in', |
| | | inactiveState: 'inactive', |
| | | }, |
| | | ], |
| | | transforms: ['place-radial-labels'], |
| | | }; |
| | | const extraOption: DeepPartial<GraphOptions> = getLayoutOption(layoutType); |
| | | |
| | | const finalOption: GraphOptions = defaultsDeep(extraOption, commonOption); |
| | | graph = new Graph(finalOption); |
| | | window.graph = graph; |
| | | graph.render(); |
| | | }; |
| | | |
| | | const getLeaf = (item) => { |
| | |
| | | }; |
| | | |
| | | const initEvent = () => { |
| | | graph.on('node:click', ({ cell, e, node, view }) => { |
| | | return; |
| | | const target = e.target; |
| | | const targetParent = target.parentElement; |
| | | if (!targetParent.className?.baseVal?.includes('name')) return; |
| | | |
| | | const data = node.getData(); |
| | | const leafData = getLeaf(data); |
| | | const routeName = routeMap.get(leafData.Code); |
| | | router.push({ |
| | | name: routeName, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | const loadData = (treeData: any[]) => { |
| | | const data: any = { |
| | | nodes: [], |
| | | edges: [], |
| | | }; |
| | | travelTree( |
| | | treeData, |
| | | (item) => { |
| | | let shapeAttrs = null; |
| | | switch (item.type) { |
| | | case 'agent': |
| | | shapeAttrs = rectShape(item.treeId, item.label); |
| | | break; |
| | | case 'metrics': |
| | | shapeAttrs = ellipseShape(item.treeId, item.label); |
| | | break; |
| | | case 'dimension': |
| | | shapeAttrs = circleShape(item.treeId, item.label); |
| | | break; |
| | | default: |
| | | break; |
| | | } |
| | | data.nodes.push({ |
| | | id: item.treeId, |
| | | |
| | | ...shapeAttrs, |
| | | children: item.children?.map((item) => item.treeId), |
| | | data: item, |
| | | }); |
| | | if (item.children) { |
| | | for (const child of item?.children) { |
| | | const sourceId = `${item.treeId}-b`; |
| | | const targetId = `${child.treeId}-t`; |
| | | |
| | | data.edges.push({ |
| | | source: { cell: item.treeId, port: sourceId }, |
| | | target: { cell: child.treeId, port: targetId }, |
| | | ...edgeShape, |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | undefined, |
| | | undefined, |
| | | 'children' |
| | | ); |
| | | const dagreLayout = new DagreLayout({ |
| | | type: 'dagre', |
| | | rankdir: 'TB', |
| | | // align: 'UL', |
| | | // ranksep: 30, |
| | | nodesep: 30, |
| | | |
| | | controlPoints: false, |
| | | }); |
| | | let newData = dagreLayout.layout(data); |
| | | |
| | | graph.fromJSON(newData); |
| | | selfAdapt(); |
| | | // graph.on('node:click', ({ cell, e, node, view }) => { |
| | | // }); |
| | | }; |
| | | |
| | | const getFirstOrgTreeList = async () => { |
| | | // const res = await GetSMCenterFirstOrgTreeList(); |
| | | |
| | | /** @description 维度数量 */ |
| | | let dimensionCount = 0; |
| | | /** @description 指标数量 */ |
| | | let metricsCount = 0; |
| | | const allAgentRes = getMetricAgentListByPost(); |
| | | const metricsRes = getMetricNameListByPost({ |
| | | agent_id: agentId, |
| | |
| | | const allAgentList = allAgentResult?.values ?? []; |
| | | const metricsList = metricsResult?.values ?? []; |
| | | // const foundAgent = allAgentList.find(item=>item.) |
| | | |
| | | metricsCount = metricsList.length; |
| | | const foundAgent = allAgentList.find((item) => item.id === agentId); |
| | | if (!foundAgent) return []; |
| | | |
| | | const agentTreeId = `agent-${foundAgent.id}`; |
| | | let logicTree = [ |
| | | { |
| | | treeId: agentTreeId, |
| | | logicId: foundAgent.id, |
| | | model: foundAgent, |
| | | type: 'agent', |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | children: metricsList.map((curVal) => { |
| | | const metricsTreeId = `${agentTreeId}-metrics-${curVal.id}`; |
| | | const metrics = { |
| | | treeId: metricsTreeId, |
| | | logicId: curVal.id, |
| | | type: 'metrics', |
| | | |
| | | model: curVal, |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | children: (curVal.dimensions ?? []).map((item) => { |
| | | const dimensionTreeId = `${metricsTreeId}-dimension-${item.id}`; |
| | | return { |
| | | treeId: dimensionTreeId, |
| | | logicId: item.id, |
| | | type: 'dimension', |
| | | model: item, |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | }; |
| | | }), |
| | | }; |
| | | return metrics; |
| | | }, []), |
| | | let logicTree: OrgTreeItem = { |
| | | treeId: agentTreeId, |
| | | logicId: foundAgent.id, |
| | | model: foundAgent, |
| | | type: 'agent', |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | ]; |
| | | level: 0, |
| | | children: metricsList.map((curVal) => { |
| | | const metricsTreeId = `${agentTreeId}-metrics-${curVal.id}`; |
| | | const dimensionList = curVal.dimensions ?? []; |
| | | const metrics: OrgTreeItem = { |
| | | treeId: metricsTreeId, |
| | | logicId: curVal.id, |
| | | type: 'metrics', |
| | | |
| | | model: curVal, |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | level: 1, |
| | | children: dimensionList.map((item) => { |
| | | const dimensionTreeId = `${metricsTreeId}-dimension-${item.id}`; |
| | | return { |
| | | treeId: dimensionTreeId, |
| | | logicId: item.id, |
| | | type: 'dimension', |
| | | model: item, |
| | | get label() { |
| | | return this.model.title; |
| | | }, |
| | | level: 2, |
| | | }; |
| | | }), |
| | | // .filter((item, index) => index === 0), |
| | | }; |
| | | dimensionCount += metrics.children.length; |
| | | |
| | | return metrics; |
| | | }, []), |
| | | }; |
| | | const resData = logicTree; |
| | | return resData; |
| | | const maxCount = Math.max(dimensionCount, metricsCount); |
| | | return [resData, maxCount]; |
| | | }; |
| | | const selfAdapt = () => { |
| | | setTimeout(() => { |
| | | graph.zoomToFit(); |
| | | graph.fitView(); |
| | | }, 100); |
| | | }; |
| | | |
| | |
| | | }, |
| | | { |
| | | icon: 'zishiying', |
| | | label: '重置', |
| | | label: '自适应', |
| | | type: ToolBarType.Reset, |
| | | }, |
| | | ]; |
| | | |
| | | const searchIsShow = ref(false); |
| | | const searchValue = ref(''); |
| | | const ZOOM_OFFSET = 0.05; |
| | | const ZOOM_OFFSET = 0.15; |
| | | const toolBarItemClick = (item: ToolBarItem) => { |
| | | switch (item.type) { |
| | | case ToolBarType.Search: |
| | | searchIsShow.value = true; |
| | | break; |
| | | case ToolBarType.ZoomIn: |
| | | graph.zoom(ZOOM_OFFSET); |
| | | graph.zoomBy(1 + ZOOM_OFFSET); |
| | | break; |
| | | |
| | | case ToolBarType.ZoomOut: |
| | | graph.zoom(-ZOOM_OFFSET); |
| | | graph.zoomBy(1 - ZOOM_OFFSET); |
| | | |
| | | break; |
| | | |
| | |
| | | if (!agentId) return; |
| | | firstLoading.value = true; |
| | | |
| | | const orgTreeList = await getFirstOrgTreeList().catch(() => { |
| | | const [orgTreeData, maxLevelNodeCount] = await (getFirstOrgTreeList() as any).catch(() => { |
| | | firstLoading.value = false; |
| | | }); |
| | | setTimeout(() => { |
| | | initGraph(); |
| | | initEvent(); |
| | | loadData(orgTreeList as any); |
| | | const layoutType = getLayoutType(maxLevelNodeCount); |
| | | initGraph(orgTreeData as OrgTreeItem, layoutType); |
| | | |
| | | // initEvent(); |
| | | // loadData(orgTreeList as any); |
| | | firstLoading.value = false; |
| | | }, 100); |
| | | }); |