React+Ant Design日期选择器:实现前后按钮单次滚动日期及循环切换功能
React+Ant Design日期选择器:实现前后按钮单次滚动日期及循环切换功能
嗨,我看了你遇到的循环日期切换问题,咱们先理清楚核心需求:点击左右的箭头按钮时,当前日期要在给定的enabledDates列表里循环切换,同时周围的日期列表也要跟着循环更新,而不是跑到列表外的日期。
先分析下你现有代码的问题:
- 左右箭头按钮和普通日期按钮共用了同一个点击逻辑,但箭头按钮应该是切换到上一个/下一个日期,而不是直接跳转到某个固定日期;
findSurroundingDates里的循环补全逻辑太绕,没有清晰处理当前日期在列表首尾时的循环取数,导致出现列表外的日期。
接下来我给你修改后的完整代码,重点调整了箭头按钮的点击逻辑和日期列表的循环生成逻辑:
import { DatePicker, Button } from 'antd'; import './ButtonedAntDDatePicker.css'; import dayjs from 'dayjs'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import { useState, useEffect } from 'react'; dayjs.extend(customParseFormat); function ButtonedAntDDatePicker({ selectedDate, disabledDate, isDatePickerDisabled, enabledDates, setSelectedDate }) { const [isAnimating, setIsAnimating] = useState(false); const [prevButtonsStyle, setPrevButtonsStyle] = useState({}); const [nextButtonsStyle, setNextButtonsStyle] = useState({}); const [datePickerInput, setDatePickerInput] = useState(null); // handle date picker input field useEffect(() => { const input = document.querySelector('.date-picker-container .ant-picker-input input'); if (input) { setDatePickerInput(input); // If selectedDate is already set (from URL), update the input field if (selectedDate) { input.value = selectedDate.format('DD/MM/YYYY'); } const handleInputEvent = (e) => { const inputValue = e.target.value; const formats = ['DD/MM/YYYY', 'D/M/YYYY', 'DD/M/YYYY', 'D/MM/YYYY']; for (const format of formats) { const parsedDate = dayjs(inputValue, format, true); if (parsedDate.isValid()) { setSelectedDate(parsedDate); e.target.value = parsedDate.format('DD/MM/YYYY'); break; } } }; input.addEventListener('blur', handleInputEvent); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { handleInputEvent(e); } }); return () => { input.removeEventListener('blur', handleInputEvent); input.removeEventListener('keypress', handleInputEvent); }; } }, [setSelectedDate, selectedDate]); const findSurroundingDates = (currentDate, count) => { if (enabledDates.length === 0 || !currentDate) return { prevDates: [], nextDates: [] }; const sortedDates = [...enabledDates].sort((a, b) => dayjs(a).diff(dayjs(b))); const dateCount = sortedDates.length; const currentIndex = sortedDates.findIndex(date => dayjs(date).isSame(currentDate, 'day') ); // 如果当前日期不在列表里,取第一个大于它的日期的索引,否则取当前索引 const effectiveIndex = currentIndex === -1 ? sortedDates.findIndex(date => dayjs(date).isAfter(currentDate)) : currentIndex; // 循环生成前count个日期:用取模处理首尾循环 const prevDates = []; for (let i = 1; i <= count; i++) { const idx = (effectiveIndex - i + dateCount) % dateCount; prevDates.unshift(dayjs(sortedDates[idx])); } // 循环生成后count个日期:用取模处理首尾循环 const nextDates = []; for (let i = 1; i <= count; i++) { const idx = (effectiveIndex + i) % dateCount; nextDates.push(dayjs(sortedDates[idx])); } return { prevDates, nextDates }; }; // 你要的前后各3个日期,所以count传3 const { prevDates, nextDates } = findSurroundingDates(selectedDate, 3); const handleDatePickerChange = (date) => { if (!date) return; setSelectedDate(date); }; const handleDateButtonClick = (date) => { if (isAnimating || !date) return; setIsAnimating(true); const isGoingLeft = date.isAfter(selectedDate); if (isGoingLeft) { setPrevButtonsStyle({ animation: 'moveLeftOut 0.15s cubic-bezier(0.33, 1.0, 0.68, 1.0) forwards' }); setNextButtonsStyle({ animation: 'moveLeftIn 0.15s cubic-bezier(0.22, 1.0, 0.36, 1.0) forwards' }); } else { setPrevButtonsStyle({ animation: 'moveRightIn 0.15s cubic-bezier(0.22, 1.0, 0.36, 1.0) forwards' }); setNextButtonsStyle({ animation: 'moveRightOut 0.15s cubic-bezier(0.33, 1.0, 0.68, 1.0) forwards' }); } setTimeout(() => { setSelectedDate(date); setIsAnimating(false); setPrevButtonsStyle({}); setNextButtonsStyle({}); }, 200); }; // 箭头按钮:循环切换到上一个日期 const handlePrevClick = () => { if (isAnimating || enabledDates.length === 0) return; setIsAnimating(true); const sortedDates = [...enabledDates].sort((a, b) => dayjs(a).diff(dayjs(b))); const currentIndex = sortedDates.findIndex(date => dayjs(date).isSame(selectedDate, 'day')); // 循环取上一个:如果是第一个,就取最后一个,否则取前一个 const nextIndex = currentIndex === 0 ? sortedDates.length - 1 : currentIndex - 1; const nextDate = dayjs(sortedDates[nextIndex]); setPrevButtonsStyle({ animation: 'moveRightIn 0.15s cubic-bezier(0.22, 1.0, 0.36, 1.0) forwards' }); setNextButtonsStyle({ animation: 'moveRightOut 0.15s cubic-bezier(0.33, 1.0, 0.68, 1.0) forwards' }); setTimeout(() => { setSelectedDate(nextDate); setIsAnimating(false); setPrevButtonsStyle({}); setNextButtonsStyle({}); }, 200); }; // 箭头按钮:循环切换到下一个日期 const handleNextClick = () => { if (isAnimating || enabledDates.length === 0) return; setIsAnimating(true); const sortedDates = [...enabledDates].sort((a, b) => dayjs(a).diff(dayjs(b))); const currentIndex = sortedDates.findIndex(date => dayjs(date).isSame(selectedDate, 'day')); // 循环取下一个:如果是最后一个,就取第一个,否则取后一个 const nextIndex = currentIndex === sortedDates.length - 1 ? 0 : currentIndex + 1; const nextDate = dayjs(sortedDates[nextIndex]); setPrevButtonsStyle({ animation: 'moveLeftOut 0.15s cubic-bezier(0.33, 1.0, 0.68, 1.0) forwards' }); setNextButtonsStyle({ animation: 'moveLeftIn 0.15s cubic-bezier(0.22, 1.0, 0.36, 1.0) forwards' }); setTimeout(() => { setSelectedDate(nextDate); setIsAnimating(false); setPrevButtonsStyle({}); setNextButtonsStyle({}); }, 200); }; const formatButtonDate = (date) => { return date.format('DD/MM/YYYY'); }; const renderPrevButtons = () => { if (!prevDates.length) return null; return prevDates.map((date, index) => { return ( <div key={`prev-${index}`} style={{ position: 'relative' }} > <Button onClick={() => index === 0 ? handlePrevClick() : handleDateButtonClick(date)} className={`date-button prev-date ${index === 0 ? 'first-button' : ''}`} > {index === 0 ? <i className="fa-solid fa-caret-left"></i> : formatButtonDate(date)} </Button> </div> ); }); }; const renderNextButtons = () => { if (!nextDates.length) return null; return nextDates.map((date, index) => { return ( <div key={`next-${index}`} style={{ position: 'relative' }} > <Button onClick={() => index === nextDates.length - 1 ? handleNextClick() : handleDateButtonClick(date)} className={`date-button next-date ${index === nextDates.length - 1 ? 'last-button' : ''}`} > {index === nextDates.length - 1 ? <i className="fa-solid fa-caret-right"></i> : formatButtonDate(date)} </Button> </div> ); }); }; if (isDatePickerDisabled) { return null; } return ( <div className="date-picker-container"> <div className="date-buttons-container prev-dates" style={prevButtonsStyle} > {renderPrevButtons()} </div> <DatePicker value={selectedDate} style={{ zIndex: 9999, margin: '0px' }} format={'DD/MM/YYYY'} onChange={handleDatePickerChange} disabledDate={disabledDate} allowClear={true} disabled={isDatePickerDisabled} inputReadOnly={false} /> <div className="date-buttons-container next-dates" style={nextButtonsStyle} > {renderNextButtons()} </div> </div> ); } export default ButtonedAntDDatePicker;
使用代码保持不变:
<div className='datepickers'> <ButtonedAntDDatePicker selectedDate={startDate} disabledDate={disabledDate} isDatePickerDisabled={isDatePickerDisabled} enabledDates={enabledDates} setSelectedDate={setStartDate} /> </div>
关键修改点说明:
- 箭头按钮逻辑拆分:新增了
handlePrevClick和handleNextClick函数,专门处理左右箭头的循环切换,用取模运算确保在列表首尾时能正确切换到另一端的日期; - 循环日期列表生成:重写了
findSurroundingDates,用循环+取模的方式生成前后日期,不管当前日期在列表的哪个位置,都能正确拿到循环的前后日期,不会出现列表外的无效日期; - 按钮点击事件区分:修改了箭头按钮的点击事件,让它触发循环切换逻辑,而非直接跳转到某个日期,符合你期望的“单次滚动”需求。
这样修改后,当你点击最右边的箭头按钮,当前日期是列表最后一个时,会自动切换到第一个日期,同时周围的日期列表也会跟着循环更新;点击左边箭头按钮时同理。
备注:内容来源于stack exchange,提问作者Kunkka
相关产品推荐
相关产品推荐

