鸿蒙新特性:DatePicker 日期选择器——构建纪念日管理器
在移动端表单中,日期选择是最常见的输入场景之一。生日、纪念日、截止日期——这些都需要一个直观的日期选择组件。与日历视图(CalendarPicker)不同,滚轮式日期选择器更适合快速翻选年月日,特别适合选择出生日期、历史日期等需要跨越较大时间范围的场景。
HarmonyOS NEXT ArkUI 提供了 DatePicker 组件——一个滚轮式的日期选择器,用户通过上下滑动年/月/日三列滚轮来精确选择日期。本文将深入讲解 DatePicker 的 API 和使用细节,并构建一个"纪念日管理器"——支持日期选择、事件命名、倒计时/正计时计算和分类筛选。
关键词:HarmonyOS、ArkUI、DatePicker、日期选择器、纪念日、倒计时、日期计算
一、DatePicker 组件 API
1.1 基本用法
DatePicker({ selected: this.pickDate })
.onChange((value: DatePickerResult) => {
let y = value.year ?? new Date().getFullYear();
let m = value.month ?? 0;
let d = value.day ?? 1;
this.pickDate = new Date(y, m, d);
})
核心参数与属性:
| 参数/属性 | 类型 | 说明 |
|---|---|---|
selected |
Date |
默认选中的日期 |
start |
Date |
可选日期范围的起始(可选) |
end |
Date |
可选日期范围的结束(可选) |
.onChange() |
callback | 选中日期变化时的回调(已标记为 deprecated) |
1.2 DatePickerResult 的可选字段
DatePicker 的 onChange 回调接收一个 DatePickerResult 对象,包含三个字段:
interface DatePickerResult {
year?: number; // 选中的年份(可能为 undefined)
month?: number; // 选中的月份(0-11,0=一月)
day?: number; // 选中的日期(1-31)
}
重要:这三个字段都是可选的(number | undefined)。在构建 Date 对象时必须处理 undefined 的情况:
.onChange((value: DatePickerResult) => {
let y = value.year ?? new Date().getFullYear();
let m = value.month ?? 0;
let d = value.day ?? 1;
this.pickDate = new Date(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 {
let m = (date.getMonth() + 1).toString().padStart(2, '0');
// 显示用 1-based:一月显示为 "01"
return y.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 数据类设计
class AnniversaryEvent {
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 {
let today = new Date();
today.setHours(0, 0, 0, 0);
let eventTime = new Date(this.dateStr);
eventTime.setHours(0, 0, 0, 0);
let diffMs = eventTime.getTime() - today.getTime();
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
}
getDaysLabel(): string {
let days = this.getDaysDiff();
if (days > 0) return '还有 '.concat(days.toString()).concat(' 天');
if (days === 0) return '今天';
return '已过 '.concat(Math.abs(days).toString()).concat(' 天');
}
isUpcoming(): boolean { return this.getDaysDiff() >= 0; }
isToday(): boolean { return this.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 {
let y = date.getFullYear();
let m = date.getMonth() + 1;
let d = date.getDate();
let weekDays = ['日', '一', '二', '三', '四', '五', '六'];
let w = weekDays[date.getDay()];
return y.toString().concat('年').concat(m.toString()).concat('月')
.concat(d.toString()).concat('日 周').concat(w);
}
2.4 添加事件
addEvent(): void {
let name = this.eventName.trim();
if (name.length === 0) {
promptAction.showToast({ message: '请输入事件名称', duration: 1500 });
return;
}
let dateStr = this.formatDate(this.pickDate);
let event = new AnniversaryEvent(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 {
let today = new Date();
today.setHours(0, 0, 0, 0);
let eventTime = new Date(this.dateStr);
eventTime.setHours(0, 0, 0, 0);
let diffMs = eventTime.getTime() - today.getTime();
return Math.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 三种视图
@State activeTab: string = 'all'; // 'all' | 'upcoming' | 'past'
与上一篇 Checkbox 任务清单的筛选逻辑类似,纪念日管理器使用三态筛选:
activeTab |
含义 | 筛选条件 |
|---|---|---|
'all' |
全部 | 不过滤 |
'upcoming' |
即将到来 | isUpcoming() === true(含"今天") |
'past' |
已过去 | isUpcoming() === false |
4.2 删除时的筛选感知
deleteEvent 方法需要感知当前筛选状态——用户可能在"即将到来"视图下删除某个事件,但该事件在完整列表中的索引与筛选后列表的索引不同。Demo 中采用"按名称+日期匹配删除"的策略:
deleteEvent(index: number): void {
let sourceEvents = this.getFilteredEvents();
let target = sourceEvents[index];
// 从完整列表中移除匹配的事件
let fullNew: AnniversaryEvent[] = [];
let skipName = target.name;
let skipDate = target.dateStr;
for (let i = 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 字段:
class AnniversaryEvent {
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 的滚轮交互让用户在海量的日期空间中快速定位——对于需要跨越数十年范围的日期选择(如出生日期),它比日历视图高效得多。配合日期计算和状态管理,开发者可以轻松构建纪念日、倒计时、事件规划等各种时间相关功能。
更多推荐



所有评论(0)