日历选择是移动应用中极为常见的基础交互。无论是预约挂号、酒店预订、会议日程还是活动报名,用户都需要一个直观的日历界面来浏览日期并做出选择。HarmonyOS NEXT ArkUI 提供了 CalendarPicker 组件,以优雅的文本入口承载日历浏览功能——点击弹出日历对话框,月份可左右滑动,年份可快速跳转,日期一目了然。

本文将深入讲解 CalendarPicker 组件的使用方法,并构建一个完整的"日历事件管理"应用——用户可以选择任意日期,为每个日期添加事件,查看和管理日程安排。

关键词:HarmonyOS、ArkUI、CalendarPicker、日历选择器、事件管理、日程规划

一、CalendarPicker 组件基础

1.1 API 概述

CalendarPicker 是 ArkUI 提供的日历选择组件。与传统的日历视图不同,CalendarPicker 采用"入口文本 + 弹出对话框"的设计模式——组件本身在页面上显示为一个可点击的文本区域(展示当前所选日期),点击后弹出完整的日历对话框供用户浏览和选择。

CalendarPicker({ selected: this.selectedDate })
  .edgeAlign(CalendarAlign.CENTER)
  .textStyle({ fontSize: 16, fontColor: '#1677FF' })
  .onChange((value: Date) => {
    // value 为用户选择的日期
    this.selectedDate = value;
  })

核心属性说明:

  • selected:CalendarOptions 中的预设选中日期,类型为 Date。页面初始化时,日历对话框默认选中此日期
  • edgeAlign:控制入口文本与日历对话框的对齐方式。可选值 CalendarAlign.START(左对齐)、CalendarAlign.CENTER(居中)、CalendarAlign.END(右对齐)
  • textStyle:设置入口文本的样式,通过 PickerTextStyle 类型指定字号、颜色等
  • onChange:用户选择日期后触发的回调函数,参数为选中的 Date 对象

CalendarPicker 从 API 10 开始提供,API 11 起支持原子化服务(atomicservice),是一个经过两个版本迭代的成熟组件。

1.2 与 DatePicker 的区别

很多开发者会混淆 CalendarPicker 和 DatePicker。两者虽然都是日期选择,但交互模式完全不同:

特性 CalendarPicker DatePicker
展示方式 文本入口 + 日历对话框 滚轮选择器
日期浏览 日历网格,直观可视化 纯数字滚动,无明显日期关系
适用场景 需要"看到日期在周几"的场景 快速选择日期,不关心是周几
交互成本 两次点击(打开 + 选择) 多次滑动滚轮
月份切换 左右滑动,支持年份快速跳转 独立月/日/年滚轮

简单来说,CalendarPicker 适合"我需要看日历来确定日期"的场景(如订酒店看哪天是周末),而 DatePicker 适合"我已经知道日期只需要快速输入"的场景(如设置生日)。

1.3 CalendarOptions 配置项

CalendarPicker 构造函数接受一个 CalendarOptions 对象,除 selected 外还有以下可选配置:

interface CalendarOptions {
  hintRadius?: number | Resource;   // 日期背景圆角半径
  selected?: Date;                  // 预选日期
  start?: Date;                     // API 18+ 日历起始日期
  end?: Date;                       // API 18+ 日历结束日期
  disabledDateRange?: DateRange[];  // API 19+ 禁用日期范围
}

startend 用于限制可选日期范围,例如酒店预订不能选择过去的日期,可以设置 start: new Date() 从今天开始。

二、日历事件管理的整体设计

2.1 页面架构

本文 Demo 构建一个"日历事件管理"应用,包含以下功能模块:

CalendarPickerPage
├── 标题栏 — 显示"日历事件管理"和当前星期几
├── 组件说明卡片 — CalendarPicker 介绍
├── CalendarPicker 组件 — 日历选择入口
├── 选中日期信息 — 日期文字 + 星期几 + 事件数量
├── 添加事件表单 — 事件标题 + 时间输入
└── 事件列表 — 选中日期的事件清单,支持删除

2.2 数据结构设计

日历事件管理需要建立"日期 → 事件列表"的映射关系。本文采用扁平化存储——所有事件存储在一个数组中,每个事件带有一个 dateKey 字段(格式为 YYYY-MM-DD),通过筛选获取特定日期的事件:

interface CalendarEvent {
  id: number;
  dateKey: string;  // '2026-07-08' 格式
  title: string;
  time: string;     // '14:30' 格式
}

@State allEvents: CalendarEvent[] = [];
@State filteredEvents: CalendarEvent[] = [];
@State selectedDate: Date = new Date();
@State dateKey: string = '';

