单选注意点:
-
@touchmove.prevent : 在
touchmove
事件上添加.prevent
修饰符,以阻止默认的滚动行为。 -
handleTouchStart : 记录触摸开始的 Y 坐标和当前的
translateY
值。 -
handleTouchMove : 计算触摸移动的距离,并更新
translateY
值。 -
handleTouchEnd : 根据
translateY
计算当前选中的索引,并更新translateY
值。 -
handleCancel: 触发取消事件。
-
handleConfirm: 触发确认事件,并传递当前选中的选项。
-
clampTranslateY : 确保
translateY
值在合理范围内。
多选注意点:
-
clickItem: 为选中的选项改变样式
-
handleConfirm: 触发确认事件,并传递当前选中的选项。
远程搜索多选注意点:
- 单独给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>