mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-19 16:19:12 +08:00
feat: refactor date-and-time-picker to use custom dayjs utility and add timezone support (#15101)
This commit is contained in:
parent
6101733232
commit
5be8fbab56
@ -1,7 +1,7 @@
|
|||||||
import React, { type FC } from 'react'
|
import React, { type FC } from 'react'
|
||||||
import type { CalendarItemProps } from '../types'
|
import type { CalendarItemProps } from '../types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from '../utils/dayjs'
|
||||||
|
|
||||||
const Item: FC<CalendarItemProps> = ({
|
const Item: FC<CalendarItemProps> = ({
|
||||||
day,
|
day,
|
||||||
|
@ -22,18 +22,18 @@ const Header: FC<DatePickerHeaderProps> = ({
|
|||||||
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary' />
|
<RiArrowDownSLine className='w-4 h-4 text-text-tertiary' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={onClickNextMonth}
|
|
||||||
className='p-1.5 hover:bg-state-base-hover rounded-lg'
|
|
||||||
>
|
|
||||||
<RiArrowDownSLine className='w-[18px] h-[18px] text-text-secondary' />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={onClickPrevMonth}
|
onClick={onClickPrevMonth}
|
||||||
className='p-1.5 hover:bg-state-base-hover rounded-lg'
|
className='p-1.5 hover:bg-state-base-hover rounded-lg'
|
||||||
>
|
>
|
||||||
<RiArrowUpSLine className='w-[18px] h-[18px] text-text-secondary' />
|
<RiArrowUpSLine className='w-[18px] h-[18px] text-text-secondary' />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClickNextMonth}
|
||||||
|
className='p-1.5 hover:bg-state-base-hover rounded-lg'
|
||||||
|
>
|
||||||
|
<RiArrowDownSLine className='w-[18px] h-[18px] text-text-secondary' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import dayjs, { type Dayjs } from 'dayjs'
|
|
||||||
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
|
import { RiCalendarLine, RiCloseCircleFill } from '@remixicon/react'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { DatePickerProps, Period } from '../types'
|
import type { DatePickerProps, Period } from '../types'
|
||||||
import { ViewType } from '../types'
|
import { ViewType } from '../types'
|
||||||
import { cloneTime, getDaysInMonth, getHourIn12Hour } from '../utils'
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs, {
|
||||||
|
clearMonthMapCache,
|
||||||
|
cloneTime,
|
||||||
|
getDateWithTimezone,
|
||||||
|
getDaysInMonth,
|
||||||
|
getHourIn12Hour,
|
||||||
|
} from '../utils/dayjs'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
@ -22,6 +28,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const DatePicker = ({
|
const DatePicker = ({
|
||||||
value,
|
value,
|
||||||
|
timezone,
|
||||||
onChange,
|
onChange,
|
||||||
onClear,
|
onClear,
|
||||||
placeholder,
|
placeholder,
|
||||||
@ -32,12 +39,15 @@ const DatePicker = ({
|
|||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [view, setView] = useState(ViewType.date)
|
const [view, setView] = useState(ViewType.date)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const isInitial = useRef(true)
|
||||||
|
const inputValue = useRef(value ? value.tz(timezone) : undefined).current
|
||||||
|
const defaultValue = useRef(getDateWithTimezone({ timezone })).current
|
||||||
|
|
||||||
const [currentDate, setCurrentDate] = useState(value || dayjs())
|
const [currentDate, setCurrentDate] = useState(inputValue || defaultValue)
|
||||||
const [selectedDate, setSelectedDate] = useState(value)
|
const [selectedDate, setSelectedDate] = useState(inputValue)
|
||||||
|
|
||||||
const [selectedMonth, setSelectedMonth] = useState((value || dayjs()).month())
|
const [selectedMonth, setSelectedMonth] = useState((inputValue || defaultValue).month())
|
||||||
const [selectedYear, setSelectedYear] = useState((value || dayjs()).year())
|
const [selectedYear, setSelectedYear] = useState((inputValue || defaultValue).year())
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@ -50,6 +60,25 @@ const DatePicker = ({
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitial.current) {
|
||||||
|
isInitial.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearMonthMapCache()
|
||||||
|
if (value) {
|
||||||
|
const newValue = getDateWithTimezone({ date: value, timezone })
|
||||||
|
setCurrentDate(newValue)
|
||||||
|
setSelectedDate(newValue)
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setCurrentDate(prev => getDateWithTimezone({ date: prev, timezone }))
|
||||||
|
setSelectedDate(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [timezone])
|
||||||
|
|
||||||
const handleClickTrigger = (e: React.MouseEvent) => {
|
const handleClickTrigger = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -58,15 +87,15 @@ const DatePicker = ({
|
|||||||
}
|
}
|
||||||
setView(ViewType.date)
|
setView(ViewType.date)
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
|
if (value) {
|
||||||
|
setCurrentDate(value)
|
||||||
|
setSelectedDate(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClear = (e: React.MouseEvent) => {
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
const newDate = dayjs()
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setSelectedDate(undefined)
|
setSelectedDate(undefined)
|
||||||
setCurrentDate(prev => prev || newDate)
|
|
||||||
setSelectedMonth(prev => prev || newDate.month())
|
|
||||||
setSelectedYear(prev => prev || newDate.year())
|
|
||||||
if (!isOpen)
|
if (!isOpen)
|
||||||
onClear()
|
onClear()
|
||||||
}
|
}
|
||||||
@ -84,13 +113,13 @@ const DatePicker = ({
|
|||||||
}, [currentDate])
|
}, [currentDate])
|
||||||
|
|
||||||
const handleDateSelect = useCallback((day: Dayjs) => {
|
const handleDateSelect = useCallback((day: Dayjs) => {
|
||||||
const newDate = cloneTime(day, selectedDate || dayjs())
|
const newDate = cloneTime(day, selectedDate || getDateWithTimezone({ timezone }))
|
||||||
setCurrentDate(newDate)
|
setCurrentDate(newDate)
|
||||||
setSelectedDate(newDate)
|
setSelectedDate(newDate)
|
||||||
}, [selectedDate])
|
}, [selectedDate, timezone])
|
||||||
|
|
||||||
const handleSelectCurrentDate = () => {
|
const handleSelectCurrentDate = () => {
|
||||||
const newDate = dayjs()
|
const newDate = getDateWithTimezone({ timezone })
|
||||||
setCurrentDate(newDate)
|
setCurrentDate(newDate)
|
||||||
setSelectedDate(newDate)
|
setSelectedDate(newDate)
|
||||||
onChange(newDate)
|
onChange(newDate)
|
||||||
@ -119,19 +148,19 @@ const DatePicker = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectHour = useCallback((hour: string) => {
|
const handleSelectHour = useCallback((hour: string) => {
|
||||||
const selectedTime = selectedDate || dayjs()
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
||||||
handleTimeSelect(hour, selectedTime.minute().toString().padStart(2, '0'), selectedTime.format('A') as Period)
|
handleTimeSelect(hour, selectedTime.minute().toString().padStart(2, '0'), selectedTime.format('A') as Period)
|
||||||
}, [selectedDate])
|
}, [selectedDate, timezone])
|
||||||
|
|
||||||
const handleSelectMinute = useCallback((minute: string) => {
|
const handleSelectMinute = useCallback((minute: string) => {
|
||||||
const selectedTime = selectedDate || dayjs()
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
||||||
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), minute, selectedTime.format('A') as Period)
|
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), minute, selectedTime.format('A') as Period)
|
||||||
}, [selectedDate])
|
}, [selectedDate, timezone])
|
||||||
|
|
||||||
const handleSelectPeriod = useCallback((period: Period) => {
|
const handleSelectPeriod = useCallback((period: Period) => {
|
||||||
const selectedTime = selectedDate || dayjs()
|
const selectedTime = selectedDate || getDateWithTimezone({ timezone })
|
||||||
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), selectedTime.minute().toString().padStart(2, '0'), period)
|
handleTimeSelect(getHourIn12Hour(selectedTime).toString().padStart(2, '0'), selectedTime.minute().toString().padStart(2, '0'), period)
|
||||||
}, [selectedDate])
|
}, [selectedDate, timezone])
|
||||||
|
|
||||||
const handleOpenYearMonthPicker = () => {
|
const handleOpenYearMonthPicker = () => {
|
||||||
setSelectedMonth(currentDate.month())
|
setSelectedMonth(currentDate.month())
|
||||||
@ -156,15 +185,13 @@ const DatePicker = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleYearMonthConfirm = () => {
|
const handleYearMonthConfirm = () => {
|
||||||
setCurrentDate((prev) => {
|
setCurrentDate(prev => prev.clone().month(selectedMonth).year(selectedYear))
|
||||||
return prev ? prev.clone().month(selectedMonth).year(selectedYear) : dayjs().month(selectedMonth).year(selectedYear)
|
|
||||||
})
|
|
||||||
setView(ViewType.date)
|
setView(ViewType.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeFormat = needTimePicker ? 'MMMM D, YYYY hh:mm A' : 'MMMM D, YYYY'
|
const timeFormat = needTimePicker ? 'MMMM D, YYYY hh:mm A' : 'MMMM D, YYYY'
|
||||||
const displayValue = value?.format(timeFormat) || ''
|
const displayValue = value?.format(timeFormat) || ''
|
||||||
const displayTime = (selectedDate || dayjs().startOf('day')).format('hh:mm A')
|
const displayTime = selectedDate?.format('hh:mm A') || '--:-- --'
|
||||||
const placeholderDate = isOpen && selectedDate ? selectedDate.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
|
const placeholderDate = isOpen && selectedDate ? selectedDate.format(timeFormat) : (placeholder || t('time.defaultPlaceholder'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from './utils/dayjs'
|
||||||
import { Period } from './types'
|
import { Period } from './types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { Period, TimePickerProps } from '../types'
|
import type { Period, TimePickerProps } from '../types'
|
||||||
import { cloneTime, getHourIn12Hour } from '../utils'
|
import dayjs, { cloneTime, getDateWithTimezone, getHourIn12Hour } from '../utils/dayjs'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
@ -16,6 +15,7 @@ import cn from '@/utils/classnames'
|
|||||||
|
|
||||||
const TimePicker = ({
|
const TimePicker = ({
|
||||||
value,
|
value,
|
||||||
|
timezone,
|
||||||
placeholder,
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
onClear,
|
onClear,
|
||||||
@ -24,7 +24,8 @@ const TimePicker = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const [selectedTime, setSelectedTime] = useState(value)
|
const isInitial = useRef(true)
|
||||||
|
const [selectedTime, setSelectedTime] = useState(value ? getDateWithTimezone({ timezone, date: value }) : undefined)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@ -35,6 +36,22 @@ const TimePicker = ({
|
|||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitial.current) {
|
||||||
|
isInitial.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
const newValue = getDateWithTimezone({ date: value, timezone })
|
||||||
|
setSelectedTime(newValue)
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setSelectedTime(prev => prev ? getDateWithTimezone({ date: prev, timezone }) : undefined)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [timezone])
|
||||||
|
|
||||||
const handleClickTrigger = (e: React.MouseEvent) => {
|
const handleClickTrigger = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@ -42,6 +59,8 @@ const TimePicker = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
|
if (value)
|
||||||
|
setSelectedTime(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClear = (e: React.MouseEvent) => {
|
const handleClear = (e: React.MouseEvent) => {
|
||||||
@ -74,11 +93,11 @@ const TimePicker = ({
|
|||||||
}, [selectedTime])
|
}, [selectedTime])
|
||||||
|
|
||||||
const handleSelectCurrentTime = useCallback(() => {
|
const handleSelectCurrentTime = useCallback(() => {
|
||||||
const newDate = dayjs()
|
const newDate = getDateWithTimezone({ timezone })
|
||||||
setSelectedTime(newDate)
|
setSelectedTime(newDate)
|
||||||
onChange(newDate)
|
onChange(newDate)
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}, [onChange])
|
}, [onChange, timezone])
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
onChange(selectedTime)
|
onChange(selectedTime)
|
||||||
|
@ -21,6 +21,7 @@ type TriggerProps = {
|
|||||||
|
|
||||||
export type DatePickerProps = {
|
export type DatePickerProps = {
|
||||||
value: Dayjs | undefined
|
value: Dayjs | undefined
|
||||||
|
timezone?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
needTimePicker?: boolean
|
needTimePicker?: boolean
|
||||||
onChange: (date: Dayjs | undefined) => void
|
onChange: (date: Dayjs | undefined) => void
|
||||||
@ -46,6 +47,7 @@ export type DatePickerFooterProps = {
|
|||||||
|
|
||||||
export type TimePickerProps = {
|
export type TimePickerProps = {
|
||||||
value: Dayjs | undefined
|
value: Dayjs | undefined
|
||||||
|
timezone?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
onChange: (date: Dayjs | undefined) => void
|
onChange: (date: Dayjs | undefined) => void
|
||||||
onClear: () => void
|
onClear: () => void
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import type { Dayjs } from 'dayjs'
|
import dayjs, { type Dayjs } from 'dayjs'
|
||||||
import type { Day } from './types'
|
import type { Day } from '../types'
|
||||||
|
import utc from 'dayjs/plugin/utc'
|
||||||
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
dayjs.extend(timezone)
|
||||||
|
|
||||||
|
export default dayjs
|
||||||
|
|
||||||
const monthMaps: Record<string, Day[]> = {}
|
const monthMaps: Record<string, Day[]> = {}
|
||||||
|
|
||||||
@ -58,7 +65,16 @@ export const getDaysInMonth = (currentDate: Dayjs) => {
|
|||||||
return days
|
return days
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const clearMonthMapCache = () => {
|
||||||
|
for (const key in monthMaps)
|
||||||
|
delete monthMaps[key]
|
||||||
|
}
|
||||||
|
|
||||||
export const getHourIn12Hour = (date: Dayjs) => {
|
export const getHourIn12Hour = (date: Dayjs) => {
|
||||||
const hour = date.hour()
|
const hour = date.hour()
|
||||||
return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour
|
return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDateWithTimezone = (props: { date?: Dayjs, timezone?: string }) => {
|
||||||
|
return props.date ? dayjs.tz(props.date, props.timezone) : dayjs().tz(props.timezone)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user