vue 移动端实现tree结构数据选择 (可多选、可单选、可全选、可反选)

第一步 先创建组件目录结构 

 

第二步 封装组件

index.vue

<template>
  <div class="tree-select-box">
    <air-cell type="text" :label="label" :placeholder="placeholder" :downIcon="downIcon" v-model="text" @click="onSelect"></air-cell>
    <mt-popup v-model="showPopup" position="bottom" :closeOnClickModal="closeOnClickModal">
      <div class="popup-box">
        <div class="popup-header">
          <span @click="handleCancel">取消</span>
          <span class="title">{{ label }}</span>
          <span @click="handleConfirm">确认</span>
        </div>
        <div class="popup-content">
          <Tree ref="tree" :data="treeData"/>
        </div>
      </div>
    </mt-popup>
  </div>
</template>
<script>
import { MessageBox } from 'mint-ui'
import { filterMultipleData } from '@/utils/air-service/commonMethods.js'
import { mannagerModule, airServiceModule } from '@/service/api/commonApi.js'
import { getStorage } from 'hr-utils'
import AirCell from '@/components/air-service/common/search/cell.vue'
import Tree from './tree.vue'
export default {
  name: 'treeSelect',
  components: {
    AirCell,
    Tree
  },
  props: {
    label: {
      type: String,
      default: '所属部门'
    },
    downIcon: {
      type: Boolean,
      default: true
    },
    // 展示的文字内容
    text: {
      type: [String, Number],
      default: ''
    },
    value: {
      type: [String, Number],
      default: ''
    },
    // 默认是部门
    type: {
      type: String,
      default: ''
    },
    placeholder: {
      type: String,
      default: '请选择'
    },
    loading: {
      type: Boolean,
      default: false
    },
    data: {
      type: Array,
      default () {
        return []
      }
    },
    // 兼容不同字段的tree数据
    treeParams: {
      type: Object,
      default () {
        return {
          id: 'id',
          name: 'label',
          children: 'children',
          isLeaf: ''
        }
      }
    },
    // 是否展开子级
    showTree: {
      type: Boolean,
      default: false
    },
    // 处理单选和多选
    checkbox: {
      type: Boolean,
      default: false
    },
    // 是否可点击遮罩层关闭
    closeOnClickModal: {
      type: Boolean,
      required: false,
      default: function () {
        return true
      },
    },
    // 懒加载
    lazy: {
      type: Boolean,
      default: false,
    },
    // 判断值是否能为空
    require: {
      type: Boolean,
      default: false
    },
    // 判断是否全选
    checkAll: {
      type: Boolean,
      default: false
    }
  },
  provide() {
    return {
      treeParams: this.treeParams,
      showTree: this.showTree,
      checkbox: this.checkbox,
      type: this.type,
      lazy: this.lazy
    }
  },
  data () {
    return {
      showPopup: false,
      treeData: this.data,
      // 用户信息
      userinfo: getStorage('userinfo')
    }
  },
  created () {
    this.initData()
  },
  methods: {
    // 初始话
    async initData () {
      try {
        await this.getShowData()
        if (this.checkAll) {
          await this.$refs.tree.handleCheckAllChange(true)
          this.setUpData()
        }
      } catch (error) {
      }
    },
    // 选择内容
    onSelect () {
      this.showPopup = true
    },
    // 取消按钮
    handleCancel () {
      this.showPopup = false
      this.$emit('cancel')
    },
    // 确认按钮
    handleConfirm () {
      this.selectData = this.$refs.tree.getSelectData()
      if (this.require && !this.selectData.length) {
        MessageBox('温馨提示', '请选择数据')
        return
      }
      this.setUpData()
    },
    // 获取选中的数据并更新
    setUpData () {
      const { id, name } = this.treeParams
      this.selectData = this.$refs.tree.getSelectData()
      let selectId = []
      let selectName = []
      this.showPopup = false
      this.selectData.map(item => {
        selectId.push(item[id])
        selectName.push(item[name])
      })
      this.upData(selectName.join(','), selectId.join(','))
    },
    // 更新值
    upData (text, value) {
      this.$emit('update:text', text)
      this.$emit('update:value', value)
      this.$emit('change', { text: text, value: value })
      this.$nextTick(() => {
        this.$refs.tree.setSelectData(value.split(','))
      })
    },
    // 获取展示数据
    async getShowData () {
      try {
        const { id, name, isLeaf } = this.treeParams
        this.$emit('update:loading', true)
        // 部门
        if (this.type === 'department') {
          const res = await mannagerModule.getUserMDept()
          let deptData = res.data.deptList.map(item => {
            return {
              ...item,
              [isLeaf]: item[isLeaf] === '1' ? true : false
            }
          })
          this.treeData = filterMultipleData([...deptData] || [])
          // 部门单选设置默认值
          if (!this.checkbox) {
            let defaultData = deptData.find(item => item[id] === this.userinfo.pk_dept)
            this.upData(defaultData[name], defaultData[id])
          }
        }
        // 人员类别
        if (this.type === 'category') {
          const res = await airServiceModule.queryReferencePsn({
            refType: 'pk_psncl',
            test: 'test',
          })
          let list = res && res.data && res.data.body ? res.data.body : []
          list = list.slice(1) || [] // 第一条是目录,剔除掉
          this.treeData = list
        }
      } catch (error) {
        console.log(error)
      } finally {
      }
    }
  }
}
</script>
<style lang="less" scoped>
.popup-box {
  width: 100vw;
  height: 50vh;
  display: flex;
  flex-direction: column;
  .popup-header {
    height: 0.9rem;
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: space-between;
    padding: 0 0.3rem;
    border-bottom: 1px solid #f2f2f2;
    span {
      color: #b4c4d4;
    }
    span.title {
      color: #333333;
    }
  }
  .popup-content {
    flex-grow: 1;
    overflow: auto;
    padding: 0.3rem;
    box-sizing: border-box;
  }
}
</style>

