自定义多级联动选择器指南(uni-app)

简介: 在本文中,探讨了如何在uni-app中创建自定义多级联动选择器组件。这个组件具有强大的多端支持,可适用于H5、APP、微信小程序、支付宝小程序等多种平台。

介绍

  • 多端支持:可以运行在H5、APP、微信小程序还是支付宝小程序,都可以轻松使用改组件。
  • 自定义配置:您可以根据需要配置选择器的级数,使其适应不同的数据结构和用例。
  • 无限级联:此组件支持无限级联选择,使您能够创建具有复杂数据结构的选择器。
参数 类型 描述 默认值 必选
title string 标题 ''
layer number 控制几级联动 1
data array 数据:[{name: '', id: '', children: [{name: '', id: ''}]}] []

效果图

image.png

数据结构

接口返回数据结构:

[
    {
   id: 47, name: "工厂1", parentId: 0, type: 0},
    {
   id: 48, name: "区域1", parentId: 47, type: 0},
    {
   id: 19, name: "设备1", parentId: 48, type: 1}
]

处理后数据结构:

[{
    
    id: 47, name: "工厂1", parentId: 0, type: 0,
    children: [{
    
        id: 48, name: "区域1", parentId: 47, type: 0,
        children: [
            {
    id: 19, name: "设备1", parentId: 48, type: 1 }
        ] 
    }] 
}]

引用示例

<template>
    <view class="container">
        <view @click="bindDevice">选择设备</view>
        <cascade-picker ref="picker" :title="cascadePicker.title" :layer="cascadePicker.layer" :data="cascadePicker.data" @callback="pickerCallback"></cascade-picker>
    </view>
</template>
<script>
import cascadePicker from '@/components/cascade-picker/cascade-picker.vue';
import {
   handleTree} from "@/utils/tree";
export default {
   
    components: {
   
        cascadePicker
    },
    data() {
   
        return {
   
            deviceArr: [],
            deviceId: '',
            cascadePicker: {
   
                title: 'picker标题',
                layer: null,
                data: []
          }
        }
    },
    onLoad() {
   
        this.getDeviceSimpleList()
      },
    methods: {
   
        // 获取设备列表
        getDeviceSimpleList() {
   
          getDeviceSimpleList().then(res => {
   
            this.deviceArr = handleTree(res.data)
          })
        },
        // 打开设备选择器
        bindDevice() {
   
          const _this = this;
          _this.cascadePicker.data = _this.deviceArr;
          _this.$refs.picker.open().then(function() {
   
            console.log('打开成功');
          });
        },
        // picker多级联动回调
        pickerCallback(e) {
   
          const {
   data} = e;
          const lastItem = data[data.length - 1];
          this.deviceId = lastItem.id;
        }
    }
}
</script>

@/utils/tree文件中的handleTree方法

/**
 * 构造树型结构数据
 * @param {*} data 数据源
 * @param {*} id id字段 默认 'id'
 * @param {*} parentId 父节点字段 默认 'parentId'
 * @param {*} children 孩子节点字段 默认 'children'
 * @param {*} rootId 根Id 默认 0
 */
export function handleTree(data, id = 'id', parentId = 'parentId', children = 'children', rootId = 0) {
   
  rootId = rootId || Math.min.apply(Math, data.map(item => {
   
    return item[parentId]
  })) || 0
  //对源数据深度克隆
  const cloneData = JSON.parse(JSON.stringify(data))
  //循环所有项
  const treeData = cloneData.filter(father => {
   
    let branchArr = cloneData.filter(child => {
   
      //返回每一项的子级数组
      return father[id] === child[parentId]
    });
    branchArr.length > 0 ? father.children = branchArr : '';
    //返回第一层
    return father[parentId] === rootId;
  });
  return treeData !== '' ? treeData : data;
}

cascade-picker组件完整代码:

components文件夹下创建cascade-picker文件夹,然后在此文件夹下创建cascade-picker.vue - 多级联动组件

