OrgChart.vue 5.3 KB

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