背景
本文寫的組件是基于 uni-app
框架下的,但是其實(shí)框架不重要,思路都是一樣的纱耻。
有同學(xué)可能會(huì)問(wèn)了,uni-app
本身不是就有 picker
险耀,mode=time
的時(shí)候就是時(shí)間選擇器了嗎弄喘,為什么還要自己寫一個(gè)?那是因?yàn)槲覀儺a(chǎn)品大佬說(shuō)甩牺,不要固定在底部彈出選擇的蘑志,想嵌套在頁(yè)面篩選條件里,因?yàn)榭紤]到交互blabla的……我想了想贬派,好吧急但,給時(shí)間啥都好說(shuō),咱就自己造個(gè)輪子唄~
效果演示
先來(lái)看看效果~
思路
開始動(dòng)手之前先捋一下思路搞乏。
移動(dòng)端的日期篩選器交互方式比較常見(jiàn)的都是多列滾動(dòng)的波桩,所以我們可以用 picker-view
來(lái)實(shí)現(xiàn)。除了基礎(chǔ)交互请敦,組件需要注意的點(diǎn)就是年月日之間的相互關(guān)聯(lián)镐躲,比如1月有31天,4月是30天侍筛,閏年2月是29天等這些萤皂,也就是年月日需要相互關(guān)聯(lián)動(dòng)態(tài)變化。此外還可以添加支持配置最大最小時(shí)間范圍匣椰,支持切換不同的時(shí)間模式(比如年月日/年月/年月日時(shí)分秒)等裆熙。
一個(gè)常用的日期選擇器組件主要的功能就是以上這些了。
完整代碼見(jiàn):https://github.com/Dandelion-drq/uniapp-datetime-picker
歡迎喜歡的朋友給個(gè)star哈~
實(shí)現(xiàn)
1. picker-view
實(shí)現(xiàn)基礎(chǔ)交互
先封裝一個(gè)接受多個(gè)數(shù)組的多列滾動(dòng)選擇組件,方便后面支持不同日期模式切換弛车。
<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: {
// 所有列選項(xiàng)數(shù)據(jù)
columns: {
type: Array,
default: () => []
},
// 每一列默認(rèn)選中值數(shù)組齐媒,不傳默認(rèn)選中第一項(xiàng)
selectVals: {
type: Array,
default: () => []
}
},
computed: {
// 每一列選中項(xiàng)的索引,當(dāng)默認(rèn)選中值變化的時(shí)候這個(gè)值也要變化
indexArr: {
// 多維數(shù)組纷跛,深度監(jiān)聽(tīng)
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
});
}
}
};
2. 年月日動(dòng)態(tài)配置以及支持最大最小日期
年份比較簡(jiǎn)單,從配置的最小日期年份到最大日期年份生成數(shù)組就好邀杏。月份要注意當(dāng)如果選中的年份剛好是最小/最大可選日期的年份時(shí)贫奠,月份要從最小/最大可選日期開始/結(jié)束,其他時(shí)候月份都是1~12望蜡。日就先列出正常一年每個(gè)人的天數(shù)配置唤崭,然后注意閏年2月是29天,還有同樣跟月一樣要注意的是當(dāng)如果選中的年份和月份剛好是最小/最大可選日期的年月時(shí)脖律,日要從最小/最大可選日期開始/結(jié)束谢肾。時(shí)分秒同理。
<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: {
// 可選的最小日期芦疏,默認(rèn)十年前
minDate: {
type: String,
default: ''
},
// 可選的最大日期,默認(rèn)十年后
maxDate: {
type: String,
default: ''
}
},
computed: {
minDateObj() {
let minDate = this.minDate;
if (minDate) {
if (this.mode == 2 && minDate.replace(/\-/g, '/').split('/').length == 2) {
// 日期模式為年月時(shí)有可能傳進(jìn)來(lái)的minDate是2022-02這樣的格式微姊,在ios下new Date會(huì)報(bào)錯(cuò)酸茴,加上日期部分做兼容
minDate += '-01';
}
return new Date(DateUtil.handleDateStr(minDate));
} else {
// 沒(méi)有傳最小日期,默認(rèn)十年前
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) {
// 日期模式為年月時(shí)有可能傳進(jìn)來(lái)的maxDate是2022-02這樣的格式兢交,在ios下new Date會(huì)報(bào)錯(cuò)薪捍,加上日期部分做兼容
maxDate += '-01';
}
return new Date(DateUtil.handleDateStr(maxDate));
} else {
// 沒(méi)有傳最小日期,默認(rèn)十年后
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;
}
// 如果選中的年份剛好是最大可選日期的年份酪穿,那月份就要在最大日期的月份結(jié)束
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個(gè)月每個(gè)月的天數(shù)
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
/**
* 日期時(shí)間格式化
* @param {Date} date 要格式化的日期對(duì)象
* @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(), // 小時(shí)
'h+': date.getHours(), // 小時(shí)
'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;
}
/**
* 處理時(shí)間字符串晴裹,兼容ios下new Date()返回NaN問(wèn)題
* @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
};
3. 支持不同日期模式
支持多種不同的日期模式息拜,包括年月日(默認(rèn))溉潭、年月、年份少欺、年月日時(shí)分秒喳瓣。主要的處理邏輯是要根據(jù) mode
的變化,來(lái)動(dòng)態(tài)生成傳給 pickerView
組件的數(shù)組赞别,以及其默認(rèn)選中值畏陕,還有注意 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:年月日時(shí)分秒
mode: {
type: Number,
default: 1
},
// 默認(rèn)選中日期(注意要跟日期模式對(duì)應(yīng))
defaultDate: {
type: String,
default: ''
}
}
computed: {
// 傳給pickerView組件的數(shù)組鞠绰,根據(jù)mode來(lái)生成不同的數(shù)據(jù)
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) {
// 年月日時(shí)分秒模式
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 + '時(shí)');
let minutes = this.minutes.map((m) => m + '分');
let seconds = this.seconds.map((s) => s + '秒');
return [years, months, days, hours, minutes, seconds];
} else {
// 默認(rèn)腰埂,年月日模式
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默認(rèn)值,根據(jù)mode的切換來(lái)變換值
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 + '時(shí)',
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]) {
// 年月日時(shí)分秒模式
this.selectYear = Number(value[0].replace('年', ''));
this.selectMonth = Number(value[1].replace('月', ''));
this.selectDay = Number(value[2].replace('日', ''));
this.selectHour = Number(value[3].replace('時(shí)', ''));
this.selectMinute = Number(value[4].replace('分', ''));
this.selectSecond = Number(value[5].replace('秒', ''));
} else if (value[0] && value[1] && value[2]) {
// 默認(rèn)蜈膨,年月日模式
this.selectYear = Number(value[0].replace('年', ''));
this.selectMonth = Number(value[1].replace('月', ''));
this.selectDay = Number(value[2].replace('日', ''));
} else {
// 其他情況可能是pickerView返回的數(shù)據(jù)有問(wèn)題屿笼,不處理
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點(diǎn),日期選擇器組件就寫好了翁巍,完整代碼以及使用demo見(jiàn):https://github.com/Dandelion-drq/uniapp-datetime-picker
歡迎喜歡的朋友給個(gè)star~