引言

很多应用都会产生带时间属性的事件——买了张火车票、预约了一场直播、信用卡该还款了、晚上有节线上课。这些信息散落在各个应用里,用户需要自己记住或者手动添加提醒,难免遗漏。

鸿蒙的 Calendar Kit 提供了一种更好的方式:让应用直接把这些事件写入系统日历。写入后,日程会通过通知中心、桌面卡片、日历应用等多个入口触达用户,还能配上一个**“一键服务"按钮**,用户点一下就能跳回应用完成操作——比如"加入会议”、“马上还款”、“立即观看”。

本文面向希望接入日历服务的鸿蒙开发者,梳理日历日程的创建机制、一键服务的场景设计,并通过两个典型场景的完整实现,帮你快速理解开发要点。


一、日历服务能做什么

1.1 核心能力

简单说就是三件事:

  1. 写入日程:把应用中带时间属性的事件以标准格式写入系统日历,包括标题、时间、地点、备注等信息。
  2. 提醒用户:通过系统级的提醒机制,在日程开始前的指定时间通知用户。
  3. 一键直达:在日程卡片上提供服务按钮,用户点击后通过 DeepLink 跳回应用的对应页面,完成后续操作。

写入日历的日程会出现在多个地方——日历应用内部、桌面日历卡片、通知中心。用户不需要打开你的应用就能看到这些信息,这对提升事件的到达率很有帮助。

1.2 一键服务按钮的显示时机

一键服务按钮不是一直显示的,不同入口的出现时机不一样:

入口 显示时机
桌面卡片 / 月视图日程列表卡片 日程开始前 15 分钟显示,日程结束后自动隐藏
日程详情页 始终显示
日程通知 通知弹出时显示,点击通知卡片后显示

这意味着一键服务按钮是有"时效性"的——它在用户最需要行动的时间窗口出现,而不是一直挂在那里。

1.3 9 种典型服务场景

Calendar Kit 为不同的业务场景预定义了服务类型,每种类型对应一个具体的按钮文案:

场景 ServiceType 按钮文案
会议 Meeting 加入会议
追剧 Watching 立即观看
还款 Repayment 马上还款
直播 Live 开启直播
购物 Shopping 开始选购
出行 Trip 立即查看
上课 Class 开始上课
赛事 SportsEvents 立即观看
运动 SportsExercise 开始运动

选择合适的服务类型,按钮文案就会自动匹配,不需要开发者自定义。


二、开发前的准备工作

在写第一行业务代码之前,有三步准备工作需要完成:

第一步,导入依赖。日历管理相关的能力都在 @kit.CalendarKit 中。

第二步,申请权限。日历是用户的私有数据,读写操作需要在 module.json5 中声明两个权限:ohos.permission.READ_CALENDARohos.permission.WRITE_CALENDAR

第三步,获取日程管理器对象。通过上下文获取 calendarMgr 对象,后续所有日历账户和日程的管理操作都通过它来进行。推荐在 EntryAbility.ets 中完成这一步,确保管理器对象在应用生命周期内可用。


三、理解日程的数据结构

在看具体场景之前,先理解日程涉及的几个关键概念,后面写代码时会更清楚为什么要这样配置。

3.1 日历账户

每个写入系统日历的日程都归属于一个日历账户。你可以理解为日历中的一个"分组"——用户打开日历应用时,能看到来自不同应用的日程被归类在各自的账户下。

账户有三个关键属性:

  • name:账户标识,供系统内部使用。
  • type:账户类型,一般使用 LOCAL
  • displayName:展示给用户看的名称,建议和应用在应用市场中的名称保持一致,方便用户识别"这个日程是哪个应用写的"。

3.2 日程字段

一条日程的核心字段包括:

  • title:标题,出现在日程卡片上最醒目的位置。
  • startTime / endTime:起止时间,时间戳格式。
  • isAllDay:是否是全天日程。全天日程不会显示具体时刻,适合"入住日""还款日"这类以天为单位的事件。
  • reminderTime:提醒时间,是一个数组,单位是分钟。比如 [0, 10] 表示日程开始时和开始前 10 分钟各提醒一次。对于全天日程,0 表示当天上午 9 点提醒,1440(即 24 小时)表示前一天上午 9 点提醒。
  • description:备注信息,可以补充标题里放不下的细节。
  • location:地点信息,包含地址文本和经纬度。
  • service:一键服务配置,包括服务类型(type)和跳转链接(uri,DeepLink 格式)。

3.3 日程的增删改查