<template name="cascade-picker">
  <view class="aui-picker" v-if="SHOW" :class="{'aui-picker-in': FADE==1,'aui-picker-out': FADE==0}">
    <view class="aui-mask" @click.stop="close"></view>
    <view class="aui-picker-main">
      <view class="aui-picker-header">
        <view class="aui-picker-header-icon" @click.stop="close">取消</view>
        <view class="aui-picker-title" v-if="title">{
   {
    title }}</view>
        <view class="aui-picker-header-icon aui-picker-confirm" @click.stop="_confirm">确认</view>
      </view>
      <view class="aui-picker-nav">
        <view class="aui-picker-navitem"
              v-if="nav.length>0"
              v-for="(item, index) in nav"
              :key="index"
              :data-index="index"
              :class="[index==navCurrentIndex ? 'active' : '', 'aui-picker-navitem-'+index]"
              :style="{margin: nav.length>2 ? '0 10px 0 0' : '0 30px 0 0'}"
              @click.stop="_changeNav($event)"
        >{
   {
    item.name }}
        </view>
        <view class="aui-picker-navitem"
              :key="nav.length"
              :data-index="nav.length"
              :class="[nav.length==navCurrentIndex ? 'active' : '', 'aui-picker-navitem-'+nav.length]"
              :style="{margin: nav.length>2 ? '0 10px 0 0' : '0 30px 0 0'}"
              @click.stop="_changeNav($event)">请选择
        </view>
        <view class="aui-picker-navborder" :style="{left: navBorderLeft+'px'}"></view>
      </view>
      <view class="aui-picker-content">
        <view class="aui-picker-lists">
          <view class="aui-picker-list"
                v-for="(list, index) in queryItems.length + 1"
                :key="index"
                :data-index="index"
                :class="[index==navCurrentIndex ? 'active' : '']"
          >
            <view class="aui-picker-list-warp" v-if="index == 0">
              <view class="aui-picker-item"
                    v-for="(item, key) in items"
                    v-if="item.pid=='0'"
                    :key="key"
                    :data-pindex="index"
                    :data-index="key"
                    :data-id="item.id"
                    :data-pid="item.pid"
                    :data-name="item.name"
                    :data-type="item.type"
                    :class="{'active': result.length>index && result[index].id==item.id}"
                    :style="{'background': touchConfig.index==key && touchConfig.pindex==index ? touchConfig.style.background : ''}"
                    @click.stop="_chooseItem($event)"
                    @touchstart="_btnTouchStart($event)"
                    @touchmove="_btnTouchEnd($event)"
                    @touchend="_btnTouchEnd($event)"
              >{
   {
    item.name }}
              </view>
            </view>
            <view class="aui-picker-list-warp" v-else>
              <view class="aui-picker-item"
                    v-for="(item, key) in queryItems[index-1]"
                    :key="key"
                    :data-pindex="index"
                    :data-index="key"
                    :data-id="item.id"
                    :data-pid="item.pid"
                    :data-name="item.name"
                    :data-type="item.type"
                    :class="{'active': result.length>index && result[index].id==item.id}"
                    :style="{'background': touchConfig.index==key && touchConfig.pindex==index ? touchConfig.style.background : ''}"
                    @click.stop="_chooseItem($event)"
                    @touchstart="_btnTouchStart($event)"
                    @touchmove="_btnTouchEnd($event)"
                    @touchend="_btnTouchEnd($event)"
              >{
   {
    item.name }}
              </view>
            </view>
          </view>
        </view>
      </view>
    </view>
  </view>
