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(…

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

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

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 背景与动机 文章发…

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 …

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;并将整体振动值作为电流/电压信号传输。它测量加速度、速度和位移等不同参数的振动。配置…

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

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

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

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

稳定扩散AI 纹理生成器

推荐基于稳定扩散(stable diffusion) AI 模型开发的自动纹理工具&#xff1a; DreamTexture.js自动纹理化开发包 - NSDT 什么是稳定扩散&#xff1f; 从技术上讲&#xff0c;Stable Diffusion 是一种用于机器学习的潜在扩散模型 &#xff08;LDM&#xff09;。这种类型的专用深…

【dbeaver】添加mysql高低版本选择驱动

添加mysql高低版本选择驱动 连接到数据库->全部->查询mysql MySQL 版本驱动 8.0 MySQL 5 版本驱动 5.7.x 其他需要就&#xff1a;https://downloads.mysql.com/archives/c-j/ 密码查看 项目设置密码&#xff1a; File -> Project security ->设置密码 It i…

Ubuntu 22.04 安装水星无线 USB 网卡

我的 USB 网卡是水星 Mercury 的&#xff0c; 在 Ubuntu 22.04 下面没有自动识别。 没有无线网卡的时候只能用有线接到路由器上&#xff0c;非常不方便。 寻思着把无线网卡驱动装好。折腾了几个小时装好了驱动。 1.检查网卡类型 & 安装驱动 使用 lsusb 看到的不一定是准确…

法治智能起航 | 拓世法宝AI智慧政务一体机重塑法治格局,开启智能司法新篇章

在科技的巨轮推动下&#xff0c;我们的社会正快速迈向一个以数据和智能为核心的新时代。在这个波澜壮阔的变革中&#xff0c;人工智能&#xff08;AI&#xff09;显得尤为突出&#xff0c;它不仅是科技进步的象征&#xff0c;更是未来发展的助力者。 2023年&#xff0c;最高人…

医学影像系统源码(MRI、CT三维重建)

一、MRI概述 核磁共振成像&#xff08;英语&#xff1a;Nuclear Magnetic Resonance Imaging&#xff0c;简称NMRI&#xff09;&#xff0c;又称自旋成像&#xff08;英语&#xff1a;spin imaging&#xff09;&#xff0c;也称磁共振成像&#xff08;Magnetic Resonance Imag…

Labview利用声卡捕获波形

一般的计算机上自带的声卡&#xff0c;均既有A/D功能&#xff0c;又有D/A功能&#xff0c;就是一款具备基本配置的数据采集卡&#xff0c;并且技术成熟&#xff0c;性能稳定。 后台如下&#xff1a;

【Word自定义配置,超简单,图文并茂】自定义Word中的默认配置,比如标题大小与颜色(参考科研作图配色),正文字体等

▚ 01 自定义样式Styles中的默认标题模板 &#x1f4e2;自定义标题的显示效果&#xff0c;如下图所示&#xff1a; 1.1 自定义标题的模板Normal.dotm 1.1.1 选择所需修改的标题 新建一个空白Word文档&#xff0c;依次选择菜单栏的开始Home&#xff0c;样式Styles&#xff0c;…

光刻掩膜版怎么制作的?

光掩膜版基本上是 IC 设计的“主模板”。掩模版有不同的尺寸。常见尺寸为 6 x 6 英寸一般的掩膜版由石英或玻璃基板组成。光掩膜版涂有不透明薄膜。更复杂的掩模版使用其他材料。 一般来说&#xff0c;术语“photo mask”用于描述与 1X 步进机或光刻系统一起使用的“主模板”。…

Ubuntu 安装常见问题

1. 安装oh my zsh 搜狗输入法不能用 vim /etc/environmentexport XIM_PROGRAMfcitx export XIMfcitx export GTK_IM_MODULEfcitx export QT_IM_MODULEfcitx export XMODIFIERS“imfcitx” export LANG“zh_CN.UTF-8”配置完后重启&#xff0c;稍等一会&#xff0c;右上角会有个…