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.mzph.cn/news/138184.shtml

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

相关文章

uniapp vue2 vuex 持久化

1.vuex的使用 一、uniapp中有自带vuex插件&#xff0c;直接引用即可 二、在项目中新建文件夹store,在main.js中导入 在根目录下新建文件夹store,在此目录下新建index.js文件 index.js import Vue from vueimport Vuex from vuexVue.use(Vuex)const store new Vuex.Store(…

core-site.xml,yarn-site.xml,hdfs-site.xml,mapred-site.xml配置

core-site.xml <?xml version"1.0" encoding"UTF-8"?> <?xml-stylesheet type"text/xsl" href"configuration.xsl"?> <!--Licensed under the Apache License, Version 2.0 (the "License");you may no…

微服务如何做负载均衡?

笔者在参与联通某子公司时&#xff0c;遇到了这样一个问题。感觉比较实际&#xff0c;特来记录一波。 先看腾讯混元的解答&#xff1a; 微服务架构中&#xff0c;负载均衡是必不可少的。在微服务中&#xff0c;负载均衡可以通过以下几种方式来实现&#xff1a; 1. DNS轮询&am…

【objectarx.net】读写扩展字典

在objectarx.net中操作扩展字典

代码随想录图论部分-695. 岛屿的最大面积|1020. 飞地的数量

695. 岛屿的最大面积 题目&#xff1a;给你一个大小为 m x n 的二进制矩阵 grid 。岛屿 是由一些相邻的 1 (代表土地) 构成的组合&#xff0c;这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0&#xff08;代表水&#xff0…

flutter实现上拉加载下拉刷新

效果如下&#xff1a; flutter实现上拉加载下拉刷新 使用到的库是easy_refresh 安装请查看官网 接口用的是提供的接口https://reqres.in/ 请求接口用到的库是dio 下面主要是介绍如何使用easy_refresh实现上拉加载数据&#xff0c;详细学习其它例子请查看easy_refresh main…

electron 内部api capturePage实现webview截图

官方文档 .capturePage([rect]) rect Rectangle (可选) - 要捕获的页面区域。 返回 Promise - 完成后返回一个NativeImage 在 rect内捕获页面的快照。 省略 rect 将捕获整个可见页面。 async function cap(){ let image await webviewRef.value.capturePage() console.log(im…

Postman的环境变量和全局变量

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 多种环境&#xff1a;开发环境、测试环境、预发布环境、生产环境&#xff0c;可以用环境变量来解决。 今天的分享就到这里&a…

【论文阅读】Progressive Spatio-Temporal Prototype Matching for Text-Video Retrieval

资料链接 论文链接&#xff1a;https://openaccess.thecvf.com/content/ICCV2023/papers/Li_Progressive_Spatio-Temporal_Prototype_Matching_for_Text-Video_Retrieval_ICCV_2023_paper.pdf 代码链接&#xff1a;https://github.com/imccretrieval/prost 背景与动机 文章发…

【微软技术栈】C#.NET 依赖项注入

本文内容 多个构造函数发现规则使用扩展方法注册服务组框架提供的服务服务生存期服务注册方法作用域验证范围场景 .NET 支持依赖关系注入 (DI) 软件设计模式&#xff0c;这是一种在类及其依赖项之间实现控制反转 (IoC) 的技术。 .NET 中的依赖关系注入是框架的内置部分&#…

LabVIEW在OPC中使用基金会现场总线

LabVIEW在OPC中使用基金会现场总线 本文讨论了如何使用开放的OPC&#xff08;用于过程控制的OLE&#xff09;接口访问基金会现场总线网络和设备。 NI-FBUS通信管理器随附了一个OPC数据访问服务器。 &#xff08;NI-FBUS Configurator自动包含NI-FBUS通信管理器。&#xff09…

Visual Studio2010保姆式安装教程(VS2010 旗舰版),以及如何运行第一个C语言程序,超详细

安装前请关闭杀毒软件&#xff0c;系统防火墙&#xff0c;断开网络连接 参考链接&#xff1a;请点击 下载链接&#xff1a; 通过百度网盘分享的文件&#xff1a;VS2010.zip 链接:https://pan.baidu.com/s/1yQUUCxMJP7FMaistFX94SQ 提取码:96ga 复制这段内容打开「百度网盘APP …

C++ 数组学习资料

C 数组学习资料 目录 什么是数组&#xff1f;声明和初始化数组访问数组元素多维数组数组和指针常见的数组操作数组的限制和注意事项 什么是数组&#xff1f; 在 C 中&#xff0c;数组是一种用于存储相同类型元素的数据结构。它是一个固定大小的连续内存块&#xff0c;每个元…

Linux下的调试工具——GDB

GDB 1.什么是GDB GDB 是由 GNU 软件系统社区提供的调试工具&#xff0c;同 GCC 配套组成了一套完整的开发环境&#xff0c;GDB 是 Linux 和许多 类Unix系统的标准开发环境。 一般来说&#xff0c;GDB 主要能够提供以下四个方面的帮助&#xff1a; 启动程序&#xff0c;可以按…

GF0-57CQD-002 测量参数:加速度、速度、位移–现场可配置

GF0-57CQD-002 测量参数:加速度、速度、位移–现场可配置 GF0-57CQD-002 是一款创新的双通道变送器&#xff0c;专为精确的振动测量而设计。它激励并读取来自加速度计的信号&#xff0c;并将整体振动值作为电流/电压信号传输。它测量加速度、速度和位移等不同参数的振动。配置…

vue3使用vuex的示例(模块化功能)

目录 1. store/index.ts 2. main.ts 3. App.vue调用 4. 如果删除moduleA的namespaced属性, 保留moduleB的namespaced:true 5. 则App.vue修改为: 1. store/index.ts 注意: 需要使用时带上模块名称的namespaced必须为true, 不写或者为false时调用时不需要写模块名称(获取st…

模电学习路径--google镜像chatgpt

交流通路实质 列出电路方程1&#xff0c;方程1对时刻t做微分 所得方程1‘ 即为 交流通路 方程1对时刻t做微分&#xff1a;两个不同时刻的方程1相减&#xff0c;并 令两时刻差为 无穷小 微分 改成 差 模电学习路径&#xff1a; 理论 《电路原理》清华大学 于歆杰 朱桂萍 陆文…

【数据结构】二叉树的遍历递归算法详解

二叉树的遍历 &#x1f4ab;二叉树的结点结构定义&#x1f4ab;创建一个二叉树结点&#x1f4ab;在主函数中手动创建一颗二叉树&#x1f4ab;二叉树的前序遍历&#x1f4ab;调用栈递归——实现前序遍历&#x1f4ab;递归实现中序和后序遍历 &#x1f4ab;二叉树的结点结构定义 …

看电影相关的日语,柯桥日语培训

1&#xff0e;映画を見たいです。 我想看电影。 2&#xff0e;どこの映画館&#xff08;えいがかん&#xff09;で上映&#xff08;上映&#xff09;しますか。 在哪里的影院上映&#xff1f; 3&#xff0e;どこで映画が見られますか。 哪里能看电影呢&#xff1f; 4&#x…