react 使用div写流程图

代码中使用到的组件和icon 是arco.design ,项目中遇到一个这样的需求,我使用了gojs实现了这个交互,但是看到有人使用div自己写交互还是值得借鉴,也值得收藏以方便自己以后学习、巩固

原始数据

export default [
	{ id: '1', parentId: null, name: '叶子节点', remark: '' },
	{ id: '1_1', parentId: '1', name: '一级1', remark: '' },
	{ id: '1_1_1', parentId: '1_1', name: '三级1', remark: '' },
	{ id: '1_1_2', parentId: '1_1', name: '三级2', remark: '' },
	{ id: '1_2', parentId: '1', name: '一级2', remark: '' },
	{ id: '1_2_1', parentId: '1_2', name: '三级3', remark: '' },
	{ id: '1_2_2', parentId: '1_2', name: '三级4', remark: '' },
]

父组件中使用时,调用组件,里面有多余编辑,删除,查看,新增弹窗的代码就没有贴出来了

const getDefaultNode = () => {
    return {
        name: '',
        remark: '',
        caliber: '',
        unit: '',
        parentId: ''
    }
}
class Test extends React.Component<any, State> {
    constructor(props: any, context: any) {
        super(props);
        this.state = {
            loading: true,
            loadingTip: '数据加载中...',
            drawerLoading: false,
            drawerLoadingTip: '',
            treeMap:{},
            expandMap: {},
            editType: null,
            drawerVisible: false,
            drawerTitle: '',
            currentNode: null,
            modelId: null,
            initialValues: {}
        };
        this.baseInfoFormRef = React.createRef();
        this.formRef = React.createRef();
        this.seniorRef = React.createRef();
    }
    
