手撸一个DatePicker日期组件

2,394 阅读3分钟

一、序言

前端小伙伴都知道我们每天都在与它打交道,在组件库里有这么一类 徒有其表 的组件,为什么说它徒有其表呢,是因为它用着挺便利,但是乍一想它的实现好像挺难,这类组件我管它叫徒有其表,DatePicker 组件就是一个代表,今天我就带大家手撸一个它的最基础版,日后大家可以在它的基础上根据公司的业务需求来实现相应的定制化(好吧,这句话有点吹牛了)。

二、市场上的DatePicker

2.1、antdesign

datePicker1.png

在这片文章里,我们会将datePicker组件分为三部分,如上图,header(头部)、panel(主要放置当月所有日期)、footer(尾部)

2.2、devui

datePick2.png

2.3、elementui

datePicker3.png

注意

我们以上图举例,上图展示的是2月的控制面板,其中包括1月的31,30;3月的1,2等等。在下面我们会通过isFill字段来控制这个面板里哪些日期是当月的,哪些不是。

2.4、总结

这三种框架在布局上都一样,header + panel + footer;其中 panel 部分应该是我们关注的重点,因为这个区域放置的是 每月的日期数量。panel部分这三种框架实现的UI分2种,一种是日 -> 六,另一种是一 -> 日

三、panel的实现

这里我们实现 日 -> 六,另一种 一 -> 日 的实现思路也一样,做一下小修改即可。

3.1、当月的日期数组

   function createPanel (year, month){
       // 获取当月的第一天
       const nowMonthFirstDate = new Date(year, month - 1, 1).getDate();
       // 获取当月的最后一天
       const nowMonthFinalDate = new Date(year, month, 0).getDate();
       // 声明一个数组,用来存放当月日期
       let nowMonthDays = [];
       for (let index = nowMonthFirstDate; index <= nowMonthFinalDate; index++){
           nowMonthDays.push({
               date: index,
               isFill: false
           });
       };
   };

说明

Date对象

Date1.png

从mdn上可以知道, new Date 的参数有很多,其中第二个参数的范围:0-11,对用着一年的12个月。下面举几个例子,剩下的具体的信息自行mdn。

   new Date(2022, 1, 1)   // 2022.02.01
   new Date(2022, 1, 0)   // 2022.01.31
   new Date(2022, 1, -1)  // 2022.01.30

我们仔细看antd、devui、elementui里datePicker组件的时候会发现它们的panel面板展示的日期数量里不仅仅是当月份的还会有前一个月与下一个月份的,然后它们会根据颜色的深浅来区分日期是否属于当前月份, 接下来我们来补齐一个panel面板中缺失的前一个月份与后一个月份的相应日期。

   function createPanel (year, month){
       // 返回一个具体日期中一周的第几天, 0代表星期日,1代表星期一,以此类推
       let nowMonthFirstDay = new Date(year, month - 1, 1).getDay();
       const leftDateArr = [];
       while (nowMonthFirstDay > 0){
           nowMonthFirstDay = nowMonthFirstDay - 1;
           left.push({
               date: new Date(year, month - 1, -nowMonthFirstDay).getDate(),
               isFill: true
           });
       };
       return leftDateArr;
   };

我们在数组里放入date对象,其中date代表日期,isFill代表是否是填充的日期。

获取 当月数组 函数(完整)

    const getMonthDays = function (year, month){
            // 获取当月的第一天的day
            let nowDay = new Date(year, month - 1, 1).getDay();
            // 获取当月最后一天的day
            let nowMonthFinalDay = new Date(year, month, 0).getDay();

            const leftFillArr = [];
            const rightFillArr = [];
            let monthDaysArr = [];
            const result = [];

            // 日历面板展示形式 0->6
            // 日          一       二   三   四   五   六
            // 1.30       1.31     2.1
            //  0          1        2
            while(nowDay > 0){     // 左对齐
                nowDay -= 1;
                leftFillArr.push({
                    date: new Date(year, month - 1, -nowDay).getDate(),
                    isFill: true
                });
            };

            // 获取当月第一天
            const nowMonthFirstDate = new Date(year, month - 1, 1).getDate();
            // 获取当月最后一天
            const nowMonthFinalDate = new Date(year, month, 0).getDate();
            // 获取当月所有日期
            for (let index = nowMonthFirstDate; index <= nowMonthFinalDate; index++){
                monthDaysArr.push({
                    date: index,
                    isFill: false
                });
            };

            // 右侧补齐
            if (nowMonthFinalDay != 6){
                for (let index = nowMonthFinalDay; index < 6; index++){
                    rightFillArr.push({
                        date: new Date(year, month, 1 + index - nowMonthFinalDay).getDate(),
                        isFill: true
                    });
                };
            }
            // 获取完整的日期数组
            monthDaysArr = leftFillArr.concat(monthDaysArr, rightFillArr);
            // 控制面板是 42 or 35; 分行,5行还是6行
            for (let index = 0; index < (monthDaysArr.length === 35 ? 5 : 6); index++){
                monthDaysArr.length === 0 ? null : result[index] = monthDaysArr.splice(0, 7);
            };
            return result;
        };

