小程序自定义组件筛选器

540 阅读4分钟

公司需求需要使用到筛选器,但是网上搜索到的和自己的需求不太符合,顺便学习一下自定义组件的开发

效果预览

主要功能有:

  • 显示所有筛选项
  • 点击筛选项
  • 重置
  • 确定

创建组件文件和目录

微信开发者工具中,在components文件夹下新建一个filterbar的文件夹, 然后filterbar文件夹右键, 选择新建Component,设置组件名称,如filterbar, 随后生成四个文件.

编写组件结构

<view class="filter-container">
  <view class="list">
    <view class="search-cat" wx:for="{{searchList}}" wx:for-item="p" wx:for-index="pIndex" wx:key="pIndex">
      <view class="search-title">{{p.screenLabel}}</view>
      <view class="search-items">
        <view 
          bindtap="_onChange" 
          wx:for="{{p.screenValue}}" 
          wx:for-item="g" 
          data-parent-index="{{ pIndex }}" 
          data-index="{{ index }}" 
          data-item="{{ p }}" 
          class="item {{ g.checked ? 'active' : '' }}" 
          wx:key="index">
          {{g.label}}
        </view>
      </view>
    </view>
  </view>
  <view class="btn-wrap">
    <view class="reset">
      <button class="reset-btn" bindtap="_onReset">重置</button>
    </view>
    <view class="search-bottom">
      <button class="search-btn" bindtap="_search" >确定({{searchTotal}}{{searchSuffix}})</button>
    </view>
  </view>
</view>

上面部分主要是渲染数据

  • searchList 所有筛选项
    • 取出筛选项进行显示
    • 在子选项上绑定点击事件_onChange

下面是按钮区域

  • 重置按钮, 绑定事件_onReset
  • 确定按钮, 绑定事件_search, 确定按钮中可以显示查询数量和修饰词,如 searchTotal=0, searchSuffix='件', 就显示为确定(0件)

编写组件逻辑

显示

组件在properties属性中通过filterList接收父组件传递的数据(这里就省略searchTotal和searchSuffix)

properties: {
  filterList: {
    type: Array,
      value: []
  }
}

在组件的生命周期函数attached中初始化

lifetimes: {
  attached: function() {
    // 在组件实例进入页面节点树时执行,一般用于初始化工作
    this._init()
  }
}

初始化函数init

_init() {
  const { filterList } = this.data
  this.setData({
    searchList: filterList
  })
}

点击筛选项事件

命名: 组件使用的建议使用下划线_开头,不需要给外界

在点击某一个选项时,需要知道他属于谁parentIndex, 自身的数据item, 自身在兄弟中的位置index 所以渲染时,将这些信息存储到data-

<view 
  bindtap="_onChange" 
  wx:for="{{p.screenValue}}" 
  wx:for-item="g" 
  data-parent-index="{{ pIndex }}" 
  data-index="{{ index }}" 
  data-item="{{ p }}" 
  class="item {{ g.checked ? 'active' : '' }}" 
  wx:key="index">
  {{g.label}}
</view>

筛选项事件

// 点击筛选项
_onChange(e) {
  const { parentIndex, item, index } = e.currentTarget.dataset

  // 单选
  if (item.type === 'radio') {
    // 全部重置为未选择
    item.screenValue.map(n => (n.checked = false))
    item.screenValue[index].checked = true
  } else {
    // 多选,将点击的设置为选中
    item.screenValue[index].checked = !item.screenValue[index].checked
  }

  // 修改数组中的成员
  this.setData({
    [`searchList[${parentIndex}]`]: item
  })
  // 获取已经选中的值
  const selectedObj = this._getSelectedQuery()

  // 触发事件filterChange,将已选值传递出去
  // 在父组件中需要监听该事件,如bind:filterChange="onChange" 然后在onChange写自己的业务逻辑
  this.triggerEvent('filterChange', {
    selectedObj
  })
}

发生点击事件时,取出这些数据,判断是单选还是多选,进行标记 然后重新更新数据到页面上 取出已选中值,触发父组件的filterChange事件,同时将选择值传递出去, 父组件监听filterChange拿到已选中的值进行一系列的逻辑处理

  this.triggerEvent('filterChange', {
    selectedObj
  })

图示:

