基于antdesign开发日历calendar组件

534 阅读4分钟

1. 先贴效果图

image.png

2. 思路

引用的antdesign的Calendar组件,引用的时候需要注意calendar的版本问题,看项目中安转的哪个版本的ant,然后才能使用相应的api.这一点一定要注意。

(1)自定义头部

这一点比较好,可以根据实际需求来控制样式,我这里修改了原本的位置,并加了“今天”这项功能,代码如下:

headerRender={({ value, type, onChange, onTypeChange }) => {
            const start = 0;
            const end = 12;
            const monthOptions = [];

            let current = value.clone();
            const localeData = value.localeData();
            const months = [];
            for (let i = 0; i < 12; i++) {
              current = current.month(i);
              months.push(localeData.monthsShort(current));
            }

            for (let i = start; i < end; i++) {
              monthOptions.push(
                <Select.Option key={i} value={i}>
                  {months[i]}
                </Select.Option>,
              );
            }

            const year = value.year();
            const month = value.month();
            const options = [];
            for (let i = year - 50; i < year + 50; i += 1) {
              options.push(
                <Select.Option key={i} value={i}>
                  {i}
                </Select.Option>,
              );
            }
            return (
              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
                <div>
                  <Select
                    size="small"
                    style={{ marginRight: '8px' }}
                    dropdownMatchSelectWidth={false}
                    value={year}
                    onChange={(newYear) => {
                      const now = value.clone().year(newYear);
                      onChange(now);
                    }}
                  >
                    {options}
                  </Select>

                  <Select
                    size="small"
                    dropdownMatchSelectWidth={false}
                    value={month}
                    onChange={(newMonth) => {
                      const now = value.clone().month(newMonth);
                      onChange(now);
                    }}
                  >
                    {monthOptions}
                  </Select>
                </div>
                <div style={{ marginLeft: '-98px', fontSize: '16px', fontWeight: '500' }}>{moment(value).format('YYYY年MM月DD日')}</div>
                <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '60px', cursor: 'pointer' }} onClick={onSetToday}>
                  <a>今天</a>
                </div>
              </div>
            );
          }}

切换到“今天”的日期:

// 切换到今天
  const onSetToday = () => {
    curCell.current = moment();
    onUpdate(curCell.current);
  };

(2)切换年月,以及切换日

// 切换  年/月/日时,执行这里
  const onSelect = (newValue: Moment) => {
    curCell.current = newValue;
    onUpdate(newValue);
  };

(3)自定义每个单元格的样式以及要显示的内容:

// 渲染单元格
  const dateCellRender = (value: Dayjs) => {
    const curDate: any = dateObj[value.format('YYYY-MM-DD')];
    return (
      <div className={curCell.current.format('YYYY-MM-DD') === value.format('YYYY-MM-DD') ? styles.cruCellWrap : styles.cellWrap}>
        <span className={curCell.current.format('YYYY-MM') === value.format('YYYY-MM') ? styles.cellNum : styles.noCruCellNum}>{value.format('DD')}</span>
        <span style={dateStyleMap[curDate]?.style as any}>{dateStyleMap[curDate]?.content}</span>
      </div>
    );
  };

(4)组件代码:

<Calendar
          value={moment(currentDate) as any}
          onSelect={onSelect as any} // 点击选择日期回调
          dateFullCellRender={dateCellRender as any} // 自定义覆盖日期单元格
          headerRender={({ value, type, onChange, onTypeChange }) => {
            const start = 0;
            const end = 12;
            const monthOptions = [];

            let current = value.clone();
            const localeData = value.localeData();
            const months = [];
            for (let i = 0; i < 12; i++) {
              current = current.month(i);
              months.push(localeData.monthsShort(current));
            }

            for (let i = start; i < end; i++) {
              monthOptions.push(
                <Select.Option key={i} value={i}>
                  {months[i]}
                </Select.Option>,
              );
            }

            const year = value.year();
            const month = value.month();
            const options = [];
            for (let i = year - 50; i < year + 50; i += 1) {
              options.push(
                <Select.Option key={i} value={i}>
                  {i}
                </Select.Option>,
              );
            }
            return (
              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
                <div>
                  <Select
                    size="small"
                    style={{ marginRight: '8px' }}
                    dropdownMatchSelectWidth={false}
                    value={year}
                    onChange={(newYear) => {
                      const now = value.clone().year(newYear);
                      onChange(now);
                    }}
                  >
                    {options}
                  </Select>

                  <Select
                    size="small"
                    dropdownMatchSelectWidth={false}
                    value={month}
                    onChange={(newMonth) => {
                      const now = value.clone().month(newMonth);
                      onChange(now);
                    }}
                  >
                    {monthOptions}
                  </Select>
                </div>
                <div style={{ marginLeft: '-98px', fontSize: '16px', fontWeight: '500' }}>{moment(value).format('YYYY年MM月DD日')}</div>
                <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '60px', cursor: 'pointer' }} onClick={onSetToday}>
                  <a>今天</a>
                </div>
              </div>
            );
          }}
        />

