项目需求

antv/G6 - 4.8.24 版本地址
实现一个流程图,根据不同阶段、不同功能、不同状态来显示图形
1、线,需要根据状态展示不同的颜色和动画效果
2、节点部分区域需要点击功能
3、文本太长需要显示…(三个点)
4、不同状态的节点需要使用不同icon(svg图片)
5、根据需求,采用G6缩进树的布局方式,缩进树地址
6、鼠标悬浮需要展示详情数据
7、需要操作栏快速缩放还原比例

一、需要解决的问题

1、4xx版本,节点拖拽会留下痕迹,由于我画布是白色的底,所以使用官方提供的解决方案,就是在节点最底层画一个白色的矩形(图形后画的会覆盖先画)

二、初步使用

1.动态数据-组件封装(解决拖拽会留下痕迹的问题,引用图片,在节点右上角渲染图标,实现,事现旋转动画,达到loading效果)

由于旋转会绕着节点的中心点,所以需要将节点的中心点移到右上角图形的中心
假设:右上角图形中心点距离顶部和在右边的距离是12,则中心点设置为(-w + 12,-12)
vue3代码如下(示例):

<template>
  <div
    id="mountNode"
    ref="mountNodeRef"
  ></div>
</template>
<script setup lang="ts">
import { ref,reactive } from 'vue'
import G6 from '@antv/g6'
import runImg from '@/assets/run.svg'

const treeGraph = reactive<any>({
  graph: {},
})

interface DataType{
	id:string
	children:DataType[]
}

const drawerImg= (cfg: any, group: any, w: number, h: number) => {
  // 图片
  let img
  switch (cfg.status) {
    case StatusType.ING:
      img = runImg
      break
    case StatusType.ABNORMAL:
      img = abnormalImg
      break
    case StatusType.END:
      img = successImg
      break
    default:
      img = waitImg
  }
  const image = group.addShape('image', {
    attrs: {
      x: -8,
      y: -8,
      width: 16,
      height: 16,
      img, // import 引入的图片
    },
    name: 'image-shape',
  })
  // 旋转动画
  if (cfg.status === StatusType.ING) {
    image.animate(
      (ratio: any) => {
        // 每一帧的操作,入参 ratio:这一帧的比例值(Number)。返回值:这一帧需要变化的参数集(Object)。
        // 旋转通过矩阵来实现
        // 当前矩阵(矩阵文档中有描述)
        const matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]
        // 目标矩阵
        const toMatrix = G6.Util.transform(matrix, [['r', ratio * Math.PI * 2]])
        // 返回这一帧需要的参数集,本例中只有目标矩阵
        return {
          matrix: toMatrix,
        }
      },
      {
        repeat: true, // 动画重复
        duration: 3000,
        easing: 'easeLinear',
      }
    )
  }
}


