在鸿蒙元服务开发中,预约成功、会议提醒、生日或健身计划等场景,经常需要把日程自动写入系统日历。ASCF 提供了两个日历 API:addPhoneCalendar 用于添加单次日程,addPhoneRepeatCalendar 用于添加重复日程。它们能减少用户手动打开日历应用的操作,但实际接入时,授权、时间戳类型和重复规则很容易踩坑。下面按从权限到场景的顺序梳理。
一、权限要两步:声明 + 运行时授权
日历 API 的权限比较特殊,不是只在 module.json5 声明就能用。第一步是在 module.json5 中声明 ohos.permission.READ_CALENDAR 和 ohos.permission.WRITE_CALENDAR:
- {
- "requestPermissions": [
- {
- "name": "ohos.permission.READ_CALENDAR",
- "reason": "用于读取日历信息",
- "usedScene": {
- "abilities": ["EntryAbility"],
- "when": "inuse"
- }
- },
- {
- "name": "ohos.permission.WRITE_CALENDAR",
- "reason": "用于添加日历日程",
- "usedScene": {
- "abilities": ["EntryAbility"],
- "when": "inuse"
- }
- }
- ]
- }
复制代码
第二步是在调用 API 前申请 scope.addPhoneCalendar 运行时授权。原文作者一开始以为声明权限就够了,结果调用一直报错,后来才发现还需要这一步:
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- console.info('授权成功');
- // 这里才能调用日历 API
- },
- fail: (err) => {
- console.error('授权失败:', err);
- }
- });
复制代码
二、添加单次日程:addPhoneCalendar
授权成功后,可以用 addPhoneCalendar 添加单次日程。下面示例把当前时间往后推 1 小时作为开始时间,2 小时后作为结束时间,并提前 5 分钟提醒:
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- const now = Math.floor(Date.now() / 1000);
- const startTime = now + 3600; // 1小时后
- const endTime = now + 7200; // 2小时后
- has.addPhoneCalendar({
- title: '项目评审会议',
- startTime: startTime,
- endTime: endTime,
- description: '讨论 Q4 项目进展',
- location: '会议室 A301',
- alarm: true,
- alarmOffset: 300, // 提前 5 分钟提醒
- success: () => {
- console.info('日程添加成功');
- has.showToast({ title: '已添加到日历' });
- },
- fail: (err) => {
- console.error('添加失败:', err);
- }
- });
- }
- });
复制代码
这里最关键的是 startTime 和 endTime 都是 unix 时间戳,单位是秒,不是毫秒。JavaScript 的 Date.now() 返回毫秒,因此要除以 1000 并取整。
三、添加重复日程:addPhoneRepeatCalendar
重复日程使用 addPhoneRepeatCalendar。示例创建一个每周重复的周会,提前 10 分钟提醒,并设置 3 个月后结束重复:
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- const now = Math.floor(Date.now() / 1000);
- const startTime = now + 3600;
- const endTime = now + 7200;
- // 重复结束时间:3个月后
- const repeatEndTime = now + 90 * 24 * 3600;
- has.addPhoneRepeatCalendar({
- title: '周会',
- startTime: startTime,
- endTime: endTime,
- description: '每周例会',
- location: '线上会议',
- alarm: true,
- alarmOffset: 600, // 提前 10 分钟
- repeatInterval: 'week',
- repeatEndTime: repeatEndTime,
- success: () => {
- console.info('重复日程添加成功');
- has.showToast({ title: '已添加到日历' });
- },
- fail: (err) => {
- console.error('添加失败:', err);
- }
- });
- }
- });
复制代码
repeatInterval 默认是 month。如果不传 repeatEndTime,重复日程会一直重复下去。
四、常见业务场景
预约成功后添加日程:预约系统可以在用户预约成功后,把服务名、预约时间、地址、订单号写进日程,并提前 30 分钟提醒。
- Page({
- handleAppointmentSuccess(appointment) {
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- has.addPhoneCalendar({
- title: '预约: ' + appointment.serviceName,
- startTime: appointment.timestamp,
- endTime: appointment.timestamp + 3600,
- location: appointment.address,
- description: '预约编号: ' + appointment.orderNo,
- alarm: true,
- alarmOffset: 1800, // 提前 30 分钟
- success: () => {
- console.info('预约日程已添加');
- }
- });
- }
- });
- }
- });
复制代码
生日提醒(每年重复):根据生日字符串构造今年的生日时间戳;如果今年生日已过,就用明年的。然后以 year 为重复周期添加,allDay 设为 true,提前 1 天提醒。
- Page({
- addBirthdayReminder(name, birthday) {
- // birthday 格式: '01-15' 表示 1月15日
- const parts = birthday.split('-');
- const now = new Date();
- const year = now.getFullYear();
- // 构造今年的生日时间戳
- const birthdayDate = new Date(year, parseInt(parts[0]) - 1, parseInt(parts[1]), 9, 0, 0);
- let startTime = Math.floor(birthdayDate.getTime() / 1000);
- // 如果今年的生日已过,用明年的
- if (startTime < Math.floor(Date.now() / 1000)) {
- birthdayDate.setFullYear(year + 1);
- startTime = Math.floor(birthdayDate.getTime() / 1000);
- }
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- has.addPhoneRepeatCalendar({
- title: name + ' 的生日',
- startTime: startTime,
- allDay: true,
- alarm: true,
- alarmOffset: 86400, // 提前1天
- repeatInterval: 'year',
- success: () => {
- has.showToast({ title: '生日提醒已添加' });
- }
- });
- }
- });
- }
- });
复制代码
健身计划(每周重复):从下周周一早上 8 点开始,持续 1 小时,重复 3 个月。
- Page({
- addFitnessPlan() {
- // 下周一早上 8 点
- const now = new Date();
- const dayOfWeek = now.getDay();
- const daysUntilMonday = dayOfWeek === 0 ? 1 : 8 - dayOfWeek;
- const nextMonday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysUntilMonday, 8, 0, 0);
- const startTime = Math.floor(nextMonday.getTime() / 1000);
- const endTime = startTime + 3600; // 1小时
- const repeatEndTime = startTime + 90 * 24 * 3600; // 3个月后
- has.authorize({
- scope: 'scope.addPhoneCalendar',
- success: () => {
- has.addPhoneRepeatCalendar({
- title: '健身时间',
- startTime: startTime,
- endTime: endTime,
- location: '健身房',
- description: '坚持就是胜利',
- alarm: true,
- alarmOffset: 1800,
- repeatInterval: 'week',
- repeatEndTime: repeatEndTime,
- success: () => {
- has.showToast({ title: '健身计划已添加' });
- }
- });
- }
- });
- }
- });
复制代码
五、踩坑与排障
1. 时间戳单位是秒,不是毫秒。这是最常见的坑。Date.now() 和 new Date().getTime() 返回毫秒,但 API 要秒。需要:
- const startTime = Math.floor(Date.now() / 1000);
复制代码
如果直接用毫秒值,日程会被加到几十年后。
2. 时间参数类型不一致。原文作者前前后后踩了三次:第一次全用 number,报错 endTime is not type string;第二次全改成 String,又报错 startTime is not type number。后来才发现 endTime 要 string,startTime 和 repeatEndTime 要 number。
- const now = Math.floor(Date.now() / 1000);
- const startTime = now + 3600; // number
- const endTime = String(now + 7200); // string,注意要转换
- const repeatEndTime = now + 180 * 24 * 3600; // number
复制代码
3. 每月重复不能超过 28 号。repeatInterval 为 month 时,日程日期不能大于 28 号。因为有些月份只有 28 天,系统无法确定“31 号”在这些月份该在哪天触发。如果要在月底重复,用 28 号更安全。
4. 授权必须在运行时完成。日历 API 不像有些 API 声明权限就行,必须通过 has.authorize 申请 scope.addPhoneCalendar。不授权直接调用会失败。
5. 不支持修改和删除。ASCF 的日历 API 只能添加,不能修改和删除已有日程。如果需要修改,得引导用户去系统日历应用手动操作。
6. allDay 事件仍要传 startTime。即使 allDay 为 true,startTime 还是需要 unix 时间戳;系统会根据这个时间戳的日期来确定是哪一天,时间部分会被忽略。
六、完整示例与 API 速查
原文还给出一个较完整的 Page 示例:data 中包含 title、description、location、repeatMode、repeatOptions、alarmOffset、lastResult;通过输入事件更新表单;addCalendar 先授权,再根据 repeatMode 是否为 none,决定调用 addPhoneCalendar 还是 addPhoneRepeatCalendar,重复结束时间设为半年后;addQuickMeeting 则快速添加一个 5 分钟后开始、1 小时 5 分钟后结束、提前 60 秒提醒的会议。核心逻辑与上文单次/重复示例一致。
对于大多数元服务场景,添加日程已经够用。如果确实需要查询或修改已有日程,ASCF 目前不支持,得使用系统的 @ohos.calendarManager 模块。总体来看,日历 API 适合预约、提醒、计划类场景,落地时重点处理授权和时间戳这两个容易出错的点。 |