3. 我这里的需求是弹窗里包着日历,完整代码如下:

(1)封装组件的使用:

{isOpenCalendar && (
          <ProCalendar
            open={isOpenCalendar}
            currentDate={dateValue}
            dateObj={dateObj}
            onUpdate={(value: Moment) => {
              setDateValue(value);
              startAndEndParams(value);
            }}
            handleOk={(flag, value) => {
              setIsOpenCalendar(flag);
            }}
            onClose={(flag: boolean) => {
              setIsOpenCalendar(flag);
            }}
          />
        )}

(2)完整的弹窗日历组件:

import React, { useRef } from 'react';
import { Modal } from 'antd';
import moment from 'moment';
import type { Moment } from 'moment';
import { Calendar, Select } from 'antd';
import type { Dayjs } from 'dayjs';
import 'dayjs/locale/zh-cn';
import styles from './index.less';
import { dateStyleMap } from './config';

interface IDateObj {
  [key: string]: string;
}

interface IProps {
  open: boolean;
  currentDate: string; // 只能传YYYY-MM-DD格式的
  dateObj: IDateObj; // 日期和属性
  onUpdate: (value: Moment) => void; // 更新
  onClose: (flag: boolean) => void;
  handleOk: (flag: boolean, value: string | Dayjs) => void;
}

/**
 * 组件:日历
 * @param props
 * @returns
 */
const ProCalendar = (props: IProps) => {
  const { open, currentDate, dateObj, onUpdate, onClose, handleOk } = props;
  const curCell = useRef<any>(moment(currentDate));

  // 渲染单元格
  const dateCellRender = (value: Dayjs) => {
    const curDate: any = dateObj[value.format('YYYY-MM-DD')];
    return (
      <div className={curCell.current.format('YYYY-MM-DD') === value.format('YYYY-MM-DD') ? styles.cruCellWrap : styles.cellWrap}>
        <span className={curCell.current.format('YYYY-MM') === value.format('YYYY-MM') ? styles.cellNum : styles.noCruCellNum}>{value.format('DD')}</span>
        <span style={dateStyleMap[curDate]?.style as any}>{dateStyleMap[curDate]?.content}</span>
      </div>
    );
  };

  // 切换  年/月/日时,执行这里
  const onSelect = (newValue: Moment) => {
    curCell.current = newValue;
    onUpdate(newValue);
  };

  // 切换到今天
  const onSetToday = () => {
    curCell.current = moment();
    onUpdate(curCell.current);
  };

  return (
    <Modal
      bodyStyle={{ padding: '0 12px 12px 12px', height: '544px' }}
      wrapClassName={styles.calendarModalWrap}
      title={'查看工作日历'}
      width={700}
      open={open}
      onOk={() => handleOk(false, currentDate)}
      onCancel={() => onClose(false)}
    >
      <div className={styles.calendarWrap}>
        <Calendar
          value={moment(currentDate) as any}
          onSelect={onSelect as any} // 点击选择日期回调
          dateFullCellRender={dateCellRender as any} // 自定义覆盖日期单元格
          headerRender={({ value, type, onChange, onTypeChange }) => {
            const start = 0;
            const end = 12;
            const monthOptions = [];

            let current = value.clone();
            const localeData = value.localeData();
            const months = [];
            for (let i = 0; i < 12; i++) {
              current = current.month(i);
              months.push(localeData.monthsShort(current));
            }

            for (let i = start; i < end; i++) {
              monthOptions.push(
                <Select.Option key={i} value={i}>
                  {months[i]}
                </Select.Option>,
              );
            }

            const year = value.year();
            const month = value.month();
            const options = [];
            for (let i = year - 50; i < year + 50; i += 1) {
              options.push(
                <Select.Option key={i} value={i}>
                  {i}
                </Select.Option>,
              );
            }
            return (
              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '8px' }}>
                <div>
                  <Select
                    size="small"
                    style={{ marginRight: '8px' }}
                    dropdownMatchSelectWidth={false}
                    value={year}
                    onChange={(newYear) => {
                      const now = value.clone().year(newYear);
                      onChange(now);
                    }}
                  >
                    {options}
                  </Select>

                  <Select
                    size="small"
                    dropdownMatchSelectWidth={false}
                    value={month}
                    onChange={(newMonth) => {
                      const now = value.clone().month(newMonth);
                      onChange(now);
                    }}
                  >
                    {monthOptions}
                  </Select>
                </div>
                <div style={{ marginLeft: '-98px', fontSize: '16px', fontWeight: '500' }}>{moment(value).format('YYYY年MM月DD日')}</div>
                <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', width: '60px', cursor: 'pointer' }} onClick={onSetToday}>
                  <a>今天</a>
                </div>
              </div>
            );
          }}
        />
      </div>
    </Modal>
  );
};