// 注册自定义节点
G6.registerNode('card-node', {
  draw: function drawShape(cfg: any, group) {
    // 获取初始化时defaultNode设置的宽高
    const w = cfg.size[0]
    const h = cfg.size[1]
    
    // 中心点坐标(默认是节点左上角),这里设置成图形中心(影响图像旋转等功能)
    // const centerX =  -w / 2
    // const centerY = -h / 2
    // 中心点坐标(默认是节点左上角),这里设置成节点右上角距离顶部和右边12的位置
    const centerX = -w + 12
    const centerY = -12
 
    const r = 10 // 边的倒角 radius
    const color = '#004CFE' // 文本颜色
    const baseColor = '#001043' // 文本颜色
    const backgroundColor = 'rgba(0,76,254,0.2)' // 填充颜色

    // 主图,容器矩形,画白色容器矩形,防止拖拽产生的痕迹
    const shape = group.addShape('rect', {
	   attrs: {
	      x: centerX,
	      y: centerY,
	      width: w,
	      height: h,
	      shadowColor: 'rgba(0,0,0,0.16)',
	      shadowOffsetX: 0,
	      shadowOffsetY: 0,
	      shadowBlur: 4,
	      radius: r, // 4个角都设置圆角
	      fill: '#fff',
	    },
	    name: 'main-box', // 必须,用来操作图行,需要唯一
	    // draggable: true, // 只用为true,图形才可以拖拽,同时需要配置modes中开启拖拽功能,如果上层重叠有图形,重叠的图形也需要开启该属性
	})
	  
	// 之后添加的图形会默认覆盖在之前添加的图形上面
	// 新增图形,矩形头部
	group.addShape('rect', {
	    attrs: {
	      x: centerX,
	      y: centerY,
	      width: w,
	      height: 28,
	      fill: baseColor ,
	      radius: [r, r, 0, 0], // 左上和右上设置圆角,左下和右下不变
	    },
	    name: 'header-box',
	    // draggable: true,
	})
	  
	// 矩形头部文本
	group.addShape('text', {
	    attrs: {
	      x: centerX + 8,
	      y: centerY + 14,
	      lineHeight: 20,
	      text: cfg.text, // 节点数据text字段
	      fill: color,
	      textBaseline: 'middle', // 文本垂直居中
	    },
	    name: 'title',
	    // draggable: true,
	})

    // 右上角图标
    drawerImg(cfg, group, w, h)

    // 有子数据的矩形添加收起/展开的按钮
    cfg.children &&
      group.addShape('marker', {
        attrs: {
          x: 12,
          y: h / 2 - 12,
          r: 6,
          cursor: 'pointer',
          symbol: cfg.collapsed ? G6.Marker.expand : G6.Marker.collapse,// G6 自带的标记
          stroke: '#666',
          lineWidth: 1,
          fill: '#fff',
        },
        name: 'collapse-icon',
      })

    return shape
  },
  setState(name, value, item: any) {
    // 开启缩进树的节点按钮,响应节点点击事件,展开、收起子节点树
    if (name === 'collapsed') {
      const marker = item.get('group').find((ele: any) => ele.get('name') === 'collapse-icon')
      const icon = value ? G6.Marker.expand : G6.Marker.collapse
      marker.attr('symbol', icon)
    }
  },
})

// 初始化图形实例
const initGraph = () => {
  const width = mountNodeRef.value.scrollWidth
  const height = mountNodeRef.value.scrollHeight

  const graph = new G6.TreeGraph({
    container: 'mountNode', // String | HTMLElement,必须,容器 id 或容器本身
    width, // Number,必须,图的宽度
    height, // Number,必须,图的高度
    plugins: [tooltip, toolbar], // 添加tooltip
    // 画布配置
    modes: {
      default: ['drag-canvas', 'zoom-canvas'], // 允许拖拽画布、放缩画布(没有添加节点拖拽)
    },
    defaultNode: {
      type: 'card-node',// 自定义node节点
      size: [132, 98],
    },
    defaultEdge: {
      type: 'cubic-horizontal',
      style: {
        endArrow: true,
      },
    },
    // 基本布局配置
    layout: {
      type: 'indented', // 布局模式(缩进树布局)
      direction: 'LR', // 布局方向
      dropCap: false,
      indent: 260, // 图形水平间距
      getHeight: () => {
        return 100 // 图形垂直间距
      },
    },
  })

  toRaw(treeGraph).graph = graph
}

onMounted(() => {
  if (mountNodeRef.value) {
  	// 初始化图形,渲染需要在异步数据更新之后
    initGraph()
  }
})

// 模拟数据
// const data = {
//   id: 'A',
//   text:'我是文本超级长的文本给个省略号',
//   status:'ING',
//   children: [
//     {
//       id: 'A1',
//       text:'我是文本',
//       status:'ING',
//       children: [{ id: 'A11', text:'我是文本', }, { id: 'A12',  text:'我是文本', }],
//     },
//     {
//       id: 'A2',
//       text:'我是文本',
//       children: [
//         {
//           id: 'A21',
//           text:'我是文本',
//           children: [{ id: 'A211',  text:'我是文本', }, { id: 'A212',  text:'我是文本', }],
//         },
//         {
//           id: 'A22',
//           text:'我是文本',
//         },
//       ],
//     },
//   ],
// };


