react模仿antd手写一个多选日期日历组件

业务需求

  1. 多选近三个月的日期。
  2. 不能选择当日之前的日期。

因为antd的日期组件都是选择单个日期或者日期范围。不符合需求,所以自己就实现了一个。写的不好的地方大家请指教

效果展示

在这里插入图片描述
测试组件

<CheckCalendar
      visible={this.state.showCalendar}
       onClose={()=>{
            this.setState({
                 showCalendar:false
            })
       }}
       onConfirm={(isCheck)=>{
            console.log(isCheck)
            this.setState({
                 showCalendar:false
            })
       }}
       />

CheckCalendar.jsx

import React, { Component, Fragment } from "react";
import { cloneDeep, chunk } from "lodash";
import PropTypes from 'prop-types';
import "animate.css";
import "./index.scss"

class CheckCalendar extends Component {
    constructor(props) {
        super(props)
        this.state = {
            dateTable: [],
            isCheck: [],
        }
        this.calendar = React.createRef();
        this.mask = React.createRef();
    }

    componentWillMount() {
        this.initDateTable()
    }

    initDateTable() {
        let temp = []
        for (let i = 0; i < 2; i++) {  // 取近三个月内的日期
            let obj = this.getDateTable(i);
            temp.push(obj);
        }
        this.setState({
            dateTable: temp
        });
    }

    getDateTable(plus) {
        let curDate = new Date()  //现在时间
        let curYear = curDate.getFullYear();
        let curMonth = curDate.getMonth() + 1;
        let curDay = curDate.getDate();
        if (curMonth + plus > 12) {
            curYear++
            curMonth = curMonth + plus - 12
        } else {
            curMonth = curMonth + plus
        }
        let date = new Date(curYear, curMonth, 0);
        let year = date.getFullYear(); // 当前年
        let month = date.getMonth() + 1; // 当前月
        // console.log(`${year}年${month}月.`);

        let date2 = new Date(year, month, 0);
        let days = date2.getDate(); // 当月有多少天
        // console.log(`当月有${days}天.`);

        date2.setDate(1);
        let day = date2.getDay(); // 当月第一天是星期几
        // console.log(`当月第一天是星期${day}.`);

        let list = [];

        for (let i = 0; i < days + day; i++) {
            if (i < day) {  // 头部补零
                list.push({
                    isActive: false,
                    number: 0
                });
            } else {
                if (plus === 0) {
                    if ((i - day + 1) < curDay) {
                        list.push({
                            disable: true,
                            isActive: false,
                            number: i - day + 1
                        });
                    } else {
                        list.push({
                            isActive: false,
                            number: i - day + 1
                        });
                    }
                } else {
                    list.push({
                        isActive: false,
                        number: i - day + 1
                    });
                }
            }
        }
        let hlist = chunk(list, 7); // 转换为二维数组
        let len = hlist.length;
        let to = 7 - hlist[len - 1].length;

        // 循环尾部补0
        for (let i = 0; i < to; i++) {
            hlist[len - 1].push({
                isActive: false,
                number: 0
            });
        }
        if (month < 10) {
            month = "0" + month
        }
        const str = `${year}-${month}`
        return {
            "list": hlist,
            "desc": str
        }
    }

    handleItemClick(desc, number, index, index1, index2) {
        let temp = cloneDeep(this.state.dateTable)
        const flag = !temp[index].list[index1][index2].isActive
        temp[index].list[index1][index2].isActive = flag
        this.setState({
            dateTable: temp,
        })
        const arr = desc.split("-");
        if (number < 10) {
            number = "0" + number
        }
        if (flag) {
            let temp = cloneDeep(this.state.isCheck);
            temp.push(arr[0] + "-" + arr[1] + "-" + number)
            this.setState({
                isCheck: temp
            })
        } else {
            let temp = cloneDeep(this.state.isCheck);
            let filted = temp.filter((item) => {
                return item !== arr[0] + "-" + arr[1] + "-" + number
            })
            this.setState({
                isCheck: filted
            })
        }
    }

    onExit = () => {
        const { onCancel } = this.props;

        onCancel && onCancel();
    }

    onConfirm = () => {
        const { onConfirm } = this.props;

        onConfirm && onConfirm(this.state.isCheck);
    }

    render() {
        return this.props.visible ? (
            <div className="calendar-mask">
                <div className="calendar-wrap animated fadeInUp">
                    <RenderCalendarHeader
                        onExit={this.onExit}
                    />
                    <RenderChineseWeek />
                    <RenderDateTemp
                        dateTable={this.state.dateTable}
                        handleItemClick={this.handleItemClick}
                        self={this}
                    />
                    <div className="fake-area"></div>
                </div>
                <RenderConfirm
                    onConfirm={this.onConfirm}
                />
            </div>
        ) : (<span></span>)
    }
}

/**
 * 渲染表格每个item
 * 
 */
const RenderDateItem = (props) => {
    const { number, active } = props;

    return number === 0 ?
        (
            <div className="date-wrap">
                <span className="left"></span><div className="item"></div><span className="right"></span>
            </div>
        ) : props.disable ?
            (
                <div className="date-wrap">
                    <span className="left"></span>
                    <div className="item disable">{number}</div>
                    <span className="right"></span>
                </div>
            ) :
            (
                <div className="date-wrap">
                    <span className="left"></span>
                    <div className={`item ${active ? 'active' : ''}`} onClick={props.itemClick} >
                        <span>{number}</span>
                    </div>
                    <span className="right"></span>
                </div>
            )
}

