Taro 封装日历组件

API

参数 说明 类型 默认值 必填
lang 星期的写法,中/英 ‘CN’/‘EN’ ‘CN’
columnGap 列间距 number 0
rowGap 行间距 number 0
backgroundColor 背景色 string ‘#ffffff’
currentDayColor 当前日期的背景色 string ‘#A399E2’
planDayColor 有计划的日期的背景色 string ‘#CEBAF9’
planDayArr 拥有计划的日期 number[] []
onClickDay 点击日期的回调函数 (year: number, month: number, day: number) => void -
headComponent 自定义日历头组件 ReactNode -
containerStyle 整体日历的样式 CSSProperties -
className 类名 string -

对外暴露的方法

使用场景:父组件控制日历的组件的翻动

方法名 说明
toNextMonth 日历组件跳转到下一个月
toPreMonth 日历组件返回上一个月(目前设置,不能返回当月之前的月份)

使用方法:

  1. 挂载ref
// 父组件
const controlRef = useRef<null | ControlRefFunctionProps>(null)
....
<HCalendar ref={controlRef}/>
  1. 使用
controlRef.current.toNextMonth()
controlRef.current.toPreMonth()

使用案例

<HCalendar 
    ref={controlRef}
    className={styles['calendar']}
    headComponent={<HeadRender/>}
    lang={'EN'}
    backgroundColor={'#ffffff'}
    currentDayColor={'#1f95af'}
    planDayColor={'#3e646d'}
    onClickDay={(year,month,day) => {
        console.log(year,month,day)
    }}
/>

<HeadRender/>为自定义日历头组件

const HeadRender = () => {
    return (
        <View className={styles['calendar-header']}>
            <View className={styles['calendar-header-controll']}>
                <Text className={styles['mounth-text']}>Januari {(calCurTimeInfo && calCurTimeInfo[0]) || 2022}</Text>
                <View className={styles['calendar-header-arrows']}>
                    <HArrow onClick={(e: { stopPropagation: () => any }) => controlFunctions(e,'toPreMonth')} color={'#90959e'} fixedAngle={-135}></HArrow>
                    <HArrow onClick={(e: { stopPropagation: () => any }) => controlFunctions(e,'toNextMonth')} color={'#90959e'} fixedAngle={45}></HArrow>
                </View>
            </View>
            <View className={styles['division']}></View>
        </View>
    )
}

工具类方法

封装得比较差,可根据效果自行封装

/**
 * 获取时间信息,返回数组 年、月、日、星期、原始的星期
 */
const getTime = () => {
    const date = new Date()
    const year = date.getFullYear()
    const month = date.getMonth() + 1
    const day = date.getDate()
    const originalWeek = date.getDay()
    const week = '星期' + ['天','一', '二', '三', '四', '五', '六'][originalWeek] // 获取星期
    return [year, month, day, week,originalWeek] as const
}

/**
 * 返回指定年指定月的天数
 * @param year  年
 * @param month  月
 * @returns  天数
 */
function getMonthDayNumber(year:number,month:number) {
    if (month === 2) {
        // 判断是平年还是闰年
        if ((!(year % 4) && year % 100) || !(year % 400)) {
            return 29
        } else {
            return 28
        }
    } else {
        return longMonth.indexOf(month as number) === -1 ? 30 : 31
    }
}
/**
 * fields 节点选择,可添加返回的数据
 * selector:节点选择
 * extraAttribute:额外返回的数据
 *  */ 
const selectorQueryFields = (selector: string, extraAttribute: string[] = []): Promise<TaroGeneral.IAnyObject> => {
    return new Promise(resolve => {
        const query = Taro.createSelectorQuery()
        query.select(selector).fields({
            dataset: true,
            size: true,
            scrollOffset: true,
            properties: ['scrollX', 'scrollY'],
            computedStyle: extraAttribute,
            context: true,
        }, res => resolve(res)).exec()
    })
}

使用到的自定义组件

<HGrid>:用于Grid布局封装的组件

详细请点击此处

完整代码

index.tsx

