用vue2+elementUI封装手机端选择器picker组件,支持单选、多选、远程搜索多选

单选注意点:

  1. @touchmove.prevent: 在 touchmove 事件上添加 .prevent 修饰符,以阻止默认的滚动行为。

  2. handleTouchStart: 记录触摸开始的 Y 坐标和当前的 translateY 值。

  3. handleTouchMove: 计算触摸移动的距离,并更新 translateY 值。

  4. handleTouchEnd: 根据 translateY 计算当前选中的索引,并更新 translateY 值。

  5. handleCancel: 触发取消事件。

  6. handleConfirm: 触发确认事件,并传递当前选中的选项。

  7. clampTranslateY: 确保 translateY 值在合理范围内。

多选注意点:

  1. clickItem: 为选中的选项改变样式

  2. handleConfirm: 触发确认事件,并传递当前选中的选项。

远程搜索多选注意点:

     1. 单独给el-popper设置样式,发现无效,原因是el-popper和<div id="app">...</div>组件处于同一层级,解决方法是使用popper-class属性给el-popper定义一个class,另外在style中去掉scoped。

效果如下:

        

单选/多选picker.vue组件:

<template>
  <div>
    <div class="picker-mask"></div>
    <div
      class="picker"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchmove.prevent="handleTouchMove"
      @touchend="handleTouchEnd"
    >
      <div class="picker-actions">
        <el-button type="text" @click="handleCancel">取消</el-button>
        <el-button type="text" @click="handleConfirm">确认</el-button>
      </div>
      <div class="picker-box" v-if="chooseOptions.length">
        <div
          class="picker-content"
          :style="{ transform: `translateY(${translateY}px)` }"
        >
          <div
            v-for="(item, index) in chooseOptions"
            :key="index"
            :class="item.chooseFlag ? 'choose-item' : 'picker-item'"
            @click="multiple ? clickItem(index) : null"
          >
            {{ labelKey ? item[labelKey] : item }}
          </div>
        </div>
      </div>
      <div class="empty" v-else>暂无数据</div>
      <div
        v-if="chooseOptions.length && !multiple"
        class="picker-highlight"
      ></div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    options: {
      type: Array,
      default: () => []
    },
    labelKey: {
      type: String,
      default: ''
    },
    selectedOption: {
      type: [Object, Array],
      default: () => []
    },
    multiple: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      startY: 0,
      translateY: 40,
      currentIndex: 0,
      startTranslateY: 40,
      chooseOptions: this.multiple
        ? this.options?.map(v => ({ ...v, chooseFlag: false })) || []
        : this.options || []
    };
  },

  mounted() {
    if (this.multiple && this.selectedOption?.length) {
      console.log('selectedOption', this.selectedOption);
      console.log(this.options);
      this.chooseOptions =
        this.options?.map(v => ({
          ...v,
          chooseFlag: this.selectedOption?.some(
            item => item[this.labelKey] === v[this.labelKey]
          )
        })) || [];
    }
    // 根据选项列表和当前选中项,设置当前索引和滚动位置
    if (!this.multiple && this.options.indexOf(this.selectedOption) !== -1) {
      this.currentIndex = this.options.indexOf(this.selectedOption);
      this.translateY = -40 * this.currentIndex + 40;
    }
  },
  methods: {
    // 记录触摸开始的 Y 坐标和当前的 translateY 值
    handleTouchStart(event) {
      this.startY = event.touches[0].clientY;
      this.startTranslateY = this.translateY ?? 0;
    },
    // 计算触摸移动的距离,并更新 translateY 值。
    handleTouchMove(event) {
      const deltaY = event.touches[0].clientY - this.startY;
      this.translateY = this.startTranslateY + deltaY;
      this.clampTranslateY();
    },
    // 根据 translateY 计算当前选中的索引,并更新 translateY 值
    handleTouchEnd() {
      const index = Math.round(this.translateY / 40);
      this.translateY = index * 40;
      this.currentIndex = -Math.round((this.translateY - 40) / 40);
    },
    // 确保 translateY 值在合理范围内
    clampTranslateY() {
      const itemHeight = 40;
      const maxTranslateY = 40;
      const minTranslateY =
        maxTranslateY - (this.options.length - 1) * itemHeight;
      this.translateY = Math.max(
        minTranslateY,
        Math.min(maxTranslateY, this.translateY)
      );
    },
    clickItem(index) {
      this.chooseOptions[index].chooseFlag =
        !this.chooseOptions[index].chooseFlag;
    },
    handleCancel() {
      console.log('cancel');
      this.$emit('cancel');
    },
    handleConfirm() {
      const result = this.multiple
        ? this.chooseOptions?.filter(v => v.chooseFlag)
        : this.chooseOptions?.[this.currentIndex];
      this.$emit('confirm', result);
    }
  }
};
</script>

