react 怎么获取img 的src_基于React+Topology构建在线绘图工具

本文详述如何使用React搭建一个在线绘图工具,包括环境搭建、自定义图片、在线图片添加、文件操作、节点属性及事件设置、线条样式修改、预览功能等,提供自动排版和图形锁定等实用功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

本文将会带着大家基于React搭建一套属于自己的绘图工具。

环境搭建

初始化项目

create-react-app project

安装依赖

"@topology/activity-diagram": "^0.2.24",
"@topology/chart-diagram": "^0.3.0",
"@topology/class-diagram": "^0.2.24",
"@topology/core": "^0.3.1",
"@topology/flow-diagram": "^0.2.24",
"@topology/layout": "^0.3.0",
"@topology/sequence-diagram": "^0.2.24",
"antd": "^3.26.7",

至于基础布局的代码, 大家可以自由发挥, 本文就不赘述了。ok! 完成项目基本环境的搭建后, 就可以开始逐个完成以下功能点了。

功能介绍

自定义图片示例

主页面左侧的图形渲染区域, 可以自定义渲染。

const Layout = ({ Tools, onDrag }) => {
  return Tools.map((item, index) => (
    
"title">{item.group}
"button">
        {item.children.map((item, idx) => {
          // eslint-disable-next-line jsx-a11y/anchor-is-validreturn (              key={idx}
              title={item.name}
              draggable
              href="/#"
              onDragStart={(ev) => onDrag(ev, item)}
            >'iconfont ' + item.icon} style={{ fontSize: 13 }}>
          );
        })}

  ));
};

自定义配置项数据源, icon: 'icon-image'  指的是左侧显示的小图标。data数据name属性的值即代表image, topology通过此属性来判断渲染的是否是图片。image属性的值即是图片的地址。

{
  group: '自定义图片',
  children: [
    {
      name: 'image',
      icon: 'icon-image',
      data: {
        text: '',
        rect: {
          width: 100,
          height: 100
        },
        name: 'image',
        image: require('./machine.jpg')
      }
    },
  ]
}

支持在线图片添加的功能 如果觉得使用本地图片麻烦, 我们可以换成在线的图片。

首先我们根据图片的url得出base64.

function getBase64(url, callback) {
  var Img = new Image(),
    dataURL = '';
  Img.src = url + '?v=' + Math.random();
  Img.setAttribute('crossOrigin', 'Anonymous');
  Img.onload = function () {
    var canvas = document.createElement('canvas'),
      width = Img.width,
      height = Img.height;
    canvas.width = width;
    canvas.height = height;
    canvas.getContext('2d').drawImage(Img, 0, 0, width, height);
    dataURL = canvas.toDataURL('image/jpeg');
    return callback ? callback(dataURL) : null;
  };
}

最后通过onDrag 方法将在线的图片拖到画布上即可。

const onDrag = (event, image) => {
  event.dataTransfer.setData(
    'Text',
    JSON.stringify({
      name: 'image',
      rect: {
        width: 100,
        height: 100
      },
      image
    })
  );
};

支持新建文件, 打开文件, 导出json, 保存png与svg

新建一个空的画板

  canvas.open({ nodes: [], lines: [] });

打开已有图形的文件

const onHandleImportJson = () => {
  const input = document.createElement('input');
  input.type = 'file';
  input.onchange = event => {
    const elem = event.srcElement || event.target;
    if (elem.files && elem.files[0]) {
      const reader = new FileReader();
      reader.onload = e => {
        const text = e.target.result + '';
        try {
          const data = JSON.parse(text);
          canvas.open(data);
        } catch (e) {
          return false;
        } finally {

        }
      };
      reader.readAsText(elem.files[0]);
    }
  };
  input.click();
}

将画好的图保存为json文件

import * as FileSaver from 'file-saver';

FileSaver.saveAs(
  new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' }),
  `le5le.topology.json`
);

保存为png文件

canvas.saveAsImage('le5le.topology.png');

保存为SVG文件

const onHandleSaveToSvg = () => {
  const C2S = window.C2S;
  const ctx = new C2S(canvas.canvas.width + 200, canvas.canvas.height + 200);
  if (canvas.data.pens) {
    for (const item of canvas.data.pens) {
      item.render(ctx);
    }
  }
  let mySerializedSVG = ctx.getSerializedSvg();
  mySerializedSVG = mySerializedSVG.replace(
    '',
    ``
  );
  mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '');
  const urlObject = window.URL || window;
  const export_blob = new Blob([mySerializedSVG]);
  const url = urlObject.createObjectURL(export_blob);
  const a = document.createElement('a');
  a.setAttribute('download', 'le5le.topology.svg');
  a.setAttribute('href', url);
  const evt = document.createEvent('MouseEvents');
  evt.initEvent('click', true, true);
  a.dispatchEvent(evt);
}

撤销、恢复、复制、剪切、粘贴

 canvas.undo(); // 撤销
  canvas.redo(); // 恢复
  canvas.copy();  // 复制
  canvas.cut(); // 剪切
  canvas.paste(); // 粘贴

支持节点外观属性(位置大小边距, 边框样式, 字体样式)的设置

节点的位置和大小

const renderForm = useMemo(() => {
  return "X(px)">
          {getFieldDecorator('x', {
            initialValue: x
          })()}"Y(px)" name="y">
          {getFieldDecorator('y', {
            initialValue: y
          })()}"宽(px)" name="width">
          {getFieldDecorator('width', {
            initialValue: width
          })()}"高(px)" name="height">
          {getFieldDecorator('height', {
            initialValue: height
          })()}"角度(deg)" name="rotate">
          {getFieldDecorator('rotate', {
            initialValue: rotate
          })()}
}, [x, y, width, height, rotate, getFieldDecorator]);

边框样式

const renderStyleForm = useMemo(() => {
  return "线条颜色">
          {getFieldDecorator('strokeStyle', {
            initialValue: strokeStyle
          })(type="color" />)}"线条样式">
          {getFieldDecorator('dash', {
            initialValue: dash
          })('95%' }}>_________---------_ _ _ _ _- . - . - .
          )}"线条宽度">
          {getFieldDecorator('lineWidth', {
            initialValue: lineWidth
          })('100%' }} />)}
}, [lineWidth, strokeStyle, dash, getFieldDecorator]);

字体设置

const renderFontForm = useMemo(() => {
  return "字体颜色">
        {getFieldDecorator('color', {
          initialValue: color
        })(type="color" />)}
    
    
      "字体类型">
        {getFieldDecorator('fontFamily', {
          initialValue: fontFamily
        })()}
    
    
      "字体大小">
        {getFieldDecorator('fontSize', {
          initialValue: fontSize
        })()}
    
    
      "内容">
        {getFieldDecorator('text', {
          initialValue: text
        })()}
    
  
}, [color, fontFamily, fontSize, text, getFieldDecorator])

当我们对表单里面的每一项都进行修改时, 都会调用onFormValueChange 方法去改变对应节点的属性。最后将修改后的节点, 更新到画布上。

if (changedValues.node) {
  // 遍历查找修改的属性,赋值给原始Node
  for (const key in changedValues.node) {
    if (Array.isArray(changedValues.node[key])) {
    } else if (typeof changedValues.node[key] === 'object') {
      for (const k in changedValues.node[key]) {
        selected.node[key][k] = changedValues.node[key][k];
      }
    } else {
      selected.node[key] = changedValues.node[key];
    }
  }
}
canvas.updateProps(selected.node);

支持节点的数据属性 在实际的业务开发中, 难免会出现默认Node节点上的属性不够用的情况, 或者节点上有特定的业务数据。那么这个时候, 我们可以将这些特殊的数据存在节点的自定义数据字段。

const renderExtraDataForm = useMemo(() => {
  return  1.单击事件 2.双击事件 3.websocket事件 4.mqtt"自定义数据字段">
        {getFieldDecorator('data', {
          initialValue: JSON.stringify(extraFields) 
        })()}
    
  
}, [extraFields, getFieldDecorator])

注意: Le5leTopology.Node节点默认是没有data这个属性的.

支持节点自定义事件的功能

由上图可知, 节点的事件分为事件类型和事件行为两部分。事件类型可以分为:

  1. 单击事件
  2. 双击事件
  3. websocket事件 4.mqtt事件。

事件行为可以分为:

  1. 跳转链接
  2. 执行动画
  3. 执行函数
  4. 执行window下的全局函数
  5. 更新属性数据。

Node节点中自定events属性, 因此根据文档中各个属性的枚举值, 我们可以很简单的绘制出各个事件类型与事件行为的对应关系。具体的代码由于篇幅限制, 就不粘贴了。有兴趣的同学可以阅读对应的源码.接下来, 我来演示一下如何对节点进行单击事件与websocket事件的绑定。

单击执行自定义函数

查看上图动画, 我们可以发现, 每次点击图形, 都会输出我是自定义函数。那么在我们的编辑器上, 该如何配置呢?

接收来自于websocket的值

我们通过websocket往服务器发送一个信号, 同时将会接收对应的值。首先我们需要事先连接好ws服务器。

canvas.openSocket('ws://123.207.136.134:9010/ajaxchattest');

这一步很关键, 否则之后的流程都将会报错。然后我们新增一个拥有点击事件的节点, 模拟信号的发起。

最后我们定义一个节点用于接收websocket返回的值。

接下来, 我们可以点击预览按钮, 测试我们配置的代码对不对。

支持线条的样式修改

目前只支持上图几种属性的设置。线条的更新与节点的更新类似, 我们直接修改线条的属性, 然后通过updateProps 更新对应线条的样式。

const onHandleLineFormValueChange = useCallback(
  (value) => {
    const { dash, lineWidth, strokeStyle, name, fromArrow, toArrow, ...other } = value;
    const changedValues = {
      line: { rect: other, lineWidth, dash, strokeStyle, name, fromArrow, toArrow }
    };
    if (changedValues.line) {
      // 遍历查找修改的属性,赋值给原始line
      for (const key in changedValues.line) {
        if (Array.isArray(changedValues.line[key])) {
        } else if (typeof changedValues.line[key] === 'object') {
          for (const k in changedValues.line[key]) {
            selected.line[key][k] = changedValues.line[key][k];
          }
        } else {
          selected.line[key] = changedValues.line[key];
        }
      }
    }
    canvas.updateProps(selected.line);
  },
  [selected]
);

支持预览功能 当我们编辑完图形后, 需要预览。那么我们可以将画布上的数据通过路由传参(state)传递到新的页面, 最后通过

new Topology重新生成一块画布, 将图形渲染上去。
  let reader = new FileReader();
  const result = new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' });
  reader.readAsText(result, 'text/plain;charset=utf-8');
  reader.onload = (e) => {
    history.push({ pathname: '/preview', state: { data: JSON.parse(reader.result) } });
  }

支持锁定,设置全局线的起始和终止箭头

锁定

canvas.lock(2)
canvas.lock(0)

设置默认的连线类型

const onHandleSelectMenu = data => {
  setLineStyle(data.item.props.children);
  canvas.data.lineName = data.key;
  canvas.render();
}

设置默认的连线起始箭头

const onHandleSelectMenu1 = data => {
  setFromArrowType(data.item.props.children);
  canvas.data.fromArrowType = data.key;
  canvas.render();
}

设置默认的连线终止箭头

const onHandleSelectMenu2 = data => {
  setToArrowType(data.item.props.children);
  canvas.data.toArrowType = data.key;
  canvas.render();
}

支持自动排版功能 如果画出的图形比较乱, 那么可以使用自动居中的功能。首先我们通过 rect.calcCenter(); 获取当前图形的中心点,  然后我们计算出画布中心点与当前图形的中心点的差值, 最后通过调用  canvas.translate(x, y) 方法对图形进行平移。

 const onHandleFit = () => {
    const rect = canvas.getRect();
    rect.calcCenter();
    x = document.body.clientWidth / 2 - rect.center.x;
    y = (document.body.clientHeight - 66) / 2 - rect.center.y;
    canvas.translate(x, y);
  };

结尾 虽然Topology的官网有各个API的详细说明, 但是从API转化到实际业务中, 还是需要耗费蛮多时间。其次官方的React版本的数据流比较复杂且对于新手上手的成本比较高, 因此就萌生了想要写一版简单的topology-react 帮助大家快速上手。最后的最后, 感谢Alsmile开源的绘图引擎。如果对你有帮助, 别忘了给一个小小的star哦, 谢谢啦~

593f260422eea57d3ed9c333c514aa20.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值