纯手写一个移动端tree组件

1,194 阅读2分钟

事情是这样的产品经理需要实现一个类似于tree结构的人员选择功能,由于数据量过大,需要一级一级加载数据。效果如下:第一层加载完成以后,点击再加载它的子集,以此类推,直到人员

image.png

找了一圈移动端的UI组件发现没有合适的。element-ui倒是有tree树形组件,总不能为了实现个移动端功能安装个PC端的依赖吧(事实上之前还真这么干过,为了实现需求脸都不要了)

QQ图片20230527031820.gif 既然找不到合适的那就撸起袖子自己搞一个,毕竟咱的人生信条只有一个,那就是

IMG_0733(20230527-033241).JPG

既然要干,那就得干好,写个公用组件,方便将来自己使用。于是乎开整,开发组件三要素上马 (props参数,slot定制插槽,event自定义事件)

第一:中国人办事向来是谋定而后动第一步当然是实现思路喽

首先是列表,少不了循环,其次是层级不定,异步加载数据,递归组件肯定是跑不了......。 额,编不下去了,废话少说,还是直接上代码比较实在

存放文件目录如下:

image.png

preRecursive.vue文件内容如下

<template>
  <div style="height: 100%;">
    <div class="opearte-box">
      <van-button type="default" size="small" @click="closePop">取消</van-button>
      <van-button type="primary" color="#1B82D1" size="small" @click="businessUserQd">确定</van-button>
    </div>
    <div class="user-select-area">
      <div class="search-box1">
        <i></i>
        <input type="text" v-model="searchPersonText" placeholder="搜索人员">
        <button type="button" @click="searchPerson">查询</button>
      </div>
      <div class="result-box">
        <Recursion :list="personList" :multiple="multiple" @childEvent="getNextLevelData" />
      </div>
    </div>
  </div>
</template>
<script>
/* eslint-disable */
import Recursion from './recursive.vue'
export default {
  name: 'pre-recursive',
  props: {
    multiple: {
      type: Boolean,
      default: false
    },
    getUserList: {
      type: Function
    },
    getSearchUserList: {
      type: Function
    }
  },
  data () {
    return {
      searchPersonText: '',
      personList: [],
      curItem: null,
      curItemList: []
    }
  },
  components: {
    Recursion
  },
  mounted () {
    const _this = this
    _this.getUserList({}).then(res => {
      _this.personList = res
    })
  },
  methods: {
    businessUserQd () {
      const _this = this
      let vals = _this.multiple ? _this.curItemList : _this.curItem
      _this.$emit('childEvent', vals)
      _this.closePop()
    },
    closePop () {
      this.$emit('closePop')
    },
    // 将搜索返回的所有层级都展开
    showAllLevel (arr) {
      for (let i = 0; i < arr.length; i++) {
        if (arr[i].type === 1) {
          arr[i].isDown = true
        } else if (arr[i].type === 2) {
          arr[i].isCheck = false
        }
        if (arr[i].children && arr[i].children.length > 0) {
          this.showAllLevel(arr[i].children)
        }
      }
    },
    getNextLevelData (item) {
      const _this = this
      if (item.type === 1) {
        // 箭头转换的样式
        _this.closeArrow(item, _this.personList)
        // 首次点击加载数据
        if (item.children.length === 0) {
          _this.getUserList(item).then(res => {
            _this.recursiveData(item.id, _this.personList, res)
          })
        }
      } else if (item.type === 2) {
        // 区分单选和多选
        if (_this.multiple) {
          if (!item.isCheck) {
            _this.curItemList.push(item)
          } else {
            _this.curItemList = _this.curItemList.filter( item1 => {
              return item.id !== item1.id
            })
          }
          _this.changeSelect1(item, _this.personList)
        } else {
          _this.curItem = item.isCheck ? null : item
          _this.changeSelect(item, _this.personList)
        }
      }
    },
    // 收起当前箭头
    closeArrow (item, arr) {
      const _this = this
      if (arr && arr.length > 0) {
        let ids = []
        let curIndex = 0
        for (let i = 0; i < arr.length; i++) {
          ids.push(arr[i].id)
          if (item.id === arr[i].id) {
            curIndex = i
          }
        }
        if (ids.includes(item.id)) {
          arr[curIndex].isDown = !item.isDown
        } else {
          for (let i = 0; i < arr.length; i++) {
            _this.closeArrow(item, arr[i].children)
          }
        }
      }
    },
    // 修改选中状态(单选)
    changeSelect (item, arr) {
      const _this = this
      if (arr && arr.length > 0) {
        for (let i = 0; i < arr.length; i++) {
          if (arr[i].id === item.id) {
            arr[i].isCheck = !item.isCheck
          } else {
            arr[i].isCheck = false
          }
          if (arr[i].children && arr[i].children.length > 0) {
            _this.changeSelect(item, arr[i].children)
          }
        }
      }
    },
    // 修改选中状态(多选)
    changeSelect1 (item, arr) {
      const _this = this
      if (arr && arr.length > 0) {
        for (let i = 0; i < arr.length; i++) {
          if (arr[i].id === item.id) {
            arr[i].isCheck = !item.isCheck
          }
          if (arr[i].children && arr[i].children.length > 0) {
            _this.changeSelect1(item, arr[i].children)
          }
        }
      }
    },
    // 递归返回的列表重新整合数据
    recursiveData (id, arr, arr1) {
      const _this = this
      if (arr && arr.length > 0) {
        let ids = []
        let curIndex = 0
        for (let i = 0; i < arr.length; i++) {
          ids.push(arr[i].id)
          if (id === arr[i].id) {
            curIndex = i
          }
        }
        // 可替代用数组的some()方法检测当前数组是否有想要的id
        /*
        let isHaveId = arr.some((item, index, array) => item.id === id)
        */
        if (ids.includes(id)) {
          arr[curIndex].children = arr1
        } else {
          for (let i = 0; i < arr.length; i++) {
            _this.recursiveData(id, arr[i].children, arr1)
          }
        }
      }
    }
  }
}
</script>
<style lang="less" scope>
.user-select-area {
  padding: 0 16px;
  height: calc(100% - 40px);
}
.opearte-box {
  display: flex;
  justify-content: space-between;
  padding: 4px 16px;
}
.result-box {
  margin-top: 10px;
  height: calc(100% - 46px);
  overflow: auto;
}
.search-box1 {
  flex: 1;
  display: flex;
  align-items: center;
  background: #F7F7F7 ;
  border-radius: 18px;
  padding-left: 10px;
  height: 36px;
  i {
    display: inline-block;
    width: 16px;
    height: 16px;
    background: url("./img/icon_baseSearch@2x.png") 0 0 no-repeat;
    background-size: contain;
  }
  input {
    width: calc(100% - 60px);
    background: #F7F7F7;
    margin-left: 5px;
    border: none;
  }
  button {
    width: 50px;
    height: 30px;
    border-radius: 18px;
    color: #fff;
    background: #1B82D1;
    border: none;
  }
}
</style>

递归组件recursive.vue内容如下:

