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 } = propsconst [activeKey, setActiveKey] = useState('TC20230404000001_1')const handleNode = (type = 'front' as NodeOperationTypes, item: NodeItemProps, index: number) => {switch (type) {case 'click' : {setActiveKey(`${item.ticketTemplateCode}_${item.priority}`)}; breakcase '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 (<NodeGroupkey={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 } = propsconst [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()}<NodeItemactive={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 nullconst 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 (<PlusCircleOutlinedclassName={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>) : nullreturn (<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}<SelectdefaultValue="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 = falseconst findNode = (child = [] as NodeItemProps[]) => {return child.map(k => {if (flag) return kif (type === 'front' && k.priority + 1 === item.priority && k.next && k.next?.findIndex(m => m.ticketTemplateCode === item.ticketTemplateCode) > -1) {flag = truereturn { ...k, next: [{ ticketTemplateCode: addCode, priority: item.priority, next: k.next }]}}if (type === 'next' && k.ticketTemplateCode === item.ticketTemplateCode) {flag = truereturn { ...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 = truereturn { ...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 = trueconsole.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.rhkb.cn/news/188977.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

链表经典OJ题(链表回文结构,链表带环,链表的深拷贝)

目录 前言 1.反转一个单链表。 2. 给定一个带有头结点 head 的非空单链表&#xff0c;返回链表的中间结点。 3.链表的回文结构。 4.链表带环问题&#xff08;*****&#xff09; 4.1是否带环 4.2 入环的节点 5.随机链表的复制&#xff08;链表的深拷贝&#xff09; 前言…

kubernetes (k8s)的使用

一、kubernetes 简介 谷歌2014年开源的管理工具项目&#xff0c;简化微服务的开发和部署。 提供功能&#xff1a;自愈和自动伸缩、调度和发布、调用链监控、配置管理、Metrics监控、日志监控、弹性和容错、API管理、服务安全等。官网&#xff1a;https://kubernetes.io/zh-cn…

KubeSphere v3.4.0 部署K8S Docker + Prometheus + grafana

KubeSphere v3.4.0 部署K8S 1、整体思路2、修改linux主机名3、 离线安装3.1 问题列表3.2 执行命令成功列表 1、整体思路 将KubeSphere v3.4.0 安装包传输到其中一台机器修改Linux主机名&#xff08;选取3台&#xff0c;修改为master01、master02、master03&#xff09;安装官方…

2023-11-12 LeetCode每日一题(Range 模块)

2023-03-29每日一题 一、题目编号 715. Range 模块二、题目链接 点击跳转到题目位置 三、题目描述 Range模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为 半开区间 的范围并查询它们。 半开区间 [left, right) 表示所有 left < x < right 的实数 x 。 实…

Java必刷入门递归题×5(内附详细递归解析图)

目录 1.求N的阶乘 2.求12...N的和 3.顺序打印数字的每一位 4.求数字的每一位之和 5.求斐波拉契数列 1.求N的阶乘 &#xff08;1&#xff09;解析题目意思 比如求5的阶乘&#xff0c;符号表示就是5&#xff01;&#xff1b;所以5&#xff01;5*4*3*2*1我们下面使用简单的…

Android中Toast与Snack

1. Toast : 使用Toast类的makeText()方法创建Toast对象&#xff0c;makeText()方法有两个参数&#xff0c;第一个参数为显示Tosat的上下文环境&#xff0c;第二个参数为显示时长&#xff08; Toast.LENGTH_LONG 或 Toast.LENGTH_SHORT &#xff09;。 使用Toast类的show()方法…

【ubuntu 快速熟悉】

ubuntu 快速熟悉 2.ubuntu桌面管理器3.ubuntu常见文件夹说明4.ubuntu任务管理器4.1 gnome桌面的任务管理器4.2 实时监控GPU4.3 top 命令 5.ubuntu必备命令5.1 .deb文件5.2 查找命令5.2.1 find文件搜索5.2.2 which查找可执行文件的路径5.2.3 which的进阶&#xff0c;whereis5.2.…

【Spring】SpringBoot配置文件

文章目录 1. 配置文件格式2. properties 配置⽂件说明2.1 properties 基本语法2.2 读取配置文件2.3 properties 缺点分析 3. yml 配置文件说明3.1 yml 基本语法3.2 读取配置文件3.3 PostConstruct 注解3.4 配置null和空格3.5 value值加单双引号3.6 配置对象3.7 配置集合3.8 配置…

环形链表解析(c语言)c语言版本!自我解析(看了必会)

目录 1.判断一个表是否是环形链表&#xff01; 代码如下 解析如下 2.快指针的步数和慢指针的步数有什么影响&#xff08;无图解析&#xff09; 3.怎么找到环形链表的入环点 代码如下 解析如下 1.判断一个表是否是环形链表&#xff01; 代码如下 bool hasCycle(struct L…

Leetcode—69.x的平方根【简单】

2023每日刷题&#xff08;二十七&#xff09; Leetcode—69.x的平方根 直接法实现代码 int mySqrt(int x) {long long i 0;while(i * i < x) {i;}if(i * i > x) {return i - 1;}return i; }运行结果 二分法实现代码 int mySqrt(int x) {long long left 0, right (l…

自动化测试(Java+eclipse)教程

webdriver环境配置 1.下载chromedriver到本地&#xff08;一定要选择和自己浏览器相对应的版本chromedriver下载地址&#xff09; 2.加入到环境变量path中 webdriver工作原理 创建web自动化测试脚本 1.Maven项目创建 File->New->project->(搜索maven)选择maven pr…

功能案例 -- 通过开关,改变白天和黑夜

效果展示 代码展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><style>:root {--default-bac-color: #f…

会员题-力扣408-有效单词缩写

有效单词缩写 字符串可以用 缩写 进行表示&#xff0c;缩写 的方法是将任意数量的 不相邻 的子字符串替换为相应子串的长度。例如&#xff0c;字符串 “substitution” 可以缩写为&#xff08;不止这几种方法&#xff09;&#xff1a; “s10n” (“s ubstitutio n”) “sub4…

基于STM32单片机抢答器设计

**单片机设计介绍&#xff0c; 基于STM32单片机抢答器设计-Proteus仿真 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于STM32单片机的抢答器设计可以用于教育和培训场景中的抢答游戏或考试环节。以下是一个基本的介绍设计步骤…

手摸手入门Springboot+Grafana10.2接收JSON

JSON&#xff08;JavaScript Object Notation, JS对象简谱&#xff09;是一种轻量级的数据交换格式。它基于 ECMAScript&#xff08;European Computer Manufacturers Association, 欧洲计算机协会制定的js规范&#xff09;的一个子集&#xff0c;采用完全独立于编程语言的文本…

2013年108计网

第33题 在 OSI 参考模型中, 下列功能需由应用层的相邻层实现的是()A. 对话管理B. 数据格式转换C. 路由选择D. 可靠数据传输 很显然&#xff0c;题目所问的应用层的相邻层是表示层。该层实现与数据表示相关的功能。选项a中的对话管理属于会话层。选项c中的路由选择属于网络层。…

Maven Profile组设置

application.properties中xxxx

统一消息分发中心设计

背景 我们核心业务中订单完成时&#xff0c;需要完成后续的连带业务&#xff0c;扣件库存库存、增加积分、通知商家等。 如下图的架构&#xff1a; 这样设计出来导致我们的核心业务和其他业务耦合&#xff0c;每次新增连带业务或者去掉连带业务都需要修改核心业务。 一方面&…

UPLAOD-LABS2

less7 任务 拿到一个shell服务器 提示 禁止上传所有可以解析的后缀 发现所有可以解析的后缀都被禁了 查看一下源代码 $is_upload false; $msg null; if (isset($_POST[submit])) {if (file_exists($UPLOAD_ADDR)) {$deny_ext array(".php",".php5&quo…

网络安全深入学习第八课——反向代理(工具:frp)

文章目录 一、实验环境二、实验要求三、开始模拟1、攻击机配置frp文件2、攻击拿下跳板机&#xff0c;并且上传frpc.ini、frpc.exe、frpc_full.ini文件3、把frps.ini、、frps.exe、frps_full.ini文件放到VPS主机上4、VPS机开启frp5、跳板机开启frp6、验证 一、实验环境 攻击机&…