写在前面
在一些管理系统中,会对组织架构、级联数据等做一些管理,你会怎么实现呢?在经过调研很多插件之后决定使用 Antv G6 实现,文档也比较清晰,看看怎么实现吧,先来看看效果图。点击在线体验
实现的功能有:
- 增加节点
- 删除节点
- 编辑节点
- 展开收起
具体实现
- 先在项目中安装 antv g6
shell
npm install --save @antv/g6
- vue 文件创建容器渲染
- 渲染的容器
vue
<div id="container" class="one-tree"></div>
- 渲染方法和初始化树图
js
import G6 from '@antv/g6'
const state = reactive({
treeData: {
id: 'root',
sname: 'root',
name: uniqueId(),
children: [],
},
graph: null,
})
function renderMap(data: any[], graph: Graph): void {
G6.registerNode(
'icon-node',
{
options: {
size: [60, 20],
stroke: '#73D13D',
fill: '#fff'
},
draw(cfg: any, group: any) {
const styles = (this as any).getShapeStyle(cfg)
const { labelCfg = {} } = cfg
const w = cfg.size[0]
const h = cfg.size[1]
const keyShape = group.addShape('rect', {
attrs: {
...styles,
cursor: 'pointer',
x: 0,
y: 0,
width: w, // 200,
height: h, // 60
fill: cfg.style.fill || '#fff'
},
name: 'node-rect',
draggable: true
})
// 动态增加和删除元素
group.addShape('text', {
attrs: {
x: 131,
y: 20,
r: 6,
stroke: '#707070',
cursor: 'pointer',
opacity: 1,
fontFamily: 'iconfont',
textAlign: 'center',
textBaseline: 'middle',
text: '\ue658',
fontSize: 16
},
name: 'add-item'
})
// 删除icon,根元素不能删除
if (cfg.id !== 'root') {
group.addShape('text', {
attrs: {
x: 110,
y: 20,
r: 6,
fontFamily: 'iconfont',
textAlign: 'center',
textBaseline: 'middle',
text: '\ue74b',
fontSize: 14,
stroke: '#909399',
cursor: 'pointer',
opacity: 0
},
name: 'remove-item'
})
}
if (cfg.sname) {
group.addShape('text', {
attrs: {
...labelCfg.style,
text: fittingString(cfg.sname, 110, 12),
textAlign: 'left',
x: 10,
y: 25
}
})
}
// 展开收起
if (cfg.children && cfg.children.length > 0) {
group.addShape('circle', {
attrs: {
width: 24,
height: 24,
x: 154,
y: 20,
r: 12,
cursor: 'pointer',
lineWidth: 1,
fill: !cfg.collapsed ? '#9e9e9e' : '#2196f3',
opacity: 1,
text: 1
},
name: 'collapse-icon'
})
group.addShape('text', {
attrs: {
...labelCfg.style,
text: cfg.children.length,
textAlign: 'left',
x: 150,
y: 25,
fill: '#ffffff',
fontWeight: 500,
cursor: 'pointer'
},
name: 'collapse-icon'
})
}
return keyShape
},
setState(name, value, item) {
const group = item?.getContainer()
if (name === 'collapsed') {
const marker = item?.get('group').find((ele: any) => ele.get('name') === 'collapse-icon')
const icon = value ? G6.Marker.expand : G6.Marker.collapse
marker.attr('symbol', icon)
}
if (name === 'selected') {
const nodeRect = group?.find(function (e) {
return e.get('name') === 'node-rect'
})
if (value) {
nodeRect?.attr({
stroke: '#2196f3',
lineWidth: 2
})
}
}
if (name === 'hover') {
const addMarker = group?.find(function (e) {
return e.get('name') === 'add-item'
})
const reduceMarker = group?.find(function (e) {
return e.get('name') === 'remove-item'
})
if (value) {
addMarker?.attr({
opacity: 1
})
reduceMarker?.attr({
opacity: 1
})
}
}
},
update: undefined
},
'rect'
)
graph.data(data)
graph.render()
mouseenterNode(graph)
mouseLeaveNode(graph)
collapseNode(graph)
}
function initGraph(graphWrapId: string): Graph {
const width = (document.getElementById(graphWrapId) as HTMLElement).clientWidth || 1000
const height = (document.getElementById(graphWrapId) as HTMLElement).clientHeight || 1000
const graph = new G6.TreeGraph({
container: graphWrapId,
width,
height,
linkCenter: true,
animate: false,
fitView: false, // 自动调整节点位置和缩放,使得节点适应画布大小
modes: {
default: ['scroll-canvas'],
edit: ['click-select']
},
defaultNode: {
type: 'icon-node',
size: [120, 40],
style: defaultNodeStyle,
labelCfg: defaultLabelCfg
},
defaultEdge: {
type: 'cubic-vertical'
},
comboStateStyles,
layout: defaultLayout
})
return graph
}
- 事件处理
js
/**
* @description:树型图的事件绑定
*/
// 展开收起子节点
function collapseNode(graph: Graph): void {
// 展开和收起子节点
graph.on('node:click', (e: any) => {
if (e.target.get('name') === 'collapse-icon') {
e.item.getModel().collapsed = !e.item.getModel().collapsed
graph.setItemState(e.item, 'collapsed', e.item.getModel().collapsed)
graph.layout()
}
})
}
// 鼠标滑入
function mouseenterNode(graph: Graph): void {
graph.on('node:mouseover', (evt: any) => {
const { item, target } = evt
if (item._cfg.id === 'root') return
const canHoverName = ['node-rect', 'remove-item']
if (!canHoverName.includes(target.get('name'))) return
// 显示icon
const deleteItem = item.get('group').find(function (el: any) {
return el.cfg.name === 'remove-item'
})
deleteItem.attr('opacity', 1)
if (item._cfg && item._cfg.keyShape) {
item._cfg.keyShape.attr('stroke', '#2196f3')
}
graph.setItemState(item, 'active', true)
})
}
// 鼠标离开
function mouseLeaveNode(graph: Graph): void {
graph.on('node:mouseout', (evt: any) => {
const { item, target } = evt
const canHoverName = ['node-rect', 'remove-item']
if (item._cfg.id === 'root') return
if (!canHoverName.includes(target.get('name'))) return
// 隐藏icon
const deleteItem = item.get('group').find(function (el: any) {
return el.cfg.name === 'remove-item'
})
deleteItem.attr('opacity', 0)
if (item._cfg && item._cfg.keyShape) {
item._cfg.keyShape.attr('stroke', '#fff')
}
graph.setItemState(item, 'active', false)
})
}
/**
* @description 文本超长显示
*/
const fittingString = (str: string, maxWidth: number, fontSize: number): string => {
const ellipsis = '...'
const ellipsisLength = Util.getTextSize(ellipsis, fontSize)[0]
let currentWidth = 0
let res = str
const pattern = new RegExp('[\u4E00-\u9FA5]+')
str.split('').forEach((letter, i) => {
if (currentWidth > maxWidth - ellipsisLength) return
if (pattern.test(letter)) {
currentWidth += fontSize
} else {
currentWidth += Util.getLetterWidth(letter, fontSize)
}
if (currentWidth > maxWidth - ellipsisLength) {
res = `${str.substr(0, i)}${ellipsis}`
}
})
return res
}
- 节点的增加、删除、编辑时间
js
const addEvent = (graph: any) => {
graph.on('node:click', (evt: any) => {
const { item, target } = evt
const name = target.get('name')
// 增加元素
const model = item.getModel()
if (name === 'add-item') {
state.editType = 'add'
// 如果收起需要展开
if (model.collapsed) model.collapsed = false
// 没有子级的时候设置空数组
if (!model.children) model.children = []
const id = uniqueId()
model.children.push({
id,
name: 1,
sname: '',
parentId: model.id,
})
graph.updateChild(model, model.id)
const curTarget = graph.findDataById(id)
const canvasXY = graph.getCanvasByPoint(curTarget.x, curTarget.y)
state.editOne = curTarget
state.input = curTarget.sname
setTimeout(() => {
state.showInput = true
nextTick(() => {
inputRref.value.focus()
})
}, 200)
// 更改输入框的位置
state.inputStyle = {
left: `${canvasXY.x}px`,
top: `${canvasXY.y}px`,
}
}
// 删除节点
if (name === 'remove-item') {
graph.removeChild(model.id)
// 查找当前的父id,更新其子元素的长度
graph.updateItem(model.parentId, {})
}
// 编辑
if (name === 'node-rect') {
const curTarget = graph.findDataById(item._cfg.id)
const canvasXY = graph.getCanvasByPoint(curTarget.x, curTarget.y)
state.editOne = evt.item
state.input = curTarget.sname
state.showInput = true
state.editType = 'edit'
nextTick(() => {
inputRref.value.focus()
})
state.inputStyle = {
left: `${canvasXY.x}px`,
top: `${canvasXY.y}px`,
}
}
})
// 画布滚动、拖动时,不能编辑节点名称
graph.on('dragstart', () => {
state.showInput = false
})
graph.on('wheel', () => {
state.showInput = false
})
}
- dom 节点渲染后渲染树图
js
onMounted(() => {
nextTick(() => {
state.graph = initGraph('container')
state.graph.clear()
addEvent(state.graph)
renderMap(state.treeData, state.graph)
})
})