鸿蒙原生 ArkTS 日历日程应用开发详解

运行截图:在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、项目概述

本文将详细介绍一个基于 HarmonyOS ArkTS 框架开发的日历日程应用。该应用采用 ArkUI 声明式 UI 开发范式,通过 Grid 组件实现月历日期网格展示,通过 List 组件展示选中日期的日程列表,并在日期格子中以彩色圆点直观标记当日事件类型。整个应用仅使用单一页面 Index.ets 实现全部功能,代码结构清晰、逻辑紧凑,非常适合 ArkTS 初学者学习和参考。

应用的核心功能包括:月份切换导航、日期选择与高亮、今日标记、事件圆点指示器、日程列表展示以及空状态提示。技术栈方面,项目完全基于 ArkTS 声明式开发范式,使用 @State 装饰器管理组件状态,@Builder 装饰器封装可复用的 UI 构建逻辑,ForEach 循环渲染列表数据,颜色资源统一管理在 color.json 中,遵循 HarmonyOS 推荐的资源引用规范。

从项目结构来看,这是一个标准的 HarmonyOS 应用工程。入口模块配置在 module.json5 中,声明了应用的主元素为 EntryAbility,目标设备类型为手机。页面路由配置在 main_pages.json 中,目前仅注册了一个页面 pages/Index。所有的 UI 逻辑集中在 Index.ets 文件中,颜色资源集中在 color.json 中进行统一管理。

二、技术架构与设计思路

2.1 整体架构

应用采用单页面架构设计,所有功能集中在一个 @Entry @Component 组件 Index 中实现。这种设计在日历类工具应用中非常常见,因为日历的核心交互(切换月份、选择日期、查看日程)本身就是一个连贯的单页面流程,无需复杂的页面跳转。

整体布局采用垂直方向的 Column 容器,从上到下依次排列三个主要区域:

  1. 顶部导航栏(TopBar):包含月份切换箭头、年月标题和"今天"快捷按钮
  2. 月历网格(CalendarGrid):星期标题行 + 6行7列的日期网格
  3. 日程列表(ScheduleList):选中日期的日程标题 + 日程卡片列表或空状态

三个区域之间通过 Divider 分隔线进行视觉分隔,整体布局紧凑而层次分明。

2.2 状态管理设计

应用使用四个 @State 装饰的状态变量驱动整个 UI 的更新:

  • currentYear:当前显示的年份,初始值为系统当前年份
  • currentMonth:当前显示的月份(0-11),初始值为系统当前月份
  • selectedDay:用户选中的日期,初始值为系统当前日期
  • schedules:日程数据数组,初始值由模拟数据生成函数填充

当用户点击月份切换箭头时,currentYearcurrentMonth 发生变化,ArkUI 框架自动检测到状态变更并重新渲染 CalendarGrid 区域,生成新月份的日期网格。当用户点击某个日期格子时,selectedDay 更新,框架重新渲染日期高亮状态和下方的日程列表。这种数据驱动的 UI 更新模式是 ArkUI 声明式开发范式的核心优势。

值得注意的是,应用中所有的数据查询方法(如 getSelectedSchedulesgetEventTypesForDay)都是普通成员方法而非 get 访问器。这是因为在 ArkTS 的 @Component 中,get 访问器与 @State 的响应式追踪配合不够稳定,使用显式方法调用可以确保状态变化后 UI 能正确刷新。

2.3 月历算法设计

月历网格的生成是整个应用最核心的算法逻辑。generateCalendarDays 函数接收年份和月份参数,返回一个包含 42 个(6行 × 7列)CalendarDate 对象的数组。

算法的关键步骤如下:

首先,计算当月第一天是星期几(firstDay),这决定了当月日期在网格中的起始位置。然后计算当月总天数(daysInMonth)和上月总天数(daysInPrevMonth),用于填充网格的空白位置。

对于 42 个单元格中的每一个,根据其索引位置判断属于哪个月份:

  • 索引小于 firstDay 的位置属于上个月,使用上月总天数计算对应的日期
  • 索引在 firstDayfirstDay + daysInMonth - 1 之间的位置属于当月
  • 索引大于等于 firstDay + daysInMonth 的位置属于下个月

