一、antd的datePicker自定义
需求:用户需要为日期选择器的每个日期单元格添加一个Tooltip,当鼠标悬停时显示日期、可兑换流量余额和本公会可兑流量。这些数据需要从接口获取。我需要结合之前的代码,确保Tooltip正确显示,并且数据来自接口。
主要汉化点:
- 整个日期选择器面板中文化
- 星期显示为中文(周一 到 周日)
- 月份显示为中文格式
- 操作按钮汉化("确定"、"现在" 等)
- 日期格式统一使用中文年月日
- 加载提示中文化
- 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;