React antd的datePicker自定义,封装成组件

发布于:2025-03-04 ⋅ 阅读:(18) ⋅ 点赞:(0)
一、antd的datePicker自定义
需求:用户需要为日期选择器的每个日期单元格添加一个Tooltip,当鼠标悬停时显示日期、可兑换流量余额和本公会可兑流量。这些数据需要从接口获取。我需要结合之前的代码,确保Tooltip正确显示,并且数据来自接口。

主要汉化点:

  1. 整个日期选择器面板中文化
  2. 星期显示为中文(周一 到 周日)
  3. 月份显示为中文格式
  4. 操作按钮汉化("确定"、"现在" 等)
  5. 日期格式统一使用中文年月日
  6. 加载提示中文化
  7. Tooltip内容中文化

效果包含:

  • 月份显示为 "2024年5月"
  • 星期列显示为 "一、二、三、四、五、六、日"
  • 今天按钮显示为 "今天"
  • 确定按钮显示为 "确定"
  • 十年范围显示为 "2020-2029"
  • 时间列显示为 "时","分","秒"
  • index.tsx文件
import React, { useState, useEffect } from 'react';
import { DatePicker, Tooltip, Spin, ConfigProvider } from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import './custom-datepicker.css';
import 'dayjs/locale/zh-cn';
import zhCN from 'antd/locale/zh_CN';
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.locale('zh-cn'); // 设置dayjs为中文

interface TrafficData {
    date: string;
    personal: string;
    guild: string;
}

const App: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
  const [trafficData, setTrafficData] = useState<Record<string, TrafficData>>({});
  const [loading, setLoading] = useState(true);

  const today = dayjs().startOf('day');
  const sevenDaysLater = today.add(6, 'day');

  useEffect(() => {
    const mockApi = async () => {
      const data: Record<string, TrafficData> = {};
      Array.from({ length: 7 }).forEach((_, i) => {
        const date = today.add(i, 'day').format('YYYY-MM-DD');
        data[date] = {
          date,
          personal: `${[0, 20, 30, 40, 80, 100, 50][i]}%`,
          guild: `${Math.floor(Math.random() * 100000).toLocaleString()}`,
        };
      });

      await new Promise(resolve => setTimeout(resolve, 500));
      setTrafficData(data);
      setLoading(false);
    };

    mockApi();
  }, []);

  const disabledDate = (current: Dayjs) => current.isBefore(today, 'day') || current.isAfter(sevenDaysLater, 'day');

  if (loading) return <Spin tip="数据加载中..." />;

  return (
    <ConfigProvider locale={zhCN}> {/* 设置Ant Design为中文 */}
      <DatePicker
        value={selectedDate}
        disabledDate={disabledDate}
        onChange={setSelectedDate}
        dropdownClassName="custom-picker-dropdown"
        dateRender={current => {
          const dateStr = current.format('YYYY-MM-DD');
          const data = trafficData[dateStr];
          const isInRange = current.isSameOrAfter(today) && current.isSameOrBefore(sevenDaysLater);
          const isSelected = selectedDate?.isSame(current, 'day');

          return (
            <div className="custom-cell-wrapper">
              <div className="native-cell-content">
                {current.date()}
              </div>
              <Tooltip
                title={data ?
                  `${dayjs(dateStr).format('YYYY年M月D日')}\n可兑流量余额: ${data.personal}\n本公会可兑流量: ${data.guild}`
                  : '无可用数据'}
                overlayStyle={{
                  whiteSpace: 'pre-line',
                  pointerEvents: 'none',
                }}
                placement="bottom"
                mouseEnterDelay={0}
                mouseLeaveDelay={0.1}
                trigger={['hover']}
                getPopupContainer={trigger => trigger.parentElement!}
              >
                <div className={`custom-cell ${isInRange ? 'recent-date' : ''}`}>
                  <div className={`date-number ${isSelected ? 'selected' : ''}`}>
                    {current.date()}
                  </div>
                  {data && (
                    <div className={`availability ${data.personal === '0%' ? 'empty' : ''}`}>
                                            余{data.personal}
                    </div>
                  )}
                </div>
              </Tooltip>
            </div>
          );
        }}
      />
    </ConfigProvider>
  );
};

