| | |
| | | <template> |
| | | <div v-loading="firstLoading" class="h-full w-full bg-white relative flex flex-col"> |
| | | <!-- <div class="toolbar flex-0 border-x-0 border-b border-t-0 border-gray-200 border-solid flex-items-center"> |
| | | <span> |
| | | 格式 |
| | | </span> |
| | | </div> --> |
| | | <div class="graph flex-auto"> |
| | | <div class="h-full" ref="graphRef"></div> |
| | | |
| | | <div |
| | | class="absolute left-4 top-4 z-10 w-16 divide-y-[1.5px] divide-solid divide-gray-100 rounded-lg" |
| | | style="box-shadow: 0 0 15px #dbdee6" |
| | | > |
| | | <div |
| | | class="flex-column content-center items-center border-x-0 py-2 hover:bg-gray-200 active:bg-gray-300 cursor-pointer" |
| | | @click="toolBarItemClick(item)" |
| | | v-for="item in toolBarFunList" |
| | | > |
| | | <span class="ywifont !text-[20px] mb-1 p-1.5" :class="[`ywicon-${item.icon}`]"></span> |
| | | <span>{{ item.label }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="absolute right-2 top-4 py-7 px-5 rounded-lg w-96" style="box-shadow: 0 0 15px #dbdee6" v-if="searchIsShow"> |
| | | <span |
| | | class="absolute ywifont ywicon-guanbi right-4 top-4 cursor-pointer hover:bg-gray-200 active:bg-gray-300" |
| | | @click="closeSearch" |
| | | ></span> |
| | | <div class="flex-column"> |
| | | <span class="mb-5">查找</span> |
| | | <el-input v-model="searchValue" @input="debounceSearch" placeholder="输入查找内容" v-elInputFocus></el-input> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="absolute right-2 top-4 py-7 px-5 rounded-lg w-96" style="box-shadow: 0 0 15px #dbdee6" v-if="layoutIsShow"> |
| | | <span |
| | | class="absolute ywifont ywicon-guanbi right-4 top-4 cursor-pointer hover:bg-gray-200 active:bg-gray-300" |
| | | @click="closeLayout" |
| | | ></span> |
| | | <div class="flex-column"> |
| | | <span class="mb-5">布局</span> |
| | | |
| | | <el-select v-model="activeLayout" @change="layoutChange"> |
| | | <el-option |
| | | v-for="item in Object.keys(customLayoutMap)" |
| | | :key="item" |
| | | :value="parseInt(item)" |
| | | :label="customLayoutMap[item]" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="h-full" v-loading="firstLoading"> |
| | | <TreeGraph v-if="graphData" :data="graphData" class="h-full" :maxCount="maxCount" /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { Graph, GraphOptions, treeToGraphData } from '@antv/g6'; |
| | | import { defaultsDeep } from 'lodash'; |
| | | import { onMounted, ref } from 'vue'; |
| | | import { CustomLayout, OrgTreeItem, customLayoutMap, getLayoutType } from './types'; |
| | | import { getMetricAgentListByPost, getMetricNameListByPost } from '/@/api/metrics'; |
| | | import TreeGraph from '/@/components/graph/treeGraph/TreeGraph.vue'; |
| | | import router from '/@/router'; |
| | | import { DeepPartial } from '/@/utils/types'; |
| | | import { deepClone } from '/@/utils/other'; |
| | | const graphRef = ref<HTMLDivElement>(null); |
| | | |
| | | let graph: Graph; |
| | | const firstLoading = ref(false); |
| | | import { getMetricAgentListByPost, getMetricNameListByPost } from '/@/api/metrics'; |
| | | import { OrgTreeItem } from './types'; |
| | | |
| | | const agentId = router.currentRoute.value.query.id as string; |
| | | const maxCount = ref(null); |
| | | const graphData = ref(null); |
| | | const firstLoading = ref(false); |
| | | const convertOrgTreeToTreeNode = (orgTreeData: OrgTreeItem) => { |
| | | const treeData = { |
| | | id: orgTreeData.treeId, |
| | |
| | | }; |
| | | return treeData; |
| | | }; |
| | | 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 graphRender = async() => { |
| | | await graph.render(); |
| | | if (activeLayout.value === CustomLayout.Radial) { |
| | | selfAdapt(); |
| | | } |
| | | }; |
| | | const rebuildGraph = (extraOption: DeepPartial<GraphOptions>) => { |
| | | const finalOptions = defaultsDeep(extraOption, commonOption); |
| | | graph.setOptions(finalOptions); |
| | | graphRender(); |
| | | }; |
| | | |
| | | let commonOption: GraphOptions; |
| | | const initGraph = async (orgTreeData: OrgTreeItem, layoutType: CustomLayout) => { |
| | | const treeNodeData = convertOrgTreeToTreeNode(orgTreeData); |
| | | |
| | | commonOption = { |
| | | container: graphRef.value, |
| | | 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', |
| | | }, |
| | | }, |
| | | }, |
| | | 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); |
| | | graphRender(); |
| | | }; |
| | | |
| | | const getLeaf = (item) => { |
| | | if (item.Children && item.Children.length > 0) { |
| | | const first = item.Children[0]; |
| | | return getLeaf(first); |
| | | } else { |
| | | return item; |
| | | } |
| | | }; |
| | | |
| | | const initEvent = () => { |
| | | // graph.on('node:click', ({ cell, e, node, view }) => { |
| | | // }); |
| | | }; |
| | | |
| | | const getFirstOrgTreeList = async () => { |
| | | // const res = await GetSMCenterFirstOrgTreeList(); |
| | | /** @description 维度数量 */ |
| | |
| | | level: 2, |
| | | }; |
| | | }), |
| | | // .filter((item, index) => index === 0), |
| | | }; |
| | | dimensionCount += metrics.children.length; |
| | | |
| | |
| | | const maxCount = Math.max(dimensionCount, metricsCount); |
| | | return [resData, maxCount]; |
| | | }; |
| | | const selfAdapt = () => { |
| | | setTimeout(() => { |
| | | graph.fitView(); |
| | | }, 100); |
| | | }; |
| | | |
| | | //#region ====================== 工具栏 ====================== |
| | | const enum ToolBarType { |
| | | /** @description 布局 */ |
| | | Layout, |
| | | /** @description 搜索 */ |
| | | Search, |
| | | /** @description 放大 */ |
| | | ZoomIn, |
| | | /** @description 缩小 */ |
| | | ZoomOut, |
| | | /** @description 自适应 */ |
| | | Reset, |
| | | } |
| | | type ToolBarItem = { |
| | | icon: string; |
| | | label: string; |
| | | type: ToolBarType; |
| | | }; |
| | | const toolBarFunList: ToolBarItem[] = [ |
| | | // { |
| | | // icon: 'sousuo', |
| | | // label: '搜索', |
| | | // type: ToolBarType.Search, |
| | | // }, |
| | | { |
| | | icon: 'buju', |
| | | label: '布局', |
| | | type: ToolBarType.Layout, |
| | | }, |
| | | { |
| | | icon: 'fangda', |
| | | label: '放大', |
| | | type: ToolBarType.ZoomIn, |
| | | }, |
| | | { |
| | | icon: 'suoxiao', |
| | | label: '缩小', |
| | | type: ToolBarType.ZoomOut, |
| | | }, |
| | | { |
| | | icon: 'zishiying', |
| | | label: '自适应', |
| | | type: ToolBarType.Reset, |
| | | }, |
| | | ]; |
| | | |
| | | const searchIsShow = ref(false); |
| | | const searchValue = ref(''); |
| | | const ZOOM_OFFSET = 0.15; |
| | | const toolBarItemClick = (item: ToolBarItem) => { |
| | | switch (item.type) { |
| | | case ToolBarType.Layout: |
| | | layoutIsShow.value = true; |
| | | break; |
| | | case ToolBarType.Search: |
| | | searchIsShow.value = true; |
| | | break; |
| | | case ToolBarType.ZoomIn: |
| | | graph.zoomBy(1 + ZOOM_OFFSET); |
| | | break; |
| | | |
| | | case ToolBarType.ZoomOut: |
| | | graph.zoomBy(1 - ZOOM_OFFSET); |
| | | |
| | | break; |
| | | |
| | | case ToolBarType.Reset: |
| | | selfAdapt(); |
| | | default: |
| | | break; |
| | | } |
| | | }; |
| | | |
| | | const debounceSearch = () => {}; |
| | | const closeSearch = () => { |
| | | searchIsShow.value = false; |
| | | }; |
| | | //#endregion |
| | | |
| | | //#region ====================== 布局 ====================== |
| | | const layoutIsShow = ref(false); |
| | | const closeLayout = () => { |
| | | layoutIsShow.value = false; |
| | | }; |
| | | const activeLayout = ref<CustomLayout>(); |
| | | const layoutChange = (layoutType: CustomLayout) => { |
| | | const extraOption = getLayoutOption(layoutType); |
| | | rebuildGraph(extraOption); |
| | | }; |
| | | //#endregion |
| | | onMounted(async () => { |
| | | if (!agentId) return; |
| | | firstLoading.value = true; |
| | | |
| | | const [orgTreeData, maxLevelNodeCount] = await (getFirstOrgTreeList() as any).catch(() => { |
| | | const [orgTreeData, maxLevelNodeCount] = await (getFirstOrgTreeList() as any).finally(() => { |
| | | firstLoading.value = false; |
| | | }); |
| | | setTimeout(() => { |
| | | const layoutType = getLayoutType(maxLevelNodeCount); |
| | | activeLayout.value = layoutType; |
| | | initGraph(orgTreeData as OrgTreeItem, layoutType); |
| | | |
| | | // initEvent(); |
| | | // loadData(orgTreeList as any); |
| | | firstLoading.value = false; |
| | | }, 100); |
| | | maxCount.value = maxLevelNodeCount; |
| | | graphData.value = convertOrgTreeToTreeNode(orgTreeData); |
| | | }); |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | :deep(g[data-shape='rect'].x6-node > text:hover) { |
| | | // text-decoration: underline; |
| | | cursor: pointer; |
| | | } |
| | | </style> |
| | | <style scoped lang="scss"></style> |