import styles from "./index.module.css"
import { View, Text } from "@tarojs/components"
import HGrid from "../HGrid"
import HGridItem from "../HGridItem"
import { CSSProperties, ReactNode, useEffect, useImperativeHandle, useMemo, useState } from "react"
import { getTime, getMonthDayNumber } from "../../utils/date"
import { selectorQueryFields } from "../../utils/index"
import React from "react"

interface CalendarProps {
    lang?: 'CN' | 'EN', // 星期的写法,中/英
    columnGap?: number, // 列间距
    rowGap?: number, // 行间距
    backgroundColor?: string, // 背景色
    currentDayColor?: string, // 当前日期的颜色
    planDayColor?: string, /// 计划日期的颜色
    planDayArr?: number[], // 计划时间的列表
    onClickDay?: (year: number, month: number, day: number) => void, // 点击日期的回调函数
    headComponent?: ReactNode,
    containerStyle?: CSSProperties, // 样式
    className?: string // 类名
}

// 对外暴露方法的interface
export interface ControlRefFunctionProps {
    toPreMonth: Function,
    toNextMonth: Function
}
/**
 * 创建日历数组
 * @param dayLength 一个月的天数
 * @param startWeek 一个月开始的星期
 */
const createCalendarData = (dayLength: number, startWeek: number) => {
    let res: string[] = []
    // 下一个月所占的天数
    const remaining = 35 - dayLength - startWeek
    // 填充上一个月的位置
    while (startWeek) {
        res.push('')
        startWeek--
    }
    // 填充这个月的日子
    for (let i = 1; i <= dayLength; i++) {
        res.push(String(i))
    }
    // 填充下一个月的位置
    for (let j = 0; j < remaining; j++) {
        res.push('')
    }
    return res
}