</template><script>
export default {
   
  name: 'cascade-picker',
  props: {
   
    title: {
    //标题
      type: String,
      default: ''
    },
    layer: {
    //控制几级联动,默认无限级(跟随数据有无下级)
      type: Number,
      default: null
    },
    data: {
    //数据 如:[{id: '', name: '', children: [{id: '', name: ''}]}]
      type: Array,
      default() {
   
        return [
          // [{id: '', name: '', children: [{id: '', name: ''}]}]
        ]
      }
    }
  },
  data() {
   
    return {
   
      SHOW: false,
      FADE: -1,
      nav: [],
      items: [], // 第一级数据列表
      queryItems: [], // 各级数据列表
      navCurrentIndex: 0, // 当前选中的导航项索引
      navBorderLeft: 35, // 导航栏的边框左侧位置
      result: [],
      touchConfig: {
   
        index: -1,
        pindex: -1,
        style: {
   
          color: '#214579',
          background: '#EFEFEF'
        }
      },
      selectedData: [] // 用于回显数据的属性
    }
  },
  watch: {
   
    data() {
   
      const _this = this;
      const data = _this.data;
      _this.items = _this._flatten(data, '0')
    }
  },
  methods: {
   
    // 打开
    open() {
   
      const _this = this;
      _this.reset(); //打开时重置picker
      return new Promise(function (resolve, reject) {
   
        _this.SHOW = true;
        _this.FADE = 1;
        resolve();
      });
    },
    // 关闭
    close() {
   
      const _this = this;
      return new Promise(function (resolve, reject) {
   
        _this.FADE = 0;
        const _hidetimer = setTimeout(() => {
   
          _this.SHOW = false;
          _this.FADE = -1;
          clearTimeout(_hidetimer);
          resolve();
        }, 100)
      });
    },
    //重置
    reset() {
   
      const _this = this;
      _this.queryItems = [];
      _this.nav = [];
      _this.navBorderLeft = 35;
      _this.navCurrentIndex = 0;
      _this.result = [];
    },
    //导航栏切换
    _changeNav(e) {
   
      const _this = this;
      const index = Number(e.currentTarget.dataset.index);
      _this.navCurrentIndex = index;
      const _el = uni.createSelectorQuery().in(this).select(".aui-picker-navitem-" + index);
      _el.boundingClientRect(data => {
   
        _this.navBorderLeft = data.left + 20;
      }).exec();
    },
    //数据选择
    _chooseItem(e) {
   
      const _this = this;
      const id = e.currentTarget.dataset.id;
      const name = e.currentTarget.dataset.name;
      const pid = e.currentTarget.dataset.pid;
      const type = e.currentTarget.dataset.type;
      const _arr = [];
      _this.result[_this.navCurrentIndex] = {
   id: id, name: name, pid: pid, type: type};
      if (
        (!_this._isDefine(_this.layer) && _this._isDefine(_this._deepQuery(_this.data, id).children))
        ||
        (_this.navCurrentIndex < (Number(_this.layer) - 1) && _this._isDefine(_this._deepQuery(_this.data, id).children))
      ) {
    //有下级数据
        _this._deepQuery(_this.data, id).children.forEach(function (item, index) {
   
          _arr.push({
   id: item.id, name: item.name, pid: id, type: item.type});
        });
        if (_this.navCurrentIndex == _this.queryItems.length) {
    //选择数据
          _this.queryItems.push(_arr);
          _this.nav.push({
   name: name});
        } else {
    //重新选择数据
          _this.queryItems.splice(_this.navCurrentIndex + 1, 1);
          _this.nav.splice(_this.navCurrentIndex + 1, 1);
          _this.queryItems.splice(_this.navCurrentIndex, 1, _arr);
          _this.nav.splice(_this.navCurrentIndex, 1, {
   name: name});
        }
        _this.navCurrentIndex = _this.navCurrentIndex + 1;
        const _el = uni.createSelectorQuery().in(this).select(".aui-picker-navitem-" + _this.navCurrentIndex);
        setTimeout(() => {
   
          _el.boundingClientRect(data => {
   
            _this.navBorderLeft = data.left + 20;
          }).exec();
        }, 100)
      } else {
    //无下级数据且最后一级数据的type为1时,则可以确认关闭
        _this._confirm();
      }
    },
    _confirm() {
   
      const _this = this;
      const lastItem = _this.result[_this.result.length - 1];
      if (lastItem && lastItem.type === 1) {
   
        _this.close().then(() => {
   
          _this.$emit("callback", {
   status: 0, data: _this.result});
        });
      } else {
   
        uni.$u.toast('请选择设备')
      }
    },
    //递归遍历——将树形结构数据转化为数组格式
    _flatten(tree, pid) {
   
      return tree.reduce((arr, {
   id, name, type, children = []}) =>
        arr.concat([{
   id, name, pid, type}], this._flatten(children, id)), [])
    },
    //根据id查询对应的数据(如查询id=10100对应的对象)
    _deepQuery(tree, id) {
   
      let isGet = false;
      let retNode = null;function deepSearch(tree, id) {
   
        for (let i = 0; i < tree.length; i++) {
   
          if (tree[i].children && tree[i].children.length > 0) {
   
            deepSearch(tree[i].children, id);
          }
          if (id === tree[i].id || isGet) {
   
            isGet || (retNode = tree[i]);
            isGet = true;
            break;
          }
        }
      }deepSearch(tree, id);
      return retNode;
    },
    /***判断字符串是否为空
     @param {string} str 变量
     @example: aui.isDefine("变量");
     */
    _isDefine(str) {
   
      if (str == null || str == "" || str == "undefined" || str == undefined || str == "null" || str == "(null)" || str == 'NULL' || typeof (str) == 'undefined') {
   
        return false;
      } else {
   
        str = str + "";
        str = str.replace(/\s/g, "");
        if (str == "") {
   
          return false;
        }
        return true;
      }
    },
    _btnTouchStart(e) {
   
      const _this = this,
        index = Number(e.currentTarget.dataset.index),
        pindex = Number(e.currentTarget.dataset.pindex);
      _this.touchConfig.index = index;
      _this.touchConfig.pindex = pindex;
    },
    _btnTouchEnd(e) {
   
      const _this = this,
        index = Number(e.currentTarget.dataset.index),
        pindex = Number(e.currentTarget.dataset.pindex);
      _this.touchConfig.index = -1;
      _this.touchConfig.pindex = -1;
    },
  }
}
</script><style lang="scss" scoped>
/* ====================
  多级联动弹窗
 =====================*/
