OrgChart.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. <template>
  2. <div class="org-chart-container" ref="container"></div>
  3. </template>
  4. <script setup lang="ts">
  5. import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
  6. import { Graph, treeToGraphData, NodeEvent, CanvasEvent } from '@antv/g6';
  7. // import { Graph, treeToGraphData, register, ExtensionCategory, NodeEvent, Polyline, CanvasEvent } from '@antv/g6';
  8. /**
  9. * @description: 为了确保图的正确渲染和交互,建议按照 G6 标准数据结构组织数据。
  10. * 每个元素(节点、边、组合)应包含一个 data 字段,用于存放业务数据和自定义属性。
  11. */
  12. // 定义 props 接口
  13. interface TreeData {
  14. id: string;
  15. data: { name: string };
  16. children?: TreeData[];
  17. }
  18. const props = defineProps<{
  19. treeData: TreeData;
  20. }>();
  21. const emits = defineEmits<{
  22. (event: 'node-click', nodeData: any): void;
  23. (event: 'canvas-click'): void;
  24. }>();
  25. let data = treeToGraphData(props.treeData); // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
  26. const container = ref<HTMLDivElement | null>(null); // 图表容器引用
  27. let graph: any = null;
  28. let pending = false; // 防止并发 initGraph
  29. // 初始化图表
  30. const initGraph = async () => {
  31. if (pending || !container.value) return;
  32. pending = true;
  33. // 销毁旧实例
  34. if (graph) {
  35. graph.off();
  36. graph.destroy();
  37. graph = null;
  38. }
  39. // 等待下一帧,确保销毁完成
  40. await nextTick();
  41. // 创建新的 G6 实例
  42. graph = new Graph({
  43. container: container.value,
  44. padding: [20, 20, 20, 20], // 图表内边距
  45. data,
  46. node: {
  47. type: 'rect', // 使用内置的矩形节点类型
  48. style: {
  49. labelText: (d: any) => d.data.name, // 节点文本
  50. labelFill: '#333', // 文本颜色
  51. labelFontSize: 14, // 文本大小
  52. size: [250, 50],
  53. lineWidth: 1, // 边框宽度
  54. // lineDash: [5, 5], // 虚线边框
  55. stroke: '#1777FF', // 边框色
  56. fill: '#E7F1FF', // 填充色
  57. radius: 8,
  58. labelPlacement: 'center',
  59. ports: [{ placement: 'top' }, { placement: 'bottom' }],
  60. },
  61. // 节点状态样式
  62. state: {
  63. selected: {
  64. fill: '#1777FF',
  65. stroke: '#1777FF',
  66. lineWidth: 1,
  67. labelFill: '#fff', // 选中状态下文本颜色
  68. labelFontSize: 16, // 选中状态下文本大小
  69. },
  70. },
  71. },
  72. edge: {
  73. type: 'ant-line', // 边类型
  74. style: {
  75. stroke: '#1777FF', // 边的颜色
  76. lineWidth: 1, // 边的宽度
  77. lineDash: [10, 10],
  78. endArrow: true, // 是否有箭头
  79. router: {
  80. type: 'orth',
  81. },
  82. },
  83. },
  84. layout: {
  85. type: 'dagre',
  86. nodesep: 100,
  87. ranksep: 120,
  88. preventOverlap: true, // 防止节点重叠
  89. nodeStrength: -50, // 节点之间的斥力
  90. edgeStrength: 0.5, // 边的弹性系数
  91. iterations: 200, // 迭代次数
  92. animation: true, // 启用布局动画
  93. },
  94. autoFit: {
  95. type: 'center', // 自适应类型:'view' 或 'center'
  96. // options: {
  97. // // 仅适用于 'view' 类型
  98. // when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配)
  99. // direction: 'both', // 适配方向:'x'、'y' 或 'both'
  100. // },
  101. // animation: {
  102. // // 自适应动画效果
  103. // duration: 1000, // 动画持续时间(毫秒)
  104. // easing: 'ease-in-out', // 动画缓动函数
  105. // },
  106. },
  107. autoResize: true, // 自动调整大小
  108. behaviors: [
  109. 'drag-canvas',
  110. {
  111. type: 'zoom-canvas',
  112. sensitivity: 0.5, // 配置灵敏度
  113. key: 'zoom-behavior', // 为交互指定key,便于后续更新
  114. },
  115. 'focus-element',
  116. {
  117. type: 'click-select',
  118. state: 'selected',
  119. unselectedState: 'inactive',
  120. multiple: true,
  121. trigger: ['shift'],
  122. },
  123. ],
  124. });
  125. // 渲染
  126. graph.render();
  127. // 监听节点点击事件
  128. graph.on(NodeEvent.CLICK, (evt) => {
  129. const { target } = evt;
  130. const nodeData = graph.getNodeData(target.id); // 获取节点数据
  131. emits('node-click', nodeData);
  132. });
  133. graph.on(CanvasEvent.CLICK, () => {
  134. graph?.fitCenter();
  135. emits('canvas-click');
  136. });
  137. window.addEventListener('resize', handleResize);
  138. pending = false;
  139. };
  140. // 处理窗口大小变化
  141. const handleResize = () => {
  142. if (graph && container.value) {
  143. graph.resize(container.value.offsetWidth, container.value.offsetHeight);
  144. graph.fitCenter();
  145. }
  146. };
  147. // 监听 treeData 变化
  148. watch(
  149. () => props.treeData,
  150. () => {
  151. data = treeToGraphData(props.treeData);
  152. initGraph();
  153. },
  154. { deep: true },
  155. );
  156. // 生命周期钩子
  157. onMounted(() => {
  158. data = treeToGraphData(props.treeData);
  159. initGraph();
  160. });
  161. onBeforeUnmount(() => {
  162. window.removeEventListener('resize', handleResize);
  163. if (graph) {
  164. graph.off(); // 移除所有事件监听
  165. graph.destroy();
  166. graph = null;
  167. }
  168. });
  169. </script>
  170. <style lang="scss" scoped>
  171. .org-chart-container {
  172. width: 100%;
  173. height: 100%;
  174. }
  175. </style>