VUE 饿了么树形控件添加增删改功能

本贴最后更新于 2227 天前,其中的信息可能已经时移世改

在原文档中有个案例是有新增和删除功能,但是后来发现其修改的数据并不能直接影响到树形数据,所以采用了 render-content 的 API 重新写了个组件。
写个开发的步骤,所以文章比较长 emmm

大致效果如图:


1.省市 API

在网上复制了个省市的 list,有两个属性是新增的

  • isEdit :控制编辑状态
  • maxexpandId :为现下 id 的最大值
export default{
    maxexpandId: 95,
    treelist: [{  
        id: 1,  
        name: "北京市",  
        ProSort: 1,  
        remark: "直辖市",
        pid: '',
        isEdit: false,
        children: [{
            id: 35,
            name: "朝阳区",
            pid: 1,
            remark: '',
            isEdit: false,
            children: []
        }]
    }{...}]
}

2.el-tree Component 基本

咱们一步步来,先写个饿了么的组件

<template>
    <el-tree ref="expandMenuList" class="expand-tree"
        v-if="isLoadingTree"
        :data="setTree"
        node-key="id"
        highlight-current
        :props="defaultProps"
        :expand-on-click-node="false"
        :render-content="renderContent"
        :default-expanded-keys="defaultExpandKeys"></el-tree>
</template>
<!--
* highlight-current :为了点击时节点高亮
* expand-on-click-node : 只能箭头控制树形的展开收缩
* render-content : 节点渲染方式
* default-expanded-keys :默认展开节点
-->

同时引入 API 和节点渲染的组件

import TreeRender from '@/components/tree_render'
import api from '@/resource/api'

然后搭建好基础

data(){
  return{
    maxexpandId: api.maxexpandId,//新增节点开始id
    non_maxexpandId: api.maxexpandId,//新增节点开始id(不更改)
    isLoadingTree: false,//是否加载节点树
    setTree: api.treelist,//节点树数据
    defaultProps: {
      children: 'children',
      label: 'name'
    },
    defaultExpandKeys: [],//默认展开节点列表
  }
},

添加个渲染的 method

methods: {
    renderContent(h,{node,data,store}){
      let that = this;//指向vue
      return h(TreeRender,{
        props: {
          DATA: data,//节点数据
          NODE: node,//节点内容
          STORE: store,//完整树形内容
        },
        on: {//绑定方法
          nodeAdd: ((s,d,n) => that.handleAdd(s,d,n)),
          nodeEdit: ((s,d,n) => that.handleEdit(s,d,n)),
          nodeDel: ((s,d,n) => that.handleDelete(s,d,n))
        }
      });
    },
    handleAdd(s,d,n){//增加节点
      console.log(s,d,n)
    },
    handleEdit(s,d,n){//编辑节点
      console.log(s,d,n)
    },
    handleDelete(s,d,n){//删除节点
      console.log(s,d,n)
    }
}

3.tree_render Component 基本

渲染组件:

<template>
    <span class="tree-expand">
        <span class="tree-label">
            <span>{{DATA.name}}</span>
        </span>
        <span class="tree-btn">
            <i class="el-icon-plus" @click.stop="nodeAdd(STORE,DATA,NODE)"></i>
            <i class="el-icon-edit" @click.stop="nodeEdit(STORE,DATA,NODE)"></i>
            <i class="el-icon-delete" @click.stop="nodeDel(STORE,DATA,NODE)"></i>
        </span>
    </span>
</template>

添加好几个按钮(element-ui 自带 icon:地址)对应的方法:

export default{
    props: ['NODE', 'DATA', 'STORE'],
    methods: {
      nodeAdd(s,d,n){//新增
        this.$emit('nodeAdd',s,d,n)
      },
      nodeEdit(s,d,n){//编辑
        this.$emit('nodeEdit',s,d,n)
      },
      nodeDel(s,d,n){//删除
        this.$emit('nodeDel',s,d,n)
      }
    }
}