日历服务提供了完整的 CRUD 操作。创建日历账户后,可以在该账户下添加日程、按条件查询日程、更新日程信息、删除日程。后面的场景示例中会展示这些操作的具体写法。


四、典型场景实践

下面通过两个最常见的场景——出行服务会议——来完整走一遍开发流程。其他场景(直播、购物、还款、课程等)的开发思路完全一致,区别只在于字段内容和 ServiceType 的选择。

4.1 出行服务场景

这大概是最容易理解的场景了:用户在购票应用里买了一张高铁票,应用把行程信息写入日历,出发前提醒用户,用户还能一键跳回应用查看电子客票。

字段设计思路:

标题要一目了然,建议包含车次和起终点,比如"行程信息:G107 上海虹桥-北京南"。备注里可以放检票口和座位号这类到了车站才需要的细节。提醒时间设两个——4 小时前提醒用户该出门了,2 小时前再提醒一次。一键服务类型选 TRIP

创建日程:

import { calendarMgr } from '../entryability/EntryAbility';
import { calendarManager } from '@kit.CalendarKit';

let tripCalendar: calendarManager.Calendar | undefined = undefined;
let oriEvent: calendarManager.Event | null = null;
let id: number = 0;

async createTripCalendarAndEvent(): Promise<void> {
  const calendarAccount: calendarManager.CalendarAccount = {
    name: 'TripCalendar',
    type: calendarManager.CalendarType.LOCAL,
    displayName: '高铁出行'
  };

  const config: calendarManager.CalendarConfig = {
    color: '#aabbcc'
  };

  const startTime = new Date('2025-10-01T08:17:00').getTime();
  const endTime = new Date('2025-10-01T12:51:00').getTime();

  const event: calendarManager.Event = {
    type: calendarManager.EventType.NORMAL,
    title: '行程信息:G107 上海虹桥-北京南',
    startTime: startTime,
    endTime: endTime,
    isAllDay: false,
    reminderTime: [120, 240],
    description: '检票口:南二楼1口或北广场B2候车室 \n座位号:02车04二等座',
    service: {
      type: calendarManager.ServiceType.TRIP,
      uri: 'demo://mobile/player?params='
    }
  };

  try {
    tripCalendar = await calendarMgr?.createCalendar(calendarAccount);
    if (!tripCalendar) {
      console.error('Failed to create calendar.');
      return;
    }
    await tripCalendar.setConfig(config);
    id = await tripCalendar.addEvent(event);
    oriEvent = event;
    oriEvent.id = id;
    console.info(`日程创建成功,ID: ${id}`);
  } catch (error) {
    console.error(`创建失败: ${error.code}, ${error.message}`);
  }
}

这段代码做了三件事:创建日历账户、设置账户配色、添加日程。注意一定要确保日历账户创建成功后再进行日程操作,否则后续调用会失败。

日程的后续管理:

出行场景下,行程变更是常有的事——改签了车次、换了出发时间。这时候需要更新已有日程而不是删掉重建:

async updateTripEvent(): Promise<void> {
  if (!tripCalendar || !oriEvent) return;
  
  // 改签后更新起止时间
  oriEvent.startTime = new Date('2025-10-01T07:03:00').getTime();
  oriEvent.endTime = new Date('2025-10-01T11:51:00').getTime();

  try {
    await tripCalendar.updateEvent(oriEvent);
    console.info('日程更新成功');
  } catch (err) {
    console.error(`更新失败: ${err.code}, ${err.message}`);
  }
}

如果用户退票了,则直接删除日程:

async deleteTripEvent(): Promise<void> {
  if (!tripCalendar) return;
  try {
    await tripCalendar.deleteEvent(id);
    oriEvent = null;
    console.info('日程已删除');
  } catch (err) {
    console.error(`删除失败: ${err.code}, ${err.message}`);
  }
}

需要查询已有日程时,通过 EventFilter.filterById 按 ID 查询:

async getTripEvent(): Promise<void> {
  if (!tripCalendar) return;
  try {
    const filter = calendarManager.EventFilter.filterById([id]);
    let data = await tripCalendar.getEvents(filter, 
      ['title', 'type', 'startTime', 'endTime']);
    if (data && data.length > 0) {
      oriEvent = data[0];
    }
  } catch (err) {
    console.error(`查询失败: ${err.code}, ${err.message}`);
  }
}

4.2 会议场景

会议场景和出行的最大区别在于:它有与会人信息。用户在会议应用中创建或被邀请参加一个会议,应用将其写入日历,到时间时用户看到提醒,点击"加入会议"按钮就能直接进入会议。

