//App.js import React, { Component } from 'react'; import './App.css' class App extends Component { constructor(props) { super(props) this.getNode = this.getNode.bind(this) this.drawLine = this.drawLine.bind(this) this.addLine = this.addLine.bind(this) this.state = { //节点树 存储节点的关系 node: { node: { text: '根节点', level: 0, id: 0, children: [] } }, //当前节点 curNode: null, //要绘制边的节点id nodeId: [], //边的信息 line: [], //父节点与子节点的关系 parentAndChild: [ { id: 0, childrenId: [] } ] } } drawLine() { return this.state.line.map(item => { let path = `M${item.curX} ${item.curY} L${item.parentX} ${item.parentY} Z` return ( <path d={path} style={{ fill: 'white', stroke: 'red', strokeWidth: 2 }} /> ) }) } getNode(root) { let node = root.node let margin = 10 let className, x if (!node) { return } switch (node.level) { case 0: className = 'node-0' break; case 1: className = 'node-1' x = 80 break; default: className = 'node-2' x = 100 break; } return ( <div id={node.id} style={{ left: x }} key={node.id} className="flex" > <div onClick={this.chooseNode.bind(this, node)} style={{ margin }} suppressContentEditableWarning contentEditable='true' className={className}>{node.text}</div> <div> { node.children ? node.children.map(item => this.getNode(item)) : '' } </div> </div> ) } chooseNode(node) { this.setState({ curNode: node }) } blur() { this.setState({ curNode: null }) } addParentAndChild(arr, parentId, childId) { let has = false for (let i = 0; i < arr.length; i++) { if (arr[i].id == parentId) { arr[i].childrenId.push(childId) } if (arr[i].id == childId) { has = true } } if (!has) { let obj = {} obj.childrenId = [] obj.id = childId arr.push(obj) } } addNode() { let curNode = this.state.curNode let nodeId = this.state.nodeId let parentAndChild = this.state.parentAndChild if (!curNode) { alert('请选中一个节点') return } let obj = {} let level if (curNode.level == 2) { level = 2 } else { level = curNode.level + 1 } obj.level = level obj.text = '新节点' obj.id = +new Date() obj.children = [] curNode.children.push({ node: obj }) nodeId.push(obj.id) this.addParentAndChild(parentAndChild, curNode.id, obj.id) let state = Object.assign({}, this.state.node, curNode, nodeId, parentAndChild) this.setState({ state }) setTimeout(() => { let newLine = [] this.state.nodeId.forEach(item => this.addLine(item, newLine)) let line = this.state.line this.setState({ line: newLine }) }, 0); } addLine(id, newLine) { let e = document.getElementById(id) if (!e) { return } let parent = e.parentNode.parentNode.children[0] let pType = Number(parent.className.split('-')[1]) let eType = Number(parent.className.split('-')[1]) let pLeftOffset = 0, pTopOffset = 0, eLeftOffset = 10, eTopOffset = 0 switch (pType) { case 0: pLeftOffset = 150 pTopOffset = 25 break; case 1: pLeftOffset = 100 pTopOffset = 20 default: pLeftOffset = 70 pTopOffset = 15 break; } let obj = {} obj.parentX = (parent.getBoundingClientRect().left + pLeftOffset) obj.parentY = (parent.getBoundingClientRect().top + pTopOffset) obj.curX = e.getBoundingClientRect().left + eLeftOffset //加上margin大小 eTopOffset = Number(window.getComputedStyle(e).height.split('p')[0]) obj.curY = (e.getBoundingClientRect().top + eTopOffset / 2) //半个子节点高度 newLine.push(obj) } getNodeAndChildren() { let curId = this.state.curNode.id let parentAndChild = this.state.parentAndChild let shouldDeleteNodeId = [] let nodeId = this.state.nodeId for (let i = 0; i < parentAndChild.length; i++) { if (parentAndChild[i].id === curId) { shouldDeleteNodeId.push(parentAndChild[i].id) if (parentAndChild[i].childrenId && parentAndChild[i].childrenId !== 0) { parentAndChild[i].childrenId.forEach(item => { shouldDeleteNodeId.push(item) }) } } } nodeId = nodeId.filter(key => !shouldDeleteNodeId.includes(key)) this.setState({ line: [] }) setTimeout(() => { let newLine = [] nodeId.forEach(item => this.addLine(item, newLine)) let line = this.state.line this.setState({ line: newLine, nodeId }) }, 0); } deleteNode() { let curNode = this.state.curNode let nodeId = this.state.nodeId if (curNode.id == 0) { alert('无法删除根节点') return } let node = this.state.node //删除边 this.getNodeAndChildren() //删除节点 let curId = curNode.id let jsonStr = JSON.stringify(node, (key, val) => { if (key == 'node' && val.id == curId) { return undefined } else { return val } }) let n = JSON.parse(jsonStr) this.setState({ node: n }) } render() { return ( <> <button style={{ position: 'absolute' }} onClick={this.addNode.bind(this)}>增加节点</button> <button style={{ position: 'absolute', left: 100 }} onClick={this.deleteNode.bind(this)}>删除节点</button> <svg width="100vw" height="100vh" version="1.1"> {this.drawLine()} </svg> <div className="container"> {this.getNode(this.state.node)} </div> </> ) } } export default App; //App.css body, html { margin: 0; padding: 0; overflow: hidden; } .container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .node-0 { position: relative; width: 150px; height: 50px; text-align: center; line-height: 50px; background-color: skyblue; color: #fff; border-radius: 10px; } .node-1 { position: relative; width: 100px; height: 40px; text-align: center; background-color: violet; border-radius: 10px; line-height: 40px; color: #fff; } .node-2 { position: relative; width: 70px; height: 30px; border-radius: 8px; line-height: 30px; text-align: center; color: #fff; background-color: sandybrown } .flex { display: flex; position: relative; align-items: center; } .line { position: relative; } .link { fill: none; stroke: red; stroke-width: 6px; cursor: default; }