<style scoped>
.picker-mask {
  z-index: 2014;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 8px;
}
.picker {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 200px;
  overflow: hidden;
  background-color: #fff;
  z-index: 2014;
  color: #323233;
  font-size: 16px;
  border-radius: 8px;
}
.picker-box {
  flex: 1;
  overflow: hidden;
}

.picker-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  transition: transform 0.3s ease;
}

.picker-item {
  height: 40px;
  line-height: 40px;
  text-align: center;
  width: 100%;
}

.picker-highlight {
  position: absolute;
  top: 60%;
  left: 0;
  width: 100%;
  height: 40px;
  transform: translateY(-50%);
  background-color: rgba(255, 255, 255, 0.7);
  border-top: 1px solid #ccc;
  border-bottom: 1px solid #ccc;
}

.picker-actions {
  width: 100%;
  display: flex;
  justify-content: space-between;
  padding: 10px 0;
  z-index: 2024;
  background-color: #fff;

  .el-button {
    margin: 0 8px;
  }
}

.choose-item {
  height: 40px;
  line-height: 40px;
  text-align: center;
  color: #2c68ff;
  width: 100%;
}

.choose-item::after {
  position: absolute;
  right: 20px;
  font-family: 'element-icons';
  content: '';
  font-size: 12px;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}

.empty {
  text-align: center;
}
</style>

远程搜索多选picker组件,可以增加远程搜索属性,自己改造使用:

<template>
  <div>
    <div class="picker-mask"></div>
    <div class="picker">
      <div class="picker-actions">
        <el-button type="text" @click="handleCancel">取消</el-button>
        <el-button type="text" @click="handleConfirm">确认</el-button>
      </div>
      <div class="picker-box">
        <el-select
          collapse-tags
          filterable
          multiple
          value-key="id"
          default-first-option
          :clearable="true"
          v-model="selectList"
          placeholder="请输入"
          popper-class="select-popper"
          @change="handleChange"
        >
          <el-option
            v-for="item in userSuccessorNowList"
            :key="item.id"
            :value="item"
            :label="item.vname"
          />
        </el-select>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    selectedOption: {
      type: Array,
      default: () => []
    },
    labelKey: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      selectList: [],
      userSuccessorNowList: [
        { id: 1, vname: 'aaa' },
        { id: 2, vname: 'bbb' },
        { id: 3, vname: 'ccc' },
        { id: 4, vname: 'aaa' },
        { id: 5, vname: 'bbb' },
        { id: 6, vname: 'ccc' }
      ]
    };
  },

  mounted() {
    if (this.selectedOption?.length) {
      console.log('selectedOption', this.selectedOption);
      this.selectList = this.selectedOption;
    }
  },
  methods: {
    handleChange(val) {
      this.selectList = val;
      console.log('selectList', this.selectList);
    },
    handleCancel() {
      this.$emit('cancel');
    },
    handleConfirm() {
      this.$emit('confirm', this.selectList);
    }
  }
};
</script>
<style>
.select-popper {
  width: 100vw !important;
  z-index: 2014 !important;
  left: 0 !important;
  box-shadow: none;
  height: 120px !important;
  overflow: auto;
  .popper__arrow {
    display: none !important;
  }
  .el-scrollbar__view {
    text-align: center;
  }
}
</style>

