前言

  • 日期选择肯定很多人都做过,最近在做移动端的项目,产品想做一个类似这样的日期时间选择界面,详看下图:

产品:我想要一个这样的日期时间选择界面!!!_java

  • 通过这个界面来看,我们正常的日期时间选择的组件库肯定不能直接满足,但是有些部分可以满足要求;像右边的时间选择就可以,但是左边的这个日期和星期几怎么做呢?

  • 界面上没有年份?这里做一个伏笔,后面再看。

  • 下面我们一步步来实现这个需求,我使用的是 Vue + Vant,Vant官网。

开始

基础组件选择

  • 首先我们需要查看相应的 UI 组件库找到可以基本满足需要的组件,我主要找了两个组件一个是 DatetimePicker 时间选择,另一个是Picker 选择器。

  • 这两个按理说应该都可以,我选择的是 Picker 选择器组件,DatetimePicker 时间选择 组件怎么实现可以自己试试。

数据构造

  • 从图片上我们可以知道这个需要三列,第一列显示日期和星期,第二列显示小时,最后一列显示分钟。

1、html

<van-picker
   title="标题"
   show-toolbar
   :columns="dateColumns"
   @confirm="onConfirm"
   @cancel="onCancel"
   @change="onChange"
/>
复制代码

2、vue

  • 我们在进页面后初始化一个 dateColumns,包括默认的第一列以及后面两列时间。后面的 onChange 和 getNewDateArray 方法就是我们后面逻辑实现的地方。

