dify
This commit is contained in:
255
dify/web/app/components/base/date-and-time-picker/utils/dayjs.ts
Normal file
255
dify/web/app/components/base/date-and-time-picker/utils/dayjs.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import dayjs, { type Dayjs } from 'dayjs'
|
||||
import type { Day } from '../types'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import tz from '@/utils/timezone.json'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export default dayjs
|
||||
|
||||
const monthMaps: Record<string, Day[]> = {}
|
||||
const DEFAULT_OFFSET_STR = 'UTC+0'
|
||||
const TIME_ONLY_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/
|
||||
const TIME_ONLY_12H_REGEX = /^(\d{1,2}):(\d{2})(?::(\d{2}))?\s?(AM|PM)$/i
|
||||
|
||||
const COMMON_PARSE_FORMATS = [
|
||||
'YYYY-MM-DD',
|
||||
'YYYY/MM/DD',
|
||||
'DD-MM-YYYY',
|
||||
'DD/MM/YYYY',
|
||||
'MM-DD-YYYY',
|
||||
'MM/DD/YYYY',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSSZ',
|
||||
'YYYY-MM-DDTHH:mm:ssZ',
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm',
|
||||
'YYYY-MM-DDTHH:mmZ',
|
||||
'YYYY-MM-DDTHH:mm:ss',
|
||||
'YYYY-MM-DDTHH:mm:ss.SSS',
|
||||
]
|
||||
|
||||
export const cloneTime = (targetDate: Dayjs, sourceDate: Dayjs) => {
|
||||
return targetDate.clone()
|
||||
.set('hour', sourceDate.hour())
|
||||
.set('minute', sourceDate.minute())
|
||||
}
|
||||
|
||||
export const getDaysInMonth = (currentDate: Dayjs) => {
|
||||
const key = currentDate.format('YYYY-MM')
|
||||
// return the cached days
|
||||
if (monthMaps[key])
|
||||
return monthMaps[key]
|
||||
|
||||
const daysInCurrentMonth = currentDate.daysInMonth()
|
||||
const firstDay = currentDate.startOf('month').day()
|
||||
const lastDay = currentDate.endOf('month').day()
|
||||
const lastDayInLastMonth = currentDate.clone().subtract(1, 'month').endOf('month')
|
||||
const firstDayInNextMonth = currentDate.clone().add(1, 'month').startOf('month')
|
||||
const days: Day[] = []
|
||||
const daysInOneWeek = 7
|
||||
const totalLines = 6
|
||||
|
||||
// Add cells for days before the first day of the month
|
||||
for (let i = firstDay - 1; i >= 0; i--) {
|
||||
const date = cloneTime(lastDayInLastMonth.subtract(i, 'day'), currentDate)
|
||||
days.push({
|
||||
date,
|
||||
isCurrentMonth: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Add days of the month
|
||||
for (let i = 1; i <= daysInCurrentMonth; i++) {
|
||||
const date = cloneTime(currentDate.startOf('month').add(i - 1, 'day'), currentDate)
|
||||
days.push({
|
||||
date,
|
||||
isCurrentMonth: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Add cells for days after the last day of the month
|
||||
const totalLinesOfCurrentMonth = Math.ceil((daysInCurrentMonth - ((daysInOneWeek - firstDay) + lastDay + 1)) / 7) + 2
|
||||
const needAdditionalLine = totalLinesOfCurrentMonth < totalLines
|
||||
for (let i = 0; lastDay + i < (needAdditionalLine ? 2 * daysInOneWeek - 1 : daysInOneWeek - 1); i++) {
|
||||
const date = cloneTime(firstDayInNextMonth.add(i, 'day'), currentDate)
|
||||
days.push({
|
||||
date,
|
||||
isCurrentMonth: false,
|
||||
})
|
||||
}
|
||||
|
||||
// cache the days
|
||||
monthMaps[key] = days
|
||||
return days
|
||||
}
|
||||
|
||||
export const clearMonthMapCache = () => {
|
||||
for (const key in monthMaps)
|
||||
delete monthMaps[key]
|
||||
}
|
||||
|
||||
export const getHourIn12Hour = (date: Dayjs) => {
|
||||
const hour = date.hour()
|
||||
return hour === 0 ? 12 : hour >= 12 ? hour - 12 : hour
|
||||
}
|
||||
|
||||
export const getDateWithTimezone = ({ date, timezone }: { date?: Dayjs, timezone?: string }) => {
|
||||
if (!timezone)
|
||||
return (date ?? dayjs()).clone()
|
||||
return date ? dayjs.tz(date, timezone) : dayjs().tz(timezone)
|
||||
}
|
||||
|
||||
export const convertTimezoneToOffsetStr = (timezone?: string) => {
|
||||
if (!timezone)
|
||||
return DEFAULT_OFFSET_STR
|
||||
const tzItem = tz.find(item => item.value === timezone)
|
||||
if (!tzItem)
|
||||
return DEFAULT_OFFSET_STR
|
||||
// Extract offset from name format like "-11:00 Niue Time" or "+05:30 India Time"
|
||||
// Name format is always "{offset}:{minutes} {timezone name}"
|
||||
const offsetMatch = tzItem.name.match(/^([+-]?\d{1,2}):(\d{2})/)
|
||||
if (!offsetMatch)
|
||||
return DEFAULT_OFFSET_STR
|
||||
// Parse hours and minutes separately
|
||||
const hours = Number.parseInt(offsetMatch[1], 10)
|
||||
const minutes = Number.parseInt(offsetMatch[2], 10)
|
||||
const sign = hours >= 0 ? '+' : ''
|
||||
// If minutes are non-zero, include them in the output (e.g., "UTC+5:30")
|
||||
// Otherwise, only show hours (e.g., "UTC+8")
|
||||
return minutes !== 0 ? `UTC${sign}${hours}:${offsetMatch[2]}` : `UTC${sign}${hours}`
|
||||
}
|
||||
|
||||
export const isDayjsObject = (value: unknown): value is Dayjs => dayjs.isDayjs(value)
|
||||
|
||||
export type ToDayjsOptions = {
|
||||
timezone?: string
|
||||
format?: string
|
||||
formats?: string[]
|
||||
}
|
||||
|
||||
const warnParseFailure = (value: string) => {
|
||||
if (process.env.NODE_ENV !== 'production')
|
||||
console.warn('[TimePicker] Failed to parse time value', value)
|
||||
}
|
||||
|
||||
const normalizeMillisecond = (value: string | undefined) => {
|
||||
if (!value) return 0
|
||||
if (value.length === 3) return Number(value)
|
||||
if (value.length > 3) return Number(value.slice(0, 3))
|
||||
return Number(value.padEnd(3, '0'))
|
||||
}
|
||||
|
||||
const applyTimezone = (date: Dayjs, timezone?: string) => {
|
||||
return timezone ? getDateWithTimezone({ date, timezone }) : date
|
||||
}
|
||||
|
||||
export const toDayjs = (value: string | Dayjs | undefined, options: ToDayjsOptions = {}): Dayjs | undefined => {
|
||||
if (!value)
|
||||
return undefined
|
||||
|
||||
const { timezone: tzName, format, formats } = options
|
||||
|
||||
if (isDayjsObject(value))
|
||||
return applyTimezone(value, tzName)
|
||||
|
||||
if (typeof value !== 'string')
|
||||
return undefined
|
||||
|
||||
const trimmed = value.trim()
|
||||
|
||||
if (format) {
|
||||
const parsedWithFormat = tzName
|
||||
? dayjs(trimmed, format, true).tz(tzName, true)
|
||||
: dayjs(trimmed, format, true)
|
||||
if (parsedWithFormat.isValid())
|
||||
return parsedWithFormat
|
||||
}
|
||||
|
||||
const timeMatch = TIME_ONLY_REGEX.exec(trimmed)
|
||||
if (timeMatch) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
const rawHour = Number(timeMatch[1])
|
||||
const minute = Number(timeMatch[2])
|
||||
const second = timeMatch[3] ? Number(timeMatch[3]) : 0
|
||||
const millisecond = normalizeMillisecond(timeMatch[4])
|
||||
|
||||
return base
|
||||
.set('hour', rawHour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', millisecond)
|
||||
}
|
||||
|
||||
const timeMatch12h = TIME_ONLY_12H_REGEX.exec(trimmed)
|
||||
if (timeMatch12h) {
|
||||
const base = applyTimezone(dayjs(), tzName).startOf('day')
|
||||
let hour = Number(timeMatch12h[1]) % 12
|
||||
const isPM = timeMatch12h[4]?.toUpperCase() === 'PM'
|
||||
if (isPM)
|
||||
hour += 12
|
||||
const minute = Number(timeMatch12h[2])
|
||||
const second = timeMatch12h[3] ? Number(timeMatch12h[3]) : 0
|
||||
|
||||
return base
|
||||
.set('hour', hour)
|
||||
.set('minute', minute)
|
||||
.set('second', second)
|
||||
.set('millisecond', 0)
|
||||
}
|
||||
|
||||
const candidateFormats = formats ?? COMMON_PARSE_FORMATS
|
||||
for (const fmt of candidateFormats) {
|
||||
const parsed = tzName
|
||||
? dayjs(trimmed, fmt, true).tz(tzName, true)
|
||||
: dayjs(trimmed, fmt, true)
|
||||
if (parsed.isValid())
|
||||
return parsed
|
||||
}
|
||||
|
||||
const fallbackParsed = tzName ? dayjs.tz(trimmed, tzName) : dayjs(trimmed)
|
||||
if (fallbackParsed.isValid())
|
||||
return fallbackParsed
|
||||
|
||||
warnParseFailure(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Parse date with multiple format support
|
||||
export const parseDateWithFormat = (dateString: string, format?: string): Dayjs | null => {
|
||||
if (!dateString) return null
|
||||
|
||||
// If format is specified, use it directly
|
||||
if (format) {
|
||||
const parsed = dayjs(dateString, format, true)
|
||||
return parsed.isValid() ? parsed : null
|
||||
}
|
||||
|
||||
// Try common date formats
|
||||
const formats = [
|
||||
...COMMON_PARSE_FORMATS,
|
||||
]
|
||||
|
||||
for (const fmt of formats) {
|
||||
const parsed = dayjs(dateString, fmt, true)
|
||||
if (parsed.isValid())
|
||||
return parsed
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Format date output with localization support
|
||||
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, _locale: string = 'en-US'): string => {
|
||||
if (!date || !date.isValid()) return ''
|
||||
|
||||
if (includeTime) {
|
||||
// Output format with time
|
||||
return date.format('YYYY-MM-DDTHH:mm:ss.SSSZ')
|
||||
}
|
||||
else {
|
||||
// Date-only output format without timezone
|
||||
return date.format('YYYY-MM-DD')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user