    getNodeMap = () => {
       
    }
    addNode = (node) => {
        const n = getDefaultNode()
        this.setState(() => {
            return {
                currentParentNode: node,
                currentNode: n,
                drawerTitle: '新增',
                drawerVisible: true,
                editType: 'add',
                initialValues: n
            }
        }, () => {
            this.formRef?.current?.clearFields();
            this.formRef?.current?.setFieldsValue(n);
        })
    }
    editNode = (node) => {
        let dataUsage = [];
        if (node.dataUsage && typeof node.dataUsage === 'string') {
            dataUsage = node.dataUsage.split(',').filter(val => {
                return this.state.dataUsageLeafRelaMap[val];
            }).map(val => {
                return this.state.dataUsageLeafRelaMap[val]
            })
        }
        let n = {...node, dataUsage};
        this.setState(() => {
            return {
                modelId: node.id,
                currentNode: node,
                drawerTitle: '编辑',
                drawerVisible: true,
                editType: 'edit',
                initialValues: {...n}
            }
        }, () => {
            this.formRef?.current?.clearFields();

            this.formRef?.current?.setFieldsValue({...n});
        })
    }
    viewNode = (node) => {
        console.log('node',node)
    }
    deleteNode = (node) => {
        const {
            treeMap
        } = this.state;
        this.setState({
            loading: true,
            loadingTip: '删除中...'
        })
        deleteNode(node.id).then(res => {
            if (res.data && res.data.success) {
                const children = treeMap[node.parentId].children;
                const index = children.indexOf(node.id)
                children.splice(index, 1);
                treeMap[node.parentId].children = children;
                delete treeMap[node.id];

                this.setState({
                    loading: false,
                    treeMap
                })
                Message.success('删除成功!');
                return;
            }
            throw 'err';
        }).catch(err => {
            Message.success('删除失败!');
        })
    }
    handleSaveBaseInfo = () => {
        const {
            treeMap,
            editType,
            currentNode,
            currentParentNode
        } = this.state;

        if (editType === 'add') {
            this.setState({
                drawerLoading: true,
                drawerLoadingTip: '新建中...',
            });
            
            let node = {
                ...getDefaultNode(),
                ...this.baseInfoFormRef.current.getValue(),
                parentId: currentParentNode.id,
                modelId: this.props.rootId
            }
            node.dataUsage = Array.isArray(node.dataUsage) ? node.dataUsage.map(arr => arr[arr.length - 1]).join(',') : '';

            createNode(node).then(res => {
                if (res.data && res.data.success) {
                    const n = res.data.data;
                    n.children = [];
                    treeMap[n.id] = n;
                    treeMap[n.parentId].children.push(n.id);
                    this.setState({
                        drawerLoading: false,
                        drawerVisible: false,
                        treeMap
                    })

                    Message.success('新建成功!');
                    return;
                }
                throw 'err';
            }).catch(err => {
                Message.success('新建失败!');
                this.setState({
                    drawerLoading: false
                })
            })
        } else if (editType === 'edit') {
            const node = {
                ...currentNode,
                ...this.baseInfoFormRef.current.getValue()
            };
            updateNodeInfo(node).then(res => {
                if (res.data && res.data.success) {
                    treeMap[node.id] = node;
                    this.setState({
                        drawerLoading: false,
                        drawerVisible: false,
                        treeMap
                    })

                    Message.success('更新成功!');
                    return;
                }
                throw 'err';
            }).catch(err => {
                Message.success('更新失败!');
                this.setState({
                    drawerLoading: false
                })
            })
        }
        
    }
    handleSave = async () => {
        const valid = await this.baseInfoFormRef.current.validate();
        if (!valid) {
            return;
        }
        const {
            modelId,
            editType
        } = this.state;
        this.setState({
            drawerLoading: true,
            drawerLoadingTip: '更新中...',
        });
        let v = true;
        if (modelId === '320885092937818118' && editType === 'edit') {
            await updateCPI(this.seniorRef.current.getValue()).then(res => {
                if (res?.data?.success) {
                    return;
                }
                throw 'err';
            }).catch(err => {
                v = false;
                Message.success('更新失败!');
                this.setState({
                    drawerLoading: false
                })
            })
        }
        if (modelId === '320885092937818115' && editType === 'edit') {
            await updateGI(this.seniorRef.current.getValue()).then(res => {
                if (res?.data?.success) {
                    return;
                }
                throw 'err';
            }).catch(err => {
                v = false;
                Message.success('更新失败!');
                this.setState({
                    drawerLoading: false
                })
            })
        }
        if (v) {
            this.handleSaveBaseInfo();
        }

        return;
       
    }
    componentDidUpdate(prevProps, prevState) {
        if (prevProps.rootId !== this.props.rootId && this.props.rootId) {
            this.setState(() => {
                return {
                    loading: false,
                    treeMap: this.props.treeMap
                }
            }, () => {
            })
            
        }
    }
    componentDidMount() {
        console.log('this.props.',this.props)
        let { mockTreeData ,rootId}=this.props;
		let treeMap_ = {};
		mockTreeData.forEach(n => {
			if (!n.parentId) {
				rootId = n.id;
			}
			treeMap_[n.id] = {
				...n,
				children: []
			}
		});
		mockTreeData.forEach(n => {
			if (n.parentId && treeMap_[n.parentId]) {
				treeMap_[n.parentId].children.push(n.id);
			}
		})
        this.setState({
            treeMap:treeMap_,
            rootId:rootId,
        })
    }
    render() {
        const {
            loading,
            loadingTip,
            drawerLoading,
            drawerLoadingTip,
            treeMap,
            expandMap,
            currentNode,
            drawerTitle,
            drawerVisible,
            editType,
            initialValues,
            modelId
        } = this.state;
        const {
            rootId
        } = this.props;
        return ( <div
                    className="valuation-model-diagram">
                    {rootId && Object.keys(treeMap).length > 0 && (
                        <DiagramNode
                            node={treeMap[rootId]}
                            lvl={0}
                            hasNext={false}
                            lines={[]}
                            diagram={this}
                            hidden={expandMap[rootId] === false}
                            />
                    )}
                  
                </div>
           
        )
    }
}

