React 递归手写流程图展示树形数据

news2024/11/28 4:39:38

需求

根据树的数据结构画出流程图展示,支持新增前一级、后一级、同级以及删除功能(便于标记节点,把节点数据当作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:带箭头连接线+节点本身+不带箭头的下一级连接线

最终效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1192569.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vite - 配置 - 不同的环境执行不同的配置文件

目标描述 通过不同的命令&#xff0c;执行不同的环境的配置文件中的内容&#xff1a; npm run dev : 执行开发环境的配置文件 npm run build: 执行生产环境的配置文件 环境文件准备 为了在不同的环境中使用不同的配置文件&#xff0c;我们将配置文件拆分开来。 开发环境使用开发…

【Python基础】基于UPD协议实现简易聊天室(Socket编程)

UDP通信 1.什么是 socket2. 创建 socket3.udp 网络程序-发送、接收数据&#xff08;User Datagram Protocol&#xff09;udp 网络程序-发送、接收数据&#xff08;客户端&#xff09;udp 绑定信息udp 绑定信息---服务器端总结 4.udp 聊天器 1.什么是 socket socket(简称 套接字…

如何快速编写测试用例?

当你学会了如何设计测试用例之后&#xff0c;接下来便是开始用例的编写。 在设计阶段&#xff0c;更准确的说应该是识别测试点的过程&#xff0c;而编写阶段则是将测试点细化成一条条测试用例的过程&#xff0c;有了比较全的用例场景后&#xff0c;如何让别人更舒服、更方便、…

Python + UnitTest 软件测试流程总结

以测试用户登录流程为例&#xff1a; TestCase&#xff1a; TestCase 主要用来编写测试用例&#xff0c;这里结合 断言&#xff08;assertEqual 和 assertIn&#xff09; 进行判断&#xff0c;避免了手动书写判断。 # tools.py # 登录验证方法 def login(username, password…

Android Studio导入,删除第三方库

Android项目经常用到无私的程序员们提供的第三方类库。本篇博客就是实现第三方库的导入和删除。 一、导入第三方库 1、将需要的库下载到本地&#xff1b; 2、新建Moudle (1)File --- New Moudle (2)选择Android Library --- Next (3)填写Moudle名 --- Finish。一个新的Mou…

【优选算法系列】【专题七分治】第一节.75. 颜色分类和912. 排序数组

文章目录 前言一、颜色分类 1.1 题目描述 1.2 题目解析 1.2.1 算法原理 1.2.2 代码编写二、排序数组 2.1 题目描述 2.2 题目解析 2.2.1 算法原理 2.2.2 代码编写总结 前言 一、颜色分类 1.1 题目描述 描述&…

QML9、输入元素

1、输入元素(Input Element) 我们已经使用过MouseArea(鼠标区域)作为鼠标输入元素。这里我们将更多的介绍关于键盘输入的一些东西。我们开始介绍文本编辑的元素:TextInput(文本输入)和TextEdit(文本编辑)。 2、文本输入(TextInput) 文本输入允许用户输入一行文本…

Redis之缓存

文章目录 前言一、缓存使用缓存的原因 二、使用缓存实现思路提出问题 三、三大缓存问题缓存穿透缓存雪崩缓存击穿互斥锁实现逻辑过期时间实现 总结 前言 本篇文章即将探索的问题&#xff08;以黑马点评为辅助讲解&#xff0c;大家主要体会实现逻辑&#xff09; 使用redis缓存的…

Nmap-NSE

一.Nmap的脚本引擎类别 参数说明ALL允许所有的脚本Auth认证Default默认的脚本引擎&#xff0c;-sC&#xff1a;equivalent to --script default 或 --script default &#xff0c;执行一些脚本的脚本扫描Discovery发现&#xff0c;获取目标的深度信息External扩展&#xff0c…

2023面试笔记四

1、gc导致的cpu冲高 排查是否为gc导致&#xff0c;看如下两点&#xff1a; gc频率和耗时 内存占用率 &#xff08;1&#xff09;gc频率和耗时有两种手段看&#xff1a; 第一种&#xff1a;根据gc日志的打印时间&#xff0c;可确定每次gc间隔的时间和耗时&#xff1a; 使用…

聚铭国产化日志合规版 → 中小企事业单位等保建设的最优解

聚铭网络最新发布聚铭综合日志分析系统国产化合规版本 &#xff0c;相较于同类型同档次非国产化设备性能无衰减、功能无裁减、成本不提高&#xff0c;适用于信创替换以及等保日志建设等应用场景。 面对日趋复杂的外部环境&#xff0c;近年来&#xff0c;国家越来越重视关键技术…

谭浩强【C语言程序设计】第一章习题详解

目录 1&#xff0c;什么是程序&#xff1f;什么是程序设计&#xff1f; 2&#xff0c;为什么需要计算机语言&#xff1f;高级语言有哪些特点&#xff1f; 3&#xff0c;正确理解以下名词及其含义&#xff1a; (1)源程序&#xff0c;目标程序&#xff0c;可执行程序。 (2)程…

免费小程序HTTPS证书

随着互联网的快速发展&#xff0c;小程序已经成为人们日常生活中不可或缺的一部分。然而&#xff0c;在小程序的开发和使用过程中&#xff0c;安全问题一直是开发者们关注的重点。其中&#xff0c;HTTPS 证书是保障小程序安全的重要工具之一。在这方面&#xff0c;免费的小程序…

【机器学习】正则化到底是什么?

先说结论&#xff1a;机器学习中的正则化主要解决模型过拟合问题。 如果模型出现了过拟合&#xff0c;一般会从两个方面去改善&#xff0c;一方面是训练数据&#xff0c;比如说增加训练数据量&#xff0c;另一方面则是从模型角度入手&#xff0c;比如&#xff0c;降低模型复杂…

HDR 成像技术学习(四)

HDR(High Dynamic Range,高动态范围)仿佛是成像领域永恒的话题,动态范围越大,图像能清晰呈现的明暗差别也就越大。与传统的SDR(标准动态范围)相比,HDR图像能够以更高质量同时显示画面的亮部和暗部。 随这些年CMOS图像传感器工艺技术进步,以及后端数字信号处理算力的提升…

编译内核源码

本文将记录内核源码编译步骤&#xff0c;供有需要的人参考使用。 一、内核源码下载网址 内核源码网址&#xff1a;https://kernel.org/ 二、准备编译环境 这里需要注意区分x86架构和arm架构&#xff0c;需要不同的架构内核就准备对应的服务器即可&#xff0c;在服务器上安装…

arthas常用命令

arthas常用命令 IDEA插件 arthas idea退出arthasjad 反编译watch 方法执行数据观测tracemonitor https://arthas.aliyun.com/doc/ IDEA插件 arthas idea 退出arthas # quit或者exit命令,只是退出当前的连接, Attach到目标进程上的arthas还会继续运行&#xff0c;端口会保持开…

火力全开!腾讯云这次直接开卖5年

如果你是一名网站管理员&#xff0c;或者是一名创业公司的CEO&#xff0c;那么腾讯云这个词一定不会陌生。作为国内领先的云计算服务提供商&#xff0c;腾讯云一直以来都在为各行各业的用户提供着高效、稳定、安全的云计算服务。 而在今天&#xff0c;我们要给大家带来一个重磅…

2020年五一杯数学建模B题基于系统性风险角度的基金资产配置策略分析解题全过程文档及程序

2020年五一杯数学建模 B题 基于系统性风险角度的基金资产配置策略分析 原题再现 近年来&#xff0c;随着改革开放程度的不断提高&#xff0c;我国经济运行中的各种风险逐渐暴露并集中传导和体现于金融领域。党的“十九大”报告提出“守住不发生系统性金融风险的底线”要求&am…

官媒代运营:让大众倾听品牌的声音

在当今数字时代&#xff0c;媒体的影响力和多样性远远超出了以往的范畴。品牌和企业越来越依赖媒体来传播信息、建立声誉以及与大众互动。而媒体矩阵成为了现代品牌传播的关键策略&#xff0c;使大众能够倾听品牌的声音。媒体矩阵&#xff1a;多元化的传播渠道 媒体矩阵是指利…