diff --git a/app/(tabs)/challenges.tsx b/app/(tabs)/challenges.tsx index 5fe88a4..f338b2b 100644 --- a/app/(tabs)/challenges.tsx +++ b/app/(tabs)/challenges.tsx @@ -1,5 +1,4 @@ import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard'; -import { IconSymbol } from '@/components/ui/IconSymbol'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; @@ -127,7 +126,7 @@ export default function ChallengesScreen() { 挑战 参与精选活动,保持每日动力 - + {/* - + */} {ongoingChallenge ? ( diff --git a/app/challenges/[id].tsx b/app/challenges/[id].tsx index c679fd7..ebbf660 100644 --- a/app/challenges/[id].tsx +++ b/app/challenges/[id].tsx @@ -139,6 +139,7 @@ export default function ChallengeDetailScreen() { const progress = challenge?.progress; const rankingData = useMemo(() => challenge?.rankings ?? [], [challenge?.rankings]); + const participantAvatars = useMemo( () => rankingData.filter((item) => item.avatar).map((item) => item.avatar as string).slice(0, 6), [rankingData], @@ -296,11 +297,11 @@ export default function ChallengeDetailScreen() { tone="light" transparent withSafeTop={false} - right={ - - - - } + // right={ + // + // + // + // } /> diff --git a/services/backgroundTaskManager.ts b/services/backgroundTaskManager.ts index 9bad20c..fa0a3a5 100644 --- a/services/backgroundTaskManager.ts +++ b/services/backgroundTaskManager.ts @@ -1,7 +1,8 @@ import { store } from '@/store'; import AsyncStorage from '@/utils/kvStore'; import { log } from '@/utils/logger'; -import { StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers'; +import { listChallenges } from '@/services/challengesApi'; +import { ChallengeNotificationHelpers, StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers'; import * as BackgroundTask from 'expo-background-task'; import * as TaskManager from 'expo-task-manager'; import { TaskManagerTaskBody } from 'expo-task-manager'; @@ -99,6 +100,55 @@ async function executeStandReminderTask(): Promise { } } +async function executeChallengeReminderTask(): Promise { + try { + console.log('执行挑战鼓励提醒后台任务...'); + + const state = store.getState(); + const normalizedUserName = state.user.profile?.name?.trim(); + const userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友'; + + const challenges = await listChallenges(); + const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress); + + if (!joinedChallenges.length) { + console.log('没有加入的挑战或挑战没有进度,跳过挑战提醒'); + return; + } + + const todayKey = new Date().toISOString().slice(0, 10); + + for (const challenge of joinedChallenges) { + const progress = challenge.progress; + if (!progress || progress.checkedInToday) { + continue; + } + + const storageKey = `@challenge_encouragement_sent:${challenge.id}`; + const lastSent = await AsyncStorage.getItem(storageKey); + if (lastSent === todayKey) { + continue; + } + + try { + await ChallengeNotificationHelpers.sendEncouragementNotification({ + userName, + challengeTitle: challenge.title, + challengeId: challenge.id, + }); + + await AsyncStorage.setItem(storageKey, todayKey); + } catch (notificationError) { + console.error('发送挑战鼓励通知失败:', notificationError); + } + } + + console.log('挑战鼓励提醒后台任务完成'); + } catch (error) { + console.error('执行挑战鼓励提醒后台任务失败:', error); + } +} + // 发送测试通知以验证后台任务执行 async function sendTestNotification(): Promise { try { @@ -149,6 +199,8 @@ async function executeBackgroundTasks(): Promise { // 执行站立提醒检查任务 // await executeStandReminderTask(); + await executeChallengeReminderTask(); + console.log('后台任务执行完成'); } catch (error) { console.error('执行后台任务失败:', error); diff --git a/services/challengesApi.ts b/services/challengesApi.ts index f6ec29d..5be96a7 100644 --- a/services/challengesApi.ts +++ b/services/challengesApi.ts @@ -6,6 +6,7 @@ export type ChallengeProgressDto = { completed: number; target: number; remaining: number + checkedInToday: boolean; }; export type RankingItemDto = { diff --git a/services/notifications.ts b/services/notifications.ts index e58d740..8a193ad 100644 --- a/services/notifications.ts +++ b/services/notifications.ts @@ -161,6 +161,10 @@ export class NotificationService { if (data?.url) { router.push(data.url as any); } + } else if (data?.type === NotificationTypes.CHALLENGE_ENCOURAGEMENT) { + console.log('用户点击了挑战提醒通知', data); + const targetUrl = (data?.url as string) || '/(tabs)/challenges'; + router.push(targetUrl as any); } else if (data?.type === 'mood_reminder') { // 处理心情提醒通知 console.log('用户点击了心情提醒通知', data); @@ -506,6 +510,7 @@ export const NotificationTypes = { MOOD_REMINDER: 'mood_reminder', WATER_REMINDER: 'water_reminder', REGULAR_WATER_REMINDER: 'regular_water_reminder', + CHALLENGE_ENCOURAGEMENT: 'challenge_encouragement', } as const; // 便捷方法 diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 626d8e1..6029ea6 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -1,5 +1,5 @@ import * as Notifications from 'expo-notifications'; -import { NotificationData, notificationService } from '../services/notifications'; +import { NotificationData, NotificationTypes, notificationService } from '../services/notifications'; import { getNotificationEnabled } from './userPreferences'; /** @@ -287,6 +287,33 @@ export class GoalNotificationHelpers { } } +export class ChallengeNotificationHelpers { + static buildChallengesTabUrl(): string { + return '/(tabs)/challenges'; + } + + static async sendEncouragementNotification(params: { + userName: string; + challengeTitle: string; + challengeId: string; + }): Promise { + const { userName, challengeTitle, challengeId } = params; + const notification: NotificationData = { + title: '挑战提醒', + body: `${userName},今天还没有完成「${challengeTitle}」挑战,快来打卡吧!`, + data: { + type: NotificationTypes.CHALLENGE_ENCOURAGEMENT, + challengeId, + url: ChallengeNotificationHelpers.buildChallengesTabUrl(), + }, + sound: true, + priority: 'high', + }; + + return notificationService.sendImmediateNotification(notification); + } +} + /** * 营养相关的通知辅助函数 */