Files
digital-pilates/services/notifications.ts
richarjiang 20a244e375 feat: 实现目标通知功能及相关组件
- 新增目标通知功能,支持根据用户创建目标时选择的频率和开始时间自动创建本地定时推送通知
- 实现每日、每周和每月的重复类型,用户可自定义选择提醒时间和重复规则
- 集成目标通知测试组件,方便开发者测试不同类型的通知
- 更新相关文档,详细描述目标通知功能的实现和使用方法
- 优化目标页面,确保用户体验和界面一致性
2025-08-23 17:13:04 +08:00

429 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
// 配置通知处理方式
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
export interface NotificationData {
title: string;
body: string;
data?: Record<string, any>;
sound?: boolean;
priority?: 'default' | 'normal' | 'high';
vibrate?: number[];
}
export class NotificationService {
private static instance: NotificationService;
private isInitialized = false;
private constructor() {}
public static getInstance(): NotificationService {
if (!NotificationService.instance) {
NotificationService.instance = new NotificationService();
}
return NotificationService.instance;
}
/**
* 初始化推送通知服务
*/
async initialize(): Promise<void> {
if (this.isInitialized) return;
try {
// 请求通知权限
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('推送通知权限未授予');
return;
}
// 获取推送令牌(用于远程推送,本地推送不需要)
if (Platform.OS !== 'web') {
const token = await Notifications.getExpoPushTokenAsync({
projectId: 'your-project-id', // 需要替换为实际的Expo项目ID
});
console.log('推送令牌:', token.data);
}
// 设置通知监听器
this.setupNotificationListeners();
this.isInitialized = true;
console.log('推送通知服务初始化成功');
} catch (error) {
console.error('推送通知服务初始化失败:', error);
}
}
/**
* 设置通知监听器
*/
private setupNotificationListeners(): void {
// 监听通知接收
Notifications.addNotificationReceivedListener((notification) => {
console.log('收到通知:', notification);
});
// 监听通知点击
Notifications.addNotificationResponseReceivedListener((response) => {
console.log('用户点击了通知:', response);
// 这里可以处理通知点击后的逻辑
this.handleNotificationResponse(response);
});
}
/**
* 处理通知响应
*/
private handleNotificationResponse(response: Notifications.NotificationResponse): void {
const { notification } = response;
const data = notification.request.content.data;
// 根据通知类型处理不同的逻辑
if (data?.type === 'workout_reminder') {
// 处理运动提醒
console.log('用户点击了运动提醒通知');
} else if (data?.type === 'goal_achievement') {
// 处理目标达成通知
console.log('用户点击了目标达成通知');
} else if (data?.type === 'mood_checkin') {
// 处理心情打卡提醒
console.log('用户点击了心情打卡提醒');
} else if (data?.type === 'goal_reminder') {
// 处理目标提醒通知
console.log('用户点击了目标提醒通知', data);
// 这里可以添加导航到目标页面的逻辑
}
}
/**
* 发送本地推送通知
*/
async scheduleLocalNotification(
notification: NotificationData,
trigger?: Notifications.NotificationTriggerInput
): Promise<string> {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: trigger || null, // null表示立即发送
});
console.log('本地通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排本地通知失败:', error);
throw error;
}
}
/**
* 发送立即通知
*/
async sendImmediateNotification(notification: NotificationData): Promise<string> {
return this.scheduleLocalNotification(notification);
}
/**
* 安排定时通知
*/
async scheduleNotificationAtDate(
notification: NotificationData,
date: Date
): Promise<string> {
try {
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: {
date: date.getTime(),
} as any,
});
console.log('定时通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排定时通知失败:', error);
throw error;
}
}
/**
* 安排重复通知(仅支持秒级别)
*/
async scheduleRepeatingNotification(
notification: NotificationData,
interval: {
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
weeks?: number;
months?: number;
years?: number;
}
): Promise<string> {
try {
// 计算总秒数
const totalSeconds =
(interval.seconds || 0) +
(interval.minutes || 0) * 60 +
(interval.hours || 0) * 3600 +
(interval.days || 0) * 86400 +
(interval.weeks || 0) * 604800 +
(interval.months || 0) * 2592000 +
(interval.years || 0) * 31536000;
if (totalSeconds <= 0) {
throw new Error('重复间隔必须大于0');
}
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger: {
seconds: totalSeconds,
repeats: true,
} as any,
});
console.log('重复通知已安排ID:', notificationId);
return notificationId;
} catch (error) {
console.error('安排重复通知失败:', error);
throw error;
}
}
/**
* 安排日历重复通知(支持每日、每周、每月)
*/
async scheduleCalendarRepeatingNotification(
notification: NotificationData,
options: {
type: 'daily' | 'weekly' | 'monthly';
hour: number;
minute: number;
weekdays?: number[]; // 0-60为周日仅用于weekly类型
dayOfMonth?: number; // 1-31仅用于monthly类型
}
): Promise<string> {
try {
let trigger: any;
switch (options.type) {
case 'daily':
trigger = {
hour: options.hour,
minute: options.minute,
repeats: true,
};
break;
case 'weekly':
if (options.weekdays && options.weekdays.length > 0) {
trigger = {
hour: options.hour,
minute: options.minute,
weekday: options.weekdays[0], // Expo只支持单个weekday
repeats: true,
};
} else {
trigger = {
hour: options.hour,
minute: options.minute,
repeats: true,
};
}
break;
case 'monthly':
trigger = {
hour: options.hour,
minute: options.minute,
day: options.dayOfMonth || 1,
repeats: true,
};
break;
default:
throw new Error('不支持的重复类型');
}
const notificationId = await Notifications.scheduleNotificationAsync({
content: {
title: notification.title,
body: notification.body,
data: notification.data || {},
sound: notification.sound ? 'default' : undefined,
priority: notification.priority || 'default',
vibrate: notification.vibrate,
},
trigger,
});
console.log(`${options.type}重复通知已安排ID:`, notificationId);
return notificationId;
} catch (error) {
console.error('安排日历重复通知失败:', error);
throw error;
}
}
/**
* 取消特定通知
*/
async cancelNotification(notificationId: string): Promise<void> {
try {
await Notifications.cancelScheduledNotificationAsync(notificationId);
console.log('通知已取消:', notificationId);
} catch (error) {
console.error('取消通知失败:', error);
throw error;
}
}
/**
* 取消所有通知
*/
async cancelAllNotifications(): Promise<void> {
try {
await Notifications.cancelAllScheduledNotificationsAsync();
console.log('所有通知已取消');
} catch (error) {
console.error('取消所有通知失败:', error);
throw error;
}
}
/**
* 获取所有已安排的通知
*/
async getAllScheduledNotifications(): Promise<Notifications.NotificationRequest[]> {
try {
const notifications = await Notifications.getAllScheduledNotificationsAsync();
return notifications;
} catch (error) {
console.error('获取已安排通知失败:', error);
throw error;
}
}
/**
* 获取通知权限状态
*/
async getPermissionStatus(): Promise<Notifications.PermissionStatus> {
try {
const { status } = await Notifications.getPermissionsAsync();
return status;
} catch (error) {
console.error('获取通知权限状态失败:', error);
throw error;
}
}
/**
* 请求通知权限
*/
async requestPermission(): Promise<Notifications.PermissionStatus> {
try {
const { status } = await Notifications.requestPermissionsAsync();
return status;
} catch (error) {
console.error('请求通知权限失败:', error);
throw error;
}
}
}
// 导出单例实例
export const notificationService = NotificationService.getInstance();
// 预定义的推送通知类型
export const NotificationTypes = {
WORKOUT_REMINDER: 'workout_reminder',
GOAL_ACHIEVEMENT: 'goal_achievement',
MOOD_CHECKIN: 'mood_checkin',
NUTRITION_REMINDER: 'nutrition_reminder',
PROGRESS_UPDATE: 'progress_update',
} as const;
// 便捷方法
export const sendWorkoutReminder = (title: string, body: string, date?: Date) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.WORKOUT_REMINDER },
sound: true,
priority: 'high',
};
if (date) {
return notificationService.scheduleNotificationAtDate(notification, date);
} else {
return notificationService.sendImmediateNotification(notification);
}
};
export const sendGoalAchievement = (title: string, body: string) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.GOAL_ACHIEVEMENT },
sound: true,
priority: 'high',
};
return notificationService.sendImmediateNotification(notification);
};
export const sendMoodCheckinReminder = (title: string, body: string, date?: Date) => {
const notification: NotificationData = {
title,
body,
data: { type: NotificationTypes.MOOD_CHECKIN },
sound: true,
priority: 'normal',
};
if (date) {
return notificationService.scheduleNotificationAtDate(notification, date);
} else {
return notificationService.sendImmediateNotification(notification);
}
};