微信小程序树型选择器的使用

找了一圈,有一些能用的但是不是很完善,所以打算自己改完后记录一下。如果有需要的可以直接复制使用。

支持多选的树型选择

props属性

名称 说明 类型 默认值
list 传入的树型列表 Array []
isOpenAll 是否展开全部节点 Boolean false
props 属性配置项 Object {label: 'name',children: 'children',value: 'id'}

默认数组结构的字段为显示的名称为name,子项为children,绑定的值为id,根据自己需要传入修改

事件

名称 说明
select 选项改变后触发,返回两个对象,一个为item,当前点击项,一个为idList,所有选中的数组列表数据
clickItem 点击子项触发,返回当前的item

使用举例

html 复制代码
<tree-select-mut
  isOpenAll
  list="{{dataTree}}"
></tree-select-mut>

代码

tree.wxml

html 复制代码
<view wx:for="{{tree}}" wx:key="index" style="margin-left: {{treeListIndex*10+20}}rpx">
  <!-- 一级菜单 -->
  <view class="tree-item">
    <view wx:if="{{item[props.children] && item[props.children].length > 0}}" bindtap="isOpen" data-index="{{index}}">
      <image src="{{arrowUrl}}" class="tree-arrow {{item.open ? 'tree-expand' : ''}}" />
    </view>
    <view class="tree-item-no-children" wx:else> </view>
    <view class="tree-item-name-warp" bindtap="select" data-item="{{item}}" data-index="{{index}}">
      <image wx:if="{{item.checked === 1}}" src="{{choiceUrl}}" class="tree-check-box"></image>
      <image wx:if="{{item.checked === 0}}" src="{{unchoiceUrl}}" class="tree-check-box"></image>
      <image wx:if="{{item.checked === -1}}" src="{{unfullChoiceUrl}}" class="tree-check-box"></image>
      <view class="tree-item-name {{item.checked === 1 ? 'tree-item-name-select' : '' }}">{{item[props.label]}}</view>
    </view>
  </view>
  <!-- 二级菜单 -->
  <tree
    wx:if="{{item.children && item.children.length > 0 && item.open }}"
    data-parent="{{item}}"
    list="{{ item.children }}"
    props="{{props}}"
    isOpenAll="{{isOpenAll}}"
    treeListIndex="{{treeListIndex+1}}"
    catch:select="handleSelect"
  />
</view>

tree.js

