最近接到一个需求,实现组织架构图,领导说,你去研究下思维导图,用思维导图实现。 于是我一古脑的就研究了思维导图,研究来研究去,总感觉有点地方不对。 我告诉领导,我研究的思维导图智能化左右不能不能上下啊。领导说那不行,继续研究。 于是我对着销售给来的截图10分钟,才发现这不是组织架构图吗?领导说是思维导图。
所以,领导有时候也是猪啊。
翻遍组织架构插件,都不能完全实现我们的需求,于是决定用d3结合自己来写。 上代码:
<template>
<div ref="orgTreeContainer" class="org-tree-wrap">
<div id="treeSvg"></div>
</div>
</template>
<script>
import * as d3 from 'd3'
export default {
name: 'CommonOrgTree',
props: {
data: {
type: Object,
default: () => {
return {}
},
},
url: {
type: String,
default: '',
},
svgConfig: {
type: Object,
default: () => {
return {
y: 40, //y轴下移40px
}
},
},
arrowConfig: {
type: Object,
default: () => {
return { color: '#409eff' }
},
},
circleConfig: {
type: Object,
default: () => {
return {
r: 7,
padding: 3,
fillColor: '#409eff',
strokeColor: '#fff',
}
},
},
linkConfig: {
type: Object,
default: () => {
return {
lineColor: '#409eff',
textColor: '#409eff',
height: 140,
textField: 'GDP',
}
},
},
nodeConfig: {
type: Object,
default: () => {
return {
fields: ['name', 'id', 'GDP'],
height: 75,
width: 100,
}
},
},
},
setup(props) {
const DEFAULTSVGCONFIG = {
y: 40,
}
const DEFAULTARROWCONFIG = {
color: '#409eff',
}
const DEFAULTCIRCLECONFIG = {
r: 7,
padding: 3,
fillColor: '#409eff',
strokeColor: '#fff',
}
const DEFAULTLINKCONFIG = {
lineColor: '#409eff',
textColor: '#409eff',
height: 140,
}
const DEFAULTNODECONFIG = {
fields: ['name', 'id', 'GDP'],
height: 75,
width: 100,
}
let treeData = null
const orgTreeContainer = ref(null)
let treeMap = null
let svg = null
let timeContaniner = null
/**
* 获取树结构深度
*/
const getTreedeep = (treeData, sum = 0) => {
let sums = []
if (treeData.children) {
sums = treeData.children.map((item) => {
const sumC = sum
if (item.children) {
return getTreedeep(item, sumC + 1)
} else {
return sumC + 1
}
})
}
const value = sums.reduce((pre, cur) => {
if (cur > pre) return cur
return pre
})
return value
}
/**
*创建箭头
* @param {*} svg 初始化创建的svg
*/
const createMark = () => {
const { arrowConfig = DEFAULTARROWCONFIG } = props
const marker = svg
.append('marker')
.attr('id', 'resolved')
.attr('markerUnits', 'strokeWidth') //设置为strokeWidth箭头会随着线的粗细发生变化
.attr('markerUnits', 'userSpaceOnUse')
.attr('viewBox', '0 -5 10 10') //坐标系的区域
.attr('refX', 10) //箭头坐标
.attr('refY', 0)
.attr('markerWidth', 12) //标识的大小
.attr('markerHeight', 12)
.attr('orient', 'auto') //绘制方向,可设定为:auto(自动确认方向)和 角度值
.attr('stroke-width', 2) //箭头宽度
.append('path')
.attr('d', 'M0,-5L10,0L0,5') //箭头的路径
.attr('fill', arrowConfig.color) //箭头颜色
return marker
}
/**
* 节点收缩子节点符号
* @param {*} node 节点
*/
const drawCircle = (node) => {
const { r, padding, fillColor, strokeColor } = {
...props.circleConfig,
...DEFAULTCIRCLECONFIG,
}
const { width, height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
const gMark = node
.append('g')
.style('display', (d) => {
if (!d.data._children) {
return 'none'
}
})
.attr('class', 'node-circle')
.attr('transform', `translate(${width / 2},${height + r})`)
gMark
.append('circle')
.attr('fill', 'none')
.attr('r', (d) => (d.depth === 0 ? 0 : r)) //根节点不设置圆圈
.attr('fill', fillColor)
gMark
.append('path')
.attr('d', `m -${padding} 0 l ${2 * padding} 0`)
.attr('stroke', strokeColor) //横线
gMark
.append('path') //竖线,根据展开/收缩动态控制显示
.attr('d', `m 0 -${padding} l 0 ${2 * padding}`)
.attr('stroke-width', 1)
.attr('stroke', strokeColor)
.attr('class', 'node-circle-vertical')
return gMark
}
/**
* 点击节点展开收缩子节点
* @param {*} d
*/
const clickNode = (d) => {
if (!d.data._children && !d.data.children) {
//无子节点
return
}
if (d.data.children) {
d.data._children = d.data.children
d.data.children = null
} else {
d.data.children = d.data._children
d.data._children = null
}
//**清除画布 */
d3.select('#treeSvg').selectAll('svg').remove()
updateTree()
}
/**
* 初始化树
*/
const initSvgTree = () => {
const { y } = { ...props.svgConfig, ...DEFAULTSVGCONFIG }
const { height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
const { height: lineHeight } = {
...props.linkConfig,
...DEFAULTLINKCONFIG,
}
if (!orgTreeContainer.value) return
const { offsetWidth, offsetHeight } = orgTreeContainer.value
const treeDepth = getTreedeep(treeData)
const treeHeight = treeDepth * lineHeight + height
const curHeight = Math.max(treeHeight, offsetHeight)
treeMap = d3.tree().size([offsetWidth - 50, curHeight])
svg = d3
.select('#treeSvg')
.append('svg')
.attr('width', offsetWidth)
.attr('height', curHeight + height)
.append('g')
.attr('transform', `translate(0,${y})`)
.attr('width', offsetWidth)
.attr('height', curHeight + y)
}
/**
* 创建节点div
* @param {*} d
*/
function createNodeDiv(d) {
const { html, fields } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
if (!html) {
let str = ''
const dom = fields.reduce((pre, cur, index) => {
let strHtml = ''
if (index === 0) {
strHtml = `<div class="title">${d.data[cur] ?? ''}</div>`
} else {
strHtml += `${pre}<div class="des">${d.data[cur] ?? ''}</div>`
}
return strHtml
}, '')
str = `<div class="org-content-wrap "> ${dom}</div>`
return str
}
const html1 = html.replace(/\{\{(\w+|d+)\}\}/g, (word, $1) => {
console.warn(6859869, word, $1)
return d.data[$1] ?? ''
})
return html1
}
/**
* 创建links
*/
const createLinks = (nodes) => {
const { lineColor, textColor, textField } = {
...props.linkConfig,
...DEFAULTLINKCONFIG,
}
const links = nodes.links()
const link = svg.selectAll('.link').data(links)
link
.enter()
.append('path')
.attr('class', 'link')
.attr('fill', 'none')
.attr('stroke-width', 1)
.attr('stroke', lineColor)
.attr(
'd',
d3
.linkVertical() // linkVertical() 垂直 linkHorizontal() 水平
.x(function (d) {
return d.x + 20
})
.y(function (d) {
return d.y
})
)
.attr('marker-end', 'url(#resolved)')
link
.enter()
.append('g')
.attr('transform', function (d) {
const x = d.target.x + 10
const y = d.target.y - 40
return `translate(${x},${y})`
})
.append('text')
.attr('dy', '.33em')
.attr('font-size', '12px')
.attr('fill', textColor)
.attr('writing-mode', 'tb')
.text(function (d) {
return d.target.data[textField]
})
}
/**
* 创建nodes
* @param {*} nodes
*/
const createNodes = (nodesDes) => {
const { width, height } = { ...props.nodeConfig, ...DEFAULTNODECONFIG }
const node = svg
.selectAll('.node')
.data(nodesDes)
.enter()
.append('g')
.attr('class', function (d) {
return `node${d.children ? ' node--internal' : ' node--leaf'}`
})
.attr('transform', function (d) {
const x = d.x - 25
const y = d.y
return `translate(${x},${y})`
})
.on('click', ($event, d) => {
d.depth !== 0 && clickNode(d) //根节点不执行点击事件
})
node
.append('foreignObject')
.attr('class', (d) => {
const className = d.depth === 0 ? 'first-org-oreign' : ''
return `org-oreign ${className}`
})
.attr('width', width)
.attr('height', height)
.append('xhtml:div')
.html((d) => {
return createNodeDiv(d)
})
drawCircle(node)
}
/**
* 创建树以及节点
*/
function updateTree() {
const { height } = props.linkConfig
initSvgTree()
//箭头
createMark(svg)
const dataSet = d3.hierarchy(treeData)
const nodes = treeMap(dataSet)
const nodeInfos = nodes.descendants()
nodeInfos.forEach((d) => {
d.y = height * d.depth
})
//获取link以及创建link
createLinks(nodes)
//获取node以及创建node
createNodes(nodeInfos)
}
onUnmounted(() => {
clearTimeout(timeContaniner)
})
watch(
() => props.data,
(newVal) => {
if (!props.url) {
treeData = newVal
nextTick(() => {
timeContaniner = setTimeout(() => {
updateTree()
}, 350)
})
}
},
{
deep: true,
immediate: true,
}
)
return {
orgTreeContainer,
}
},
}
</script>
<style>
.org-tree-wrap {
flex: 1;
}
.foreign_add {
width: 10px;
height: 10px;
border-radius: 5px;
}
.org-oreign {
padding: 0 10px;
font-size: 12px;
background-color: #4ae;
border-radius: 5px;
}
.first-org-oreign {
position: relative;
transform: translateY(-40px);
}
.org-oreign div {
height: 25px;
line-height: 25px;
}
.org-oreign .title {
font-weight: bold;
}
</style>
整体思路为:创建画布,初始化树,创建连线,创建node,且可折叠子节点 最后展示效果: