React 遞歸手寫流程圖展示樹(shù)形數(shù)據(jù)的操作方法
需求
根據(jù)樹(shù)的數(shù)據(jù)結(jié)構(gòu)畫出流程圖展示,支持新增前一級(jí)、后一級(jí)、同級(jí)以及刪除功能(便于標(biāo)記節(jié)點(diǎn),把節(jié)點(diǎn)數(shù)據(jù)當(dāng)作label展示出來(lái)了,實(shí)際業(yè)務(wù)中跟據(jù)情況處理)

文件結(jié)構(gòu)

初始數(shù)據(jù)
[
{
"ticketTemplateCode": "TC20230404000001",
"priority": 1,
"next": [
{
"ticketTemplateCode": "TC20230705000001",
"priority": 2,
"next": [
{
"ticketTemplateCode": "TC20230707000001",
"priority": 3
},
{
"ticketTemplateCode": "TC20230404000002",
"priority": 3
}
]
}
]
}
]功能實(shí)現(xiàn) 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;
}
/**
* 樹(shù)形流程圖
*/
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)
// 添加前置工單時(shí)需要處理選中項(xiàng)
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; // 同級(jí)工單數(shù)量
front?: boolean; // 是否有前置工單
next?: boolean; // 是否有后置工單
children?: any;
item?: NodeItemProps;
}
/**
* 流程圖-同層級(jí)組
*/
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[]; // 單項(xiàng)選項(xiàng)數(shù)據(jù) 放在select中
handleNode?: any;
sameLevelCount?: number; // 同級(jí)工單數(shù)量
front?: boolean; // 是否有前置工單
next?: boolean; // 是否有后置工單
same?: boolean; // 是否有同級(jí)工單
item?: NodeItemProps;
}
/**
* 流程圖-單項(xiàng)
*/
export default memo<IProps>(props => {
const {
index,
active,
options = [],
handleNode,
front = false,
next = false,
item,
} = props
// 添加 or 刪除工單圖標(biāo)
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" />
// 可以不需要展示 寫的時(shí)候便于處理節(jié)點(diǎn)操作
{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={[ // 應(yīng)該為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'
/**
* 添加前置/后置/同級(jí)/刪除工單
* @param type 操作類型
* @param list 工單樹(shù)
* @param addCode 被添加的工單節(jié)點(diǎn)模版Code
* @param item 操作節(jié)點(diǎn)
*/
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))
}
// 處理層級(jí)關(guān)系
export const handleNodePriority = (list = [] as NodeItemProps[], priority = 1) => { // priority 層級(jí)
return list.map((k: NodeItemProps) => ({ ...k, priority, next: handleNodePriority(k.next, priority + 1) }))
}
// 得到最大層級(jí) 即工單樹(shù)的深度
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: '增加同級(jí)工單',
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);
}叭叭
難點(diǎn)一個(gè)主要在前期數(shù)據(jù)結(jié)構(gòu)的梳理以及具體實(shí)現(xiàn)上,用遞歸將每個(gè)節(jié)點(diǎn)以及子節(jié)點(diǎn)的數(shù)據(jù)作為一個(gè)Group組,如下圖。節(jié)點(diǎn)組 包括 當(dāng)前節(jié)點(diǎn)+子節(jié)點(diǎn),同層級(jí)為不同組

第二個(gè)比較麻煩的是由于純寫流程圖,葉子節(jié)點(diǎn)間的箭頭指向連接線需要處理??梢詫⒁粋€(gè)節(jié)點(diǎn)拆分為 前一個(gè)節(jié)點(diǎn)的尾巴+當(dāng)前節(jié)點(diǎn)含有箭頭的連接線+平級(jí)其他節(jié)點(diǎn)含有箭頭(若存在同級(jí)節(jié)點(diǎn)不含箭頭)的連接線+豎向連接線(若存在同級(jí)節(jié)點(diǎn)),計(jì)算邏輯大概為94 * (下一級(jí)節(jié)點(diǎn)數(shù)量 - 1)

后來(lái)發(fā)現(xiàn)在實(shí)際添加節(jié)點(diǎn)的過(guò)程中,若葉子節(jié)點(diǎn)過(guò)多,會(huì)出現(xiàn)豎向連接線缺失(不夠長(zhǎng))的情況,因?yàn)殚L(zhǎng)度計(jì)算依賴下一級(jí)節(jié)點(diǎn)數(shù)量,無(wú)法通過(guò)后面的子節(jié)點(diǎn)的子節(jié)點(diǎn)等等數(shù)量做計(jì)算算出長(zhǎng)度(也通過(guò)這種方式實(shí)現(xiàn)過(guò),計(jì)算當(dāng)前節(jié)點(diǎn)的最多層子節(jié)點(diǎn)數(shù)量……很奇怪的方式)
反思了一下,豎向連接線應(yīng)該根據(jù)當(dāng)前節(jié)點(diǎn)的Group組高度計(jì)算得出,連接線分組也應(yīng)該重新調(diào)整,豎向連接線從單個(gè)節(jié)點(diǎn)的末端調(diào)整到group的開(kāi)頭,第一個(gè)節(jié)點(diǎn)只保留下半部分(為了占位,上半部分背景色調(diào)整為白色),最后一個(gè)節(jié)點(diǎn)只保留上半部分,中間的節(jié)點(diǎn)保留整個(gè)高度的連接線

最后展示上的結(jié)構(gòu)是
tree :group根據(jù)樹(shù)形數(shù)據(jù)結(jié)構(gòu)遞歸展示
group :豎向連接線(多個(gè)同級(jí)節(jié)點(diǎn))+ 節(jié)點(diǎn)本身Item + 當(dāng)前節(jié)點(diǎn)子節(jié)點(diǎn)們
item:帶箭頭連接線+節(jié)點(diǎn)本身+不帶箭頭的下一級(jí)連接線
最終效果

到此這篇關(guān)于React 遞歸手寫流程圖展示樹(shù)形數(shù)據(jù)的文章就介紹到這了,更多相關(guān)React 遞歸展示樹(shù)形內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用react-activation實(shí)現(xiàn)keepAlive支持返回傳參
本文主要介紹了使用react-activation實(shí)現(xiàn)keepAlive支持返回傳參,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
React 項(xiàng)目中動(dòng)態(tài)設(shè)置環(huán)境變量
本文主要介紹了React 項(xiàng)目中動(dòng)態(tài)設(shè)置環(huán)境變量,本文將介紹兩種常用的方法,使用 dotenv 庫(kù)和通過(guò)命令行參數(shù)傳遞環(huán)境變量,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
圖文示例講解useState與useReducer性能區(qū)別
這篇文章主要為大家介紹了useState與useReducer性能區(qū)別圖文示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
React?Native中原生實(shí)現(xiàn)動(dòng)態(tài)導(dǎo)入的示例詳解
在React?Native社區(qū)中,原生動(dòng)態(tài)導(dǎo)入一直是期待已久的功能,在這篇文章中,我們將比較靜態(tài)和動(dòng)態(tài)導(dǎo)入,學(xué)習(xí)如何原生地處理動(dòng)態(tài)導(dǎo)入,以及有效實(shí)施的最佳實(shí)踐,希望對(duì)大家有所幫助2024-02-02
為什么說(shuō)form元素是React的未來(lái)
這篇文章主要介紹了為什么說(shuō)form元素是React的未來(lái),本文會(huì)帶你聊聊React圍繞form的布局與發(fā)展,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06