<style scoped>
.picker-mask {
  z-index: 1014;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.7);
  border-radius: 8px;
}
.picker {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 250px;
  overflow: hidden;
  background-color: #fff;
  z-index: 1014;
  color: #323233;
  font-size: 16px;
  border-radius: 8px;

  .el-select {
    width: 100%;
    .el-input__suffix {
      display: none;
    }
  }
}

.picker-actions {
  width: 100%;
  display: flex;
  justify-content: space-between;
  padding: 10px 0;
  z-index: 2024;
  background-color: #fff;

  .el-button {
    margin: 0 8px;
  }
}

.picker-box {
  flex: 1;
  overflow: hidden;
}
</style>

选择器父组件:

<template>
  <div>
    <div class="select-box" @click="togglePicker">
      <div class="select-content">
        <div class="select-label">
          <span class="label">
            {{ label }}
            <span v-if="required" style="color: rgba(253, 75, 76, 1)"> * </span>
          </span>
        </div>
        <span
          class="select-value"
          :style="{ color: `${!selectedOption ? 'rgba(0,0,0,0.25)' : ''}` }"
          >{{ selectedOption ? showResults(selectedOption) : placeholder }}
        </span>
      </div>
      <i class="el-icon-arrow-right"></i>
    </div>
    <div v-if="showPicker">
      <SearchPicker
        v-if="pickerType === 'search'"
        :options="options"
        :labelKey="labelKey"
        @confirm="handleConfirm"
        @cancel="handleCancel"
        :selectedOption="selectedOption"
      />
      <Picker
        v-else
        :options="options"
        :labelKey="labelKey"
        @confirm="handleConfirm"
        @cancel="handleCancel"
        :selectedOption="selectedOption"
        :multiple="multiple"
      />
    </div>
  </div>
</template>

<script>
import Picker from '../Picker';
import SearchPicker from '../SearchPicker';

export default {
  components: {
    Picker,
    SearchPicker
  },
  props: {
    label: {
      type: String,
      default: ''
    },
    pickerType: {
      type: String,
      default: ''
    },
    required: {
      type: Boolean,
      default: false
    },
    options: {
      type: Array,
      default: () => []
    },
    labelKey: {
      type: String,
      default: ''
    },
    placeholder: {
      type: String,
      default: '请选择'
    },
    multiple: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      selectedOption: null,
      showPicker: false
    };
  },

  mounted() {
    this.getResults();
  },

  methods: {
    getResults() {
      if (this.multiple) {
        this.selectedOption = this.selectedOption || null;
      } else {
        this.selectedOption = this.selectedOption?.[this.labelKey];
      }
    },
    showResults(selectedOption) {
      if (this.multiple) {
        return selectedOption.map(v => v[this.labelKey]).join(',');
      } else {
        return selectedOption;
      }
    },
    togglePicker() {
      this.showPicker = !this.showPicker;
    },
    handleConfirm(selectedOption) {
      if (this.multiple) {
        this.selectedOption = selectedOption?.length ? selectedOption : null;
      } else {
        this.selectedOption = selectedOption;
      }
      this.showPicker = false;
      this.getResults();
    },
    handleCancel() {
      this.showPicker = false;
    }
  }
};
</script>

