diff --git a/src/locales/base.ts b/src/locales/base.ts index bc4e0010c5..8216f215f8 100644 --- a/src/locales/base.ts +++ b/src/locales/base.ts @@ -34,6 +34,10 @@ export interface BaseLang { start: string confirm: string title: string + week: string + month: string + year: string + quarter: string // eslint-disable-next-line @typescript-eslint/ban-types monthTitle: Function today: string diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index e812d330ae..0b228ac678 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -34,6 +34,10 @@ const enUS: BaseLang = { start: 'Start', confirm: 'Confirm', title: 'Calendar', + week: 'W', + month: 'M', + year: 'Y', + quarter: 'Q', monthTitle: (year: number, month: number) => `${year}/${Number(month) < 10 ? `0${Number(month)}` : month}`, today: 'Today', diff --git a/src/locales/id-ID.ts b/src/locales/id-ID.ts index 7ba1328ad0..e5c435cc49 100644 --- a/src/locales/id-ID.ts +++ b/src/locales/id-ID.ts @@ -34,6 +34,10 @@ const idID: BaseLang = { start: 'Mulai', confirm: 'Mengonfirmasi', title: 'Kalender', + week: 'Mg', + month: 'Bl', + year: 'Th', + quarter: 'Kt', monthTitle: (year: number, month: number) => `${year}/${Number(month) < 10 ? `0${Number(month)}` : month}`, today: 'Hari ini', diff --git a/src/locales/tr-TR.ts b/src/locales/tr-TR.ts index cc13028177..898f259d8e 100644 --- a/src/locales/tr-TR.ts +++ b/src/locales/tr-TR.ts @@ -42,6 +42,10 @@ const trTR: BaseLang = { start: 'Başlangıç', confirm: 'Onayla', title: 'Takvim seçimi', + week: 'H', + month: 'A', + year: 'Y', + quarter: 'Ç', monthTitle: (year: number, month: number) => `${year}/${Number(month) < 10 ? `0${Number(month)}` : month}`, today: 'Bugün', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 896064def4..982e69a6d6 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -36,6 +36,10 @@ const zhCN: BaseLang = { start: '开始', confirm: '确认', title: '日历选择', + week: '周', + month: '月', + year: '年', + quarter: '季度', monthTitle: (year: number, month: number) => `${year}年${Number(month) < 10 ? `0${Number(month)}` : month}月`, today: '今天', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 1aff33151e..624dd32e74 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -36,6 +36,10 @@ const zhCN: BaseLang = { start: '開始', confirm: '確認', title: '日歷選擇', + week: '周', + month: '月', + year: '年', + quarter: '季度', monthTitle: (year: number, month: number) => `${year}年${Number(month) < 10 ? `0${Number(month)}` : month}月`, today: '今天', diff --git a/src/locales/zh-UG.ts b/src/locales/zh-UG.ts index 62dc80c998..08569e3815 100644 --- a/src/locales/zh-UG.ts +++ b/src/locales/zh-UG.ts @@ -34,6 +34,10 @@ const zhUG: BaseLang = { start: 'بېشى', confirm: 'جەزملەشتۈرۈڭ', title: 'تاللاڭ', + week: 'ھە', + month: 'ئا', + year: 'ي', + quarter: 'پە', monthTitle: (year: number, month: number) => `${year} يىلى ${Number(month) < 10 ? `0${Number(month)}` : month} ئاي`, today: 'بۈگۈن', diff --git a/src/packages/calendar/__tests__/__snapshots__/calendar.spec.tsx.snap b/src/packages/calendar/__tests__/__snapshots__/calendar.spec.tsx.snap index 9d733365e6..c777d7455b 100644 --- a/src/packages/calendar/__tests__/__snapshots__/calendar.spec.tsx.snap +++ b/src/packages/calendar/__tests__/__snapshots__/calendar.spec.tsx.snap @@ -1,7 +1,15 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`range prop 1`] = `"
2025年01月
30
31
1
开始
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2025年02月
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
今天
22
23
24
25
26
27
28
2025年03月
24
25
26
27
28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
结束
"`; +exports[`range prop 1`] = `"
2025年01月
30
31
1
开始
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2025年02月
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2025年03月
24
25
26
27
28
1
2
3
4
5
6
7
8
9
10
11
12
13
今天
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
结束
"`; -exports[`should render slot correctly 1`] = `"
2022年03月
custom27
下旬
custom28
下旬
custom1
上旬
custom2
上旬
custom3
上旬
custom4
上旬
custom5
上旬
custom6
上旬
custom7
上旬
custom8
上旬
custom9
上旬
custom10
复盘
上旬
custom11
下旬
custom12
下旬
custom13
下旬
custom14
下旬
custom15
下旬
custom16
下旬
custom17
下旬
custom18
下旬
custom19
下旬
custom20
下旬
custom21
下旬
custom22
下旬
custom23
下旬
custom24
下旬
custom25
下旬
custom26
下旬
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
custom31
下旬
2022年04月
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
custom31
下旬
custom1
上旬
custom2
上旬
custom3
上旬
custom4
上旬
custom5
上旬
custom6
上旬
custom7
上旬
custom8
上旬
custom9
上旬
custom10
复盘
上旬
custom11
下旬
custom12
下旬
custom13
下旬
custom14
下旬
custom15
下旬
custom16
下旬
custom17
下旬
custom18
下旬
custom19
下旬
custom20
下旬
custom21
下旬
custom22
下旬
custom23
下旬
custom24
下旬
custom25
下旬
custom26
下旬
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
"`; +exports[`should render calendar in month view with extended date range 1`] = `"
2025
1月
2月
3月
4月
5月
6月
7月
8月
9月
10月
11月
12月
2026
1月
2月
3月
4月
5月
6月
7月
8月
9月
10月
11月
12月
2027
1月
2月
3月
"`; -exports[`week prop 1`] = `"
2025年01月
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2025年02月
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
开始
11
12
13
14
15
16
结束
17
18
19
20
21
今天
22
23
24
25
26
27
28
2025年03月
24
25
26
27
28
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"`; +exports[`should render calendar in month view with onItemClick 1`] = `"
2025
1月
2月
3月
"`; + +exports[`should render calendar in quarter view with extended date range 1`] = `"
2025
1季度
2季度
3季度
4季度
2026
1季度
2季度
3季度
4季度
2027
1季度
"`; + +exports[`should render slot correctly 1`] = `"
2022年03月
custom27
下旬
custom28
下旬
custom1
上旬
custom2
上旬
custom3
上旬
custom4
上旬
custom5
上旬
custom6
上旬
custom7
上旬
custom8
上旬
custom9
上旬
custom10
复盘
上旬
custom11
下旬
custom12
下旬
custom13
下旬
custom14
下旬
custom15
下旬
custom16
下旬
custom17
下旬
custom18
下旬
custom19
下旬
custom20
下旬
custom21
下旬
custom22
下旬
custom23
下旬
custom24
下旬
custom25
下旬
custom26
下旬
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
custom31
下旬
2022年04月
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
custom31
下旬
custom1
上旬
custom2
上旬
custom3
上旬
custom4
上旬
custom5
上旬
custom6
上旬
custom7
上旬
custom8
上旬
custom9
上旬
custom10
复盘
上旬
custom11
下旬
custom12
下旬
custom13
下旬
custom14
下旬
custom15
下旬
custom16
下旬
custom17
下旬
custom18
下旬
custom19
下旬
custom20
下旬
custom21
下旬
custom22
下旬
custom23
下旬
custom24
下旬
custom25
下旬
custom26
下旬
custom27
下旬
custom28
下旬
custom29
下旬
custom30
下旬
"`; + +exports[`viewmode prop 1`] = `"
2025
1季度
"`; + +exports[`week prop 1`] = `"
2025年01月
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2025年02月
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
开始
11
12
13
14
15
16
结束
17
18
19
20
21
22
23
24
25
26
27
28
2025年03月
24
25
26
27
28
1
2
3
4
5
6
7
8
9
10
11
12
13
今天
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"`; diff --git a/src/packages/calendar/__tests__/calendar.spec.tsx b/src/packages/calendar/__tests__/calendar.spec.tsx index 633787ec83..c20d7eac6b 100644 --- a/src/packages/calendar/__tests__/calendar.spec.tsx +++ b/src/packages/calendar/__tests__/calendar.spec.tsx @@ -64,8 +64,10 @@ test('week prop', async () => { firstDayOfWeek={1} /> ) - const viewArea = container.querySelector('.viewArea') as HTMLElement - // expect(viewArea.innerHTML).toMatchSnapshot() + const viewArea = container.querySelector( + '.nut-calendar-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() }) test('range prop', async () => { @@ -80,8 +82,76 @@ test('range prop', async () => { firstDayOfWeek={1} /> ) - const viewArea = container.querySelector('.viewArea') as HTMLElement - // expect(viewArea.innerHTML).toMatchSnapshot() + const viewArea = container.querySelector( + '.nut-calendar-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() +}) + +test('viewmode prop', async () => { + const onItemClick = vi.fn() + const { container } = render( + + ) + const viewArea = container.querySelector( + '.nut-calendar-viewmode-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() +}) + +test('should render calendar in quarter view with extended date range', async () => { + const { container } = render( + + ) + const viewArea = container.querySelector( + '.nut-calendar-viewmode-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() +}) + +test('should render calendar in month view with onItemClick', async () => { + const onItemClick = vi.fn() + const { container } = render( + + ) + const viewArea = container.querySelector( + '.nut-calendar-viewmode-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() +}) + +test('should render calendar in month view with extended date range', async () => { + const onItemClick = vi.fn() + const { container } = render( + + ) + const viewArea = container.querySelector( + '.nut-calendar-viewmode-pannel' + ) as HTMLElement + expect(viewArea.innerHTML).toMatchSnapshot() }) test('popup prop', async () => { @@ -121,7 +191,9 @@ test('should render slot correctly', async () => { const topSlot = container.querySelector( '.nut-calendar-header-buttons' ) as HTMLElement - const viewArea = container.querySelector('.viewArea') as HTMLElement + const viewArea = container.querySelector( + '.nut-calendar-pannel' + ) as HTMLElement expect(topSlot.innerHTML).toContain('
最近七天
') expect(viewArea.innerHTML).toMatchSnapshot() }) @@ -217,7 +289,7 @@ test('range select event when click item', () => { const start = container.querySelectorAll( '.nut-calendar-day-active .nut-calendar-day-day' )[0] - expect(start.innerHTML).toBe('13') + expect(start.innerHTML).toBe('10') const calendarMonthDay2 = container.querySelectorAll('.nut-calendar-day')[20] fireEvent.click(calendarMonthDay2) @@ -225,5 +297,5 @@ test('range select event when click item', () => { const next = container.querySelectorAll( '.nut-calendar-day-choose .nut-calendar-day-day' )[0] - expect(next.innerHTML).toBe('14') + expect(next.innerHTML).toBe('11') }) diff --git a/src/packages/calendar/calendar.scss b/src/packages/calendar/calendar.scss index 4a9d9267c8..340c7a7e6d 100644 --- a/src/packages/calendar/calendar.scss +++ b/src/packages/calendar/calendar.scss @@ -1,4 +1,5 @@ @import '../popup/popup.scss'; +@import './calendarviewmode.scss'; .nut-calendar { position: relative; @@ -62,6 +63,10 @@ height: 36px; border-radius: 0px 0px 12px 12px; box-shadow: 0px 4px 10px 0px rgba($color: #000000, $alpha: 0.06); + + &-shrink { + padding-left: 17%; + } } &-week-item { @@ -111,6 +116,22 @@ margin: 8px 0; } + &-weeknumber { + width: 35%; + &-index { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + position: relative; + height: $calendar-day-height; + font-weight: $font-weight; + font-size: $font-size-base; + margin-bottom: 4px; + color: $color-text-help; + } + } + &-days { overflow: hidden; } @@ -205,6 +226,10 @@ } } + .shrink { + display: flex; + } + // 底部导航 &-footer { display: flex; diff --git a/src/packages/calendar/calendar.taro.tsx b/src/packages/calendar/calendar.taro.tsx index e1fc7057c7..1430d800d1 100644 --- a/src/packages/calendar/calendar.taro.tsx +++ b/src/packages/calendar/calendar.taro.tsx @@ -1,6 +1,7 @@ import React, { useRef, ReactNode } from 'react' import Popup from '@/packages/popup/index.taro' import CalendarItem from '@/packages/calendaritem/index.taro' +import CalendarViewModeItem from './calendarviewmodeitem.taro' import { getDateString } from '@/utils/date' import { useConfig } from '@/packages/configprovider/configprovider.taro' import type { CalendarDay, CalendarType, CalendarRef } from './types' @@ -8,10 +9,12 @@ import { ComponentDefaults } from '@/utils/typings' export interface CalendarProps { type?: CalendarType + viewMode: 'day' | 'month' | 'quarter' autoBackfill?: boolean popup?: boolean visible?: boolean title?: string + value?: string defaultValue?: string | string[] startDate?: string endDate?: string @@ -21,6 +24,7 @@ export interface CalendarProps { confirmText?: ReactNode showTitle?: boolean showSubTitle?: boolean + showMonthNumber?: boolean scrollAnimation?: boolean firstDayOfWeek: number closeIcon?: ReactNode @@ -33,16 +37,19 @@ export interface CalendarProps { onClose?: () => void onConfirm?: (param: string) => void onDayClick?: (data: string) => void + onItemClick?: (param: string) => void onPageChange?: (param: string) => void } const defaultProps = { ...ComponentDefaults, type: 'single', + viewMode: 'day', autoBackfill: false, popup: true, visible: false, title: '', + value: '', defaultValue: '', startDate: getDateString(0), endDate: getDateString(365), @@ -52,6 +59,7 @@ const defaultProps = { confirmText: '', showTitle: true, showSubTitle: true, + showMonthNumber: false, scrollAnimation: true, firstDayOfWeek: 0, disableDate: (date: CalendarDay) => false, @@ -62,6 +70,7 @@ const defaultProps = { onClose: () => {}, onConfirm: (param: string) => {}, onDayClick: (data: string) => {}, + onItemClick: () => {}, onPageChange: (param: string) => {}, } as CalendarProps @@ -77,8 +86,10 @@ export const Calendar = React.forwardRef< popup, visible, type, + viewMode, autoBackfill, title, + value, defaultValue, startDate, endDate, @@ -88,6 +99,7 @@ export const Calendar = React.forwardRef< confirmText, showTitle, showSubTitle, + showMonthNumber, scrollAnimation, firstDayOfWeek, closeIcon, @@ -100,6 +112,7 @@ export const Calendar = React.forwardRef< onClose, onConfirm, onDayClick, + onItemClick, onPageChange, } = { ...defaultProps, ...props } @@ -117,10 +130,6 @@ export const Calendar = React.forwardRef< close() } - const select = (param: string) => { - onDayClick && onDayClick(param) - } - const scrollToDate = (date: string) => { calendarRef.current?.scrollToDate(date) } @@ -135,53 +144,75 @@ export const Calendar = React.forwardRef< const renderItem = () => { return ( - + <> + {viewMode !== 'day' ? ( + + ) : ( + + )} + ) } return ( <> - {popup ? ( + {popup && viewMode === 'day' ? ( {renderItem()} diff --git a/src/packages/calendar/calendar.tsx b/src/packages/calendar/calendar.tsx index 0c2eba3361..8e76dba913 100644 --- a/src/packages/calendar/calendar.tsx +++ b/src/packages/calendar/calendar.tsx @@ -1,6 +1,7 @@ import React, { useRef, ReactNode } from 'react' import Popup from '@/packages/popup' import CalendarItem from '@/packages/calendaritem' +import CalendarViewModeItem from './calendarviewmodeitem' import { getDateString } from '@/utils/date' import { useConfig } from '@/packages/configprovider' import type { CalendarDay, CalendarType, CalendarRef } from './types' @@ -8,10 +9,12 @@ import { ComponentDefaults } from '@/utils/typings' export interface CalendarProps { type?: CalendarType + viewMode: 'day' | 'month' | 'quarter' autoBackfill?: boolean popup?: boolean visible?: boolean title?: string + value?: string defaultValue?: string | string[] startDate?: string endDate?: string @@ -21,6 +24,7 @@ export interface CalendarProps { confirmText?: ReactNode showTitle?: boolean showSubTitle?: boolean + showMonthNumber?: boolean scrollAnimation?: boolean firstDayOfWeek: number closeIcon?: ReactNode @@ -33,16 +37,19 @@ export interface CalendarProps { onClose?: () => void onConfirm?: (param: string) => void onDayClick?: (data: string) => void + onItemClick?: (param: string) => void onPageChange?: (param: string) => void } const defaultProps = { ...ComponentDefaults, type: 'single', + viewMode: 'day', autoBackfill: false, popup: true, visible: false, title: '', + value: '', defaultValue: '', startDate: getDateString(0), endDate: getDateString(365), @@ -52,6 +59,7 @@ const defaultProps = { confirmText: '', showTitle: true, showSubTitle: true, + showMonthNumber: false, scrollAnimation: true, firstDayOfWeek: 0, disableDate: (date: CalendarDay) => false, @@ -62,6 +70,7 @@ const defaultProps = { onClose: () => {}, onConfirm: (param: string) => {}, onDayClick: (data: string) => {}, + onItemClick: () => {}, onPageChange: (param: string) => {}, } as CalendarProps @@ -77,8 +86,10 @@ export const Calendar = React.forwardRef< popup, visible, type, + viewMode, autoBackfill, title, + value, defaultValue, startDate, endDate, @@ -88,6 +99,7 @@ export const Calendar = React.forwardRef< confirmText, showTitle, showSubTitle, + showMonthNumber, scrollAnimation, firstDayOfWeek, closeIcon, @@ -100,6 +112,7 @@ export const Calendar = React.forwardRef< onClose, onConfirm, onDayClick, + onItemClick, onPageChange, } = { ...defaultProps, ...props } @@ -117,10 +130,6 @@ export const Calendar = React.forwardRef< close() } - const select = (param: string) => { - onDayClick && onDayClick(param) - } - const scrollToDate = (date: string) => { calendarRef.current?.scrollToDate(date) } @@ -135,42 +144,64 @@ export const Calendar = React.forwardRef< const renderItem = () => { return ( - + <> + {viewMode !== 'day' ? ( + + ) : ( + onDayClick && onDayClick(param)} + onPageChange={yearMonthChange} + /> + )} + ) } return ( <> - {popup ? ( + {popup && viewMode === 'day' ? ( void +} +// 年面板的高度:cssHeight 231 +const YearMonthPanelHeight = 231 +// 年面板的高度:cssHeight 103 +const YearQuarterPanelHeight = 103 + +export interface CalendarViewModeItemProps { + type: CalendarType + viewMode: 'month' | 'quarter' + title: string + value?: CalendarValue + defaultValue?: CalendarValue + startDate: CalendarValue + endDate: CalendarValue + showTitle: boolean + scrollAnimation: boolean + renderDay: (date: CalendarDay) => string | JSX.Element + onItemClick: (data: string) => void +} +const defaultProps = { + ...ComponentDefaults, + type: 'single', + viewMode: 'month', + title: '', + startDate: getDateString(0), + endDate: getDateString(365), + showToday: true, + showTitle: true, + scrollAnimation: true, + renderDay: undefined, + onItemClick: () => {}, +} as unknown as CalendarViewModeItemProps + +export const CalendarViewModeItem = React.forwardRef< + CalendarRef, + Partial & + Omit, ''> +>((props) => { + const { locale } = useConfig() + const { + style, + className, + viewMode, + title, + value, + defaultValue, + startDate, + endDate, + showTitle, + renderDay, + onItemClick, + } = { ...defaultProps, ...props } + + const classPrefix = 'nut-calendar-viewmode' + + // 为了便于区分,用'YYYY-MM'表示月,用'YYYY-QX'表示Q + const [panelDate, setPanelDate] = useState({ + months: [ + { + year: new Date().getFullYear(), + months: [] as CalendarMonth[], + cssHeight: 0, + scrollTop: 0, + currYear: false, + }, + ], + quarters: [ + { + year: new Date().getFullYear(), + quarters: [] as CalendarQuarter[], + cssHeight: 0, + scrollTop: 0, + currYear: false, + }, + ], + }) + + const [scrollTop, setScrollTop] = useState(0) + + // 初始化开始结束数据 + const propStartDate = (startDate || getDateString(0)) as string + const propEndDate = (endDate || getDateString(365)) as string + const startDates = splitDate(propStartDate) + const endDates = splitDate(propEndDate) + + const [innerValue, setInnerValue] = usePropsValue({ + value, + defaultValue, + finalValue: [], + onChange: (val) => {}, + }) + + const monthsRef = useRef(null) + const monthsPanel = useRef(null) + const viewAreaRef = useRef(null) + let viewHeight = 0 + + const getMonthsPanel = () => { + return monthsPanel.current as HTMLDivElement + } + const getMonthsRef = () => { + return monthsRef.current as HTMLDivElement + } + const requestAniFrameFunc = (viewMode: string) => { + switch (viewMode) { + case 'month': + { + const lastItem = panelDate.months[panelDate.months.length - 1] + const containerHeight = lastItem.cssHeight + lastItem.scrollTop + const currentIndex = panelDate.months.findIndex( + (item) => item.currYear === true + ) + requestAniFrame(() => { + // 初始化 日历位置 + if (monthsRef && monthsPanel && viewAreaRef) { + viewHeight = getMonthsRef().clientHeight + getMonthsPanel().style.height = `${containerHeight}px` + const currTop = panelDate.months[currentIndex]?.scrollTop || 0 + getMonthsRef().scrollTop = currTop + setScrollTop(currTop) + } + }) + } + break + case 'quarter': + { + const lastItem = panelDate.quarters[panelDate.quarters.length - 1] + const containerHeight = lastItem.cssHeight + lastItem.scrollTop + const currentIndex = panelDate.quarters.findIndex( + (item) => item.currYear === true + ) + requestAniFrame(() => { + // 初始化 日历位置 + if (monthsRef && monthsPanel && viewAreaRef) { + viewHeight = getMonthsRef().clientHeight + getMonthsPanel().style.height = `${containerHeight}px` + const currTop = panelDate.quarters[currentIndex]?.scrollTop || 0 + getMonthsRef().scrollTop = currTop + setScrollTop(currTop) + } + }) + } + break + default: + break + } + } + const isCurrYear = (year: number) => { + return (innerValue as string).split('-')[0] === `${year}` + } + const getMonthsData = () => { + // 获取区间范围内可用的月数,包括边界值所在的月份 + const startYear = Number(startDates[0]) + const startMonth = Number(startDates[1]) + const endYear = Number(endDates[0]) + const endMonth = Number(endDates[1]) + const panelData: CalendarMonthInfoOfPanel[] = [] + // 第某年的scrollTop:年面板高度 * (第某年数-1) + const addPanelData = ( + year: number, + months: CalendarMonth[], + scrollTop: number + ) => { + panelData.push({ + year, + months, + scrollTop, + cssHeight: YearMonthPanelHeight, + currYear: isCurrYear(year), + }) + } + // 在同一年时 + if (startYear === endYear) { + addPanelData( + startYear, + [ + ...getPreMonths('prev', startYear, startMonth), + ...getMonths('curr', startYear, startMonth, endMonth), + ...getMonths('next', endYear, endMonth + 1), + ], + 0 + ) + } else { + let scrollTop = panelData.length * YearMonthPanelHeight + const startMonths = [ + ...getPreMonths('prev', startYear, startMonth), + ...getMonths('curr', startYear, startMonth), + ] + addPanelData(startYear, startMonths, scrollTop) + // 不同年份时,注意可能跨多个年 + for (let i = startYear + 1; i < endYear; i++) { + scrollTop = panelData.length * YearMonthPanelHeight + const midMonths = [...getMonths('curr', i, 1)] + addPanelData(i, midMonths, scrollTop) + } + const lastMonths = [ + ...getPreMonths('curr', endYear, endMonth + 1), + ...getMonths('next', endYear, endMonth + 1), + ] + scrollTop = panelData.length * YearMonthPanelHeight + addPanelData(endYear, lastMonths, scrollTop) + } + return panelData + } + const getQuartersData = () => { + // 获取区间范围内可用的季度数,包括边界值所在的季度数 + const startYear = Number(startDates[0]) + const startMonth = Number(startDates[1]) + const endYear = Number(endDates[0]) + const endMonth = Number(endDates[1]) + const panelData: CalendarQuarterInfoOfPanel[] = [] + // 第某年的scrollTop:年面板高度 * (第某年数-1) + const addPanelData = ( + year: number, + quarters: CalendarQuarter[], + scrollTop: number + ) => { + panelData.push({ + year, + quarters, + scrollTop, + cssHeight: YearQuarterPanelHeight, + currYear: isCurrYear(year), + }) + } + // 在同一年时 + if (startYear === endYear) { + const quarters = [ + ...getPreQuarters('prev', startYear, startMonth), + ...getQuarters('curr', startYear, startMonth, endMonth), + ...getNextQuarters('next', endYear, endMonth), + ] + addPanelData(startYear, quarters, 0) + } else { + let scrollTop = panelData.length * YearQuarterPanelHeight + const startQuarters = [ + ...getPreQuarters('prev', startYear, startMonth), + ...getQuarters('curr', startYear, startMonth), + ] + addPanelData(startYear, startQuarters, scrollTop) + // 不同年份时,注意可能跨多个年 + for (let i = startYear + 1; i < endYear; i++) { + scrollTop = panelData.length * YearQuarterPanelHeight + const midQuarters = [...getQuarters('curr', i, 1)] + addPanelData(i, midQuarters, scrollTop) + } + const lastQuarters = [ + ...getQuarters('curr', endYear, 1, endMonth), + ...getNextQuarters('next', endYear, endMonth), + ] + scrollTop = panelData.length * YearQuarterPanelHeight + addPanelData(endYear, lastQuarters, scrollTop) + } + return panelData + } + /* + * 初始化面板数据 + * 获取总数据panelDate + * 根据当前默认值跳转到指定位置 + */ + const initData = () => { + // 获取起止时间内的所有的月、季 + const data = + // eslint-disable-next-line no-nested-ternary + viewMode === 'month' + ? getMonthsData() + : viewMode === 'quarter' + ? getQuartersData() + : null + setPanelDate({ ...panelDate, [`${viewMode}s`]: data }) + } + + useEffect(() => { + requestAniFrameFunc(viewMode) + }, [panelDate]) + + useEffect(() => { + initData() + }, []) + + const monthsViewScroll = (e: any) => { + const scrollTop = (e.target as HTMLElement).scrollTop + Taro.getEnv() === 'WEB' && setScrollTop(scrollTop) + } + + const handleItemClick = (viewMode: string, item: any) => { + // 点击事件,可以返回所点击元素的数据 + // 如果非可点击,则直接返回,不做处理 + if (item.type !== 'curr') return + // 可点击时,需要关注当前元素是否已被选中,选中,取消选中,拿到数据 + const val = viewMode === 'month' ? item.yearAndMonth : item.yearAndQuarter + setInnerValue(val) + onItemClick?.(val) + } + + const isDisable = (item: any) => { + return item.type === 'prev' || item.type === 'next' + } + + const isActive = (item: any) => { + const val = viewMode === 'month' ? item.yearAndMonth : item.yearAndQuarter + return val === innerValue + } + + const getClasses = (item: any) => { + if (isDisable(item)) { + return ['disabled'] + } + const res = [] + if (item.type === 'curr') { + if (isActive(item)) { + res.push('active') + } + } + return res + } + + const classes = classNames(classPrefix, className) + + const renderHeader = () => { + return ( + showTitle && ( + + {title} + + ) + ) + } + + const renderItem = (item: any, index: number) => { + const units = { + month: locale.calendaritem.month, + quarter: locale.calendaritem.quarter, + } + return ( +
handleItemClick(viewMode, item)} + key={index} + > +
+ {renderDay ? renderDay(item) : `${item[viewMode]}${units[viewMode]}`} +
+
+ ) + } + + const renderPanel = () => { + return ( + <> + {panelDate[`${viewMode}s`].map((item: any, key: number) => ( +
+
{item.year}
+
+ {item[`${viewMode}s`].map((item: any, i: number) => + renderItem(item, i) + )} +
+
+ ))} + + ) + } + + const renderContent = () => { + return ( + +
+
{renderPanel()}
+
+
+ ) + } + + return ( +
+ {renderHeader()} + {renderContent()} +
+ ) +}) + +CalendarViewModeItem.displayName = 'NutCalendarViewModeItem' +export default CalendarViewModeItem diff --git a/src/packages/calendar/calendarviewmodeitem.tsx b/src/packages/calendar/calendarviewmodeitem.tsx new file mode 100644 index 0000000000..a9aba98e14 --- /dev/null +++ b/src/packages/calendar/calendarviewmodeitem.tsx @@ -0,0 +1,411 @@ +import React, { useState, useEffect, useRef } from 'react' +import classNames from 'classnames' +import { ComponentDefaults } from '@/utils/typings' +import { getDateString } from '@/utils/date' +import requestAniFrame from '@/utils/raf' +import { useConfig } from '@/packages/configprovider' +import { usePropsValue } from '@/utils/use-props-value' +import { + splitDate, + getPreMonths, + getMonths, + getPreQuarters, + getNextQuarters, + getQuarters, +} from './utils' +import { + CalendarDay, + CalendarValue, + CalendarType, + CalendarMonth, + CalendarMonthInfoOfPanel, + CalendarQuarterInfoOfPanel, + CalendarQuarter, +} from './types' + +type CalendarRef = { + scrollToDate: (date: string) => void +} + +// 年面板的高度:cssHeight 231 +const YearMonthPanelHeight = 231 +// 年面板的高度:cssHeight 103 +const YearQuarterPanelHeight = 103 + +export interface CalendarViewModeItemProps { + type: CalendarType + viewMode: 'month' | 'quarter' + title: string + value?: CalendarValue + defaultValue?: CalendarValue + startDate: CalendarValue + endDate: CalendarValue + showTitle: boolean + scrollAnimation: boolean + renderDay: (date: CalendarDay) => string | JSX.Element + onItemClick: (data: string) => void +} +const defaultProps = { + ...ComponentDefaults, + type: 'single', + viewMode: 'month', + title: '', + startDate: getDateString(0), + endDate: getDateString(365), + showToday: true, + showTitle: true, + scrollAnimation: true, + renderDay: undefined, + onItemClick: () => {}, +} as unknown as CalendarViewModeItemProps + +export const CalendarViewModeItem = React.forwardRef< + CalendarRef, + Partial & + Omit, ''> +>((props) => { + const { locale } = useConfig() + const { + style, + className, + viewMode, + title, + value, + defaultValue, + startDate, + endDate, + showTitle, + renderDay, + onItemClick, + } = { ...defaultProps, ...props } + + const classPrefix = 'nut-calendar-viewmode' + + // 为了便于区分,用'YYYY-MM'表示月,用'YYYY-QX'表示Q + const [panelDate, setPanelDate] = useState({ + months: [ + { + year: new Date().getFullYear(), + months: [] as CalendarMonth[], + cssHeight: 0, + scrollTop: 0, + currYear: false, + }, + ], + quarters: [ + { + year: new Date().getFullYear(), + quarters: [] as CalendarQuarter[], + cssHeight: 0, + scrollTop: 0, + currYear: false, + }, + ], + }) + // 初始化开始结束数据 + const propStartDate = (startDate || getDateString(0)) as string + const propEndDate = (endDate || getDateString(365)) as string + const startDates = splitDate(propStartDate) + const endDates = splitDate(propEndDate) + + const [innerValue, setInnerValue] = usePropsValue({ + value, + defaultValue, + finalValue: [], + onChange: (val) => {}, + }) + + const monthsRef = useRef(null) + const monthsPanel = useRef(null) + const viewAreaRef = useRef(null) + let viewHeight = 0 + + const getMonthsPanel = () => { + return monthsPanel.current as HTMLDivElement + } + const getMonthsRef = () => { + return monthsRef.current as HTMLDivElement + } + const requestAniFrameFunc = (viewMode: string) => { + switch (viewMode) { + case 'month': + { + const lastItem = panelDate.months[panelDate.months.length - 1] + const containerHeight = lastItem.cssHeight + lastItem.scrollTop + const currentIndex = panelDate.months.findIndex( + (item) => item.currYear === true + ) + requestAniFrame(() => { + // 初始化 日历位置 + if (monthsRef && monthsPanel && viewAreaRef) { + viewHeight = getMonthsRef().clientHeight + getMonthsPanel().style.height = `${containerHeight}px` + getMonthsRef().scrollTop = + panelDate.months[currentIndex].scrollTop + } + }) + } + break + case 'quarter': + { + const lastItem = panelDate.quarters[panelDate.quarters.length - 1] + const containerHeight = lastItem.cssHeight + lastItem.scrollTop + const currentIndex = panelDate.quarters.findIndex( + (item) => item.currYear === true + ) + requestAniFrame(() => { + // 初始化 日历位置 + if (monthsRef && monthsPanel && viewAreaRef) { + viewHeight = getMonthsRef().clientHeight + getMonthsPanel().style.height = `${containerHeight}px` + getMonthsRef().scrollTop = + panelDate.quarters[currentIndex].scrollTop + } + }) + } + break + default: + break + } + } + const isCurrYear = (year: number) => { + return (innerValue as string).split('-')[0] === `${year}` + } + const getMonthsData = () => { + // 获取区间范围内可用的月数,包括边界值所在的月份 + const startYear = Number(startDates[0]) + const startMonth = Number(startDates[1]) + const endYear = Number(endDates[0]) + const endMonth = Number(endDates[1]) + const panelData: CalendarMonthInfoOfPanel[] = [] + // 第某年的scrollTop:年面板高度 * (第某年数-1) + const addPanelData = ( + year: number, + months: CalendarMonth[], + scrollTop: number + ) => { + panelData.push({ + year, + months, + scrollTop, + cssHeight: YearMonthPanelHeight, + currYear: isCurrYear(year), + }) + } + // 在同一年时 + if (startYear === endYear) { + addPanelData( + startYear, + [ + ...getPreMonths('prev', startYear, startMonth), + ...getMonths('curr', startYear, startMonth, endMonth), + ...getMonths('next', endYear, endMonth + 1), + ], + 0 + ) + } else { + let scrollTop = panelData.length * YearMonthPanelHeight + const startMonths = [ + ...getPreMonths('prev', startYear, startMonth), + ...getMonths('curr', startYear, startMonth), + ] + addPanelData(startYear, startMonths, scrollTop) + // 不同年份时,注意可能跨多个年 + for (let i = startYear + 1; i < endYear; i++) { + scrollTop = panelData.length * YearMonthPanelHeight + const midMonths = [...getMonths('curr', i, 1)] + addPanelData(i, midMonths, scrollTop) + } + const lastMonths = [ + ...getPreMonths('curr', endYear, endMonth + 1), + ...getMonths('next', endYear, endMonth + 1), + ] + + scrollTop = panelData.length * YearMonthPanelHeight + addPanelData(endYear, lastMonths, scrollTop) + } + return panelData + } + const getQuartersData = () => { + // 获取区间范围内可用的季度数,包括边界值所在的季度数 + const startYear = Number(startDates[0]) + const startMonth = Number(startDates[1]) + const endYear = Number(endDates[0]) + const endMonth = Number(endDates[1]) + const panelData: CalendarQuarterInfoOfPanel[] = [] + // 第某年的scrollTop:年面板高度 * (第某年数-1) + const addPanelData = ( + year: number, + quarters: CalendarQuarter[], + scrollTop: number + ) => { + panelData.push({ + year, + quarters, + scrollTop, + cssHeight: YearQuarterPanelHeight, + currYear: isCurrYear(year), + }) + } + // 在同一年时 + if (startYear === endYear) { + const quarters = [ + ...getPreQuarters('prev', startYear, startMonth), + ...getQuarters('curr', startYear, startMonth, endMonth), + ...getNextQuarters('next', endYear, endMonth), + ] + addPanelData(startYear, quarters, 0) + } else { + let scrollTop = panelData.length * YearQuarterPanelHeight + const startQuarters = [ + ...getPreQuarters('prev', startYear, startMonth), + ...getQuarters('curr', startYear, startMonth), + ] + addPanelData(startYear, startQuarters, scrollTop) + // 不同年份时,注意可能跨多个年 + for (let i = startYear + 1; i < endYear; i++) { + scrollTop = panelData.length * YearQuarterPanelHeight + const midQuarters = [...getQuarters('curr', i, 1)] + addPanelData(i, midQuarters, scrollTop) + } + const lastQuarters = [ + ...getQuarters('curr', endYear, 1, endMonth), + ...getNextQuarters('next', endYear, endMonth), + ] + scrollTop = panelData.length * YearQuarterPanelHeight + addPanelData(endYear, lastQuarters, scrollTop) + } + return panelData + } + /* + * 初始化面板数据 + * 获取总数据panelDate + * 根据当前默认值跳转到指定位置 + */ + const initData = () => { + // 获取起止时间内的所有的月、季 + const data = + // eslint-disable-next-line no-nested-ternary + viewMode === 'month' + ? getMonthsData() + : viewMode === 'quarter' + ? getQuartersData() + : null + setPanelDate({ ...panelDate, [`${viewMode}s`]: data }) + } + + useEffect(() => { + requestAniFrameFunc(viewMode) + }, [panelDate]) + + useEffect(() => { + initData() + }, []) + + const handleItemClick = (item: any) => { + // 点击事件,可以返回所点击元素的数据 + // 如果非可点击,则直接返回,不做处理 + if (item.type !== 'curr') return + // 可点击时,需要关注当前元素是否已被选中,选中,取消选中,拿到数据 + const val = viewMode === 'month' ? item.yearAndMonth : item.yearAndQuarter + setInnerValue(val) + onItemClick?.(val) + } + + const isDisable = (item: any) => { + return item.type === 'prev' || item.type === 'next' + } + + const isActive = (item: any) => { + const val = viewMode === 'month' ? item.yearAndMonth : item.yearAndQuarter + return val === innerValue + } + + const getClasses = (item: any) => { + if (isDisable(item)) { + return ['disabled'] + } + const res = [] + if (item.type === 'curr') { + if (isActive(item)) { + res.push('active') + } + } + return res + } + + const classes = classNames(classPrefix, className) + + const renderHeader = () => { + return ( + showTitle && ( +
+
{title}
+
+ ) + ) + } + + const renderItem = (item: any, index: number) => { + const units = { + month: locale.calendaritem.month, + quarter: locale.calendaritem.quarter, + } + return ( +
handleItemClick(item)} + key={index} + > +
+ {renderDay ? renderDay(item) : `${item[viewMode]}${units[viewMode]}`} +
+
+ ) + } + + const renderPanel = () => { + return ( + <> + {panelDate[`${viewMode}s`].map((item: any, key: number) => ( +
+
{item.year}
+
+ {item[`${viewMode}s`].map((item: any, i: number) => + renderItem(item, i) + )} +
+
+ ))} + + ) + } + + const renderContent = () => { + return ( +
+
+
{renderPanel()}
+
+
+ ) + } + + return ( +
+ {renderHeader()} + {renderContent()} +
+ ) +}) + +CalendarViewModeItem.displayName = 'NutCalendarViewModeItem' +export default CalendarViewModeItem diff --git a/src/packages/calendar/demo.taro.tsx b/src/packages/calendar/demo.taro.tsx index 7bee1cfbf3..64a8b032d6 100644 --- a/src/packages/calendar/demo.taro.tsx +++ b/src/packages/calendar/demo.taro.tsx @@ -15,6 +15,9 @@ import Demo8 from './demos/taro/demo8' import Demo9 from './demos/taro/demo9' import Demo10 from './demos/taro/demo10' import Demo11 from './demos/taro/demo11' +import Demo101 from './demos/taro/demo101' +import Demo102 from './demos/taro/demo102' +import Demo103 from './demos/taro/demo103' const CalendarDemo = () => { const [translated] = useTranslate({ @@ -29,6 +32,9 @@ const CalendarDemo = () => { cfbdc781: '快捷选择', c3a3a1d2: '选择日期', e51e4582: '平铺展示', + ees99933: '季度面板', + e2s99933: '月面板', + e4s99933: '展示周号', '7db1a8b2': '选择多个日期', '7db1a8b3': '选择周', '7db1a8b4': '日期不可选', @@ -52,6 +58,9 @@ const CalendarDemo = () => { cfbdc781: '快捷選擇', c3a3a1d2: '選擇日期', e51e4582: '平鋪展示', + ees99933: '季度面板', + e2s99933: '月面板', + e4s99933: '展示周号', '7db1a8b2': '選擇多個日期', '7db1a8b3': '選擇周', '7db1a8b4': '日期不可選', @@ -75,6 +84,9 @@ const CalendarDemo = () => { cfbdc781: 'quick selection', c3a3a1d2: 'select date', e51e4582: 'Tiled display', + ees99933: 'Quarter Panel', + e2s99933: 'Month Panel', + e4s99933: 'Display Week Number', '7db1a8b2': 'select multiple dates', '7db1a8b3': 'select week', '7db1a8b4': 'disable date', @@ -94,34 +106,26 @@ const CalendarDemo = () => {

{translated.ce5c5446}

- - - - - - -

{translated.cfbdc781}

- - -

{translated.d04fcbda}

- - -

{translated.e51e4582}

- +

{translated.ees99933}

+ +

{translated.e2s99933}

+ +

{translated.e4s99933}

+
) diff --git a/src/packages/calendar/demo.tsx b/src/packages/calendar/demo.tsx index 03d93d8cb3..f9f5a2c417 100644 --- a/src/packages/calendar/demo.tsx +++ b/src/packages/calendar/demo.tsx @@ -12,6 +12,9 @@ import Demo8 from './demos/h5/demo8' import Demo9 from './demos/h5/demo9' import Demo10 from './demos/h5/demo10' import Demo11 from './demos/h5/demo11' +import Demo101 from './demos/h5/demo101' +import Demo102 from './demos/h5/demo102' +import Demo103 from './demos/h5/demo103' const CalendarDemo = () => { const [translated] = useTranslate({ @@ -26,6 +29,9 @@ const CalendarDemo = () => { cfbdc781: '快捷选择', c3a3a1d2: '选择日期', e51e4582: '平铺展示', + ees99933: '季度面板', + e2s99933: '月面板', + e4s99933: '展示周号', '7db1a8b2': '选择多个日期', '7db1a8b3': '选择周', '7db1a8b4': '日期不可选', @@ -48,7 +54,9 @@ const CalendarDemo = () => { '8dab2f68': '请选择截止时间', cfbdc781: '快捷選擇', c3a3a1d2: '選擇日期', - e51e4582: '平鋪展示', + ees99933: '季度面板', + e2s99933: '月面板', + e4s99933: '展示周号', '7db1a8b2': '選擇多個日期', '7db1a8b3': '選擇周', '7db1a8b4': '日期不可選', @@ -72,6 +80,9 @@ const CalendarDemo = () => { cfbdc781: 'quick selection', c3a3a1d2: 'select date', e51e4582: 'Tiled display', + ees99933: 'Quarter Panel', + e2s99933: 'Month Panel', + e4s99933: 'Display Week Number', '7db1a8b2': 'select multiple dates', '7db1a8b3': 'select week', '7db1a8b4': 'disable date', @@ -90,34 +101,26 @@ const CalendarDemo = () => { <>

{translated.ce5c5446}

- - - - - - -

{translated.cfbdc781}

- - -

{translated.d04fcbda}

- - -

{translated.e51e4582}

- +

{translated.ees99933}

+ +

{translated.e2s99933}

+ +

{translated.e4s99933}

+
) diff --git a/src/packages/calendar/demos/h5/demo1.tsx b/src/packages/calendar/demos/h5/demo1.tsx index 18e7f906fa..7279f9765c 100644 --- a/src/packages/calendar/demos/h5/demo1.tsx +++ b/src/packages/calendar/demos/h5/demo1.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react' import { Cell, Calendar } from '@nutui/nutui-react' -import { Star } from '@nutui/icons-react' const Demo1 = () => { const d = new Date() @@ -35,12 +34,10 @@ const Demo1 = () => { /> } /> ) diff --git a/src/packages/calendar/demos/h5/demo101.tsx b/src/packages/calendar/demos/h5/demo101.tsx new file mode 100644 index 0000000000..e0bfe4ece8 --- /dev/null +++ b/src/packages/calendar/demos/h5/demo101.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react' + +const Demo1 = () => { + const d = new Date() + const [date, setDate] = useState('2026-Q2') + + const select = (param: string) => { + console.log('select quarter', param) + setDate(param) + } + + return ( +
+ +
+ ) +} +export default Demo1 diff --git a/src/packages/calendar/demos/h5/demo102.tsx b/src/packages/calendar/demos/h5/demo102.tsx new file mode 100644 index 0000000000..3fd5711418 --- /dev/null +++ b/src/packages/calendar/demos/h5/demo102.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react' + +const Demo2 = () => { + const [date, setDate] = useState('2025-02') + + const select = (param: string) => { + console.log('select', param) + setDate(param) + } + + return ( +
+ +
+ ) +} +export default Demo2 diff --git a/src/packages/calendar/demos/h5/demo103.tsx b/src/packages/calendar/demos/h5/demo103.tsx new file mode 100644 index 0000000000..a07b0a55ee --- /dev/null +++ b/src/packages/calendar/demos/h5/demo103.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react' + +const Demo3 = () => { + const [date, setDate] = useState([]) + + const setChooseValue = (chooseData: any) => { + const dateArr = chooseData.map((item: any) => { + return item[3] + }) + setDate([...dateArr]) + } + + return ( +
+ +
+ ) +} +export default Demo3 diff --git a/src/packages/calendar/demos/h5/demo11.tsx b/src/packages/calendar/demos/h5/demo11.tsx index f271767c86..7523cc2f1e 100644 --- a/src/packages/calendar/demos/h5/demo11.tsx +++ b/src/packages/calendar/demos/h5/demo11.tsx @@ -9,34 +9,32 @@ const Demo11 = () => { } const yearMonthChange = (param: string) => { - console.log(param) + console.log('yearMonthChange', param) } return ( - <> -
- test
} - endText="leave" - autoBackfill - onConfirm={setChooseValue} - onPageChange={yearMonthChange} - /> - - +
+ test
} + endText="leave" + autoBackfill + onConfirm={setChooseValue} + onPageChange={yearMonthChange} + /> + ) } export default Demo11 diff --git a/src/packages/calendar/demos/taro/demo101.tsx b/src/packages/calendar/demos/taro/demo101.tsx new file mode 100644 index 0000000000..720803aa37 --- /dev/null +++ b/src/packages/calendar/demos/taro/demo101.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react-taro' + +const Demo1 = () => { + const d = new Date() + const [date, setDate] = useState('2026-Q2') + + const select = (param: string) => { + console.log('select quarter', param) + setDate(param) + } + + return ( +
+ +
+ ) +} +export default Demo1 diff --git a/src/packages/calendar/demos/taro/demo102.tsx b/src/packages/calendar/demos/taro/demo102.tsx new file mode 100644 index 0000000000..943b088dc6 --- /dev/null +++ b/src/packages/calendar/demos/taro/demo102.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react-taro' + +const Demo2 = () => { + const [date, setDate] = useState('2025-02') + + const select = (param: string) => { + console.log('select', param) + setDate(param) + } + + return ( +
+ +
+ ) +} +export default Demo2 diff --git a/src/packages/calendar/demos/taro/demo103.tsx b/src/packages/calendar/demos/taro/demo103.tsx new file mode 100644 index 0000000000..624f15df9d --- /dev/null +++ b/src/packages/calendar/demos/taro/demo103.tsx @@ -0,0 +1,37 @@ +import React, { useState } from 'react' +import { Calendar } from '@nutui/nutui-react-taro' + +const Demo3 = () => { + const [date, setDate] = useState(['2025-03-14']) + + const setChooseValue = (chooseData: any) => { + const dateArr = chooseData.map((item: any) => { + return item[3] + }) + setDate([...dateArr]) + } + + return ( +
+ +
+ ) +} +export default Demo3 diff --git a/src/packages/calendar/demos/taro/demo11.tsx b/src/packages/calendar/demos/taro/demo11.tsx index d4ff7a8647..64db20401f 100644 --- a/src/packages/calendar/demos/taro/demo11.tsx +++ b/src/packages/calendar/demos/taro/demo11.tsx @@ -19,7 +19,7 @@ const Demo11 = () => { style={{ display: 'flex', width: '100%', - height: '613px', + height: '350px', overflow: 'hidden', }} > diff --git a/src/packages/calendar/doc.en-US.md b/src/packages/calendar/doc.en-US.md index 160eed3c20..9aeb914053 100644 --- a/src/packages/calendar/doc.en-US.md +++ b/src/packages/calendar/doc.en-US.md @@ -106,6 +106,7 @@ import { Calendar } from '@nutui/nutui-react' | --- | --- | --- | --- | | visible | Is it visible | `boolean` | `false` | | type | type, 'single' and 'range' | `string` | `single` | +| viewMode `v2.7.10` | view mode | `day` \| `month` \| `quarter` | `day` | | popup | Whether to display the pop-up status | `boolean` | `true` | | autoBackfill | Automatic backfill | `boolean` | `false` | | title | show title | `string` | `日期选择` | diff --git a/src/packages/calendar/doc.md b/src/packages/calendar/doc.md index 4edee2c0d1..60fc40b984 100644 --- a/src/packages/calendar/doc.md +++ b/src/packages/calendar/doc.md @@ -106,6 +106,7 @@ import { Calendar } from '@nutui/nutui-react' | --- | --- | --- | --- | | visible | 是否可见 | `boolean` | `false` | | type | 类型,日期选择'single',区间选择'range' | `string` | `single` | +| viewMode `v2.7.10` | 面板模式,用于展示不一样的面板 | `day` \| `month` \| `quarter` | `day` | | popup | 是否弹窗状态展示 | `boolean` | `true` | | autoBackfill | 自动回填 | `boolean` | `false` | | title | 显示标题 | `string` | `日期选择` | diff --git a/src/packages/calendar/doc.taro.md b/src/packages/calendar/doc.taro.md index 659755d0b4..a6fee2c310 100644 --- a/src/packages/calendar/doc.taro.md +++ b/src/packages/calendar/doc.taro.md @@ -106,6 +106,7 @@ import { Calendar } from '@nutui/nutui-react-taro' | --- | --- | --- | --- | | visible | 是否可见 | `boolean` | `false` | | type | 类型,日期选择'single',区间选择'range' | `string` | `single` | +| viewMode `v2.7.10` | 面板模式,用于展示不一样的面板 | `day` \| `month` \| `quarter` | `day` | | popup | 是否弹窗状态展示 | `boolean` | `true` | | autoBackfill | 自动回填 | `boolean` | `false` | | title | 显示标题 | `string` | `日期选择` | diff --git a/src/packages/calendar/doc.zh-TW.md b/src/packages/calendar/doc.zh-TW.md index 86954b7a65..7032fe40c2 100644 --- a/src/packages/calendar/doc.zh-TW.md +++ b/src/packages/calendar/doc.zh-TW.md @@ -106,6 +106,7 @@ import { Calendar } from '@nutui/nutui-react' | --- | --- | --- | --- | | visible | 是否可見 | `boolean` | `false` | | type | 類型,日期選擇'single',區間選擇'range' | `string` | `single` | +| viewMode `v2.7.10` | 面板模式,用於展示不一樣的面板 | `day` \| `month` \| `quarter` | `day` | | popup | 是否彈窗狀態展示 | `boolean` | `true` | | autoBackfill | 自動回填 | `boolean` | `false` | | title | 顯示標題 | `string` | `日期選擇` | diff --git a/src/packages/calendar/types.ts b/src/packages/calendar/types.ts index 27d3a485c2..46352bf4f5 100644 --- a/src/packages/calendar/types.ts +++ b/src/packages/calendar/types.ts @@ -3,10 +3,25 @@ export interface CalendarDay { type: string } +export interface CalendarMonth { + year: number + month: number + yearAndMonth?: string + type?: string +} + +export interface CalendarQuarter { + year: number + quarter: number + yearAndQuarter?: string + type?: string +} + export interface CalendarMonthInfo { curData: string[] | string title: string monthData: CalendarDay[] + weekNo?: string[] cssHeight?: number scrollTop?: number } @@ -18,3 +33,19 @@ export type CalendarType = 'single' | 'range' | 'multiple' | 'week' export type CalendarRef = { scrollToDate: (date: string) => void } + +export type CalendarMonthInfoOfPanel = { + year: number + months: CalendarMonth[] + cssHeight: number + scrollTop: number + currYear: boolean +} + +export type CalendarQuarterInfoOfPanel = { + year: number + quarters: CalendarQuarter[] + cssHeight: number + scrollTop: number + currYear: boolean +} diff --git a/src/packages/calendar/utils.tsx b/src/packages/calendar/utils.tsx index a4d4bcf4d1..f34dfcbcd2 100644 --- a/src/packages/calendar/utils.tsx +++ b/src/packages/calendar/utils.tsx @@ -36,3 +36,126 @@ export const isEnd = (day: string, days: string[]) => { export const isStartAndEnd = (days: string[]) => { return days.length >= 2 && isEqual(days[0], days[1]) } + +/* + * 以下用于 view 模式 + */ + +// 获取当前年月前的月份 +export const getPreMonths = (type: string, year: number, month: number) => { + const preMonth = +month - 1 + const months = Array.from(Array(preMonth), (v, k) => { + return { + year, + month: k + 1, + yearAndMonth: formatMonth(year, k + 1), + type, + } + }) + return months +} + +// 获取当前年月后的月份 +export const getMonths = ( + type: string, + year: number, + month: number, + endMonth: number = 12 +) => { + const nextMonth = endMonth - month + 1 + const months = Array.from(Array(nextMonth), (v, k) => { + return { + year, + month: k + month, + yearAndMonth: formatMonth(year, k + month), + type, + } + }) + return months +} + +export const formatMonth = (year: number, month: number) => { + return `${year}-${String(month).padStart(2, '0')}` +} + +export const formatQuarter = (year: number, quarter: number) => { + return `${year}-Q${quarter}` +} + +/** + * 判断某年某月某日属于哪一个季度 + * @param year 年份 + * @param month 月份(1-12) + * @param day 日期(1-31) + * @returns 季度编号(1、2、3、4) + */ +export const getQuarter = (month: number): number => { + if (month < 1 || month > 12) { + throw new Error('月份必须在 1 到 12 之间') + } + // 计算季度 + const quarter = Math.floor((month - 1) / 3) + 1 + return quarter +} + +// 当前年份包含的季度 +export const getQuarters = ( + type: string, + year: number, + month: number, + endMonth: number = 12 +) => { + const quarters = [] + // 当前月区间数据所在的季度 + const startIndex = month // 从1开始计算 + const endIndex = endMonth + for (let index = startIndex; index <= endIndex; index += 3) { + const quarter = getQuarter(index) + quarters.push({ + year, + quarter, + yearAndQuarter: formatQuarter(year, quarter), + type, + }) + } + return quarters +} + +// 当前月之前的在当年的月所在的季度 +export const getPreQuarters = (type: string, year: number, month: number) => { + const quarters = [] + const startIndex = 1 // 从1开始计算 + const endIndex = month - 3 // 当前月份不能算进之前的季度,-3 判断。 + for (let index = startIndex; index <= endIndex; index += 3) { + const quarter = getQuarter(index) + quarters.push({ + year, + quarter, + yearAndQuarter: formatQuarter(year, quarter), + type, + }) + } + return quarters +} + +// 当前月之后的当年内的月所在的季度 +export const getNextQuarters = ( + type: string, + year: number, + month: number, + endMonth: number = 12 +) => { + const quarters = [] + const startIndex = month + 3 + const endIndex = endMonth + for (let index = startIndex; index <= endIndex; index += 3) { + const quarter = getQuarter(index) + quarters.push({ + year, + quarter, + yearAndQuarter: formatQuarter(year, quarter), + type, + }) + } + return quarters +} diff --git a/src/packages/calendaritem/calendaritem.taro.tsx b/src/packages/calendaritem/calendaritem.taro.tsx index 49feda0d5a..717e8cacaf 100644 --- a/src/packages/calendaritem/calendaritem.taro.tsx +++ b/src/packages/calendaritem/calendaritem.taro.tsx @@ -6,17 +6,20 @@ import { PopupProps } from '@/packages/popup/index.taro' import { ComponentDefaults } from '@/utils/typings' import { getDateString, - getCurrMonthData, - getDaysStatus, - getPreMonthDates, compareDate, getMonthDays, - getWeekDate, - formatResultDate, isEqual, getNumTwoBit, getWhatDay, } from '@/utils/date' +import { + getCurrMonthData, + getPreMonthDates, + getWeekDate, + formatResultDate, + getDaysStatus, + getWeekNosOfYear, +} from './utils' import requestAniFrame from '@/utils/raf' import { useConfig } from '@/packages/configprovider/configprovider.taro' import { usePropsValue } from '@/utils/use-props-value' @@ -59,6 +62,7 @@ export interface CalendarItemProps extends PopupProps { confirmText: ReactNode showTitle: boolean showSubTitle: boolean + showMonthNumber: boolean scrollAnimation: boolean firstDayOfWeek: number disableDate: (date: CalendarDay) => boolean @@ -86,6 +90,7 @@ const defaultProps = { confirmText: '', showTitle: true, showSubTitle: true, + showMonthNumber: false, scrollAnimation: true, firstDayOfWeek: 0, disableDate: (date: CalendarDay) => false, @@ -123,14 +128,15 @@ export const CalendarItem = React.forwardRef< confirmText, showTitle, showSubTitle, + showMonthNumber, scrollAnimation, firstDayOfWeek, disableDate, renderHeaderButtons, + renderBottomButton, renderDay, renderDayTop, renderDayBottom, - renderBottomButton, onConfirm, onUpdate, onDayClick, @@ -173,7 +179,7 @@ export const CalendarItem = React.forwardRef< ? ([...(defaultValue as string[])] as string[]) : (defaultValue as string[]) } - return undefined + return type === 'single' ? '' : [] } const [currentDate, setCurrentDate] = usePropsValue({ @@ -211,6 +217,7 @@ export const CalendarItem = React.forwardRef< const monthInfo = { curData: date, title: monthTitle(y, m), + weekNo: getWeekNosOfYear(y, m, firstDayOfWeek), monthData: days, cssHeight, scrollTop, @@ -236,7 +243,7 @@ export const CalendarItem = React.forwardRef< const currentMonthsData = monthsData[current] if (currentMonthsData.title === yearMonthTitle) return const [year, month] = currentMonthsData.curData - onPageChange && onPageChange([year, month, `${year}-${month}`]) + onPageChange?.([year, month, `${year}-${month}`]) setYearMonthTitle(currentMonthsData.title) } @@ -464,7 +471,7 @@ export const CalendarItem = React.forwardRef< initData() } useEffect(() => { - setCurrentDate(resetDefaultValue() || []) + setCurrentDate(resetDefaultValue()) }, [defaultValue]) useEffect(() => { @@ -647,7 +654,7 @@ export const CalendarItem = React.forwardRef< } } if (!isFirst) { - onDayClick && onDayClick(state.currDateArray) + onDayClick?.(state.currDateArray) if (autoBackfill || !popup) { confirm() } @@ -661,9 +668,9 @@ export const CalendarItem = React.forwardRef< type !== 'range' ) { const chooseData = state.currDateArray.slice(0) - onConfirm && onConfirm(chooseData) + onConfirm?.(chooseData) if (popup) { - onUpdate && onUpdate() + onUpdate?.() } } } @@ -717,7 +724,10 @@ export const CalendarItem = React.forwardRef< {showSubTitle && (
{yearMonthTitle}
)} -
+
{weeks.map((item: string) => (
{item} @@ -752,9 +762,9 @@ export const CalendarItem = React.forwardRef<
)} {noStartNorEnd && - isCurrDay(month, day.day) && !renderDayBottom && - showToday && ( + showToday && + isCurrDay(month, day.day) && (
{locale.calendaritem.today}
@@ -783,10 +793,21 @@ export const CalendarItem = React.forwardRef< return (
{month.title}
-
- {month.monthData.map((day: CalendarDay, i: number) => - renderItem(month, day, i) +
+ {showMonthNumber && ( +
+ {month.weekNo.map((item: string, index: number) => ( +
+ {item} +
+ ))} +
)} +
+ {month.monthData.map((day: CalendarDay, i: number) => + renderItem(month, day, i) + )} +
) @@ -805,7 +826,6 @@ export const CalendarItem = React.forwardRef< >
diff --git a/src/packages/calendaritem/calendaritem.tsx b/src/packages/calendaritem/calendaritem.tsx index 788f197562..30cdfa2e95 100644 --- a/src/packages/calendaritem/calendaritem.tsx +++ b/src/packages/calendaritem/calendaritem.tsx @@ -5,17 +5,20 @@ import { PopupProps } from '@/packages/popup/index' import { ComponentDefaults } from '@/utils/typings' import { getDateString, - getCurrMonthData, - getDaysStatus, - getPreMonthDates, compareDate, getMonthDays, - getWeekDate, - formatResultDate, isEqual, getNumTwoBit, getWhatDay, } from '@/utils/date' +import { + getCurrMonthData, + getPreMonthDates, + getWeekDate, + formatResultDate, + getDaysStatus, + getWeekNosOfYear, +} from './utils' import requestAniFrame from '@/utils/raf' import { useConfig } from '@/packages/configprovider' import { usePropsValue } from '@/utils/use-props-value' @@ -58,6 +61,7 @@ export interface CalendarItemProps extends PopupProps { confirmText: ReactNode showTitle: boolean showSubTitle: boolean + showMonthNumber: boolean scrollAnimation: boolean firstDayOfWeek: number disableDate: (date: CalendarDay) => boolean @@ -85,6 +89,7 @@ const defaultProps = { confirmText: '', showTitle: true, showSubTitle: true, + showMonthNumber: false, scrollAnimation: true, firstDayOfWeek: 0, disableDate: (date: CalendarDay) => false, @@ -111,6 +116,7 @@ export const CalendarItem = React.forwardRef< type, autoBackfill, title, + value, defaultValue, startDate, endDate, @@ -120,6 +126,7 @@ export const CalendarItem = React.forwardRef< confirmText, showTitle, showSubTitle, + showMonthNumber, scrollAnimation, firstDayOfWeek, disableDate, @@ -128,7 +135,6 @@ export const CalendarItem = React.forwardRef< renderDay, renderDayTop, renderDayBottom, - value, onConfirm, onUpdate, onDayClick, @@ -143,7 +149,6 @@ export const CalendarItem = React.forwardRef< ...weekdays.slice(firstDayOfWeek, 7), ...weekdays.slice(0, firstDayOfWeek), ] - const monthTitle = locale.calendaritem.monthTitle const [yearMonthTitle, setYearMonthTitle] = useState('') const [monthsData, setMonthsData] = useState([]) @@ -199,15 +204,16 @@ export const CalendarItem = React.forwardRef< ...getPreMonthDates('prev', y, m, firstDayOfWeek), ...getDaysStatus('active', y, m), ] as CalendarDay[] - const cssHeight = 39 + (days.length > 35 ? 384 : 320) let scrollTop = 0 if (monthData.length > 0) { const monthEle = monthData[monthData.length - 1] scrollTop = monthEle.scrollTop + monthEle.cssHeight } - const monthInfo: CalendarMonthInfo = { + const cssHeight = 39 + (days.length > 35 ? 384 : 320) + const monthInfo = { curData: date, title: monthTitle(y, m), + weekNo: getWeekNosOfYear(y, m, firstDayOfWeek), monthData: days, cssHeight, scrollTop, @@ -233,7 +239,7 @@ export const CalendarItem = React.forwardRef< const currentMonthsData = monthsData[current] if (currentMonthsData.title === yearMonthTitle) return const [year, month] = currentMonthsData.curData - onPageChange && onPageChange([year, month, `${year}-${month}`]) + onPageChange?.([year, month, `${year}-${month}`]) setYearMonthTitle(currentMonthsData.title) } @@ -276,7 +282,6 @@ export const CalendarItem = React.forwardRef< } return defaultData } - if (Array.isArray(currentDate) && currentDate.length) { // 日期转化为数组,限制初始日期。判断时间范围 if (type === 'range') { @@ -349,9 +354,7 @@ export const CalendarItem = React.forwardRef< const index = monthsData.findIndex((item) => { return +item.curData[0] === year && +item.curData[1] === month }) - if (index > -1) { - current = index - } + if (index > -1) current = index } return { current, @@ -399,7 +402,6 @@ export const CalendarItem = React.forwardRef< const requestAniFrameFunc = (current: number, monthNum: number) => { const lastItem = monthsData[monthsData.length - 1] const containerHeight = lastItem.cssHeight + lastItem.scrollTop - requestAniFrame(() => { // 初始化 日历位置 if (monthsRef && monthsPanel && viewAreaRef) { @@ -414,12 +416,8 @@ export const CalendarItem = React.forwardRef< const getMonthNum = () => { let monthNum = Number(endDates[1]) - Number(startDates[1]) const yearNum = Number(endDates[0]) - Number(startDates[0]) - if (yearNum > 0) { - monthNum += 12 * yearNum - } - if (monthNum <= 0) { - monthNum = 1 - } + if (yearNum > 0) monthNum += 12 * yearNum + if (monthNum <= 0) monthNum = 1 setMonthsNum(monthNum) return monthNum } @@ -449,7 +447,6 @@ export const CalendarItem = React.forwardRef< monthsData.splice(0) initData() } - useEffect(() => { setCurrentDate(resetDefaultValue()) }, [defaultValue]) @@ -627,7 +624,7 @@ export const CalendarItem = React.forwardRef< } if (!isFirst) { - onDayClick && onDayClick(state.currDateArray) + onDayClick?.(state.currDateArray) if (autoBackfill || !popup) { confirm() } @@ -641,9 +638,9 @@ export const CalendarItem = React.forwardRef< type !== 'range' ) { const chooseData = state.currDateArray.slice(0) - onConfirm && onConfirm(chooseData) + onConfirm?.(chooseData) if (popup) { - onUpdate && onUpdate() + onUpdate?.() } } } @@ -697,7 +694,10 @@ export const CalendarItem = React.forwardRef< {showSubTitle && (
{yearMonthTitle}
)} -
+
{weeks.map((item: string) => (
{item} @@ -714,7 +714,7 @@ export const CalendarItem = React.forwardRef< const noStartNorEnd = !startTip && !endTip return (
handleDayClick(day, month, false)} key={index} > @@ -741,11 +741,11 @@ export const CalendarItem = React.forwardRef< )} {startTip && (
{startText || locale.calendaritem.start}
@@ -763,10 +763,21 @@ export const CalendarItem = React.forwardRef< return (
{month.title}
-
- {month.monthData.map((day: CalendarDay, i: number) => - renderItem(month, day, i) +
+ {showMonthNumber && ( +
+ {month.weekNo.map((item: string, index: number) => ( +
+ {item} +
+ ))} +
)} +
+ {month.monthData.map((day: CalendarDay, i: number) => + renderItem(month, day, i) + )} +
) @@ -781,7 +792,6 @@ export const CalendarItem = React.forwardRef< >
diff --git a/src/packages/calendaritem/utils.ts b/src/packages/calendaritem/utils.ts new file mode 100644 index 0000000000..9f0996d391 --- /dev/null +++ b/src/packages/calendaritem/utils.ts @@ -0,0 +1,177 @@ +import { + getNumTwoBit, + getMonthDays, + date2Str, + getWhatDay, + getMonthPreDay, +} from '@/utils/date' + +// 获取当前月数据 +export const getCurrMonthData = (type: string, year: number, month: number) => { + if (type === 'prev') { + month === 1 && (year -= 1) + month = month === 1 ? 12 : --month + } else if (type === 'next') { + month === 12 && (year += 1) + month = month === 12 ? 1 : ++month + } + return [year, getNumTwoBit(month), getMonthDays(String(year), String(month))] +} + +// 获取日期状态 +export const getDaysStatus = (type: string, year: number, month: number) => { + let days = getMonthDays(`${year}`, `${month}`) + // 修复:当某个月的1号是周日时,月份下方会空出来一行 + if (type === 'prev' && days >= 7) { + days -= 7 + } + return Array.from(Array(days), (v, k) => { + return { + day: k + 1, + type, + year, + month, + } + }) +} + +// 获取上一个月的最后一周天数,填充当月空白 +export const getPreMonthDates = ( + type: string, + year: number, + month: number, + firstDayOfWeek: number +) => { + let preMonth = +month - 1 + let preYear = year + if (preMonth <= 0) { + preMonth = 12 + preYear += 1 + } + let days = getMonthPreDay(+year, +month) + days -= firstDayOfWeek + // 修复:当某个月的1号是周日时,月份下方会空出来一行 + if (type === 'prev' && days >= 7) { + days -= 7 + } + + const preDates = getMonthDays(`${preYear}`, `${preMonth}`) + const months = Array.from(Array(preDates), (v, k) => { + return { + day: k + 1, + type, + preYear, + preMonth, + } + }) + return months.slice(preDates - days) +} + +export const getWeekDate = ( + year: string, + month: string, + date: string, + firstDayOfWeek = 0 +): string[] => { + const dateNow = new Date(Number(year), parseInt(month) - 1, Number(date)) + const nowTime = dateNow.getTime() + let day = dateNow.getDay() + if (firstDayOfWeek === 0) { + const oneDayTime = 24 * 60 * 60 * 1000 + // 显示周日 + const SundayTime = nowTime - day * oneDayTime // 本周的周日 + // 显示周六 + const SaturdayTime = nowTime + (6 - day) * oneDayTime // 本周的周六 + const sunday = date2Str(new Date(SundayTime)) + const saturday = date2Str(new Date(SaturdayTime)) + return [sunday, saturday] + } + day = day === 0 ? 7 : day + const oneDayTime = 24 * 60 * 60 * 1000 + // 显示周一 + const MondayTime = nowTime - (day - 1) * oneDayTime // 本周的周一 + // 显示周日 + const SundayTime = nowTime + (7 - day) * oneDayTime // 本周的周日 + const monday = date2Str(new Date(MondayTime)) + const sunday = date2Str(new Date(SundayTime)) + return [monday, sunday] +} + +export const formatResultDate = (date: string) => { + const [year, month, day] = [...date.split('-')] + const formatterDay = getNumTwoBit(Number(day)) + const formatterDate = `${year}-${month}-${day}` + const dayOfWeek = getWhatDay(Number(year), Number(month), Number(day)) + return [year, month, formatterDay, formatterDate, dayOfWeek] +} + +// 获取某一天所在的周,按国标 8601 处理。第一周至少包含4天。 +export const getWeekOfYearByYMD = ( + year: number, + month: number, // 自然月 + date: number, + firstDayOfWeek = 0 +): number => { + // 一天的秒 + const MILLISECONDS_PER_DAY = 86400000 + + const dateNow = new Date(year, month - 1, date) + // 一年内第一天 + const dateFirst = new Date(year, 0, 1) + // 一年内第几天 + const dayOfYear = Math.round( + (dateNow.valueOf() - dateFirst.valueOf()) / MILLISECONDS_PER_DAY + ) + + // ISO 8601 标准:第一个星期至少包含 4 天 + // 同时,需要兼顾 firstDayOfWeek 一起判断 + const DAYS_OF_FIRST_WEEK = 3 + let dayOfWeek = dateNow.getDay() + let remainder = 6 - dayOfWeek - DAYS_OF_FIRST_WEEK + if (firstDayOfWeek !== 0) { + dayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek // 周日转化为 7 + remainder = 7 - dayOfWeek - DAYS_OF_FIRST_WEEK + } + + // 周号可以出现0,标识当前1月1日不在本年度的第一周,为上一年的最后一周。 + let weekNo = Math.ceil((dayOfYear + remainder + 1) / 7) + // 需要处理第一周为0的的情况,需延续上一年的周数,上一年有可能是53或52。 + // 需要判断最后一周为53的情况,因为一年至少有52周,但是会有53周的情况,这时需要判断: + // 如果最后一周少于4天,则为下一年的第一周。 + if (weekNo === 0) { + // 测试:2010/2011/2012/2017/2023/ + weekNo = getWeekOfYearByYMD(year - 1, 12, 31, firstDayOfWeek) + } else if (weekNo === 53) { + // 测试:2024/2021 + const remainder2 = 7 - dayOfWeek - DAYS_OF_FIRST_WEEK + weekNo = remainder2 > 0 ? 1 : weekNo + } + + return weekNo +} + +export const getWeekNosOfYear = ( + year: number, + month: number, + firstDayOfWeek: number +) => { + const startWeekNo = getWeekOfYearByYMD(year, month, 1, firstDayOfWeek) + const endWeekNo = getWeekOfYearByYMD( + year, + month, + getMonthDays(`${year}`, `${month}`), + firstDayOfWeek + ) + return Array.from( + { + length: + (endWeekNo === 1 ? 53 : endWeekNo) - + (startWeekNo === 53 || startWeekNo === 52 ? 0 : startWeekNo) + + 1, + }, + (_, i) => { + const lastIndex = (endWeekNo === 1 ? 53 : endWeekNo) - startWeekNo + return `${endWeekNo === 1 && i === lastIndex ? 1 : ((startWeekNo === 53 || startWeekNo === 52) && i !== 0 ? 0 : startWeekNo) + i}` + } + ) +} diff --git a/src/packages/configprovider/types.ts b/src/packages/configprovider/types.ts index 7afac92431..f80e2c26f4 100644 --- a/src/packages/configprovider/types.ts +++ b/src/packages/configprovider/types.ts @@ -317,6 +317,7 @@ export type NutCSSVariables = | 'nutuiCalendarDayHeight' | 'nutuiCalendarDayFontWeight' | 'nutuiCalendarDayActiveBorderRadius' + | 'nutuiCalendarPanelItemWidth' | 'nutuiOverlayBgColor' | 'nutuiOverlayZIndex' | 'nutuiOverlayContentBgColor' diff --git a/src/styles/variables.scss b/src/styles/variables.scss index e8964178fd..84e5ae1574 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -944,6 +944,10 @@ $calendar-day-active-border-radius: var( --nutui-calendar-day-active-border-radius, 4px ) !default; +$calendar-panel-item-width: var( + --nutui-calendar-panel-item-width, + 24.85% +) !default; //overlay(✅) $overlay-bg-color: var(--nutui-overlay-bg-color, $color-mask) !default; diff --git a/src/utils/date.ts b/src/utils/date.ts index c594246240..1993104fe8 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -117,130 +117,3 @@ export const isEqual = (date1: string, date2: string): boolean => { const endTime = new Date(date2.replace(/-/g, '/')).getTime() return startTime === endTime } - -export const getMonthWeek = ( - year: number, - month: number, - date: number, - firstDayOfWeek = 0 -): number => { - const dateNow = new Date(year, month - 1, date) - let w = dateNow.getDay() // 星期数 - let remainder = 6 - w - if (firstDayOfWeek !== 0) { - w = w === 0 ? 7 : w - remainder = 7 - w - } - return Math.ceil((date + remainder) / 7) -} -export const getYearWeek = ( - year: number, - month: number, - date: number, - firstDayOfWeek = 0 -): number => { - const dateNow = new Date(year, month - 1, date) - const dateFirst = new Date(year, 0, 1) - const dataNumber = Math.round( - (dateNow.valueOf() - dateFirst.valueOf()) / 86400000 - ) - return Math.ceil((dataNumber + (dateFirst.getDay() + 1 - 1)) / 7) -} -export const getWeekDate = ( - year: string, - month: string, - date: string, - firstDayOfWeek = 0 -): string[] => { - const dateNow = new Date(Number(year), parseInt(month) - 1, Number(date)) - const nowTime = dateNow.getTime() - let day = dateNow.getDay() - if (firstDayOfWeek === 0) { - const oneDayTime = 24 * 60 * 60 * 1000 - // 显示周日 - const SundayTime = nowTime - day * oneDayTime // 本周的周日 - // 显示周六 - const SaturdayTime = nowTime + (6 - day) * oneDayTime // 本周的周六 - const sunday = date2Str(new Date(SundayTime)) - const saturday = date2Str(new Date(SaturdayTime)) - return [sunday, saturday] - } - day = day === 0 ? 7 : day - const oneDayTime = 24 * 60 * 60 * 1000 - // 显示周一 - const MondayTime = nowTime - (day - 1) * oneDayTime // 本周的周一 - // 显示周日 - const SundayTime = nowTime + (7 - day) * oneDayTime // 本周的周日 - const monday = date2Str(new Date(MondayTime)) - const sunday = date2Str(new Date(SundayTime)) - return [monday, sunday] -} - -export const formatResultDate = (date: string) => { - const [year, month, day] = [...date.split('-')] - const formatterDay = getNumTwoBit(Number(day)) - const formatterDate = `${year}-${month}-${day}` - const dayOfWeek = getWhatDay(Number(year), Number(month), Number(day)) - return [year, month, formatterDay, formatterDate, dayOfWeek] -} - -// 获取当前月数据 -export const getCurrMonthData = (type: string, year: number, month: number) => { - if (type === 'prev') { - month === 1 && (year -= 1) - month = month === 1 ? 12 : --month - } else if (type === 'next') { - month === 12 && (year += 1) - month = month === 12 ? 1 : ++month - } - return [year, getNumTwoBit(month), getMonthDays(String(year), String(month))] -} - -// 获取日期状态 -export const getDaysStatus = (type: string, year: number, month: number) => { - let days = getMonthDays(`${year}`, `${month}`) - // 修复:当某个月的1号是周日时,月份下方会空出来一行 - if (type === 'prev' && days >= 7) { - days -= 7 - } - return Array.from(Array(days), (v, k) => { - return { - day: k + 1, - type, - year, - month, - } - }) -} - -// 获取上一个月的最后一周天数,填充当月空白 -export const getPreMonthDates = ( - type: string, - year: number, - month: number, - firstDayOfWeek: number -) => { - let preMonth = +month - 1 - let preYear = year - if (preMonth <= 0) { - preMonth = 12 - preYear += 1 - } - let days = getMonthPreDay(+year, +month) - days -= firstDayOfWeek - // 修复:当某个月的1号是周日时,月份下方会空出来一行 - if (type === 'prev' && days >= 7) { - days -= 7 - } - - const preDates = getMonthDays(`${preYear}`, `${preMonth}`) - const months = Array.from(Array(preDates), (v, k) => { - return { - day: k + 1, - type, - preYear, - preMonth, - } - }) - return months.slice(preDates - days) -}