_onChange => bind:filterChange => onChange

重置事件

// 重置筛选
_onReset: function() {
  this.data.searchList.forEach(p => {
    p.screenValue.forEach(s => {
      s.checked = false
    })
  })
  this.setData({
    searchList: this.data.searchList
  })

  const selectedObj = this._getSelectedQuery()
  // 控制权给父组件, 可以请求数据,之类的
  this.triggerEvent('filterReset', {
    selectedObj
  })
}

将所有筛选项的选中状态去掉, 触发事件filterReset

确定事件

// 点击确定按钮
_search() {
  // 获取已经选中的值
  const selectedObj = this._getSelectedQuery()
  // 控制权给父组件, 可以请求数据,之类的
  this.triggerEvent('filterSearch', selectedObj)
},

获取已选的值, 触发事件filterSearch

完整代码

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    /**
     * 筛选数组
     *  filterList: [
     *    {
     *      type: 'radio',           // 筛选类型 radio 单选 checkbox多选
     *      screenLabel: '站点类型',  // 筛选标题
     *      screenKey: 'type',       // 筛选类别
     *      screenValue: [           // 筛选值
     *        {
     *          label: '快充',        // 显示内容
     *          value: '1',          // 对应的值
     *        }
     *      ]
     *    },
     *    {
     *     ....
     *    }
     *  ]
     * 
     */
    filterList: {
      type: Array,
      value: []
    },

    // 搜索结果总数
    searchTotal: {
      type: Number,
      value: 0
    },
    // 搜索结果描述字符串
    searchSuffix: {
      type: String,
      value: '件商品'
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
    searchList: [] // 所有筛选项
  },

  // 生命周期函数
  lifetimes: {
    attached: function() {
      // 在组件实例进入页面节点树时执行,一般用于初始化工作
      this._init()
    },
    detached: function() {
      // 在组件实例被从页面节点树移除时执行
    },
  },

  /**
   * 组件的方法列表
   */
  methods: {
    // 点击筛选项
    _onChange(e) {
      const {
        parentIndex,
        item,
        index
      } = e.currentTarget.dataset

      // 单选
      if (item.type === 'radio') {
        // 全部重置为未选择
        item.screenValue.map(n => (n.checked = false))
        item.screenValue[index].checked = true
      } else {
        // 多选,将点击的设置为选中
        item.screenValue[index].checked = !item.screenValue[index].checked
      }

      // 修改数组中的成员
      this.setData({
        [`searchList[${parentIndex}]`]: item
      })
      // 获取已经选中的值
      const selectedObj = this._getSelectedQuery()

      // 触发事件filterChange,将已选值传递出去
      // 在父组件中需要监听该事件,如bind:filterChange="onChange" 然后在onChange写自己的业务逻辑
      this.triggerEvent('filterChange', {
        selectedObj
      })
    },

    // 重置筛选
    _onReset: function() {
      this.data.searchList.forEach(p => {
        p.screenValue.forEach(s => {
          s.checked = false
        })
      })
      this.setData({
        searchList: this.data.searchList
      })

      const selectedObj = this._getSelectedQuery()
      // 控制权给父组件, 可以请求数据,之类的
      this.triggerEvent('filterReset', {
        selectedObj
      })
    },

    // 点击确定按钮
    _search() {
      // 获取已经选中的值
      const selectedObj = this._getSelectedQuery()
      // 控制权给父组件, 可以请求数据,之类的
      this.triggerEvent('filterSearch', selectedObj)
    },

    // 获取已经选中的值
    _getSelectedQuery() {

      let selected = {}
      // 过滤出已选值
      const rs = this.data.searchList.map(p => {
        const {
          type,
          screenKey,
          screenValue
        } = p
        selected[screenKey] = ''

        // 单选框
        if (type === 'radio') {
          p.screenValue.filter(s => {
            if (s.checked) {
              selected[screenKey] = s.value
            }
          })
        }

        // 多选框
        if (type === 'checkbox') {
          p.screenValue.filter(s => {
            if (s.checked) {
              // 已存在相同类型的值
              if (selected[screenKey]) {
                // 运营商传参数格式 operatorIds=1&operatorIds=2
                // selected[screenKey] = `${selected[screenKey]}&operatorIds=${s.value}`
                selected[screenKey] = `${selected[screenKey]},${s.value}`
              } else {
                // 第一次出现该类型的值
                selected[screenKey] = s.value
              }
            }
          })
        }
      })
      return selected
    },

    // 初始化
    _init() {
      const {
        filterList
      } = this.data
      this.setData({
        searchList: filterList
      })
    }

  }
})

