From 69f1aed68d0f52e63339ccdad47212695f7350b5 Mon Sep 17 00:00:00 2001 From: LLmoskk <1398145450@qq.com> Date: Fri, 25 Oct 2024 16:22:48 +0800 Subject: [PATCH] feat: calculate virtual scroll height and organize code --- .../calendar-picker-view.tsx | 396 +++++++++++------- 1 file changed, 238 insertions(+), 158 deletions(-) diff --git a/src/components/calendar-picker-view/calendar-picker-view.tsx b/src/components/calendar-picker-view/calendar-picker-view.tsx index 887bb1adda..a4990a0877 100644 --- a/src/components/calendar-picker-view/calendar-picker-view.tsx +++ b/src/components/calendar-picker-view/calendar-picker-view.tsx @@ -88,6 +88,16 @@ type RowProps = { style: React.CSSProperties } +type Cell = { + year: number + month: number + daysInMonth: number + monthIterator: dayjs.Dayjs +} + +// default 每个月的高度是 344px +const defaultMonthHeight = 344 + export const CalendarPickerView = forwardRef< CalendarPickerViewRef, CalendarPickerViewProps @@ -97,6 +107,12 @@ export const CalendarPickerView = forwardRef< const props = mergeProps(defaultProps, p) const { locale } = useConfig() + // 修改为虚拟滚动后不能固定高度, 所以需要创建用户自定义日历后的月份高度来确定虚拟滚动 List 的高度 + const [monthPlaceholderHeight, setMonthPlaceholderHeight] = + useState(defaultMonthHeight) + const [isPlaceholderRendered, setIsPlaceholderRendered] = useState(true) + const monthPlaceholderRef = useRef(null) + const markItems = [...locale.Calendar.markItems] if (props.weekStartsOn === 'Sunday') { const item = markItems.pop() @@ -156,6 +172,17 @@ export const CalendarPickerView = forwardRef< } }, [dateRange]) + // 用来获取计算用户自定义日历的月份高度 获取到后隐藏掉这个基础款月份 dom + useEffect(() => { + if (monthPlaceholderRef.current) { + const calculatedHeight = monthPlaceholderRef.current.offsetHeight + if (calculatedHeight) { + setMonthPlaceholderHeight(calculatedHeight) + } + setIsPlaceholderRendered(false) + } + }, []) + const maxDay = useMemo( () => (props.max ? dayjs(props.max) : defaultMax), [props.max, defaultMax] @@ -198,16 +225,199 @@ export const CalendarPickerView = forwardRef< ) + // =============================== Render Month Body =============================== + + const renderMonthTitle = (year: number, month: number) => { + const renderMap = { year, month } + return locale.Calendar.yearAndMonth?.replace( + /\${(.*?)}/g, + (_, variable: keyof typeof renderMap) => { + return renderMap[variable]?.toString() + } + ) + } + + const getPresetEmptyCells = (monthIterator: dayjs.Dayjs) => { + const presetEmptyCellCount = + props.weekStartsOn === 'Monday' + ? monthIterator.date(1).isoWeekday() - 1 + : monthIterator.date(1).isoWeekday() + + return presetEmptyCellCount === 7 + ? null + : Array.from({ length: presetEmptyCellCount }).map((_, index) => ( +
+ )) + } + + const getDateStatus = (d: dayjs.Dayjs, cells: Cell[]) => { + let isSelect = false + let isBegin = false + let isEnd = false + let isSelectRowBegin = false + let isSelectRowEnd = false + if (dateRange) { + const [begin, end] = dateRange + isBegin = d.isSame(begin, 'day') + isEnd = d.isSame(end, 'day') + isSelect = + isBegin || isEnd || (d.isAfter(begin, 'day') && d.isBefore(end, 'day')) + if (isSelect) { + isSelectRowBegin = + (cells.length % 7 === 0 || d.isSame(d.startOf('month'), 'day')) && + !isBegin + isSelectRowEnd = + (cells.length % 7 === 6 || d.isSame(d.endOf('month'), 'day')) && + !isEnd + } + } + const disabled = props.shouldDisableDate + ? props.shouldDisableDate(d.toDate()) + : (maxDay && d.isAfter(maxDay, 'day')) || + (minDay && d.isBefore(minDay, 'day')) + + return { + isSelect, + isBegin, + isEnd, + isSelectRowBegin, + isSelectRowEnd, + disabled, + } + } + + const handleDateClick = (d: dayjs.Dayjs, disabled: boolean) => { + if (!props.selectionMode) return + if (disabled) return + const date = d.toDate() + function shouldClear() { + if (!props.allowClear) return false + if (!dateRange) return false + const [begin, end] = dateRange + return d.isSame(begin, 'date') && d.isSame(end, 'day') + } + if (props.selectionMode === 'single') { + if (props.allowClear && shouldClear()) { + onDateChange(null) + return + } + onDateChange([date, date]) + } else if (props.selectionMode === 'range') { + if (!dateRange) { + onDateChange([date, date]) + setIntermediate(true) + return + } + if (shouldClear()) { + onDateChange(null) + setIntermediate(false) + return + } + if (intermediate) { + const another = dateRange[0] + onDateChange(another > date ? [date, another] : [another, date]) + setIntermediate(false) + } else { + onDateChange([date, date]) + setIntermediate(true) + } + } + } + + const renderDays = ( + monthIterator: dayjs.Dayjs, + daysInMonth: number, + cells: Cell[] + ) => { + return Array.from({ length: daysInMonth }).map((_, index) => { + const d = monthIterator.date(index + 1) + + const { + isSelect, + isBegin, + isEnd, + isSelectRowBegin, + isSelectRowEnd, + disabled, + } = getDateStatus(d, cells) + + const renderTop = () => { + const top = props.renderTop?.(d.toDate()) + + if (top) { + return top + } + + if (props.selectionMode === 'range') { + if (isBegin) { + return locale.Calendar.start + } + + if (isEnd) { + return locale.Calendar.end + } + } + + if (d.isSame(today, 'day') && !isSelect) { + return locale.Calendar.today + } + } + + return ( +
{ + handleDateClick(d, disabled) + }} + > +
{renderTop()}
+
+ {props.renderDate ? props.renderDate(d.toDate()) : d.date()} +
+
+ {props.renderBottom?.(d.toDate())} +
+
+ ) + }) + } + + // 获取用户自定义后的月份面板高度 + const renderPlaceholderMonths = () => { + const monthIterator = minDay.clone() + const year = monthIterator.year() + const month = monthIterator.month() + 1 + const daysInMonth = monthIterator.daysInMonth() + const presetEmptyCells = getPresetEmptyCells(monthIterator) + + return ( +
+
+ {renderMonthTitle(year, month)} +
+
+ {/* 空格填充 */} + {presetEmptyCells} + {/* 遍历每月 */} + {renderDays(monthIterator, daysInMonth, [])} +
+
+ ) + } + function renderBody() { const totalMonths = Math.ceil(maxDay.diff(minDay, 'months', true)) - // default 每个月的高度是 344px - const monthHeight = 344 - const cells: { - year: number - month: number - daysInMonth: number - monthIterator: dayjs.Dayjs - }[] = [] + + const cells: Cell[] = [] let monthIterator = minDay.clone() while (monthIterator.isSameOrBefore(maxDay, 'month')) { @@ -222,176 +432,46 @@ export const CalendarPickerView = forwardRef< const Row = ({ index, style }: RowProps) => { const { year, month, daysInMonth, monthIterator } = cells[index] - const renderMap = { - year, - month, - } - const yearMonth = `${year}-${month}` // 获取需要预先填充的空格,如果是 7 天则不需要填充 - const presetEmptyCellCount = - props.weekStartsOn === 'Monday' - ? monthIterator.date(1).isoWeekday() - 1 - : monthIterator.date(1).isoWeekday() - - const presetEmptyCells = - presetEmptyCellCount == 7 - ? null - : Array.from({ length: presetEmptyCellCount }).map((_, index) => ( -
- )) + const presetEmptyCells = getPresetEmptyCells(monthIterator) return (
- {locale.Calendar.yearAndMonth?.replace( - /\${(.*?)}/g, - (_, variable: keyof typeof renderMap) => { - return renderMap[variable]?.toString() - } - )} + {renderMonthTitle(year, month)}
{/* 空格填充 */} {presetEmptyCells} {/* 遍历每月 */} - {Array.from({ length: daysInMonth }).map((_, index) => { - const d = monthIterator.date(index + 1) - let isSelect = false - let isBegin = false - let isEnd = false - let isSelectRowBegin = false - let isSelectRowEnd = false - if (dateRange) { - const [begin, end] = dateRange - isBegin = d.isSame(begin, 'day') - isEnd = d.isSame(end, 'day') - isSelect = - isBegin || - isEnd || - (d.isAfter(begin, 'day') && d.isBefore(end, 'day')) - if (isSelect) { - isSelectRowBegin = - (cells.length % 7 === 0 || - d.isSame(d.startOf('month'), 'day')) && - !isBegin - isSelectRowEnd = - (cells.length % 7 === 6 || - d.isSame(d.endOf('month'), 'day')) && - !isEnd - } - } - const disabled = props.shouldDisableDate - ? props.shouldDisableDate(d.toDate()) - : (maxDay && d.isAfter(maxDay, 'day')) || - (minDay && d.isBefore(minDay, 'day')) - - const renderTop = () => { - const top = props.renderTop?.(d.toDate()) - - if (top) { - return top - } - - if (props.selectionMode === 'range') { - if (isBegin) { - return locale.Calendar.start - } - - if (isEnd) { - return locale.Calendar.end - } - } - - if (d.isSame(today, 'day') && !isSelect) { - return locale.Calendar.today - } - } - return ( -
{ - if (!props.selectionMode) return - if (disabled) return - const date = d.toDate() - function shouldClear() { - if (!props.allowClear) return false - if (!dateRange) return false - const [begin, end] = dateRange - return d.isSame(begin, 'date') && d.isSame(end, 'day') - } - if (props.selectionMode === 'single') { - if (props.allowClear && shouldClear()) { - onDateChange(null) - return - } - onDateChange([date, date]) - } else if (props.selectionMode === 'range') { - if (!dateRange) { - onDateChange([date, date]) - setIntermediate(true) - return - } - if (shouldClear()) { - onDateChange(null) - setIntermediate(false) - return - } - if (intermediate) { - const another = dateRange[0] - onDateChange( - another > date ? [date, another] : [another, date] - ) - setIntermediate(false) - } else { - onDateChange([date, date]) - setIntermediate(true) - } - } - }} - > -
{renderTop()}
-
- {props.renderDate ? props.renderDate(d.toDate()) : d.date()} -
-
- {props.renderBottom?.(d.toDate())} -
-
- ) - })} + {renderDays(monthIterator, daysInMonth, cells)}
) } return ( - - {({ height, width }) => ( - - {Row} - - )} - + <> + {isPlaceholderRendered && renderPlaceholderMonths()} + + {({ height, width }) => ( + + {Row} + + )} + + ) } + const body = (
{renderBody()}