|
@@ -4,12 +4,17 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
|
- import { Graph, treeToGraphData } from '@antv/g6';
|
|
|
|
|
|
|
+ import { Graph, treeToGraphData, Rect, register, ExtensionCategory, NodeEvent, Polyline } from '@antv/g6';
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * @description: 为了确保图的正确渲染和交互,建议按照 G6 标准数据结构组织数据。
|
|
|
|
|
+ * 每个元素(节点、边、组合)应包含一个 data 字段,用于存放业务数据和自定义属性。
|
|
|
|
|
+ */
|
|
|
|
|
|
|
|
// 定义 props 接口
|
|
// 定义 props 接口
|
|
|
interface TreeData {
|
|
interface TreeData {
|
|
|
id: string;
|
|
id: string;
|
|
|
- label: string;
|
|
|
|
|
|
|
+ data: { name: string };
|
|
|
children?: TreeData[];
|
|
children?: TreeData[];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -17,12 +22,49 @@
|
|
|
treeData: TreeData;
|
|
treeData: TreeData;
|
|
|
}>();
|
|
}>();
|
|
|
|
|
|
|
|
- // 转换数据
|
|
|
|
|
|
|
+ // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
|
|
|
const data = treeToGraphData(props.treeData);
|
|
const data = treeToGraphData(props.treeData);
|
|
|
// 图表容器引用
|
|
// 图表容器引用
|
|
|
const container = ref<HTMLDivElement | null>(null);
|
|
const container = ref<HTMLDivElement | null>(null);
|
|
|
let graph: any = null;
|
|
let graph: any = null;
|
|
|
|
|
|
|
|
|
|
+ // 设置图表节点样式
|
|
|
|
|
+ class ChartNode extends Rect {
|
|
|
|
|
+ get data() {
|
|
|
|
|
+ return this.context.model.getElementDataById(this.id).data;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ getLabelStyle() {
|
|
|
|
|
+ const text = this.data?.name;
|
|
|
|
|
+ const labelStyle = {
|
|
|
|
|
+ fill: '#000',
|
|
|
|
|
+ fontSize: 20,
|
|
|
|
|
+ fontWeight: 600,
|
|
|
|
|
+ textAlign: 'center',
|
|
|
|
|
+ transform: [['translate', 0, 0]],
|
|
|
|
|
+ };
|
|
|
|
|
+ return { text, ...labelStyle };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ render(attributes = this.parsedAttributes, container = this) {
|
|
|
|
|
+ super.render(attributes, container);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 自定义edge样式
|
|
|
|
|
+ class AntLine extends Polyline {
|
|
|
|
|
+ onCreate() {
|
|
|
|
|
+ const shape = this.shapeMap.key;
|
|
|
|
|
+ shape.animate([{ lineDashOffset: -20 }, { lineDashOffset: 0 }], {
|
|
|
|
|
+ duration: 500,
|
|
|
|
|
+ iterations: Infinity,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ register(ExtensionCategory.NODE, 'chart-node', ChartNode);
|
|
|
|
|
+ register(ExtensionCategory.EDGE, 'ant-line', AntLine);
|
|
|
|
|
+
|
|
|
// 初始化图表
|
|
// 初始化图表
|
|
|
const initGraph = () => {
|
|
const initGraph = () => {
|
|
|
if (!container.value) return;
|
|
if (!container.value) return;
|
|
@@ -36,69 +78,43 @@
|
|
|
// 创建新的 G6 实例
|
|
// 创建新的 G6 实例
|
|
|
graph = new Graph({
|
|
graph = new Graph({
|
|
|
container: container.value,
|
|
container: container.value,
|
|
|
- // width: window.innerWidth,
|
|
|
|
|
- // height: window.innerHeight,
|
|
|
|
|
data,
|
|
data,
|
|
|
- layout: {
|
|
|
|
|
- type: 'dagre',
|
|
|
|
|
- rankdir: 'LR', // 水平方向布局
|
|
|
|
|
- nodesep: 100,
|
|
|
|
|
- ranksep: 120,
|
|
|
|
|
- preventOverlap: true, // 防止节点重叠
|
|
|
|
|
- nodeStrength: -50, // 节点之间的斥力
|
|
|
|
|
- edgeStrength: 0.5, // 边的弹性系数
|
|
|
|
|
- iterations: 200, // 迭代次数
|
|
|
|
|
- animation: true, // 启用布局动画
|
|
|
|
|
- },
|
|
|
|
|
- behaviors: [
|
|
|
|
|
- 'drag-canvas',
|
|
|
|
|
- {
|
|
|
|
|
- type: 'zoom-canvas',
|
|
|
|
|
- sensitivity: 1.5, // 配置灵敏度
|
|
|
|
|
- key: 'zoom-behavior', // 为交互指定key,便于后续更新
|
|
|
|
|
- },
|
|
|
|
|
- ],
|
|
|
|
|
- autoFit: {
|
|
|
|
|
- type: 'view', // 自适应类型:'view' 或 'center'
|
|
|
|
|
- options: {
|
|
|
|
|
- // 仅适用于 'view' 类型
|
|
|
|
|
- when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配)
|
|
|
|
|
- direction: 'both', // 适配方向:'x'、'y' 或 'both'
|
|
|
|
|
- },
|
|
|
|
|
- animation: {
|
|
|
|
|
- // 自适应动画效果
|
|
|
|
|
- duration: 1000, // 动画持续时间(毫秒)
|
|
|
|
|
- easing: 'ease-in-out', // 动画缓动函数
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
node: {
|
|
node: {
|
|
|
- type: 'circle', // 节点类型
|
|
|
|
|
|
|
+ type: 'chart-node', // 节点类型
|
|
|
style: {
|
|
style: {
|
|
|
- fill: '#e6f7ff', // 填充色
|
|
|
|
|
- stroke: '#91d5ff', // 边框色
|
|
|
|
|
|
|
+ fill: '#E7F1FF', // 填充色
|
|
|
|
|
+ labelPlacement: 'center',
|
|
|
lineWidth: 1, // 边框宽度
|
|
lineWidth: 1, // 边框宽度
|
|
|
- r: 20, // 半径
|
|
|
|
|
- labelText: (d) => d.id, // 标签文本
|
|
|
|
|
|
|
+ ports: [{ placement: 'top' }, { placement: 'bottom' }],
|
|
|
|
|
+ radius: 2,
|
|
|
|
|
+ // shadowBlur: 10,
|
|
|
|
|
+ // shadowColor: '#e0e0e0',
|
|
|
|
|
+ // shadowOffsetX: 3,
|
|
|
|
|
+ size: [150, 60],
|
|
|
|
|
+ stroke: '#1777FF', // 边框色
|
|
|
},
|
|
},
|
|
|
// 节点状态样式
|
|
// 节点状态样式
|
|
|
state: {
|
|
state: {
|
|
|
- hover: {
|
|
|
|
|
- lineWidth: 2,
|
|
|
|
|
- stroke: '#69c0ff',
|
|
|
|
|
- },
|
|
|
|
|
selected: {
|
|
selected: {
|
|
|
fill: '#bae7ff',
|
|
fill: '#bae7ff',
|
|
|
stroke: '#1890ff',
|
|
stroke: '#1890ff',
|
|
|
lineWidth: 2,
|
|
lineWidth: 2,
|
|
|
},
|
|
},
|
|
|
|
|
+ active: {
|
|
|
|
|
+ fill: '#0b0',
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
edge: {
|
|
edge: {
|
|
|
- type: 'polyline', // 边类型
|
|
|
|
|
|
|
+ type: 'ant-line', // 边类型
|
|
|
style: {
|
|
style: {
|
|
|
- stroke: '#91d5ff', // 边的颜色
|
|
|
|
|
- lineWidth: 2, // 边的宽度
|
|
|
|
|
|
|
+ stroke: '#1777FF', // 边的颜色
|
|
|
|
|
+ lineWidth: 1, // 边的宽度
|
|
|
|
|
+ lineDash: [10, 10],
|
|
|
endArrow: true, // 是否有箭头
|
|
endArrow: true, // 是否有箭头
|
|
|
|
|
+ router: {
|
|
|
|
|
+ type: 'orth',
|
|
|
|
|
+ },
|
|
|
},
|
|
},
|
|
|
// 边的状态样式
|
|
// 边的状态样式
|
|
|
state: {
|
|
state: {
|
|
@@ -108,20 +124,67 @@
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
+ layout: {
|
|
|
|
|
+ type: 'dagre',
|
|
|
|
|
+ nodesep: 100,
|
|
|
|
|
+ ranksep: 120,
|
|
|
|
|
+ preventOverlap: true, // 防止节点重叠
|
|
|
|
|
+ nodeStrength: -50, // 节点之间的斥力
|
|
|
|
|
+ edgeStrength: 0.5, // 边的弹性系数
|
|
|
|
|
+ iterations: 200, // 迭代次数
|
|
|
|
|
+ animation: true, // 启用布局动画
|
|
|
|
|
+ },
|
|
|
|
|
+ autoFit: {
|
|
|
|
|
+ type: 'view', // 自适应类型:'view' 或 'center'
|
|
|
|
|
+ options: {
|
|
|
|
|
+ // 仅适用于 'view' 类型
|
|
|
|
|
+ when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配)
|
|
|
|
|
+ direction: 'both', // 适配方向:'x'、'y' 或 'both'
|
|
|
|
|
+ },
|
|
|
|
|
+ animation: {
|
|
|
|
|
+ // 自适应动画效果
|
|
|
|
|
+ duration: 1000, // 动画持续时间(毫秒)
|
|
|
|
|
+ easing: 'ease-in-out', // 动画缓动函数
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ behaviors: [
|
|
|
|
|
+ 'drag-canvas',
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'zoom-canvas',
|
|
|
|
|
+ sensitivity: 1.5, // 配置灵敏度
|
|
|
|
|
+ key: 'zoom-behavior', // 为交互指定key,便于后续更新
|
|
|
|
|
+ },
|
|
|
|
|
+ 'focus-element',
|
|
|
|
|
+ {
|
|
|
|
|
+ type: 'click-select',
|
|
|
|
|
+ degree: 1,
|
|
|
|
|
+ state: 'active',
|
|
|
|
|
+ unselectedState: 'inactive',
|
|
|
|
|
+ multiple: true,
|
|
|
|
|
+ trigger: ['shift'],
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 渲染
|
|
// 渲染
|
|
|
graph.render();
|
|
graph.render();
|
|
|
|
|
|
|
|
// 添加交互效果
|
|
// 添加交互效果
|
|
|
- graph.on('node:mouseenter', (evt) => {
|
|
|
|
|
- const node = evt.item;
|
|
|
|
|
- graph.setItemState(node, 'hover', true);
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- graph.on('node:mouseleave', (evt) => {
|
|
|
|
|
- const node = evt.item;
|
|
|
|
|
- graph.setItemState(node, 'hover', false);
|
|
|
|
|
|
|
+ // graph.on('node:mouseenter', (evt) => {
|
|
|
|
|
+ // const node = evt.item;
|
|
|
|
|
+ // graph.setItemState(node, 'hover', true);
|
|
|
|
|
+ // });
|
|
|
|
|
+
|
|
|
|
|
+ // graph.on('node:mouseleave', (evt) => {
|
|
|
|
|
+ // const node = evt.item;
|
|
|
|
|
+ // graph.setItemState(node, 'hover', false);
|
|
|
|
|
+ // });
|
|
|
|
|
+ graph.on(NodeEvent.POINTER_ENTER, (event) => {
|
|
|
|
|
+ const { target } = event;
|
|
|
|
|
+ graph.updateNodeData([
|
|
|
|
|
+ { id: target.id, style: { labelText: 'Hovered', fill: 'lightgreen', labelFill: 'lightgreen' } },
|
|
|
|
|
+ ]);
|
|
|
|
|
+ graph.draw();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 响应窗口大小变化
|
|
// 响应窗口大小变化
|