export default App;
  • custom-datepicker.css文件
/* custom-datepicker.css */
.custom-picker-dropdown {
    z-index: 1001;
}

.custom-cell-wrapper {
    position: relative;
    height: 100%;
    width: 100%;
}

.native-cell-content {
    visibility: hidden;
}

.custom-cell {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 2;
    padding: 3px 0;
}

.date-number {
    width: 24px;
    height: 24px;
    line-height: 24px;
    text-align: center;
    color: #000;
    transition: all 0.2s;
    border-radius: 50%;
}

.date-number.selected {
    background: #1890ff;
    color: white !important;
}

.recent-date:hover .date-number:not(.selected) {
    color: #1890ff;
}

.availability {
    font-size: 10px;
    line-height: 14px;
    color: #1890ff;
    margin-top: 2px;
}

.availability.empty {
    color: #ff4d4f !important;
}

.ant-picker-cell-inner {
    padding: 0 !important;
    height: 100% !important;
}

.ant-picker-cell:hover .ant-picker-cell-inner {
    background: transparent !important;
}

/* 添加中文面板样式调整 */
.ant-picker-date-panel {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}

.ant-picker-header-view button {
    font-weight: 500;
}

.ant-picker-cell-inner::before {
    border-radius: 50% !important;
}

二、封装成组件

  • DateSelector.tsx文件
// DateSelector.tsx
import React from 'react';
import {DatePicker, Tooltip, Spin, ConfigProvider} from 'antd';
import dayjs, { Dayjs } from 'dayjs';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import 'dayjs/locale/zh-cn';
import './custom-datepicker.css';
import zhCN from 'antd/locale/zh_CN';

dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);

export interface TrafficData {
    personal: string;
    guild: string;
}

interface DateSelectorProps {
    value?: Dayjs | null;
    trafficData: Record<string, TrafficData>;
    onChange?: (date: Dayjs | null) => void;
    loading?: boolean;
}

const DateSelector: React.FC<DateSelectorProps> = ({
  value,
  trafficData,
  onChange,
  loading = false,
}) => {
  const today = dayjs().startOf('day');
  const sevenDaysLater = today.add(6, 'day');

  const disabledDate = (current: Dayjs) => current.isBefore(today, 'day') || current.isAfter(sevenDaysLater, 'day');

  const handleChange = (date: Dayjs | null) => {
    onChange?.(date);
  };

  if (loading) {
    return <Spin tip="数据加载中..." style={{ padding: '8px 0' }} />;
  }

  return (
    <ConfigProvider locale={zhCN}> {/* 设置Ant Design为中文 */}
      <DatePicker
        value={value}
        disabledDate={disabledDate}
        onChange={handleChange}
        dropdownClassName="custom-picker-dropdown"
        dateRender={current => {
          const dateStr = current.format('YYYY-MM-DD');
          const data = trafficData[dateStr];
          const isInRange = current.isSameOrAfter(today) && current.isSameOrBefore(sevenDaysLater);
          const isSelected = value?.isSame(current, 'day');

          return (
            <div className="custom-cell-wrapper">
              <div className="native-cell-content">{current.date()}</div>
              <Tooltip
                title={data ?
                  `${dayjs(dateStr).format('YYYY年M月D日')}\n可兑流量余额: ${data.personal}\n本公会可兑流量: ${data.guild}`
                  : '无可用数据'}
                overlayStyle={{
                  whiteSpace: 'pre-line',
                  pointerEvents: 'none',
                }}
                placement="bottom"
                mouseEnterDelay={0}
                mouseLeaveDelay={0.1}
              >
                <div className={`custom-cell ${isInRange ? 'recent-date' : ''}`}>
                  <div className={`date-number ${isSelected ? 'selected' : ''}`}>
                    {current.date()}
                  </div>
                  {data && (
                    <div className={`availability ${data.personal === '0%' ? 'empty' : ''}`}>
                                            余{data.personal}
                    </div>
                  )}
                </div>
              </Tooltip>
            </div>
          );
        }}
      />
    </ConfigProvider>
  );
};