字段设计思路:

标题就是会议主题。提醒时间设准时和 15 分钟前——太早没意义,太晚来不及。会议场景特有的是 attendee 字段,用来记录与会人信息,每个与会人有姓名、邮箱、角色(组织者还是参与者)和类型(必选还是可选)。

async createMeetingEvent(): Promise<void> {
  const calendarAccount: calendarManager.CalendarAccount = {
    name: 'meetingCalendar',
    type: calendarManager.CalendarType.LOCAL,
    displayName: '会议'
  };

  const config: calendarManager.CalendarConfig = {
    color: '#aabbcc'
  };

  let attendee: calendarManager.Attendee[] = [
    {
      name: 'Alice',
      email: 'alice@example.com',
      role: calendarManager.AttendeeRole.ORGANIZER
    },
    {
      name: 'Jack',
      email: 'jack@example.com',
      role: calendarManager.AttendeeRole.PARTICIPANT,
      type: calendarManager.AttendeeType.REQUIRED
    },
    {
      name: 'Jerry',
      email: 'jerry@example.com',
      role: calendarManager.AttendeeRole.PARTICIPANT,
      type: calendarManager.AttendeeType.REQUIRED
    }
  ];

  const startTime = new Date('2025-10-20T09:00:00').getTime();
  const endTime = new Date('2025-10-20T10:00:00').getTime();

  const event: calendarManager.Event = {
    type: calendarManager.EventType.NORMAL,
    title: '产品方案评审会议',
    startTime: startTime,
    endTime: endTime,
    isAllDay: false,
    reminderTime: [0, 15],
    attendee: attendee,
    description: 'Q4产品方案评审',
    service: {
      type: calendarManager.ServiceType.MEETING,
      uri: 'demo://mobile/player?params='
    }
  };

  try {
    let calendar = await calendarMgr?.createCalendar(calendarAccount);
    if (!calendar) return;
    await calendar.setConfig(config);
    id = await calendar.addEvent(event);
    console.info(`会议日程创建成功,ID: ${id}`);
  } catch (error) {
    console.error(`创建失败: ${error.code}, ${error.message}`);
  }
}

与会人信息会展示在日程详情中,帮助用户确认参会人员。对于会议应用来说,service.uri 中的 DeepLink 通常会携带会议室 ID 等参数,用户点击"加入会议"后直接进入对应的会议房间。


五、其他场景速览

前面详细讲了出行和会议两个场景的完整实现,其他场景的代码结构完全一样,差异只在字段内容的填写上。这里简要列出几个场景的要点,帮你快速对照:

酒店住宿:适合设置为全天日程(isAllDay: true),标题包含酒店名称和地址,别忘了填 location 字段(包含经纬度),ServiceType 用 TRIP。提醒建议设前一天上午 9 点(reminderTime: [1440])和当天上午 9 点(reminderTime: [0])。

还款提醒:也是全天日程,毕竟还款日是以"天"为单位的。备注里写上待还款金额,ServiceType 用 REPAYMENT,提醒一次就够了——当天上午 9 点(reminderTime: [0])。

直播 / 抢购 / 课程 / 赛事 / 运动:都是精确到具体时刻的非全天日程,提醒时间一般设准时和开始前 10-30 分钟。区别就是选对 ServiceType,按钮文案就会自动匹配。


六、总结与实践建议

日历服务的接入逻辑并不复杂——创建账户、配置日程、写入系统。但要把体验做好,有几个细节值得注意:

  1. 标题要有信息量。用户在桌面卡片上看到的可能只有标题,所以"G107 上海虹桥-北京南"远比"火车票行程"有用。
  2. 提醒时间要合理。出行类提前 2-4 小时,会议和课程提前 10-15 分钟,全天日程用上午 9 点。不要设过多提醒,免得用户觉得被打扰。
  3. 及时更新和清理。行程改签了就更新日程,退票了就删除。不要让过期或无效的日程留在用户的日历里,这会损害用户对应用的信任。
  4. displayName 要用应用真名。用户看到一条日程时,会通过日历账户名称判断"这是哪个应用写的"。用正式的应用名称,而不是内部代号或缩写。
  5. DeepLink 要能真正落地。一键服务按钮点下去后跳转的链接,必须能正确打开应用的对应页面。如果链接失效或跳错位置,这个功能反而会让用户感到困惑。

日历是一个天然的时间管理入口,用户每天都会看。把应用中有价值的时间事件写进去,既帮用户管理好了日程,也为应用争取到了在系统级入口的露出机会。

Logo

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

更多推荐