1. 先贴效果图
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的组件,通过自己定制化修改,发现其实日历组件很好写,并不想之前认为的很难。简单思路梳理:
- 先写42格子 和 头部星期(一排7个格子)
- 再给每个格子绑定事件
- 写年月切换功能,进行控制格子
- 定制化每个格子的内容和样式
以上仅仅是简单记录,如有疑惑,欢迎探讨。也很欢迎各位大佬指出我的不足之处。