鸿蒙新特性:DatePicker 日期选择器——构建纪念日管理器
在移动端表单中,日期选择是最常见的输入场景之一。生日、纪念日、截止日期——这些都需要一个直观的日期选择组件。与日历视图(CalendarPicker)不同,滚轮式日期选择器更适合快速翻选年月日,特别适合选择出生日期、历史日期等需要跨越较大时间范围的场景。
HarmonyOS NEXT ArkUI 提供了 DatePicker 组件——一个滚轮式的日期选择器,用户通过上下滑动年/月/日三列滚轮来精确选择日期。本文将深入讲解 DatePicker 的 API 和使用细节,并构建一个"纪念日管理器"——支持日期选择、事件命名、倒计时/正计时计算和分类筛选。
关键词:HarmonyOS、ArkUI、DatePicker、日期选择器、纪念日、倒计时、日期计算
一、DatePicker 组件 API
1.1 基本用法
DatePicker({selected:this.pickDate}).onChange((value:DatePickerResult)=>{lety=value.year??newDate().getFullYear();letm=value.month??0;letd=value.day??1;this.pickDate=newDate(y,m,d);})核心参数与属性:
| 参数/属性 | 类型 | 说明 |
|---|---|---|
selected | Date | 默认选中的日期 |
start | Date | 可选日期范围的起始(可选) |
end | Date | 可选日期范围的结束(可选) |
.onChange() | callback | 选中日期变化时的回调(已标记为 deprecated) |
1.2 DatePickerResult 的可选字段
DatePicker 的onChange回调接收一个DatePickerResult对象,包含三个字段:
interfaceDatePickerResult{year?:number;// 选中的年份(可能为 undefined)month?:number;// 选中的月份(0-11,0=一月)day?:number;// 选中的日期(1-31)}重要:这三个字段都是可选的(number | undefined)。在构建Date对象时必须处理undefined的情况:
.onChange((value:DatePickerResult)=>{lety=value.year??newDate().getFullYear();letm=value.month??0;letd=value.day??1;this.pickDate=newDate(y,m,d);})使用??(空值合并运算符)为undefined提供合理的默认值。不处理undefined会导致编译错误:Argument of type 'number | undefined' is not assignable to parameter of type 'number'。
1.3 month 的索引问题
DatePickerResult.month返回的是0-based月份索引——即 0 代表一月,1 代表二月,…,11 代表十二月。这与 JavaScript 的Date构造函数中month参数的语义一致。
但Date.getMonth()也返回 0-based 的月份,所以直接用于new Date(y, m, d)是正确的。然而在显示时需要加 1:
formatDate(date:Date):string{letm=(date.getMonth()+1).toString().padStart(2,'0');// 显示用 1-based:一月显示为 "01"returny.toString().concat('-').concat(m).concat('-').concat(d);}1.4 onChange 的 deprecated 状态
在当前 API 版本中,DatePicker 的.onChange()被标记为deprecated。虽然仍然可以使用(不会导致编译错误,仅为警告),但未来版本可能会移除。由于 ArkUI 的 DatePicker 目前没有公开的替代回调 API(如onDateChange),现阶段继续使用.onChange()是唯一可行的方案。当官方提供替代 API 时,迁移也非常简单——只需修改回调方法名。
1.5 DatePicker vs CalendarPicker
这是读者容易混淆的两个组件:
| 维度 | DatePicker | CalendarPicker |
|---|---|---|
| 交互形态 | 滚轮翻选(年月日三列) | 日历网格(月视图) |
| 日期范围 | 适合大跨度(几十年) | 适合近距选择(几个月内) |
| 空间占用 | 固定高度,可嵌入表单 | 需要较大空间展示整月 |
| 典型场景 | 出生日期、证件有效期 | 日程安排、事件日期查询 |
| 视觉效果 | 选中行高亮居中 | 选中日期圆圈高亮 |
选择建议:
- 需要快速选择跨越几十年的日期(如出生年份选到 1980 年)→ 用DatePicker,滚轮翻选比翻日历快得多
- 需要展示月度视图、查看某天是否有事件 → 用CalendarPicker
二、纪念日管理器的整体设计
2.1 页面架构
DatePickerPage ├── 标题栏 — "纪念日管理" + 事件计数 ├── 日期选择卡片 │ ├── DatePicker 组件(年月日三列滚轮) │ └── 格式化日期显示(含星期) ├── 添加事件卡片 │ ├── 事件名称输入框 │ └── "添加事件"按钮 └── 事件列表卡片 ├── 筛选标签(全部/即将到来/已过去) ├── 事件行(名称 + 日期 + 倒计时/正计时 + 删除) └── 空状态提示2.2 数据类设计
classAnniversaryEvent{name:string;dateStr:string;// YYYY-MM-DD 格式,用于 Date 解析timestamp:number;constructor(name:string,dateStr:string,timestamp:number){this.name=name;this.dateStr=dateStr;this.timestamp=timestamp;}getDaysDiff():number{lettoday=newDate();today.setHours(0,0,0,0);leteventTime=newDate(this.dateStr);eventTime.setHours(0,0,0,0);letdiffMs=eventTime.getTime()-today.getTime();returnMath.floor(diffMs/(1000*60*60*24));}getDaysLabel():string{letdays=this.getDaysDiff();if(days>0)return'还有 '.concat(days.toString()).concat(' 天');if(days===0)return'今天';return'已过 '.concat(Math.abs(days).toString()).concat(' 天');}isUpcoming():boolean{returnthis.getDaysDiff()>=0;}isToday():boolean{returnthis.getDaysDiff()===0;}}getDaysDiff()是核心计算方法。它将"今天"和"事件日期"的时间部分都归零(setHours(0,0,0,0)),只保留日期部分进行比较。毫秒差除以1000 * 60 * 60 * 24得到天数差,Math.floor()取整。
正数表示未来日期(“还有 X 天”),0 表示今天,负数表示过去的日期(“已过 X 天”)。
isUpcoming()判断事件是否即将到来。注意这里用的是>= 0——“今天"也算入"即将到来”,因为今天的事件确实还没有过去。
2.3 日期格式化
Demo 中实现了两种格式化方式:
存储格式(formatDate):YYYY-MM-DD,用于dateStr字段。这个格式可以直接传给new Date()构造函数精确解析。
显示格式(formatDisplay):中文日期 + 星期,例如"2026年7月8日 周三"。星期通过date.getDay()获取(0=周日,1=周一,…,6=周六),映射到中文星期名数组。
formatDisplay(date:Date):string{lety=date.getFullYear();letm=date.getMonth()+1;letd=date.getDate();letweekDays=['日','一','二','三','四','五','六'];letw=weekDays[date.getDay()];returny.toString().concat('年').concat(m.toString()).concat('月').concat(d.toString()).concat('日 周').concat(w);}2.4 添加事件
addEvent():void{letname=this.eventName.trim();if(name.length===0){promptAction.showToast({message:'请输入事件名称',duration:1500});return;}letdateStr=this.formatDate(this.pickDate);letevent=newAnniversaryEvent(name,dateStr,Date.parse(dateStr));this.events=this.events.slice().concat(event);this.eventName='';promptAction.showToast({message:'已添加:'.concat(name),duration:1500});}trim()去空格后检查是否为空- 将 DatePicker 选中的日期格式化为
YYYY-MM-DD Date.parse(dateStr)获取时间戳(备用,实际计算使用new Date(dateStr))slice().concat(event)创建新数组更新@State
三、倒计时与正计时
3.1 天数计算
getDaysDiff():number{lettoday=newDate();today.setHours(0,0,0,0);leteventTime=newDate(this.dateStr);eventTime.setHours(0,0,0,0);letdiffMs=eventTime.getTime()-today.getTime();returnMath.floor(diffMs/(1000*60*60*24));}setHours(0,0,0,0)是关键——它消除了时间部分对日期比较的影响。如果不归零时间部分,同一天的不同时刻(如上午 8 点和下午 6 点)会得到不同的天数差。
3.2 三种状态的视觉呈现
| 状态 | getDaysDiff() | 显示文字 | 文字颜色 |
|---|---|---|---|
| 未来 | > 0 | “还有 X 天” | 蓝色#1677FF |
| 今天 | === 0 | “今天” | 绿色#52C41A |
| 已过 | < 0 | “已过 X 天” | 灰色#BBBBCC |
Text(event.getDaysLabel()).fontSize(14).fontColor(event.isToday()?'#52C41A':(event.isUpcoming()?'#1677FF':'#BBBBCC')).fontWeight(FontWeight.Bold)三种颜色传达三种时间关系——蓝色表示期待(即将到来)、绿色表示正在发生(今天)、灰色表示回忆(已过去)。
四、分类筛选
4.1 三种视图
@StateactiveTab:string='all';// 'all' | 'upcoming' | 'past'与上一篇 Checkbox 任务清单的筛选逻辑类似,纪念日管理器使用三态筛选:
activeTab | 含义 | 筛选条件 |
|---|---|---|
'all' | 全部 | 不过滤 |
'upcoming' | 即将到来 | isUpcoming() === true(含"今天") |
'past' | 已过去 | isUpcoming() === false |
4.2 删除时的筛选感知
deleteEvent方法需要感知当前筛选状态——用户可能在"即将到来"视图下删除某个事件,但该事件在完整列表中的索引与筛选后列表的索引不同。Demo 中采用"按名称+日期匹配删除"的策略:
deleteEvent(index:number):void{letsourceEvents=this.getFilteredEvents();lettarget=sourceEvents[index];// 从完整列表中移除匹配的事件letfullNew:AnniversaryEvent[]=[];letskipName=target.name;letskipDate=target.dateStr;for(leti=0;i<this.events.length;i++){if(this.events[i].name===skipName&&this.events[i].dateStr===skipDate){skipName='';// 只删除第一个匹配项}else{fullNew=fullNew.slice().concat(this.events[i]);}}this.events=fullNew;}通过匹配name + dateStr在完整列表中定位要删除的事件,确保在任意筛选视图下都能正确删除。
五、交互流程演示
5.1 选择日期
进入页面,DatePicker 显示当前日期。上下滑动年、月、日三列滚轮来改变日期。格式化显示区实时更新——例如选择 2026 年 12 月 25 日,显示"2026年12月25日 周五"。
5.2 添加事件
在输入框中输入"圣诞节",点击"添加事件"。Toast 提示"已添加:圣诞节"。事件列表出现一条记录:名称"圣诞节"、日期"2026-12-25"、倒计时"还有 170 天"(蓝色)。
再添加一个已过去的日期——将 DatePicker 调到"2026-01-01",输入"元旦"。倒计时显示"已过 188 天"(灰色)。
5.3 筛选视图
点击"即将到来"标签——列表只显示未来的事件(包括今天)。元旦(已过 188 天)被隐藏。
点击"已过去"标签——列表只显示过去的事件。圣诞节被隐藏,只看到元旦。
点击"全部"标签——恢复显示所有事件。
5.4 删除事件
点击某条事件右侧的红色"删除"按钮。Toast 提示"已删除",该事件从所有视图中消失。标题栏的事件计数减 1。
5.5 今日事件
将 DatePicker 调到今天的日期,输入一个事件名称,点击"添加事件"。事件列表显示"今天"标签(绿色),与其他"还有 X 天"或"已过 X 天"形成对比。
六、实用扩展建议
6.1 重复提醒
当前 Demo 中的事件是一次性的——每个事件对应一个具体日期。实际应用通常需要重复提醒(每年生日、每月还款日等)。可以扩展AnniversaryEvent类增加repeat字段:
classAnniversaryEvent{repeat:string;// 'none' | 'yearly' | 'monthly'// ...}对于"每年重复"的事件,计算天数差时取下一年的对应日期。
6.2 通知提醒
结合@ohos.notification能力,在纪念日到来时推送系统通知。可以在事件添加时计算剩余天数并设置定时提醒。
6.3 农历支持
中文纪念日场景中,农历日期非常常见(如春节、中秋节)。可以集成农历转换库,在 DatePicker 选中的公历日期旁边显示对应的农历日期。
七、总结
本文通过"纪念日管理器"这个实战案例,全面讲解了 ArkUI DatePicker 日期选择器组件的使用方法。核心知识点包括:
- DatePicker 基础 API:
selected默认日期 +.onChange()回调获取选中结果 - DatePickerResult 可选字段处理:
year/month/day均为number | undefined,需用??提供默认值 - month 索引陷阱:
DatePickerResult.month为 0-based(0=一月),显示时需 +1 - onChange deprecated 警告:当前仍可使用,未来需要迁移到新 API
- 日期天数计算:
setHours(0,0,0,0)归零时间 + 毫秒差 → 天数整数 - 三种时间状态:未来(“还有 X 天”/蓝色)、今天(“今天”/绿色)、已过(“已过 X 天”/灰色)
- 筛选感知删除:按
name + dateStr在完整列表中定位要删除的事件 - DatePicker vs CalendarPicker:交互形态(滚轮 vs 日历网格)和适用场景的差异
日期选择是移动端的核心输入方式之一。DatePicker 的滚轮交互让用户在海量的日期空间中快速定位——对于需要跨越数十年范围的日期选择(如出生日期),它比日历视图高效得多。配合日期计算和状态管理,开发者可以轻松构建纪念日、倒计时、事件规划等各种时间相关功能。
