iView树型图Tree增加编辑、新建、删除以及虚线样式

前言

今天做数仓需要个树形图,想着本来UI用的就是iView,不想再引入别的插件了,但是iView的Tree组件和产品要求的有些不一样。

要求:要求前面要用加减号图标,不要箭头图标,要有同级虚线连接,需要可编辑、可添加、可删除节点;

但是Iview貌似没有现成的配置,只有个添加、删除大致的配置,如图(一);
因此在参考几篇的帖子之后,决定用iView Tree强大的 render 改造一番;最终效果图 如图(二)
图一
在这里插入图片描述

代码部分

<template>
  <div class="v-tree"> 
    <Tree :data="treeList" :render="renderContent" ></Tree>
  </div>
</template>
<script>
// import service from 'libs/service/api'
import { cloneDeep } from 'lodash-es'
export default {
  name: 'VTree',
  props: {
    name: String,
    bdId: [String, Number] // 数据库Id
  },
  data() {
    return {
      editState: false, // 编辑状态
      treeList: [
      {
        title: '深圳分公司',
        expand: true,
        // 单独给某个节点设置render,此render会优先Tree组件中的render(此组件可删除,只是为了展示一下,没有实际用处)
        render: (h, { root, node, data }) => { 
            return h('span', {  
              style: {
                display: 'inline-block',
                width: '100%'
              }
            }, [
              h('span', [
                  h('Icon', {
                      props: {
                        type: 'ios-folder'
                      },
                      style: {
                        marginRight: '8px',  
                      }
                  }),
                  h( "span" , data.title)
              ]), 
            ]);
        },
        children: [
            {
              title: '父-子',
              expand: true, 
              children: [
                  {
                    title: '子',
                    expand: true
                  },
                  {
                    title: '子2',
                    expand: true
                  }
              ]
            } 
        ]
      },
      {
        title: '深圳分公司',
        expand: true,
          children: [
            {
              title: '父-子',
              expand: true, 
              children: [
                {
                  title: '子',
                  expand: true
                },
                {
                  title: '子2',
                  expand: true
                }
              ]
            } 
          ]
        }
      ],
      buttonProps: {
        type: 'default',
        size: 'small',
      },
      // 输入框要修改的内容
      inputContent: '',
      // 修改前的TreeNode名称
      oldName: ''
    }
  },
  mounted() {
    // this.getTreeList()
  },
  methods: {
    // 树渲染逻辑
    renderContent (h, { root, node, data }) { 
      return h('span', {
        class: 'common-wrap',
        on:{
          click:() => {
            // 点击Tree节点触发
            if (!data.editState) {
              this.handleClickTreeNode(root, data, node)
            }
          }
        }
      }, [  
        h('span', [
          // 文件前面的文件夹图标
          h('Icon', {
            props: {
              type: `${data.children === undefined || data.children.length === 0 ? 'md-document' :'ios-folder'}`
            },
            style: {
              marginRight: '8px', 
            }
          }),
          h(`${ data.editState ? '' : 'span'}`, data.title),
          h(`${ data.editState ? 'input' : ''}`, 
            {
              class: 'edit-input',
              attrs:{
                value:`${ data.editState ? data.title : ''}`, 
                autofocus: 'true'
              },  
              style: {     
                // cursor: 'auto' ,
                // borderRadius: '3px',
                // border: '1px solid #e5e5e5',
                // color: '#515a6e'
              },
              on:{
                change: (event) => { 
                  this.inputContent = event.target.value 
                }
              }
            }
          )
        ]), 
        // 增删改按钮部分
        h(`${ data.editState ? '' : 'span'}`,
          {
            class: 'btnNone'
          },
          [
            // 操作按钮部分 
            // 编辑按钮
            h('Button', {
              props: {
                ...this.buttonProps,
                icon: 'md-color-filter'
              },
              on: {
                click: () => { this.editTree(data) }
              }
            }),
            // 添加按钮
            h('Button', {
              props: {
                ...this.buttonProps,
                icon: 'md-add'
              },
              on: {
                click: () => { this.append(data) }
              }
            }),
            // 删除按钮
            h('Button', {
              props: {
                ...this.buttonProps,
                icon: 'md-remove'
              },
              on: {
                click: () => { this.remove(root, node, data) }
              }
            })
          ]
        ),    
        // 确认/取消修改部分
        h(`${ data.editState ? 'span' : ''}`,
          { 
            style: { 
              marginLeft: '.5rem'
            }
          },
          [  
            // 确认按钮
            h('Button', {
              props: {
                ...this.buttonProps,
                icon: 'md-checkmark' 
              },
              style: {
                border: 0,
                background: 'rgba(0,0,0,0)',
                fontSize: '16px',
                outline: 'none'
              },
              on: {
                click: (event) => {  
                  this.confirmTheChange(data) 
                }
              }
            }),
            // 取消按钮
            h('Button', {
              props: {
                ...this.buttonProps,
                icon: 'md-close'
              },
              style: {
                border: '0',
                background: 'rgba(0,0,0,0)',
                fontSize: '16px',
                outline: 'none'
              },
              on: {
                click: () => { this.cancelChange(data) }
              }
            }) 
          ]
        ) 
      ])
    },
    /**
     * @description: 获取树列表(这部分可以换成自己的)
     * @param {*}
     * @return {*}
     */    
    async getTreeList() {
      let res = await service.getCategoryTreeList({
        name: this.name,
        id: this.bdId
      })
      if (res.data) {
        this.treeList = [...res.data]
        // 路由参数带有id,则置为选中状态
        if (this.bdId) {
          this.filterTableMater(this.bdId, this.treeList)
        }
      }
    },
    // 控制Tree当前状态函数
    setStates(data){
      let editState=data.editState
      if (editState) {
        this.$set(data, 'editState', false)
      } else {
        this.$set(data, 'editState', true)
      }  
    },
    // Tree修改按钮
    editTree(data){
      event.stopPropagation()  
      this.inputContent=data.title 
      this.oldName=data.title
      this.setStates(data)  
    },
    // 添加按钮
    append (data) {
      event.stopPropagation()
      const children = data.children || []
      children.push({
        title: '新建节点',
        expand: true
      })
      this.$set(data, 'children', children)
    },
    // 删除按钮
    remove (root, node, data) {
      event.stopPropagation()
      this.$Modal.confirm({
        title: '提示',
        content: `您确定删除 “${data.title}” 吗?`,
        onOk: () => {
          const parentKey = root.find(el => el === node).parent
          const parent = root.find(el => el.nodeKey === parentKey).node
          const index = parent.children.indexOf(data)
          parent.children.splice(index, 1)
          this.$Message.info('删除成功')
        },
        onCancel: () => {
          this.$Message.info('取消')
        }
      })
    }, 
    // 确认修改树节点
    confirmTheChange(data) {   
      if (!this.inputContent) {
        this.$Notice.warning({
          title: '当前输入有误', 
        })
      } else { 
        if(this.oldName !== this.inputContent){  
          this.$Modal.confirm({
            title: '提示',
            content: `您确定将  “${this.oldName}”  重命名为 “ ${this.inputContent} ” 吗?`,
            onOk: () => {
              data.title=this.inputContent 
              this.$Message.info('修改成功')
            },
            onCancel: () => {
              this.$Message.info('取消')
            }
          })
          this.setStates(data)
        } else{
          this.setStates(data)
        }
      }
    },
    // 取消修改树节点
    cancelChange(data) { 
      this.$Notice.info({
        title: '取消修改',
      })
      this.setStates(data)
    },
    // 点击Tree节点触发
    handleClickTreeNode(root, data, node) { 
      if (this.bdId) {
      	// 这里是因为 filterTableMater的时候this.treeList不是响应式的,倒是选中之后再次点击别的不去除样式
        this.treeList = cloneDeep(this.treeList) 
      }
      this.$emit('handleTreeNode',node) // 触发父级组件拉去新列表
    },
    // js 递归遍历查找对象数组的某一个属性
    filterTableMater(code, arr) {
      for (const item of arr) {
        if (item.id === code) {
          item.selected = true
        }
        if (item.children && item.children.length) {
          this.filterTableMater(code, item.children)
        }
      }
    }
  }
}
</script>