4.改

我们用 isEdit 来切换 inputspan 的显示状态,首先加个 input:

<!-- tree_render component -->
<template>
    <span class="tree-expand">
        <span class="tree-label" v-if="DATA.isEdit">
            <el-input class="edit" size="mini"
            :ref="'treeInput'+DATA.id"
            v-model="DATA.name"></el-input>
        </span>
        <template v-else>
            <span class="tree-label">
                <span>{{DATA.name}}</span>
            </span>
            <span class="tree-btn" v-show="!DATA.isEdit">
                <i class="el-icon-plus" @click.stop="nodeAdd(STORE,DATA,NODE)"></i>
                <i class="el-icon-edit" @click.stop="nodeEdit(STORE,DATA,NODE)"></i>
                <i class="el-icon-delete" @click.stop="nodeDel(STORE,DATA,NODE)"></i>
            </span>
        </template>
    </span>
</template>

编辑的时候按钮同时消失,那么什么时候编辑完成呢?

  • 编辑完按 enter 键=》监听 input 的 enter 输入
  • 点击其他节点=》input 失焦-blur=》编辑时自动聚焦-focus
  • 点击当前节点范围

当以上三点发生一项,节点对应的 data 都要 isEdit = false;

  1. enter 键

    <!-- tree_render component -->
    <el-input @keyup.enter.native="nodeEditPass(STORE,DATA,NODE)"></el-input>
    

    添加方法:

    //tree_render component
    methods: {
        nodeEditPass(s,d,n){
            d.isEdit = false;
        }
    }
    
  2. focus or blur

    <!-- tree_render component -->
    <el-input @blur="nodeEditPass(STORE,DATA,NODE)"></el-input>
    

    后来发现第一次编辑时能让 input 聚焦,点击第二个 input 就不起作用了,加了 autofocus 属性也同样如此。所以我们要在点击编辑 icon 的时候,用原生的 input autofocus
    修改方法:

    //tree_render component
    nodeEdit(s,d,n){//编辑
      d.isEdit = true;
      this.$nextTick(() => {
        this.$refs['treeInput'+d.id].$refs.input.focus()
      })
      this.$emit('nodeEdit',s,d,n)
    }
    
  3. 当前节点点击
    采用 el-tree 已有的 API——node-click

    <!-- el-tree component -->
    <el-tree @node-click="handleNodeClick"></el-tree>
    

    添加 methods:

    //el-tree component
    methods: {
        handleNodeClick(d,n,s){//点击节点
          d.isEdit = false;//放弃编辑状态
        }
    }
    

    问题来了,如果在编辑状态下点击此节点也同样会影响 input,这就无法进入编辑,所以要阻止 input 事件冒泡

    <!-- tree_render component -->
    <el-input @click.stop.native="nodeEditFocus"></el-input>
    

    添加 methods:

    //tree_render component
    methods: {
        nodeEditFocus(){}
    }
    
  4. v-show 代替 v-if

    这里有个新的问题,当用户经常编辑修改,v-if 模板的开销更高,所以改用 v-show。而后者不支持 template 模板,所以要适当调整一下位置:

<template>
    <span class="tree-expand">
        <span class="tree-label" v-show="DATA.isEdit">
            <el-input class="edit" size="mini" autofocus
            v-model="DATA.name"
            :ref="'treeInput'+DATA.id"
            @click.stop.native="nodeEditFocus"
            @blur="nodeEditPass(STORE,DATA,NODE)"
            @keyup.enter.native="nodeEditPass(STORE,DATA,NODE)"></el-input>
        </span>
        <span v-show="!DATA.isEdit">
            <span>{{DATA.name}}</span>
        </span>
        <span class="tree-btn" v-show="!DATA.isEdit">
            <i class="el-icon-plus" @click.stop="nodeAdd(STORE,DATA,NODE)"></i>
            <i class="el-icon-edit" @click.stop="nodeEdit(STORE,DATA,NODE)"></i>
            <i class="el-icon-delete" @click.stop="nodeDel(STORE,DATA,NODE)"></i>
        </span>
    </span>