.aui-picker {
   
  width: 100vw;
  height: 100vh;
  //opacity: 0;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 999;
  /* display: none; */
}// 遮罩层
.aui-mask {
   
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, .3);
  position: absolute;
  left: 0;
  top: 0;
  z-index: 999;
}.aui-picker.aui-picker-in {
   
  -moz-animation: aui-fade-in .1s ease-out forwards;
  -ms-animation: aui-fade-in .1s ease-out forwards;
  -webkit-animation: aui-fade-in .1s ease-out forwards;
  animation: aui-fade-in .1s ease-out forwards;
}.aui-picker.aui-picker-out {
   
  -moz-animation: aui-fade-out .1s ease-out forwards;
  -ms-animation: aui-fade-out .1s ease-out forwards;
  -webkit-animation: aui-fade-out .1s ease-out forwards;
  animation: aui-fade-out .1s ease-out forwards;
}.aui-picker-main {
   
  width: 100vw;
  height: 50vh;
  background: #FFF;
  //border-radius: 15px 15px 0 0;
  position: absolute;
  left: 0px;
  bottom: 0;
  z-index: 999;
}.aui-picker.aui-picker-in .aui-picker-main {
   
  -moz-animation: aui-slide-up-screen .2s ease-out forwards;
  -ms-animation: aui-slide-up-screen .2s ease-out forwards;
  -webkit-animation: aui-slide-up-screen .2s ease-out forwards;
  animation: aui-slide-up-screen .2s ease-out forwards;
}.aui-picker.aui-picker-out .aui-picker-main {
   
  -moz-animation: aui-slide-down-screen .2s ease-out forwards;
  -ms-animation: aui-slide-down-screen .2s ease-out forwards;
  -webkit-animation: aui-slide-down-screen .2s ease-out forwards;
  animation: aui-slide-down-screen .2s ease-out forwards;
}.aui-picker-header {
   
  width: 100%;
  min-height: 50px;
  position: relative;
  z-index: 999;
  display: flex;
  justify-content: space-between;
  align-items: center;&-icon {
   
    font-size: 15px;
    color: #aaa;
    padding: 0 15px;
  }.aui-picker-confirm {
   
    height: 50px;
    line-height: 50px;
    text-align: center;
    font-size: 15px;
    color: $custom-content-color;
    padding: 0 15px;
  }
}.aui-picker-header::after {
   
  content: '';
  width: 100%;
  height: 1px;
  background: rgba(100, 100, 100, .3);
  -moz-transform: scaleY(.3);
  -ms-transform: scaleY(.3);
  -webkit-transform: scaleY(.3);
  transform: scaleY(.3);
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 999;
}.aui-picker-title {
   
  line-height: 20px;
  text-align: center;
  font-size: 17px;
  color: #333;
  padding: 15px;
  box-sizing: border-box;
  position: absolute;
  left: 50px;
  right: 50px;
  top: 0;
}.aui-picker-content {
   
  width: 100%;
  height: -webkit-calc(100% - 100px);
  height: calc(100% - 100px);
}.aui-picker-nav {
   
  width: 100%;
  height: 50px;
  text-align: left;
  padding: 0 15px;
  margin: 0 0 1px 0;
  justify-content: flex-start;
  white-space: nowrap;
  box-sizing: border-box;
  position: relative;
  overflow-x: scroll;
  overflow-y: hidden;
}.aui-picker-nav::after {
   
  content: '';
  width: 100%;
  height: 1px;
  background: rgba(100, 100, 100, .3);
  -moz-transform: scaleY(.3);
  -ms-transform: scaleY(.3);
  -webkit-transform: scaleY(.3);
  transform: scaleY(.3);
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 999;
}.aui-picker-navitem {
   
  width: 80px;
  line-height: 50px;
  font-size: 16px;
  margin: 0 30px 0 0;
  text-align: center;
  display: inline-block;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}.aui-picker-navitem.active {
   
  color: $custom-content-color;
}.aui-picker-navborder {
   
  width: 40px;
  height: 3px;
  background: $custom-content-color;
  border-radius: 5px;
  transition: left .15s;
  position: absolute;
  left: 40px;
  bottom: 0;
}.aui-picker-lists {
   
  width: 100%;
  height: 100%;
  justify-content: space-around;
  white-space: nowrap;
}.aui-picker-list {
   
  width: 100%;
  height: 100%;
  overflow: hidden;
  overflow-y: scroll;
  display: none;
  vertical-align: top;
}.aui-picker-list.active {
   
  display: inline-block;
}.aui-picker-list-warp {
   
  width: 100%;
  height: auto;
  box-sizing: border-box;
  padding: 15px 0;
  display: inline-block;
}.aui-picker-item {
   
  width: 100%;
  height: 50px;
  line-height: 50px;
  padding: 0 15px;
  box-sizing: border-box;
  font-size: 15px;
  color: #333;
  position: relative;
}.aui-picker-item.active {
   
  color: $custom-content-color;
}.aui-picker-item.active::after {
   
  content: '✔';
  font-size: 15px;
  color: $custom-content-color;
  position: absolute;
  top: 0px;
  right: 10px;
}</style>

