<template> <view class="e-stat__select" :style="{ width: width, minWidth: minWidth }"> <!-- 主体区域 --> <view class="e-select-main"> <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" v-model="currentData" @input="filter" v-if="search && !disabled" /> <view class="e-select__input-text" v-else> {{ currentData || currentData === 0 ? currentData : placeholder }} </view> <!-- 用一个更大的盒子包裹图标,便于点击 --> <view class="e-select-icon" @click.stop="clearVal" v-if="currentData && clear && !disabled"> <uni-icons type="clear" color="#e1e1e1" size="18"></uni-icons> </view> <view class="e-select-icon" @click.stop="toggleSelector" v-else> <uni-icons size="14" color="#999" type="top" class="arrowAnimation" :class="showSelector ? 'top' : 'bottom'"></uni-icons> </view> </view> <!-- 全屏遮罩--> <view class="e-select--mask" v-if="showSelector" @click="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" v-show="showSelector"> <!-- 三角小箭头 --> <view class="e-popper__arrow"></view> <scroll-view scroll-y="true" :scroll-top="scrollTop" class="e-select__selector-scroll" :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)"> <text>{{ item[props.text] }}</text> <view id="scrollToId" v-if="currentData == item[props.text]"></view> </view> </scroll-view> <slot /> </view> </view> </view> </view> </template> <script> export default { name: 'e-select', data() { return { // 是否显示下拉选择列表 showSelector: false, // 当前选项 currentOptions: [], // 当前值 currentData: '', // 旧的滚动高度 oldScrollTop: 0, // 最新的滚动高度 scrollTop: 0, // 滚动至的id scrollToId: '', // 滚动动画 scrollWithAnimation: false, }; }, props: { // 选项列表 options: { type: Array, default() { return []; }, }, // 配置选项 props: { type: Object, default: function () { return { text: 'text', value: 'value', disabled: 'disabled', }; }, }, // vue2 v-model传值方式 value: { type: [String, Number], default: '', }, // vue3 v-model传值方式 modelValue: { type: [String, Number], default: '', }, // 占位 placeholder: { type: String, default: '请选择', }, // 宽度 width: { type: String, default: '100%', }, // 最小宽度 minWidth: { type: String, default: '120rpx', }, // 空值占位 emptyTips: { type: String, default: '暂无选项', }, // 是否可清除 clear: { type: Boolean, default: false, }, // 是否禁用 disabled: { type: Boolean, default: false, }, // 开启搜索 search: { type: Boolean, default: true, }, // 搜索开启滚动动画 animation: { type: Boolean, default: true, }, }, watch: { options: { handler() { this.currentOptions = this.options; this.initData(); }, immediate: true, deep: true, }, modelValue: { handler() { this.initData(); }, immediate: true, }, value: { handler() { this.initData(); }, immediate: true, }, }, methods: { /** 处理数据,此函数用于兼容vue2 vue3 */ initData() { this.currentData = ''; // vue2 if (this.value || this.value === 0) { for (let item of this.options) { if (item[this.props.value] === this.value) { this.currentData = item[this.props.text]; this.$emit('getText', this.currentData); return; } } } // vue3 if (this.modelValue || this.modelValue === 0) { for (let item of this.options) { if (item[this.props.value] === this.modelValue) { this.currentData = item[this.props.text]; this.$emit('getText', this.currentData); return; } } } }, /** 过滤选项列表,会自动回到顶部 */ filter() { this.$emit('getText', this.currentData); if (this.currentData) { this.currentOptions = this.options.filter((item) => { return item[this.props.text].indexOf(this.currentData) > -1; }); } else { this.currentOptions = this.options; } // scrollTop变化,才能触发滚动顶部 this.scrollTop = 1; this.$nextTick(() => { this.scrollTop = 0; }); }, /** 改变值 */ change(item, index) { if (item[this.props.disabled]) return; const data = { ...item, index, }; 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) { this.currentOptions = this.options; // scrollToId变化,才能触发scroll-to-view的滚动 this.scrollToId = ''; this.$nextTick(() => { this.scrollToId = 'scrollToId'; // 设计理念:只在filter时触发滚动动画,因为每次打开就触发,用户体验不好 if (this.animation) { setTimeout(() => { // 开启滚动动画 this.scrollWithAnimation = true; }, 100); } }); } else { // 关闭时关闭动画 this.scrollWithAnimation = false; } }, }, }; </script> <style lang="scss" scoped> .e-stat__select { display: flex; align-items: center; cursor: pointer; box-sizing: border-box; width: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .e-select-main { width: 100%; } .e-select-disabled { background-color: #f5f7fa; cursor: not-allowed; } .e-select { font-size: 14px; box-sizing: border-box; border-radius: 4px; padding: 0 5px; position: relative; display: flex; user-select: none; flex-direction: row; align-items: center; border: 1px solid #dcdfe6; border-bottom: solid 1px #dddddd; .e-select__input-box { width: 100%; min-height: 34px; position: relative; display: flex; flex: 1; flex-direction: row; align-items: center; .e-select-icon { width: 50px; height: 100%; display: flex; justify-content: center; align-items: center; } .arrowAnimation { transition: transform 0.3s; } .top { transform: rotateZ(0deg); } .bottom { transform: rotateZ(180deg); } .e-select__input-text { color: #303030; padding-left: 7px; width: 100%; color: #333; white-space: nowrap; text-overflow: ellipsis; -o-text-overflow: ellipsis; overflow: hidden; } .e-select__input-placeholder { padding-left: 7px; color: #666; } } .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; top: calc(100% + 12px); 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 4px; transition: all 2s; .e-popper__arrow, .e-popper__arrow::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-select__selector-scroll { max-height: 200px; box-sizing: border-box; .e-select__selector-empty, .e-select__selector-item { display: flex; cursor: pointer; line-height: 34px; font-size: 14px; text-align: center; padding: 0px 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; } } } } </style>