功能
- 节点内容自定义
- 节点可实现折叠和展开
- 可支持思维导图的放大和缩小
- 可实现拖拽移动查看
- 节点的增加和删除
实现思路
- 整体:组件递归调用
- 节点内容自定义:外部传入
childComponent内部用component标签实现
- 折叠和展开:
v-show控制节点显示和隐藏
- 拖拽:自定义指令
v-bind实现v-drag
- 节点增加和删除:将方法传出通过修改传入源数据实现
实现效果

源码
增加删除节点组件vMindNodeSetting.vue
<template>
<div class="add-node-RowTree-box">
<el-popover
ref="popover"
v-model="visible"
v-click-out="hiddenPopover"
placement="right"
trigger="manual"
>
<div class="add-node">
<div class="add-node-item" @click="vMindNodeSet('addNode')">
<i class="el-icon-plus" style="cursor: pointer"/>
<span>添加</span>
</div>
<div class="add-node-item" @click="vMindNodeSet('delNode')">
<i class="el-icon-delete" style="cursor: pointer"/>
<span>删除</span>
</div>
</div>
<span
slot="reference"
class="el-icon-s-tools"
style="cursor: pointer"
@focus="visible=!visible"/>
</el-popover>
</div>
</template>
<script>
export default {
name: 'VMindNodeSetting',
components: {
},
directives: {
clickOut: {
bind(el, binding) {
el.clickOutsideEvent = function(event) {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unbind(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}
},
props: {
addOrDel: {
type: String,
default: ''
}
},
data() {
return {
visible: false
}
},
mounted() {},
methods: {
hiddenPopover() {
this.visible = false
},
vMindNodeSet(type) {
this.$emit('update:addOrDel', type)
this.$emit('vMindNodeSetClick')
this.visible = !this.visible
}
}
}
</script>
<style lang="scss" scoped>
.add-node-RowTree-box{
display: inline-block;
float: right;
}
.add-node {
font-size: 12px;
cursor: pointer;
display: flex;
height: 60px;
}
.add-node-item {
margin-bottom: 5px;
margin-right: 5px;
display: flex;
flex-direction: column;
align-items: center;
> span {
font-size: 14px;
font-weight: 500;
}
}
.add-node-item:hover {
color: #33BBF6;
}
.el-icon-plus,.el-icon-delete{
&:before{
padding: 20px;
font-size: 40px;
}
}
</style>
vMindRowTree节点组件--->index.vue
<template>
<div class="TreeRight" style="width: 100%;height: 100%">
<div v-if="list.length" class="childS">
<div v-drag v-for="(item,index) in list" id="child" :key="item.id +'-child-'+index" :class="{ mindRank: isRank }" class="child">
<div
:style="{marginRight: item.children && item.children.length > 1 ? '20px' :'',opacity:item.isShow?1:0}"
class="child-item"
>
<div :id="item.id" class="childName">
<el-card class="childName-card">
<div class="card-title">
<el-tooltip :content="item.label" class="item" effect="dark" placement="top">
<span>{{ item.label }}</span>
</el-tooltip>
<v-mind-node-setting v-show="canAddOrDel&&!readOnly" :add-or-del.sync="addOrDel" @vMindNodeSetClick="vMindNodeSetClick({node:item,type:addOrDel})"/>
</div>
<component
:key="item.id"
:is="childComponent"
:data="item"
/>
</el-card>
<i v-show="(item.children && item.children.length>1)&&!isShowBorderRight(item)" class="el-icon-circle-plus-outline" @click.stop="spreadChild(item)"/>
<i v-show="(item.children && item.children.length>1)&&isShowBorderRight(item)" class="el-icon-remove-outline" @click.stop="foldChild(item)"/>
<div v-if="list.length>1" class="position-arrow">
<i class="el-icon-right"/>
</div>
</div>
<div v-if="item.children && item.children.length&&isShowBorderRight(item)" class="position-arrow">
<i class="el-icon-right"/>
</div>
</div>
<div v-if="item.children && item.children.length" class="child-children">
<VMindRowTree
:list="item.children"
:child-component="childComponent"
:is-rank="isRank"
:is-show-title="isShowTitle"
:read-only="readOnly"
:can-add-or-del="canAddOrDel"
v-on="$listeners"/>
</div>
<div
v-show="isFirst(item.id) && domMounted&&item.isShow&&list.length>1"
class="borderLeftFirst"
/>
<div
v-show="isLast(item.id)&&item.isShow&&list.length>1"
class="borderLeftLast"
/>
<div
v-show="!(isFirst(item.id) && domMounted)&&!isLast(item.id)&&item.parentId&&item.isShow&&list.length>1"
class="borderLeftNormal"
/>
</div>
</div>
<div v-else>
<el-empty description="空空如也"/>
</div>
</div>
</template>
<script>
import vMindNodeSetting from '@/components/VmindRowTree/vMindNodeSetting'
export default {
name: 'VMindRowTree',
components: {
vMindNodeSetting
},
directives: {
drag: {
bind: function(el) {
const mindDiv = el
mindDiv.onmousedown = (e) => {
const arr = Array.from(mindDiv.classList)
if (!arr.includes('mindRank')) return
const disX = e.clientX - mindDiv.offsetLeft
const disY = e.clientY - mindDiv.offsetTop
console.log('offsetLeft和offsetTop', mindDiv.offsetLeft, mindDiv.offsetTop)
console.log('clientX和clientY', e.clientX, e.clientY)
document.onmousemove = (e) => {
const left = e.clientX - disX
const top = e.clientY - disY
mindDiv.style.left = left + 'px'
mindDiv.style.top = top + 'px'
}
document.onmouseup = (e) => {
document.onmousemove = null
document.onmouseup = null
}
}
}
}
},
props: {
list: {
type: Array,
default: () => []
},
childComponent: {
type: Object,
required: true
},
isShowTitle: {
type: Boolean,
default: false
},
readOnly: {
type: Boolean,
default: false
},
isRank: {
type: Boolean,
default: false
},
canAddOrDel: {
type: Boolean,
default: false
}
},
data() {
return {
addOrDel: '',
domMounted: false,
listNow: []
}
},
watch: {
list: {
handler(n, o) {
if (n && n.length) {
n !== o && this.doInit()
}
},
immediate: true,
deep: true
}
},
mounted() {
this.$nextTick(() => {
this.domMounted = true
})
},
methods: {
doInit() {
this.addOrDel = ''
this.domMounted = false
this.$nextTick(() => {
this.domMounted = true
})
},
isShowBorderRight(node) {
if (node.children && node.children.length) {
return node.children.every((x) => x.isShow === true)
} else {
return false
}
},
isFirst(id) {
return (
this.list.length > 1 && this.list.map((x) => x.id).indexOf(id) === 0
)
},
isLast(id) {
return (
this.list.length > 1 &&
this.list.map((x) => x.id).indexOf(id) === this.list.length - 1
)
},
spreadChild(node) {
new Promise((resolve, reject) => {
this.$emit('spreadOrFoldChild', { node: node, type: 'spread' }, resolve)
}).then((res) => {
res && this.spreadOrFold(node, 'spread')
})
},
foldChild(node) {
new Promise((resolve, reject) => {
this.$emit('spreadOrFoldChild', { node: node, type: 'fold' }, resolve)
}).then((res) => {
res && this.spreadOrFold(node, 'fold')
})
},
spreadOrFold(node, type) {
if (type === 'spread') {
node.children.forEach((item) => {
item.isShow = true
})
} else if (type === 'fold') {
node.children.forEach((item) => {
item.isShow = false
if (item.children && item.children.length > 0) {
this.spreadOrFold(item, 'fold')
}
})
}
},
vMindNodeSetClick(addOrDelObj) {
this.$emit('vMindNodeSetClick', addOrDelObj)
}
}
}
</script>
<style lang="scss" scoped>
.TreeRight {
display: flex;
.childS {
.child {
width: 100%;
display: flex;
background-color: #fff;
position: relative;
.borderLeftNormal,.borderLeftLast{
&:after{
content: "";
width: 1px;
height: 50%;
border-left: solid 1px #606266;
white-space: nowrap;
display: inline-block;
position: absolute;
left: -20px;
top: 0;
}
}
.borderLeftNormal,.borderLeftFirst{
&:before{
content: "";
width: 1px;
height: 50%;
border-left: solid 1px #606266;
white-space: nowrap;
display: inline-block;
position: absolute;
left: -20px;
bottom: 0;
}
}
.child-item {
display: flex;
align-items: center;
margin: 10px 0;
transition: opacity 0.2s linear;
.childName {
height: 100%;
display: flex;
align-items: center;
width: 450px;
text-align: center;
justify-content: center;
position: relative;
padding: 10px 0;
.position-arrow {
position: absolute;
left: -22px;
}
.childName-card{
height: auto;
width: 100%;
overflow: auto;
::v-deep.el-card__body{
padding: 8px 15px;
}
}
.childArrow {
width: 1px;
height: 100%;
background-color: black;
position: absolute;
display: flex;
align-items: center;
top: 0;
right: -16px;
}
}
}
.mindRank {
cursor: move;
}
}
.child-children {
display: flex;
flex-direction: column;
justify-content: center;
}
}
}
</style>
vMindRowTree面板组件--->vMindRowTree.vue
<template>
<div v-loading="loading" class="vMind-warp" style="width: 100%;height: 100%">
<div class="header">
<div>
<el-input-number
v-model="num"
:precision="2"
:step="0.1"
:max="2"
:min="0"
style="width: 100px"
size="mini"
controls-position="right"
@change="numberChange"
/>
倍
</div>
<div>
<el-tooltip :content="'点击开启或关闭移动模式'" effect="dark" placement="top">
<el-button
:type="isRank ? 'primary' : ''"
icon="el-icon-rank"
circle
@click="rankFn"
/>
</el-tooltip>
</div>
<div>
<el-button
icon="el-icon-refresh"
circle
@click="refresh"
/>
</div>
</div>
<div ref="refresh" class="mind">
<vMindRowTree
:is-rank="isRank"
:list="list"
:read-only="readOnly"
:child-component="component"
:is-show-title="isShowTitle"
:can-add-or-del="canAddOrDel"
:style="'transform: scale(' + num + ')'"
v-on="$listeners"/>
</div>
</div>
</template>
<script>
import vMindRowTree from '@/components/VmindRowTree/index'
export default {
name: 'VMindRowTreeWrap',
components: { vMindRowTree },
props: {
listCache: {
type: Array,
default: () => []
},
childComponent: {
type: Object,
required: true
},
isShowTitle: {
type: Boolean,
default: false
},
readOnly: {
type: Boolean,
default: false
},
canAddOrDel: {
type: Boolean,
default: false
}
},
data() {
return {
component: {},
isRank: false,
loading: false,
list: [],
num: 1
}
},
computed: {},
watch: {
num(newVal, oldVal) {
console.log(newVal, oldVal)
if (newVal < oldVal && newVal <= 0.5) {
this.num = 0.5
}
},
listCache: {
handler(n, o) {
if (n && n.length) {
this.init()
console.log('组件初始化', this.listCache)
}
},
immediate: true,
deep: true
}
},
created() {},
mounted() {
},
methods: {
rankFn() {
this.isRank = !this.isRank
},
numberChange() {
console.log(' this.num--', this.num)
},
init() {
this.loading = true
const { listCache, childComponent } = this
this.list = JSON.parse(JSON.stringify(listCache))
this.component = childComponent
this.loading = false
},
refresh() {
this.init()
}
}
}
</script>
<style scoped lang="scss">
.vMind-warp{
width: 100%;
height: 100%;
position: relative;
.header{
position: absolute;
top: 0;
left: 0;
display: inline-block;
align-items: center;
z-index: 2;
height: 40px;
line-height: 40px;
background-color: #fff;
& > div {
display: inline-block;
margin-right: 20px;
}
}
.mind {
height: calc(100% - 40px);
width: 100%;
position: absolute;
user-select: none;
background-color: #fff;
}
}
</style>
使用
<VMindRowTree
ref="vMindRef"
:child-component="component"
:list-cache="vMindData"
:is-show-title="true"
:read-only="false"
@spreadOrFoldChild="spreadOrFoldChild"
@vMindNodeSetClick="vMindNodeSetClick"
/>
label
说明
:child-component="component"是必传的,为每个节点的内容组件
:list-cache="vMindData"数据格式形如下,id唯一值,label显示值,isShow是否显示节点,parentId当前节点上级id,这些参数是必须的
vMindData: [{
label: '根节点',
id: 1,
isShow: true,
parentId: null,
salesMetricsCompletions: [{ name: '张三', age: 16 }],
children: [
{ label: '1-1节点', id: 2, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
children: [{ label: '1-1-1节点', id: 6, isShow: true, parentId: 2, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [
{ label: '1-1-1-1节点', id: 10, isShow: true, parentId: 6, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] },
{ label: '1-1-1-2节点', id: 11, isShow: true, parentId: 6, salesMetricsCompletions: [{ name: '张三', age: 16 }],
children: [{ label: '1-1-1-2-1节点', id: 12, isShow: true, parentId: 11, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-1-1-2-2节点', id: 15, isShow: true, parentId: 11, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }] }]
}, { label: '1-1-2节点', id: 9, isShow: true, parentId: 2, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
},
{ label: '1-2节点', id: 3, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
children: [{ label: '1-2-1节点', id: 7, isShow: true, parentId: 3, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-2-2节点', id: 13, isShow: true, parentId: 3, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }] },
{ label: '1-3节点', id: 4, level: 1, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
children: [{ label: '1-3-1节点', id: 8, isShow: true, parentId: 4, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-3-2节点', id: 14, isShow: true, parentId: 4, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
},
{ label: '1-4节点', id: 5, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
}]
- 传出的方法
-
- 节点展开和折叠方法
spreadOrFoldChild({node:Object,type:String},callBack)
node为某个节点数据
type有spread展开和fold折叠
-
- 添加或删除节点方法
vMindNodeSetClick({node:Object,type:String})
node为某个节点数据
type有addNode展开和delNode折叠