export default {
...
   data() {
    dateColumns: [],
       defaultYear: new Date().getFullYear(), // 存储年份,默认当前年
       dateArray: [], // 存储第一列的values
   },
   mounted() {
       this.dateColumns = [
           this.getNewDateArray(undefined, true),// 第一列
           {
               values: Array.from({ length: 24 }, (v, k) => {
                   if (k < 10) {
                       k = `0${k}`
                   }
                   return k;
               }),
               defaultIndex: 1,
           }, { // 第二列
               values: Array.from({ length: 60 }, (v, k) => {
                   if (k < 10) {
                       k = `0${k}`
                   }
                   return k;
               }),
               defaultIndex: 1,
           }]; // 第三列
   },
   method:{
       onConfirm(picker, values) {...},
       onChange(picker, values) {
           console.log('piker', picker, values)
           // ...
       },
       onCancel() { ... },
       getNewDateArray(date, flag, picker, type) {
        ...
       }
复制代码

逻辑分析

  • 首先我们看第一列的数据结构,是XX月XX日 XX,第一个XX是月份,第二个XX是多少日,第三个XX是星期几,我们需要用到的就是月份(onChange 中 getNewDateArray 的调用为什么不加第几天而默认使用 1这个后面‘疑问’目录中会详细说明),每次滚动到头或者到尾的时候我们需要通过这个来判断是第一个月还是最后一个月,从而对月份和年份重新设置。

    1、当滚动到 dateColumns 第一列 values 数组的第一个的时候,需要加载上一个月的数据;
    2、当滚动到 dateColumns 第一列 values 数组的最后一个的时候,需要加载后一个月的数据;
    3、当滚动到 dateColumns 第一列 values 数组的第一个的时候,需要判断 month(月份)是否为第一个月,如果是第一个月我们需要将月份重置为第 12 个月,并且将默认的年份 -1
    4、当滚动到 dateColumns 第一列 values 数组的最后一个的时候,需要判断 month(月份)是否为最后一个月,如果是最后一个月我们需要将月份重置为第 1 个月,并且将默认的年份 +1

  • 相关代码如下

onChange(picker, values) {
   console.log('piker', picker, values)
   const dateArr = values[0].text.split(' ')[0].slice(0, -1).split('月');
   const month = dateArr[0]; // 当前的月份

   if(values[0] === this.dateArray[0]) { // 判断`dateColumns` 第一列 `values` 数组的第一个
       let newMonth;
       if(month === '1') { // 判断 `month`(月份)是否为`第一个月`
           newMonth = 12;
           this.defaultYear -= 1;
       } else {
           newMonth = Number(month) - 1;
       }
       this.getNewDateArray(`${this.defaultYear}-${newMonth}-1`, false, picker, true)
   } else if (values[0] === this.dateArray[this.dateArray.length - 1]) { // 判断`dateColumns` 第一列 `values` 数组的最后一个
       let newMonth;
       if(month === '12') { // 判断 `month`(月份)是否为`最后一个月`
           newMonth = 1;
           this.defaultYear += 1;
       } else {
           newMonth = Number(month) + 1
       }
       this.getNewDateArray(`${this.defaultYear}-${newMonth}-1`, false, picker, false)
   }

},
复制代码

初始化

  • 第一次进入页面我们初始化当前月份的数据。

1、我们需要拿到当前月份的总天数;
2、生成 UI 上对应的数据格式,每次需要通过当前的日期拿到星期几;
3、定义一个周一到周日的数组方便对应取值,注意星期日是返回 0 ;
4、通过 flag 判断为 true 返回对象。

代码如下:

 const  weekArray = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
/**
* date: 需要增加的数据
*  flag:是否是第一次进入
*  picker:Picker的实例
*  type:数据push的方向
*/
getNewDateArray(date, flag, picker, type) {
    date = date ? new Date(date) : new Date();
    let month = date.getMonth() + 1; // 存储当前月份
    const monthDays = new Date(date.getFullYear(), month, 0).getDate(); // 获取当前月份的总天数
   
      let arr = [];
      let index = 0;
      for (let i = 1; i <= monthDays; i ++) {
          let str = `${month}月${i}日 ${weekArray[new Date(`${date.getFullYear()}-${month}-${i}`).getDay()]}`
          arr.push(str);
      }
      ... // code
     
      if(flag) {
          return {
              values: arr,
              defaultIndex: date.getDate() - 1 // 设置默认选中
          }
      }
      ... // code
}
复制代码

后续上下滚动加载

  • 什么的代码只是第一次进入的时候初始化的数据,上下滑动到开始或结尾并不会新增内容,所以对什么的代码进行补充和修改。

1、通过 type 判断是向下滚动到一个还是向上滚动到最后一个;
2、向下滚动到一个将默认值设为添加的上一个月的总天数,并将新月份添加到 dateArray 数组之前;
3、向上滚动到最后一个将默认值设为为添加新月份的数组长度 -1,因为 column 的索引是 0 开始的,这里需要注意一下 ;并将新月份添加到 dateArray 数组之后;
4、通过 flag 判断为 false,调用Picker实例修改已存在的values及默认选中的索引。

代码如下:

 ... // code
+  if(type) {
+     this.dateArray = arr.concat(this.dateArray); // 添加到dateArray数组之前
+     index = monthDays;
+ } else {
+     index = this.dateArray.length - 1; // 设置新数组前设置默认选中索引
+     this.dateArray = this.dateArray.concat(arr); // 添加到dateArray数组之后
+ }
if(flag) {
    return {
+         values: this.dateArray,
-          values: arr,
        defaultIndex: date.getDate() - 1
    }
+  } else {
+     picker.setColumnValues(0, this.dateArray)
+     picker.setColumnIndex(0, index) // 设置默认选中
}
复制代码

大功告成?

  • 刚刚我们看到只是选择的时候的 UI 界面,那选择完成后需要怎么显示呢?我们再看一下选完后的 UI 界面?

  • 哈哈哈哈哈,wtf,年份呢?居然不显示年份?为了安全起见,我们还是把年份加上,当然后端也会用到这个。

  • 查阅文档,我发现数组的值可以是一个对象,显示的是 text 字段,那我们把生成的的数据 str 那个结构改一下。

产品:我想要一个这样的日期时间选择界面!!!_java_02

  • 代码如下:

onChange() {
-  if(values[0] === this.dateArray[0]) {
+  if(values[0].text === this.dateArray[0].text) {
     ...
-  } else if (values[0] === this.dateArray[this.dateArray.length - 1]) {
+  } else if (values[0].text === this.dateArray[this.dateArray.length - 1].text) {
     ...
 }
},
getNewDateArray(date, flag, picker, type) {
-   let str = `${month}月${i}日 ${weekArray[new Date(`${date.getFullYear()}-${month}-${i}`).getDay()]}`
+   let str = {
+     year: date.getFullYear(),,
+     text: `${month}月${i}日 ${weekArray[new Date(`${newYear}-${month}-${i}`).getDay()]}`
+   };
}
复制代码
  • 数据结构是加上了,那生不生效呢?我们看一下界面;

产品:我想要一个这样的日期时间选择界面!!!_java_03

  • 界面倒是没什么问题,那是否能拿到数据呢?我们在 onChange 中打印一下当前选择的 values

产品:我想要一个这样的日期时间选择界面!!!_java_04

  • ok,完美!年的问题就解决了,是不是觉得 so easy

产品:我想要一个这样的日期时间选择界面!!!_java_05

再试试?

  • 试试当前日期是当前月的第一天或者最后一天。 是不是发现什么了?如果当前是第一天,你下拉是不会加载前一个月的数据的,因为没有触发 onChange 事件,你可以上拉到新的日期,再下拉到第一个,这样就会刷新了,当然如果你们产品能接受那也是可以的;最后一天是相同的道理。

解决方案
  • 我们可以通过判断初始时是否是今天是当月的第一天或者最后一天,来多加载前一个月或者后一个月;

1、增加一个标识符变量 getMoreMonth ,false:不需要获取更多, 1:获取前一个月,2:获取后一个月;
2、增加一个长度标识变量 moreLen 初始化为当前日期的,用来存储对默认选中需要增加的长度;
3、获取当前年份和月份,判断获取之前还是之后的月份,对年份和月份进行重置;
4、生成新的一个月的数组,判断是之前还是之后的月份,生成之前的月份将所有数据放到新数组中;之后的月份直接向之前的 arr 数组中 push 就可以了;
5、循环结束后,设置索引需要增加的长度,如果是获取之前的一个月(getMoreMonth === 1),将 moreLen 加上之前一个月的总天数;并将新的数组和之前的数组进行拼接;

  • 新增代码如下:

getNewDateArray(date, flag, picker, type) {
+    let getMoreMonth = false; // 不需要获取更多 1、获取前一个月;2、获取后一个月
+   if(flag) { // 初次渲染
+       const newDate = new Date();
+       const monthDays = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0).getDate()
+       if(new Date().getDate() === 1) { // 当月第一天
+           getMoreMonth = 1;
+       } else if (new Date().getDate() === monthDays) { // 当月最后一天
+           getMoreMonth = 2;
+       }
+   }
   date = date ? new Date(date) : new Date();
   ...
   for (let i = 1; i <= monthDays; i ++) {
       let str = {
           year: date.getFullYear(),
           text: `${month}月${i}日 ${weekArray[new Date(`${date.getFullYear()}-${month}-${i}`).getDay()]}`
       };
       arr.push(str);
   }
+   let moreLen = date.getDate() - 1;
+   if(getMoreMonth) {
+       let newYear = date.getFullYear();
+       if (getMoreMonth === 1) { // 当月第一天
+           if (month === 1) {
+               month = 12;
+               newYear -= 1;
+           } else {
+               month -= 1;
+           }
+       } else {
+           if (month === 12) {//  当月最后一天
+               month = 1;
+               newYear += 1;
+           } else {
+               month += 1;
+           }
+      }
+       const moreMonthDays = new Date(newYear, month, 0).getDate();
+       let beforeArray = [];
+       for (let i = 1; i <= moreMonthDays; i ++) {
+           let str = {
+               year: newYear,
+               text: `${month}月${i}日 ${weekArray[new Date(`${newYear}-${month}-${i}`).getDay()]}`
+           };
+           if (getMoreMonth === 2) { // 获取后一个月直接push
+               arr.push(str);
+           } else {
+               beforeArray.push(str); // 获取前一个月存入新的数组
+           }
+       }
+       moreLen = getMoreMonth === 1 ? (moreLen + beforeArray.length) : moreLen; // 获取前一个月索引增加前一个月总天数
+       arr = beforeArray.concat(arr);
+   }

   if(flag) {
       return {
           values: this.dateArray,
-           defaultIndex: date.getDate() - 1
+           defaultIndex: moreLen
       }
   } else {
   ...
复制代码

测试一下

  • 修改 mounted 中的首次调用入参为当前月份的第一日,并修改 getNewDateArray 中第一个判断是否首次进入的逻辑。

  • 代码修改如下:

  mounted() {
     this.dateColumns = [
-       this.getNewDateArray(undefined, true),// 第一列
+       this.getNewDateArray('2020-11-1', true),// 第一列
        ...
 },
 
 getNewDateArray(date, flag, picker, type) {
     let getMoreMonth = false; // 不需要获取更多 1、获取前一个月;2、获取后一个月
     if(flag) {
-         const newDate = new Date();
+         const newDate = new Date(date);
         const monthDays = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0).getDate()
-         if(new Date().getDate() === 1) {
+         if(new Date(date).getDate() === 1) {
+             console.log('first day')
             getMoreMonth = 1;
         } else if (new Date().getDate() === monthDays) {
             getMoreMonth = 2;
         }
     }
     ...
 }
复制代码
  • 看一下测试的效果,获取前面的一个月的没有问题,相应的我们再看看获取下一个月。

产品:我想要一个这样的日期时间选择界面!!!_java_06

  • 获取下一个月,修改代码如下:

mounted() {
     this.dateColumns = [
-       this.getNewDateArray('2020-11-1', true),// 第一列
+       this.getNewDateArray('2020-11-30', true),// 第一列
        ...
 },
 
 getNewDateArray(date, flag, picker, type) {
     let getMoreMonth = false; // 不需要获取更多 1、获取前一个月;2、获取后一个月
     if(flag) {
-         const newDate = new Date(date);
+         const newDate = new Date();
         const monthDays = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0).getDate()
+         if(new Date().getDate() === 1) {
-         if(new Date(date).getDate() === 1) {
-             console.log('first day')
             getMoreMonth = 1;
-         } else if (new Date().getDate() === monthDays) {
+         } else if (new Date(date).getDate() === monthDays) {
+          console.log('last day')
             getMoreMonth = 2;
         }
     }
     ...
 }
复制代码
  • 好的,也没有问题,针对目前的问题就基本上解决了。

产品:我想要一个这样的日期时间选择界面!!!_java_07

疑问

1、为什么 this.getNewDateArray(${this.defaultYear}-${newMonth}-1, false, picker, true)这里不用选择的数据中的日作为第一个参数日期的最后日子传进去呢?这里有个坑,之前我也以为需要将这个日子用上,所以我穿了这个过去;但是后面在使用时,我发现滚动到1月份底的时候,加载的是 3 月份,并没有加载 2 月份。

产品:我想要一个这样的日期时间选择界面!!!_java_08

  • 将如果是当月第一天或最后一天预加载两个月的代码全部注释(便于测试),并修改代码测试如下就会得到上述结果:

mounted() {
 this.dateColumns = [
-     this.getNewDateArray('2020-11-30', true),// 第一列
+     this.getNewDateArray(undefined, true),// 第一列
   ...
   ]
},
onChange(picker, values) {
   ...
   const month = dateArr[0]; // 当前的月份
   const day = dateArr[1]; // 当月第几天
if(...) {
    ...
       this.getNewDateArray(`${this.defaultYear}-${newMonth}-${day}`, false, picker, true)
   } else if (...) {
    ...
       this.getNewDateArray(`${this.defaultYear}-${newMonth}-${day}`, false, picker, false)
   }
}
复制代码
  • 原因是因为这里获取到 2020 年的 1 月份的最后一天是 31 号,但是 2 月只有 29 天,所以 getMonth() 拿到的就是 2,再 +1 就变成了 3

可优化点

1、 每次会到第一个或最后一个再去加载,是否可以优化为滚动去加载?
2、代码由于比较写得比较急,有些点是可以优化的。

总结

  • 通过产品出的原型和 UI,如果现有的UI库不能完全满足需求,我们可以找一个相似度比较高的进行修改。

  • 在功能做完之后,需要针对一些临界点做一些测试。

  • 都看到这儿了点个赞呗,男帅女美,谢谢大家。