总结

自定义多级联动选择器组件将有助于您在uni-app中创建灵活的选择器,以满足各种不同平台和级数的需求。如果有任何问题、反馈或需要进一步的帮助,请不要犹豫,在下面的评论中提出。期待听到您的声音,以便改进和完善这个组件。

目录
相关文章
|
6月前
|
XML Java Android开发
Android Studio App开发中改造已有的控件实战(包括自定义支付宝月份选择器、给翻页栏添加新属性、不滚动的列表视图 附源码)
Android Studio App开发中改造已有的控件实战(包括自定义支付宝月份选择器、给翻页栏添加新属性、不滚动的列表视图 附源码)
117 1
|
6月前
|
XML 搜索推荐 Java
Android App开发之自定义图形中位图与图形互转、剪裁图形内部区域、给图形添加部件的讲解及实战(附源码 简单易懂)
Android App开发之自定义图形中位图与图形互转、剪裁图形内部区域、给图形添加部件的讲解及实战(附源码 简单易懂)
67 0
|
6月前
|
XML 前端开发 Java
Android Studio App自定义控件中自定义视图的绘制讲解及实战(附源码 包括自定义绘制各种图形)
Android Studio App自定义控件中自定义视图的绘制讲解及实战(附源码 包括自定义绘制各种图形)
102 1
|
3月前
|
安全 前端开发 API
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
【Azure 应用服务】Azure Web App 服务默认支持一些 Weak TLS Ciphers Suite,是否有办法自定义修改呢?
|
3月前
|
Java 应用服务中间件 Windows
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
|
30天前
|
JavaScript 前端开发 UED
Vue与uni-app开发中通过@font-face巧妙引入自定义字体
Vue与uni-app开发中通过@font-face巧妙引入自定义字体
70 9
|
3月前
|
Java 应用服务中间件 Windows
【App Service for Windows】为 App Service 配置自定义 Tomcat 环境
【App Service for Windows】为 App Service 配置自定义 Tomcat 环境
|
3月前
|
PHP 开发工具 git
【Azure 应用服务】在 App Service for Windows 中自定义 PHP 版本的方法
【Azure 应用服务】在 App Service for Windows 中自定义 PHP 版本的方法
|
3月前
|
开发框架 .NET Docker
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
|
3月前
|
安全 Java 应用服务中间件
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法
【Azure 应用服务】App Service中,为Java应用配置自定义错误页面,禁用DELETE, PUT方法

热门文章

最新文章