js 复制代码
Component({
  properties: {
    list: {
      type: Array,
      value: []
    },
    treeListIndex: {
      // 当期树形列表的索引
      type: Number,
      value: 1
    },
    props: {
      type: Object,
      value: {
        label: 'name',
        children: 'children',
        value: 'id'
      }
    },
    isOpenAll: {
      // 是否展开全部节点
      type: Boolean,
      value: false
    }
  },
  observers: {
    list: function (params) {
      this.setData({
        tree: this._initSourceData(params)
      })
    }
  },
  data: {
    tree: [],
    allChoiceIdList: [], // 所有选中的id数组
    choiceUrl:
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAMAAADypuvZAAAAUVBMVEUAAAAAe/8AfP8Ae/8Aev8Aev8Ae/8AfP8Aev8Aev8AfP8Ae/8AfP8AfP8Ae/8Ae/8Ae/8AfP8Aev8Aev8Ae/8AfP8Aev8Ae/8Akv8Aev8Aev+kEjuvAAAAGnRSTlMA82X4/Mi9Pe92cUxEN4R9XzIZqWhakGwDkkXSKaQAAADMSURBVEjH7dbLDoIwEEbhmQoq4hXEy7z/g1oMCZIjhXZn5Oy/ZFaTX3wuV5uZ5k7aqrVFta488iZSiTiLzkkej3LReKRiCS1oQVlTR6OslEcdiValyKeSmcZ3Bpo2TfYdZZOG6LnR8dvehugmQrXa03jUG99BQ4ZoJ52icWZAQ7UNGCAqpSGyYqB0I21XMyKqy5aGiIqGqO/Uqc7cLYCgYICgunYWQFAwQOgIAzSmCgsjKhoiquKnnuUfII03mjRzkgZVynRLG4lJc/QFPGf4PGUpSLcAAAAASUVORK5CYII=',
    unchoiceUrl:
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0BAMAAAA3VgbYAAAAG1BMVEUAAACampqRkZGRkZGQkJCQkJCSkpKSkpKQkJDSjQFsAAAACHRSTlMAF/PjyL1lWZ2u+0EAAABaSURBVDjLY2Bgc+rAAlQSGBgYIzqwglYBBtbmQgYsQNwigCHDkAErEG5j8CjALsXewqAhgF2KsYmhgwEH6BiVGpUalRqVwiqFp0jBUxDhKb7wFHp4iko8BSwAp9JkJXcYpZEAAAAASUVORK5CYII=',
    unfullChoiceUrl:
      'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0BAMAAAA3VgbYAAAAFVBMVEUAAAAAe/8Aev8Ae/8AfP8Aev8Aev8LwEpAAAAABnRSTlMA88i9ZRljrLGUAAAAVElEQVQ4y2NgYFFMwwKEHBgYWM3SsILkAAbmNBzAgMENl1QKgxouqSQGMVxSiQxpOMGo1HCTYkABVJcajF4elaKNFJ4iBU9BhKf4wlPo4Skq8RSwALWN/JpuKoEOAAAAAElFTkSuQmCC',
    arrowUrl:
      'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjYzhjOGM4IiBkPSJNOCA2YTEgMSAwIDAgMSAxLjYtLjhsOCA2YTEgMSAwIDAgMSAwIDEuNmwtOCA2QTEgMSAwIDAgMSA4IDE4VjZaIi8+PC9zdmc+'
  },
  methods: {
    isOpen(e) {
      const open = 'tree[' + e.currentTarget.dataset.index + '].open'
      this.setData({
        [open]: !this.data.tree[e.currentTarget.dataset.index].open
      })
    },
    _initSourceData(nodes) {
      const childrenName = this.properties.props.children
      nodes.forEach((element) => {
        if (element.checked === undefined) element.checked = 0
        element.open = this.properties.isOpenAll // 是否展开

        if (element[childrenName] && element[childrenName].length > 0)
          element[childrenName] = this._initSourceData(element[childrenName])
      })
      return nodes
    },
    // 选择
    select(e) {
      let item = e.currentTarget.dataset.item
      item = this._handleClickItem(item)
      // console.log('当前节点:', item)
      this.data.tree = this._updateTree(this.data.tree, item)
      this.setData({
        tree: this.data.tree
      })
      this.data.allChoiceIdList = this.getAllChoiceId(this.data.tree)
      this.triggerEvent('select', { item: item, idList: this.data.allChoiceIdList }, { bubbles: true, composed: true })
      this.triggerEvent('clickItem', { item: item }, { bubbles: true, composed: true })
    },
    // 选择冒泡事件
    handleSelect(e) {
      const childrenName = this.properties.props.children
      let parent = e.currentTarget.dataset.parent
      let currentTap = e.detail.item
      // console.log('parent节点:', parent)
      // 修正它的父节点
      parent[childrenName] = this._updateTree(parent[childrenName], currentTap)
      const { half, all, none } = this.getChildState(parent[childrenName])
      // console.log(`half:${half},all:${all},none:${none}`)
      if (half) parent.checked = -1
      if (all) parent.checked = 1
      if (none) parent.checked = 0
      // 修正整个tree
      this.data.tree = this._updateTree(this.data.tree, parent)
      this.setData({
        tree: this.data.tree
      })
      this.data.allChoiceIdList = this.getAllChoiceId(this.data.tree)
      this.triggerEvent(
        'select',
        { item: parent, idList: this.data.allChoiceIdList },
        { bubbles: true, composed: true }
      )
    },
    /**
     * @method 处理点击选择
     * @param {Object} node 节点对象
     * @returns {Object} node 处理完毕的节点
     * @description 有子节点则全选中或全取消,当前为最底层单节点则选中或单取消
     */
    _handleClickItem(node) {
      const childrenName = this.properties.props.children
      switch (node.checked) {
        case 0:
          node.checked = 1
          if (node[childrenName] && node[childrenName].length > 0)
            node[childrenName] = this._allChoice(node[childrenName])
          break
        case 1:
          node.checked = 0
          if (node[childrenName] && node[childrenName].length > 0)
            node[childrenName] = this._allCancel(node[childrenName])
          break
        default:
          node.checked = 1
          if (node[childrenName] && node[childrenName].length > 0)
            node[childrenName] = this._allChoice(node[childrenName])
          break
      }
      return node
    },
    /**
     * @method 全选
     * @param {Array} nodes 节点数组
     * @returns {Array} nodes 处理完毕的节点数组
     */
    _allChoice(nodes) {
      const childrenName = this.properties.props.children
      if (nodes.length <= 0) return
      for (let i = 0; i < nodes.length; i++) {
        nodes[i].checked = 1
        if (nodes[i][childrenName] && nodes[i][childrenName].length > 0)
          nodes[i][childrenName] = this._allChoice(nodes[i][childrenName])
      }
      return nodes
    },
    /**
     * @method 全取消
     * @param {Array} nodes 节点数组
     * @returns {Array} nodes 处理完毕的节点数组
     */
    _allCancel(nodes) {
      const childrenName = this.properties.props.children
      if (nodes.length <= 0) return
      for (let i = 0; i < nodes.length; i++) {
        nodes[i].checked = 0
        if (nodes[i][childrenName] && nodes[i][childrenName].length > 0)
          nodes[i][childrenName] = this._allCancel(nodes[i][childrenName])
      }
      return nodes
    },
    /**
     * @method 更新tree
     * @param {Array} tree 节点树
     * @param {Object} newItem 需要替换新节点
     * @description 找到tree中目标进行替换
     */
    _updateTree(tree, newItem) {
      const childrenName = this.properties.props.children
      const valueName = this.properties.props.value
      if (!tree || tree.length <= 0) return
      for (let i = 0; i < tree.length; i++) {
        if (tree[i][valueName] === newItem[valueName]) {
          tree[i] = newItem
          break
        } else {
          if (tree[i][childrenName] && tree[i][childrenName].length > 0) {
            tree[i][childrenName] = this._updateTree(tree[i][childrenName], newItem)
          }
        }
      }
      return tree
    },
    /**
     * @method 获取子节点的状态
     * @param {Array} node 节点数组
     */
    getChildState(node) {
      let all = true
      let none = true
      for (let i = 0, j = node.length; i < j; i++) {
        const n = node[i]
        if (n.checked === 1 || n.checked === -1) {
          none = none && false
        }
        if (n.checked === 0 || n.checked === -1) {
          all = all && false
        }
      }
      return { all, none, half: !all && !none }
    },
    // 获取所有选中的节点id
    getAllChoiceId(nodes, res = []) {
      const childrenName = this.properties.props.children
      const valueName = this.properties.props.value
      const labelName = this.properties.props.label
      for (let i = 0; i < nodes.length; i++) {
        if (nodes[i].checked === 1)
          res.push({
            id: nodes[i][valueName],
            label: nodes[i][labelName]
          })
        if (nodes[i][childrenName] && nodes[i][childrenName].length > 0)
          this.getAllChoiceId(nodes[i][childrenName], res)
      }
      return res
    }
  }
})

