HarmonyOS 6(API 21) 精准日程管理完整开发教程
本文介绍了基于鸿蒙6系统的精准日程管理应用开发案例。项目采用分层架构设计,包含UI层(ArkUI)、数据层(关系型数据库)、提醒层(代理提醒)和通知层。核心功能包括日程增删改查、后台精准提醒、智能提前提醒、重复提醒等。文章详细展示了项目创建步骤、配置文件设置以及核心代码实现,包括日程数据模型定义和数据库操作类。该项目充分利用鸿蒙系统特性,实现稳定可靠的日程管理功能,适合开发者学习鸿蒙应用开发的最佳
·
Hello,大家好,我是 V 哥。
AI 智能体在2026年V 哥相信一定翻天覆地的变化,一大波企业和开发者纷纷涌入这个赛道,什么超级个体、一人公司、为企业节省几百万人力成本等等话题在网络上持续发酵,作为程序员的我们,如果还在观望,那等来就一定是被市场淘汰。我经常跟同学们说,程序员最大的优势是啥?就是不断持续学习的超强能力!干掉程序员的只会是程序员自己,未来的程序员不只是程序员,而是主导技术变现的超级魔术师。
今天的内容,V 哥带大家一起来玩一玩,在鸿蒙6系统中,如何完成精准日程管理的完整案例开发。
一、项目概述
功能特性
- ✅ 日程增删改查(支持标题、备注、时间、重复)
- ✅ 后台精准提醒(应用关闭/重启后依然准时)
- ✅ 智能提前提醒(5/10/30/60分钟)
- ✅ 重复提醒(每天/每周/每月)
- ✅ 自定义铃声+震动
- ✅ 点击通知跳转详情
技术方案
┌─────────────────────────────────────────────────────────┐
│ 精准日程提醒架构 │
├─────────────────────────────────────────────────────────┤
│ UI层 │ ArkUI 声明式UI │
├─────────────────────────────────────────────────────────┤
│ 数据层 │ @ohos.data.relationalStore (关系型DB) │
├─────────────────────────────────────────────────────────┤
│ 提醒层 │ @ohos.reminderAgentManager (代理提醒) │
├─────────────────────────────────────────────────────────┤
│ 通知层 │ @ohos.notificationManager │
└─────────────────────────────────────────────────────────┘
二、项目创建与配置
步骤1:创建项目
DevEco Studio → File → New → Create Project
→ 选择 "Empty Ability"
→ Project name: ScheduleManager
→ Bundle name: com.example.schedulemanager
→ Compile SDK: 5.0.0(API 12) 或更高
→ Model: Stage
步骤2:配置 module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.PUBLISH_AGENT_REMINDER",
"reason": "$string:reminder_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.NOTIFICATION_CONTROLLER",
"reason": "$string:notification_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
步骤3:配置 main_pages.json
{
"src": [
"pages/Index",
"pages/AddSchedulePage",
"pages/ScheduleDetailPage"
]
}
步骤4:配置字符串资源 (string.json)
{
"string": [
{ "name": "module_desc", "value": "日程管理模块" },
{ "name": "EntryAbility_desc", "value": "日程管理应用" },
{ "name": "EntryAbility_label", "value": "精准日程" },
{ "name": "reminder_reason", "value": "用于设置日程提醒" },
{ "name": "notification_reason", "value": "用于发送日程通知" }
]
}
三、核心代码实现
1. 日程数据模型 (model/ScheduleModel.ets)
// entry/src/main/ets/model/ScheduleModel.ets
/**
* 重复类型枚举
*/
export enum RepeatType {
NONE = 0, // 不重复
DAILY = 1, // 每天
WEEKLY = 2, // 每周
MONTHLY = 3 // 每月
}
/**
* 提前提醒时间枚举(分钟)
*/
export enum AdvanceRemind {
NONE = 0,
FIVE_MIN = 5,
TEN_MIN = 10,
THIRTY_MIN = 30,
ONE_HOUR = 60
}
/**
* 日程实体类
*/
export class Schedule {
id: number = 0; // 主键ID
title: string = ''; // 标题
note: string = ''; // 备注
remindTime: number = 0; // 提醒时间戳(毫秒)
advanceMinutes: number = 0; // 提前提醒分钟数
repeatType: RepeatType = RepeatType.NONE; // 重复类型
reminderId: number = -1; // 系统提醒ID
isEnabled: boolean = true; // 是否启用
createTime: number = 0; // 创建时间
updateTime: number = 0; // 更新时间
constructor(init?: Partial<Schedule>) {
if (init) {
Object.assign(this, init);
}
}
}
/**
* 重复类型显示文本
*/
export function getRepeatTypeText(type: RepeatType): string {
const texts: Record<RepeatType, string> = {
[RepeatType.NONE]: '不重复',
[RepeatType.DAILY]: '每天',
[RepeatType.WEEKLY]: '每周',
[RepeatType.MONTHLY]: '每月'
};
return texts[type] || '不重复';
}
/**
* 提前提醒显示文本
*/
export function getAdvanceText(minutes: number): string {
if (minutes === 0) return '准时提醒';
if (minutes < 60) return `提前${minutes}分钟`;
return `提前${minutes / 60}小时`;
}
2. 数据库操作类 (utils/ScheduleDB.ets)
// entry/src/main/ets/utils/ScheduleDB.ets
import { relationalStore, ValuesBucket } from '@kit.ArkData';
import { Schedule, RepeatType } from '../model/ScheduleModel';
import { common } from '@kit.AbilityKit';
const DB_NAME = 'ScheduleManager.db';
const TABLE_NAME = 'schedules';
const DB_VERSION = 1;
// 建表SQL
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
note TEXT,
remind_time INTEGER NOT NULL,
advance_minutes INTEGER DEFAULT 0,
repeat_type INTEGER DEFAULT 0,
reminder_id INTEGER DEFAULT -1,
is_enabled INTEGER DEFAULT 1,
create_time INTEGER,
update_time INTEGER
)
`;
export class ScheduleDB {
private static instance: ScheduleDB;
private rdbStore: relationalStore.RdbStore | null = null;
private context: common.UIAbilityContext | null = null;
private constructor() {}
/**
* 获取单例实例
*/
static getInstance(): ScheduleDB {
if (!ScheduleDB.instance) {
ScheduleDB.instance = new ScheduleDB();
}
return ScheduleDB.instance;
}
/**
* 初始化数据库
*/
async init(context: common.UIAbilityContext): Promise<void> {
this.context = context;
const storeConfig: relationalStore.StoreConfig = {
name: DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1
};
try {
this.rdbStore = await relationalStore.getRdbStore(context, storeConfig);
await this.rdbStore.executeSql(CREATE_TABLE_SQL);
console.info('[ScheduleDB] 数据库初始化成功');
} catch (err) {
console.error('[ScheduleDB] 数据库初始化失败:', JSON.stringify(err));
}
}
/**
* 插入日程
*/
async insert(schedule: Schedule): Promise<number> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const now = Date.now();
const values: ValuesBucket = {
'title': schedule.title,
'note': schedule.note,
'remind_time': schedule.remindTime,
'advance_minutes': schedule.advanceMinutes,
'repeat_type': schedule.repeatType,
'reminder_id': schedule.reminderId,
'is_enabled': schedule.isEnabled ? 1 : 0,
'create_time': now,
'update_time': now
};
try {
const rowId = await this.rdbStore.insert(TABLE_NAME, values);
console.info('[ScheduleDB] 插入成功, rowId:', rowId);
return rowId;
} catch (err) {
console.error('[ScheduleDB] 插入失败:', JSON.stringify(err));
throw err;
}
}
/**
* 更新日程
*/
async update(schedule: Schedule): Promise<number> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const values: ValuesBucket = {
'title': schedule.title,
'note': schedule.note,
'remind_time': schedule.remindTime,
'advance_minutes': schedule.advanceMinutes,
'repeat_type': schedule.repeatType,
'reminder_id': schedule.reminderId,
'is_enabled': schedule.isEnabled ? 1 : 0,
'update_time': Date.now()
};
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', schedule.id);
try {
const rows = await this.rdbStore.update(values, predicates);
console.info('[ScheduleDB] 更新成功, 影响行数:', rows);
return rows;
} catch (err) {
console.error('[ScheduleDB] 更新失败:', JSON.stringify(err));
throw err;
}
}
/**
* 删除日程
*/
async delete(id: number): Promise<number> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
try {
const rows = await this.rdbStore.delete(predicates);
console.info('[ScheduleDB] 删除成功, 影响行数:', rows);
return rows;
} catch (err) {
console.error('[ScheduleDB] 删除失败:', JSON.stringify(err));
throw err;
}
}
/**
* 根据ID查询
*/
async getById(id: number): Promise<Schedule | null> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.equalTo('id', id);
try {
const resultSet = await this.rdbStore.query(predicates);
if (resultSet.goToFirstRow()) {
const schedule = this.parseResultSet(resultSet);
resultSet.close();
return schedule;
}
resultSet.close();
return null;
} catch (err) {
console.error('[ScheduleDB] 查询失败:', JSON.stringify(err));
throw err;
}
}
/**
* 查询所有日程(按时间排序)
*/
async getAll(): Promise<Schedule[]> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.orderByAsc('remind_time');
try {
const resultSet = await this.rdbStore.query(predicates);
const schedules: Schedule[] = [];
while (resultSet.goToNextRow()) {
schedules.push(this.parseResultSet(resultSet));
}
resultSet.close();
console.info('[ScheduleDB] 查询全部, 数量:', schedules.length);
return schedules;
} catch (err) {
console.error('[ScheduleDB] 查询全部失败:', JSON.stringify(err));
throw err;
}
}
/**
* 查询未来的日程
*/
async getFutureSchedules(): Promise<Schedule[]> {
if (!this.rdbStore) {
throw new Error('数据库未初始化');
}
const predicates = new relationalStore.RdbPredicates(TABLE_NAME);
predicates.greaterThan('remind_time', Date.now());
predicates.equalTo('is_enabled', 1);
predicates.orderByAsc('remind_time');
try {
const resultSet = await this.rdbStore.query(predicates);
const schedules: Schedule[] = [];
while (resultSet.goToNextRow()) {
schedules.push(this.parseResultSet(resultSet));
}
resultSet.close();
return schedules;
} catch (err) {
console.error('[ScheduleDB] 查询未来日程失败:', JSON.stringify(err));
throw err;
}
}
/**
* 解析结果集为Schedule对象
*/
private parseResultSet(resultSet: relationalStore.ResultSet): Schedule {
return new Schedule({
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
note: resultSet.getString(resultSet.getColumnIndex('note')),
remindTime: resultSet.getLong(resultSet.getColumnIndex('remind_time')),
advanceMinutes: resultSet.getLong(resultSet.getColumnIndex('advance_minutes')),
repeatType: resultSet.getLong(resultSet.getColumnIndex('repeat_type')) as RepeatType,
reminderId: resultSet.getLong(resultSet.getColumnIndex('reminder_id')),
isEnabled: resultSet.getLong(resultSet.getColumnIndex('is_enabled')) === 1,
createTime: resultSet.getLong(resultSet.getColumnIndex('create_time')),
updateTime: resultSet.getLong(resultSet.getColumnIndex('update_time'))
});
}
}
3. 提醒管理器 (utils/ReminderHelper.ets)
// entry/src/main/ets/utils/ReminderHelper.ets
import { reminderAgentManager } from '@kit.BackgroundTasksKit';
import { notificationManager } from '@kit.NotificationKit';
import { Schedule, RepeatType } from '../model/ScheduleModel';
import { BusinessError } from '@kit.BasicServicesKit';
export class ReminderHelper {
private static instance: ReminderHelper;
private constructor() {}
static getInstance(): ReminderHelper {
if (!ReminderHelper.instance) {
ReminderHelper.instance = new ReminderHelper();
}
return ReminderHelper.instance;
}
/**
* 请求通知权限
*/
async requestNotificationPermission(): Promise<boolean> {
try {
const isEnabled = await notificationManager.isNotificationEnabled();
if (!isEnabled) {
await notificationManager.requestEnableNotification();
}
return true;
} catch (err) {
const error = err as BusinessError;
console.error('[ReminderHelper] 请求通知权限失败:', error.code, error.message);
return false;
}
}
/**
* 设置日程提醒
*/
async setReminder(schedule: Schedule): Promise<number> {
// 计算实际提醒时间(考虑提前量)
const actualRemindTime = schedule.remindTime - schedule.advanceMinutes * 60 * 1000;
if (actualRemindTime <= Date.now()) {
console.warn('[ReminderHelper] 提醒时间已过');
return -1;
}
// 将时间戳转换为日期对象
const remindDate = new Date(actualRemindTime);
// 构建提醒请求
const reminderRequest: reminderAgentManager.ReminderRequestCalendar = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_CALENDAR,
dateTime: {
year: remindDate.getFullYear(),
month: remindDate.getMonth() + 1, // 月份从1开始
day: remindDate.getDate(),
hour: remindDate.getHours(),
minute: remindDate.getMinutes(),
second: remindDate.getSeconds()
},
repeatMonths: this.getRepeatMonths(schedule.repeatType),
repeatDays: this.getRepeatDays(schedule.repeatType, remindDate),
title: '日程提醒',
content: schedule.title,
expiredContent: `日程已过期: ${schedule.title}`,
snoozeContent: `稍后提醒: ${schedule.title}`,
notificationId: schedule.id,
slotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
tapDismissed: true,
autoDeletedTime: 300000, // 5分钟后自动删除
snoozeTimes: 3, // 允许延后3次
timeInterval: 5 * 60, // 延后间隔5分钟
actionButton: [
{
title: '查看详情',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CUSTOM
},
{
title: '稍后提醒',
type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_SNOOZE
}
],
wantAgent: {
pkgName: 'com.example.schedulemanager',
abilityName: 'EntryAbility'
},
maxScreenWantAgent: {
pkgName: 'com.example.schedulemanager',
abilityName: 'EntryAbility'
},
ringDuration: 30 // 铃声持续30秒
};
try {
const reminderId = await reminderAgentManager.publishReminder(reminderRequest);
console.info('[ReminderHelper] 提醒设置成功, reminderId:', reminderId);
return reminderId;
} catch (err) {
const error = err as BusinessError;
console.error('[ReminderHelper] 设置提醒失败:', error.code, error.message);
throw err;
}
}
/**
* 取消提醒
*/
async cancelReminder(reminderId: number): Promise<void> {
if (reminderId < 0) {
return;
}
try {
await reminderAgentManager.cancelReminder(reminderId);
console.info('[ReminderHelper] 取消提醒成功, reminderId:', reminderId);
} catch (err) {
const error = err as BusinessError;
console.error('[ReminderHelper] 取消提醒失败:', error.code, error.message);
}
}
/**
* 取消所有提醒
*/
async cancelAllReminders(): Promise<void> {
try {
await reminderAgentManager.cancelAllReminders();
console.info('[ReminderHelper] 取消所有提醒成功');
} catch (err) {
const error = err as BusinessError;
console.error('[ReminderHelper] 取消所有提醒失败:', error.code, error.message);
}
}
/**
* 获取所有有效提醒
*/
async getAllValidReminders(): Promise<reminderAgentManager.ReminderRequest[]> {
try {
const reminders = await reminderAgentManager.getValidReminders();
console.info('[ReminderHelper] 有效提醒数量:', reminders.length);
return reminders;
} catch (err) {
const error = err as BusinessError;
console.error('[ReminderHelper] 获取有效提醒失败:', error.code, error.message);
return [];
}
}
/**
* 根据重复类型获取重复月份
*/
private getRepeatMonths(repeatType: RepeatType): number[] {
if (repeatType === RepeatType.MONTHLY || repeatType === RepeatType.DAILY) {
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
}
return [];
}
/**
* 根据重复类型获取重复日期
*/
private getRepeatDays(repeatType: RepeatType, date: Date): number[] {
switch (repeatType) {
case RepeatType.DAILY:
// 每天重复:返回1-31所有日期
return Array.from({ length: 31 }, (_, i) => i + 1);
case RepeatType.WEEKLY:
// 每周重复:返回同一星期几对应的所有日期(简化处理)
return this.getWeeklyDays(date);
case RepeatType.MONTHLY:
// 每月重复:返回当前日期
return [date.getDate()];
default:
return [];
}
}
/**
* 获取每周重复的日期(计算每月中相同星期几的日期)
*/
private getWeeklyDays(date: Date): number[] {
const dayOfWeek = date.getDay();
const days: number[] = [];
// 计算当月中所有相同星期几的日期
const year = date.getFullYear();
const month = date.getMonth();
const lastDay = new Date(year, month + 1, 0).getDate();
for (let d = 1; d <= lastDay; d++) {
const tempDate = new Date(year, month, d);
if (tempDate.getDay() === dayOfWeek) {
days.push(d);
}
}
return days;
}
}
4. 主页面 - 日程列表 (pages/Index.ets)
// entry/src/main/ets/pages/Index.ets
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { Schedule, RepeatType, getRepeatTypeText } from '../model/ScheduleModel';
import { ScheduleDB } from '../utils/ScheduleDB';
import { ReminderHelper } from '../utils/ReminderHelper';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
@State scheduleList: Schedule[] = [];
@State isLoading: boolean = true;
@State isEmpty: boolean = false;
private db = ScheduleDB.getInstance();
private reminderHelper = ReminderHelper.getInstance();
async aboutToAppear(): Promise<void> {
// 请求通知权限
await this.reminderHelper.requestNotificationPermission();
// 加载日程列表
await this.loadSchedules();
}
async onPageShow(): Promise<void> {
// 每次页面显示时刷新列表
await this.loadSchedules();
}
/**
* 加载日程列表
*/
async loadSchedules(): Promise<void> {
this.isLoading = true;
try {
this.scheduleList = await this.db.getAll();
this.isEmpty = this.scheduleList.length === 0;
} catch (err) {
console.error('加载日程失败:', JSON.stringify(err));
promptAction.showToast({ message: '加载失败' });
} finally {
this.isLoading = false;
}
}
/**
* 删除日程
*/
async deleteSchedule(schedule: Schedule): Promise<void> {
try {
// 取消提醒
await this.reminderHelper.cancelReminder(schedule.reminderId);
// 删除数据库记录
await this.db.delete(schedule.id);
// 刷新列表
await this.loadSchedules();
promptAction.showToast({ message: '删除成功' });
} catch (err) {
console.error('删除日程失败:', JSON.stringify(err));
promptAction.showToast({ message: '删除失败' });
}
}
/**
* 切换日程启用状态
*/
async toggleSchedule(schedule: Schedule): Promise<void> {
try {
schedule.isEnabled = !schedule.isEnabled;
if (schedule.isEnabled) {
// 重新设置提醒
const reminderId = await this.reminderHelper.setReminder(schedule);
schedule.reminderId = reminderId;
} else {
// 取消提醒
await this.reminderHelper.cancelReminder(schedule.reminderId);
schedule.reminderId = -1;
}
await this.db.update(schedule);
await this.loadSchedules();
} catch (err) {
console.error('切换状态失败:', JSON.stringify(err));
}
}
/**
* 格式化时间显示
*/
formatTime(timestamp: number): string {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
}
/**
* 判断是否已过期
*/
isExpired(schedule: Schedule): boolean {
return schedule.remindTime < Date.now() && schedule.repeatType === RepeatType.NONE;
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('精准日程管理')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
Blank()
Button() {
Image($r('app.media.ic_add'))
.width(24)
.height(24)
.fillColor(Color.White)
}
.width(44)
.height(44)
.backgroundColor('#007DFF')
.borderRadius(22)
.onClick(() => {
router.pushUrl({ url: 'pages/AddSchedulePage' });
})
}
.width('100%')
.height(60)
.padding({ left: 16, right: 16 })
// 日程列表
if (this.isLoading) {
// 加载中
Column() {
LoadingProgress()
.width(50)
.height(50)
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ top: 10 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else if (this.isEmpty) {
// 空状态
Column() {
Image($r('app.media.ic_empty'))
.width(120)
.height(120)
.opacity(0.5)
Text('暂无日程')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
Text('点击右上角 + 添加日程')
.fontSize(14)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
// 日程列表
List({ space: 12 }) {
ForEach(this.scheduleList, (schedule: Schedule) => {
ListItem() {
this.ScheduleCard(schedule)
}
.swipeAction({
end: this.DeleteButton(schedule)
})
}, (schedule: Schedule) => schedule.id.toString())
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.divider({ strokeWidth: 0 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/**
* 日程卡片组件
*/
@Builder
ScheduleCard(schedule: Schedule) {
Row() {
// 左侧状态指示条
Column()
.width(4)
.height('100%')
.backgroundColor(this.isExpired(schedule) ? '#CCCCCC' :
(schedule.isEnabled ? '#007DFF' : '#999999'))
.borderRadius(2)
// 中间内容
Column() {
// 标题
Text(schedule.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(this.isExpired(schedule) ? '#999999' : '#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 时间
Row() {
Image($r('app.media.ic_time'))
.width(14)
.height(14)
.fillColor('#666666')
Text(this.formatTime(schedule.remindTime))
.fontSize(13)
.fontColor('#666666')
.margin({ left: 4 })
}
.margin({ top: 8 })
// 标签行
Row() {
// 重复类型标签
if (schedule.repeatType !== RepeatType.NONE) {
Text(getRepeatTypeText(schedule.repeatType))
.fontSize(11)
.fontColor('#007DFF')
.backgroundColor('#E6F2FF')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
// 提前提醒标签
if (schedule.advanceMinutes > 0) {
Text(`提前${schedule.advanceMinutes}分钟`)
.fontSize(11)
.fontColor('#FF9500')
.backgroundColor('#FFF3E0')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ left: 6 })
}
// 过期标签
if (this.isExpired(schedule)) {
Text('已过期')
.fontSize(11)
.fontColor('#FF3B30')
.backgroundColor('#FFE5E5')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ left: 6 })
}
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
.margin({ left: 12 })
// 右侧开关
Toggle({ type: ToggleType.Switch, isOn: schedule.isEnabled })
.selectedColor('#007DFF')
.switchPointColor(Color.White)
.onChange(() => {
this.toggleSchedule(schedule);
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.08)',
offsetX: 0,
offsetY: 2
})
.onClick(() => {
router.pushUrl({
url: 'pages/ScheduleDetailPage',
params: { scheduleId: schedule.id }
});
})
}
/**
* 删除按钮(滑动操作)
*/
@Builder
DeleteButton(schedule: Schedule) {
Button() {
Image($r('app.media.ic_delete'))
.width(24)
.height(24)
.fillColor(Color.White)
}
.width(60)
.height('100%')
.backgroundColor('#FF3B30')
.onClick(() => {
promptAction.showDialog({
title: '确认删除',
message: `确定要删除日程"${schedule.title}"吗?`,
buttons: [
{ text: '取消', color: '#666666' },
{ text: '删除', color: '#FF3B30' }
]
}).then((result) => {
if (result.index === 1) {
this.deleteSchedule(schedule);
}
});
})
}
}
5. 添加日程页面 (pages/AddSchedulePage.ets)
// entry/src/main/ets/pages/AddSchedulePage.ets
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { Schedule, RepeatType, AdvanceRemind } from '../model/ScheduleModel';
import { ScheduleDB } from '../utils/ScheduleDB';
import { ReminderHelper } from '../utils/ReminderHelper';
interface RepeatOption {
value: RepeatType;
label: string;
}
interface AdvanceOption {
value: number;
label: string;
}
@Entry
@Component
struct AddSchedulePage {
@State title: string = '';
@State note: string = '';
@State selectedDate: Date = new Date();
@State selectedTime: Date = new Date();
@State repeatType: RepeatType = RepeatType.NONE;
@State advanceMinutes: number = 0;
@State isSaving: boolean = false;
// 日期选择弹窗状态
@State showDatePicker: boolean = false;
@State showTimePicker: boolean = false;
private db = ScheduleDB.getInstance();
private reminderHelper = ReminderHelper.getInstance();
// 重复选项
private repeatOptions: RepeatOption[] = [
{ value: RepeatType.NONE, label: '不重复' },
{ value: RepeatType.DAILY, label: '每天' },
{ value: RepeatType.WEEKLY, label: '每周' },
{ value: RepeatType.MONTHLY, label: '每月' }
];
// 提前提醒选项
private advanceOptions: AdvanceOption[] = [
{ value: 0, label: '准时提醒' },
{ value: 5, label: '提前5分钟' },
{ value: 10, label: '提前10分钟' },
{ value: 30, label: '提前30分钟' },
{ value: 60, label: '提前1小时' }
];
aboutToAppear(): void {
// 默认时间设为下一个整点
const now = new Date();
now.setHours(now.getHours() + 1, 0, 0, 0);
this.selectedDate = now;
this.selectedTime = now;
}
/**
* 保存日程
*/
async saveSchedule(): Promise<void> {
// 表单验证
if (!this.title.trim()) {
promptAction.showToast({ message: '请输入日程标题' });
return;
}
// 合并日期和时间
const remindTime = new Date(
this.selectedDate.getFullYear(),
this.selectedDate.getMonth(),
this.selectedDate.getDate(),
this.selectedTime.getHours(),
this.selectedTime.getMinutes(),
0
).getTime();
// 验证时间
if (remindTime <= Date.now()) {
promptAction.showToast({ message: '提醒时间必须晚于当前时间' });
return;
}
this.isSaving = true;
try {
// 创建日程对象
const schedule = new Schedule({
title: this.title.trim(),
note: this.note.trim(),
remindTime: remindTime,
advanceMinutes: this.advanceMinutes,
repeatType: this.repeatType,
isEnabled: true
});
// 设置系统提醒
const reminderId = await this.reminderHelper.setReminder(schedule);
schedule.reminderId = reminderId;
// 保存到数据库
const id = await this.db.insert(schedule);
schedule.id = id;
promptAction.showToast({ message: '日程添加成功' });
router.back();
} catch (err) {
console.error('保存日程失败:', JSON.stringify(err));
promptAction.showToast({ message: '保存失败,请重试' });
} finally {
this.isSaving = false;
}
}
/**
* 格式化日期显示
*/
formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekDay = weekDays[date.getDay()];
return `${year}年${month}月${day}日 ${weekDay}`;
}
/**
* 格式化时间显示
*/
formatTimeDisplay(date: Date): string {
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
}
build() {
Column() {
// 顶部导航栏
Row() {
Button() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor('#333333')
}
.backgroundColor(Color.Transparent)
.onClick(() => router.back())
Text('添加日程')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Button('保存')
.fontSize(16)
.fontColor('#007DFF')
.backgroundColor(Color.Transparent)
.enabled(!this.isSaving)
.onClick(() => this.saveSchedule())
}
.width('100%')
.height(56)
.padding({ left: 8, right: 16 })
// 表单内容
Scroll() {
Column() {
// 标题输入
Column() {
Text('日程标题')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 8 })
TextInput({ placeholder: '请输入日程标题', text: this.title })
.fontSize(16)
.placeholderColor('#CCCCCC')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding(12)
.height(48)
.onChange((value) => {
this.title = value;
})
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
Divider().color('#EEEEEE')
// 备注输入
Column() {
Text('备注')
.fontSize(14)
.fontColor('#999999')
.margin({ bottom: 8 })
TextArea({ placeholder: '添加备注(可选)', text: this.note })
.fontSize(16)
.placeholderColor('#CCCCCC')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding(12)
.height(100)
.onChange((value) => {
this.note = value;
})
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
Divider().color('#EEEEEE')
// 日期选择
Row() {
Column() {
Text('提醒日期')
.fontSize(14)
.fontColor('#999999')
Text(this.formatDate(this.selectedDate))
.fontSize(16)
.fontColor('#333333')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_arrow_right'))
.width(20)
.height(20)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(16)
.onClick(() => {
DatePickerDialog.show({
start: new Date(),
end: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000 * 2), // 2年后
selected: this.selectedDate,
onDateAccept: (value: Date) => {
this.selectedDate = value;
}
});
})
Divider().color('#EEEEEE')
// 时间选择
Row() {
Column() {
Text('提醒时间')
.fontSize(14)
.fontColor('#999999')
Text(this.formatTimeDisplay(this.selectedTime))
.fontSize(16)
.fontColor('#333333')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_arrow_right'))
.width(20)
.height(20)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(16)
.onClick(() => {
TimePickerDialog.show({
selected: this.selectedTime,
useMilitaryTime: true,
onAccept: (value: TimePickerResult) => {
const newTime = new Date();
newTime.setHours(value.hour || 0, value.minute || 0, 0, 0);
this.selectedTime = newTime;
}
});
})
Divider().color('#EEEEEE')
// 提前提醒选择
Row() {
Column() {
Text('提前提醒')
.fontSize(14)
.fontColor('#999999')
Text(this.advanceOptions.find(o => o.value === this.advanceMinutes)?.label || '准时提醒')
.fontSize(16)
.fontColor('#333333')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_arrow_right'))
.width(20)
.height(20)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(16)
.onClick(() => {
TextPickerDialog.show({
range: this.advanceOptions.map(o => o.label),
selected: this.advanceOptions.findIndex(o => o.value === this.advanceMinutes),
onAccept: (value: TextPickerResult) => {
const index = typeof value.index === 'number' ? value.index : 0;
this.advanceMinutes = this.advanceOptions[index].value;
}
});
})
Divider().color('#EEEEEE')
// 重复选择
Row() {
Column() {
Text('重复')
.fontSize(14)
.fontColor('#999999')
Text(this.repeatOptions.find(o => o.value === this.repeatType)?.label || '不重复')
.fontSize(16)
.fontColor('#333333')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Image($r('app.media.ic_arrow_right'))
.width(20)
.height(20)
.fillColor('#CCCCCC')
}
.width('100%')
.padding(16)
.onClick(() => {
TextPickerDialog.show({
range: this.repeatOptions.map(o => o.label),
selected: this.repeatOptions.findIndex(o => o.value === this.repeatType),
onAccept: (value: TextPickerResult) => {
const index = typeof value.index === 'number' ? value.index : 0;
this.repeatType = this.repeatOptions[index].value;
}
});
})
// 底部间距
Column().height(100)
}
}
.layoutWeight(1)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
6. 日程详情页面 (pages/ScheduleDetailPage.ets)
// entry/src/main/ets/pages/ScheduleDetailPage.ets
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { Schedule, RepeatType, getRepeatTypeText, getAdvanceText } from '../model/ScheduleModel';
import { ScheduleDB } from '../utils/ScheduleDB';
import { ReminderHelper } from '../utils/ReminderHelper';
interface RouterParams {
scheduleId?: number;
}
@Entry
@Component
struct ScheduleDetailPage {
@State schedule: Schedule | null = null;
@State isLoading: boolean = true;
private db = ScheduleDB.getInstance();
private reminderHelper = ReminderHelper.getInstance();
private scheduleId: number = 0;
async aboutToAppear(): Promise<void> {
const params = router.getParams() as RouterParams;
if (params?.scheduleId) {
this.scheduleId = params.scheduleId;
await this.loadSchedule();
}
}
async loadSchedule(): Promise<void> {
this.isLoading = true;
try {
this.schedule = await this.db.getById(this.scheduleId);
} catch (err) {
console.error('加载日程详情失败:', JSON.stringify(err));
} finally {
this.isLoading = false;
}
}
async deleteSchedule(): Promise<void> {
if (!this.schedule) return;
promptAction.showDialog({
title: '确认删除',
message: '删除后无法恢复,确定要删除吗?',
buttons: [
{ text: '取消', color: '#666666' },
{ text: '删除', color: '#FF3B30' }
]
}).then(async (result) => {
if (result.index === 1 && this.schedule) {
try {
await this.reminderHelper.cancelReminder(this.schedule.reminderId);
await this.db.delete(this.schedule.id);
promptAction.showToast({ message: '删除成功' });
router.back();
} catch (err) {
promptAction.showToast({ message: '删除失败' });
}
}
});
}
formatDateTime(timestamp: number): string {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekDay = weekDays[date.getDay()];
return `${year}年${month}月${day}日 ${weekDay} ${hour}:${minute}`;
}
build() {
Column() {
// 顶部导航
Row() {
Button() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.fillColor('#333333')
}
.backgroundColor(Color.Transparent)
.onClick(() => router.back())
Text('日程详情')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Button() {
Image($r('app.media.ic_delete'))
.width(24)
.height(24)
.fillColor('#FF3B30')
}
.backgroundColor(Color.Transparent)
.onClick(() => this.deleteSchedule())
}
.width('100%')
.height(56)
.padding({ left: 8, right: 8 })
if (this.isLoading) {
Column() {
LoadingProgress().width(50).height(50)
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else if (this.schedule) {
Scroll() {
Column() {
// 标题卡片
Column() {
Text(this.schedule.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
if (this.schedule.note) {
Text(this.schedule.note)
.fontSize(15)
.fontColor('#666666')
.margin({ top: 12 })
}
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.alignItems(HorizontalAlign.Start)
// 详情信息卡片
Column() {
// 提醒时间
this.DetailRow('提醒时间', this.formatDateTime(this.schedule.remindTime))
Divider().color('#F0F0F0').margin({ left: 16, right: 16 })
// 提前提醒
this.DetailRow('提前提醒', getAdvanceText(this.schedule.advanceMinutes))
Divider().color('#F0F0F0').margin({ left: 16, right: 16 })
// 重复
this.DetailRow('重复', getRepeatTypeText(this.schedule.repeatType))
Divider().color('#F0F0F0').margin({ left: 16, right: 16 })
// 状态
this.DetailRow('状态', this.schedule.isEnabled ? '已启用' : '已禁用')
}
.width('100%')
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ top: 16 })
}
.padding(16)
}
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
DetailRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(15)
.fontColor('#999999')
Blank()
Text(value)
.fontSize(15)
.fontColor('#333333')
}
.width('100%')
.padding(16)
}
}
7. EntryAbility 入口 (entryability/EntryAbility.ets)
// entry/src/main/ets/entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { ScheduleDB } from '../utils/ScheduleDB';
export default class EntryAbility extends UIAbility {
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
hilog.info(0x0000, 'ScheduleManager', 'Ability onCreate');
// 初始化数据库
await ScheduleDB.getInstance().init(this.context);
}
onDestroy(): void {
hilog.info(0x0000, 'ScheduleManager', 'Ability onDestroy');
}
async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
hilog.info(0x0000, 'ScheduleManager', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, 'ScheduleManager', 'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(0x0000, 'ScheduleManager', 'Succeeded in loading content');
});
}
onWindowStageDestroy(): void {
hilog.info(0x0000, 'ScheduleManager', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(0x0000, 'ScheduleManager', 'Ability onForeground');
}
onBackground(): void {
hilog.info(0x0000, 'ScheduleManager', 'Ability onBackground');
}
}
四、资源文件准备
需要准备的图标资源
在 entry/src/main/resources/base/media/ 目录下添加:
| 文件名 | 用途 |
|---|---|
| ic_add.svg | 添加按钮图标 |
| ic_back.svg | 返回按钮图标 |
| ic_delete.svg | 删除按钮图标 |
| ic_time.svg | 时间图标 |
| ic_arrow_right.svg | 右箭头图标 |
| ic_empty.svg | 空状态图标 |
示例 SVG 图标内容
ic_add.svg:
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
ic_back.svg:
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
五、运行与测试
步骤1:编译运行
# 在 DevEco Studio 中
1. 连接真机或启动模拟器
2. 点击 Run 按钮或按 Shift+F10
3. 等待应用安装完成
步骤2:功能测试
1. 添加日程测试
- 点击右上角 + 按钮
- 输入标题:测试日程
- 选择时间:5分钟后
- 选择提前提醒:准时提醒
- 点击保存
2. 提醒测试
- 返回主页等待
- 5分钟后应收到系统通知
- 即使关闭应用也会收到提醒
3. 重复日程测试
- 添加一个每天重复的日程
- 验证每天都会收到提醒
六、核心API说明
reminderAgentManager 关键API
| API | 功能 | 说明 |
|---|---|---|
publishReminder() |
发布提醒 | 设置定时提醒,返回 reminderId |
cancelReminder() |
取消提醒 | 根据 reminderId 取消 |
getValidReminders() |
获取有效提醒 | 获取所有未触发的提醒 |
cancelAllReminders() |
取消所有提醒 | 取消当前应用所有提醒 |
提醒类型
// 日历提醒(精确到秒)
ReminderType.REMINDER_TYPE_CALENDAR
// 闹钟提醒(每天固定时间)
ReminderType.REMINDER_TYPE_ALARM
// 倒计时提醒
ReminderType.REMINDER_TYPE_TIMER
七、注意事项
- 权限申请:必须在 module.json5 中声明
ohos.permission.PUBLISH_AGENT_REMINDER - 时间限制:提醒时间必须大于当前时间
- 数量限制:单个应用最多设置 30 个提醒
- 重复规则:重复日程需要正确设置
repeatMonths和repeatDays - 后台保活:
reminderAgentManager由系统管理,无需应用保活
这套代码已经过实测,可以直接复制使用!我是 V 哥,关注我,一起探索新技术的魅力海洋。
更多推荐



所有评论(0)