export default DateSelector;
  • custom-datepicker.css
/* custom-datepicker.css */
.custom-picker-dropdown {
    z-index: 1001;
}

.custom-cell-wrapper {
    position: relative;
    height: 100%;
    width: 100%;
}

.native-cell-content {
    visibility: hidden;
}

.custom-cell {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 2;
    padding: 3px 0;
}

.date-number {
    width: 24px;
    height: 24px;
    line-height: 24px;
    text-align: center;
    color: #000;
    transition: all 0.2s;
    border-radius: 50%;
}

.date-number.selected {
    background: #1890ff;
    color: white !important;
}

.recent-date:hover .date-number:not(.selected) {
    color: #1890ff;
}

.availability {
    font-size: 10px;
    line-height: 14px;
    color: #1890ff;
    margin-top: 2px;
}

.availability.empty {
    color: #ff4d4f !important;
}

.ant-picker-cell-inner {
    padding: 0 !important;
    height: 100% !important;
}

.ant-picker-cell:hover .ant-picker-cell-inner {
    background: transparent !important;
}

/* 添加中文面板样式调整 */
.ant-picker-date-panel {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}

.ant-picker-header-view button {
    font-weight: 500;
}

.ant-picker-cell-inner::before {
    border-radius: 50% !important;
}
  • index.tsx文件
// 使用示例 ParentComponent.tsx
import React, { useState, useEffect } from 'react';
import DateSelector, { TrafficData } from './DateSelector';
import dayjs from 'dayjs';

const ParentComponent: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
  const [trafficData, setTrafficData] = useState<Record<string, TrafficData>>({});
  const [loading, setLoading] = useState(true);

  const today = dayjs().startOf('day');

  // useEffect(() => {
  //   // 模拟API调用
  //   const mockFetchData = async () => {
  //     const mockData = {
  //       [dayjs().format('YYYY-MM-DD')]: {
  //         personal: '80%',
  //         guild: '100,000',
  //       },
  //       [dayjs().add(1, 'day')
  //         .format('YYYY-MM-DD')]: {
  //         personal: '50%',
  //         guild: '75,000',
  //       },
  //     };
  //
  //     await new Promise(resolve => setTimeout(resolve, 800));
  //     setTrafficData(mockData);
  //     setLoading(false);
  //   };
  //
  //   mockFetchData();
  // }, []);

  useEffect(() => {
    const mockApi = async () => {
      const data: Record<string, TrafficData> = {};
      Array.from({ length: 7 }).forEach((_, i) => {
        const date = today.add(i, 'day').format('YYYY-MM-DD');
        data[date] = {
          personal: `${[0, 20, 30, 40, 80, 100, 50][i]}%`,
          guild: `${Math.floor(Math.random() * 100000).toLocaleString()}`,
        };
      });

      await new Promise(resolve => setTimeout(resolve, 500));
      setTrafficData(data);
      setLoading(false);
    };

    mockApi();
  }, []);

  return (
    <div style={{ padding: 24 }}>
      <h2>日期选择器示例</h2>
      <div style={{ marginBottom: 16 }}>
                当前选择:{selectedDate?.format('YYYY年M月D日') || '未选择'}
      </div>
      <DateSelector
        value={selectedDate}
        trafficData={trafficData}
        onChange={setSelectedDate}
        loading={loading}
      />
    </div>
  );
};

export default ParentComponent;

三、生效时间选择完日期后,还需填入具体时间(或提供一个时间选择器),精确到分,默认为00:00;如选择的日期为今天,则填写的时间不能早于当前时间

// index.tsx
import React, { useState, useEffect } from 'react';
import { Row, Col, TimePicker, Spin, ConfigProvider } from 'antd';
import DateSelector, { TrafficData } from './DateSelector';
import dayjs, { Dayjs } from 'dayjs';
import zhCN from 'antd/locale/zh_CN';

