You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

628 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<u-popup :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
:safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex">
<view class="u-datetime-picker">
<view class="u-picker-header" @touchmove.stop.prevent="">
<view class="u-btn-picker u-btn-picker--tips" :style="{ color: cancelColor }" hover-class="u-opacity"
:hover-stay-time="150" @tap="getResult('cancel')">{{cancelText}}</view>
<view class="u-picker__title">{{ title }}</view>
<view class="u-btn-picker u-btn-picker--primary" :style="{ color: moving ? cancelColor : confirmColor }"
hover-class="u-opacity" :hover-stay-time="150" @touchmove.stop="" @tap.stop="getResult('confirm')">
{{confirmText}}
</view>
</view>
<view class="u-picker-body">
<picker-view :value="valueArr" @change="change" class="u-picker-view" @pickstart="pickstart"
@pickend="pickend">
<picker-view-column v-if="!reset && params.year">
<view class="u-column-item" v-for="(item, index) in years" :key="index">
{{ item }}
<text class="u-text" v-if="showTimeTag">年</text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.month">
<view class="u-column-item" v-for="(item, index) in months" :key="index">
{{ formatNumber(item) }}
<text class="u-text" v-if="showTimeTag">月</text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.day">
<view class="u-column-item" v-for="(item, index) in days" :key="index">
{{ formatNumber(item) }}
<text class="u-text" v-if="showTimeTag">日</text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.hour">
<view class="u-column-item" v-for="(item, index) in hours" :key="index">
{{ formatNumber(item) }}
<text class="u-text" v-if="showTimeTag">时</text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.minute">
<view class="u-column-item" v-for="(item, index) in minutes" :key="index">
{{ formatNumber(item) }}
<text class="u-text" v-if="showTimeTag">分</text>
</view>
</picker-view-column>
<picker-view-column v-if="!reset && params.second">
<view class="u-column-item" v-for="(item, index) in seconds" :key="index">
{{ formatNumber(item) }}
<text class="u-text" v-if="showTimeTag"></text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</u-popup>
</template>
<script>
export default {
name: 'u-picker',
props: {
// picker中需要显示的参数
params: {
type: Object,
default () {
return {
year: true,
month: true,
day: true,
hour: false,
minute: false,
second: false,
timestamp: true,
};
}
},
// 当mode=selector或者mode=multiSelector时提供的数组
range: {
type: Array,
default () {
return [];
}
},
// 当mode=selector或者mode=multiSelector时提供的默认选中的下标
defaultSelector: {
type: Array,
default () {
return [0];
}
},
// 当 range 是一个 ArrayObject 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
rangeKey: {
type: String,
default: ''
},
// 模式选择region-地区类型time-时间类型selector-单列模式multiSelector-多列模式
mode: {
type: String,
default: 'time'
},
// 年份开始时间
startDate: {
type: String,
default: '1899-01-01 00:00:00'
},
// 年份结束时间
endDate: {
type: String,
default: '2250-12-31 23:59:59'
},
// "取消"按钮的颜色
cancelColor: {
type: String,
default: '#606266'
},
// "确定"按钮的颜色
confirmColor: {
type: String,
default: '#2979ff'
},
// 默认显示的时间2025-07-02 || 2025-07-02 13:01:00 || 2025/07/02
defaultTime: {
type: String,
default: ''
},
// 时间模式时,是否显示后面的年月日中文提示
showTimeTag: {
type: Boolean,
default: true
},
safeAreaInsetBottom: {
type: Boolean,
default: false
},
// 是否允许通过点击遮罩关闭Picker
maskCloseAble: {
type: Boolean,
default: true
},
// 通过双向绑定控制组件的弹出与收起
value: {
type: Boolean,
default: false
},
// 弹出的z-index值
zIndex: {
type: [String, Number],
default: 0
},
// 顶部标题
title: {
type: String,
default: ''
},
// 取消按钮的文字
cancelText: {
type: String,
default: '取消'
},
// 确认按钮的文字
confirmText: {
type: String,
default: '确认'
},
format: {
type: String,
default: 'yyyy-MM-dd HH:mm:ss'
}
},
data() {
return {
years: [],
months: [],
days: [],
hours: [],
minutes: [],
seconds: [],
year: 0,
month: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
reset: false,
valueArr: [],
moving: false // 列是否还在滑动中,微信小程序如果在滑动中就点确定,结果可能不准确
};
},
mounted() {
this.init();
},
computed: {
propsChange() {
// 引用这几个变量,是为了监听其变化
return `${this.mode}-${this.defaultTime}-${this.startYear}-${this.endYear}-${this.defaultRegion}-${this.areaCode}`;
},
yearAndMonth() {
return `${this.year}-${this.month}`;
},
uZIndex() {
// 如果用户有传递z-index值优先使用
return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
}
},
watch: {
propsChange() {
this.reset = true;
setTimeout(() => this.init(), 10);
},
// watch监听月份的变化实时变更日的天数因为不同月份天数不一样
// 一个月可能有3031天甚至闰年2月的29天平年2月28天
yearAndMonth(val) {
if (this.params.year) this.setDays();
},
// 微信和QQ小程序由于一些奇怪的原因(故同时对所有平台均初始化一遍),需要重新初始化才能显示正确的值
value(n) {
if (n) {
this.reset = true;
setTimeout(() => this.init(), 10);
}
}
},
methods: {
// 标识滑动开始,只有微信小程序才有这样的事件
pickstart() {
// #ifdef MP-WEIXIN
this.moving = true;
// #endif
},
// 标识滑动结束
pickend() {
// #ifdef MP-WEIXIN
this.moving = false;
// #endif
},
// 生成递进的数组
getIndex: function(arr, val) {
let index = arr.indexOf(val);
// 如果index为-1(即找不到index值)~(-1)=-(-1)-1=0导致条件不成立
return ~index ? index : 0;
},
//日期时间处理
initTimeValue() {
// 格式化时间在IE浏览器(uni不存在此情况),无法识别日期间的"-"间隔符号
let fdate = this.defaultTime.replace(/\-/g, '/');
fdate = fdate && fdate.indexOf('/') == -1 ? `1899/01/01 ${fdate}` : fdate;
let time = null;
if (fdate) time = new Date(fdate);
else time = new Date();
// 获取年日月时分秒
this.year = time.getFullYear();
this.month = Number(time.getMonth()) + 1;
this.day = time.getDate();
this.hour = time.getHours();
this.minute = time.getMinutes();
this.second = time.getSeconds();
},
init() {
this.valueArr = [];
this.reset = false;
this.initTimeValue();
if (this.params.year) {
this.valueArr.push(0);
this.setYears();
}
if (this.params.month) {
this.valueArr.push(0);
this.setMonths();
}
if (this.params.day) {
this.valueArr.push(0);
this.setDays();
}
if (this.params.hour) {
this.valueArr.push(0);
this.setHours();
}
if (this.params.minute) {
this.valueArr.push(0);
this.setMinutes();
}
if (this.params.second) {
this.valueArr.push(0);
this.setSeconds();
}
this.$forceUpdate();
},
// 设置picker的某一列值
setYears() {
// 获取年份集合
this.generateArray('year');
if (this.years[0] > this.year) this.year = this.years[0]
if (this.years[this.years.length - 1] < this.year) this.year = this.years[this.years.length - 1]
// 设置this.valueArr某一项的值是为了让picker预选中某一个值
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.years, this.year));
},
setMonths() {
this.generateArray('month');
if (this.months[0] > this.month) this.month = this.months[0]
if (this.months[this.months.length - 1] < this.month) this.month = this.months[this.months.length - 1]
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.months, this.month));
},
setDays() {
let totalDays = new Date(this.year, this.month, 0).getDate();
this.generateArray('day');
let index = 0;
// 这里不能使用类似setMonths()中的this.valueArr.splice(this.valueArr.length - 1, xxx)做法
// 因为this.month和this.year变化时会触发watch中的this.setDays()导致this.valueArr.length计算有误
if (this.params.year && this.params.month) index = 2;
else if (this.params.month) index = 1;
else if (this.params.year) index = 1;
else index = 0;
// 当月份变化时,会导致日期的天数也会变化,如果原来选的天数大于变化后的天数,则重置为变化后的最大值
// 比如原来选中3月31日调整为2月后日期变为最大29这时如果day值继续为31显然不合理于是将其置为29(picker-column从1开始)
// if (this.day > this.days.length) this.day = this.days.length;
if (this.days[0] > this.day) this.day = this.days[0]
if (this.days[this.days.length - 1] < this.day) this.day = this.days[this.days.length - 1]
this.valueArr.splice(index, 1, this.getIndex(this.days, this.day));
},
setHours() {
this.generateArray('hour');
if (this.hours[0] > this.hour) this.hour = this.hours[0]
if (this.hours[this.hours.length - 1] < this.hour) this.hour = this.hours[this.hours.length - 1]
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.hours, this.hour));
},
setMinutes() {
this.generateArray('minute');
if (this.minutes[0] > this.minute) this.minute = this.minutes[0]
if (this.minutes[this.minutes.length - 1] < this.minute) this.minute = this.minutes[this.minutes.length -
1]
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.minutes, this.minute));
},
setSeconds() {
this.generateArray('second');
if (this.seconds[0] > this.second) this.second = this.seconds[0]
if (this.seconds[this.seconds.length - 1] < this.second) this.second = this.seconds[this.seconds.length -
1]
this.valueArr.splice(this.valueArr.length - 1, 1, this.getIndex(this.seconds, this.second));
},
generateArray(type) {
let startArr = this.startDate.split(" "); //开始日期时间
let endArr = this.endDate.split(" "); //结束日期时间
if (!this.month) this.month = this.months[this.months.length - 1]
let totalDays = new Date(this.year, this.month, 0).getDate(); //当月天数
//开始年月日时分秒
let startDateArr = startArr[0] ? startArr[0].split("-") : []; //开始日期
let startTimeArr = startArr[1] ? startArr[1].split(":") : []; //开始时间
let startYear = Number(startDateArr[0]) || 1; //开始年
let startMonth = Number(startDateArr[1]) || 1; //开始月
let startDay = Number(startDateArr[2]) || 1; //开始天数
let startHour = Number(startTimeArr[0]) || 0 //开始小时
let startMinute = Number(startTimeArr[1]) || 0 //开始分钟
let startSecond = Number(startTimeArr[2]) || 0 //开始秒
//结束年月日时分秒
let endDateArr = endArr[0] ? endArr[0].split("-") : [] //结束日期
let endTimeArr = endArr[1] ? endArr[1].split(":") : []; //结束时间
let endYear = Number(endDateArr[0]) || 12; //结束年
let endMonth = Number(endDateArr[1]) || 12; //结束月
let endDay = Number(endDateArr[2]) || totalDays; //结束天数
let endHour = Number(endTimeArr[0]) || 0 //结束小时
let endMinute = Number(endTimeArr[1]) || 0 //结束分钟
let endSecond = Number(endTimeArr[2]) || 0 //结束秒
// 转为数值格式否则用户给end-year等传递字符串值时下面的end+1会导致字符串拼接而不是相加
if (type == 'year') {
startYear = Number(startYear);
endYear = Number(endYear);
endYear = endYear > startYear ? endYear : startYear;
// 生成数组,获取其中的索引,并剪出来
this.years = [...Array(endYear + 1).keys()].slice(startYear);
this.generateArray('month')
} else if (type == 'month') {
let months = []
if (startYear == Number(this.year)) {
if (endYear == Number(this.year)) { // 起始年份,末尾年份一样时
months = [...Array(endMonth + 1).keys()].slice(startMonth);
} else {
months = [...Array(12 + 1).keys()].slice(startMonth);
}
} else if (endYear == Number(this.year)) {
months = [...Array(endMonth + 1).keys()].slice(1);
} else {
months = [...Array(12 + 1).keys()].slice(1);
}
this.months = months
this.generateArray('day')
} else if (type === 'day') {
let days = []
if (startYear == Number(this.year) && startMonth == Number(this.month)) {
if (endYear == Number(this.year) && endMonth == Number(this
.month)) {
days = [...Array(endDay + 1).keys()].slice(startDay);
} else {
days = [...Array(totalDays + 1).keys()].slice(startDay);
}
} else if (endYear == Number(this.year) && endMonth == Number(this.month)) {
days = [...Array(endDay + 1).keys()].slice(1);
} else {
days = [...Array(totalDays + 1).keys()].slice(1);
}
this.days = days
this.generateArray('hour')
} else if (type === 'hour') {
let hours = []
if (startYear == Number(this.year) && startMonth == Number(this.month) && startDay == Number(
this.day)) {
if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(this
.day)) {
hours = [...Array(endHour + 1).keys()].slice(startHour);
} else {
hours = [...Array(23 + 1).keys()].slice(startHour);
}
} else if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(
this.day)) {
hours = [...Array(endHour + 1).keys()].slice(0);
} else {
hours = [...Array(23 + 1).keys()].slice(0);
}
this.hours = hours
this.generateArray('minute')
} else if (type === 'minute') {
let minutes = []
if (startYear == Number(this.year) && startMonth == Number(this.month) && startDay == Number(
this.day) && startHour == Number(this.hour)) {
if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(this.day) &&
endHour == Number(this.hour)) {
minutes = [...Array(endMinute + 1).keys()].slice(startMinute);
} else {
minutes = [...Array(59 + 1).keys()].slice(startMinute);
}
} else if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(
this.day) && endHour == Number(this.hour)) {
minutes = [...Array(endMinute + 1).keys()].slice(0);
} else {
minutes = [...Array(59 + 1).keys()].slice(0);
}
this.minutes = minutes
this.generateArray('seconds')
} else {
let seconds = []
if (startYear == Number(this.year) && startMonth == Number(this.month) && startDay == Number(
this.day) && startHour == Number(this.hour) && startMinute ==
Number(this.minute)) {
if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(this
.day) && endHour == Number(this.hour) && endMinute ==
Number(this.minute)) {
seconds = [...Array(endSecond + 1).keys()].slice(startSecond);
} else {
seconds = [...Array(59 + 1).keys()].slice(startSecond);
}
} else if (endYear == Number(this.year) && endMonth == Number(this.month) && endDay == Number(this
.day) && endHour == Number(this.hour) && endMinute == Number(
this.minute)) {
seconds = [...Array(endSecond + 1).keys()].slice(0);
} else {
seconds = [...Array(59 + 1).keys()].slice(0);
}
this.seconds = seconds
}
},
close() {
this.$emit('input', false);
},
// 用户更改picker的列选项
change(e) {
this.valueArr = e.detail.value;
let i = 0;
// 这里使用i++是因为this.valueArr数组的长度是不确定长度的它根据this.params的值来配置长度
// 进入if规则i会加1保证了能获取准确的值
if (this.params.year) {
this.year = this.years[this.valueArr[i++]];
this.generateArray('year')
}
if (this.params.month) {
this.month = this.months[this.valueArr[i++]];
this.generateArray('month')
}
if (this.params.day) {
this.day = this.days[this.valueArr[i++]];
this.generateArray('day')
}
if (this.params.hour) {
this.hour = this.hours[this.valueArr[i++]];
this.generateArray('hour')
}
if (this.params.minute) {
this.minute = this.minutes[this.valueArr[i++]];
this.generateArray('minute')
}
if (this.params.second) {
this.second = this.seconds[this.valueArr[i++]];
this.generateArray('second')
}
},
// 用户点击确定按钮
getResult(event = null) {
// #ifdef MP-WEIXIN
if (this.moving) return;
// #endif
let result = {};
// 只返回用户在this.params中配置了为true的字段
if (this.params.year) result.year = this.formatNumber(this.year || 0);
if (this.params.month) result.month = this.formatNumber(this.month || 0);
if (this.params.day) result.day = this.formatNumber(this.day || 0);
if (this.params.hour) result.hour = this.formatNumber(this.hour || 0);
if (this.params.minute) result.minute = this.formatNumber(this.minute || 0);
if (this.params.second) result.second = this.formatNumber(this.second || 0);
if (this.params.timestamp) result.timestamp = this.getTimestamp();
if (event) this.$emit(event, result);
this.close();
},
// 小于10前面补0用于月份日期时分秒等
formatNumber(num) {
return +num < 10 ? '0' + num : String(num);
},
// 获取时间戳
getTimestamp() {
let format = this.jnpf.handelFormat(this.format)
let timeType = format === 'yyyy' ? '/01/01 00:00:00' : format === 'yyyy-MM' ? '/01 00:00:00' :
format === 'yyyy-MM-dd' ?
' 00:00:00' : ''
// yyyy-mm-dd为安卓写法不支持iOS需要使用"/"分隔,才能二者兼容
let time = "";
if (this.params.year && !this.params.month && !this.params.day && !this.params.hour && !this.params
.minute && !this.params.second) {
time = this.year + timeType
} else if (this.params.year && this.params.month && !this.params.day && !this.params.hour && !this.params
.minute && !this.params.second) {
time = this.year + '/' + this.month + timeType
} else if (this.params.year && this.params.month && this.params.day && !this.params.hour && !this.params
.minute && !this.params.second) {
time = this.year + '/' + this.month + '/' + this.day + timeType
} else if (this.params.year && this.params.month && this.params.day && this.params.hour && !this.params
.minute && !this.params.second) {
time = this.year + '/' + this.month + '/' + this.day + " " + this.hour + timeType
} else if (this.params.year && this.params.month && this.params.day && this.params.hour && this.params
.minute && !this.params.second) {
time = this.year + '/' + this.month + '/' + this.day + " " + this.hour + ":" + this.minute + timeType
} else {
time = this.year + '/' + this.month + '/' + this.day + " " + this.hour + ":" + this.minute + ":" + this
.second + timeType
}
return new Date(time).getTime();
}
}
};
</script>
<style lang="scss" scoped>
.u-datetime-picker {
position: relative;
z-index: 999;
}
.u-picker-view {
height: 100%;
box-sizing: border-box;
}
.u-picker-header {
width: 100%;
height: 90rpx;
padding: 0 40rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
font-size: 30rpx;
background: #fff;
position: relative;
}
.u-picker-header::after {
content: '';
position: absolute;
border-bottom: 1rpx solid #eaeef1;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
bottom: 0;
right: 0;
left: 0;
}
.u-picker__title {
color: $u-content-color;
}
.u-picker-body {
width: 100%;
height: 500rpx;
overflow: hidden;
background-color: #fff;
}
.u-column-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $u-main-color;
padding: 0 8rpx;
}
.u-text {
font-size: 24rpx;
padding-left: 8rpx;
}
.u-btn-picker {
padding: 16rpx;
box-sizing: border-box;
text-align: center;
text-decoration: none;
}
.u-opacity {
opacity: 0.5;
}
.u-btn-picker--primary {
color: $u-type-primary;
}
.u-btn-picker--tips {
color: $u-tips-color;
}
</style>