<template> <view class="e-select-box" :style="{ width: width, minWidth: minWidth }"> <view class="e-select" :class="{ 'e-select-disabled': disabled }"> <view class="e-select-input-box" @click="toggleSelector"> <!-- 微信小程序input组件在部分安卓机型上会出现文字重影,placeholder抖动问题,2019年时微信小程序就有这个问题,一直没修复,估计短时间内也别指望修复了 --> <input class="e-select-input-text" :placeholder="placeholder" placeholder-class="e-select-input-placeholder" v-model="currentData" @input="filter" v-if="search && !disabled" /> <view class="e-select-input-text" :class="{ 'e-select-input-placeholder': !(currentData || currentData === 0), }" v-else> {{ currentData || currentData === 0 ? currentData : placeholder }} </view> <!-- 清空图标,用一个更大的盒子包裹图标,便于点击 --> <view class="e-select-icon" @click.stop="clearVal" v-if="currentData && clearable && !disabled"> <uni-icons type="clear" color="#e1e1e1" size="16"></uni-icons> </view> <!-- 箭头图标,同上 --> <view class="e-select-icon" @click.stop="toggleSelector" v-else> <uni-icons size="16" color="#6A6A6A" type="top" class="arrowAnimation" :class="showSelector ? 'top' : 'bottom'"></uni-icons> </view> </view> <!-- 全屏遮罩--> <view class="e-select--mask" v-if="showSelector" @click.stop="toggleSelector" /> <!-- 选项列表 这里用v-show是因为微信小程序会报警告 [Component] slot "" is not found,v-if会导致开发工具不能正确识别到slot --> <!-- https://developers.weixin.qq.com/community/minihome/doc/000c8295730700d1cd7c81b9656c00 --> <view class="e-select-selector" :style=" position === 'top' ? 'bottom: calc(0px + 42px)' : 'top: calc(100% + 12px)' " v-show="showSelector"> <!-- 三角小箭头 --> <view :class=" position === 'top' ? 'e-popper-arrow-bottom' : 'e-popper-arrow' "></view> <scroll-view scroll-y="true" :scroll-top="scrollTop" class="e-select-selector-scroll" :style="{ maxHeight: maxHeight }" :scroll-into-view="scrollToId" :scroll-with-animation="scrollWithAnimation" v-if="showSelector"> <view class="e-select-selector-empty" v-if="currentOptions.length === 0"> <text>{{ emptyTips }}</text> </view> <!-- 非空,渲染选项列表 --> <view v-else class="e-select-selector-item" :class="[ { highlight: currentData == item[props.text] }, { 'e-select-selector-item-disabled': item[props.disabled], }, ]" v-for="(item, index) in currentOptions" :key="index" @click="change(item, index)"> <view id="scrollToId" v-if="currentData == item[props.text]"></view> <text>{{ item[props.text] }}</text> </view> </scroll-view> <slot /> </view> </view> </view> </template> <script> export default { name: 'e-select', data() { return { // 是否显示下拉选择列表 showSelector: false, // 当前选项列表 currentOptions: [], // 过滤后的选项列表 filterOptions: [], // 当前值 currentData: '', // 滚动高度 scrollTop: 0, // 滚动至的id scrollToId: 'scrollToId', // 滚动动画 scrollWithAnimation: false, }; }, props: { // vue2 v-model传值方式 value: { type: [String, Number], default: '', }, // vue3 v-model传值方式 modelValue: { type: [String, Number], default: '', }, // 选项列表 options: { type: Array, default() { return []; }, }, // 选项列表自定义数据格式 props: { type: Object, default: () => { return { text: 'text', value: 'value', disabled: 'disabled', }; }, }, // 占位文本 placeholder: { type: String, default: '请选择', }, // 输入框宽度 width: { type: String, default: '100%', }, // 输入框最小宽度 minWidth: { type: String, default: '120rpx', }, // 选项列表悬浮框最大高度 maxHeight: { type: String, default: '160px', }, // 选项列表空值占位空值占位 emptyTips: { type: String, default: '暂无选项', }, // 是否可清空 clearable: { type: Boolean, default: false, }, // 是否禁用 disabled: { type: Boolean, default: false, }, // 是否开启搜索 search: { type: Boolean, default: true, }, // 是否开启搜索的滚动动画 animation: { type: Boolean, default: true, }, // 悬浮框位置top/bottom position: { type: String, default: 'bottom', }, // 分页每页条数 pageSize: { type: Number, default: 0, }, // 分页当前页数 pageIndex: { type: Number, default: 1, }, }, watch: { options: { handler(val) { this.filterOptions = val.slice(); this.initOptions(); this.initData(); }, immediate: true, deep: true, }, modelValue: { handler() { this.initData(); }, immediate: true, }, value: { handler() { this.initData(); }, immediate: true, }, pageSize() { this.initOptions(); }, pageIndex() { this.initOptions(); }, }, methods: { /** 处理数据,此函数用于兼容vue2 vue3 */ initData() { this.currentData = ''; // vue2 if (this.value || this.value === 0) { for (let i = 0; i < this.options.length; i++) { const item = this.options[i]; if (item[this.props.value] === this.value) { this.currentData = item[this.props.text]; this.$emit('getText', this.currentData); // 如果分页,初始化分页当前页数 if (this.pageSize && this.pageIndex) { this.$emit('update:pageIndex', Math.floor(i / this.pageSize) + 1); } return; } } } // vue3 else if (this.modelValue || this.modelValue === 0) { for (let i = 0; i < this.options.length; i++) { const item = this.options[i]; if (item[this.props.value] === this.modelValue) { this.currentData = item[this.props.text]; this.$emit('getText', this.currentData); if (this.pageSize && this.pageIndex) { this.$emit('update:pageIndex', Math.floor(i / this.pageSize) + 1); } return; } } } }, /** 初始化选项列表 */ initOptions() { // 设置分页情况下列表 if (this.pageSize && this.pageIndex) { this.currentOptions = this.filterOptions.slice( (this.pageIndex - 1) * this.pageSize, this.pageIndex * this.pageSize ); } else { this.currentOptions = this.filterOptions; } // scrollTop变化,才能触发滚动顶部,再低如0.01则不能触发,真神奇 this.scrollTop = 0.1; this.$nextTick(() => { this.scrollTop = 0; }); }, /** 过滤选项列表,会自动回到顶部 */ filter() { // 回到分页第一页 this.$emit('update:pageIndex', 1); this.$emit('getText', this.currentData); if (this.currentData) { this.filterOptions = this.options.filter((item) => { return item[this.props.text].indexOf(this.currentData) > -1; }); this.$emit('update:total', this.filterOptions.length); } // 等待update:pageIndex事件执行完成 setTimeout(() => { this.initOptions(); }, 0); }, /** 改变值 */ change(item, index) { if (item[this.props.disabled]) return; const data = { index, ...item, }; this.$emit('change', data); this.emit(data); this.toggleSelector(); }, /** 传递父组件值 */ emit(item) { this.$emit('input', item[this.props.value]); this.$emit('update:modelValue', item[this.props.value]); }, /** 清空值 */ clearVal() { this.$emit('change', 'clear'); this.$emit('input', ''); this.$emit('update:modelValue', ''); }, /** 切换下拉显示 */ toggleSelector() { if (this.disabled) return; this.showSelector = !this.showSelector; if (this.showSelector) { // 设计理念:只在filter时触发滚动动画,因为每次打开就触发,用户体验不好 if (this.animation) { setTimeout(() => { // 开启滚动动画 this.scrollWithAnimation = true; }, 100); } } else { // 关闭时重新初始化 this.filterOptions = this.options.slice(); this.initData(); this.initOptions(); this.$emit('update:total', this.options.length); this.scrollWithAnimation = false; } }, }, }; </script> <style lang="scss" scoped> .e-select-box { display: flex; align-items: center; width: 100%; box-sizing: border-box; cursor: pointer; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .e-select { width: 100%; border-radius: 4px; box-sizing: border-box; display: flex; align-items: center; user-select: none; position: relative; border: 1px solid #dcdfe6; } .e-select-disabled { background-color: #f5f7fa; cursor: not-allowed; } .e-select-input-box { width: 100%; padding: 0px 20rpx; min-height: 34px; position: relative; display: flex; flex: 1; flex-direction: row; align-items: center; .e-select-input-text { color: #303030; width: 100%; color: #333; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; overflow: hidden; font-size: 28rpx; } .e-select-input-placeholder { font-size: 28rpx; color: #999999; } .e-select-icon { width: 50px; padding-right: 3px; height: 100%; display: flex; justify-content: flex-end; align-items: center; } .arrowAnimation { transition: transform 0.3s; } .top { transform: rotateZ(0deg); } .bottom { transform: rotateZ(180deg); } } .e-select--mask { position: fixed; top: 0; bottom: 0; right: 0; left: 0; z-index: 999; } .e-select-selector { box-sizing: border-box; position: absolute; left: 0; width: 100%; background-color: #ffffff; border: 1px solid #ebeef5; border-radius: 6px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); z-index: 999; padding: 4px 2px; transition: all 1s; .e-select-selector-scroll { box-sizing: border-box; .e-select-selector-empty, .e-select-selector-item { display: flex; cursor: pointer; line-height: 35rpx; font-size: 28rpx; text-align: left; padding: 15rpx 10px; } .e-select-selector-item:hover { background-color: #f9f9f9; } .e-select-selector-empty:last-child, .e-select-selector-item:last-child { border-bottom: none; } .e-select-selector-item-disabled { color: #b1b1b1; cursor: not-allowed; } .highlight { color: #409eff; font-weight: bold; background-color: #f5f7fa; border-radius: 3px; } } } .e-popper-arrow, .e-popper-arrow::after, .e-popper-arrow-bottom, .e-popper-arrow-bottom::after { position: absolute; display: block; width: 0; height: 0; left: 50%; border-color: transparent; border-style: solid; border-width: 6px; } .e-popper-arrow { filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03)); top: -6px; left: 50%; transform: translateX(-50%); margin-right: 3px; border-top-width: 0; border-bottom-color: #ebeef5; } .e-popper-arrow::after { content: ' '; top: 1px; margin-left: -6px; border-top-width: 0; border-bottom-color: #fff; } .e-popper-arrow-bottom { filter: drop-shadow(0 2px 12px rgba(0, 0, 0, 0.03)); bottom: -6px; left: 50%; transform: translateX(-50%); margin-right: 3px; border-bottom-width: 0; border-top-color: #ebeef5; } .e-popper-arrow-bottom::after { content: ' '; bottom: 1px; margin-left: -6px; border-bottom-width: 0; border-top-color: #fff; } /* 设置定位元素的位置 */ #scrollToId { margin-top: -15rpx; } </style>