用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>
相关推荐
熊的猫几秒前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
速盾cdn7 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
四喜花露水40 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
工业甲酰苯胺1 小时前
C# 单例模式的多种实现
javascript·单例模式·c#
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称2 小时前
购物车-多元素组合动画css
前端·css