Repository navigation
cn hx components DateTimePicker
日期时间选择器组件,提供基于日历的弹出面板。支持多种历法系统(公历、日本和历、民国纪年、佛历等)、国际化及键盘导航。
HxWithCheckDateTimePicker 添加验证功能(参见 WithCheck)。
// 仅日期选择
<HxDateTimePicker
$model={form} $field="date"
displayFormat="@d/ymd"
/>
// 日期时间选择
<HxDateTimePicker
$model={form} $field="datetime"
displayFormat="@d/ymd :hns"
/>
// 日本和历显示
<HxDateTimePicker
$model={form} $field="jpDate"
calendarLocale="ja-JP"
displayFormat="@d/ymd"
/>
// 带验证
<HxWithCheckDateTimePicker
$model={form} $field="requiredDate"
displayFormat="@d/ymd"
check
/>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
$model |
HxObject<T> |
— | 响应式模型 |
$field |
ModelPath<T> | HxDataPath |
— | 模型字段路径 |
displayFormat |
HxDateTimePickerDisplayFormat |
— | 显示格式字符串、模板或函数(函数形式接收 UTCDate) |
availableParts |
HxDateTimeRelatedFormat |
自动检测 | 可用日期时间部分(日期、时间或两者) |
defaultValue |
HxDateTimeDefaultValuesInStr | HxDateTimeValue |
— | 模型为空时的默认值 |
valueFormat |
HxDateTimeRelatedFormat |
— | 模型绑定的值格式 |
firstDayOfWeek |
'sun' | 'mon' | 'default' |
'default' |
每周第一天 |
weekendDays |
HxDateWeekendDays | 'default' |
'default' |
周末配置 |
calendarLocale |
'gregory' | HxLanguageCode |
— | 强制使用指定 locale 或公历 |
enterToOpenPopup |
boolean |
false |
按 Enter 打开弹出面板 |
spaceToOpenPopup |
boolean |
true |
按 Space 打开弹出面板 |
clearable |
boolean |
false |
显示清除按钮 |
placeholder |
boolean |
true |
值为空时显示占位文字 |
placeholderKey |
ReactNode |
'~HxCommon.DateTimePickerPlaceholder' |
占位文字 |
calendarIcon |
ReactNode |
— | 自定义日历图标 |
todayKey |
ReactNode |
'~HxCommon.TodayButton' |
"现在"按钮文字 |
clearKey |
ReactNode |
'~HxCommon.ClearButton' |
"清除"按钮文字 |
confirmKey |
ReactNode |
'~HxCommon.OkButton' |
"确定"按钮文字(模式含时间部分时显示) |
valueSyncMode |
'default' | 'immediate' |
'default' |
'default' 时面板编辑为草稿,点击日期或确定后提交;'immediate' 时每次编辑立即同步到模型 |
zIndex |
number |
— | 弹出面板 z-index |
gapToEdge |
number |
— | 触发器与面板间间距 |
日期时间选择器的公历日期范围为 0001/01/01 至 9999/12/31。此边界适用于所有历法系统。
| 边界 | 公历日期 | 行为 |
|---|---|---|
| 下限 | 0001/01/01 | 公元纪元起点。公元1年没有上一年;公元1年1月没有上月。没有公元0年(公元前1年 → 公元1年)。 |
| 上限 | 9999/12/31 | 最大可表示日期。9999年没有下一年;9999年12月没有下月。 |
当到达任一边界时,弹出面板头部对应的上/下年、上/下月导航按钮将被禁用。
非公历经法将导航边界映射到相同的公历纪元下限(0001/01/01)和上限(9999/12/31),以各自历法表示。每种历法实现了精确到日的阈值,考虑了边界处的跨月窗口:
| 历法 | 下限(历法日期) | 上限(历法日期) |
|---|---|---|
| 公历 | 0001/01/01 | 9999/12/31 |
| 日本和历 | 1/01/03 | 9999/12/31 |
| 民国纪年 | −1911/01/03 | 8088/12/31 |
| 佛历 | 544/01/03 | 10542/12/31 |
| 希伯来历 | 3761/04/18 | 13760/02/28 |
| 伊斯兰历(表格式) | −640/05/20 | 9666/03/30 |
| 伊斯兰历(民用) | −640/05/18 | 9666/04/02 |
| 伊斯兰历(Umalqura) | −640/05/18 | 9666/04/02 |
| 波斯历 | −621/10/11 | 9378/10/10 |
| 科普特历 | −284/05/08 | 9716/02/21 |
| 埃塞俄比亚历 | 5493/05/08 | 9992/02/21 |
| 印度国定历(Saka) | −78/10/11 | 9921/10/10 |
这三个历法为何从 01/03 开始: 日本和历、民国纪年、佛历在 1582 年之前使用儒略历。儒略历的额外闰日累积了 +12 天偏差,1582 年改革移除 10 天,在公元元年净差 +2 天。因此历法 01/01–02 对应公历公元前 12/30–31(被 clamp 至纪元起点),01/03 才对应公历 0001/01/01。
注意: 伊斯兰历的三个变体(表格式、民用、Umalqura)的年/月移动操作(
moveYear/moveMonth)通过共享的DateMoveAnyMonthsProvider超类实现(与印度历、波斯历相同的 12 个月机制),月份/年份面板通过共享面板骨架实现。
日期解析和格式化输入组件对范围边界的处理与选择器不同:
| 层 | 范围校验 |
|---|---|
DateParseUtils.parseValue |
无语义校验。接受任意数字值(如月份 "61" 可被接受)。仅校验字符串结构(分隔符匹配、尾部字符)。 |
DateParseUtils.fromParsed / toParsed
|
年份 clamp 至 [0, 9999],其他部分 clamp 至 [0, 99]。不校验月份 ≤ 12 或日期 ≤ 31。 |
HxFormatInput(日期时间) |
编辑时仅结构校验:每字段位数(年=4,其他=2)、分隔符位置。范围检查(如月份 > 12)延迟至 blur/submit 校验。 |
| 负数 | 不支持。- 被当作日期分隔符,不作为负号处理。 |
当显示格式不包含完整日期(y/m/d 三者齐全——例如时间-only 或年月格式)时,无法显示日历弹窗。组件不会抛出异常,而是退化为纯 HxFormatInput。这是配置错误的软性提示,而非可用的时间-only 模式:输入框 pattern 从 availableParts → valueFormat → 全局默认(y/m/dTh:n:s)推导,displayFormat 本身被有意忽略。要获得日历弹窗,请使用完整的日期显示格式(或显式设置 availableParts / valueFormat)。
通过 calendarLocale 属性支持多种历法系统:
-
公历(默认)—
calendarLocale="gregory"或省略 -
日本和历 —
calendarLocale="ja-JP"(令和、平成、昭和等年号) -
民国纪年 —
calendarLocale="zh-TW"(台湾) -
佛历(B.E.) —
calendarLocale="th"(泰国) -
科普特历 —
calendarLocale="ar-EG"(埃及,殉教纪年) -
埃塞俄比亚历 —
calendarLocale="am-ET"或"ti-ET"(埃塞俄比亚/厄立特里亚,道成肉身纪元) -
希伯来历 —
calendarLocale="he-IL"(以色列) -
伊斯兰历 —
calendarLocale="ar-SA"(沙特阿拉伯) -
波斯历 —
calendarLocale="fa-IR"(伊朗;月份与年份面板以波斯历显示,含 BC/10k 边界标记) -
印度国定历(Saka) —
calendarLocale="hi-IN"(印度;月份与年份面板以 Saka 历显示,含 BC/10k 边界标记)
完整的历法映射请参见日期本地化工具。
解析为公历的语言仍会遵循各自的语言习惯来显示弹窗的星期表头、星期名与日期顺序。周起始遵循 ICU 周数据(Intl.Locale.getWeekInfo):
| 语言 | 文字 | 周起始 | 说明 |
|---|---|---|---|
vi-VN(越南语) |
拉丁字母 | 周一(Thứ 2) | 星期表头使用窄格式标签 |
ms-MY(马来语) |
拉丁字母 | 周一(Isnin) | |
id-ID(印尼语) |
拉丁字母 | 周日(Minggu) | |
lo-LA(老挝语) |
老挝文 | 周日 | 老挝官方使用佛历(ພ.ສ.),但佛历 provider 仅覆盖 th-*,故 lo-LA 解析为公历 |
my-MM(缅甸语) |
缅甸文 | 周日(တနင်္ဂနွေ) | 缅甸官方使用缅历,但无 provider 覆盖,故 my-MM 解析为公历 |
mn-MN(蒙古语) |
西里尔字母 | 周一(Даваа) | |
de-DE(德语) |
拉丁字母 | 周一(Montag) | 带点序数日(10. Juni) |
nl-NL(荷兰语) |
拉丁字母 | 周一(maandag) | 月份名小写 |
fr-FR(法语) |
拉丁字母 | 周一(lundi) | 窄格式星期标签(L, M, M, J, V, S, D) |
ru-RU(俄语) |
西里尔字母 | 周一(понедельник) | 属格月份;年份带「г.」 |
es-ES(西班牙语) |
拉丁字母 | 周一(lunes) | 「de」连接词(10 de junio de 2025) |
pt-BR(巴西葡萄牙语) |
拉丁字母 | 周日(domingo) | 月份名小写 |
it-IT(意大利语) |
拉丁字母 | 周一(lunedì) | 月份名小写 |
tr-TR(土耳其语) |
拉丁字母 | 周一(Pazartesi) | 日期在前、星期在后(10 Haziran 2025 Salı) |
pl-PL(波兰语) |
拉丁字母 | 周一(poniedziałek) | 属格月份;窄格式星期标签(p, w, ś, c, p, s, n) |
uk-UA(乌克兰语) |
西里尔字母 | 周一(понеділок) | 属格月份;年份带「р.」 |
en-US(英语,美国) |
拉丁字母 | 周日 | 月前日顺序(June 10, 2025) |
en-GB(英语,英国) |
拉丁字母 | 周一 | 日前月顺序(10 June 2025) |
弹层的月份面板与年份面板基于共享的「走步 + 重锚定」骨架(DateLocaleNotGregorianHelper)实现;公历-儒略系历法(日本、民国、佛历)通过 DateLocaleGregorianAndJulianHelper 额外处理 1582/10 短月。
- 日本和历 — 年份与月份面板完整实现;月份按年号制显示:当月首日年号与前一月不同时显示该年号(1 月恒显示年首年号),并列出月内出现的其他年号——例如 1989/01(1 月 7 日昭和→平成)与 1926/12(12 月 25 日大正→昭和)。年份面板基于连续年(= 公历年,含改革前边界修正)并在年份间正向传播年号;仅有的两个年内三个年号的年份为 1387(元中/至徳/嘉慶)与 749(天平/天平感宝/天平勝宝,均系南北朝重叠),显式处理。唯一三个月内出现三个年号的月份为 1387/8;1582/10 短月(21 天,跳过 5-14 日)经由共享的 Gregorian-and-Julian helper 处理。
- 民国 / 佛历 — 年份与月份面板完整实现,涵盖纪元边界(民国 −1 ↔ 1)、1582 改革跨越与日历上下限钳制。
-
印度国定历(Saka) — 年份与月份面板完整实现;Saka 历范围 [−78, 9921] 两端均为不完整年,因此 −78 年 1-9 月标记为
bc,9921 年 11-12 月标记为y10k。 -
波斯历 — 年份与月份面板完整实现;波斯历范围 [−621, 9378] 两端均为不完整年,因此 −621 年 1-9 月标记为
bc,9378 年 11-12 月标记为y10k。 -
伊斯兰历(表格式 / 民用 / Umalqura) — 年份与月份面板完整实现;伊斯兰历范围 [−640, 9666] 两端均为不完整年,因此 −640 年 1-4 月标记为
bc,9666 年 5-12 月标记为y10k。年份面板每步按 353 天/年行走(伊斯兰历一年 354/355 天),再按落点月份回退到穆哈兰姆月 1 日;底部钳制页首格锚定在 −640/1/1(公元前 1 年 8/17),先于历法最早可表示日期——与波斯 −621/1/1 锚点同属预期行为。 -
科普特历 / 埃塞俄比亚历 — 年份与月份面板完整实现;月份面板含 13 个月格(12 × 30 天 + 第 13 月 5/6 天)。科普特历范围 [−284, 9716] 两端均为不完整年,因此 −284 年 1-4 月标记为
bc,9716 年 3-13 月标记为y10k;埃塞俄比亚历(B.I. 5493–5500、A.I. 1–9992)同样将 5493 年 1-4 月标记为bc,9992 年 3-13 月标记为y10k。 -
希伯来历 — 年份与月份面板完整实现;希伯来历范围 [3761, 13760] 两端均为不完整年,因此 3761 年 1-3 月标记为
bc,13760 年 3-12 月标记为y10k。月份按民用月序编号(1 = Tishrei);闰年时 Adar 拆分为 Adar I / Adar II,网格追加第 13 个月(Elul)。年份面板每步按 353 天/年行走并对年份做迭代修正(希伯来平年 353/354/355 天、闰年 383/384/385 天),翻年步进 355 天并跳过闰月。
年份面板每个格子的日期为该历法年的第一天(ICU 语义);在日历边缘,格子日期可能落在公历 [0001, 9999] 范围之外(例如民国 −1911/1/1 与 Saka −78/1/1 锚定在公元前 1 年)——这是预期行为。点击格子时按格子的年份偏移移动状态日期,从不直接使用格子日期。
使用日本和历(ja-JP)时,一个公历月份内可能跨越两个年号。这发生在日本年号更替落在某个月份中间的情况。例如,明治5年(1872年)在月份中期经历年号更替,以及1387年8月的至徳/嘉慶年号更替。
HxDateTimePickerStateRef 上的 yearHeaderLabel、monthHeaderLabel、eraOfDays 方法委托给 DateLocaleUtils,后者将非公历调用路由到对应的 DateLocaleNotGregorianProvider 插件(例如日语的 DateJapaneseUtils)。插件的 eraOfDays 实现:
- 使用
DateLocaleFormatUtils.formatDateInNumeric()检查当月第一天和最后一天的所属年号是否不同。 - 如果不同,对当月所有日期执行二分查找,精确定位年号更替的边界日。
- 二分查找逻辑:反复检查中间日的年号:
- 如果中间日仍在第一个年号中,搜索右半部分。
- 如果中间日已进入新年号,记录该日为候选边界并搜索左半部分。
- 找到后,将边界日标记为新年号的年号标签,存入
Map<Date, string>。
对于1387年8月的至徳/嘉慶年号更替,存在一个特殊的硬编码处理,因为该场景下 Intl API 的年号检测会产生歧义。
useHxDateTimePickerPopupStateRef 是日期时间选择器弹出面板的核心状态管理 hook。暴露以下方法:
| 方法 | 说明 |
|---|---|
modelValue() |
从模型获取值(为空时返回 null/undefined) |
stateValue() |
获取内部状态值(已填充;导航后可能与模型值不同) |
formatted() |
获取格式化标签(年号、年、月、日、星期) |
yearHeaderLabel(era, year) |
获取头部年份标签 |
monthHeaderLabel(era, year, month) |
获取头部月份标签 |
eraOfDays(days) |
获取每日年号标签(用于跨年号月份显示) |
isPreviousYearAllowed() / isNextYearAllowed() / isPreviousMonthAllowed() / isNextMonthAllowed()
|
检查历法导航边界 |
currentDatePanel() |
获取当前面板('days' / 'months' / 'years') |
switchDatePanel(panel, notifyEvent) |
切换日期面板 |
gregorian() |
检查是否为公历模式 |
language() |
获取当前语言代码 |
weekdays() |
计算日历网格的星期标签 |
days(weekdays) |
计算日历网格的日期单元格 |
months() |
计算月份面板的月份单元格 |
years() |
计算年份面板的年份单元格 |
changeYear(yearOffset, applyToModel) |
按年偏移导航 |
changeMonth(monthOffset, applyToModel) |
按月偏移导航 |
changeDayTo(year, month, day) |
选择特定日期 |
clearModelValue() |
清除模型值 |
clearState() |
清除全部缓存状态;下次访问时从模型重新读取 |
选择器使用 EventEmitter 进行触发器与弹出面板通信:
| 事件 | 说明 |
|---|---|
EvtHxDateTimePicker_ValueChange |
面板中选择了值 |
EvtHxDateTimePicker_ValueClear |
值已清除 |
EvtHxDateTimePicker_ClosePopup |
请求关闭面板 |
EvtHxDateTimePicker_GetPicker |
获取选择器 DOM 节点 |
EvtHxDateTimePicker_ArrowKey |
按下方向键 |
当模式含时间部分时,弹出面板在日期面板与底部按钮区之间显示时间输入行:整数输入框的数量与显示格式包含的时间字段一致(h 时、n 分、s 秒——例如 :h、:hn 或 :hns;时/分/秒分别使用 @iu23z / @iu59z / @iu59z),由静态冒号连接,样式上呈现为一个整体控件,另附三个时间快捷按钮:开始(00:00:00)、正午(12:00:00)、结束(23:59:59)。
- 输入字段的第二位数字后自动跳转焦点到下一字段
- 越界数字(如分
66)由整数工具包拒绝,不跳转 - 清空的字段失焦时显示补零为
00 - 快捷按钮一次性填充全部三个字段
默认(valueSyncMode: 'default')下面板编辑为草稿:选日、年/月导航、今天按钮与时间编辑都只更新面板状态;值在用户点击日期或确定按钮时写入模型(两者都提交面板完整值并关闭面板)。valueSyncMode: 'immediate' 时每次编辑立即同步到模型,且不显示确定按钮。模式不含时间部分时,点击日期在两种模式下都立即提交。
清除按钮清空模型并关闭面板 —— 清除是终结操作,之后无法再提交草稿。
面板支持完整的键盘操作。面板打开时,真实焦点移动到面板容器,网格当前项以视觉高亮显示(与 select 选项的交互方式一致);方向键移动高亮,Enter/Space 选中高亮项。鼠标悬停驱动同一高亮,鼠标下的元素与键盘目标始终保持同步。
-
Enter — 打开弹出面板(
enterToOpenPopup为true时);面板打开时选中高亮的网格单元格 -
Space — 打开弹出面板(
spaceToOpenPopup为true时,默认启用);面板打开时选中高亮的网格单元格 - 面板切换 — 头部月/年标签切换网格面板;选中月份(Enter/Space/点击)返回日面板,选中年份进入月面板,高亮自动重定位到新面板的状态单元格
- Escape — 关闭弹出面板,焦点返回输入框
- 方向键 — 在日/月/年网格中移动高亮(左/右 ±1 格,上/下 ±1 行;第一列向左回绕到上一行末列,最后一列向右到下一行首列;跳过 BC/10K 单元格;12 月向下到 13 月)
- 第一行向上 — 离开网格到头部月/年标签;最后一行向下 — 进入时间行第一个输入框(存在时间部分时)或"今天"按钮
- Tab / Shift+Tab — 在面板区块间移动(头部 → 网格 → 时间行 → 底部)及区块内部;底部链尾 Tab 自然移出面板(与 select 过滤框一致),头部链首 Shift+Tab 返回输入框
- 时间输入框 — 输入数字(自动跳转下一字段:格式包含分/秒时依次跳转,否则跳到"一日之始"快捷按钮);光标在开头时向左移到上一字段,在末尾时向右移到下一字段或快捷按钮
import { configHxDateTimePicker } from '@hx/components';
configHxDateTimePicker({
clearable: true,
firstDayOfWeek: 'mon',
todayKey: '~MyApp.Now',
});可用配置选项:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
valueFormat |
HxDateTimeRelatedFormat |
— | 默认值格式 |
clearable |
boolean |
false |
显示清除按钮 |
firstDayOfWeek |
'sun' | 'mon' | 'default' |
'default' |
每周第一天 |
weekendDays |
HxDateWeekendDays |
'default' |
周末日 |
forceGregorian |
boolean |
true |
强制使用公历 |
enterToOpenPopup |
boolean |
false |
按 Enter 打开面板 |
spaceToOpenPopup |
boolean |
true |
按 Space 打开面板 |
zIndex |
number |
— | 面板 z-index 基准 |
gapToEdge |
number |
— | 面板与视口边缘间距 |
placeholderKey |
string |
'~HxCommon.DateTimePickerPlaceholder' |
占位文字 i18n 键名 |
placeholder |
boolean |
true |
显示占位文字 |
todayKey |
string |
'~HxCommon.TodayButton' |
"现在"按钮 i18n 键名 |
clearKey |
string |
'~HxCommon.ClearButton' |
"清除"按钮 i18n 键名 |
startOfDayKey |
ReactNode |
'~HxCommon.StartOfDayButton' |
"开始"时间快捷按钮键值 |
noonOfDayKey |
ReactNode |
'~HxCommon.NoonOfDayButton' |
"正午"时间快捷按钮键值 |
endOfDayKey |
ReactNode |
'~HxCommon.EndOfDayButton' |
"结束"时间快捷按钮键值 |
confirmKey |
string |
'~HxCommon.OkButton' |
"确定"按钮 i18n 键名 |
valueSyncMode |
'default' | 'immediate' |
'default' |
编辑是否立即同步到模型 |
日本和历中可能存在年号在月份中期更替的情况。当使用 calendarLocale="ja-JP" 在日历面板中显示月份视图时,状态 ref 的 eraOfDays 方法委托给 DateLocaleUtils.eraOfDays(),后者路由到 DateJapaneseUtils.eraOfDays() —— 日语 DateLocaleNotGregorianProvider 插件实现 —— 以检测跨年号月份并精确定位更替日。
算法流程如下:
输入: daysOfThisMonth — 当月所有日期
输出: Map<Date, string> — 日期 → 年号标签(单年号月份返回空 Map)
1. 取当月第一天和最后一天。
2. 将两天格式化为数值形式以获取年号名称:
eraOfFirstDay, eraOfLastDay
3. 如果 eraOfFirstDay === eraOfLastDay → 返回空 Map(单年号月份)。
4. 否则,对 [0, daysOfThisMonth.length - 1] 执行二分查找:
a. 计算中间索引。
b. 将中间日格式化为数值形式以获取年号。
c. 如果中间日年号 === eraOfFirstDay → 搜索右半部分。
d. 如果中间日年号 !== eraOfFirstDay → 记录为 foundDay
并搜索左半部分。
5. 将 foundDay → eraOfLastDay 存入结果 Map。
6. 返回带新年号日期的年号标签 Map。
对于 1387 年 8 月(儒略历)的 至徳/嘉慶 年号更替,存在硬编码覆盖处理,因为该场景下 Intl.DateTimeFormat 产生的年号标签可能不一致。
触发器输入框和弹出面板转发大多数标准 DOM 事件。常用事件:onFocus、onBlur。值变更通过 $model/$field 绑定自动处理。
- Overview
- Common Concepts
- I18n
- Data Attributes
- Form Components
- Layout Components
- Display Components
- Overlay Components
- Structure Components
- Validation
- Hooks
- Utilities