React 递归手写流程图展示树形数据的操作方法

 更新时间:2023年11月11日 11:55:14   作者:adindefinite  
这篇文章主要介绍了React 递归手写流程图展示树形数据的操作方法,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

需求

根据树的数据结构画出流程图展示,支持新增前一级、后一级、同级以及删除功能(便于标记节点,把节点数据当作label展示出来了,实际业务中跟据情况处理)

文件结构

初始数据

[
  {
    "ticketTemplateCode": "TC20230404000001",
    "priority": 1,
    "next": [
      {
        "ticketTemplateCode": "TC20230705000001",
        "priority": 2,
        "next": [
          {
            "ticketTemplateCode": "TC20230707000001",
            "priority": 3
          },
          {
            "ticketTemplateCode": "TC20230404000002",
            "priority": 3
          }
        ]
      }
    ]
  }
]

功能实现 index.tsx

import React, { memo, useState } from 'react'
import uniqueId from 'lodash/uniqueId'
import NodeGroup from './group'
import { handleNodeOperation, NodeItemProps, NodeOperationTypes } from './utils'
import styles from './index.less'
export interface IProps {
  value?: any;
  onChange?: any;
}
/**
 * 树形流程图
 */
export default memo<IProps>(props => {
  const { value = [], onChange } = props
  const [activeKey, setActiveKey] = useState('TC20230404000001_1')
  const handleNode = (type = 'front' as NodeOperationTypes, item: NodeItemProps, index: number) => {
    switch (type) {
      case 'click' : {
        setActiveKey(`${item.ticketTemplateCode}_${item.priority}`)
      }; break
      case 'front':
      case 'next':
      case 'same':
      case 'del' : {
        const newList = handleNodeOperation(type, value, `${uniqueId()}`, item, index)
        // 添加前置工单时需要处理选中项
        if (type === 'front') {
          setActiveKey(`${item.ticketTemplateCode}_${item.priority + 1}`)
        }
        onChange?.(newList)
      }; break
    }
  }
  const renderNodes = (list = [] as NodeItemProps[]) => {
    return list.map((item, index) => {
      const key = `${item.ticketTemplateCode}_${item.priority}_${index}`
      const nodeGroupProps = {
        active: `${item.ticketTemplateCode}_${item.priority}` === activeKey,
        options: [],
        handleNode,
        front: item.priority !== 1,
        next: item.next && item.next.length > 0,
        item,
        index,
        sameLevelCount: list.length,
      }
      if (item.next && item.next.length > 0) {
        return (
          <NodeGroup
            key={key}
            {...nodeGroupProps}
            next
          >
            {renderNodes(item.next)}
          </NodeGroup>
        )
      }
      return <NodeGroup key={key} {...nodeGroupProps} />
    })
  }
  return (
    <div style={{ overflowX: 'auto' }}>
      <div className={styles.settingStyle}>{renderNodes(value)}</div>
    </div>
  )
})

group.tsx

import React, { memo, useEffect, useState } from 'react'
import NodeItem from './item'
import styles from './index.less'
import { NodeItemProps } from './utils'
export interface IProps {
  index?: number;
  active?: boolean;
  handleNode?: any;
  sameLevelCount?: number; // 同级工单数量
  front?: boolean; // 是否有前置工单
  next?: boolean; // 是否有后置工单
  children?: any;
  item?: NodeItemProps;
}
/**
 * 流程图-同层级组
 */
export default memo<IProps>(props => {
  const { active, front = false, next = false, handleNode, children, item, index, sameLevelCount = 1 } = props
  const [groupHeight, setGroupHeight] = useState(0)
  useEffect(() => {
    const groupDom = document.getElementById(`group_${item?.ticketTemplateCode}`)
    setGroupHeight(groupDom?.clientHeight || 0)
  }, [children])
  // 处理连接线展示
  const handleConcatLine = () => {
    const line = (showLine = true) => <div className={styles.arrowVerticalLineStyle} style={{ height: groupHeight / 2, backgroundColor: showLine ? 'rgba(0, 0, 0, 0.25)' : 'white' }} />
    return (
      <span>{line(index !== 0)}{line(index + 1 !== sameLevelCount)}</span>
    )
  }
  return (
    <div className={styles.groupDivStyle} id={`group_${item?.ticketTemplateCode}`}>
      {sameLevelCount < 2 ? null : handleConcatLine()}
      <NodeItem
        active={active}
        options={[]}
        handleNode={handleNode}
        front={front}
        next={next}
        item={item}
        sameLevelCount={sameLevelCount}
        index={index}
      />
      {children?.length ? <div>{children}</div> : null}
    </div>
  )
})

