添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
开朗的海豚  ·  PayloadsAllTheThings/X ...·  2 周前    · 
淡定的葡萄  ·  服务器-小叶白龙博客·  4 月前    · 
火爆的草稿本  ·  Coordinating ...·  8 月前    · 
温柔的红薯  ·  Jest | 正儿八经·  10 月前    · 
Document

1、makeSVG.js 生成svg元素

const makeSVG = (tag, attrs = {})  => {
    const ns = 'http://www.w3.org/2000/svg', xlinkns = 'http://www.w3.org/1999/xlink';
    let el = document.createElementNS(ns, tag);
    if (tag === 'svg') {
        el.setAttribute('xmlns:xlink', xlinkns);
        el.setAttribute('draggable', true);
    for (let k in attrs) {
        k === 'xlink:href' ? el.setAttributeNS(xlinkns, k, attrs[k]) : el.setAttribute(k, attrs[k]);
    return el;

2、organ.js 数据处理为树形结构,接口一般返回的是一维数组,这里根据实际需要

class OrganModel {
    originData = [];                          // 接口返回的原始数据
    tableData = [];                           // 处理过层级的数据
    // 处理数据为树形结构
    handleData() {
        let data = JSON.parse(JSON.stringify(this.originData))
        let findChild = (rootEl) => {
            let a = data.filter(v => v.parent_id === rootEl.id)
            let b = data.filter(v => v.parent_id !== rootEl.id)
            data = b
            rootEl.children = a
            let level = isNaN(rootEl.level) ? 1 : (Number(rootEl.level) + 1)
            for (let v of rootEl.children) {
                v.level = level
                findChild(v)
        let top = {id: 0, level: 0}
        findChild(top)
        this.tableData = top.children || []

3、node.js 节点:一个节点包含矩形框、文本、操作按钮、父子节点连线

const lineColor = '#4ec2ff'; // 线条颜色 const lineWidth = 2; // 线条宽度 const collapseSize = 6; // 折叠点圆圈尺寸 const fontSize = 13; // 节点文本字号 const paddingSize = 5; // 一个节点的padding值 const marginSize = 40; // 一个节点的右边距 const line1 = 30; // 父子节点间距(上方) const line2 = 80; // 父子节点间距(下方) const maxWidth = 200; // 节点矩形框最大宽度 const maxHeight = 200; // 节点矩形框最大高度 const lineHeight = 10; // 文字行间距 const letterSpacing = 3; // 文字字符间距 const toolsHeight = 30 class NodeOrgan { line1 = line1; line2 = line2; marginSize = marginSize; xStart; // x 坐标 yStart; // y 坐标 direction = 'horizontal'; // 文字排列方向 horizontal:水平 vertical:垂直 nodeText = []; // 节点主文本 有换行的情况,需要分段显示 width; // 节点最终宽度 height; // 节点最终高度 middle; prevNode; // 前一个兄弟节点 parentNode; // 父节点 constructor(props = {}) { for (let k in props) this[k] = props[k] this.setNodeText() // 设置节点文本 setNodeText() { if(!this.name) return let name = `${this.name}` let nodeText, w, h, isHor = this.direction === 'horizontal' // 文字水平排列 let compareLength = name.length let maxWords = ((isHor ? maxWidth : maxHeight) - paddingSize) / (fontSize + letterSpacing) - 1 // 一行或一竖最多可显示字数 if(compareLength > maxWords) { let lines = Math.ceil(compareLength / maxWords) if(isHor) { w = maxWidth h = lines * (fontSize + lineHeight) + paddingSize + fontSize }else { w = lines * (fontSize + lineHeight) + paddingSize + fontSize h = maxHeight nodeText = [] let func = (str) => { if(!str) return nodeText.push(str.substring(0, maxWords)) func(str.substring(maxWords)) func(name) }else { if(isHor) { w = paddingSize + (fontSize + letterSpacing) * compareLength h = paddingSize + fontSize + lineHeight + fontSize }else { w = paddingSize + fontSize + lineHeight + fontSize h = paddingSize + (fontSize + letterSpacing) * compareLength nodeText = [name] if(w < 90) w = 90 // 按钮宽度预留 this.width = w this.height = h + toolsHeight // 此处的fontSize 表示负责人一横数据高度 this.nodeText = nodeText // 节点矩形框 createRect() { this.middle = this.xStart + this.width / 2 // 中间位置 return makeSVG('rect', { x: this.xStart, y: this.yStart, width: this.width, height: this.height, rx: 5, class: 'node-rect', fill: 'white', stroke: lineColor, 'stroke-width': lineWidth, // 节点文字 createText() { let startY = this.yStart + paddingSize + fontSize let startX = this.xStart + fontSize let textGroup = makeSVG('g', ) let setAttrs = (i) => { return this.direction === 'horizontal' ? x: startX, y: startY + (fontSize + lineHeight) * i, x: startX + (fontSize + letterSpacing + 10) * i, y: startY, transform: `rotate(90, ${startX + (fontSize + letterSpacing + 10) * i}, ${startY})`, rotate: '-90', if(this.nodeText.length === 1) { let text = makeSVG('text', { fill: 'black', stroke: 'none', 'font-size': fontSize, 'letter-spacing': letterSpacing, 'text-anchor': 'middle', 'dominant-baseline': "middle", x: this.middle, y: startY text.innerHTML = this.nodeText[0] textGroup.appendChild(text) }else { for(let i in this.nodeText) { let text = makeSVG('text', { fill: 'black', stroke: 'none', 'font-size': fontSize, 'letter-spacing': letterSpacing, ...setAttrs(i) text.innerHTML = this.nodeText[i] textGroup.appendChild(text) return textGroup // 节点操作按钮 createTools() { let tools = makeSVG('text', { x: this.middle, y: this.yStart + this.height - 10, fill: 'black', class: 'node-tools', 'font-size': 12, 'text-anchor': 'middle', 'dominant-baseline': "middle", let createBtn = (type) => { let btn = makeSVG('tspan', { fill: {'add': '#4ec2ff', 'edit': '#5fb878', 'del': '#ff5722'}[type], class: 'node-tools-item' btn.innerHTML = {'add': '新增 ', 'edit': '编辑 ', 'del': '删除'}[type] btn.addEventListener('click', () => { this.toolsHandle && this.toolsHandle(this, type) tools.appendChild(btn) if (this.level < 6) createBtn('add') // 最多新增6级 createBtn('edit') if (this.level > 1) createBtn('del') // 第一级不可删除 return tools // 父子节点连接线 createLine() { let lines = makeSVG('g', { close: 'open' // 折叠节点使用 let parent = this.parentNode if (parent) { let startYParent = parent.yStart + parent.height + line1 // 折叠节点下方的竖线:line2 竖线 lines.appendChild(makeSVG('path', { d: `M ${this.middle} ${startYParent} L ${this.middle} ${this.yStart} z`, fill: 'none', stroke: lineColor, 'stroke-width': lineWidth, // 折叠节点下方的横线:向左画(第一个节点不需要画) 横线 // 寻找前一个节点 // console.log(this.prevNode) let prev = this.prevNode if(prev) { let start = `${prev.middle} ${startYParent}`, end = `${this.middle} ${startYParent}` let lineChilds = makeSVG('path', { d: `M ${start} L ${end} z`, stroke: lineColor, 'stroke-width': lineWidth, fill: 'none', lines.appendChild(lineChilds) if (!this.children || this.children.length <= 0) return lines let startY = this.yStart + this.height + line1 // 与子节点的第一段连线 lines.appendChild(makeSVG('path', { d: `M ${this.middle} ${this.yStart + this.height} L ${this.middle} ${startY} z`, stroke: lineColor, 'stroke-width': lineWidth, fill: 'none' // 添加一个折叠节点 let collapse = makeSVG('circle', { cx: this.middle, cy: startY, r: collapseSize, stroke: lineColor, fill: 'white', 'stroke-width': 1, style: 'cursor: pointer', let iconText = makeSVG('text', { x: this.middle, y: startY, 'font-size': 12, fill: lineColor, 'text-anchor': 'middle', 'dominant-baseline': "middle", style: 'cursor: pointer', iconText.innerHTML = '-' let clickFunc = function () { let close = lines.getAttribute('close') === 'close', brother = lines.parentNode.childNodes lines.setAttribute('close', close ? 'open' : 'close') iconText.innerHTML = close ? '-' : '+' brother.forEach((v) => { if (v.tagName === 'g' && v.getAttribute('collapse') === 'yes') v.style.display = close ? '' : 'none' collapse.addEventListener('click', clickFunc) iconText.addEventListener('click', clickFunc) lines.appendChild(collapse) lines.appendChild(iconText) return lines

4、draw.js 根据数据,绘制节点并连接在一起,写入页面

class Draw { hasCreated = false; // 是否已经绘制过 data; // 要绘制的数据 $box; // 生成的svg要填充到的 dom元素 $svg; // 生成的svg元素 constructor(options = {}) { if (options.data) this.setData(options.data) if (options.$box) this.$box = options.$box if (options.toolsHandle) this.toolsHandle = options.toolsHandle setData(tableData) { if (!tableData || tableData.length === 0) { this.data = [] return this.data = JSON.parse(JSON.stringify(tableData)) // 深拷贝原始数据 // 画图 create() { if (!this.data || this.data.length <= 0) return // if (this.hasCreated) return; // 避免重复创建 // this.hasCreated = true this.$box.innerHTML = '' this.$svg = makeSVG('svg') this.setAxis() this.createSvg(this.data) this.$box.append(this.$svg) // 滚动到中心位置 // box.scrollTo(svgWidth / 2 - box.offsetWidth / 2, 0) // 操作按钮 toolsHandle () { // 设置节点坐标 setAxis() { let levelXStart = {}, // 寻找同级节点的离当前线最近的x坐标,防止节点重叠 xStart = 100, svgWidth = 0, svgHeight = 0 let func = (arr, parent) => { if (!arr || arr.length <= 0) return let y = parent ? (parent.yStart + parent.line1 + parent.line2 + parent.height) : 0 // line1 line2 参见 node.js arr.forEach((v, i) => { let node = new NodeOrgan(v) node.yStart = y node.parentNode = parent node.prevNode = arr[i - 1] node.toolsHandle = this.toolsHandle // 操作按钮 arr[i] = node if (levelXStart[v.level] > xStart) xStart = levelXStart[v.level] if (node.children && node.children.length) { let minXStart = xStart func(node.children, node) let end = node.children[node.children.length - 1], first = node.children[0] let nowXStart = (end.xStart + end.width - first.xStart - node.width) / 2 + first.xStart if (nowXStart < minXStart) { // 可能有重叠块,重新计算一下位置 let num = minXStart - nowXStart let resetAxis = (childs) => { for (let v of childs) { v.xStart += num let x = v.xStart + v.width + v.marginSize if (levelXStart[v.level] < x) levelXStart[v.level] = x if (v.children && v.children.length) resetAxis(v.children) resetAxis(node.children) xStart = minXStart } else { xStart = nowXStart node.xStart = xStart if (arr[i + 1]) xStart += node.width + node.marginSize } else { node.xStart = xStart xStart += node.width + node.marginSize levelXStart[v.level] = xStart // 画布大小设置 if (y > svgHeight) svgHeight = y if (xStart > svgWidth) svgWidth = xStart func(this.data) // console.log(this.data) this.$svg.setAttribute('width', svgWidth + 300) this.$svg.setAttribute('height', svgHeight + 300) // 生成各节点实例 createSvg(arr, parentDiv) { if (!arr || arr.length <= 0) return [] for (let v of arr) { let g = makeSVG('g', { collapse: 'yes', id: `level-${v.level}-${v.id}` // 节点矩形框 g.appendChild(v.createRect()) // 节点文本 g.appendChild(v.createText()) // 操作:新增、编辑、删除 g.appendChild(v.createTools()) let hasChild = v.children && v.children.length > 0 if (hasChild) this.createSvg(v.children, g) // 节点与父节点的连线 if (parentDiv || hasChild) g.appendChild(v.createLine()) let top = parentDiv || this.$svg top.appendChild(g)

5、data.js 测试数据

const originData = [
        "id": 28,
        "name": "大名称",
        "parent_id": 0,
        "id": 37,
        "name": "总经办",
        "parent_id": 28,
        "id": 38,
        "name": "行政中心",
        "parent_id": 28,
        "id": 40,
        "name": "三层",
        "parent_id": 38,
        "id": 41,
        "name": "四层",
        "parent_id": 40,
        "id": 42,
        "name": "五层",
        "parent_id": 41,
        "id": 43,
        "name": "八层",
        "parent_id": 42,
        "id": 63,
        "name": "二层节点",
        "parent_id": 28,
        "id": 64,
        "name": "三层节点",
        "parent_id": 63,
        "id": 65,
        "name": "二层2",
        "parent_id": 28,
        "id": 66,
        "name": "二层3",
        "parent_id": 28,
        "id": 67,
        "name": "三层3-1",
        "parent_id": 66,
        "id": 68,
        "name": "二层4",
        "parent_id": 28,
        "id": 71,
        "name": "二层5",
        "parent_id": 28,
        "id": 72,
        "name": "二层6",
        "parent_id": 28,
        "id": 73,
        "name": "二层5-1",
        "parent_id": 71,
        "id": 76,
        "name": "1212",
        "parent_id": 65,
        "id": 81,
        "name": "2121",
        "parent_id": 65,