赞
踩
本文写的组件是基于 uni-app
框架下的,但是其实框架不重要,思路都是一样的。
有同学可能会问了,uni-app
本身不是就有 picker
,mode=time
的时候就是时间选择器了吗,为什么还要自己写一个?那是因为我们产品大佬说,不要固定在底部弹出选择的,想嵌套在页面筛选条件里,因为考虑到交互blabla的……我想了想,好吧,给时间啥都好说,咱就自己造个轮子呗~
先来看看效果~
开始动手之前先捋一下思路。
移动端的日期筛选器交互方式比较常见的都是多列滚动的,所以我们可以用 picker-view
来实现。除了基础交互,组件需要注意的点就是年月日之间的相互关联,比如1月有31天,4月是30天,闰年2月是29天等这些,也就是年月日需要相互关联动态变化。此外还可以添加支持配置最大最小时间范围,支持切换不同的时间模式(比如年月日/年月/年月日时分秒)等。
一个常用的日期选择器组件主要的功能就是以上这些了。
完整代码见:https://github.com/Dandelion-drq/uniapp-datetime-picker
欢迎喜欢的朋友给个star哈~
先封装一个接受多个数组的多列滚动选择组件,方便后面支持不同日期模式切换。
<template>
<picker-view class="picker-view" :value="indexArr" @change="onChange">
<picker-view-column class="picker-view-column" v-for="(col, colIdx) in columns" :key="colIdx">
<view v-for="(item, idx) in col" :key="idx">{{ item }}</view>
</picker-view-column>
</picker-view>
</template>
<script src="./index.js"></script>
<style lang="css" scoped src="./index.css"></style>
.picker-view {
height: 356rpx;
}
.picker-view-column {
font-size: 14px;
line-height: 34px;
text-align: center;
color: #333;
}
export default { data() { return {}; }, props: { // 所有列选项数据 columns: { type: Array, default: () => [] }, // 每一列默认选中值数组,不传默认选中第一项 selectVals: { type: Array, default: () => [] } }, computed: { // 每一列选中项的索引,当默认选中值变化的时候这个值也要变化 indexArr: { // 多维数组,深度监听 cache: false, get() { // console.log('indexArr', this.selectVals, this.columns); if (this.selectVals.length > 0) { return this.columns.map((col, cIdx) => { return col.findIndex((i) => i == this.selectVals[cIdx]); }); } else { return [].fill(0, 0, this.columns.length); } } } }, methods: { onChange(e) { const { value } = e.detail; // console.log('pickerview改变', value, this.columns); let ret = this.columns.map((item, index) => { let idx = value[index]; if (idx < 0) { idx = 0; } if (idx > item.length - 1) { idx = item.length - 1; } return item[idx]; }); // console.log('选中值', ret); this.$emit('onChange', { value: ret }); } } };
年份比较简单,从配置的最小日期年份到最大日期年份生成数组就好。月份要注意当如果选中的年份刚好是最小/最大可选日期的年份时,月份要从最小/最大可选日期开始/结束,其他时候月份都是1~12。日就先列出正常一年每个人的天数配置,然后注意闰年2月是29天,还有同样跟月一样要注意的是当如果选中的年份和月份刚好是最小/最大可选日期的年月时,日要从最小/最大可选日期开始/结束。时分秒同理。
<template>
<view class="datetime-picker">
<CustomPickerView :columns="dateConfig" :selectVals="selectVals" @onChange="onChangePickerValue" />
</view>
</template>
<script src="./index.js"></script>
import CustomPickerView from '../customPickerView/index.vue'; import DateUtil from '../dateTimePicker/dateUtil'; export default { components: { CustomPickerView }, data() { return { selectYear: new Date().getFullYear(), selectMonth: new Date().getMonth() + 1, // 选中的月份,1~12 selectDay: new Date().getDate(), selectHour: new Date().getHours(), selectMinute: new Date().getMinutes(), selectSecond: new Date().getSeconds() }; }, props: { // 可选的最小日期,默认十年前 minDate: { type: String, default: '' }, // 可选的最大日期,默认十年后 maxDate: { type: String, default: '' } }, computed: { minDateObj() { let minDate = this.minDate; if (minDate) { if (this.mode == 2 && minDate.replace(/\-/g, '/').split('/').length == 2) { // 日期模式为年月时有可能传进来的minDate是2022-02这样的格式,在ios下new Date会报错,加上日期部分做兼容 minDate += '-01'; } return new Date(DateUtil.handleDateStr(minDate)); } else { // 没有传最小日期,默认十年前 minDate = new Date(); minDate.setFullYear(minDate.getFullYear() - 10); return minDate; } }, maxDateObj() { let maxDate = this.maxDate; if (maxDate) { if (this.mode == 2 && maxDate.replace(/\-/g, '/').split('/').length == 2) { // 日期模式为年月时有可能传进来的maxDate是2022-02这样的格式,在ios下new Date会报错,加上日期部分做兼容 maxDate += '-01'; } return new Date(DateUtil.handleDateStr(maxDate)); } else { // 没有传最小日期,默认十年后 maxDate = new Date(); maxDate.setFullYear(maxDate.getFullYear() + 10); return maxDate; } }, years() { let years = []; let minYear = this.minDateObj.getFullYear(); let maxYear = this.maxDateObj.getFullYear(); for (let i = minYear; i <= maxYear; i++) { years.push(i); } return years; }, months() { let months = []; let minMonth = 1; let maxMonth = 12; // 如果选中的年份刚好是最小可选日期的年份,那月份就要从最小日期的月份开始 if (this.selectYear == this.minDateObj.getFullYear()) { minMonth = this.minDateObj.getMonth() + 1; } // 如果选中的年份刚好是最大可选日期的年份,那月份就要在最大日期的月份结束 if (this.selectYear == this.maxDateObj.getFullYear()) { maxMonth = this.maxDateObj.getMonth() + 1; } for (let i = minMonth; i <= maxMonth; i++) { months.push(i); } return months; }, days() { // 一年中12个月每个月的天数 let monthDaysConfig = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; // 闰年2月有29天 if (this.selectMonth == 2 && this.selectYear % 4 == 0) { monthDaysConfig[1] = 29; } let minDay = 1; let maxDay = monthDaysConfig[this.selectMonth - 1]; if (this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1) { minDay = this.minDateObj.getDate(); } if (this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1) { maxDay = this.maxDateObj.getDate(); } let days = []; for (let i = minDay; i <= maxDay; i++) { days.push(i); } return days; }, hours() { let hours = []; let minHour = 0; let maxHour = 23; if ( this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1 && this.selectDay == this.minDateObj.getDate() ) { minHour = this.minDateObj.getHours(); } if ( this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1 && this.selectDay == this.maxDateObj.getDate() ) { maxHour = this.maxDateObj.getHours(); } for (let i = minHour; i <= maxHour; i++) { hours.push(i); } return hours; }, minutes() { let mins = []; let minMin = 0; let maxMin = 59; if ( this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1 && this.selectDay == this.minDateObj.getDate() && this.selectHour == this.minDateObj.getHours() ) { minMin = this.minDateObj.getMinutes(); } if ( this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1 && this.selectDay == this.maxDateObj.getDate() && this.selectHour == this.maxDateObj.getHours() ) { maxMin = this.maxDateObj.getMinutes(); } for (let i = minMin; i <= maxMin; i++) { mins.push(i); } return mins; }, seconds() { let seconds = []; let minSecond = 0; let maxSecond = 59; if ( this.selectYear == this.minDateObj.getFullYear() && this.selectMonth == this.minDateObj.getMonth() + 1 && this.selectDay == this.minDateObj.getDate() && this.selectHour == this.minDateObj.getHours() && this.selectMinute == this.minDateObj.getMinutes() ) { minSecond = this.minDateObj.getSeconds(); } if ( this.selectYear == this.maxDateObj.getFullYear() && this.selectMonth == this.maxDateObj.getMonth() + 1 && this.selectDay == this.maxDateObj.getDate() && this.selectHour == this.maxDateObj.getHours() && this.selectMinute == this.maxDateObj.getMinutes() ) { maxSecond = this.maxDateObj.getSeconds(); } for (let i = minSecond; i <= maxSecond; i++) { seconds.push(i); } return seconds; } } }
// DateUtil.js /** * 日期时间格式化 * @param {Date} date 要格式化的日期对象 * @param {String} fmt 格式化字符串,eg:YYYY-MM-DD HH:mm:ss * @returns 格式化后的日期字符串 */ function formatDate(date, fmt) { if (typeof date == 'string') { date = new Date(handleDateStr(date)); } var o = { 'M+': date.getMonth() + 1, // 月份 'd+': date.getDate(), // 日 'D+': date.getDate(), // 日 'H+': date.getHours(), // 小时 'h+': date.getHours(), // 小时 'm+': date.getMinutes(), // 分 's+': date.getSeconds(), // 秒 'q+': Math.floor((date.getMonth() + 3) / 3), // 季度 S: date.getMilliseconds() // 毫秒 }; if (/([y|Y]+)/.test(fmt)) { fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').slice(4 - RegExp.$1.length)); } for (var k in o) { if (new RegExp('(' + k + ')').test(fmt)) { fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).slice(('' + o[k]).length)); } } return fmt; } /** * 处理时间字符串,兼容ios下new Date()返回NaN问题 * @param {*} dateStr 日期字符串 * @returns */ function handleDateStr(dateStr) { return dateStr.replace(/\-/g, '/'); } /** * 判断日期1是否在日期2之前,即日期1小于日期2 * @param {Date} date1 * @param {Date} date2 * @returns */ function isBefore(date1, date2) { if (typeof date1 == 'string') { date1 = new Date(handleDateStr(date1)); } if (typeof date2 == 'string') { date2 = new Date(handleDateStr(date2)); } return date1.getTime() < date2.getTime(); } /** * 判断日期1是否在日期2之后,即日期1大于日期2 * @param {Date} date1 * @param {Date} date2 * @returns */ function isAfter(date1, date2) { if (typeof date1 == 'string') { date1 = new Date(handleDateStr(date1)); } if (typeof date2 == 'string') { date2 = new Date(handleDateStr(date2)); } return date1.getTime() > date2.getTime(); } export default { formatDate, handleDateStr, isBefore, isAfter };
支持多种不同的日期模式,包括年月日(默认)、年月、年份、年月日时分秒。主要的处理逻辑是要根据 mode
的变化,来动态生成传给 pickerView
组件的数组,以及其默认选中值,还有注意 pickerView
组件 onChange
事件的处理也需要考虑不同日期模式的情况。
<template>
<view class="datetime-picker">
<PickerView :columns="dateConfig" :selectVals="selectVals" @onChange="onChangePickerValue" />
</view>
</template>
<script src="./index.js"></script>
<style scoped></style>
{ props: { // 日期模式,1:年月日,2:年月,3:年份,4:年月日时分秒 mode: { type: Number, default: 1 }, // 默认选中日期(注意要跟日期模式对应) defaultDate: { type: String, default: '' } } computed: { // 传给pickerView组件的数组,根据mode来生成不同的数据 dateConfig() { if (this.mode == 2) { // 年月模式 let years = this.years.map((y) => y + '年'); let months = this.months.map((m) => m + '月'); return [years, months]; } else if (this.mode == 3) { // 只有年份模式 let years = this.years.map((y) => y + '年'); return [years]; } else if (this.mode == 4) { // 年月日时分秒模式 let years = this.years.map((y) => y + '年'); let months = this.months.map((m) => m + '月'); let days = this.days.map((d) => d + '日'); let hours = this.hours.map((h) => h + '时'); let minutes = this.minutes.map((m) => m + '分'); let seconds = this.seconds.map((s) => s + '秒'); return [years, months, days, hours, minutes, seconds]; } else { // 默认,年月日模式 let years = this.years.map((y) => y + '年'); let months = this.months.map((m) => m + '月'); let days = this.days.map((d) => d + '日'); return [years, months, days]; } }, // pickerView默认值,根据mode的切换来变换值 selectVals() { if (this.mode == 2) { return [this.selectYear + '年', this.selectMonth + '月']; } else if (this.mode == 3) { return [this.selectYear + '年']; } else if (this.mode == 4) { return [ this.selectYear + '年', this.selectMonth + '月', this.selectDay + '日', this.selectHour + '时', this.selectMinute + '分', this.selectSecond + '秒' ]; } else { return [this.selectYear + '年', this.selectMonth + '月', this.selectDay + '日']; } } }, methods: { onChangePickerValue(e) { const { value } = e; // console.log('onChangePickerValue', value); if (this.mode == 2 && value[0] && value[1]) { // 年月模式 this.selectYear = Number(value[0].replace('年', '')); this.selectMonth = Number(value[1].replace('月', '')); } else if (this.mode == 3 && value[0]) { // 只有年份模式 this.selectYear = Number(value[0].replace('年', '')); } else if (this.mode == 4 && value[0] && value[1] && value[2] != '' && value[3] && value[4] && value[5]) { // 年月日时分秒模式 this.selectYear = Number(value[0].replace('年', '')); this.selectMonth = Number(value[1].replace('月', '')); this.selectDay = Number(value[2].replace('日', '')); this.selectHour = Number(value[3].replace('时', '')); this.selectMinute = Number(value[4].replace('分', '')); this.selectSecond = Number(value[5].replace('秒', '')); } else if (value[0] && value[1] && value[2]) { // 默认,年月日模式 this.selectYear = Number(value[0].replace('年', '')); this.selectMonth = Number(value[1].replace('月', '')); this.selectDay = Number(value[2].replace('日', '')); } else { // 其他情况可能是pickerView返回的数据有问题,不处理 console.log('onChangePickerValue其他情况'); return; } let formatTmpl = 'YYYY-MM-DD'; if (this.mode == 2) { formatTmpl = 'YYYY-MM'; } else if (this.mode == 3) { formatTmpl = 'YYYY'; } else if (this.mode == 4) { formatTmpl = 'YYYY-MM-DD HH:mm:ss'; } this.$emit( 'onChange', DateUtil.formatDate( new Date(`${this.selectYear}/${this.selectMonth}/${this.selectDay} ${this.selectHour}:${this.selectMinute}:${this.selectSecond}`), formatTmpl ) ); } } }
完成了以上3点,日期选择器组件就写好了,完整代码以及使用demo见:https://github.com/Dandelion-drq/uniapp-datetime-picker
欢迎喜欢的朋友给个star~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。