export default ProCalendar;



(3)样式:

.calendarModalWrap {
  :global {
    .ant-modal-header {
      padding: 12px !important;
    }
  }
}

.calendarWrap {
  background-color: greenyellow;
  :global {
    .ant-picker-calendar-header {
      padding: 8px 0 0 0;
    }

    .ant-picker-content {
      thead {
        height: 36px;
        text-align: center;
        color: #323233;
        background-color: #f2f2f2;

        tr {
          th {
            padding: 0 !important;
          }
        }
      }
    }
  }

  .cellWrap,
  .cruCellWrap {
    display: block;
    height: 78px;
    margin: 0 4px;
    padding: 4px 8px;
    color: rgba(0, 0, 0, 0.65);
    text-align: center;
    border-bottom: 1px solid #e8e8e8;
    transition: background 0.5s;

    .cellNum {
      display: inline-flex;
      width: 20px;
      height: 24px;
      margin: 10px 0 4px 0;
      font-size: 16px;
      font-weight: 500;
    }

    .noCruCellNum {
      display: inline-flex;
      width: 20px;
      height: 24px;
      margin: 10px 0 4px 0;
      font-size: 16px;
      font-weight: 500;
      color: rgba(0, 0, 0, 0.25);
      transition: color 0.3s;
    }
  }

  .cellWrap {
    &:hover {
      background-color: rgb(243, 243, 243);
    }
  }
  .cruCellWrap {
    &:hover {
      background-color: rgb(243, 243, 243);
    }
    background-color: #fff5f0 !important;
    color: #ed5832 !important;
  }
}

(4)抽离的配置:

export const dateStyleMap: any = {
  '1': {
    type: 'workday',
    content: '工作日',
    style: {
      display: 'inline-flex',
      width: 'calc(100%  - 8px)',
      margin: '0 4px',
      justifyContent: 'center',
      textAlign: 'center',
      position: 'absolute',
      bottom: '0',
      left: '0',
      color: '#fff',
      backgroundColor: '#5FC770',
      borderRadius: '2px 2px 0px 0px',
    },
  },
  '2': {
    type: 'dayOff',
    content: '非工作日',
    style: {
      display: 'inline-flex',
      width: 'calc(100%  - 8px)',
      margin: '0 4px',
      justifyContent: 'center',
      textAlign: 'center',
      position: 'absolute',
      bottom: '0',
      left: '0',
      color: '#fff',
      backgroundColor: '#DB5996',
      borderRadius: '2px 2px 0px 0px',
    },
  },
  '3': {
    type: 'dayOff',
    content: '非工作日',
    style: {
      display: 'inline-flex',
      width: 'calc(100%  - 8px)',
      margin: '0 4px',
      justifyContent: 'center',
      textAlign: 'center',
      position: 'absolute',
      bottom: '0',
      left: '0',
      color: '#fff',
      backgroundColor: '#DB5996',
      borderRadius: '2px 2px 0px 0px',
    },
  },
};

4.总结

根据使用ant design的组件,通过自己定制化修改,发现其实日历组件很好写,并不想之前认为的很难。简单思路梳理:

  1. 先写42格子 和 头部星期(一排7个格子)
  2. 再给每个格子绑定事件
  3. 写年月切换功能,进行控制格子
  4. 定制化每个格子的内容和样式

以上仅仅是简单记录,如有疑惑,欢迎探讨。也很欢迎各位大佬指出我的不足之处。