这种扁平化结构的好处:

  1. 简单直观:增删改查都是标准数组操作
  2. 易于扩展:后续如需添加"事件提醒""事件分类"等字段,只需扩展 CalendarEvent 接口
  3. 持久化友好CalendarEvent[] 可以直接序列化为 JSON 存入本地存储

2.3 日期格式化

ArkTS 的 Date 对象本身不提供友好的格式化方法。本文封装了三个辅助函数:

// 格式化为 YYYY-MM-DD(用作存储 key)
formatDate(date: Date): string {
  let y = date.getFullYear();
  let m = (date.getMonth() + 1).toString().padStart(2, '0');
  let d = date.getDate().toString().padStart(2, '0');
  return y.toString().concat('-').concat(m).concat('-').concat(d);
}

// 格式化为中文显示(如 "2026年7月8日")
formatDisplay(date: Date): string {
  let y = date.getFullYear();
  let m = date.getMonth() + 1;
  let d = date.getDate();
  return y.toString().concat('年').concat(m.toString()).concat('月').concat(d.toString()).concat('日');
}

// 获取星期几的中文显示
getWeekday(date: Date): string {
  let days = ['日', '一', '二', '三', '四', '五', '六'];
  return '星期'.concat(days[date.getDay()]);
}

注意:Date.getMonth() 返回值是 0-11(JavaScript 标准),因此显示月份需要 + 1Date.getDay() 返回 0(周日)到 6(周六)。
在这里插入图片描述

三、日期选择与事件联动

3.1 日期变更处理

当用户在日历对话框中选择了新日期,onChange 回调触发,我们需要同步更新三个状态:

onDateChange(value: Date): void {
  this.selectedDate = value;                        // 更新选中日期
  this.dateKey = this.formatDate(value);            // 更新日期 key
  this.updateFilteredEvents();                      // 刷新事件列表
  this.showForm = false;                            // 关闭添加表单
}

关键设计决策:切换日期时自动关闭添加表单。这样做的原因是——用户换个日期通常意味着想在新的日期上添加事件,而表单中已经填写的标题和时间应该被重置(以防误操作把事件添加到错误的日期)。这是一个微妙的 UX 细节:清空表单虽然可能丢失用户已输入的内容,但防止了更严重的数据错误(事件被添加到错误的日期)。

3.2 事件筛选

updateFilteredEvents 方法从 allEvents 中筛选出当前选中日期的事件:

updateFilteredEvents(): void {
  this.filteredEvents = this.allEvents.filter(
    (e: CalendarEvent) => e.dateKey === this.dateKey
  );
}

Array.filter 返回新数组(不修改原数组),这在 ArkUI 的 @State 响应式系统中非常重要——赋值新数组引用才能触发 UI 更新。如果直接修改 filteredEvents 的内容而不更换引用,UI 不会感知变化。

3.3 初始状态

页面加载时,通过 aboutToAppear 生命周期初始化:

aboutToAppear(): void {
  this.dateKey = this.formatDate(this.selectedDate);
  this.updateFilteredEvents();
}

selectedDate 初始值为 new Date()(今天),因此页面打开后默认展示今天的日程。
在这里插入图片描述

四、事件添加功能

4.1 表单设计

添加事件表单使用两个 TextInput 组件——一个用于事件标题,一个用于事件时间:

// 事件标题输入
TextInput({ text: this.newTitle, placeholder: '事件名称' })
  .onChange((value: string) => {
    this.newTitle = value;
  })

// 时间输入
TextInput({ text: this.newTime, placeholder: 'HH:MM' })
  .onChange((value: string) => {
    this.newTime = value;
  })

时间输入采用自由文本格式(如 09:0014:30),灵活性高于滚轮选择器——用户想要"早上"可以输入 07:00,想要"下午"可以输入 15:00,想要"傍晚"可以输入 18:30。这符合日程管理的使用习惯:用户通常已经知道时间,不需要通过滚轮从 0-23 中慢慢寻找。

4.2 表单显示/隐藏切换

添加按钮使用 showForm 状态控制表单的显示与隐藏:

Row() {
  Text(this.showForm ? '取消添加' : '+ 添加事件')
}
.onClick(() => {
  this.showForm = !this.showForm;
})

按钮文字和颜色随状态变化:

  • 未展开时:蓝色背景,白色文字 “+ 添加事件”
  • 展开后:灰色背景,灰色文字 “取消添加”

这是一个常见的"展开/收起"模式——用户的注意力在需要时扩展到表单,不需要时收起到最小状态,减少页面视觉负担。

4.3 事件保存

addEvent 方法验证标题非空后,将新事件加入 allEvents 数组:

addEvent(): void {
  if (this.newTitle.trim().length === 0) {
    return;  // 空标题不保存
  }
  let newEvent: CalendarEvent = {
    id: this.nextId,
    dateKey: this.dateKey,
    title: this.newTitle.trim(),
    time: this.newTime
  };
  this.nextId++;
  this.allEvents = this.allEvents.slice().concat([newEvent]);
  this.updateFilteredEvents();
  this.newTitle = '';
  this.newTime = '09:00';
  this.showForm = false;
}

几个细节:

  • trim 处理:去除标题首尾空格,避免"看起来有文字实际上全是空格"的事件
  • slice + concat:先复制再追加,产生新数组引用以触发 UI 更新。直接 .push() 不改变引用,UI 不会刷新
  • 自增 IDnextId 每次加 1,保证 ID 唯一性。在真实应用中可能使用 UUID 或时间戳
  • 重置表单:保存后清空标题、重置时间为 09:00、关闭表单
  • 保存按钮状态:当 newTitle 为空时按钮变为灰色禁用态(.backgroundColor(this.newTitle.trim().length > 0 ? '#1677FF' : '#CCCCDD')

五、事件删除功能

5.1 删除实现

每个事件卡片右侧有一个红色"删除"按钮,点击后从 allEvents 中移除该事件:

removeEvent(id: number): void {
  this.allEvents = this.allEvents.slice()
    .filter((e: CalendarEvent) => e.id !== id);
  this.updateFilteredEvents();
}

同样采用 slice() + filter() 的组合——先复制数组,再过滤掉目标事件,生成新数组引用。

5.2 删除交互设计

删除按钮的设计遵循"可见但不突兀"原则:

  • 文字颜色 #FF4D4F(红色),表示危险操作
  • 背景 #FFF1F0(浅红),与文字呼应
  • 字号 11,小于正文,降低视觉权重
  • 无确认弹窗(日程管理的删除是可逆性较低的操作,且事件信息简单)

对于更重要的场景(如删除整个日期下的所有事件),建议增加确认对话框(AlertDialog)作为二次确认。

六、事件列表展示

6.1 有事件的展示

事件列表采用"时间圆 + 标题 + 删除按钮"的布局:

Row() {
  // 左侧:时间圆
  Column() {
    Text(event.time)
      .fontSize(16)
      .fontColor('#1677FF')
  }
  .width(52).height(52)
  .borderRadius(26)       // 圆形
  .backgroundColor('#F0F5FF')

  // 中间:标题 + 日期
  Column() {
    Text(event.title)
    Text(this.formatDisplay(this.selectedDate))
  }

  // 右侧:删除按钮
  Text('删除')
}

时间显示在圆形背景中(直径 52vp),视觉上类似手表表盘——这是一个信息层次设计:时间信息通过"独立的视觉容器"获得更高权重,用户扫视时一眼就能看到"几点钟有什么事"。

6.2 空状态设计

当选中日期没有事件时,展示空状态提示:

if (this.filteredEvents.length === 0) {
  Column() {
    Text('📅').fontSize(40)
    Text('暂无事件')
    Text('点击上方按钮为此日期添加事件')
  }
}

空状态不是简单的"暂无数据"——它包含三个层次:

  1. 图标(日历 emoji),建立视觉隐喻
  2. 状态说明(“暂无事件”),告知当前状态
  3. 行动指引(“点击上方按钮为此日期添加事件”),引导用户下一步操作

这种"空状态 + CTA"的设计模式在 SaaS 产品中已被广泛验证——空状态是引导用户首次操作的最佳时机。

七、选中的日期信息面板

在 CalendarPicker 和事件列表之间,有一个日期信息面板展示当前选中日期和事件统计:

Row() {
  Column() {
    Text(this.formatDisplay(this.selectedDate))  // '2026年7月8日'
    Text(this.getWeekday(this.selectedDate))     // '星期三'
  }
  Blank()
  Column() {
    Text(this.filteredEvents.length.toString())  // '3'
    Text('个事件')
  }
}

右侧的事件计数使用蓝色大字(22sp),与左侧的日期信息形成"类型对比"——日期是定性信息,计数是定量信息,两者并排让用户同时看到"哪一天"和"这天有多忙"。

八、生命周期管理

8.1 aboutToAppear

aboutToAppear(): void {
  this.dateKey = this.formatDate(this.selectedDate);
  this.updateFilteredEvents();
}

组件即将出现时,初始化 dateKeyfilteredEvents。这里不能省略——如果不调用 updateFilteredEvents(),页面初始展示的 filteredEvents 是空数组,即便今天已经有事件也不会显示。

8.2 关于 aboutToDisappear

本文 Demo 没有使用 aboutToDisappear()。在真实应用中,如果需要持久化日历事件(保存到本地存储或云端),可以在组件消失时触发保存逻辑。例如:

aboutToDisappear(): void {
  // 将 this.allEvents 序列化为 JSON 存入 PersistentStorage
}

对于更复杂的场景(如事件提醒),可能需要在 aboutToDisappear 中注册系统通知,或在 aboutToAppear 中恢复定时器。

九、CalendarPicker 的高级用法

9.1 限制日期范围

通过 startend 参数限制可选日期(API 18+):

CalendarPicker({
  selected: this.selectedDate,
  start: new Date(),                      // 从今天开始
  end: new Date(2027, 11, 31)            // 到 2027 年底
})

这在预订场景中非常有用——酒店不能预订过去的日期,会议不能预约到过于遥远的未来。

9.2 禁用特定日期

通过 disabledDateRange 禁用一段或多段日期(API 19+):

CalendarPicker({
  selected: this.selectedDate,
  disabledDateRange: [
    { start: new Date(2026, 6, 10), end: new Date(2026, 6, 15) }
  ]
})

被禁用的日期在日历对话框中显示为灰色不可点击状态。适用于假期、满房、维护日等场景。

9.3 对齐方式

edgeAlign 控制入口文本与日历对话框的弹出对齐:

CalendarPicker({ selected: this.selectedDate })
  .edgeAlign(CalendarAlign.START)   // 对话框从左边弹出
  .edgeAlign(CalendarAlign.CENTER)  // 对话框居中弹出
  .edgeAlign(CalendarAlign.END)     // 对话框从右边弹出

大多数情况下使用 CENTER 即可。如果 CalendarPicker 放置在页面左侧或右侧,可以调整对齐方向以优化视觉连贯性。

十、交互流程演示

10.1 浏览日历

用户点击 CalendarPicker 入口文本(显示为当前日期),系统弹出日历对话框。用户左右滑动切换月份,顶部年份旁有快速跳转按钮。点击某个日期格子,对话框关闭,onChange 回调触发。

10.2 添加第一个事件

选择 2026 年 7 月 10 日,点击"+ 添加事件"按钮,表单展开。输入标题"团队周会",时间 10:00,点击"保存事件"。表单关闭,日期信息面板显示"1 个事件",事件列表中出现一条记录。

10.3 添加更多事件

在同一天继续添加"午餐约会"(12:30)和"健身房"(18:00)。事件列表增长到 3 条,每条左侧显示对应时间。

10.4 切换日期查看

点击 CalendarPicker 切换到另一个日期(如 7 月 12 日),添加"产品评审"(14:00)。再切换回 7 月 10 日,之前 3 条事件仍在,证明数据按日期正确隔离。

10.5 删除事件

在 7 月 10 日的事件列表中,点击"午餐约会"旁边的"删除"按钮,该事件消失,事件计数从 3 变为 2。

十一、安全与数据持久化

11.1 当前方案的局限

本文 Demo 的所有事件数据存储在 @State 变量中——这意味着:

  • 页面关闭后所有数据丢失
  • 应用重启后数据无法恢复
  • 数据仅存在于内存中

11.2 持久化扩展建议

对于真实应用,推荐以下持久化路径:

  1. 轻量级方案:使用 PersistentStorage
// 保存
PersistentStorage.persistProp('calendarEvents', this.allEvents);
// 读取
PersistentStorage.persistProp('calendarEvents', []);
  1. 结构化方案:使用关系型数据库(relationalStore),按 dateKey 建立索引以加速日期查询。

  2. 云端同步方案:将事件数据同步到华为云空间或自建服务端,实现多设备数据同步。

11.3 输入安全

TextInputonChange 回调中,可以考虑对用户输入做以下校验:

  • 标题长度限制(如 50 字符)
  • 时间格式校验(正则匹配 \d{2}:\d{2}
  • 防止 XSS(虽然 ArkUI 文本渲染不执行脚本,但好习惯应保留)

十二、总结

本文通过"日历事件管理"这个实战案例,全面讲解了 ArkUI CalendarPicker 组件的使用方法。核心知识点包括:

  1. CalendarPicker API:selected、edgeAlign、textStyle、onChange
  2. 日历事件数据结构:扁平化存储 + dateKey 关联 + filter 筛选
  3. 日期格式化:YYYY-MM-DD 存储格式,中文显示格式,星期几计算
  4. 事件 CRUD:添加(slice+concat)、删除(slice+filter)、筛选(filter)
  5. ArkUI 响应式更新:必须使用新数组引用触发 UI 刷新(slice 模式)
  6. 空状态设计:图标 + 说明 + CTA 的三层结构

CalendarPicker 组件将复杂的日历浏览交互封装为"入口文本 + 弹出对话框"的简洁模式,开发者只需关注日期变更后的业务逻辑——更新事件列表、校验日期范围、处理日期联动。这种封装度在 ArkUI 的输入类组件中具有代表性:将"硬件级"的日历交互简化为一组配置参数和一个回调函数。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