export default connect(state => ({
	rootId: '1',
    mockTreeData:[
		{ id: '1', parentId: null, name: '叶子节点', remark: '' },
		{ id: '1_1', parentId: '1', name: '一级1', remark: '' },
		{ id: '1_1_1', parentId: '1_1', name: '三级1', remark: '' },
		{ id: '1_1_2', parentId: '1_1', name: '三级2', remark: '' },
		{ id: '1_2', parentId: '1', name: '一级2', remark: '' },
		{ id: '1_2_1', parentId: '1_2', name: '三级3', remark: '' },
		{ id: '1_2_2', parentId: '1_2', name: '三级4', remark: '' },
	],
  }))(Test);

定义组件DiagramNode.tsx

import React, { useEffect } from 'react';
import {  Popconfirm,  } from '@arco-design/web-react';
import { IconEye, IconDelete, IconAddObject, } from 'icon';//自定义库的icon
import modelPng from './imgs/model.png';//自定义库的png
import './style/index.scoped.less';

class DiagramNode extends React.Component<any, State> {
    constructor(props: any, context: any) {
        super(props);
        this.state = {
        };
    }
    render() {
        const {node, lvl, hasNext, lines, diagram, hidden} = this.props;
        const expanded = !(diagram.state.expandMap[node.id] === false);
        const canDelete = diagram.props.rootId !== node.id && node.children.length === 0 && node.canDel !== 0;

        let realCanDelete = canDelete;
        let deleteTip = '您确认要删除?';
        let nodeObj = diagram.state.treeMap[node.id];
        if (nodeObj.children.length > 0) {
            deleteTip = '请先删除!';
            realCanDelete = false;
        }
        if (!nodeObj.parentId) {
            deleteTip = '不允许删除!';
            realCanDelete = false;
        }
        const Node = (
            <>
            <div
                className={[
                    "tree-node",
                    `lvl-${lvl}`,
                    hidden ? 'hidden' : '',
                    Array.isArray(node.children) && node.children.length > 0 ? 'has-children' : ''
                ].join(' ')}>
                {lvl > 1 && <div
                    className="indent-block-group">
                    {Array(lvl - 1).fill(null).map((item, index) => {
                        return (
                            <div
                                className={[
                                    "indent-block",
                                    (lines[index]  === '1' || index === lvl - 2) ? 'pre-line' : ''
                                ].join(" ")}>
                            </div>
                        )
                    })}
                </div>}
                <div
                    className={[
                        "cost-model-card",
                        lvl === 0 ? 'root' : '',
                    ].join(' ')}>
                    <div className="title-container">
                        {lvl === 0 && <div
                            className="icon">
                            <img src={modelPng}/>
                        </div>}
                        <div
                            className="text">
                            {node.name}
                        </div>
                    </div>
                    <div className="oper-group-container">
                        <div className="oper-group-content">
                            <div
                                className="oper-left">
                                <IconEye
                                    onClick={() => {
                                        diagram.viewNode(node);
                                    }}/>
                                <IconEye
                                    onClick={() => {
                                        diagram.editNode(node);
                                    }}/>
                                {canDelete && (
                                    <Popconfirm
                                        focusLock
                                        title={deleteTip}
                                        onOk={() => {
                                            if (realCanDelete) {
                                                diagram.deleteNode(node);
                                            }
                                        }}
                                        onCancel={() => {
                                        }}>
                                        <IconDelete
                                            style={{
                                                color: 'var(--color-red-6)'
                                            }}/>
                                    </Popconfirm>
                                )}
                            </div>
                            <div
                                className="split">
                            </div>
                            <div
                                className="oper-right">
                                <IconAddObject
                                    style={{
                                        color: 'var(--color-primary-6)'
                                    }}
                                    onClick={() => {
                                        diagram.addNode(node);
                                    }}/>
                            </div>
                        </div>
                    </div>
                    {Array.isArray(node.children) && node.children.length > 0 && (
                        <div
                            className="expand-btn"
                            onClick={() => {
                                let expandObj = {};
                                expandObj[node.id] = !expanded;
                                diagram.setState({
                                    expandMap: {
                                        ...diagram.state.expandMap,
                                        ...expandObj
                                    }
                                })
                            }}>
                            {expanded ? (
                                <svg xmlns="http://www.w3.org/2000/svg" width="7" height="7" viewBox="0 0 7 7" fill="none">
                                    <path d="M0 3.5 L 7 3.5" stroke-width="1.5" stroke="#4D5E7D"/>
                                </svg>
                            ) : (
                                <svg xmlns="http://www.w3.org/2000/svg" width="7" height="7" viewBox="0 0 7 7" fill="none">
                                    <path d="M3.5 0 L 3.5 7" stroke-width="1.5" stroke="#4D5E7D"/>
                                    <path d="M0 3.5 L 7 3.5" stroke-width="1.5" stroke="#4D5E7D"/>
                                </svg>
                            )}
                        </div>
                    )}
                </div>
            </div>
            </>
        )
        const NodeChildren = (
            <>
                {Array.isArray(node.children) && node.children.map((id, index) => {
                    if (lvl > 1) {
                        lines.push(hasNext ? '1' : '0')
                    }
                    const n = diagram.state.treeMap[id]
                    return (
                        <DiagramNode
                            node={n}
                            lvl={lvl + 1}
                            hasNext={index < node.children.length - 1}
                            lines={[...lines]}
                            diagram={diagram}
                            hidden={!expanded || hidden}
                            />
                    )
                })}
            </>
        );
        if (lvl > 1) {
            return (
                <>
                {Node}
                {NodeChildren}
                </>
            );
        }

        let left = false;
        let right = false;
        let center = false;
        if (this.props.lvl === 1) {
            const children = this.props.diagram.state.treeMap[this.props.diagram.props.rootId].children;
            const index = children.indexOf(this.props.node.id);
            let centerIndex = Math.floor(children.length / 2);
            if (children.length % 2) {
                centerIndex += 1;
                if (index + 1 < centerIndex) {
                    left = true
                }
                if (index + 1 == centerIndex) {
                    center = true
                }
                if (index + 1 > centerIndex) {
                    right = true
                }
            } else {
                if (index + 1 <= centerIndex) {
                    left = true
                }
                if (index + 1 > centerIndex) {
                    right = true
                }
            }
        }
        
        return (
            <div
                className={`node-group-container ${left ? 'left' : ''} ${right ? 'right' : ''} ${center ? 'center' : ''}`}>
                {Node}
                <div
                    className={[
                        "tree-node-children",
                        `lvl-${lvl + 1}-group`,
                        !expanded || hidden ? 'hidden' : ''
                    ].join(' ')}>
                    {NodeChildren}
                </div>
            </div>
        )
    }
}