控制台打印一下结果

控制台1.png

非常完美,最基础也是最重要的部分完成了,接下来就是渲染控制面板了。

3.2、渲染当月面板

说一下这个渲染方式,antd、element使用的是table,devui使用的是无序列表,在这里咱们就用比较low的方式去渲染(div + flex)。

html的结果如下:

   <div class = 'datePicker-box'>
       <!-- 严格遵循 上  中  下-->
       <div class = 'datePicker-box-header'>Header</div>
       <div class = 'datePicker-box-panel'>
           <!-- 2部分,星期标题(日 -> 六) + 数组 -->
           <div class = 'datePicker-box-panel-header'></div>
           <div class = 'datePicker-box-panel-body'></div>
       </div>
       <div class = 'datePicker-box-footer'>今天</div>
   </div>

完整代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }
        html, body {
            width: 100%;
            height: 100%;
        }
        .datePicker-box {
            width: 280px;
            height: 280px;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            box-sizing: border-box;
            border: 1px solid salmon;
            margin-top: 50px;
            margin-left: 100px;
        }
        .datePicker-header, .datePicker-footer {
            width: 100%;
            height: 30px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .datePicker-header {
            border-bottom: 1px solid #f0f0f0;
        }
        .datePicker-footer {
            border-top: 1px solid #f0f0f0;
        }
        .datePicker-panel {
            flex: 1;
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
        }
        .datePicker-panel-header {
            width: 100%;
            height: 30px;
            display: flex;
            justify-content: space-between;
        }
        .weekday-title {
            flex: 1;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .datePicker-panel-body {
            flex: 1;
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
        };
        .dateItemRow {
            width: 100%;
            flex: 1;
            display: flex;
            justify-content: space-between;
        }
        .dateItem {
            flex: 1;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .dateItem:hover {
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="datePicker-box">
        <div class="datePicker-header">Header</div>
        <div class="datePicker-panel">
            <div class="datePicker-panel-header">
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
                <div class="weekday-title"></div>
            </div>
            <div class="datePicker-panel-body"></div>
        </div>
        <div class="datePicker-footer">今天</div>
    </div>
    <script>
        // 接下来就是如何渲染日历
        function createDatePickerPanel (){
            // getMonthDays的实现上面已经写过,此处省略
            const dateArr = getMonthDays(2022, 2);
            for (let index = 0; index < dateArr.length; index++){
                // 创建每行div
                const dateItemRow = document.createElement('div');
                dateItemRow.style.flex = 1;
                dateItemRow.style.display = 'flex';
                dateItemRow.style.justifyContent = 'space-between';
                dateItemRow.style.width = '100%';
                for (let x = 0; x < dateArr[index].length; x++){
                    const dateItem = document.createElement('div');
                    dateItem.style.color = dateArr[index][x].isFill ? '#c0c4cc' : 'black';
                    dateItem.onmouseover = function (){
                        this.style.background = 'cornflowerblue';
                    }
                    dateItem.onmouseleave = function (){
                        this.style.background = null;
                    }
                    dateItem.appendChild(document.createTextNode(dateArr[index][x].date))
                    dateItem.classList.add('dateItem');
                    dateItemRow.appendChild(dateItem);
                };
                document.querySelector('.datePicker-panel-body').appendChild(dateItemRow);
            };
        };

        // 创建日历组件
        function createDatePicker (){
            createDatePickerPanel();
        };

        createDatePicker();
    </script>
</body>
</html>

效果图

效果图3.png

最后

好了,datePicker组件到这里也就算完事了,可能会有人说你上面Header部分的操作栏还没有写呢,emmm,new Date().getMonth()获取当前月,然后左侧-1,右侧+1,点击的时候把之前的panel面板清除掉然后重新执行一遍就可以了(我就不再这里写了,因为这篇的代码实在太多,且不美观,哈哈哈哈哈) ,各位看官对这个组件有想法的可以在底下留言哦。