You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

关键修改点说明:

  1. 箭头按钮逻辑拆分:新增了handlePrevClick和handleNextClick函数,专门处理左右箭头的循环切换,用取模运算确保在列表首尾时能正确切换到另一端的日期;
  2. 循环日期列表生成:重写了findSurroundingDates,用循环+取模的方式生成前后日期,不管当前日期在列表的哪个位置,都能正确拿到循环的前后日期,不会出现列表外的无效日期;
  3. 按钮点击事件区分:修改了箭头按钮的点击事件,让它触发循环切换逻辑,而非直接跳转到某个日期,符合你期望的“单次滚动”需求。

这样修改后,当你点击最右边的箭头按钮,当前日期是列表最后一个时,会自动切换到第一个日期,同时周围的日期列表也会跟着循环更新;点击左边箭头按钮时同理。

备注:内容来源于stack exchange,提问作者Kunkka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:54:35