11import React , { useEffect , useState , useContext , useMemo , forwardRef } from 'react' ;
2- import { CloseIcon } from 'tdesign-icons-react' ;
2+ import {
3+ CloseIcon ,
4+ ChevronLeftIcon ,
5+ ChevronRightIcon ,
6+ ChevronLeftDoubleIcon ,
7+ ChevronRightDoubleIcon ,
8+ } from 'tdesign-icons-react' ;
9+ import classNames from 'classnames' ;
10+
311import parseTNode from '../_util/parseTNode' ;
412import { Button , ButtonProps } from '../button' ;
513import { TDateType , TCalendarValue } from './type' ;
@@ -8,6 +16,12 @@ import useDefaultProps from '../hooks/useDefaultProps';
816import { calendarDefaultProps } from './defaultProps' ;
917import { CalendarContext , CalendarProps } from './Calendar' ;
1018import { useLocaleReceiver } from '../locale/LocalReceiver' ;
19+ import { getPrevMonth , getPrevYear , getNextMonth , getNextYear } from './utils' ;
20+
21+ const getCurrentYearAndMonth = ( v : Date ) => {
22+ const date = new Date ( v ) ;
23+ return { year : date . getFullYear ( ) , month : date . getMonth ( ) } ;
24+ } ;
1125
1226const CalendarTemplate = forwardRef < HTMLDivElement , CalendarProps > ( ( _props , ref ) => {
1327 const calendarClass = usePrefixClass ( 'calendar' ) ;
@@ -16,6 +30,20 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
1630 const props = useDefaultProps ( context ? context . inject ( _props ) : _props , calendarDefaultProps ) ;
1731
1832 const [ selectedDate , setSelectedDate ] = useState < number | Date | TCalendarValue [ ] > ( ) ;
33+ const [ currentMonth , setCurrentMonth ] = useState <
34+ Array < {
35+ year : number ;
36+ month : number ;
37+ months : TDateType [ ] ;
38+ weekdayOfFirstDay : number ;
39+ } >
40+ > ( [ ] ) ;
41+ const [ headerButtons , setHeaderButtons ] = useState ( {
42+ preYearBtnDisable : false ,
43+ prevMonthBtnDisable : false ,
44+ nextYearBtnDisable : false ,
45+ nextMonthBtnDisable : false ,
46+ } ) ;
1947 const firstDayOfWeek = props . firstDayOfWeek || 0 ;
2048
2149 useEffect ( ( ) => {
@@ -87,41 +115,16 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
87115 return className ;
88116 } ;
89117
90- // 选择日期
91- const handleSelect = ( year , month , date , dateItem ) => {
92- if ( dateItem . type === 'disabled' ) return ;
93- const selected = new Date ( year , month , date ) ;
94- let newSelected : TCalendarValue | TCalendarValue [ ] ;
95- if ( props . type === 'range' && Array . isArray ( selectedDate ) ) {
96- if ( selectedDate . length === 1 ) {
97- if ( selectedDate [ 0 ] > selected ) {
98- newSelected = [ selected ] ;
99- } else {
100- newSelected = [ selectedDate [ 0 ] , selected ] ;
101- }
102- } else {
103- newSelected = [ selected ] ;
104- if ( ! confirmBtn && selectedDate . length === 2 ) {
105- props . onChange ?.( new Date ( selectedDate [ 0 ] ) ) ;
106- }
107- }
108- } else if ( props . type === 'multiple' ) {
109- const newVal = [ ...( Array . isArray ( selectedDate ) ? selectedDate : [ selectedDate ] ) ] ;
110- const index = newVal . findIndex ( ( item ) => isSameDate ( item , selected ) ) ;
111- if ( index > - 1 ) {
112- newVal . splice ( index , 1 ) ;
113- } else {
114- newVal . push ( selected ) ;
115- }
116- newSelected = newVal ;
117- } else {
118- newSelected = selected ;
119- if ( ! confirmBtn ) {
120- props . onChange ?.( selected ) ;
121- }
118+ const getCurrentDate = ( ) => {
119+ let time = Array . isArray ( selectedDate ) ? selectedDate [ 0 ] : selectedDate ;
120+
121+ if ( currentMonth ?. length > 0 ) {
122+ const year = currentMonth [ 0 ] ?. year ;
123+ const month = currentMonth [ 0 ] ?. month ;
124+ time = new Date ( year , month , 1 ) . getTime ( ) ;
122125 }
123- setSelectedDate ( newSelected ) ;
124- props . onSelect ?. ( newSelected as any ) ;
126+
127+ return time ;
125128 } ;
126129
127130 // 计算月份
@@ -204,6 +207,82 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
204207 // eslint-disable-next-line react-hooks/exhaustive-deps
205208 } , [ selectedDate ] ) ;
206209
210+ const updateActionButton = ( value : Date ) => {
211+ const min = getCurrentYearAndMonth ( minDate ) ;
212+ const max = getCurrentYearAndMonth ( maxDate ) ;
213+
214+ const minTimestamp = new Date ( min . year , min . month , 1 ) . getTime ( ) ;
215+ const maxTimestamp = new Date ( max . year , max . month , 1 ) . getTime ( ) ;
216+
217+ const prevYearTimestamp = getPrevYear ( value ) . getTime ( ) ;
218+ const prevMonthTimestamp = getPrevMonth ( value ) . getTime ( ) ;
219+ const nextMonthTimestamp = getNextMonth ( value ) . getTime ( ) ;
220+ const nextYearTimestamp = getNextYear ( value ) . getTime ( ) ;
221+
222+ const preYearBtnDisable = prevYearTimestamp < minTimestamp || prevMonthTimestamp < minTimestamp ;
223+ const prevMonthBtnDisable = prevMonthTimestamp < minTimestamp ;
224+ const nextYearBtnDisable = nextMonthTimestamp > maxTimestamp || nextYearTimestamp > maxTimestamp ;
225+ const nextMonthBtnDisable = nextMonthTimestamp > maxTimestamp ;
226+
227+ setHeaderButtons ( {
228+ preYearBtnDisable,
229+ prevMonthBtnDisable,
230+ nextYearBtnDisable,
231+ nextMonthBtnDisable,
232+ } ) ;
233+ } ;
234+
235+ const calcCurrentMonth = ( newValue ?: any ) => {
236+ const date = newValue || getCurrentDate ( ) ;
237+ const { year, month } = getCurrentYearAndMonth ( date ) ;
238+ setCurrentMonth ( months . filter ( ( item ) => item . year === year && item . month === month ) ) ;
239+ updateActionButton ( date ) ;
240+ // eslint-disable-next-line react-hooks/exhaustive-deps
241+ } ;
242+
243+ // 选择日期
244+ const handleSelect = ( year , month , date , dateItem ) => {
245+ if ( dateItem . type === 'disabled' || props . readonly ) return ;
246+ const selected = new Date ( year , month , date ) ;
247+ let newSelected : TCalendarValue | TCalendarValue [ ] ;
248+ if ( props . type === 'range' && Array . isArray ( selectedDate ) ) {
249+ if ( selectedDate . length === 1 ) {
250+ if ( selectedDate [ 0 ] > selected ) {
251+ newSelected = [ selected ] ;
252+ } else {
253+ newSelected = [ selectedDate [ 0 ] , selected ] ;
254+ }
255+ } else {
256+ newSelected = [ selected ] ;
257+ if ( ! confirmBtn && selectedDate . length === 2 ) {
258+ props . onChange ?.( new Date ( selectedDate [ 0 ] ) ) ;
259+ }
260+ }
261+ } else if ( props . type === 'multiple' ) {
262+ const newVal = [ ...( Array . isArray ( selectedDate ) ? selectedDate : [ selectedDate ] ) ] ;
263+ const index = newVal . findIndex ( ( item ) => isSameDate ( item , selected ) ) ;
264+ if ( index > - 1 ) {
265+ newVal . splice ( index , 1 ) ;
266+ } else {
267+ newVal . push ( selected ) ;
268+ }
269+ newSelected = newVal ;
270+ } else {
271+ newSelected = selected ;
272+ if ( ! confirmBtn ) {
273+ props . onChange ?.( selected ) ;
274+ }
275+ }
276+ setSelectedDate ( newSelected ) ;
277+
278+ if ( props . switchMode !== 'none' ) {
279+ const date = getCurrentDate ( ) ;
280+ calcCurrentMonth ( date ) ;
281+ }
282+
283+ props . onSelect ?.( newSelected as any ) ;
284+ } ;
285+
207286 const handleConfirm = ( ) => {
208287 props . onClose ?.( 'confirm-btn' ) ;
209288 props . onConfirm ?.( new Date ( Array . isArray ( selectedDate ) ? selectedDate [ 0 ] : selectedDate ) ) ;
@@ -244,10 +323,94 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
244323 [ ] ,
245324 ) ;
246325
326+ useEffect ( ( ) => {
327+ if ( props . switchMode !== 'none' ) {
328+ calcCurrentMonth ( ) ;
329+ }
330+ // eslint-disable-next-line react-hooks/exhaustive-deps
331+ } , [ props . switchMode , selectedDate ] ) ;
332+
333+ const handleSwitchModeChange = ( type : 'pre-year' | 'pre-month' | 'next-month' | 'next-year' , disabled ?: boolean ) => {
334+ if ( disabled ) return ;
335+ const date = getCurrentDate ( ) ;
336+
337+ const funcMap = {
338+ 'pre-year' : ( ) => getPrevYear ( date ) ,
339+ 'pre-month' : ( ) => getPrevMonth ( date ) ,
340+ 'next-month' : ( ) => getNextMonth ( date ) ,
341+ 'next-year' : ( ) => getNextYear ( date ) ,
342+ } ;
343+ const newValue = funcMap [ type ] ( ) ;
344+ if ( ! newValue ) return ;
345+
346+ const { year, month } = getCurrentYearAndMonth ( newValue ) ;
347+
348+ props . onPanelChange ?.( { year, month : month + 1 } ) ;
349+
350+ calcCurrentMonth ( newValue ) ;
351+ } ;
352+
353+ const onScroll = ( e : React . UIEvent < HTMLDivElement > ) => {
354+ props . onScroll ?.( { e } ) ;
355+ } ;
356+
247357 return (
248358 < div ref = { ref } className = { `${ className } ` } >
249359 < div className = { `${ calendarClass } __title` } > { parseTNode ( props . title , null , local . title ) } </ div >
250360 { props . usePopup && < CloseIcon className = { `${ calendarClass } __close-btn` } size = { 24 } onClick = { handleClose } /> }
361+ { props . switchMode !== 'none' && (
362+ < div className = { `${ calendarClass } -header` } >
363+ < div className = { `${ calendarClass } -header__action` } >
364+ { props . switchMode === 'year-month' && (
365+ < div
366+ className = { classNames ( [
367+ `${ calendarClass } -header__icon` ,
368+ { [ `${ calendarClass } -header__icon--disabled` ] : headerButtons . preYearBtnDisable } ,
369+ ] ) }
370+ onClick = { ( ) => handleSwitchModeChange ( 'pre-year' , headerButtons . preYearBtnDisable ) }
371+ >
372+ < ChevronLeftDoubleIcon />
373+ </ div >
374+ ) }
375+
376+ < div
377+ className = { classNames ( [
378+ `${ calendarClass } -header__icon` ,
379+ { [ `${ calendarClass } -header__icon--disabled` ] : headerButtons . prevMonthBtnDisable } ,
380+ ] ) }
381+ onClick = { ( ) => handleSwitchModeChange ( 'pre-month' , headerButtons . prevMonthBtnDisable ) }
382+ >
383+ < ChevronLeftIcon />
384+ </ div >
385+ </ div >
386+ < div className = { `${ calendarClass } -header__title` } >
387+ { t ( local . monthTitle , { year : currentMonth [ 0 ] ?. year , month : local . months [ currentMonth [ 0 ] ?. month ] } ) }
388+ </ div >
389+ < div className = { `${ calendarClass } -header__action` } >
390+ < div
391+ className = { classNames ( [
392+ `${ calendarClass } -header__icon` ,
393+ { [ `${ calendarClass } -header__icon--disabled` ] : headerButtons . nextMonthBtnDisable } ,
394+ ] ) }
395+ onClick = { ( ) => handleSwitchModeChange ( 'next-month' , headerButtons . nextMonthBtnDisable ) }
396+ >
397+ < ChevronRightIcon />
398+ </ div >
399+
400+ { props . switchMode === 'year-month' && (
401+ < div
402+ className = { classNames ( [
403+ `${ calendarClass } -header__icon` ,
404+ { [ `${ calendarClass } -header__icon--disabled` ] : headerButtons . nextYearBtnDisable } ,
405+ ] ) }
406+ onClick = { ( ) => handleSwitchModeChange ( 'next-year' , headerButtons . nextYearBtnDisable ) }
407+ >
408+ < ChevronRightDoubleIcon />
409+ </ div >
410+ ) }
411+ </ div >
412+ </ div >
413+ ) }
251414 < div className = { `${ calendarClass } __days` } >
252415 { days . map ( ( item , index ) => (
253416 < div key = { index } className = { `${ calendarClass } __days-item` } >
@@ -256,12 +419,14 @@ const CalendarTemplate = forwardRef<HTMLDivElement, CalendarProps>((_props, ref)
256419 ) ) }
257420 </ div >
258421
259- < div className = { `${ calendarClass } __months` } style = { { overflow : 'auto' } } >
260- { months . map ( ( item , index ) => (
422+ < div className = { `${ calendarClass } __months` } style = { { overflow : 'auto' } } onScroll = { onScroll } >
423+ { ( props . switchMode === 'none' ? months : currentMonth ) . map ( ( item , index ) => (
261424 < React . Fragment key = { `month-${ item . year } -${ item . month } -${ index } ` } >
262- < div className = { `${ calendarClass } __month` } >
263- { t ( local . monthTitle , { year : item . year , month : local . months [ item . month ] } ) }
264- </ div >
425+ { props . switchMode === 'none' && (
426+ < div className = { `${ calendarClass } __month` } >
427+ { t ( local . monthTitle , { year : item . year , month : local . months [ item . month ] } ) }
428+ </ div >
429+ ) }
265430 < div className = { `${ calendarClass } __dates` } >
266431 { new Array ( ( item . weekdayOfFirstDay - firstDayOfWeek + 7 ) % 7 ) . fill ( 0 ) . map ( ( _ , emptyIndex ) => (
267432 < div key = { `empty-${ item . year } -${ item . month } -${ emptyIndex } ` } /> // 为空 div 添加唯一 key
0 commit comments