Преглед на файлове

feat: 应急架构体系canvs

bxy преди 10 месеца
родител
ревизия
892c1649ac
променени са 2 файла, в които са добавени 145 реда и са изтрити 63 реда
  1. 120 57
      src/views/emergency/components/OrgChart.vue
  2. 25 6
      src/views/emergency/organization/PageOrganization.vue

+ 120 - 57
src/views/emergency/components/OrgChart.vue

@@ -4,12 +4,17 @@
 
 <script setup lang="ts">
   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 接口
   interface TreeData {
     id: string;
-    label: string;
+    data: { name: string };
     children?: TreeData[];
   }
 
@@ -17,12 +22,49 @@
     treeData: TreeData;
   }>();
 
-  // 转换数据
+  // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
   const data = treeToGraphData(props.treeData);
   // 图表容器引用
   const container = ref<HTMLDivElement | null>(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 = () => {
     if (!container.value) return;
@@ -36,69 +78,43 @@
     // 创建新的 G6 实例
     graph = new Graph({
       container: container.value,
-      // width: window.innerWidth,
-      // height: window.innerHeight,
       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: {
-        type: 'circle', // 节点类型
+        type: 'chart-node', // 节点类型
         style: {
-          fill: '#e6f7ff', // 填充色
-          stroke: '#91d5ff', // 边框色
+          fill: '#E7F1FF', // 填充色
+          labelPlacement: 'center',
           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: {
-          hover: {
-            lineWidth: 2,
-            stroke: '#69c0ff',
-          },
           selected: {
             fill: '#bae7ff',
             stroke: '#1890ff',
             lineWidth: 2,
           },
+          active: {
+            fill: '#0b0',
+          },
         },
       },
       edge: {
-        type: 'polyline', // 边类型
+        type: 'ant-line', // 边类型
         style: {
-          stroke: '#91d5ff', // 边的颜色
-          lineWidth: 2, // 边的宽度
+          stroke: '#1777FF', // 边的颜色
+          lineWidth: 1, // 边的宽度
+          lineDash: [10, 10],
           endArrow: true, // 是否有箭头
+          router: {
+            type: 'orth',
+          },
         },
         // 边的状态样式
         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.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();
     });
 
     // 响应窗口大小变化

+ 25 - 6
src/views/emergency/organization/PageOrganization.vue

@@ -17,16 +17,35 @@
 
   const treeData = {
     id: 'root',
-    label: '应急管理体系',
+    data: { name: '应急领导小组' },
     children: [
       {
         id: 'group1',
-        label: '应急领导小组',
+        data: { name: '应急指挥小组' },
         children: [
-          { id: 'team1', label: '综合协调组' },
-          { id: 'team2', label: '抢险处置组' },
-          { id: 'team3', label: '应急抢修组' },
-          { id: 'team4', label: '救治保障组' },
+          { id: 'team1', data: { name: '应急指挥组' } },
+          { id: 'team2', data: { name: '抢险处置组' } },
+          { id: 'team3', data: { name: '应急抢修组' } },
+          { id: 'team4', data: { name: '救治保障组' } },
+        ],
+      },
+      {
+        id: 'group2',
+        data: { name: '应急响应小组' },
+        children: [
+          { id: 'team5', data: { name: '信息收集组' } },
+          { id: 'team6', data: { name: '现场指挥组' } },
+          { id: 'team7', data: { name: '后勤保障组' } },
+        ],
+      },
+      {
+        id: 'group3',
+        data: { name: '应急支援小组' },
+        children: [
+          { id: 'team8', data: { name: '医疗救援组' } },
+          { id: 'team9', data: { name: '物资保障组' } },
+          { id: 'team10', data: { name: '心理疏导组' } },
+          { id: 'team11', data: { name: '信息发布组' } },
         ],
       },
     ],