修改日历组件

This commit is contained in:
筱野
2025-09-07 20:26:32 +08:00
parent f503bf53ac
commit 549f704c53
22 changed files with 1057 additions and 563 deletions

View File

@@ -1,117 +0,0 @@
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

View File

@@ -1,130 +0,0 @@
import React, { useState, useEffect } from 'react'
import CommonPopup from '@/components/CommonPopup'
import CalendarCard from './CalendarCard'
import DateTimePicker from '@/components/DateTimePicker'
import HourMinutePicker from '@/components/HourMinutePicker'
export interface DialogCalendarCardProps {
value?: Date
minDate?: Date
maxDate?: Date
onChange?: (date: Date) => void
onNext?: (date: Date) => void
visible: boolean
onClose: () => void
title?: React.ReactNode
}
const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
visible,
onClose,
title,
value,
minDate,
maxDate,
onChange,
onNext
}) => {
const [selected, setSelected] = useState<Date>(value || new Date())
const [type, setType] = useState<'year' | 'month' | 'time'>('year');
const [selectedHour, setSelectedHour] = useState(8)
const [selectedMinute, setSelectedMinute] = useState(0)
const handleConfirm = () => {
if (type === 'year') {
// 年份选择完成后,进入月份选择
setType('time')
} else if (type === 'month') {
// 月份选择完成后,进入时间选择
setType('year')
} else if (type === 'time') {
// 时间选择完成后调用onNext回调
const finalDate = new Date(selected.getFullYear(), selected.getMonth(), selected.getDate(), selectedHour, selectedMinute)
console.log('finalDate', finalDate)
if (onChange) onChange(finalDate)
onClose()
}
}
const handleChange = (d: Date) => {
console.log('handleChange', d)
setSelected(d)
// if (onChange) onChange(d)
}
const onHeaderClick = (date: Date) => {
console.log('onHeaderClick', date)
setSelected(date)
setType('month')
}
const getConfirmText = () => {
if (type === 'time' || type === 'month') return '完成'
return '下一步'
}
const handleDateTimePickerChange = (year: number, month: number) => {
console.log('year', year)
console.log('month', month)
setSelected(new Date(year, month - 1, 1))
}
const dialogClose = () => {
if (type === 'month') {
setType('year')
} else if (type === 'time') {
setType('year')
} else {
onClose()
}
}
useEffect(() => {
setSelected(value || new Date())
if (visible) {
setType('year')
setSelectedHour(8)
setSelectedMinute(0)
}
}, [value, visible])
return (
<CommonPopup
visible={visible}
onClose={dialogClose}
showHeader={!!title}
title={title}
hideFooter={false}
cancelText='取消'
confirmText={getConfirmText()}
onConfirm={handleConfirm}
position='bottom'
round
zIndex={1000}
>
{
type === 'year' && <CalendarCard
value={selected}
minDate={minDate}
maxDate={maxDate}
onChange={handleChange}
onHeaderClick={onHeaderClick}
/>
}
{
type === 'month' && <DateTimePicker
onChange={handleDateTimePickerChange}
defaultYear={selected.getFullYear()}
defaultMonth={selected.getMonth() + 1}
/>
}
{
type === 'time' && <HourMinutePicker
onChange={(hour, minute) => {
setSelectedHour(hour)
setSelectedMinute(minute)
}}
defaultHour={selectedHour}
defaultMinute={selectedMinute}
/>
}
</CommonPopup>
)
}
export default DialogCalendarCard

View File

@@ -1,105 +0,0 @@
.calendar-card {
background: #fff;
border-radius: 16px;
padding: 12px 12px 8px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 16px 11px 16px;
height: 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 6px;
}
.header-text {
font-size: 17px;
font-weight: 600;
color: #000;
}
.header-actions {
display: flex;
gap: 30px;
}
.month-arrow{
width: 8px
}
.arrow {
width: 10px;
position: relative;
}
.arrow.left {
left: 9px;
transform: rotate(-180deg);
}
.week-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0 0 4px 0;
}
.week-item {
text-align: center;
color: rgba(60, 60, 67, 0.30);
font-size: 13px;
}
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px 0;
padding: 4px 0 16px;
}
.cell {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.cell.empty {
opacity: 0;
}
.cell.disabled {
color: rgba(0,0,0,0.2);
}
.cell-text.selected {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.footer {
display: flex;
gap: 12px;
}
.btn {
flex: 1;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.06);
display: flex;
align-items: center;
justify-content: center;
}
.btn.primary {
background: #000;
color: #fff;
}
.hm-placeholder {
height: 240px;
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,2 +0,0 @@
export { default } from './CalendarCard'
export { default as DialogCalendarCard } from './DialogCalendarCard'