<template>
  <div>
    <div class="item">
      <ul>
        <li v-for="(item, index) in list" :key="index">
          <div class="title-style-ro" @click="getNextLevel(item)">
            <i v-if="item.type === 1" class="arrow-icon" :class="item.isDown ? 'arrow-down-icon' : 'arrow-right-icon'"></i>
            <template v-if="!multiple">
              <i v-if="item.type === 2" class="radio-icon" :class="item.isCheck ? 'radio-yes-icon' : 'radio-no-icon'"></i>
            </template>
            <template v-else>
              <i v-if="item.type === 2" class="radio-icon" :class="item.isCheck ? 'multiple-yes-icon' : 'multiple-no-icon'"></i>
            </template>
            <span>{{ item.type === 1 ? item.name : item.nameMobile }}</span>
          </div>
          <ul style="padding-left: 10px" v-show="item.children && item.type === 1 && item.isDown">
            <li>
              <index-chird :multiple="multiple" :list="item.children" @childEvent="getNextLevel"></index-chird>
            </li>
          </ul>
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
/* eslint-disable */
export default {
  name: 'index-chird',
  props: {
    list: Array,
    multiple: Boolean
  },
  data () {
    return {
      // list: []
    }
  },
  methods: {
    getNextLevel (item) {
      this.$emit('childEvent', item)
    }
  },
  watch: {
    list (newData) {
      this.list = newData
    }
  }
}
</script>
<style lang="less" scope>
.title-style-ro {
  height: 40px;
  background: #FFFFFF;
  box-shadow: inset 0px -1px 0px 0px #EEEEEE;
  display: flex;
  align-items: center;
  i {
    display: inline-block;
    margin-right: 7px;
  }
  .arrow-icon {
    width: 16px;
    height: 16px;
  }
  .arrow-right-icon {
    background: url("./img/caret-right@2x.png") 0 0 no-repeat;
    background-size: contain;
  }
  .arrow-down-icon {
    background: url("./img/caret-down-grey@2x.png") 0 0 no-repeat;
    background-size: contain;
  }
  .radio-icon {
    width: 12px;
    height: 12px;
  }
  .radio-yes-icon {
    background: url("./img/icon_round_checked@2x.png") 0 0 no-repeat;
    background-size: contain;
  }
  .radio-no-icon {
    background: url("./img/icon_round_normal@2x.png") 0 0 no-repeat;
    background-size: contain;
  }
  .multiple-yes-icon {
    background: url("./img/icon-checks-on.png") 0 0 no-repeat;
    background-size: contain;
  }
  .multiple-no-icon {
    background: url("./img/icon-checks.png") 0 0 no-repeat;
    background-size: contain;
  }
}
</style>

接下来就是如何调用该组件

<template>
  <div class="example-area">
    <div class="title title12">
      <div>树状人员选择(默认单选)</div>
    </div>
    <div>
      <van-button type="primary" color="#1B82D1" size="small" @click="showUserTree">人员选择</van-button>
    </div>
    <div class="show-list">
      选中名单:<span>{{ personListStr }}</span>
    </div>
    <van-popup v-model="businessUserShow" position="bottom" :style="{ height: '70%' }">
      <template v-if="businessUserShow">
        <pre-recursive :multiple="multiple" :getUserList="getUserList" :getSearchUserList="getSearchUserList" @closePop="businessUserShow = false" @childEvent="getUserInfo"></pre-recursive>
      </template>
    </van-popup>
  </div>
</template>

