<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>
|
</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 router from '/@/router';
|
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 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,
|
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 维度数量 */
|
let dimensionCount = 0;
|
/** @description 指标数量 */
|
let metricsCount = 0;
|
const allAgentRes = getMetricAgentListByPost();
|
const metricsRes = getMetricNameListByPost({
|
agent_id: agentId,
|
});
|
const [allAgentResult, metricsResult] = await Promise.all([allAgentRes, metricsRes]);
|
|
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: 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;
|
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(() => {
|
firstLoading.value = false;
|
});
|
setTimeout(() => {
|
const layoutType = getLayoutType(maxLevelNodeCount);
|
initGraph(orgTreeData as OrgTreeItem, layoutType);
|
|
// initEvent();
|
// loadData(orgTreeList as any);
|
firstLoading.value = false;
|
}, 100);
|
});
|
</script>
|
<style scoped lang="scss">
|
:deep(g[data-shape='rect'].x6-node > text:hover) {
|
// text-decoration: underline;
|
cursor: pointer;
|
}
|
</style>
|