tree.json

json 复制代码
{
    "component": true,
    "usingComponents": {
        "tree": "/components/TreeSelectMut/tree/tree"
    }
}

tree.scss

scss 复制代码
.tree-item {
  box-sizing: border-box;
  padding: 10rpx 0;
  display: flex;
  align-items: center;

  .tree-arrow {
    width: 36rpx;
    height: 36rpx;
    transition: all 0.3s;
  }

  .tree-expand {
    transform: rotate(90deg);
  }

  .tree-item-no-children {
    width: 20px;
    display: flex;
    flex-shrink: 0;
    justify-content: center;
    align-items: center;
  }

  .tree-item-name-warp {
    display: flex;
    align-items: center;
    flex: 1 0;
    min-width: 0;

    .tree-item-name {
      box-sizing: border-box;
      margin-left: 24rpx;
      color: #606266;
      font-size: 32rpx;
      flex: 1 0;
      min-width: 0;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .tree-check-box {
      height: 32rpx;
      width: 32rpx;
      margin-left: 30rpx;
      flex-shrink: 0;
    }

    .tree-item-name-select {
      color: #0079fe;
    }
  }
}

单选的树型选择

props属性

名称 说明 类型 默认值
list 传入的树型列表 Array []
isOpenAll 是否展开全部节点 Boolean false
props 属性配置项 Object {label: 'name',children: 'children',value: 'id'}
selectKey 选中的节点id String ''
disabledNode 禁用规则 Object {}

props默认数组结构的字段为显示的名称为name,子项为children,绑定的值为id,根据自己需要传入修改

disabledNode禁用规则使用:对象的value字段指定选取哪个字段进行匹配,rules字段为数组,依次填入对应的字段值

例子:

js 复制代码
disabledNode={value:'level',rules:['1','2']} 

表示level字段值为1和2的都禁用置灰

事件

名称 说明
select 选项改变后触发,返回当前选择的item

使用举例

html 复制代码
<tree-select
  isOpenAll
  bind:select="itemClick"
  list="{{options}}"
  disabledNode="{{disabledNode}}"
  selectKey="{{currentNode.id}}"
></tree-select>
js 复制代码
{
    data:{
        options: [],
        disabledNode: {
          value: 'level',
          rules: [1, 2]
        },
        currentNode: {}
    }
}

代码

tree.wxml

html 复制代码
<wxs src="./tools.wxs" module="tools" />
<view wx:for="{{tree}}" wx:key="index" class="tree">
  <view class="tree-item">
    <view
      class="tree-item-onOff"
      wx:if="{{item[props.children] && item[props.children].length > 0}}"
      bindtap="isOpen"
      data-index="{{index}}"
    >
      <view class="arrow-icon {{item.open ? 'tree-item-onOff-open' : ''}}">
        <t-icon name="play" color="#c4c1ce" size="22"></t-icon>
      </view>
    </view>
    <view class="tree-item-onOff" wx:else> </view>
    <view
      class="tree-item-name-warp {{selectKey == item[props.value] ? 'tree-item-name-select' : '' }}"
      bindtap="select"
      data-item="{{item}}"
      data-index="{{index}}"
    >
      <view class="tree-item-name {{tools.isDisableNode(item,disabledNode) ?'disabled-color':''}}"> {{item[props.label]}} </view>
    </view>
  </view>
  <x-tree
    wx:if="{{item[props.children] && item[props.children].length > 0 && item.open }}"
    list="{{ item[props.children] }}"
    selectKey="{{selectKey}}"
    isOpenAll="{{isOpenAll}}"
    props="{{props}}"
  >
  </x-tree>
</view>

tools.wxs

js 复制代码
function containsValue(arr, value) {
  for (var i = 0; i < arr.length; i++) {
    // eslint-disable-next-line eqeqeq
    if (arr[i] == value) {
      return true
    }
  }
  return false
}
var isDisableNode = function (data, obj) {
  if (obj.value && obj.rules) {
    return containsValue(obj.rules, data[obj.value])
  } else {
    return false
  }
}
module.exports = {
  isDisableNode: isDisableNode
}

tree.js

js 复制代码
Component({
  properties: {
    list: {
      type: Array,
      value: []
    },
    selectKey: {
      // 选中的节点id
      type: String,
      value: ''
    },
    isOpenAll: {
      //是否展开全部节点
      type: Boolean,
      value: false
    },
    // 禁用规则使用:对象的value字段指定选取哪个字段进行匹配,rules字段为数组,依次填入对应的字段值
    // 例子:disabledNode={value:'level',rules:['1','2']} ,表示level字段值为1和2的都禁用置灰
    disabledNode: {
      type: Object,
      value: {}
    },
    props: {
      type: Object,
      value: {
        label: 'name',
        children: 'children',
        value: 'id'
      }
    }
  },
  observers: {
    list: function (params) {
      params.forEach((v) => {
        v.open = this.properties.isOpenAll // 是否展开
      })
      this.setData({
        tree: params
      })
    }
  },
  data: {
    tree: []
  },
  options: {
    styleIsolation: 'apply-shared'
  },
  methods: {
    isOpen(e) {
      const open = 'tree[' + e.currentTarget.dataset.index + '].open'
      this.setData({
        [open]: !this.data.tree[e.currentTarget.dataset.index].open
      })
    },
    select(e) {
      const item = e.currentTarget.dataset.item
      if (this.properties.disabledNode.value && this.properties.disabledNode.rules) {
        const arr = this.properties.disabledNode.rules || []
        const value = this.properties.disabledNode.value
        if (arr.includes(item[value])) {
          this.isOpen(e)
          return
        }
      }
      this.triggerEvent('select', item, { bubbles: true, composed: true })
    }
  }
})

tree.json

json 复制代码
{
    "component": true,
    "usingComponents": {
        "x-tree": "/components/tree/tree"
    }
}

tree.scss

scss 复制代码
.tree {
  text-align: left;
  padding-left: 15px;
  font-size: 32rpx;

  .tree-item {
    display: flex;

    .tree-item-onOff {
      width: 30px;
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .arrow-icon {
      display: flex;
      align-items: center;
      transition: all 0.3s;
    }

    .tree-item-onOff-open {
      transform: rotate(90deg);
    }

    .tree-item-name-warp {
      width: calc(100% - 40px);
      display: flex;
      padding: 10rpx 0 10rpx 5rpx;
      color: #606266;

      .tree-item-name {
        width: calc(100% - 50px);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        display: flex;
        align-items: center;
        gap: 10rpx;
      }

      .disabled-color {
        color: #a8abb2;
      }
    }

    .tree-item-name-select {
      background: #ecf7fa;
      color: #0079fe;
      font-weight: 700;
      border-radius: 8rpx;
    }
  }
}
相关推荐
蜗牛快跑21311 分钟前
面向对象编程 vs 函数式编程
前端·函数式编程·面向对象编程
Dread_lxy12 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
涔溪1 小时前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇2 小时前
ES6进阶知识一
前端·ecmascript·es6
小泽呀x2 小时前
微信小程序中使用离线版阿里云矢量图标
微信小程序·小程序
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss