整天用 Calendar 日历组件,不如自己手写一个吧!

news2024/11/25 19:47:24

目录

Date 的 api

实现日历组件

静态的布局

再来写逻辑

增加两个参数

提供 ref 来暴露 api

总结


        日历组件想必大家都用过,在各个组件库里都有。比如 antd 的 Calendar 组件(或者 DatePicker 组件):

图片

那这种日历组件是怎么实现的呢?其实原理很简单,今天我们就来自己实现一个。

Date 的 api

首先,要过一下 Date 的 api:

        创建 Date 对象时可以传入年月日时分秒。比如 2023 年 7 月 30,就是这么创建:

new Date(2023, 6, 30);

可以调用 toLocaleString 来转成当地日期格式的字符串显示:

图片

有人说 7 月为啥第二个参数传 6 呢?因为 Date 的 month 是从 0 开始计数的,取值是 0 到 11:

图片

        而日期 date 是从 1 到 31。而且有个小技巧,当你 date 传 0 的时候,取到的是上个月的最后一天:

图片

        -1 就是上个月的倒数第二天,-2 就是倒数第三天这样。这个小技巧有很大的用处,可以用这个来拿到每个月有多少天:

图片

今年一月 31 天、二月 28 天、三月 31 天。。。

除了日期外,也能通过 getFullYear、getMonth 拿到年份和月份:

图片

还可以通过 getDay 拿到星期几。比如今天(2023-7-19)是星期三:

图片

就这么几个 api 就已经可以实现日历组件了。不信?我们来试试看:

实现日历组件

用 cra 创建 typescript 的 react 项目:

npx create-react-app --template=typescript calendar-test

图片

静态的布局

我们先来写下静态的布局:

大概一个 header,下面是从星期日到星期六,再下面是从 1 到 31:

改下 App.tsx:

import React from 'react';
import './index.css';

function Calendar() {
  return (
    <div className="calendar">
      <div className="header">
        <button>&lt;</button>
        <div>2023 年 7 月</div>
        <button>&gt;</button>
      </div>
      <div className="days">
        <div className="day">日</div>
        <div className="day">一</div>
        <div className="day">二</div>
        <div className="day">三</div>
        <div className="day">四</div>
        <div className="day">五</div>
        <div className="day">六</div>
        <div className="empty"></div>
        <div className="empty"></div>
        <div className="day">1</div>
        <div className="day">2</div>
        <div className="day">3</div>
        <div className="day">4</div>
        <div className="day">5</div>
        <div className="day">6</div>
        <div className="day">7</div>
        <div className="day">8</div>
        <div className="day">9</div>
        <div className="day">10</div>
        <div className="day">11</div>
        <div className="day">12</div>
        <div className="day">13</div>
        <div className="day">14</div>
        <div className="day">15</div>
        <div className="day">16</div>
        <div className="day">17</div>
        <div className="day">18</div>
        <div className="day">19</div>
        <div className="day">20</div>
        <div className="day">21</div>
        <div className="day">22</div>
        <div className="day">23</div>
        <div className="day">24</div>
        <div className="day">25</div>
        <div className="day">26</div>
        <div className="day">27</div>
        <div className="day">28</div>
        <div className="day">29</div>
        <div className="day">30</div>
        <div className="day">31</div>
      </div>
    </div>
  );
}

export default Calendar;

直接跑起来看下渲染结果再讲布局:

npm run start

图片

这种布局还是挺简单的:

header 就是一个 space-between 的 flex 容器:

图片

下面是一个 flex-wrap 为 wrap,每个格子宽度为 100% / 7 的容器:

图片

图片

全部样式如下:

.calendar {
  border: 1px solid #aaa;
  padding: 10px;
  width: 300px;
  height: 250px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 40px;
}

.days {
  display: flex;
  flex-wrap: wrap;
}

.empty, .day {
  width: calc(100% / 7);
  text-align: center;
  line-height: 30px;
}

.day:hover {
  background-color: #ccc;
  cursor: pointer;
}

再来写逻辑

然后我们再来写逻辑:

图片

        首先,我们肯定要有一个 state 来保存当前的日期,默认值是今天。然后点击左右按钮,会切换到上个月、下个月的第一天。

const [date, setDate] = useState(new Date());

const handlePrevMonth = () => {
    setDate(new Date(date.getFullYear(), date.getMonth() - 1, 1));
};

const handleNextMonth = () => {
    setDate(new Date(date.getFullYear(), date.getMonth() + 1, 1));
};

然后渲染的年月要改为当前 date 对应的年月:

图片

我们试试看:

图片

年月部分没问题了。再来改下日期部分:

我们定义一个 renderDates 方法:

const daysOfMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
};

const firstDayOfMonth = (year: number, month: number) => {
    return new Date(year, month, 1).getDay();
};

const renderDates = () => {
    const days = [];

    const daysCount = daysOfMonth(date.getFullYear(), date.getMonth());
    const firstDay = firstDayOfMonth(date.getFullYear(), date.getMonth());

    for (let i = 0; i < firstDay; i++) {
      days.push(<div key={`empty-${i}`} className="empty"></div>);
    }

    for (let i = 1; i <= daysCount; i++) {
      days.push(<div key={i} className="day">{i}</div>);
    }

    return days;
};

首先定义个数组,来存储渲染的内容。

然后计算当前月有多少天,这里用到了前面那个 new Date 时传入 date 为 0 的技巧。

再计算当前月的第一天是星期几,也就是 new Date(year, month, 1).getDay()

这样就知道从哪里开始渲染,渲染多少天了。

然后先一个循环,渲染 day - 1 个 empty 的块。

再渲染 daysCount 个 day 的块。

这样就完成了日期渲染:

图片

我们来试试看:

图片

        没啥问题。这样,我们就完成了一个 Calendar 组件!是不是还挺简单的?确实,Calendar 组件的原理比较简单。

增加两个参数

        接下来,我们增加两个参数,value 和 onChange。这俩参数和 antd 的 Calendar 组件一样。

value 参数设置为 date 的初始值:

图片

我们试试看:

图片

图片

年月是对了,但是日期对不对我们也看不出来,所以还得加点选中样式:

图片

图片

现在就可以看到选中的日期了:

图片

没啥问题。然后我们再加上 onChange 的回调函数:

图片

就是在点击 day 的时候,调用 bind 了对应日期的 onChange 函数。我们试试看:

图片

图片

        也没啥问题。现在这个 Calendar 组件就是可用的了,可以通过 value 来传入初始的 date 值,修改 date 之后可以在 onChange 里拿到最新的值。

提供 ref 来暴露 api

        大多数人到了这一步就完成 Calendar 组件的封装了。这当然没啥问题。但其实你还可以再做一步,提供 ref 来暴露一些 Canlendar 组件的 api。

图片

图片

关于 forwardRef + useImperativeHandle 的详细介绍,可以看我之前的那篇: 让你 React 组件水平暴增的 5 个技巧

用的时候这样用:

图片

试试看:

图片

        ref 的 api 也都生效了。这就是除了 props 之外,另一种暴露组件 api 的方式。你经常用的 Canlendar 或者 DatePicker 组件就是这么实现的,

图片

        当然,这些组件除了本月的日期外,其余的地方不是用空白填充的,而是上个月、下个月的日期。这个也很简单,拿到上个月、下个月的天数就知道填什么日期了。

全部代码如下:

import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import './index.css';

interface CalendarProps {
  value?: Date,
  onChange?: (date: Date) => void
}

interface CalendarRef {
  getDate: () => Date,
  setDate: (date: Date) => void,
}