// 大部分改写都在样式这里,如果有必要请仔细看一下
<style lang="less">
// 编辑部分
.btnNone {
  display: none;
  margin-left: 15px;
  button {
    margin-right: 8px;
    width: 24px;
    line-height: 0;
    height: 24px;
  }
}
.common-wrap {
  display: inline-block;
  line-height: 25px;
  width: 100%;
  cursor: pointer;
  span:not(.btnNone) {
    display: inline-block;
  }
  .edit-input {
    width: 90px;
    cursor: auto;
    border-radius: 3px;
    border: 1px solid #e5e5e5;
    color: #515a6e;
    padding-left: 10px;
    outline: none;
  }
  .edit-input:focus {
    border: 1px solid #2d8cf0 !important;
  }
}
.common-wrap:hover .btnNone {
  display: inline-block;
}
.common-wrap:hover {
  color: #275cd4;
}

// 换箭头到加号
.ivu-tree ul li {
  list-style: none;
  padding: 0;
  white-space: nowrap;
  outline: none;
}
.ivu-tree .ivu-tree-arrow {
  width: 15px;
}
.ivu-tree .ivu-icon { // 禁止旋转
  -webkit-transform: rotate(0deg);
  transform: rotate(0deg);
}
.ivu-tree .ivu-icon-ios-arrow-forward::before { // 改变tree默认的三角箭头
  content: "\F32F"; // 采用ui库中自带的icon图标
  width: 15px;
  height: 15px;
  display: block;
}
.ivu-tree .ivu-tree-arrow-open .ivu-icon-ios-arrow-forward::before { // 改变tree默认的三角箭头
  content: "\F417"; // 采用ui库中自带的icon图标
  display: block;
  width: 15px;
  height: 15px;
}
// 虚线
.ivu-tree {
  .ivu-tree-children {
    position: relative;
    // padding-left: 16px; // 缩进量
  }
  .ivu-tree-children::before {
    content: "";
    height: 100%;
    width: 1px;
    position: absolute;
    left: 7px;
    top: -12px;
    border-width: 1px;
    border-left: 1px dashed #ddd;
  }
  // 当前层最后一个节点的竖线高度固定
  .ivu-tree-children:last-child::before {
    height: 20px; // 可以自己调节到合适数值
  }
  // 横线
  .ivu-tree-children::after {
    content: "";
    width: 11px;
    height: 20px;
    position: absolute;
    left: 9px;
    top: 12px;
    border-width: 1px;
    border-top: 1px dashed #ddd;
  }
  // 去掉最顶层的虚线,放最下面样式才不会被上面的覆盖了
  & > .ivu-tree-children::after {
    border-top: none;
  }
  & > .ivu-tree-children::before {
    height: 100%;
    width: 1px;
    left: 7px;
    top: 13px;
    border-left: 1px dashed #ddd;
  }
  & > .ivu-tree-children:last-of-type::before {
    border-left: none;
  }
}
</style>