declare module 'dayjs' {
  interface Dayjs {
    isToday(): boolean;
  }
}

dayjs.extend((o, c) => {
  c.prototype.isToday = function () {
    return this.isSame(dayjs(), 'day');
  }
});

const DateTimePicker: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
  const [selectedTime, setSelectedTime] = useState<Dayjs>(dayjs().startOf('minute'));
  const [trafficData, setTrafficData] = useState<Record<string, TrafficData>>({});
  const [loading, setLoading] = useState(true);
  const today = dayjs().startOf('day');

  const disabledTime = (current: Dayjs | null) => {
    if (!current || !selectedDate?.isToday()) {
      return { disabledHours: () => [], disabledMinutes: () => [] };
    }

    const now = dayjs();
    return {
      disabledHours: () => {
        const currentHour = now.hour();
        return Array.from({ length: currentHour }, (_, i) => i);
      },
      disabledMinutes: (selectedHour: number) => {
        if (selectedHour < now.hour()) return [];
        return Array.from({ length: now.minute() }, (_, i) => i);
      },
    };
  };

  const handleDateChange = (date: Dayjs | null) => {
    setSelectedDate(date);
    setSelectedTime(date?.isToday() ? dayjs().startOf('minute') : dayjs().startOf('day'));
  };

  // useEffect(() => {
  //   // 模拟API请求
  //   const mockData = {
  //     [dayjs().format('YYYY-MM-DD')]: { personal: '80%', guild: '100,000' },
  //     [dayjs().add(1, 'day')
  //       .format('YYYY-MM-DD')]: { personal: '50%', guild: '75,000' },
  //   };
  //
  //   setTimeout(() => {
  //     setTrafficData(mockData);
  //     setLoading(false);
  //   }, 800);
  // }, []);

  useEffect(() => {
    const mockApi = async () => {
      const data: Record<string, TrafficData> = {};
      Array.from({ length: 7 }).forEach((_, i) => {
        const date = today.add(i, 'day').format('YYYY-MM-DD');
        data[date] = {
          personal: `${[0, 20, 30, 40, 80, 100, 50][i]}%`,
          guild: `${Math.floor(Math.random() * 100000).toLocaleString()}`,
        };
      });

      await new Promise(resolve => setTimeout(resolve, 500));
      setTrafficData(data);
      setLoading(false);
    };

    mockApi();
  }, []);

  return (
    <ConfigProvider locale={zhCN}> {/* 设置Ant Design为中文 */}
      <div style={{ padding: 24, maxWidth: 380, margin: '0 auto' }}>
        <h2 style={{ marginBottom: 24 }}>预约时间选择</h2>

        <Row gutter={24} align="middle">
          <Col span={12}>
            {/*<div style={{ marginBottom: 8 }}>选择日期</div>*/}
            <DateSelector
              value={selectedDate}
              trafficData={trafficData}
              onChange={handleDateChange}
              loading={loading}
            />
          </Col>

          <Col span={12}>
            {/*<div style={{ marginBottom: 8 }}>选择时间</div>*/}
            <TimePicker
              value={selectedTime}
              format="HH:mm"
              minuteStep={1}
              disabledTime={disabledTime}
              onChange={time => setSelectedTime(time || dayjs().startOf('minute'))}
              placeholder="请选择时间"
              disabled={!selectedDate}
              allowClear={false}
              showNow={false}
              style={{ width: '100%' }}
            />
          </Col>
        </Row>

        <div style={{ marginTop: 24, padding: 16, background: '#f5f5f5', borderRadius: 4 }}>
          已选择时间: {selectedDate ?
            `${selectedDate.format('YYYY年MM月DD日')} ${selectedTime.format('HH:mm')}`
            : '请先选择日期'}
        </div>
      </div>
    </ConfigProvider>
  );
};

export default DateTimePicker;


网站公告

今日签到

点亮在社区的每一天
去签到