编写组件样式

/* 筛选容器 */
.filter-container {
  padding: 0 0 20px 15px;
  background: #fff;
  justify-content: flex-start;
}

.search-title {
  padding: 10rpx 0 16rpx 0;
  text-align: left;
  font-size: 30rpx;
  font-family: PingFang-SC-Heavy;
  font-weight: 800;
  color: #333;
}

.search-items {
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
}

.search-items .item {
  width: 156rpx;
  box-sizing: border-box;
  height: 60rpx;
  line-height: 60rpx;
  text-align: center;
  font-weight: 500;
  font-size: 26rpx;
  border: 1rpx solid #666;
  border-radius: 4rpx;
  margin: 14rpx 22rpx 19rpx 0;
  color: #666;
  background: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding:0 10rpx;
}

.search-items .item.active {
  border-color: transparent;
  background:linear-gradient(0deg,rgba(82,148,255,1) 0%,rgba(65,134,242,1) 100%);
  color: #fff;
}

.filter-container .btn-wrap {
  display: flex;
  justify-content: space-between;
  width: 690rpx;
  margin-top: 50rpx;
  font-size: 32rpx;
  font-weight: 500;
  font-family: PingFang-SC-Medium;
}

.btn-wrap .search-bottom {
  width: 464rpx;
  height: 88rpx;
  line-height: 88rpx;
  display: flex;
  justify-content: center;
  text-align: center;
}

.btn-wrap button::after {
  border: none;
}

.btn-wrap .search-bottom .search-btn {
  background: #4186F2;
  color: #fff;
  flex: 1;
  border-radius: 4rpx;
}

.btn-wrap .reset {
  width: 214rpx;
  height: 88rpx;
  line-height: 88rpx;
  box-sizing: border-box;
}

.btn-wrap .reset .reset-btn {
  color: #4186F2;
  border-radius: 4rpx;
  background: #E0ECFF;
}

使用组件

引入组件

useFilterBar.json 在页面的 json 文件中进行引用声明。此时需要提供每个自定义组件的标签名和对应的自定义组件文件路径

{
  "usingComponents": {
    "filter-bar": "/components/filterbar/filterbar"
  }
}

在wxml使用

useFilterBar.wxml

<filter-bar 
  filterList="{{respList}}"
  searchTotal="{{searchTotal}}"
  searchSuffix="{{searchSuffix}}"
  bind:filterReset="onReset" 
  bind:filterChange="onChange" 
  bind:filterSearch="onSearch">
</filter-bar>

在js中定义数据和事件监听

useFilterBar.js

Page({

  /**
   * 页面的初始数据
   */
  data: {
    respList: [
      {
        type: 'radio',
        screenLabel: '时尚',
        screenKey: 'fashion',
        screenValue: [
          { label: '篮球鞋 ', value: '1' },
          { label: '运动鞋', value: '2' },
          { label: '板鞋 ', value: '3' },
          { label: '跑步鞋', value: '4' }
        ]
      },
      {
        type: 'checkbox',
        screenLabel: '手机',
        screenKey: 'phone',
        screenValue: [
          { label: '华为 ', value: '1' },
          { label: '小米', value: '2' },
          { label: 'oppo', value: '3' },
          { label: 'vivo', value: '4' },
          { label: '一加', value: '5' },
          { label: '魅族', value: '6' }
        ]
      },
    ],
    searchTotal: 500,
    searchSuffix: '个商品'
  },

  onReset () {
    console.log('onReset')

    this.setData({
      searchTotal: 500
    })
  },

  onChange (e) {
    console.log('onChange')
    console.log(e.detail) // 获取组件传递过来的数据

    // 根据已选值,请求新数据
    const newTotal = this.data.searchTotal - Math.floor(Math.random() * 10)
    this.setData({
      searchTotal: newTotal
    })
  },

  onSearch (e) {
    console.log('onSearch')
    console.log(e.detail)
  }
})