基于ReactFlow 实现组件拓展

758 阅读2分钟

先看看完成后效果

image.png

  • 上个版本实现了跟公司业务关联 实现了联系人库可连线、点击详情、删除等功能
  • 这个版本需要拓展一些功能 如联系人节点添加标识 添加文本框等

节点打标

需要在联系人三个角落添加标识 颜色走后端控制 基于原来的FloatingNode 更改 FloatingNode.tsx

export default memo((node: NodeProps) => {
  //connectable:控制连接点是否显隐 tag:标识文案 tagColor:标识背景色 
  // 因为业务需要 只弄了三个角
  const { data: { label, connectable, item } } = node
  const { tagColor = {}, tag = {} } = item
  return (
    <div className={styles.flowNode} >
      <div className={styles.flowNodeContent}>
        {label}
      </div>
      <div className={classNames(styles.flowNodeLabel, styles.leftTop, tagColor.decisionRole ? '' : styles.noneLabel)}
        style={tagColor.decisionRole ? { backgroundColor: tagColor.decisionRole } : {}} title={tag.decisionRole}>{tag.decisionRole}</div>

      <div className={classNames(styles.flowNodeLabel, styles.rightTop, tagColor.customerAttitude ? '' : styles.noneLabel)}
        style={tagColor.customerAttitude ? { backgroundColor: tagColor.customerAttitude } : {}} title={tag.customerAttitude}>{tag.customerAttitude}</div>

      <div className={classNames(styles.flowNodeLabel, styles.rightBtm, tagColor.competeRelation ? '' : styles.noneLabel)}
        style={tagColor.competeRelation ? { backgroundColor: tagColor.competeRelation } : {}} title={tag.competeRelation}>{tag.competeRelation}</div>
      {
        !connectable && <><Handle type="target" position={Position.Top} id="a" />
          <Handle type="source" position={Position.Bottom} id="b" />
          <Handle type="source" position={Position.Right} id="c" />
          <Handle type="target" position={Position.Left} id="d" /></>
      }

    </div>
  );
})

组件库

 const getComLibModal = () => {
     // 拖拽功能基本和联系人列表一致
    return <div className={styles.topModal}>
      <div className={styles.headTitle}>
        组件库
        <CloseOutlined onClick={() => setIsComLibShow(false)} />
      </div>
      <div className={styles.modalContent}>
        <div className={styles.comContainer} onDragStart={(event) => onDragStart(event, { label: '双击编辑文本', nodeType: 'labelText' }, 'customNode')} draggable>
          <div>
            <span className="icon iconfont icon-ziti"></span>
          </div>
          <p>文本</p>
        </div>

        <div className={styles.comContainer} onDragStart={(event) => onDragStart(event, { label: '', nodeType: 'rectangle' }, 'customNode')} draggable>
          <div>
            <span className={styles.comRectangle}></span>
          </div>
          <p>矩形</p>
        </div>

        <div className={styles.comContainer} onDragStart={(event) => onDragStart(event, { label: '', nodeType: 'circular' }, 'customNode')} draggable>
          <div>
            <span className={styles.comCircular}></span>
          </div>
          <p>圆形</p>
        </div>
      </div>
    </div>
  }

接下来难点就是如何实现文本编辑 采用给dom添加双击事件

 const addTextEvnt = () => {
    // 获取所有拖拽的组件 遍历出来 依次添加事件
    let dom: any = document.querySelectorAll(".react-flow__node-customNode")
    dom.forEach((el: HTMLDivElement) => {
      if (!el.ondblclick ) {
        el.ondblclick = ((event: MouseEvent) => {
          const id = el.getAttribute('data-id')
          const oldHtml = el.children[0].innerHTML;
          if (!oldHtml.includes('<textarea')) {
            // 触发双击事件时 创建一个文本域 插入当前dom中
            let textarea = document.createElement('textarea')
            textarea.maxLength = 30
            el.children[0].innerHTML = '';
            el.children[0].appendChild(textarea);
            textarea.focus();
            textarea.onblur = (evt: FocusEvent) => {
              el.children[0].innerHTML = (evt.target as HTMLInputElement).value
              setLabel(id, (evt.target as HTMLInputElement).value)
            }
          }

        })
      } 
    })
  }
  // 文本编辑后 Flow同步更改
 const setLabel = (labelId: string, labelVal: string) => {
    if (labelId) {
      setNodes((es: Node[]) =>
        es.map((el) => {
          if (el.id === labelId) {
            el.data.label = labelVal;
          }
          return el;
        })
      );
    }
  }

建立一个组件库node

CustomNode.tsx

export default memo((node: NodeProps) => {
  const { data: { label, item } } = node
  const { nodeType = '' } = item
  const styleMap = {
    "labelText": styles.textLabel,
    "rectangle": styles.rectangle,
    "circular": styles.circular
  }
  return (
    <div className={styleMap[nodeType]} title={label}>
      {label}
    </div>
  );
})

完成后送个产品看 产品举起大拇指 说还是有点瑕疵 就是文本框不支持换行、并且没考虑小屏用户画图体验 明天要给上面演示 今晚加班努力下~

作为一个每天雷打不动摸鱼的前端 知道今天没得鱼摸了~~

第一步 换行问题 由于是使用innerHTML 所以文本域带的\n转行符是没用的

        // 取值和存值的时候 把\n 和<br> 互换即可
        let textarea = document.createElement('textarea')
        textarea.maxLength = 30
        textarea.value = oldHtml.replaceAll("<br>", "\n")  //控制换行  innerHTML不识别\n 必须使用br
        el.children[0].innerHTML = '';
        el.children[0].appendChild(textarea);
        textarea.focus();
        textarea.onblur = (evt: FocusEvent) => {
          let textHTML = (evt.target as HTMLInputElement).value.replaceAll("\n", "<br />")
          el.children[0].innerHTML = textHTML
          setLabel(id, (evt.target as HTMLInputElement).value)
        }

第二步 本来是想用大弹窗解决菜单栏、导航栏占地面积过多导致小屏的内容区域过小问题 但是想着既然是画图 何不弄个画图区域全屏功能呢?

image.png 添加全屏功能按钮

    // ControlButton flow对外开放的自定义工具图标
     <Controls showFitView={false} showInteractive={false} >
          <ControlButton onClick={() => onFullScreen()}>
            <FullscreenOutlined />
          </ControlButton>
        </Controls>
    
  const onFullScreen = () => {
    try {
       // 获取需要全屏的dom
      let fullarea = document.querySelectorAll('.container')[0]
      if (!document.fullscreenElement) {
        // 全屏
        fullarea.requestFullscreen();
      } else {
        // 退出全屏
        if (document.exitFullscreen) {
          document.exitFullscreen();
        }
      }
    } catch (err) {
      console.log("err>>", err)
    }
  }