wujingjing
2025-02-24 a61bd8abfb6bedacccbc1f1cdb01e4f433e58fd7
src/views/project/yw/systemManage/agentGraph/AgentGraph.vue
@@ -1,56 +1,20 @@
<template>
   <div v-loading="firstLoading" class="h-full w-full bg-white relative">
      <div class="h-full w-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="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 { getMetricAgentListByPost, getMetricNameListByPost } from '/@/api/metrics';
import TreeGraph from '/@/components/graph/treeGraph/TreeGraph.vue';
import router from '/@/router';
import { DeepPartial } from '/@/utils/types';
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;
type OrgTreeItem = {
   treeId: string;
   logicId: string;
   model: any;
   type: 'agent' | 'metrics' | 'dimension';
   label: string;
   level: number;
   children?: OrgTreeItem[];
};
const maxCount = ref(null);
const graphData = ref(null);
const firstLoading = ref(false);
const convertOrgTreeToTreeNode = (orgTreeData: OrgTreeItem) => {
   const treeData = {
      id: orgTreeData.treeId,
@@ -60,177 +24,6 @@
   };
   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,
      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);
   window.graph = graph;
   graph.render();
};
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 维度数量 */
@@ -286,7 +79,6 @@
                  level: 2,
               };
            }),
            // .filter((item, index) => index === 0),
         };
         dimensionCount += metrics.children.length;
@@ -297,96 +89,15 @@
   const maxCount = Math.max(dimensionCount, metricsCount);
   return [resData, maxCount];
};
const selfAdapt = () => {
   setTimeout(() => {
      graph.fitView();
   }, 100);
};
//#region ====================== 工具栏 ======================
const enum ToolBarType {
   Search,
   ZoomIn,
   ZoomOut,
   Reset,
}
type ToolBarItem = {
   icon: string;
   label: string;
   type: ToolBarType;
};
const toolBarFunList: ToolBarItem[] = [
   {
      icon: 'sousuo',
      label: '搜索',
      type: ToolBarType.Search,
   },
   {
      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.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
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);
      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>