到这里就基本完成了,希望对有这部分需求的盆友有用处,加油💪!!!

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
要实现树形结构并带有搜索功能的 `el-select`,可以使用 `el-tree` 和 `el-cascader` 这两个组件进行组合。 首先,使用 `el-tree` 组件渲染树形结构。然后,将 `el-tree` 的数据源作为 `el-cascader` 的选项数据源,这样就可以实现级联选择。最后,为 `el-select` 组件添加搜索功能。 具体实现步骤如下: 1. 引入 `el-tree`、`el-cascader` 和 `el-select` 组件: ```html <template> <div> <el-select v-model="selected" placeholder="请选择" filterable @clear="handleClear"> <el-tree :data="treeData" :props="treeProps" :expand-on-click-node="false" :highlight-current="true" :default-expand-all="true" :current-node-key="currentNodeKey" @node-click="handleNodeClick" @current-change="handleCurrentChange" /> <template #empty> <el-empty :description="'无匹配数据'" /> </template> </el-select> </div> </template> <script> import { ElTree, ElCascader, ElSelect, ElEmpty } from 'element-plus'; import { reactive, toRefs } from 'vue'; export default { components: { ElTree, ElCascader, ElSelect, ElEmpty, }, setup() { const state = reactive({ treeData: [ { label: '一级 1', id: 1, children: [ { label: '二级 1-1', id: 11, children: [ { label: '三级 1-1-1', id: 111, }, { label: '三级 1-1-2', id: 112, }, ], }, { label: '二级 1-2', id: 12, }, ], }, { label: '一级 2', id: 2, }, ], treeProps: { children: 'children', label: 'label', }, currentNodeKey: '', selected: [], }); const handleNodeClick = (data) => { state.currentNodeKey = data.id; }; const handleCurrentChange = (nodes) => { state.selected = nodes.map((node) => node.label); }; const handleClear = () => { state.currentNodeKey = ''; state.selected = []; }; return { ...toRefs(state), handleNodeClick, handleCurrentChange, handleClear, }; }, }; </script> ``` 2. 在 `el-select` 中嵌套 `el-tree` 组件,并设置 `filterable` 属性开启搜索功能。同时,为 `el-select` 组件添加 `@clear` 事件,清空选中项和当前节点。 3. 在 `el-tree` 组件中绑定数据源 `treeData` 和属性设置。其中,`treeProps` 用于指定数据源中的节点属性,`currentNodeKey` 用于记录当前选中的节点。 4. 在 `el-select` 组件的 `empty` 模板中添加一个提示信息,用于提示搜索结果为空的情况。 至此,我们就完成了一个带有树形结构和搜索功能的 `el-select` 组件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值