// 监听数据变化渲染图形
watch(
  () => props.data,
  (value) => {
    toRaw(treeGraph).graph.data(value)
    toRaw(treeGraph).graph.render() // 渲染图
    toRaw(treeGraph).graph.fitView() // 布局

	// 监听节点点击
    toRaw(treeGraph).graph.on('node:click', (e: any) => {
      /**
       * 控制展开收起的小图标事件
       * collapse-icon 是创建图形的name,将点击响应确定在一定的范围
       */
      if (e.target.get('name') === 'collapse-icon') {
        e.item.getModel().collapsed = !e.item.getModel().collapsed
        toRaw(treeGraph).graph.setItemState(e.item, 'collapsed', e.item.getModel().collapsed)
        toRaw(treeGraph).graph.layout()
      }
    })
    
	// 可视窗口变化,更新视图
    if (typeof window !== 'undefined') {
      window.onresize = () => {
        if (!toRaw(treeGraph).graph || toRaw(treeGraph).graph.get('destroyed')) return
        if (!mountNodeRef.value || !mountNodeRef.value.clientWidth || !mountNodeRef.value.clientHeight) return
        toRaw(treeGraph).graph.changeSize(mountNodeRef.value.clientWidth, mountNodeRef.value.clientHeight)
        toRaw(treeGraph).graph.fitView()
      }
    }
  }
)
</script>

2.文本太长,超出部分显示(…),如下函数返回新的文本和文本宽度

// 计算文本宽度,和超出显示三个点
const truncateText = (text: string, maxWidth: number, fontSize = 12, fontFace = 'Microsoft YaHei') => {
  // 创建一个临时canvas来测量文本宽度
  const tempCanvas = document.createElement('canvas')
  const tempCtx = tempCanvas.getContext('2d')!
  tempCtx.font = fontSize + 'px ' + fontFace
  // 计算文本宽度
  let textWidth = tempCtx.measureText(text).width
  // 如果文本宽度超出最大宽度,则截断并添加省略号
  if (textWidth > maxWidth) {
    // 尝试去除一个字符,然后重新测量,直到文本宽度小于或等于最大宽度
    while (textWidth > maxWidth) {
      text = text.slice(0, -1) // 移除最后一个字符并添加省略号
      textWidth = tempCtx.measureText(text).width
    }
    return {
      width: textWidth,
      text: text + '...', // 移除最后一个字符并添加省略号
    }
  } else {
    return {
      width: textWidth,
      text,
    }
  }
}

// 用例,修改上文 - 矩形头部文本
G6.registerNode('card-node', {
	draw: function drawShape(cfg: any, group) {
		  // ...其他配置
		  // 矩形头部文本
		  const { text } = truncateText(cfg.text, 100)
		  group.addShape('text', {
		    attrs: {
		      x: centerX + 8,
		      y: centerY + 14,
		      lineHeight: 20,
		      // text: cfg.text, // 节点数据text字段
		      text: text,
		      fill: color,
		      textBaseline: 'middle', // 文本垂直居中
		    },
		    name: 'title',
		    // draggable: true,
		})
	}
})

3.根据某些字段的值给线增加动画,并在线上渲染文本

需要修改defaultEdge配置,代码如下(示例):


const lineDash = [4, 2, 1, 2]
G6.registerEdge(
  'line-dash',
  {
    afterDraw(cfg: any, group: any) {
      // 获取图形组中的第一个图形,在这里就是边的路径图形
      const shape = group.get('children')[0]
      // 由于没有直接的线数据,需要根据线上的源节点或者目标节点的id来获取,节点的数据
      // 这里获取目标节点的模型数据
      const targetModel = toRaw(treeGraph).graph.findById(cfg.target).getModel()

      if (targetModel.status && targetModel.status === 'ING') {
		// 增加动画
        let index = 0
        // Define the animation
        shape.animate(
          () => {
            index++
            if (index > 9) {
              index = 0
            }
            const res = {
              lineDash,
              lineDashOffset: -index,
            }
            return res
          },
          {
            repeat: true, // whether executes the animation repeatly
            duration: 3000, // the duration for executing once
          }
        )
      }
    },
  },
  'cubic-horizontal' // extend the built-in edge 'cubic-horizontal'
)