/**
 * 日历顶部
 * @param props.onExit 退出事件 
 */
const RenderCalendarHeader = (props) => {
    const { onExit } = props;
    return (
        <div className="header">
            <span>日期多选</span>
            <div className="exit" onClick={onExit}></div>
        </div>
    )
}

/**
 * 渲染中文日期
 */
const RenderChineseWeek = () => {
    const weeks = ["日", "一", "二", "三", "四", "五", "六"];
    return (
        <div className="week-wrap">
            {
                weeks.map((item, index) => (
                    <div className="week-item" key={index}>{item}</div>
                ))
            }
        </div>
    )
}

/**
 * 
 * @param props.dateTable 模板数组
 * @param prop.handleItemClick item点击事件
 * @param prop.self 父组件作用域
 */
const RenderDateTemp = (props) => {
    const { dateTable, handleItemClick, self } = props;
    return (
        <Fragment>
            {
                dateTable.map((item, index) => {
                    const arr = item.desc.split("-");
                    return (
                        <div className="date-table" key={index}>
                            <span className="desc">
                                {arr[0] + "年" + arr[1] + "月"}
                            </span>
                            {
                                item.list.map((item2, index2) => {
                                    return (
                                        <div className="row" key={index2}>
                                            {
                                                item2.map((item3, index3) => {
                                                    return (
                                                        <RenderDateItem
                                                            itemClick={handleItemClick.bind(self, item.desc, item3.number, index, index2, index3)}
                                                            active={item3.isActive}
                                                            disable={item3.disable ? item3.disable : false}
                                                            number={item3.number}
                                                            key={index3}
                                                        />
                                                    )
                                                })
                                            }
                                        </div>
                                    )
                                })
                            }
                        </div>
                    );
                })
            }
        </Fragment>
    )
}

const RenderConfirm = (props) => {
    return (
        <div className="confirm-wrap">
            <div className="confirm" onClick={props.onConfirm}>
                确定
            </div>
        </div>
    )
}

/**
 * @param onCancel 关闭事件回调
 * @param onConfirm 确认事件回调
 * @param visible 组件显示状态
 */
CheckCalendar.propTypes = {
    onCancel: PropTypes.func,
    onConfirm: PropTypes.func,
    visible: PropTypes.bool
}

export default CheckCalendar;

checkCalendar.scss

.calendar-mask {
    position: fixed;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
    background-color: rgba(0, 0, 0, 0.5);

    .calendar-wrap {
        z-index: 999;
        width: 100%;
        height: 100%;
        background-color: #ffffff;
        overflow: auto;
        animation-duration: .3s;

        .header {
            color: black;
            font-size: 17px;
            font-weight: bold;
            height: 30px;
            line-height: 30px;

            .exit {
                width: 20px;
                height: 20px;
                position: relative;
                float: left;
                left: 20px;
                top: 5px;
            }

            .exit::before,
            .exit::after {
                content: "";
                position: absolute;
                height: 20px;
                width: 1.5px;
                left: 8.5px;
                background: #098fef;
            }

            .exit::before {
                transform: rotate(45deg);
            }

            .exit::after {
                transform: rotate(-45deg);
            }

        }

        .week-wrap {
            display: flex;
            font-size: 16px;
            border-bottom: 1px solid rgb(221, 221, 221);

            .week-item {
                height: 30px;
                line-height: 30px;
                width: 14.28571429%;
            }
        }

        .date-table {
            margin-top: 20px;

            .desc {
                text-align: left;
                text-indent: 12px;
                font-size: 18px;
            }

            .row {
                display: flex;
                margin: 8px 0px;

                .date-wrap {
                    height: 35px;
                    width: 14.28571429%;
                    line-height: 30px;

                    .left {
                        width: 100%;
                    }

                    .item {
                        display: inline-block;
                        width: 35px;
                        height: 35px;
                        font-size: 15px;
                        font-weight: bold;
                        line-height: 35px;
                        border-radius: 50%;
                    }

                    .disable {
                        background-color: rgb(238, 238, 238);
                        color: rgb(187, 187, 187);
                    }

                    .active {
                        background-color: #108ee9;
                        color: #ffffff;
                    }

                    .right {
                        width: 100%;
                    }
                }
            }
        }

        .fake-area {
            height: 53px;
            width: 100%;
        }
    }

    .confirm-wrap {
        position: fixed;
        bottom: 0;
        height: 54px;
        width: 100%;
        box-sizing: border-box;
        border-top: 1px solid rgb(221, 221, 221);
        background-color: rgb(247, 247, 247);
        display: flex;
        align-items: center;
        justify-content: center;

        .confirm {
            border-radius: 5px;
            width: 90%;
            background-color: #108ee9;
            font-size: 18px;
            color: #ffffff;
            padding: 8px 0;

            &:active {
                background-color: rgb(14, 128, 210);
                color: rgb(85, 166, 223)
            }
        }
    }
}
发布了51 篇原创文章 · 获赞 18 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/weixin_42565137/article/details/100598144