118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import React, { useMemo, useState } from 'react'
|
|
import { View, Text, Image } from '@tarojs/components'
|
|
import styles from './index.module.scss'
|
|
import images from '@/config/images'
|
|
interface CalendarCardProps {
|
|
value?: Date
|
|
minDate?: Date
|
|
maxDate?: Date
|
|
onChange?: (date: Date) => void
|
|
onNext?: (date: Date) => void
|
|
onHeaderClick?: (date: Date) => void
|
|
}
|
|
|
|
const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)
|
|
const endOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth() + 1, 0)
|
|
const addMonths = (date: Date, delta: number) => new Date(date.getFullYear(), date.getMonth() + delta, 1)
|
|
|
|
const formatHeader = (date: Date) => `${date.getMonth() + 1}月 ${date.getFullYear()}`
|
|
|
|
const CalendarCard: React.FC<CalendarCardProps> = ({
|
|
value,
|
|
minDate,
|
|
maxDate,
|
|
onChange,
|
|
onHeaderClick
|
|
}) => {
|
|
const today = new Date()
|
|
const [current, setCurrent] = useState<Date>(value || startOfMonth(today))
|
|
const [selected, setSelected] = useState<Date>(value || today)
|
|
|
|
|
|
const firstDay = useMemo(() => startOfMonth(current), [current])
|
|
const lastDay = useMemo(() => endOfMonth(current), [current])
|
|
|
|
const days = useMemo(() => {
|
|
const startWeekday = firstDay.getDay() // 0 周日
|
|
const prevPadding = startWeekday // 周日为第一列
|
|
const total = prevPadding + lastDay.getDate()
|
|
const rows = Math.ceil(total / 7)
|
|
const grid: (Date | null)[] = []
|
|
for (let i = 0; i < rows * 7; i++) {
|
|
const day = i - prevPadding + 1
|
|
if (day < 1 || day > lastDay.getDate()) {
|
|
grid.push(null)
|
|
} else {
|
|
grid.push(new Date(current.getFullYear(), current.getMonth(), day))
|
|
}
|
|
}
|
|
return grid
|
|
}, [firstDay, lastDay, current])
|
|
|
|
const isDisabled = (d: Date) => {
|
|
if (minDate && d < minDate) return true
|
|
if (maxDate && d > maxDate) return true
|
|
return false
|
|
}
|
|
|
|
const gotoMonth = (delta: number) => setCurrent(prev => addMonths(prev, delta))
|
|
|
|
const handleHeaderClick = () => {
|
|
onHeaderClick && onHeaderClick(current)
|
|
}
|
|
|
|
const handleSelectDay = (d: Date | null) => {
|
|
if (!d || isDisabled(d)) return
|
|
setSelected(d)
|
|
onChange && onChange(d)
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
<View className={styles['calendar-card']}>
|
|
<View className={styles['header']}>
|
|
<View className={styles['header-left']} onClick={handleHeaderClick}>
|
|
<Text className={styles['header-text']}>{formatHeader(current)}</Text>
|
|
<Image src={images.ICON_RIGHT_MAX} className={`${styles['month-arrow']}}`} onClick={() => gotoMonth(1)} />
|
|
</View>
|
|
<View className={styles['header-actions']}>
|
|
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']} ${styles['left']}`} onClick={() => gotoMonth(-1)} />
|
|
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']}}`} onClick={() => gotoMonth(1)} />
|
|
</View>
|
|
</View>
|
|
|
|
<View className={styles['week-row']}>
|
|
{['周日','周一','周二','周三','周四','周五','周六'].map((w) => (
|
|
<Text key={w} className={styles['week-item']}>{w}</Text>
|
|
))}
|
|
</View>
|
|
|
|
<View className={styles['grid']}>
|
|
{days.map((d, idx) => {
|
|
const isSelected = !!(d && selected && d.toDateString() === new Date(selected.getFullYear(), selected.getMonth(), selected.getDate()).toDateString())
|
|
return (
|
|
<View
|
|
key={idx}
|
|
className={`${styles['cell']} ${!d ? styles['empty'] : ''} ${d && isDisabled(d) ? styles['disabled'] : ''} `}
|
|
onClick={() => handleSelectDay(d)}
|
|
>
|
|
{d ? <Text className={`${styles['cell-text']} ${isSelected ? styles['selected'] : ''}`}>{d.getDate()}</Text> : null}
|
|
</View>
|
|
)
|
|
})}
|
|
</View>
|
|
|
|
|
|
|
|
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export default CalendarCard
|