基于vue的树形结构封装
主要功能:添加同级、添加子级、拖拽、双击名称进行修改
依赖:vue、element-ui、sass
git地址:https://github.com/rongyanping/vue-tree
封装 MyTree.vue
<template>
<div class="my_tree">
<div
class="brother"
v-for="(data, idx) of treeData"
:key="idx">
<div class="node"
@drop="drop($event,treeData,idx)"
@dragover="allowDrop($event,treeData,idx)">
<span @click="data.expand=!data.expand">
<i class="el-icon-caret-bottom" v-if="data.expand && data.type==='group'"></i>
<i class="el-icon-caret-right" v-if="!data.expand && data.type==='group'"></i>
</span>
<div :draggable="!data.editable"
:class="{'cname_child':true,'cname_child_edit':data.editable}"
:contentEditable="data.editable"
@dragstart="drag($event,treeData,idx)"
@dblclick="dbclickHandle($event,treeData,idx)"
@click="data.expand=!data.expand">
{{data.name}}
</div>
<button class="operation_btn" v-if=" data.type==='group'" @click="addBrother($event,data)">添加同级</button>
<button class="operation_btn" v-if=" data.type==='group'" @click="addChild($event,data)">添加子级</button>
<button class="operation_btn" v-if=" data.type==='group'" @click="deleteNode($event, data)">删除</button>
</div>
<div
class="children"
v-if="data.children && data.children.length" v-show="data.expand">
<my-tree
:treeData="data.children"
@addBrother="addBrother"
@addChild="addChild"
@deleteNode="deleteNode"
@dragBegin="drag"
@dragStop="drop"
@dbclickChangeName="dbclickChangeName">
</my-tree>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MyTree',
props: {
treeData: {
type: Array,
default: () => [{
id: 1,
name: '分组1',
expand: true,
type:'group',
children: [{
id: 2,
expand: true,
name: '相机1',
type:'camera',
}]
},
{
id: 3,
expand: true,
name: '分组2',
type:'group',
children: [
{
id: 5,
expand: true,
name: '分组2-1',
type:'group',
children: [{
id: 6,
expand: true,
name: '相机2-1',
type:'camera',
}]
}]
}]
},
},
methods: {
// 双击
dbclickHandle(event,data,index){
const _this = this;
this.$set(data[index],'editable',true);
let target = event.target;
setTimeout(function () {
target.focus();
let selection = getSelection();
// 判断选定对象范围是编辑框还是文本节点
if(selection.anchorNode.nodeName === '#text'){
// 如果是文本节点则先获取光标对象
let range = selection.getRangeAt(0);
// 获取光标对象的范围界定对象,一般就是textNode对象
let textNode = range.startContainer;
// 光标移动到到原来的位置加上新内容的长度 rangeStartOffset +
range.setStart(textNode, target.innerHTML.length);
// 光标开始和光标结束重叠
range.collapse(true);
// 清除选定对象的所有光标对象
selection.removeAllRanges();
// 插入新的光标对象
selection.addRange(range);
}
target.onblur = function () {
_this.$emit('dbclickChangeName',data,index,target.innerText);
// console.log('失焦---------',target.innerText)
}
},10);
},
// 传给父级新名称
dbclickChangeName(data,index,name){
this.$emit('dbclickChangeName',data,index,name);
// console.log('====',name)
},
// 开始拖拽
drag(event,data,index){
this.$emit('dragBegin',event,data,index);
},
// 拖拽结束
drop(event,data,index){
event.preventDefault();
this.$emit('dragStop',event,data,index);
// console.log('drop>>>>>',event,data,index);
},
// 在何处放置被拖动的数据
allowDrop(event,data,index){
event.preventDefault();
},
// 添加子级
addChild (event, data) {
this.$emit('addChild', event, data)
},
// 添加同级
addBrother (event, data) {
this.$emit('addBrother', event, data);
},
// 删除
deleteNode (event, data) {
this.$emit('deleteNode', event, data)
},
}
}
</script>
<style scoped lang="scss">
.my_tree{
.brother{
display: flex;
flex-direction: column;
padding-top: 20px;
.node{
display: flex;
align-items: center;
.cname_child{
border-style: none;
outline: none;
width: 100px;
}
.cname_child_edit{
border: 1px solid gray;
}
.operation_btn{
margin-left: 20px
}
}
.children {
position: relative;
margin-left: 20px;
}
}
}
input[disabled],input:disabled,input.disabled{
color: #333;
-webkit-text-fill-color:#333;
opacity: 1;
background-color:white;
}
</style>
调用MyTree.vue
<template>
<div class="tree">
<my-tree
@addBrother="addBrother"
@addChild="addChild"
@deleteNode="deleteNode"
:treeData="datas"
@dragBegin="drag"
@dragStop="drop"
@dbclickChangeName="dbclickChangeName">
</my-tree>
<div v-show="iptVisible" style="margin-top: 20px">
<span>分组名:</span>
<input type="text" v-model="groupName">
<button @click="submitGroup">提交</button>
</div>
<div v-show="iptVisible2" style="margin-top: 20px">
<span>子级名:</span>
<input type="text" v-model="groupName2">
类型:分组/相机
<input type="text" v-model="addType">
<button @click="submitChild">提交</button>
</div>
</div>
</template>
<script>
import MyTree from './MyTree'
export default {
name: 'tree3',
components: {
'my-tree': MyTree
},
data () {
return {
id: 100,
datas:[{
id: 1,
name: '分组1',
expand: true,
type:'group',
editable:false,
children: [{
id: 2,
expand: true,
name: '相机1',
type:'camera',
editable:false,
}]
},
{
id: 3,
expand: true,
name: '分组2',
type:'group',
editable:false,
children: [
{
id: 5,
expand: true,
name: '分组2-1',
type:'group',
editable:false,
children: [{
id: 6,
expand: true,
name: '相机2-1',
type:'camera',
editable:false,
},{
id: 7,
expand: true,
name: '相机2-2',
type:'camera',
editable:false,
}]
}]
}],
dragData:null,
dragEvent:null,
dragIndex:null,
cid:10,
groupName:'',
iptVisible:false,
brotherData:null,
brotherEvent:null,
addChildData:null,
addChildEvent:null,
iptVisible2:false,
groupName2:'',
addType:'group'
}
},
methods: {
/* 操作数据全部在本组件内部 */
// 接收新名称
dbclickChangeName(data,index,changeName){
// 发送请求:修改名称
this.$set(data[index],'name',changeName);
this.$set(data[index],'editable',false);
console.log('dbclick=====',changeName);
},
/* 拖拽 */
drag(event,data,index){
this.dragData = data;
this.dragEvent = event;
this.dragIndex = index;
// console.log('dragStart=====',this.dragData,index);
},
drop(event,data,index){
/* 拖拽只能是拖到某个分组内 成为该分组的子级 */
/* 松开鼠标---调取修改接口,成功后添加新数据,删除之前位置*/
const _this = this;
data[index].children.push(this.dragData[this.dragIndex]);
// 删除原有数据
setTimeout(function () {
console.log('dragOver=====',_this.dragEvent,_this.dragData);
_this.deleteNode(_this.dragEvent,_this.dragData[_this.dragIndex])
},10);
},
// 添加同级
addBrother (event,data) {
this.iptVisible = true;
this.id++;
this.brotherData = data;
this.brotherEvent = event;
console.log('addBrother>>>>',this.brotherData);
},
submitGroup(){
let parentData = this.getParentData(this.brotherEvent.target);
// console.log('parentData========',this.brotherEvent.target);
if (parentData) {
let index = parentData.indexOf(this.brotherData);
if (index !== -1) {
parentData.splice(index + 1, 0, {
id: this.id,
name: this.groupName,
expand: false,
type:'group',
children: []
})
}
}
},
// 添加子级
addChild (event, data) {
this.iptVisible2 = true;
this.cid++;
this.addChildData = data;
console.log(this.addChildData)
// // data 父级数据
// console.log('addchild===tree3==',data);
// if (!data.children) {
// this.$set(data, 'children', [])
// }
// data.children.push(this.newNode('camera'))
// console.log('data====',data)
},
submitChild(){
if(!this.addChildData.children){
this.$set(this.addChildData, 'children', [])
}
if(this.addType==='group'){
this.addChildData.children.push({
id: this.cid,
children:[],
expand: false,
name: this.groupName2,
type:'group',
})
}
else {
this.addChildData.children.push({
id: this.cid,
expand: true,
name: this.groupName2+this.cid,
type:'camera',
})
}
},
// 删除组
deleteNode (event, data) {
console.log('del====',event,data)
let parentData = this.getParentData(event.target);
if (parentData) {
let index = parentData.indexOf(data);
if (index !== -1) {
parentData.splice(index, 1)
}
}
},
newNode (type) {
let id = this.id++;
return {
id,
name: type==='group'?'新分组' + id:'新相机'+id,
expand: false,
children: [],
type:type
}
},
getParentData (node) {
while (node && node.tagName !== 'BODY') {
// console.log('node=====',node,node.__vue__)
if (node.__vue__ && node.__vue__.$options._componentTag === 'my-tree') {
return node.__vue__.treeData
}
node = node.parentNode
}
return null
}
}
}
</script>
<style lang="scss" scoped>
.tree{
padding: 50px 20px;
text-align: left;
}
.tree-title{
border-bottom: 1px solid gray;
padding-bottom: 10px;
margin-bottom: 10px;
}
</style>