const InternalCalendar: React.ForwardRefRenderFunction<CalendarRef, CalendarProps> = (props, ref) => {
  const {
    value = new Date(),
    onChange,
  } = props;

  const [date, setDate] = useState(value);

  useImperativeHandle(ref, () => {
    return {
      getDate() {
        return date;
      },
      setDate(date: Date) {
        setDate(date)
      }
    }
  });

  const handlePrevMonth = () => {
    setDate(new Date(date.getFullYear(), date.getMonth() - 1, 1));
  };

  const handleNextMonth = () => {
    setDate(new Date(date.getFullYear(), date.getMonth() + 1, 1));
  };

  const monthNames = [
    '一月',
    '二月',
    '三月',
    '四月',
    '五月',
    '六月',
    '七月',
    '八月',
    '九月',
    '十月',
    '十一月',
    '十二月',
  ];

  const daysOfMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
  };

  const firstDayOfMonth = (year: number, month: number) => {
    return new Date(year, month, 1).getDay();
  };

  const renderDates = () => {
    const days = [];

    const daysCount = daysOfMonth(date.getFullYear(), date.getMonth());
    const firstDay = firstDayOfMonth(date.getFullYear(), date.getMonth());

    for (let i = 0; i < firstDay; i++) {
      days.push(<div key={`empty-${i}`} className="empty"></div>);
    }

    for (let i = 1; i <= daysCount; i++) {
      const clickHandler = onChange?.bind(null, new Date(date.getFullYear(), date.getMonth(), i));
      if(i === date.getDate()) {
        days.push(<div key={i} className="day selected" onClick={clickHandler}>{i}</div>);  
      } else {
        days.push(<div key={i} className="day" onClick={clickHandler}>{i}</div>);
      }
    }

    return days;
  };

  return (
    <div className="calendar">
      <div className="header">
        <button onClick={handlePrevMonth}>&lt;</button>
        <div>{date.getFullYear()}年{monthNames[date.getMonth()]}</div>
        <button onClick={handleNextMonth}>&gt;</button>
      </div>
      <div className="days">
        <div className="day">日</div>
        <div className="day">一</div>
        <div className="day">二</div>
        <div className="day">三</div>
        <div className="day">四</div>
        <div className="day">五</div>
        <div className="day">六</div>
        {renderDates()}
      </div>
    </div>
  );
}

const Calendar = React.forwardRef(InternalCalendar);

function Test() {
  const calendarRef = useRef<CalendarRef>(null);

  useEffect(() => {
    console.log(calendarRef.current?.getDate().toLocaleDateString());

    setTimeout(() => {
      calendarRef.current?.setDate(new Date(2024, 3, 1));
    }, 3000);
  }, []);

  return <div>
    {/* <Calendar value={new Date('2023-3-1')} onChange={(date: Date) => {
        alert(date.toLocaleDateString());
    }}></Calendar> */}
    <Calendar ref={calendarRef} value={new Date('2024-8-15')}></Calendar>
  </div>
}
export default Test;
.calendar {
  border: 1px solid #aaa;
  padding: 10px;
  width: 300px;
  height: 250px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 40px;
}

.days {
  display: flex;
  flex-wrap: wrap;
}

.empty, .day {
  width: calc(100% / 7);
  text-align: center;
  line-height: 30px;
}

.day:hover, .selected {
  background-color: #ccc;
  cursor: pointer;
}

总结

        Calendar 或者 DatePicker 组件我们经常会用到,今天自己实现了一下。其实原理也很简单,就是 Date 的 api。

        new Date 的时候 date 传 0 就能拿到上个月最后一天的日期,然后 getDate 就可以知道那个月有多少天。

        然后再通过 getDay 取到这个月第一天是星期几,就知道怎么渲染这个月的日期了。

        我们用 react 实现了这个 Calendar 组件,支持传入 value 指定初始日期,传入 onChange 作为日期改变的回调。

        除了 props 之外,还额外提供 ref 的 api,通过 forwarRef + useImperativeHandle 的方式。

        整天用 DatePicker 组件,不如自己手写一个吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/794555.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【论文阅读】A Comprehensive Survey