const Calendar = React.forwardRef((props: CalendarProps, ref) => {
    let {
        lang = 'CN',
        rowGap = 0,
        columnGap = 0,
        containerStyle,
        backgroundColor = '#E3D8FC',
        currentDayColor = '#A399E2',
        planDayColor = '#CEBAF9',
        planDayArr = [],
        onClickDay,
        headComponent,
        className
    } = props

    const [calendarData, setCalendarData] = useState<Array<Array<string>> | null>(null)
    const [point, setPoint] = useState<number>(0) // 指针
    const [dayLength, setDayLength] = useState<Array<number>>([30]) // 记录每月的长度
    const [startWeek, setStartWeek] = useState<Array<number>>([0]) // 该月第一天的星期
    const [day, setDay] = useState<Array<number>>([2022, 1, 1]) // 当天的日子,第一个参数为年,第二个参数为月,第三个参数为日
    const [chosedDay, setChosedDay] = useState(33) // 当前选中的日期
    const [computedWidth, setComputedWidth] = useState<number>(0) // 计算单个节点的宽度

    // 挂载完毕后创建数据
    useEffect(() => {
        const [year, month, day, week, originalWeek] = getTime()
        // 判断该月的天数
        let tempDayLength: number = getMonthDayNumber(year, month)

        // 计算该月第一天的星期 [0,1,2,3,4,5,6]
        const moveSteps = day % 7 - 1
        // console.log('originalWeek',originalWeek)
        const temp = originalWeek - moveSteps
        let realStartWeek: number
        if (temp >= 0) {
            realStartWeek = temp
        } else {
            realStartWeek = originalWeek + (7 - moveSteps)
        }
        setDay([year, month, day])
        setStartWeek([realStartWeek])
        setDayLength([tempDayLength])
        setChosedDay(day)
        // 创建数据
        setCalendarData(() => {
            return [createCalendarData(tempDayLength, realStartWeek)]
        })
    }, [])

    // 获取容器的宽度来实现背景圆圈的自适应
    useEffect(() => {
        // 此处选择innercontainer是因为如果外部使用flex布局,width就不受控制了
        selectorQueryFields(`.${styles.container}`).then(res => {
            // 计算单个节点的宽度
            setComputedWidth(() => Math.floor((res.width - 6 * columnGap) / 7))
        })
    }, [])

    const weekTitle = useMemo(() => {
        if (lang === 'CN') {
            return ['日', '一', '二', '三', '四', '五', '六']
        } else {
            return ['Sun', 'Mon', 'Tuse', 'Wed', 'Thur', 'Fri', 'Sat']
        }
    }, [])

    const computedCurInfo = (year, month, point: number): number[] => {
        return [
            year + Math.floor((month + point - 1) / 12),
            (month + point) % 12 || 12
        ]
    }

    // 对外暴露的方法
    const toNextMonth = () => {
        const tempPoint = point + 1
        const length = calendarData?.length || 0
        // 计算年与月
        let [year, month] = day
        let [newYear, newMonth] = computedCurInfo(year, month, tempPoint)

        // 下一个月未创建数据
        if (tempPoint === length) {

            const newStartWeek = (startWeek[point] + dayLength[point]) % 7
            const newDayLength = getMonthDayNumber(newYear, newMonth)
            const res = createCalendarData(newDayLength, newStartWeek)
            // 添加新数据
            setCalendarData(pre => {
                pre && pre.push(res)
                return pre
            })
            setPoint(tempPoint) // 修改指针
            setStartWeek(pre => [...pre, newStartWeek])
            setDayLength(pre => [...pre, newDayLength])
        } else {
            setPoint(pre => pre + 1)
        }
        return [newYear,newMonth]
    }

    const toPreMonth = () => {
        if (point > 0) {
            let [year, month] = day
            setPoint(pre => pre - 1)
            return computedCurInfo(year, month, point-1)
        }
    }

    useImperativeHandle(ref, () => {
        return {
            toNextMonth,
            toPreMonth
        }
    })

    return (
        <View className={`${styles.container} ${className}`} style={{ ...containerStyle, backgroundColor }} ref={ref}>
            {headComponent}
            <View className={styles.innercontainer} >
                <HGrid column={7} columnGap={columnGap} rowGap={rowGap}>
                    {/* 星期 */}
                    {
                        weekTitle.map(value => {
                            return (
                                <HGridItem key={value}>
                                    <Text className={styles.weekTitle}>{value}</Text>
                                </HGridItem>
                            )
                        })
                    }
                    {
                        calendarData && calendarData[point].map((value, index) => {
                            const selfDay = index + 1 - startWeek[point]
                            const isPlanDay = planDayArr.indexOf(selfDay) !== -1
                            const oneOfStartWeek = startWeek[point]
                            return (
                                <HGridItem key={index}>
                                    <Text
                                        onClick={() => {
                                            if (selfDay > 0 && selfDay <= dayLength[point]) {
                                                setChosedDay(selfDay)
                                                onClickDay && onClickDay(day[0] + Math.floor((day[1] + point - 1) / 12), (day[1] + point) % 12 === 0 ? 12 : (day[1] + point) % 12, selfDay)
                                            }
                                        }}
                                        className="date"
                                        style={{
                                            boxSizing: 'border-box',
                                            fontWeight: '500',
                                            borderRadius: '50%',
                                            width: `${computedWidth}px`,
                                            height: `${computedWidth}px`,
                                            lineHeight: `${computedWidth}px`,
                                            backgroundColor: value ? (point === 0 && oneOfStartWeek + day[2] - 1 === index ? currentDayColor : (isPlanDay ? `${planDayColor}` : '')) : '',
                                            border: selfDay === chosedDay ? '2px solid' : `2px solid ${backgroundColor}`,
                                            color: isPlanDay ? '#ffffff' : (point === 0 && oneOfStartWeek + day[2] - 1 === index ? '' : '#2d4068')
                                        }}
                                    >{value}</Text>
                                </HGridItem>
                            )
                        })
                    }
                </HGrid>
            </View>
        </View>
    )
})

export default Calendar

index.module.css

.container {
    box-sizing: border-box;
    text-align: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
    margin: 25px auto;
}
.weekTitle{
    color: #81848c;
    font-size: 28px;
}
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