export default DiagramNode;

组件样式

/deep/.arco-spin {
 	width: 100%;
	height: 100%;
	.arco-spin-children {
	    width: 100%;
		height: 100%;
	}
	.arco-spin-children::after {
		z-index: 2;
	}
}
			
.valuation-model-diagram {
	width: 100%;
	height: 100%;
	padding: 30px 16px 10px 16px;
	overflow: auto;
	box-sizing: border-box;

	&::-webkit-scrollbar {
	    width: 0px;
	    height: 10px;
	}
	* {
		box-sizing: border-box;
	}
	.node-group-container {
		width: max-content;
		margin: 0px auto;
	}
	.tree-node {
		display: flex;
		transition: all .2s;
		&.lvl-0 {
			display: block;
			width: max-content;
    		margin: 0px auto;
			&>.cost-model-card {
				margin: 0px auto 20px;
			    display: block;
			    width: max-content;
			    margin-bottom: 90px;
			    position: relative;
			    z-index: 2;
			    &:before {
    				content: '';
    				position: absolute;
    				top: 84px;
    				left: calc(50% - 10px);
    				width: 10px;
    				height: 29px;
    				border: 1px solid var(--color-border-2);
    				border-top: 0px;
    				border-left: 0px;
    				border-radius: 0px 0px 8px 0px;
    				z-index: 1;
    				background: transparent;
    			}
    			&:after {
    				content: '';
    				position: absolute;
    				top: 84px;
    				left: 50%;
    				width: 10px;
    				height: 29px;
    				border: 1px solid var(--color-border-2);
    				border-top: 0px;
    				border-right: 0px;
    				border-radius: 0px 0px 0px 8px;
    				z-index: 1;
    				background: transparent;
    			}
    			.expand-btn {
    				position: absolute;
    				top: 72px;
    				left: calc(50% - 6px);
    				width: 12px;
    				height: 12px;
    				border-radius: 2px;
					background: var(--color-fill-2);
					color: var(--color-text-2);
					text-align: center;
					line-height: 12px;
					font-size: 6px;
					svg {
						vertical-align: top;
    					margin-top: 2.5px;
					}
    			}
			}
			
		}
		.indent-block-group {
			display: flex;
			.indent-block {
			    height: 68px;
	    		width: 30px;
			}
		}
		
		&:not(.lvl-0) {
			.expand-btn {
				position: absolute;
				top: calc(50% - 6px);
				left: -20px;
				width: 12px;
				height: 12px;
				border-radius: 2px;
				background: var(--color-fill-2);
				color: var(--color-text-2);
				text-align: center;
				line-height: 12px;
				font-size: 6px;
				svg {
					vertical-align: top;
    				margin-top: 2.5px;
				}
			}
		}
	}
	.lvl-1-group {
		display: flex;
		column-gap: 40px;
		margin-left: 0px;
		position: relative;
		z-index: 2;
		&:before {
			content: '';
			position: absolute;
			top: -44px;
			left: 80px;
			width: calc(50% - 80px - 10px);
			height: 45px;
			border: 1px solid var(--color-border-2);
			border-bottom: 0px;
			border-right: 0px;
			border-radius: 8px 0px 0px 0px;
			z-index: 1;
		}
		&:after {
			content: '';
			position: absolute;
			top: -44px;
			right: 80px;
			width: calc(50% - 80px - 10px);
			height: 45px;
			border: 1px solid var(--color-border-2);
			border-bottom: 0px;
			border-left: 0px;
			border-radius: 0px 8px 0px 0px;
			z-index: 1;
		}
		&>.node-group-container {
			&:not(:first-child):not(:last-child)>.tree-node>.cost-model-card:after  {
			    content: "";
			    position: absolute;
			    top: -45px;
			    height: 42px;
			    width: 10px;
			    border-top: 1px solid var(--color-border-2);
			    border-right: 1px solid var(--color-border-2);
			    border-radius: 0px 8px 0px 0px;
			    left: calc(50% + 10px);
			}
			&.left:not(:first-child):not(:last-child)>.tree-node>.cost-model-card:after {
				left: calc(50% - 10px);
				border-top: 1px solid var(--color-border-2);
			    border-left: 1px solid var(--color-border-2);
			    border-right: 0px;
			    border-radius: 8px 0px 0px 0px;
			}
			&.center:not(:first-child):not(:last-child)>.tree-node>.cost-model-card:after {
				width: 0px;
				border-top: 0px;
			}
			&.right:not(:first-child):not(:last-child)>.tree-node>.cost-model-card:after {
			}
		}
	}
	.indent-block {
	    position: relative;
	    transition: all .2s;
	}
	.indent-block.pre-line:before {
	    content: '';
	    position: absolute;
	    width: 1px;
	    height: 90px;
	    top: -58px;
	    left: -13.5px;
	    border-left: 1px solid var(--color-border-2);
	}
	.indent-block.pre-line:last-child:before {
		height: 84px;
	}
	.indent-block.pre-line:last-child:after {
	    content: '';
	    position: absolute;
	    top: 24px;
	    left: -13.5px;
	    border-bottom: 1px solid var(--color-border-2);
	    border-left: 1px solid var(--color-border-2);
	    width: 44px;
	    height: 10px;
	    border-radius: 0px 0px 0px 8px;
	}
	.has-children>.indent-block-group>.indent-block.pre-line:last-child:after {
		width: 34px;
	}
	.cost-model-card {
	    height: 68px;
	    opacity: 1;
	    transition: all .2s;
	}
	.tree-node.hidden:not(.lvl-0) {
		&>.cost-model-card {
			width: 0px;
		    height: 0px;
		    opacity: 0;
		    margin-bottom: 0px;
		    border: 0px;
		    overflow: hidden;
	    }
	}
	.tree-node.hidden:not(.lvl-0)>.indent-block-group {
		.indent-block {
			width: 0px;
			height: 0px;
		}
		.indent-block:before {
			display: none;
		}
		.indent-block:after {
			display: none;
		}   
	}
	.tree-node.hidden.lvl-0 {
		&>.cost-model-card {
			&:before {
				display: none;
			}
			&:after {
				display: none;
			}
		}
	}
	.tree-node-children.lvl-1-group.hidden {
		&:before {
			display: none;
		}
		&:after {
			display: none;
		}
	}
	.cost-model-card {
		display: inline-block;
		min-width: 200px;
		border: 1px solid var(--color-border-2);
		background: var(--color-white);
		border-radius: 4px;
		margin-bottom: 20px;
		position: relative;
		z-index: 2;
		cursor: pointer;
		&:before {
		    content: "";
		    border-radius: 4px;
		    border-top: 2px solid var(--color-primary-6);
		    background: var(--color-bg-2);
		    position: absolute;
		    top: -2px;
		    height: 10px;
		    width: 100%;
		}
		&:hover {
			box-shadow: 0px 2px 5px 0px rgba(36, 46, 67, 0.10);
		}
		&.root {
			.title-container {
				display: block;
				text-align: center;
				.icon, .text {
					display: inline-block;
					line-height: 20px;
				}
				.icon {
					height: 14px;
    				vertical-align: middle;
				}
				.text {
				    vertical-align: middle;
				    color: var(--color-text-1);
				    font-size: 14px;
				}
			}
		}
		.title-container {
			display: flex;
			height: 36px;
			padding: 8px 16px;
			.icon {
				margin-right: 8px;
				img {
					width: 12px;
				}
			}
			.text {
				color: var(--color-text-2);
				flex: 1;
				font-weight: 600;
			}
			.oper {
				margin-left: 8px;
			}
		}
		.oper-group-container {
			height: 31px;
			line-height: 30px;
			color: var(--color-text-2);
			border-top: 1px solid var(--color-border-2);
    		.oper-group-content {
    			display: flex;
    			flex-direction: row;
    			width: max-content;
    			margin: 0px auto;
    		}
    		.split {
    			width: 1px;
    			height: 10px;
    			background: var(--color-border-2);
    			margin-right: 6px;
    			margin-top: 10px;
    		}
			svg {
				margin-right: 6px;
			}
		}
	}
	.cost-model-card.root .title-container {
		position: relative;
	}
	.cost-model-card.root .title-container:before {
	    content: "";
	    border-radius: 4px;
	    border-top: 2px solid var(--color-primary-6);
	    background: var(--color-bg-2);
	    position: absolute;
	    top: -2px;
	    height: 10px;
	    width: 100%;
	    left: 0px;
	}
}

posted @ 2025-01-08 16:38  Empress&  阅读(23)  评论(0)    收藏  举报