论文来源&#xff1a;Li M , Liu Y , Liu X ,et al.The Deep Learning Compiler: A Comprehensive Survey[J]. 2020.DOI:10.1109/TPDS.2020.3030548. 这是一篇关于深度学习编译器的综述类文章。 什么是深度学习编译器 深度学习&#xff08;Deep Learning&#xff09;编译器将…

STM32H5开发(4)----开发板介绍

STM32H5开发----4.开发板介绍 套件概述样品申请特征系统控制和生态系统访问功能示意图系统框图跳线设置开发板原理图 套件概述 STM32H503RBTx_LQFP64是STM32H5系列微控制器的一款出色评估套件&#xff0c;它采用了先进的40nm工艺制造&#xff0c;为开发者提供了卓越的性能和能…

MyBatis基础模块-类型转换模块

文章目录 1. 为什么需要类型转换模块2. TypeHandler 1. 为什么需要类型转换模块 执行sql&#xff0c;在PreparedStatement设置参数时&#xff0c;需要把java类型转换成jdbc类型&#xff0c;而从结果集中获取数据时&#xff0c;需要把jdbc类型转换为java类型。 2. TypeHandle…

软件测试未来的发展趋势以及软件测试进阶路线

全球各地的企业每天都在发展变化着&#xff0c;以应对市场挑战&#xff0c;满足日益成熟的客户需求。即使是正在进行的技术进步也会使软件测试专家在实践的过程中更加专注和精确。 2021年给软件测试领域带来了新的技术解决方案&#xff0c;以及质量保证和软件测试的实现。与此同…

【数据结构和算法14】堆结构(基于数组实现)

目录 1、有关二叉树和堆的介绍 2、大根堆的代码实现 3、小根堆的代码实现 1、有关二叉树和堆的介绍 计算机科学中&#xff0c;堆是一种基于树的数据结构&#xff0c;通常用完全二叉树实现。堆的特性如下 在大顶堆&#xff08;大根堆&#xff09;中&#xff0c;任意节点 C 与…

开源视频监控管理平台国标GB28181视频EasyCVR电子地图功能展示优化

视频监控综合管理平台EasyCVR可提供的视频能力包括&#xff1a;视频监控直播、云端录像、云存储、录像检索与回看、告警上报、平台级联、云台控制、语音对讲、电子地图、H.265自动转码等&#xff0c;也具备接入AI智能分析的能力。 视频汇聚平台EasyCVR可拓展性强、视频能力灵活…

centos7中用shell脚本实现mysql分库分表备份

环境&#xff1a; 脚本&#xff1a; #!/bash/bin back_path/backup/db databases_file/backup/databases.list [ -f $databases_file ] || touch /backup/databases.list if [[ ! -s ${databases_file} ]] thenecho "$databases_file为空,请在该文件中写上需要备份的数据…

C# Modbus TCP上位机测试

前面说了三菱和西门子PLC的上位机通信&#xff0c;实际在生产应用中&#xff0c;设备会有很多不同的厂家生产的PLC&#xff0c;那么&#xff0c;我们就需要一种通用的语言&#xff0c;进行设备之间的通信&#xff0c;工业上较为广泛使用的语言之一就是Modbus。 Modbus有多种连…

filscan api 获取错误扇区个数

获取错误扇区个数 POST 请求 curl -s -X POST -H "Content-Type: application/json" -d {"account_id": "f01889512"} https://api-v2.filscan.io/api/v1/AccountInfoByID | jq -r .result.account_info.account_miner.account_indicator.fault…

JMM内存模型深入详解,探索volatile、synchronized与VarHandle深层次的奥秘

文章目录 一、JMM内存模型1、什么是JMM&#xff08;1&#xff09;参考资料 2、竞态条件&#xff08;Race Condition&#xff09;&#xff08;1&#xff09;实例 3、同步动作&#xff08;Synchronization Order&#xff09;&#xff08;1&#xff09;实例&#xff08;2&#xff…

fSGAT批量候选基因关联分析丨快速单基因关联分析