item.tsx

/* eslint-disable curly */
import { Select, Space, Tooltip } from 'antd'
import React, { memo } from 'react'
import styles from './index.less'
import { PlusCircleOutlined, CaretRightOutlined, DeleteOutlined } from '@ant-design/icons'
import { ProjectColor } from 'styles/projectStyle'
import { nodeOperationTip, NodeItemProps } from './utils'
export interface IProps {
  index?: number;
  active?: boolean; // 选中激活
  options: any[]; // 单项选项数据 放在select中
  handleNode?: any;
  sameLevelCount?: number; // 同级工单数量
  front?: boolean; // 是否有前置工单
  next?: boolean; // 是否有后置工单
  same?: boolean; // 是否有同级工单
  item?: NodeItemProps;
}
/**
 * 流程图-单项
 */
export default memo<IProps>(props => {
  const {
    index,
    active,
    options = [],
    handleNode,
    front = false,
    next = false,
    item,
  } = props
  // 添加 or 删除工单图标
  const OperationIcon = ({ type }) => {
    if (!active) return null
    const dom = () => {
      if (type === 'del') return <DeleteOutlined style={{ marginBottom: 9 }} onClick={() => handleNode(type, item, index)} />
      if (type === 'same')
        return <PlusCircleOutlined style={{ color: ProjectColor.colorPrimary, marginTop: 9 }} onClick={() => handleNode(type, item, index)} />
      const style = () => {
        if (type === 'front') return { left: -25, top: 'calc(50% - 7px)' }
        if (type === 'next') return { right: -25, top: 'calc(50% - 7px)' }
      }
      return (
        <PlusCircleOutlined
          className={styles.itemAddIconStyle}
          style={{ ...style(), color: ProjectColor.colorPrimary }}
          onClick={() => handleNode(type, item, index)}
        />
      )
    }
    return <Tooltip title={nodeOperationTip[type]}>{dom()}</Tooltip>
  }
  // 箭头
  const ArrowLine = ({ width = 50, show = false, arrow = true }) =>
    show ? (
      <div className={styles.arrowDivStyle} style={front && arrow ? { marginRight: -4 } : {}}>
        <div className={styles.arrowLineStyle} style={{ width, marginRight: front && arrow ? -4 : 0 }} />
        {!arrow ? null : (
          <CaretRightOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />
        )}
      </div>
    ) : null
  return (
    <div className={styles.itemStyle}>
      <Space direction="vertical" align="center">
        <div className={styles.itemMainStyle}>
          <ArrowLine show={front} />
          <div className={styles.itemSelectDivStyle}>
            <OperationIcon type="del" />
            // 可以不需要展示 写的时候便于处理节点操作
            {item?.ticketTemplateCode}
            <Select
              defaultValue="lucy"
              bordered={false}
              style={{
                minWidth: 120,
                border: `1px solid ${active ? ProjectColor.colorPrimary : '#D9D9D9'}`,
                borderRadius: 4,
              }}
              onClick={() => handleNode('click', item, index)}
              // onChange={handleChange}
              options={[ // 应该为props中的options
                { value: 'jack', label: 'Jack' },
                { value: 'lucy', label: 'Lucy' },
                { value: 'Yiminghe', label: 'yiminghe' },
                { value: 'disabled', label: 'Disabled', disabled: true },
              ]}
            />
            <OperationIcon type="same" />
            <OperationIcon type="front" />
            <OperationIcon type="next" />
          </div>
          <ArrowLine show={next} arrow={false} />
        </div>
      </Space>
    </div>
  )
})

