|
|
@@ -4,12 +4,17 @@
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
|
- import { Graph, treeToGraphData } from '@antv/g6';
|
|
|
+ import { Graph, treeToGraphData, register, ExtensionCategory, NodeEvent, Polyline, CanvasEvent } from '@antv/g6';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @description: 为了确保图的正确渲染和交互,建议按照 G6 标准数据结构组织数据。
|
|
|
+ * 每个元素(节点、边、组合)应包含一个 data 字段,用于存放业务数据和自定义属性。
|
|
|
+ */
|
|
|
|
|
|
// 定义 props 接口
|
|
|
interface TreeData {
|
|
|
id: string;
|
|
|
- label: string;
|
|
|
+ data: { name: string };
|
|
|
children?: TreeData[];
|
|
|
}
|
|
|
|
|
|
@@ -17,12 +22,27 @@
|
|
|
treeData: TreeData;
|
|
|
}>();
|
|
|
|
|
|
- // 转换数据
|
|
|
- const data = treeToGraphData(props.treeData);
|
|
|
- // 图表容器引用
|
|
|
- const container = ref<HTMLDivElement | null>(null);
|
|
|
+ const emits = defineEmits<{
|
|
|
+ (event: 'node-click', nodeData: any): void;
|
|
|
+ (event: 'canvas-click'): void;
|
|
|
+ }>();
|
|
|
+
|
|
|
+ const data = treeToGraphData(props.treeData); // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
|
|
|
+ const container = ref<HTMLDivElement | null>(null); // 图表容器引用
|
|
|
let graph: any = null;
|
|
|
|
|
|
+ // 自定义edge样式
|
|
|
+ class AntLine extends Polyline {
|
|
|
+ onCreate() {
|
|
|
+ const shape = this.shapeMap.key;
|
|
|
+ shape.animate([{ lineDashOffset: 20 }, { lineDashOffset: 0 }], {
|
|
|
+ duration: 500,
|
|
|
+ iterations: Infinity,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ register(ExtensionCategory.EDGE, 'ant-line', AntLine);
|
|
|
+
|
|
|
// 初始化图表
|
|
|
const initGraph = () => {
|
|
|
if (!container.value) return;
|
|
|
@@ -36,12 +56,48 @@
|
|
|
// 创建新的 G6 实例
|
|
|
graph = new Graph({
|
|
|
container: container.value,
|
|
|
- // width: window.innerWidth,
|
|
|
- // height: window.innerHeight,
|
|
|
+ padding: [20, 20, 20, 20], // 图表内边距
|
|
|
data,
|
|
|
+ node: {
|
|
|
+ type: 'rect', // 使用内置的矩形节点类型
|
|
|
+ style: {
|
|
|
+ labelText: (d: any) => d.data.name, // 节点文本
|
|
|
+ labelFill: '#333', // 文本颜色
|
|
|
+ labelFontSize: 14, // 文本大小
|
|
|
+ size: [250, 50],
|
|
|
+ lineWidth: 1, // 边框宽度
|
|
|
+ // lineDash: [5, 5], // 虚线边框
|
|
|
+ stroke: '#1777FF', // 边框色
|
|
|
+ fill: '#E7F1FF', // 填充色
|
|
|
+ radius: 8,
|
|
|
+ labelPlacement: 'center',
|
|
|
+ ports: [{ placement: 'top' }, { placement: 'bottom' }],
|
|
|
+ },
|
|
|
+ // 节点状态样式
|
|
|
+ state: {
|
|
|
+ selected: {
|
|
|
+ fill: '#1777FF',
|
|
|
+ stroke: '#1777FF',
|
|
|
+ lineWidth: 1,
|
|
|
+ labelFill: '#fff', // 选中状态下文本颜色
|
|
|
+ labelFontSize: 16, // 选中状态下文本大小
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ edge: {
|
|
|
+ type: 'ant-line', // 边类型
|
|
|
+ style: {
|
|
|
+ stroke: '#1777FF', // 边的颜色
|
|
|
+ lineWidth: 1, // 边的宽度
|
|
|
+ lineDash: [10, 10],
|
|
|
+ endArrow: true, // 是否有箭头
|
|
|
+ router: {
|
|
|
+ type: 'orth',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
layout: {
|
|
|
type: 'dagre',
|
|
|
- rankdir: 'LR', // 水平方向布局
|
|
|
nodesep: 100,
|
|
|
ranksep: 120,
|
|
|
preventOverlap: true, // 防止节点重叠
|
|
|
@@ -50,14 +106,6 @@
|
|
|
iterations: 200, // 迭代次数
|
|
|
animation: true, // 启用布局动画
|
|
|
},
|
|
|
- behaviors: [
|
|
|
- 'drag-canvas',
|
|
|
- {
|
|
|
- type: 'zoom-canvas',
|
|
|
- sensitivity: 1.5, // 配置灵敏度
|
|
|
- key: 'zoom-behavior', // 为交互指定key,便于后续更新
|
|
|
- },
|
|
|
- ],
|
|
|
autoFit: {
|
|
|
type: 'view', // 自适应类型:'view' 或 'center'
|
|
|
options: {
|
|
|
@@ -71,68 +119,48 @@
|
|
|
easing: 'ease-in-out', // 动画缓动函数
|
|
|
},
|
|
|
},
|
|
|
- node: {
|
|
|
- type: 'circle', // 节点类型
|
|
|
- style: {
|
|
|
- fill: '#e6f7ff', // 填充色
|
|
|
- stroke: '#91d5ff', // 边框色
|
|
|
- lineWidth: 1, // 边框宽度
|
|
|
- r: 20, // 半径
|
|
|
- labelText: (d) => d.id, // 标签文本
|
|
|
- },
|
|
|
- // 节点状态样式
|
|
|
- state: {
|
|
|
- hover: {
|
|
|
- lineWidth: 2,
|
|
|
- stroke: '#69c0ff',
|
|
|
- },
|
|
|
- selected: {
|
|
|
- fill: '#bae7ff',
|
|
|
- stroke: '#1890ff',
|
|
|
- lineWidth: 2,
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- edge: {
|
|
|
- type: 'polyline', // 边类型
|
|
|
- style: {
|
|
|
- stroke: '#91d5ff', // 边的颜色
|
|
|
- lineWidth: 2, // 边的宽度
|
|
|
- endArrow: true, // 是否有箭头
|
|
|
+ autoResize: true, // 自动调整大小
|
|
|
+ behaviors: [
|
|
|
+ 'drag-canvas',
|
|
|
+ {
|
|
|
+ type: 'zoom-canvas',
|
|
|
+ sensitivity: 0.5, // 配置灵敏度
|
|
|
+ key: 'zoom-behavior', // 为交互指定key,便于后续更新
|
|
|
},
|
|
|
- // 边的状态样式
|
|
|
- state: {
|
|
|
- selected: {
|
|
|
- stroke: '#1890ff',
|
|
|
- lineWidth: 3,
|
|
|
- },
|
|
|
+ 'focus-element',
|
|
|
+ {
|
|
|
+ type: 'click-select',
|
|
|
+ state: 'selected',
|
|
|
+ unselectedState: 'inactive',
|
|
|
+ multiple: true,
|
|
|
+ trigger: ['shift'],
|
|
|
},
|
|
|
- },
|
|
|
+ ],
|
|
|
});
|
|
|
|
|
|
// 渲染
|
|
|
graph.render();
|
|
|
|
|
|
- // 添加交互效果
|
|
|
- graph.on('node:mouseenter', (evt) => {
|
|
|
- const node = evt.item;
|
|
|
- graph.setItemState(node, 'hover', true);
|
|
|
+ // 监听节点点击事件
|
|
|
+ graph.on(NodeEvent.CLICK, (evt) => {
|
|
|
+ const { target } = evt;
|
|
|
+ const nodeData = graph.getNodeData(target.id); // 获取节点数据
|
|
|
+ emits('node-click', nodeData);
|
|
|
});
|
|
|
|
|
|
- graph.on('node:mouseleave', (evt) => {
|
|
|
- const node = evt.item;
|
|
|
- graph.setItemState(node, 'hover', false);
|
|
|
+ graph.on(CanvasEvent.CLICK, () => {
|
|
|
+ graph.fitCenter();
|
|
|
+ emits('canvas-click');
|
|
|
});
|
|
|
|
|
|
- // 响应窗口大小变化
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
};
|
|
|
|
|
|
// 处理窗口大小变化
|
|
|
const handleResize = () => {
|
|
|
- if (graph.get('destroyed')) return;
|
|
|
if (container.value) {
|
|
|
- graph.changeSize(container.value.clientWidth, container.value.clientHeight);
|
|
|
+ graph.resize(container.value.offsetWidth, container.value.offsetHeight);
|
|
|
+ graph.fitCenter();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -153,6 +181,7 @@
|
|
|
onBeforeUnmount(() => {
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
if (graph) {
|
|
|
+ graph.off(); // 移除所有事件监听
|
|
|
graph.destroy();
|
|
|
}
|
|
|
});
|