候选基因如何分析&#xff1f; 通常情况下关联分析会得到一大堆候选基因&#xff0c;总不可能每个都有用&#xff0c;因此需要对候选基因进行深一步分析&#xff0c;本篇笔记分享一下群体遗传学研究中GWAS候选位点与候选基因的筛选思路。主要的方式包括单基因关联分析、连锁程度…

Appium+python自动化(二十三)- Monkeyrunner与Monkey傻傻的分不清楚(超详解)

monkeyrunner简介 1.monkeyrunner工具使用Jython&#xff0c;这是一种使用Java编程语言的Python实现。Jython允许monkeyrunner API与Android框架轻松交互。使用Jython&#xff0c;您可以使用Python语法来访问API的常量&#xff0c;类和方法。MonkeyRunner工具是使用Jython(使用…

【VCS】(7)Fast Gate-level Verification

Fast Gate-level Verification VCS中SDF反标(Back-Annotation)Lab 门级网表的后仿真DC综合RTL级仿真波形后仿真 网表级的仿真可以验证综合后得到的门级网表和RTL代码是否一致。也可以验证&#xff0c;在加速时序信息&#xff08;SDF&#xff09;之后&#xff0c;设计的功能是否…

基于POX、JBX交叉的遗传算法求解车间调度

对于流水车间调度问题&#xff0c;n个工件在m台设备上加工&#xff0c;已知每个工件每个工序使用的机器和每个工件每个工序所用时间&#xff0c;通过决策每个机器上工件的加工顺序和每个工序的开始时间&#xff0c;使完成所有工序所用时间(makespan)最小。具有下列约束&#xf…

58寸透明屏的画质怎么样?

58寸透明屏是一种新型的显示屏技术&#xff0c;它具有透明度高、色彩鲜艳、清晰度高等特点&#xff0c;可以广泛应用于商业展示、户外广告、智能家居等领域。 首先&#xff0c;58寸透明屏的透明度高。 透明屏采用了先进的光学技术&#xff0c;使得屏幕在显示图像的同时&#x…

ubuntu 18.04 磁盘太满无法进入系统

安装了一个压缩包&#xff0c;装了一半提示磁盘空间少导致安装失败。我也没在意&#xff0c;退出虚拟机打算扩展硬盘。等我在虚拟机设置中完成扩展操作&#xff0c;准备进入虚拟机内部进行操作时&#xff0c;发现登录不进去了 shift 登入GUN GRUB设置项的问题 网上都是在开机…

Rust vs Go:常用语法对比(十三)

题图来自 Go vs. Rust: The Ultimate Performance Battle 241. Yield priority to other threads Explicitly decrease the priority of the current process, so that other execution threads have a better chance to execute now. Then resume normal execution and call f…

在centos 7系统docker上构建mysql 5.7

一、VM上已经安装centos 7.9&#xff0c;且已完成docker的构建 二、安装mysql5.7 安装镜像&#xff1a;[rootlocalhost lll]# docker pull mysql:5.7 查看镜像[rootlocalhost lll]# docker images 根据镜像id构建mysql容器&#xff0c;且分配端口号[rootlocalhost lll]# dock…

勘探开发人工智能应用:断层识别

1 断层识别 断层是地下岩层在受到挤压或拉伸力作用下,因脆性变形而形成的地层错断,是一种重要的地质构造特征。断层检测和解释是从地震剖面中认识岩层结构和储层特性的重要步骤。 1.1 数据描述 合成地震数据: 每一个合成地震数据都是由地质模型的反射系数与雷克子波进行褶…

chrome插件推荐

chrome插件推荐 chrome的一些插件, 真的能很大程度上提升我们的工作效率。而且chrome的插件极其丰富, 基本你想要的功能,都能找到对应的插件&#xff0c;接下来给大家推荐几个我自己在用的。 插件 1、Momentum 新标签页 简介: 超漂亮的新标签页面。每日更新精彩背景壁纸图片&…