utils.ts

/* eslint-disable curly */
export interface NodeItemProps {
  ticketTemplateCode: string;
  priority: number;
  next?: NodeItemProps[];
}
export type NodeOperationTypes = 'front' | 'next' | 'del' | 'same' | 'click'
/**
 * 添加前置/后置/同级/删除工单
 * @param type 操作类型
 * @param list 工单树
 * @param addCode 被添加的工单节点模版Code
 * @param item 操作节点
 */
export const handleNodeOperation = (type: NodeOperationTypes, list = [] as NodeItemProps[], addCode: NodeItemProps['ticketTemplateCode'], item: NodeItemProps, index: number) => {
  if (item.priority === 1 && type === 'front') return handleNodePriority([{ ticketTemplateCode: addCode, priority: item.priority, next: list }])
  if (item.priority === 1 && type === 'same') {
    return [
      ...(list || []).slice(0, index + 1),
      { ticketTemplateCode: addCode, priority: item.priority },
      ...(list || []).slice(index + 1, list?.length),
    ]
  }
  let flag = false
  const findNode = (child = [] as NodeItemProps[]) => {
    return child.map(k => {
      if (flag) return k
      if (type === 'front' && k.priority + 1 === item.priority && k.next && k.next?.findIndex(m => m.ticketTemplateCode === item.ticketTemplateCode) > -1) {
        flag = true
        return { ...k, next: [{ ticketTemplateCode: addCode, priority: item.priority, next: k.next }]}
      }
      if (type === 'next' && k.ticketTemplateCode === item.ticketTemplateCode) {
        flag = true
        return { ...k, next: [...(k.next || []), { ticketTemplateCode: addCode, priority: item.priority }]}
      }
      if (type === 'same' && k.priority + 1 === item.priority && k.next && k.next?.findIndex(m => m.ticketTemplateCode === item.ticketTemplateCode) > -1) {
        flag = true
        return { ...k, next: [
          ...(k.next || []).slice(0, index + 1),
          { ticketTemplateCode: addCode, priority: item.priority },
          ...(k.next || []).slice(index + 1, k.next?.length),
        ]}
      }
      if (type === 'del' && k.priority + 1 === item.priority && k.next && k.next?.findIndex(m => m.ticketTemplateCode === item.ticketTemplateCode) > -1) {
        flag = true
        console.log(index, (k.next || []).slice(0, index), (k.next || []).slice(index + 1, k.next?.length), 223)
        return { ...k, next: [
          ...(k.next || []).slice(0, index),
          ...(k.next || []).slice(index + 1, k.next?.length),
        ]}
      }
      if (k.next && k.next.length > 0) {
        return { ...k, next: findNode(k.next) }
      }
      return k
    })
  }
  return handleNodePriority(findNode(list))
}
// 处理层级关系
export const handleNodePriority = (list = [] as NodeItemProps[], priority = 1) => { // priority 层级
  return list.map((k: NodeItemProps) => ({ ...k, priority, next: handleNodePriority(k.next, priority + 1) }))
}
// 得到最大层级 即工单树的深度
export const getDepth = (list = [] as NodeItemProps[], priority = 1) => {
  const depth = list.map(i => {
    if (i.next && i.next.length > 0) {
      return getDepth(i.next, priority + 1)
    }
    return priority
  })
  return list.length > 0 ? Math.max(...depth) : 0
}
export const nodeOperationTip = {
  front: '增加前置工单',
  next: '增加后置工单',
  same: '增加同级工单',
  del: '删除工单',
}

index.less

.settingStyle {
  margin-left: 50px;
}
.groupDivStyle {
  display: flex;
  flex-direction: row;
  align-items: center;
}
.itemStyle {
  display: flex;
  flex-direction: row;
  align-items: center;
  height: 94px;
}
.itemMainStyle {
  display: flex;
  flex-direction: row;
  align-items: center;
}
.arrowLineStyle {
  height: 1px;
  background-color: rgba(0, 0, 0, 0.25);
  margin-right: -4px;
}
.arrowDivStyle {
  display: flex;
  flex-direction: row;
  align-items: center;
}
.itemAddIconStyle {
  position: absolute;
}
.itemSelectDivStyle {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
}
.arrowVerticalLineStyle {
  width: 1px;
  background-color: rgba(0, 0, 0, 0.25);
}

