<template>
|
<div 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>
|
</template>
|
|
<script setup lang="ts">
|
import { Graph, GraphOptions, treeToGraphData } from '@antv/g6';
|
import { defaultsDeep } from 'lodash';
|
import { PropType, onMounted, ref } from 'vue';
|
import { CustomLayout, customLayoutMap, getLayoutType } from './types';
|
import { DeepPartial } from '/@/utils/types';
|
|
const props = defineProps({
|
maxCount: {
|
type: Number,
|
},
|
data: {
|
type: Object as PropType<any>,
|
},
|
});
|
|
const graphRef = ref<HTMLDivElement>(null);
|
|
let graph: Graph;
|
|
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 = async (extraOption: DeepPartial<GraphOptions>) => {
|
const finalOptions = defaultsDeep(extraOption, commonOption);
|
graph.setOptions(finalOptions);
|
await graph.render();
|
// graph.fitCenter();
|
selfAdapt();
|
};
|
|
let commonOption: GraphOptions;
|
const initGraph = async (treeData: any, layoutType: CustomLayout) => {
|
commonOption = {
|
container: graphRef.value,
|
data: treeToGraphData(treeData),
|
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 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(() => {
|
const layoutType = getLayoutType(props.maxCount);
|
activeLayout.value = layoutType;
|
initGraph(props.data, layoutType);
|
});
|
</script>
|
<style scoped lang="scss"></style>
|