</template>

5.增

新增节点 =》添加一条数据

  1. 新增的同时展开父节点
  2. 是否考虑无限新增
//el-tree component
handleAdd(s,d,n){//增加节点
  console.log(s,d,n)
  if(n.level >=6){
    this.$message.error("最多只支持五级!")
    return false;
  }
  //添加数据
  d.children.push({
    id: ++this.maxexpandId,
    name: '新增节点',
    pid: d.id,
    isEdit: false,
    children: []
  });
  //展开节点
  if(!n.expanded){
    n.expanded = true;
  }
}

新增节点字体加粗 =》给节点添加一个 class =》 如何判断是否新增?
我们有一个参数 maxexpandId
tree_render 添加一个 prop

//el-tree component
renderContent(h,{node,data,store}){//加载节点
  let that = this;
  return h(TreeRender,{
    props: {
      ...
      maxexpandId: that.non_maxexpandId
    },
    on: {...}
  });
}

根据 id 判断:

//tree_render component
props: ['NODE', 'DATA', 'STORE', 'maxexpandId']
<!-- tree_render component -->
<span v-show="!DATA.isEdit" 
:class="[DATA.id > maxexpandId ? 'tree-new tree-label' : 'tree-label']"
:ref="'treeLabel'+DATA.id">
    <span>{{DATA.name}}</span>
</span>
.tree-expand .tree-label.tree-new{
    font-weight:600;
}

6.删

跟新增同义:删除节点 =》删除一条数据

  1. 新增节点直接删除
  2. 已有节点需提示再删除
  3. 已有子级节点不能删除
handleDelete(s,d,n){//删除节点
  console.log(s,d,n)
  let that = this;
  //有子级不删除
  if(d.children && d.children.length !== 0){
    this.$message.error("此节点有子级,不可删除!")
    return false;
  }else{
    //删除操作
    let delNode = () => {
      let list = n.parent.data.children || n.parent.data,
      //节点同级数据,顶级节点时无children
        _index = 99999;//要删除的index
      list.map((c,i) => {
        if(d.id == c.id){
          _index = i;
        }
      })
      let k = list.splice(_index,1);
      //console.log(_index,k)
      this.$message.success("删除成功!")
    }
    let isDel = () => {
      that.$confirm("是否删除此节点?","提示",{
        confirmButtonText: "确认",
        cancelButtonText: "取消",
        type: "warning"
      }).then(() => {
        delNode()//此处可通过ajax做删除操作
      }).catch(() => {
        return false;
      })
    }
    //新增节点直接删除,否则要通过请求数据删除
    d.id > this.non_maxexpandId ? delNode() : isDel()
  }
}

7.拓展

还有一些特别的需求,例如:

  1. 点击高亮的时候显示 icon

    .expand-tree .is-current>.el-tree-node__content .tree-btn,
    .expand-tree .el-tree-node__content:hover .tree-btn{
      display: inline-block;
    }
    
  2. 添加顶级节点
    添加按钮:

    <!-- el-tree component -->
    <el-button @click="handleAddTop">添加顶级节点</el-button>
    

    添加 methods:

    //el-tree component
    methods: {
      handleAddTop(){
        this.setTree.push({
          id: ++this.maxexpandId,
          name: '新增节点',
          pid: '',
          isEdit: false,
          children: []
        })
      }
    }
    
  3. 默认展开树形第一级

    //el-tree component
    mounted(){
      this.initExpand()
    },
    methods: {
      initExpand(){
        //isLoadingTree用意也是在此
        this.setTree.map((a) => {
          this.defaultExpandKeys.push(a.id)
        });
        this.isLoadingTree = true;
      },
    }
    
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    261 引用 • 662 回帖 • 2 关注
  • Tree
    35 引用 • 6 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...