叭叭

难点一个主要在前期数据结构的梳理以及具体实现上,用递归将每个节点以及子节点的数据作为一个Group组,如下图。节点组 包括 当前节点+子节点,同层级为不同组

第二个比较麻烦的是由于纯写流程图,叶子节点间的箭头指向连接线需要处理。可以将一个节点拆分为 前一个节点的尾巴+当前节点含有箭头的连接线+平级其他节点含有箭头(若存在同级节点不含箭头)的连接线+竖向连接线(若存在同级节点),计算逻辑大概为94 * (下一级节点数量 - 1)

后来发现在实际添加节点的过程中,若叶子节点过多,会出现竖向连接线缺失(不够长)的情况,因为长度计算依赖下一级节点数量,无法通过后面的子节点的子节点等等数量做计算算出长度(也通过这种方式实现过,计算当前节点的最多层子节点数量……很奇怪的方式)
反思了一下,竖向连接线应该根据当前节点的Group组高度计算得出,连接线分组也应该重新调整,竖向连接线从单个节点的末端调整到group的开头,第一个节点只保留下半部分(为了占位,上半部分背景色调整为白色),最后一个节点只保留上半部分,中间的节点保留整个高度的连接线

最后展示上的结构是
tree :group根据树形数据结构递归展示
group :竖向连接线(多个同级节点)+ 节点本身Item + 当前节点子节点们
item:带箭头连接线+节点本身+不带箭头的下一级连接线

最终效果

到此这篇关于React 递归手写流程图展示树形数据的文章就介绍到这了,更多相关React 递归展示树形内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • React组件中使用JSON数据文件的方法详解

    React组件中使用JSON数据文件的方法详解

    要在 React 组件中使用 JSON 数据,有多种方法,这篇文章主要为大家详细介绍了五个常见的方法,文中的示例代码讲解详细,有需要的小伙伴可以了解下
    2024-01-01
  • 用React实现一个完整的TodoList的示例代码

    用React实现一个完整的TodoList的示例代码

    本篇文章主要介绍了用React实现一个完整的TodoList的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-10-10
  • React中useTransition钩子函数的使用详解

    React中useTransition钩子函数的使用详解

    React 18的推出标志着React并发特性的正式到来,其中useTransition钩子函数是一个重要的新增功能,下面我们就来学习一下useTransition钩子函数的具体使用吧
    2024-02-02
  • react中将html字符串渲染到页面的实现方式

    react中将html字符串渲染到页面的实现方式

    这篇文章主要介绍了react中将html字符串渲染到页面的实现方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • React 性能优化方法总结

    React 性能优化方法总结

    这篇文章主要介绍了React性能优化方法总结,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • react实现移动端二级路由嵌套详解

    react实现移动端二级路由嵌套详解

    这篇文章主要介绍了react移动端二级路由嵌套的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • React前端框架实现原理的理解

    React前端框架实现原理的理解

    React是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2022-07-07
  • React跨端动态化之从JS引擎到RN落地详解

    React跨端动态化之从JS引擎到RN落地详解

    这篇文章主要为大家介绍了React跨端动态化之从JS引擎到RN落地,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • 官方推荐react-navigation的具体使用详解

    官方推荐react-navigation的具体使用详解

    本篇文章主要介绍了官方推荐react-navigation的具体使用详解,react-navigation是致力于解决导航卡顿,数据传递,Tabbar和navigator布局,支持redux。非常具有实用价值,需要的朋友可以参考下
    2018-05-05
  • jenkins分环境部署vue/react项目的方法步骤

    jenkins分环境部署vue/react项目的方法步骤

    这篇文章主要介绍了jenkins分环境部署vue/react项目的方法,本文分步骤给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02

最新评论