React 遞歸手寫流程圖展示樹形數(shù)據(jù)的操作方法
需求
根據(jù)樹的數(shù)據(jù)結(jié)構(gòu)畫出流程圖展示,支持新增前一級、后一級、同級以及刪除功能(便于標(biāo)記節(jié)點,把節(jié)點數(shù)據(jù)當(dāng)作label展示出來了,實際業(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 } ] } ] } ]
功能實現(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; } /** * 樹形流程圖 */ 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; // 同級工單數(shù)量 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[]; // 單項選項數(shù)據(jù) 放在select中 handleNode?: any; sameLevelCount?: number; // 同級工單數(shù)量 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 刪除工單圖標(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" /> // 可以不需要展示 寫的時候便于處理節(jié)點操作 {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' /** * 添加前置/后置/同級/刪除工單 * @param type 操作類型 * @param list 工單樹 * @param addCode 被添加的工單節(jié)點模版Code * @param item 操作節(jié)點 */ 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)) } // 處理層級關(guān)系 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); }
叭叭
難點一個主要在前期數(shù)據(jù)結(jié)構(gòu)的梳理以及具體實現(xiàn)上,用遞歸將每個節(jié)點以及子節(jié)點的數(shù)據(jù)作為一個Group組,如下圖。節(jié)點組 包括 當(dāng)前節(jié)點+子節(jié)點,同層級為不同組
第二個比較麻煩的是由于純寫流程圖,葉子節(jié)點間的箭頭指向連接線需要處理??梢詫⒁粋€節(jié)點拆分為 前一個節(jié)點的尾巴+當(dāng)前節(jié)點含有箭頭的連接線+平級其他節(jié)點含有箭頭(若存在同級節(jié)點不含箭頭)的連接線+豎向連接線(若存在同級節(jié)點)
,計算邏輯大概為94 * (下一級節(jié)點數(shù)量 - 1)
后來發(fā)現(xiàn)在實際添加節(jié)點的過程中,若葉子節(jié)點過多,會出現(xiàn)豎向連接線缺失(不夠長)的情況,因為長度計算依賴下一級節(jié)點數(shù)量,無法通過后面的子節(jié)點的子節(jié)點等等數(shù)量做計算算出長度(也通過這種方式實現(xiàn)過,計算當(dāng)前節(jié)點的最多層子節(jié)點數(shù)量……很奇怪的方式)
反思了一下,豎向連接線應(yīng)該根據(jù)當(dāng)前節(jié)點的Group組高度計算得出,連接線分組也應(yīng)該重新調(diào)整,豎向連接線從單個節(jié)點的末端調(diào)整到group的開頭,第一個節(jié)點只保留下半部分(為了占位,上半部分背景色調(diào)整為白色),最后一個節(jié)點只保留上半部分,中間的節(jié)點保留整個高度的連接線
最后展示上的結(jié)構(gòu)是
tree :group根據(jù)樹形數(shù)據(jù)結(jié)構(gòu)遞歸展示
group :豎向連接線(多個同級節(jié)點)+ 節(jié)點本身Item + 當(dāng)前節(jié)點子節(jié)點們
item:帶箭頭連接線+節(jié)點本身+不帶箭頭的下一級連接線
最終效果
到此這篇關(guān)于React 遞歸手寫流程圖展示樹形數(shù)據(jù)的文章就介紹到這了,更多相關(guān)React 遞歸展示樹形內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用react-activation實現(xiàn)keepAlive支持返回傳參
本文主要介紹了使用react-activation實現(xiàn)keepAlive支持返回傳參,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05React 項目中動態(tài)設(shè)置環(huán)境變量
本文主要介紹了React 項目中動態(tài)設(shè)置環(huán)境變量,本文將介紹兩種常用的方法,使用 dotenv 庫和通過命令行參數(shù)傳遞環(huán)境變量,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04圖文示例講解useState與useReducer性能區(qū)別
這篇文章主要為大家介紹了useState與useReducer性能區(qū)別圖文示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05React?Native中原生實現(xiàn)動態(tài)導(dǎo)入的示例詳解
在React?Native社區(qū)中,原生動態(tài)導(dǎo)入一直是期待已久的功能,在這篇文章中,我們將比較靜態(tài)和動態(tài)導(dǎo)入,學(xué)習(xí)如何原生地處理動態(tài)導(dǎo)入,以及有效實施的最佳實踐,希望對大家有所幫助2024-02-02