|
1 | 1 | import dayjs from 'dayjs'; |
| 2 | +import timezone from 'dayjs/plugin/timezone'; // ES 2015 |
| 3 | +import utc from 'dayjs/plugin/utc'; // ES 2015 |
| 4 | + |
| 5 | +dayjs.extend(utc); |
| 6 | +dayjs.extend(timezone); |
| 7 | +// 默认时区:东八时区 (Asia/Shanghai) |
| 8 | +const DEFAULT_TIMEZONE = 'Asia/Shanghai'; |
| 9 | +/** |
| 10 | + * 将时间转换为默认时区 |
| 11 | + * @param {string | number | Date} time - 输入时间 |
| 12 | + * @return {dayjs.Dayjs} 带时区的Dayjs对象 |
| 13 | + */ |
| 14 | +dayjs.tz.setDefault(DEFAULT_TIMEZONE); |
2 | 15 | /** |
3 | 16 | * @category 枚举 |
4 | 17 | * 日期和时间格式模式的枚举 |
@@ -117,16 +130,115 @@ type FormatPattern = DateTimeFormat | string; |
117 | 130 | * formatDateTime(new Date(), "dddd, MMMM D, YYYY") // dayjs.Dayjs |
118 | 131 | * ``` |
119 | 132 | */ |
| 133 | +/** |
| 134 | + * Helper function to convert date to default timezone |
| 135 | + */ |
| 136 | +const toDefaultTz = (date: DateTimeInput): dayjs.Dayjs => { |
| 137 | + // 如果已经是 dayjs 对象且支持 tz,直接转换到默认时区 |
| 138 | + if ((date as any) && typeof (date as any).tz === 'function') { |
| 139 | + return (date as any).tz(DEFAULT_TIMEZONE); |
| 140 | + } |
| 141 | + // 否则使用 dayjs.tz 将输入解析为默认时区的 dayjs 对象 |
| 142 | + // 对于无效日期,dayjs.tz 可能会抛出错误,此时降级到普通 dayjs |
| 143 | + try { |
| 144 | + return (dayjs as any).tz(date, DEFAULT_TIMEZONE); |
| 145 | + } catch { |
| 146 | + return dayjs(date); |
| 147 | + } |
| 148 | +}; |
| 149 | + |
| 150 | +/** |
| 151 | + * Normalize input to native Date object |
| 152 | + */ |
| 153 | +const getNativeDate = (date: DateTimeInput): Date | null => { |
| 154 | + if ((date as any) && typeof (date as any).toDate === 'function') { |
| 155 | + return (date as any).toDate(); |
| 156 | + } else if (date instanceof Date) { |
| 157 | + return date as Date; |
| 158 | + } else { |
| 159 | + const d = new Date(date as any); |
| 160 | + return Number.isNaN(d.getTime()) ? null : d; |
| 161 | + } |
| 162 | +}; |
| 163 | + |
| 164 | +/** |
| 165 | + * Check if a date is an edge-case year (year < 1900) |
| 166 | + * Edge-case years may have parsing inconsistencies in dayjs/timezone |
| 167 | + */ |
| 168 | +const isEdgeCaseYear = (date: DateTimeInput): boolean => { |
| 169 | + const nativeDate = getNativeDate(date); |
| 170 | + if (!nativeDate) return false; |
| 171 | + return nativeDate.getFullYear() < 1900; |
| 172 | +}; |
| 173 | + |
| 174 | +/** |
| 175 | + * Format native Date object using dayjs-like format string |
| 176 | + * Leverages dayjs for standard tokens, only customizing where needed |
| 177 | + */ |
| 178 | +const formatNativeDate = (nativeDate: Date, format: string): string => { |
| 179 | + const pad = (num: number, len: number = 2) => String(num).padStart(len, '0'); |
| 180 | + const d = nativeDate; |
| 181 | + |
| 182 | + // Use dayjs for all standard token formatting |
| 183 | + const dayjsInstance = dayjs(d); |
| 184 | + |
| 185 | + // Only define custom tokens that need special handling or differ from dayjs defaults |
| 186 | + const customTokens: Record<string, () => string> = { |
| 187 | + // Timezone offset with colon (e.g., +08:00) |
| 188 | + Z: () => { |
| 189 | + const offset = d.getTimezoneOffset(); |
| 190 | + const sign = offset <= 0 ? '+' : '-'; |
| 191 | + const h = Math.abs(Math.floor(offset / 60)); |
| 192 | + const m = Math.abs(offset % 60); |
| 193 | + return `${sign}${pad(h)}:${pad(m)}`; |
| 194 | + }, |
| 195 | + // Timezone offset without separator (e.g., +0800) |
| 196 | + ZZ: () => { |
| 197 | + const offset = d.getTimezoneOffset(); |
| 198 | + const sign = offset <= 0 ? '+' : '-'; |
| 199 | + const h = Math.abs(Math.floor(offset / 60)); |
| 200 | + const m = Math.abs(offset % 60); |
| 201 | + return `${sign}${pad(h)}${pad(m)}`; |
| 202 | + }, |
| 203 | + }; |
| 204 | + |
| 205 | + // Sort tokens by length (descending) to avoid partial matches (e.g., Z before ZZ) |
| 206 | + const sortedCustomTokens = Object.keys(customTokens).sort((a, b) => b.length - a.length); |
| 207 | + |
| 208 | + let result = format; |
| 209 | + |
| 210 | + // First, replace our custom tokens |
| 211 | + for (const token of sortedCustomTokens) { |
| 212 | + const regex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); |
| 213 | + result = result.replace(regex, customTokens[token]()); |
| 214 | + } |
| 215 | + |
| 216 | + // Then, use dayjs to handle all standard tokens (YYYY, MM, DD, HH, mm, ss, etc.) |
| 217 | + // This ensures consistency with dayjs behavior for all non-custom tokens |
| 218 | + result = dayjsInstance.format(result); |
| 219 | + |
| 220 | + return result; |
| 221 | +}; |
| 222 | + |
120 | 223 | export const formatDateTime = ( |
121 | 224 | date: DateTimeInput, |
122 | 225 | format: FormatPattern = DateTimeFormat.STANDARD |
123 | 226 | ): string | dayjs.Dayjs => { |
124 | 227 | const isValidFormat = Object.values<string>(DateTimeFormat).includes(format); |
125 | | - |
126 | 228 | if (!isValidFormat) { |
127 | | - return dayjs(date); |
| 229 | + return toDefaultTz(date); |
| 230 | + } |
| 231 | + |
| 232 | + // 检测是否为极早年份(< 1900):边缘情况年份可能存在解析问题,因此需要特殊处理 |
| 233 | + if (isEdgeCaseYear(date)) { |
| 234 | + const nativeDate = getNativeDate(date); |
| 235 | + if (!nativeDate) { |
| 236 | + return 'Invalid Date'; |
| 237 | + } |
| 238 | + return formatNativeDate(nativeDate, format as string); |
128 | 239 | } |
129 | | - return dayjs(date).format(format); |
| 240 | + |
| 241 | + return toDefaultTz(date).format(format); |
130 | 242 | }; |
131 | 243 |
|
132 | 244 | export default formatDateTime; |
0 commit comments