449 lines
11 KiB
Vue
449 lines
11 KiB
Vue
|
<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>
|