<style lang="scss" scoped>
.select-box {
  width: 100%;
  height: 48px;
  line-height: 40px;
  text-align: left;
  background-color: #fff;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;

  .select-content {
    display: flex;
    align-items: center;
    .select-label {
      width: 90px;
    }
    .select-value {
      @include textElipsis(1);
      width: calc(100% - 90px);
    }
  }

  span {
    font-family: PingFangSC, PingFang SC;
    font-weight: 400;
    font-size: 16px;
    color: rgba(0, 0, 0, 0.85);
    line-height: 24px;
    text-align: left;
    font-style: normal;
    text-transform: none;
  }
  i {
    color: rgba(0, 0, 0, 0.45);
  }
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/773516.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字经济时代:AI+引领企业数字化新高度

随着新一轮科技革命和产业变革深入发展&#xff0c;5G、大数据、云计算等技术的广泛应用&#xff0c;数字经济不仅在中国&#xff0c;且在全球范围内&#xff0c;都被视为推动经济增长的关键动力。以人工智能&#xff08;AI&#xff09;为例&#xff0c;《全球数字经济白皮书&a…

大学生竞赛管理系统-计算机毕业设计源码37276

大学生竞赛管理系统的设计与实现 摘 要 随着教育信息化的不断发展&#xff0c;大学生竞赛已成为高校教育的重要组成部分。传统的竞赛组织和管理方式存在着诸多问题&#xff0c;如信息不透明、效率低下、管理不便等。为了解决这些问题&#xff0c;提高竞赛组织和管理效率&#x…

第一节-k8s架构图

一个Deployment&#xff0c;可以由多个不同Node下的Pod组成&#xff0c;每个Pod又由多个Container组成。 区分Deployment是用Labels(key:value)&#xff0c;区分Pod是用PodName&#xff0c;区分Container是用ContainerName。 一个Node可以包含多个不同Deployment中的pod&…

马拉松报名小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;赛事信息管理&#xff0c;赛事报名管理&#xff0c;活动商城管理&#xff0c;留言板管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;赛事信息&…

【动态规划】动态规划一

动态规划一 1.第 N 个泰波那契数2.面试题 08.01. 三步问题3.使用最小花费爬楼梯4.解码方法 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f603; 1.…

Android - 利用 jitpack 免费发布闭源 aar

一、简述 目前(Android/java) library 的主要发布仓库有 MavenCentral 和 jitpack,我之前也对这两种仓库的发布流程做了详细介绍: 发布至 MavenCentral: https://juejin.cn/post/6953598441817636900发布至 jitpack: https://juejin.cn/post/7040733114506674183#heading-…

AI Agent框架(LLM Agent):LLM驱动的智能体如何引领行业变革,应用探索与未来展望

AI Agent框架&#xff08;LLM Agent&#xff09;&#xff1a;LLM驱动的智能实体如何引领行业变革&#xff0c;应用探索与未来展望 1. AI Agent&#xff08;LLM Agent&#xff09;介绍 1.1. 术语 Agent&#xff1a;“代理” 通常是指有意行动的表现。在哲学领域&#xff0c;Ag…

Unreal Engine@Jetson Orin Nano尚不支持

Unreal EngineJetson Orin Nano尚不支持 1. 源由2. Unreal Engine介绍3. 问题4. 编译方法5. 补充6. 其他 1. 源由 最近在看SC-Explorer方面的内容&#xff0c;在模拟方面采用了Unreal Engine。 本打算跑下模拟&#xff0c;因此打算在JetsonOrin的板子上试试看。 2. Unreal En…

油猴Safari浏览器插件:Tampermonkey for Mac 下载

Tampermonkey 是一个强大的浏览器扩展&#xff0c;用于运行用户脚本&#xff0c;这些脚本可以自定义和增强网页的功能。它允许用户在网页上执行各种自动化任务&#xff0c;比如自动填写表单、移除广告、改变页面布局等。适用浏览器&#xff1a; Tampermonkey 适用于多数主流浏览…

python 发布应用程序包

文章目录 发布python包toml配置文件构建发布python包 官方文档参考 将自己的python项目发布成源码包或者wheel二进制包,供其他开发者使用。 方式: 使用py工具; distutils,该工具的使用已过时;setuptools,常用方式;wheel,在setuptools的基础上添加了 bdist_wheel, …

Hi3861 OpenHarmony嵌入式应用入门--SNTP

sntp&#xff08;Simple Network Time Protocol&#xff09;是一种网络时间协议&#xff0c;它是NTP&#xff08;Network Time Protocol&#xff09;的一个简化版本。 本项目是从LwIP中抽取的SNTP代码&#xff1b; Hi3861 SDK中已经包含了一份预编译的lwip&#xff0c;但没有…

SpringCloud集成Oauth2.0看完这个基本就理解原理了

目录 1.技术栈准备工作 2. 模块架构介绍 3.网关模块&#xff08;gateway&#xff09; 3.1 网关模块(gateway) 3.2 附上主要依赖包 3.3 bootstrap 相关配置 3.4 gateway.yaml 3.5 UserAuthGlobalFiter 全局拦截器配置 4.授权认证模块(auth) 4.1 启用web安全认证&#xff…

使用LocalDateTime替代Date操作处理日期时间之后:任凭风浪起,稳坐钓鱼台

1.概述 在日常开发系统过程中&#xff0c;日期和时间的操作处理是一个常见的应用功能场景&#xff0c;Java提供了多种工具和库来处理日期和时间操作&#xff0c;其中主要分为&#xff1a;Java 8之前的提供java.util.Date、java.util.Calendar。Java 8引入了全新的日期时间API&…

无线网卡怎么连接台式电脑?让上网更便捷!

随着无线网络的普及&#xff0c;越来越多的台式电脑用户希望通过无线网卡连接到互联网。无线网卡为台式电脑提供了无线连接的便利性&#xff0c;避免了有线网络的束缚。本文将详细介绍无线网卡怎么连接台式电脑的四种方法&#xff0c;包括使用USB无线网卡、内置无线网卡以及使用…

004 线程的状态

文章目录 Java线程可能的状态&#xff1a; 状态名称说明NEW初始状态&#xff0c;线程被构建&#xff0c;但是还没有调用start()方法RUNNABLE运行状态&#xff0c;Java线程将操作系统中的就绪和运行两种状态笼统地称作"运行中"BLOCKED阻塞状态&#xff0c;表示线程阻…

Websocket通信实战项目(图片互传应用)+PyQt界面+python异步编程(async) (上)服务器端python实现

Rqtz : 个人主页 ​​ 共享IT之美&#xff0c;共创机器未来 ​ Sharing the Beauty of IT and Creating the Future of Machines Together 目录 项目背景 ​编辑​专有名词介绍 服务器GUI展示 功能(位置见上图序号) 客户端GUI展示&#xff08;h5cssjs&#xf…

超融合服务器挂载硬盘--linux系统

项目中需要增加服务器的硬盘容量&#xff0c;通过超融合挂载了硬盘后&#xff0c;还需要添加到指定的路径下&#xff0c;这里记录一下操作步骤。 一&#xff1a;通过管理界面挂载硬盘 这一步都是界面操作&#xff0c;登录超融合控制云台后&#xff0c;找到对应的服务器&#…

Spring Boot 文件上传和下载指南:从基础到进阶

文章目录 引言1. 环境配置2. 文件上传2.1 配置文件上传路径2.2 创建上传服务2.3 创建上传控制器 3. 文件下载3.1 创建下载服务3.2 创建下载控制器 4. 前端页面4.1 文件上传页面4.2 文件下载页面 5. 技术分析结论 &#x1f389;欢迎来到SpringBoot框架学习专栏~ ☆* o(≧▽≦)o …

设置单实例Apache HTTP服务器

配置仓库 [rootlocalhost ~]# cd /etc/yum.repos.d/ [rootlocalhost yum.repos.d]# vi rpm.repo仓库代码&#xff1a; [BaseOS] nameBaseOS baseurl/mnt/BaseOS enabled1 gpgcheck0[AppStream] nameAppStream baseurl/mnt/AppStream enabled1 gpgcheck0挂载 [rootlocalhost …

数字IC设计-VCS和Verdi的使用

#学习记录# 前言&#xff1a;本文以一个简单的计数器来说明vcs和verdi的使用 1 代码文件 1.1 计数器代码 //Engineer&#xff1a;Mr-pn-junction module counter(input clk,input rst,output reg [5:0] count); always(posedge clk or negedge rst)beginif(!rst)coun…