Files
mini-programs/src/components/CalendarCard/CalendarCard.tsx
2025-08-30 22:25:39 +08:00

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