tree.vue

<template>
  <div class="popup-select-tree">
    <div class="select-box" v-if="checkbox">
      <span @click="handleCheckAllChange(true)">全选</span>
      <span @click="handleCheckAllChange(false)">取消全选</span>
    </div>
    <tree-item
      v-for="node in treeData"
      :key="node[treeParams.id]"
      :node="node"
      :treeParams="treeParams"
      @selectNode="selectNode">
    </tree-item>
  </div>
</template>
<script>
import treeItem from './tree-item.vue'
export default {
  name: 'popup-select-tree',
  components: {
    treeItem
  },
  inject: ['treeParams', 'checkbox'],
  props: {
    data: {
      type: Array,
      default () {
        return []
      }
    }
  },
  watch: {
    data: {
      handler(newVal, oldVal) {
        this.treeData = newVal
      },
      deep: true,
    }
  },
  data () {
    return {
      treeData: this.data,
      // 模拟数据
      data2: [{
        id: 1,
        label: '一级 1',
        children: [{
          id: 4,
          label: '二级 1-1',
          children: [{
            id: 9,
            label: '三级 1-1-1'
          }, {
            id: 10,
            label: '三级 1-1-2'
          }]
        }]
      }, {
        id: 2,
        label: '一级 2',
        children: [{
          id: 5,
          label: '二级 2-1'
        }, {
          id: 6,
          label: '二级 2-2'
        }]
      }, {
        id: 3,
        label: '一级 3',
        children: [{
          id: 7,
          label: '二级 3-1'
        }, {
          id: 8,
          label: '二级 3-2',
          children: [{
            id: 11,
            label: '三级 3-2-1'
          }, {
            id: 12,
            label: '三级 3-2-2'
          }, {
            id: 13,
            label: '三级 3-2-3'
          }]
        }]
      }],
      // 存放选择的数据
      selects: [],
      // 用于记录单选上一次选中的节点,作清除处理
      oldNode: null
    }
  },
  methods: {
    // 仅支持父子级非严格关联情况,严格关联未开发
    selectNode (node, type = '') {
      const { children } = this.treeParams
      let nodeChildren = node[children]
      if (this.checkbox) { // 多选
        // 判断是否已选中该节点,有则删除、没有则添加
        if (node.checkbox && !type) {
          this.$set(node, 'checkbox', false) // 取消选中状态
        } else {
          if (!node.checkbox) {
            this.$set(node, 'checkbox', true) // 设置选中状态
          }
          // 选择父级节点默认将子节点也选中
          if (nodeChildren && nodeChildren.length) {
            nodeChildren.map(nodes => {
              this.selectNode(nodes, 'add')
            })
          }
        }
      } else { // 单选
        if (this.oldNode) {
          this.$set(this.oldNode, 'checkbox', false)
        }
        this.$set(node, 'checkbox', true)
      }
      this.oldNode = node
    },
    handleSelectData (data = this.treeData) {
      const { children } = this.treeParams
      data.map(item => {
        if (item.checkbox) {
          this.selects.push({...item})
        }
        if (item[children] && item[children].length) {
          this.handleSelectData(item[children])
        }
      })
    },
    // 获取选中状态的数据
    getSelectData () {
      this.selects = []
      this.handleSelectData()
      return this.selects
    },
    // 设置选中值
    setSelectData (ids, data = this.treeData) {
      const { id, children } = this.treeParams
      data.map(node => {
        let nodeChildren = node[children]
        this.$set(node, 'checkbox', false)
        if (ids.includes(node[id])) {
          this.oldNode = node
          this.$set(node, 'checkbox', true)
        }
        if (nodeChildren && nodeChildren.length) {
          this.setSelectData(ids, nodeChildren)
        }
      })
    },
    // 获取到data内所有的ids、labels,用来做全选、反选处理
    getAllKeyData (data = this.treeData, ids = [], labels = []) {
      const { id, name, children} = this.treeParams
      data.map(item => {
        labels.push(item[name])
        ids.push(item[id])
        if (item[children] && item[children].length) {
          this.getAllKeyData(item[children], ids, labels)
        }
      })
      return {
        labels,
        ids
      }
    },
    // 全选 和 反选
    handleCheckAllChange (bool) {
      if (bool) {
        const { ids } = this.getAllKeyData()
        this.setSelectData(ids)
      } else {
        this.setSelectData([])
      }
    }
  }
}
</script>
<style lang="less" scoped>
.select-box {
  margin-bottom: 15px;
  display: flex;
  span {
    padding: 2px 5px;
    color: #00BBBB;
    cursor: pointer;
    font-size: 14px;
    border: 1px solid #ddd;
    border-radius: 4px;
    margin-right: 10px;
    user-select:none;
  }
}
</style>

