wujingjing
2025-02-24 a61bd8abfb6bedacccbc1f1cdb01e4f433e58fd7
src/views/project/yw/systemManage/agentGraph/AgentGraph.vue
@@ -1,74 +1,20 @@
<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,
@@ -78,172 +24,6 @@
   };
   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 维度数量 */
@@ -299,7 +79,6 @@
                  level: 2,
               };
            }),
            // .filter((item, index) => index === 0),
         };
         dimensionCount += metrics.children.length;
@@ -310,123 +89,15 @@
   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>