很多应用都会产生带时间属性的事件——买了张火车票、预约了一场直播、信用卡该还款了、晚上有节线上课。这些信息散落在各个应用里,用户需要自己记住或者手动添加提醒,难免遗漏。
鸿蒙的 Calendar Kit 提供了一种更好的方式:让应用直接把这些事件写入系统日历。写入后,日程会通过通知中心、桌面卡片、日历应用等多个入口触达用户,还能配上一个**"一键服务"按钮**,用户点一下就能跳回应用完成操作——比如"加入会议"、"马上还款"、"立即观看"。
本文面向希望接入日历服务的鸿蒙开发者,梳理日历日程的创建机制、一键服务的场景设计,并通过两个典型场景的完整实现,帮你快速理解开发要点。
简单说就是三件事:
写入日历的日程会出现在多个地方——日历应用内部、桌面日历卡片、通知中心。用户不需要打开你的应用就能看到这些信息,这对提升事件的到达率很有帮助。
一键服务按钮不是一直显示的,不同入口的出现时机不一样:
入口 | 显示时机 |
|---|---|
桌面卡片 / 月视图日程列表卡片 | 日程开始前 15 分钟显示,日程结束后自动隐藏 |
日程详情页 | 始终显示 |
日程通知 | 通知弹出时显示,点击通知卡片后显示 |
这意味着一键服务按钮是有"时效性"的——它在用户最需要行动的时间窗口出现,而不是一直挂在那里。
Calendar Kit 为不同的业务场景预定义了服务类型,每种类型对应一个具体的按钮文案:
场景 | ServiceType | 按钮文案 |
|---|---|---|
会议 | Meeting | 加入会议 |
追剧 | Watching | 立即观看 |
还款 | Repayment | 马上还款 |
直播 | Live | 开启直播 |
购物 | Shopping | 开始选购 |
出行 | Trip | 立即查看 |
上课 | Class | 开始上课 |
赛事 | SportsEvents | 立即观看 |
运动 | SportsExercise | 开始运动 |
选择合适的服务类型,按钮文案就会自动匹配,不需要开发者自定义。
在写第一行业务代码之前,有三步准备工作需要完成:
第一步,导入依赖。日历管理相关的能力都在 @kit.CalendarKit 中。
第二步,申请权限。日历是用户的私有数据,读写操作需要在 module.json5 中声明两个权限:ohos.permission.READ_CALENDAR 和 ohos.permission.WRITE_CALENDAR。
第三步,获取日程管理器对象。通过上下文获取 calendarMgr 对象,后续所有日历账户和日程的管理操作都通过它来进行。推荐在 EntryAbility.ets 中完成这一步,确保管理器对象在应用生命周期内可用。
在看具体场景之前,先理解日程涉及的几个关键概念,后面写代码时会更清楚为什么要这样配置。
每个写入系统日历的日程都归属于一个日历账户。你可以理解为日历中的一个"分组"——用户打开日历应用时,能看到来自不同应用的日程被归类在各自的账户下。
账户有三个关键属性:
LOCAL。一条日程的核心字段包括:
[0, 10] 表示日程开始时和开始前 10 分钟各提醒一次。对于全天日程,0 表示当天上午 9 点提醒,1440(即 24 小时)表示前一天上午 9 点提醒。type)和跳转链接(uri,DeepLink 格式)。日历服务提供了完整的 CRUD 操作。创建日历账户后,可以在该账户下添加日程、按条件查询日程、更新日程信息、删除日程。后面的场景示例中会展示这些操作的具体写法。
下面通过两个最常见的场景——出行服务和会议——来完整走一遍开发流程。其他场景(直播、购物、还款、课程等)的开发思路完全一致,区别只在于字段内容和 ServiceType 的选择。
这大概是最容易理解的场景了:用户在购票应用里买了一张高铁票,应用把行程信息写入日历,出发前提醒用户,用户还能一键跳回应用查看电子客票。
字段设计思路:
标题要一目了然,建议包含车次和起终点,比如"行程信息:G107 上海虹桥-北京南"。备注里可以放检票口和座位号这类到了车站才需要的细节。提醒时间设两个——4 小时前提醒用户该出门了,2 小时前再提醒一次。一键服务类型选 TRIP。
创建日程:
typescript 体验AI代码助手 代码解读复制代码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}`);
}
}这段代码做了三件事:创建日历账户、设置账户配色、添加日程。注意一定要确保日历账户创建成功后再进行日程操作,否则后续调用会失败。
日程的后续管理:
出行场景下,行程变更是常有的事——改签了车次、换了出发时间。这时候需要更新已有日程而不是删掉重建:
typescript 体验AI代码助手 代码解读复制代码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}`);
}
}如果用户退票了,则直接删除日程:
typescript 体验AI代码助手 代码解读复制代码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 查询:
typescript 体验AI代码助手 代码解读复制代码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}`);
}
}会议场景和出行的最大区别在于:它有与会人信息。用户在会议应用中创建或被邀请参加一个会议,应用将其写入日历,到时间时用户看到提醒,点击"加入会议"按钮就能直接进入会议。
字段设计思路:
标题就是会议主题。提醒时间设准时和 15 分钟前——太早没意义,太晚来不及。会议场景特有的是 attendee 字段,用来记录与会人信息,每个与会人有姓名、邮箱、角色(组织者还是参与者)和类型(必选还是可选)。
typescript 体验AI代码助手 代码解读复制代码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 等参数,用户点击"加入会议"后直接进入对应的会议房间。 http://nnkssfp.mpmpc.cn/
前面详细讲了出行和会议两个场景的完整实现,其他场景的代码结构完全一样,差异只在字段内容的填写上。这里简要列出几个场景的要点,帮你快速对照:
酒店住宿:适合设置为全天日程(isAllDay: true),标题包含酒店名称和地址,别忘了填 location 字段(包含经纬度),ServiceType 用 TRIP。提醒建议设前一天上午 9 点(reminderTime: [1440])和当天上午 9 点(reminderTime: [0])。
还款提醒:也是全天日程,毕竟还款日是以"天"为单位的。备注里写上待还款金额,ServiceType 用 REPAYMENT,提醒一次就够了——当天上午 9 点(reminderTime: [0])。
直播 / 抢购 / 课程 / 赛事 / 运动:都是精确到具体时刻的非全天日程,提醒时间一般设准时和开始前 10-30 分钟。区别就是选对 ServiceType,按钮文案就会自动匹配。
日历服务的接入逻辑并不复杂——创建账户、配置日程、写入系统。但要把体验做好,有几个细节值得注意:
日历是一个天然的时间管理入口,用户每天都会看。把应用中有价值的时间事件写进去,既帮用户管理好了日程,也为应用争取到了在系统级入口的露出机会。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。