每个 CalendarDate 对象还包含 isToday 标记,用于在渲染时区分今日高亮显示。这种固定 6 行的设计确保了所有月份的显示布局一致,避免了不同月份行数不同导致的 UI 跳动问题。

三、核心组件实现详解

3.1 顶部导航栏 TopBar

顶部导航栏使用 Row 水平布局容器,包含四个元素:左箭头、年月标题、"今天"按钮和右箭头。

左右箭头使用简单的 <> 文本符号,分别绑定 changeMonth(-1)changeMonth(1) 事件处理器。年月标题位于中央,使用模板字符串拼接为"2026年6月"的格式,字号 20、加粗显示。"今天"按钮使用主题蓝色(#007DFF),点击后调用 goToday() 方法将所有状态重置为系统当前日期。

changeMonth 方法在切换月份时处理了跨年边界:当月份从 0(一月)向前翻变为 11(上一年十二月),或从 11(十二月)向后翻变为 0(下一年一月)时,年份同步更新。同时,切换月份后将 selectedDay 重置为 1,确保选中状态始终有效。

3.2 月历网格 CalendarGrid

月历网格区域分为两部分:星期标题行和日期单元格网格。

星期标题行使用 Row 容器,通过 ForEach 循环渲染 ['日', '一', '二', '三', '四', '五', '六'] 七个文本标签。每个标签使用 layoutWeight(1) 平均分配宽度,字号 13,灰色(#999999)显示。

日期单元格网格使用 Grid 组件,关键配置如下:

Grid() {
  ForEach(generateCalendarDays(this.currentYear, this.currentMonth), (date, index) => {
    GridItem() {
      this.DateCell(date)
    }
  }, (date, index) => dateKeyStr(date.year, date.month, date.day) + date.isCurrentMonth.toString())
}
.columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr')
.rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr')
.rowsGap(2)
.columnsGap(0)
.aspectRatio(1.0)

columnsTemplate 声明 7 列等宽布局,rowsTemplate 声明 6 行等高布局,aspectRatio(1.0) 确保整个网格区域保持正方形比例,这样每个日期格子就是一个正方形单元格,视觉效果规整美观。ForEach 的第三个参数是 keyGenerator 函数,用于生成唯一的 key 值,确保列表数据变化时 ArkUI 能正确复用和更新 DOM 节点。

3.3 日期单元格 DateCell

DateCell 是一个 @Builder 方法,接收一个 CalendarDate 参数,负责渲染单个日期格子的 UI。

每个单元格由上下两部分组成:日期数字和事件圆点。日期数字部分是一个 36×36 的圆形 Text 组件,通过条件表达式动态设置样式:

  • 今日:蓝色背景(#007DFF)+ 白色文字 + 加粗字体,形成醒目的圆形高亮效果
  • 选中日期:浅蓝色背景(#E5F0FF),表示用户当前选中的日期
  • 当月普通日期:透明背景 + 深色文字(#333333
  • 非当月日期:透明背景 + 浅灰色文字(#CCCCCC),视觉上弱化处理

日期数字下方的事件圆点区域,仅在当月日期时显示。通过 getEventTypesForDay 方法查询该日期是否存在日程,如果有则渲染对应类型颜色的小圆点(直径 5px)。蓝色代表会议、橙色代表任务、绿色代表提醒。同一天有多种类型事件时,圆点横向排列,间距 2px。

点击日期格子的交互逻辑也有区分:当月日期点击后仅更新 selectedDay;非当月日期点击后会同时切换到对应月份并选中该日期,提供了便捷的跨月导航体验。

3.4 日程列表 ScheduleList

日程列表区域占据了屏幕下半部分,使用 layoutWeight(1) 自动填充剩余空间。

标题行显示"X月X日的日程"和日程数量(如"3项"),使用 Row 水平排列。当 getSelectedSchedules() 返回的数组长度大于 0 时,渲染 List 组件展示日程卡片;否则渲染空状态提示"暂无日程"。

每张日程卡片高度 72px,使用 Row 水平布局,左侧是 4px 宽的颜色条(颜色与日程类型对应),右侧是内容区域。内容区域包含两行:第一行是日程标题(加粗)和类型标签(带背景色的小标签),第二行是日程时间信息。

List 组件配置了 space: 10 设置卡片间距,scrollBar(BarState.Off) 隐藏滚动条,使界面更加简洁。ForEach 的 keyGenerator 使用日程的 id 字段,确保列表更新时的渲染性能。

四、数据模型与颜色体系

4.1 数据模型

应用定义了三个核心数据结构:

ScheduleType 枚举:定义三种日程类型——会议(MEETING)、任务(TASK)、提醒(REMINDER),每种类型对应一个中文标签。

ScheduleItem 接口:描述一条日程的完整信息,包含唯一标识 id、标题 title、时间 time、类型 type 和日期键 dateKey(格式为"YYYY-MM-DD"的字符串)。dateKey 是连接日程与日历日期的纽带,通过字符串匹配实现日期与日程的关联查询。

CalendarDate 接口:描述日历网格中每个单元格的信息,包含日期 day、月份 month、年份 year、是否当月 isCurrentMonth 和是否今日 isToday

辅助函数 dateKeyStr 负责将年月日数字格式化为"YYYY-MM-DD"标准日期字符串,供日期匹配使用。filterSchedulesgetEventTypesForDate 两个函数分别用于筛选指定日期的日程列表和提取去重后的事件类型列表。

4.2 颜色资源体系

所有颜色值统一定义在 color.json 资源文件中,通过 $r('app.color.xxx') 引用,便于统一管理和后续的主题适配。颜色体系分为以下几个层次:

基础色calendar_primary(#007DFF)作为应用主题色,用于"今天"按钮等强调元素。

日期网格色calendar_today_bg(#007DFF)今日背景、calendar_today_text(#FFFFFF)今日文字、calendar_selected_bg(#E5F0FF)选中背景、calendar_date_text(#333333)日期文字、calendar_other_month_text(#CCCCCC)非当月文字、calendar_weekday_text(#999999)星期标题文字。

事件圆点色calendar_event_dot_blue(#007DFF)会议、calendar_event_dot_orange(#FF8C00)任务、calendar_event_dot_green(#4CAF50)提醒。三种颜色对比鲜明,即使圆点直径仅 5px 也能清晰辨识。

日程列表色calendar_schedule_card_bg(#F8F9FA)卡片背景、calendar_schedule_title(#333333)标题文字、calendar_schedule_time(#666666)时间文字、calendar_divider(#F0F0F0)分隔线。

类型标签色calendar_schedule_tag_meetingcalendar_schedule_tag_taskcalendar_schedule_tag_reminder 分别对应三种日程类型,在列表卡片中以带圆角的彩色标签形式呈现。

通过将颜色集中管理,未来若需要适配深色模式或自定义主题,只需修改 color.json 中的色值即可,无需改动任何业务代码。

五、ArkTS 开发实践与踩坑记录

5.1 ForEach 必须提供 keyGenerator

在 ArkTS 中使用 ForEach 渲染列表时,第三个参数 keyGenerator 是必须的。如果省略,当列表数据发生变化时(如切换月份导致日期数组更新),ArkUI 无法正确识别哪些节点是新增、哪些是复用、哪些需要删除,可能导致 UI 渲染异常甚至崩溃。

ForEach(generateCalendarDays(this.currentYear, this.currentMonth), (date, index) => {
  GridItem() { this.DateCell(date) }
}, (date, index) => dateKeyStr(date.year, date.month, date.day) + date.isCurrentMonth.toString())

keyGenerator 的返回值必须在当前列表层级中唯一。对于日期网格,使用"日期key + 是否当月"的组合作为唯一标识;对于日程列表,使用日程的 id 作为唯一标识。

5.2 避免在 Component 中使用 get 访问器

ArkTS 的 @Component struct 中,get 访问器虽然在语法上合法,但在实际运行时可能无法正确触发 UI 的响应式更新。当 get 访问器内部读取了 @State 变量时,ArkUI 的状态追踪机制可能无法正确建立依赖关系,导致状态变化后 UI 不刷新。

解决方案是将 get 访问器改为普通的成员方法,在需要的地方显式调用。例如将 get selectedSchedules() 改为 getSelectedSchedules(),在 Builder 和模板中直接调用该方法即可。

5.3 避免使用 Set 等复杂数据结构

在 ArkTS 的运行时环境中,SetMap 等 ES6 数据结构可能存在兼容性问题,导致运行时错误。更安全的做法是使用普通数组配合 indexOf 方法来实现去重和查找逻辑。虽然在性能上略有差异,但对于日历应用这种小数据量场景完全可以接受。

5.4 枚举值的字符串转换

ArkTS 中枚举值不能直接通过 as string 进行类型强转。正确的做法是定义一个独立的转换函数(如 getScheduleTypeLabel),通过 switch-case 显式返回对应的字符串值。这种方式虽然代码量略多,但类型安全性更好,也更符合 ArkTS 的编程规范。

5.5 系统图标的兼容性

HarmonyOS 系统图标的资源路径(如 $r('sys.media.ohos_ic_public_arrow_left'))在不同 SDK 版本和设备类型上可能存在兼容性差异。如果预览或运行时出现图标加载失败的问题,可以使用简单的文字符号(如 <>)作为替代方案,确保在所有环境下都能正常显示。

六、模拟数据与功能扩展

6.1 模拟数据设计

应用内置了 11 条模拟日程数据,覆盖了当天、前 1 天和后 10 天的时间范围,包含会议、任务、提醒三种类型。数据以当前系统日期为基准动态生成,确保每次运行应用时都能在"今天"看到对应的日程内容。

这些模拟数据涵盖了多种典型场景:同一天多条不同类型日程(今天有 3 条)、间隔分布的单条日程(明天、后天各 1 条)、全天日程(“代码重构”、“准备演示文稿”)、单时间点提醒(“提交代码审查 16:30”)等。

6.2 功能扩展方向

基于当前的代码架构,可以方便地扩展以下功能:

持久化存储:将 schedules 数据从内存数组改为数据库存储,使用 HarmonyOS 的轻量级数据管理(Preferences)或关系型数据库(RDB)实现数据持久化,确保应用重启后日程数据不丢失。

日程增删改:添加日程编辑页面,支持用户创建、修改和删除日程。可以在日程列表区域添加"+"按钮,点击后跳转到编辑页面;日程卡片支持左滑删除或长按编辑。

事件提醒:结合 HarmonyOS 的后台任务管理(Background Task Manager)和通知服务(Notification),在日程开始前推送本地通知提醒用户。

周视图切换:在顶部导航栏添加周/月视图切换按钮,周视图模式下 Grid 改为 rowsTemplate('1fr'),仅显示一周的日期。

深色模式:利用 color.json 的资源引用机制,在 dark 目录下配置对应的深色模式颜色值,实现自动切换。

七、总结

本项目通过一个完整的日历日程应用,展示了 ArkTS 声明式 UI 开发的核心能力和最佳实践。从 @State 状态管理驱动 UI 更新,到 @Builder 封装可复用的 UI 片段,再到 GridList 组件的灵活运用,每一处实现都体现了 HarmonyOS ArkUI 框架的设计理念。

在开发过程中积累的经验同样重要:ForEach 必须提供 keyGenerator、避免使用 get 访问器替代普通方法、谨慎使用 Set 等复杂数据结构、枚举值需要显式转换等。这些实践总结对于后续开发更复杂的 ArkTS 应用具有普遍的指导意义。

整个应用虽然功能聚焦,但涵盖了状态管理、列表渲染、条件布局、颜色资源体系、日期算法等多个技术要点,是学习 HarmonyOS 应用开发的一个实用参考案例。随着 HarmonyOS 生态的持续发展,掌握 ArkTS 开发技能将为构建更丰富的原生应用打下坚实的基础。

Logo

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

更多推荐