前言:
开发项目中有些页面是手机和pc两端显示,然后我用了vant的popup和picker,发现其组件的定位是position:fixed,这就导致在pc端这个popup和picker是全屏的非常不好看,最主要的是,这些组件在pc端和手机端的效果是不一样,手机端可以滑动,在pc就不行,也懒得去找了,所以自己写了一个在pc和手机端都可以滑动选择的弹出层picker!
效果图:
友情提示:
应开发需求,我分享的这picker代码是position: absolute绝对定位,相对于父盒子的大小来展示内容的,需要不同定位可以自己改css。
参考代码
(1)父组件代码
<template>
<div class="home">
<el-button type="primary" @click="showPickerBtn" >显示picker</el-button>
<div style="position: relative; width: 375px;height: 90vh;" >
<div>父盒子</div>
<div>选中的日期:{{ checkedDate }}</div>
<DatePicker v-model="showPicker" @dateConfirm="dateConfirm" ></DatePicker>
</div>
</div>
</template>
<script>
import DatePicker from '@/components/DatePicker.vue'
export default {
name: 'HomeView',
components: {
DatePicker
},
data() {
return {
showPicker: false,
checkedDate: '' // 接收picker返回的日期
}
},
methods:{
showPickerBtn() {
this.showPicker = !this.showPicker
},
dateConfirm(date) {
this.checkedDate = date
}
}
}
</script>
(2)picke代码
<!-- 日期选择器 -->
<template>
<!-- elementui 内置过渡动画 (可以自己写想要的过渡效果) -->
<transition name="el-fade-in-linear">
<div class="container" v-show="showPicker" >
<!-- 遮罩放冒泡 -->
<div class="modal" @click.stop="modalClick"></div>
<transition name="el-zoom-in-bottom">
<div v-show="showPicker" class="contentBox" @click.stop>
<div class="btnBox">
<span class="cancleBtn" @click="popBtn(0)">取消</span>
<span class="confirmBtn" @click="popBtn(1)">确定</span>
</div>
<div class="optionBox">
<div class="curCheckBox"></div>
<!-- 年 -->
<div ref="yearScrollRef" class="arrange"
@touchstart="onTouchStart($event, 0)"
@mousedown="startDrag($event, 0)"
@scroll="curCheckDate($event,0)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in yearOption" :key="index">
{{ item }}
<span style="font-size: 16px;" v-if="unit">年</span>
</div>
<div class="bottomVagueBox"></div>
</div>
<!-- 月 -->
<div ref="monthScrollRef" class="arrange"
@touchstart="onTouchStart($event, 1)"
@mousedown="startDrag($event, 1)"
@scroll="curCheckDate($event, 1)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in 12" :key="index">
<span v-if="item < 10">0</span>
{{ item }}
<span v-if="unit">月</span>
</div>
<div class="bottomVagueBox"></div>
</div>
<!-- 日 -->
<div ref="dayScrollRef" class="arrange"
@touchstart="onTouchStart($event, 2)"
@mousedown="startDrag($event, 2)"
@scroll="curCheckDate($event, 2)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in dayOption" :key="index">
<span v-if="item < 10">0</span>
{{ item }}
<span v-if="unit">日</span>
</div>
<div class="bottomVagueBox"></div>
</div>
<!-- 时 -->
<div v-if="type==='dateTime'" ref="hourScrollRef" class="arrange"
@touchstart="onTouchStart($event, 3)"
@mousedown="startDrag($event, 3)"
@scroll="curCheckDate($event, 3)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in 24" :key="index">
<span v-if="index < 10">0</span>
{{ index }}
<span v-if="unit">时</span>
</div>
<div class="bottomVagueBox"></div>
</div>
<!-- 分 -->
<div v-if="type==='dateTime'" ref="minuteScrollRef" class="arrange"
@touchstart="onTouchStart($event, 4)"
@mousedown="startDrag($event, 4)"
@scroll="curCheckDate($event, 4)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in 60" :key="index">
<span v-if="item < 10">0</span>
{{ item }}
<span v-if="unit">分</span>
</div>
<div class="bottomVagueBox"></div>
</div>
<!-- 秒 -->
<div v-if="type==='dateTime'" ref="secondScrollRef" class="arrange"
@touchstart="onTouchStart($event, 5)"
@mousedown="startDrag($event, 5)"
@scroll="curCheckDate($event, 5)"
>
<div class="topVagueBox"></div>
<div class="optionItem" v-for="(item, index) in 60" :key="index">
<span v-if="item < 10">0</span>
{{ index }}
<span v-if="unit">秒</span>
</div>
<div class="bottomVagueBox"></div>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script>
export default {
name:'DatePicker',
// 自定义v-model
model: {
prop: 'showPicker',
event: 'hidePicker'
},
props: {
// 是否显示picker
showPicker: {
type: Boolean,
default: true,
},
// 是否显示单位
unit: {
type: Boolean,
default: true,
},
// 时间类型 date=="YYYY-MM-DD" dateTime=="YYYY-MM-DD HH:mm:ss"
type: {
type: String,
default: 'date',
}
},
data() {
return {
// 年
yearOption: [],
// 月
monthOption: [],
// 日
dayOption: [],
// 选择的日期
checkedDate: [],
timer: null, // 防抖
isStop: false
};
},
mounted() {
const currentDate = new Date();
let currentYear = currentDate.getFullYear()
this.yearOption = Array(10).fill(null).map((_, i) => currentYear + i + ''); // 本年-后10年
this.dayOption = Array(31).fill(null).map((_, i) => i + 1); // 31天
this.defaultDate()
},
methods: {
// 默认日期
defaultDate() {
if(this.type==='date') {
this.checkedDate = [ this.yearOption[0], '01', '01' ]
} else {
this.checkedDate = [ this.yearOption[0], '01', '01', '00', '00', '00' ]
}
},
// 获取对应scroll的ref
getScrollTopRef(type) {
const scrollContainerRef = type===0 ? this.$refs.yearScrollRef
: type===1 ? this.$refs.monthScrollRef
: type===2 ? this.$refs.dayScrollRef
: type===3 ? this.$refs.hourScrollRef
: type===4 ? this.$refs.minuteScrollRef
: this.$refs.secondScrollRef
return scrollContainerRef
},
// 手指按下
onTouchStart(event, type) {
// event.preventDefault();
const scrollContainer = this.getScrollTopRef(type)
// 记录起始位置
const startY = event.touches[0].clientY;
const startScrollTop = scrollContainer.scrollTop;
// 手指移动
const onTouchMove = (moveEvent) => {
const dy = moveEvent.touches[0].clientY - startY;
scrollContainer.scrollTop = startScrollTop - dy;
};
// 手指松开
const onTouchEnd = () => {
scrollContainer.scrollTop = Math.round(scrollContainer.scrollTop / 50) * 50;
this.isStop = true
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('touchend', onTouchEnd);
};
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('touchend', onTouchEnd);
},
// 鼠标左键按下
startDrag(event,type) {
event.preventDefault();
const scrollContainer = this.getScrollTopRef(type)
const startY = event.clientY;
const startScrollTop = scrollContainer.scrollTop;
// 鼠标移动
const onMouseMove = (moveEvent) => {
const dy = moveEvent.clientY - startY;
scrollContainer.scrollTop = startScrollTop - dy
};
// 鼠标松开
const onMouseUp = () => {
scrollContainer.scrollTop = Math.round(scrollContainer.scrollTop / 50) * 50
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
},
// 滑动 type:0年 1月 2日 3时 4分 5秒
curCheckDate(event, type) {
clearInterval(this.timer)
this.timer = setTimeout(() => {
const scrollContainer = this.getScrollTopRef(type)
let num = scrollContainer.scrollTop / 50
if(this.isStop){
scrollContainer.scrollTop = Math.round(num) * 50;
this.isStop = false
}
const itemHeight = 50;
let curCheckedIndex = Math.round(event.target.scrollTop / itemHeight);
// 选择二月
if(type===1 && curCheckedIndex===1) {
this.dayOption = Array(28).fill(null).map((_, i) => i + 1); // 28天
}
// 选择非二月
if(type===1 && curCheckedIndex!=1) {
if([1,3,5,7,8,10,12].includes(curCheckedIndex+1)) {
this.dayOption = Array(31).fill(null).map((_, i) => i + 1); // 31天
} else {
this.dayOption = Array(30).fill(null).map((_, i) => i + 1)
}
}
// 补0
let time = curCheckedIndex<9 ? '0' + (curCheckedIndex+1) : '' + (curCheckedIndex+1)
let time1 = curCheckedIndex<9 ? '0' + (curCheckedIndex+1) : '' + (curCheckedIndex+1)
// 对应赋值
if(type===0) {
this.checkedDate[0] = this.yearOption[curCheckedIndex]
} else if(type===1) {
this.checkedDate[1] = time
} else if(type===2) {
this.checkedDate[2] = time
} else if(type===3) {
this.checkedDate[3] = time1
} else if(type===4) {
this.checkedDate[4] = time
} else {
this.checkedDate[5] = time1
}
}, 200);
},
// 弹窗按钮
popBtn(type) {
if(type === 1) {
this.$emit('dateConfirm', this.checkedDate);
}
this.$emit('hidePicker', false);
},
// 点击模态框
modalClick() {
this.$emit('hidePicker', false);
}
}
}
</script>
<style lang="scss" scoped>
.container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
user-select: none;
z-index: 3000; /* 确保弹出层位于其他内容之上 */
.modal {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.contentBox {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: #fff;
border-radius: 8px 8px 0 0;
padding: 12px;
box-sizing: border-box;
.btnBox {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
cursor: pointer;
.cancleBtn {
font-size: 16px;
color: #969799;
}
.confirmBtn {
font-size: 16px;
color: #19a7fc;
}
}
.optionBox {
display: flex;
align-items: center;
justify-content: space-around;
position: relative;
cursor: grab;
.curCheckBox {
position: absolute;
left: 0;
top: 100px;
width: 100%;
height: 50px;
border-top: 1px solid #d8d9da;
border-bottom: 1px solid #d8d9da;
}
.arrange {
position: relative;
height: 250px;
width: 80px;
overflow: scroll;
.optionItem {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
font-size: 16px;
line-height: 16px;
color: #817e7e;
font-weight: 600;
}
.topVagueBox {
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 100px;
background: rgba(255, 255, 255, 0.7);
}
.bottomVagueBox {
position: sticky;
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background: rgba(255, 255, 255, 0.7);
}
}
.arrange::-webkit-scrollbar {
display: none;
}
}
}
}
</style>
总结:
1、代码有注释,有问题可以在评论区留言,看到会及时回复。
2、需要改进的地方可以自己动动小手改一改,工时太赶,有空会优化再发。