tree-item.vue

<template>
  <div class="tree-item-box">
    <div class="tree-item-list">
      <span class="arrow-box">
        <img
          v-if="(node[treeParams.children] && node[treeParams.children].length) || node[treeParams.isLeaf]"
          class="arrow"
          :class="{'arrow-down': show}"  
          :src="!laoding.require ? arrowIcon : loadingIcon"
          alt=""
          @click="showchildren(node)">
      </span>
      <span class="name" @click="selectNode(node)">{{ node[treeParams.name] }}</span>
      <img class="tick" :src="tickTrue" v-show="node.checkbox" alt="" @click="selectNode(node)">
    </div>
    <tree-item
      v-show="show"
      class="tree-children"
      v-for="item in node[treeParams.children]"
      :key="item[treeParams.id]"
      :node="item"
      @selectNode="selectNode"/>
  </div>
</template>
<script>
/*
 * showTree false为默认不展开子级 true默认展开子级
 * checkbox false为单选 true为多选
 */
import { mannagerModule } from '@/service/api/commonApi.js'
export default {
  name: 'tree-item',
  inject: ['treeParams', 'showTree', 'checkbox', 'type', 'lazy'],
  props: {
    node: {
      type: Object,
      default() {
        return {};
      }
    }
  },
  data () {
    return {
      laoding: {
        require: false
      },
      show: this.showTree,
      tick: require('../../../../../static/img/pages/airService/tick.png'),
      tickTrue: require('../../../../../static/img/pages/airService/tick-true.png'),
      arrowIcon: require('../../../../../static/img/pages/airService/arrow.png'),
      loadingIcon: require('../../../../../static/img/pages/airService/loading.gif'),
    }
  },
  methods: {
    // 显示或隐藏子级
    async showchildren (node) {
      try {
        console.log(node)
        await this.hanldeChildrenNode(node)
        this.show = !this.show
      } catch (error) {
        console.log(error)
      }
    },
    // 处理子节点数据
    async hanldeChildrenNode (node) {
      try {
        const { id, children, isLeaf } = this.treeParams
        let childrenNode = []
        // 懒加载情况下,更新node节点数据
        if (this.lazy && node[isLeaf] && node[children] && !node[children].length) {
          if (this.type === 'department') {
            this.laoding.require = true
            const res = await mannagerModule.getUserMDept({'pk_dept': node[id]})
            childrenNode = res.data.deptList.map(item => {
              return {
                ...item,
                [isLeaf]: item[isLeaf] === '1' ? true : false,
                [children]: []
              }
            })
          }
          this.updataChildrenNode(node, childrenNode)
        }
      } catch (error) {
        console.log(error)
      } finally {
        this.laoding.require = false
      }
    },
    // 更新子节点数据
    updataChildrenNode (node, childrenNode) {
      const { children, isLeaf } = this.treeParams
      node[children] = childrenNode
      this.$set(node, node[children], childrenNode)
      if (!childrenNode.length) {
        this.$set(node, isLeaf, false)
      }
    },
    // 选择树节点
    selectNode (node) {
      this.$emit('selectNode', node)
    }
  }
}
</script>
<style lang="less" scoped>
.tree-item-box {
  .tree-item-list {
    display: flex;
    align-items: center;
    height: 0.5rem;
    touch-action: none;
    .arrow-box {
      display: flex;
      align-items: center;
      width: 0.3rem;
      .arrow {
        width: 100%;
        &.arrow-down {
          transform: rotate(90deg);
        }
      }
    }
    .name {
      margin-left: 0.1rem;
      flex-grow: 1;
    }
    .tick {
      width: 0.5rem;
      margin-left: auto;
    }
  }
  .tree-children {
    margin-left: 0.2rem;
  }
}
</style>

 第三步引入组件并使用

 效果预览

 

posted @ 2021-09-06 15:24  爱放屁的菜鸟  阅读(6004)  评论(1编辑  收藏  举报