const initGraph = () => {
	const graph = new G6.TreeGraph({
		// ...其他配置
		defaultEdge: {
	      type: 'line-dash',// 自定义线段
	      style: {
	        lineWidth: 2,
	        stroke: '#bae7ff',
	        endArrow: true,
	      },
	      // 线上文本的样式配置
	      labelCfg: {
	        autoRotate: true,
	        style: {
	          fill: '#1890ff',
	          fontSize: 14,
	          background: {
	            fill: '#ffffff',
	            padding: [2, 2, 2, 2],
	            radius: 2,
	          },
	        },
	      },
	    },
	})
}

线上配置文本需要在graph.render() 之前,修改上文中的watch

watch(
  () => props.data,
  (value) => {
    // 设置各个边样式及其他配置,以及在各个状态下节点的 KeyShape 的样式。
    toRaw(treeGraph).graph.edge(function (edge: any) {
      const targetItem = toRaw(treeGraph)
        .graph.findById(edge.target as string)
        .getModel()

      const config: any = {}
      //  存在流量
      if (targetItem.status) {
          if (targetItem.status === 'ERROR') {
            config.style = {
              stroke: 'red',
            }
          }
          config.label = targetItem.status
      }

      return config
    })
    // ...其他配置
    toRaw(treeGraph).graph.render() // 渲染图
  })

4.自定义按钮,实现局部区域点击

1、按钮由一个矩形节点和文本节点组成,上文G6.registerNode增加配置
2、节点点击,锁定局部区域,graph的node:click事件

G6.registerNode('card-node', {
  draw: function drawShape(cfg: any, group) {
  	// ...其他配置
  	// 按钮矩形区域
    group.addShape('rect', {
      attrs: {
        x: -52,
        y: h - 38,
        width: 64,
        height: 26,
        fill: 'rgba(35,131,228,0.1)',
        radius: [4, 0, r, 0],
        cursor: 'pointer',
      },
      name: 'btn',
      draggable: true,
    })
    group.addShape('text', {
      attrs: {
        x: -20,
        y: h - 25,
        text: '查看详情',
        fill: '#2383E4',
        fontSize: 12,
        fontFamily: textFontFace,
        textAlign: 'center', // 文本水平居中
        textBaseline: 'middle', // 文本垂直居中
        cursor: 'pointer',
      },
      name: 'btn-text',
      draggable: true,
    })
  }
})

toRaw(treeGraph).graph.on('node:click', (e: any) => {
    // 点击了查看详情
    if (e.target.get('name') === 'btn-text' || e.target.get('name') === 'btn') {
      const model = e.item.getModel()
      // 获取数据
      console.log(model)
    }
})

5.开启自带的操作栏

const toolbar = new G6.ToolBar()
const graph = new G6.TreeGraph({
  plugins: [..., toolbar], // 添加tooltip
})

5.鼠标悬浮展示数据

const graph = new G6.TreeGraph({
  plugins: [..., tooltip], // 添加tooltip
})

const tooltip = new G6.Tooltip({
  offsetX: 10,
  offsetY: 10,
  // 允许出现 tooltip 的 item 类型
  itemTypes: ['node'],
  shouldBegin: (e: any) => {
    const model = e.item.getModel()
    const type = e.item.getType()
    // if (type === 'node' && model.id !== 'custom') {
    //  return true
    // }
    return false
  },
  // 自定义 tooltip 内容
  getContent: (e: any) => {
    const model = e.item.getModel()
    let outDiv = document.createElement('div')
    outDiv.style.width = 'fit-content'
    outDiv.innerHTML = `
        <h4 style="font-size:16px;font-weight:bold;margin-bottom:6px">节点详情</h4>
        <ul style="font-size:14px;">
          <li>type: ${model.nodeType}</li>
          <li>code: ${model.code}</li>
          <li>name: ${model.name}</li>
        </ul>`
    return outDiv
  },
})

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部