<script>
import preRecursive from './recursive/preRecursive.vue'
export default {
  name: 'example-area',
  data () {
    return {
      businessUserShow: false,
      // 人员列表参数
      multiple: false, // 人员列表参数,单选还是多选false为单选
      personListStr: '',
      // 人员选择数据要求
      personListVal: [
        {
          children: [],
          id: 112,
          loginName: '',
          lvl: 3,
          mobile: '',
          name: '杭州西湖分局',
          nameMobile: '',
          type: 1 // 是否是最后可选一级(1有子集可下拽)
        },
        {
          children: [],
          id: 33445043,
          loginName: '18888888888',
          lvl: 2,
          mobile: '18888888888',
          name: '萧峰',
          nameMobile: '萧峰(18888888888)',
          type: 2 // 是否是最后可选一级(2无子集不可下拽)
        },
        {
          children: [],
          id: 111,
          loginName: '',
          lvl: 3,
          mobile: '',
          name: '杭州上城一分局',
          nameMobile: '',
          type: 1
        },
        {
          children: [],
          id: 448,
          loginName: '',
          lvl: 3,
          mobile: '',
          name: '杭州钱塘分局',
          nameMobile: '',
          type: 1
        },
        {
          children: [],
          id: 3813808,
          loginName: '17777777777',
          lvl: 2,
          mobile: '17777777777',
          name: '虚竹',
          nameMobile: '虚竹(17777777777)',
          type: 2
        },
        {
          children: [],
          id: 3813809,
          loginName: '16666666666',
          lvl: 2,
          mobile: '16666666666',
          name: '段誉',
          nameMobile: '段誉(16666666666)',
          type: 2
        }
      ],
      // 人员搜索模拟数据
      searchPersonListVal: [
        {
          children:[
            {
              children:[
                {
                  children: [
                    {
                      children: [],
                      id: 3971617,
                      loginName: '',
                      lvl: 4,
                      mobile: "15555555555",
                      name: '周晨',
                      nameMobile: '周晨(15555555555)',
                      type: 2
                    }
                  ],
                  id: 133875,
                  loginName: '',
                  lvl: 4,
                  mobile: '',
                  name: '余杭未来科技城支局',
                  nameMobile: '',
                  type: 1
                }
              ],
              id: 13,
              loginName: '',
              lvl: 3,
              mobile: '',
              name: '杭州余杭分公司',
              nameMobile: '',
              type: 1
            },
            {
              children: [
                {
                  children: [
                    {
                      children: [],
                      id: 32992687,
                      loginName: '',
                      lvl: 4,
                      mobile: '13355555555',
                      name: '周晨霞',
                      nameMobile: '周晨霞(13355555555)',
                      type: 2
                    }
                  ],
                  id: 656,
                  loginName: '',
                  lvl: 4,
                  mobile: '',
                  name: '淳安汾口支局',
                  nameMobile: '',
                  type: 1
                }
              ],
              id: 640,
              loginName: '',
              lvl: 3,
              mobile: '',
              name: '杭州淳安分公司',
              nameMobile: '',
              type: 1
            }
          ],
          id: 3772268,
          loginName: '',
          lvl: 2,
          mobile: '',
          name: '杭州分公司',
          nameMobile: '',
          type: 1
        }
      ]
    }
  },
  components: {
    preRecursive
  },
  methods: {
    getUserInfo (val) {
      console.log(val)
      this.personListStr = ''
      if (this.multiple) {
        if (val && val.length !== 0) {
          for (let i = 0; i < val.length; i++) {
            this.personListStr += val[i].nameMobile
          }
        }
      } else {
        if (val) {
          this.personListStr = val.nameMobile
        }
      }
      console.log(this.personListStr, 4576)
    },
    showUserTree () {
      this.businessUserShow = true
    },
    // 人员获取接口
    getUserList (item) {
      console.log(item) // item为请求下级数据时候要传入的参数
      const _this = this
      // 需要使用promiss将数据异步回调到子组件
      return new Promise((resolve, reject) => {
      // 这里写请求数据,下面注释的为模拟返回的结果
      
      
      //  let list = JSON.parse(JSON.stringify(_this.personListVal)) // 实际开发中后台返回的数据不需要转
      //  list.forEach(element => {
      //    if (element.type === 1) {
      //      element.isDown = false
      //    } else if (element.type === 2) {
      //      element.isCheck = false
      //    }
      //  })
      //  resolve(list)
        
        
      })
    },
    // 外呼人员查询
    getSearchUserList (searchText) {
      console.log(searchText) // searchText为请求搜索人员参数
      const _this = this
      return new Promise((resolve, reject) => {
      // 这里写请求数据,下面注释的为模拟返回的结果
        // let list = JSON.parse(JSON.stringify(_this.searchPersonListVal)) // 实际开发中后台返回的数据不需要转
        //resolve(list)
      })
    },
  }
}
</script>

<style scoped lang="less">
.example-area {
  width: 100%;
  height: 100%;
  overflow: auto;
  .title {
    font-size: 14px;
    padding: 10px 5px;
  }
  .title12 {
    display: flex;
    align-items: center;
  }
}
.table-area {
  height: 300px;
}
.area-select-box {
  height: 50px;
}
.show-list {
  padding: 5px;
}
</style>

如果multiple参数传false则为单选点击确定后返回为对象效果如下:

image.png

image.png

如果multiple参数传tree则为多选点击确定后返回为数组效果如下:

image.png

image.png 可能大家有个疑问为啥搜索结果要另外写一个接口不放在一起呢,不就是一个参数的问题吗?当时后端小哥能力有限,只能分开,为了不难为他,我只能委屈自己!

周六的晚上无法入眠,那就起来整篇文章,分享自己,快乐大家!祝大家周末愉快!