From 20a244e37503b459aa7e8271d9bf30db856031bb Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sat, 23 Aug 2025 17:13:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增目标通知功能,支持根据用户创建目标时选择的频率和开始时间自动创建本地定时推送通知 - 实现每日、每周和每月的重复类型,用户可自定义选择提醒时间和重复规则 - 集成目标通知测试组件,方便开发者测试不同类型的通知 - 更新相关文档,详细描述目标通知功能的实现和使用方法 - 优化目标页面,确保用户体验和界面一致性 --- app/(tabs)/goals.tsx | 118 +++++++++ app/goals-detail.tsx | 50 ++-- app/task-detail.tsx | 26 +- assets/images/icons/icon-complete.png | Bin 0 -> 12134 bytes assets/images/task/icon-complete-gradient.png | Bin 0 -> 11480 bytes assets/images/task/icon-skip.png | Bin 0 -> 21853 bytes components/{TodoCard.tsx => GoalCard.tsx} | 68 +----- .../{TodoCarousel.tsx => GoalCarousel.tsx} | 24 +- components/GoalNotificationTest.tsx | 195 +++++++++++++++ components/TaskCard.tsx | 46 +--- components/TimelineSchedule.tsx | 6 +- docs/goal-notification-implementation.md | 223 ++++++++++++++++++ docs/goal-notification-summary.md | 121 ++++++++++ services/notifications.ts | 76 ++++++ utils/notificationHelpers.ts | 160 ++++++++++++- 15 files changed, 957 insertions(+), 156 deletions(-) create mode 100644 assets/images/icons/icon-complete.png create mode 100644 assets/images/task/icon-complete-gradient.png create mode 100644 assets/images/task/icon-skip.png rename components/{TodoCard.tsx => GoalCard.tsx} (70%) rename components/{TodoCarousel.tsx => GoalCarousel.tsx} (80%) create mode 100644 components/GoalNotificationTest.tsx create mode 100644 docs/goal-notification-implementation.md create mode 100644 docs/goal-notification-summary.md diff --git a/app/(tabs)/goals.tsx b/app/(tabs)/goals.tsx index 4d2c13f..dfe2537 100644 --- a/app/(tabs)/goals.tsx +++ b/app/(tabs)/goals.tsx @@ -13,6 +13,7 @@ import { clearErrors, createGoal } from '@/store/goalsSlice'; import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice'; import { CreateGoalRequest, TaskListItem } from '@/types/goals'; import { checkGuideCompleted, markGuideCompleted } from '@/utils/guideHelpers'; +import { GoalNotificationHelpers } from '@/utils/notificationHelpers'; import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; @@ -45,6 +46,8 @@ export default function GoalsScreen() { createError } = useAppSelector((state) => state.goals); + const userProfile = useAppSelector((state) => state.user.profile); + const [showCreateModal, setShowCreateModal] = useState(false); const [refreshing, setRefreshing] = useState(false); const [selectedFilter, setSelectedFilter] = useState('all'); @@ -145,6 +148,30 @@ export default function GoalsScreen() { await dispatch(createGoal(goalData)).unwrap(); setShowCreateModal(false); + // 获取用户名 + const userName = userProfile?.name || '小海豹'; + + // 创建目标成功后,设置定时推送 + try { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: goalData.title, + repeatType: goalData.repeatType, + frequency: goalData.frequency, + hasReminder: goalData.hasReminder, + reminderTime: goalData.reminderTime, + customRepeatRule: goalData.customRepeatRule, + startTime: goalData.startTime, + }, + userName + ); + + console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds); + } catch (notificationError) { + console.error('创建目标定时推送失败:', notificationError); + // 通知创建失败不影响目标创建的成功 + } + // 使用确认弹窗显示成功消息 showConfirm( { @@ -389,6 +416,82 @@ export default function GoalsScreen() { {/* 开发测试按钮 */} + + {/* 目标通知测试按钮 */} + {__DEV__ && ( + { + // 这里可以导航到测试页面或显示测试弹窗 + Alert.alert( + '目标通知测试', + '选择要测试的通知类型', + [ + { text: '取消', style: 'cancel' }, + { + text: '每日目标通知', + onPress: async () => { + try { + const userName = userProfile?.name || '小海豹'; + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每日运动目标', + repeatType: 'daily', + frequency: 1, + hasReminder: true, + reminderTime: '09:00', + }, + userName + ); + Alert.alert('成功', `每日目标通知已创建,ID: ${notificationIds.join(', ')}`); + } catch (error) { + Alert.alert('错误', `创建通知失败: ${error}`); + } + } + }, + { + text: '每周目标通知', + onPress: async () => { + try { + const userName = userProfile?.name || '小海豹'; + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每周运动目标', + repeatType: 'weekly', + frequency: 1, + hasReminder: true, + reminderTime: '10:00', + customRepeatRule: { + weekdays: [1, 3, 5], // 周一、三、五 + }, + }, + userName + ); + Alert.alert('成功', `每周目标通知已创建,ID: ${notificationIds.join(', ')}`); + } catch (error) { + Alert.alert('错误', `创建通知失败: ${error}`); + } + } + }, + { + text: '目标达成通知', + onPress: async () => { + try { + const userName = userProfile?.name || '小海豹'; + await GoalNotificationHelpers.sendGoalAchievementNotification(userName, '每日运动目标'); + Alert.alert('成功', '目标达成通知已发送'); + } catch (error) { + Alert.alert('错误', `发送通知失败: ${error}`); + } + } + }, + ] + ); + }} + > + 测试通知 + + )} ); @@ -567,4 +670,19 @@ const styles = StyleSheet.create({ fontWeight: '600', lineHeight: 18, }, + testButton: { + position: 'absolute', + top: 100, + right: 20, + backgroundColor: '#10B981', + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 8, + zIndex: 1000, + }, + testButtonText: { + color: '#FFFFFF', + fontSize: 12, + fontWeight: '600', + }, }); diff --git a/app/goals-detail.tsx b/app/goals-detail.tsx index c9fa75a..4bfe51a 100644 --- a/app/goals-detail.tsx +++ b/app/goals-detail.tsx @@ -1,13 +1,13 @@ import CreateGoalModal from '@/components/CreateGoalModal'; import { DateSelector } from '@/components/DateSelector'; +import { GoalItem } from '@/components/GoalCard'; +import { GoalCarousel } from '@/components/GoalCarousel'; import { TimeTabSelector, TimeTabType } from '@/components/TimeTabSelector'; import { TimelineSchedule } from '@/components/TimelineSchedule'; -import { TodoItem } from '@/components/TodoCard'; -import { TodoCarousel } from '@/components/TodoCarousel'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; -import { clearErrors, completeGoal, createGoal, fetchGoals } from '@/store/goalsSlice'; +import { clearErrors, createGoal, fetchGoals } from '@/store/goalsSlice'; import { CreateGoalRequest, GoalListItem } from '@/types/goals'; import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; import { useFocusEffect } from '@react-navigation/native'; @@ -20,8 +20,8 @@ import { Alert, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, Touchable dayjs.extend(isBetween); -// 将目标转换为TodoItem的辅助函数 -const goalToTodoItem = (goal: GoalListItem): TodoItem => { +// 将目标转换为GoalItem的辅助函数 +const goalToGoalItem = (goal: GoalListItem): GoalItem => { return { id: goal.id, title: goal.title, @@ -29,7 +29,6 @@ const goalToTodoItem = (goal: GoalListItem): TodoItem => { time: dayjs().startOf('day').add(goal.startTime, 'minute').toISOString() || '', category: getCategoryFromGoal(goal.category), priority: getPriorityFromGoal(goal.priority), - isCompleted: goal.status === 'completed', }; }; @@ -43,8 +42,8 @@ const getRepeatTypeLabel = (repeatType: string): string => { } }; -// 从目标分类获取TodoItem分类 -const getCategoryFromGoal = (category?: string): TodoItem['category'] => { +// 从目标分类获取GoalItem分类 +const getCategoryFromGoal = (category?: string): GoalItem['category'] => { if (!category) return 'personal'; if (category.includes('运动') || category.includes('健身')) return 'workout'; if (category.includes('工作')) return 'work'; @@ -53,8 +52,8 @@ const getCategoryFromGoal = (category?: string): TodoItem['category'] => { return 'personal'; }; -// 从目标优先级获取TodoItem优先级 -const getPriorityFromGoal = (priority: number): TodoItem['priority'] => { +// 从目标优先级获取GoalItem优先级 +const getPriorityFromGoal = (priority: number): GoalItem['priority'] => { if (priority >= 8) return 'high'; if (priority >= 5) return 'medium'; return 'low'; @@ -206,8 +205,8 @@ export default function GoalsDetailScreen() { } }; - // 将目标转换为TodoItem数据 - const todayTodos = useMemo(() => { + // 将目标转换为GoalItem数据 + const todayGoals = useMemo(() => { const today = dayjs(); const activeGoals = goals.filter(goal => goal.status === 'active' && @@ -215,7 +214,7 @@ export default function GoalsDetailScreen() { (goal.repeatType === 'weekly' && today.day() !== 0) || (goal.repeatType === 'monthly' && today.date() <= 28)) ); - return activeGoals.map(goalToTodoItem); + return activeGoals.map(goalToGoalItem); }, [goals]); // 将目标转换为时间轴事件数据 @@ -251,25 +250,11 @@ export default function GoalsDetailScreen() { console.log('filteredTimelineEvents', filteredTimelineEvents); - const handleTodoPress = (item: TodoItem) => { + const handleGoalPress = (item: GoalItem) => { console.log('Goal pressed:', item.title); // 这里可以导航到目标详情页面 }; - const handleToggleComplete = async (item: TodoItem) => { - try { - await dispatch(completeGoal({ - goalId: item.id, - completionData: { - completionCount: 1, - notes: '通过待办卡片完成' - } - })).unwrap(); - } catch (error) { - Alert.alert('错误', '记录完成失败'); - } - }; - const handleEventPress = (event: any) => { console.log('Event pressed:', event.title); // 这里可以处理时间轴事件点击 @@ -318,11 +303,10 @@ export default function GoalsDetailScreen() { - {/* 今日待办事项卡片 */} - {/* 时间筛选选项卡 */} diff --git a/app/task-detail.tsx b/app/task-detail.tsx index d290d80..a4ec1e8 100644 --- a/app/task-detail.tsx +++ b/app/task-detail.tsx @@ -312,6 +312,19 @@ export default function TaskDetailScreen() { + {/* 底部操作按钮 */} + {task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount && ( + + + + 跳过任务 + + + )} + {/* 评论区域 */} 评论区域 @@ -348,19 +361,6 @@ export default function TaskDetailScreen() { - - {/* 底部操作按钮 */} - {task.status !== 'completed' && task.status !== 'skipped' && task.currentCount < task.targetCount && ( - - - - 跳过任务 - - - )} ); diff --git a/assets/images/icons/icon-complete.png b/assets/images/icons/icon-complete.png new file mode 100644 index 0000000000000000000000000000000000000000..2e2e5c0c692a845ca740abe8e4052833553ff95c GIT binary patch literal 12134 zcmc&)1ydZ(&!$+B;$HkX+^x7DR*F;HDU{;w?uWY-_u_WA6gb@7?V!co{omi0c;A_1 zGrKcMc4xBLCp!VFD9K`=eMEzUgTs)Q1F8L!L;o=f^1rc4yE^BeKy{GQafX9~r~i-N zb>V8D{9AnItR^c7S3N;?@Na_nT|!X;4z4Z^{n-Qw4vy_f9wedR@$MuWHH*wV`|eHo ztE;l|q)5^?r%j?=AQ^R_B|HkLIA$vnzN3T-fu+*P4TMeKM#e=mgGN2-liV1~U z9~BXCY`2T*jx&QMskOAUw7op%PM{*^Fr$`=BV*@nyi|RDex7%IezN9u@k$sefC?q( z|CPA*sn4ABy1)JQ!6{m#KHR>dUo`Y~lMu|N=C)u_V%2LJJNrS2NWucW_cv<0Q}As6 z*yNw4_GY|rwm)=F&X`tW^-~C|`@Hw4yWOqF9Q|_sHP!1{Xex;63~9sA=GMVz^%9nW z9&2=YoxhNM(d5ER3kATwe*R^Pqn0X$_&HJ9PQm)N>k>xkpMq`jwWYRxDo#9*>Wt9# z-E#~t`=)^J5@Yj;?b2!RA8y5KWAY^#x{tXK4(pNlompGE(ZNWwKFtO#vfJ}}iYcu9 zGz+W)SC7J5`;H5l)lydL!L-Q^TlYTAw4m>*pS|p)LYRLyr=vYLA2mZo_?|vXIge)_ z6A%><91`UrNxbrf=r#I`U*d58ja@l)NhWGGKQ#C&FR<=jJpC1y<@cSBy~hK?>WPE@ zr$(RSJ&DMPP*F@>>|M)qw{cR(+Z6IimK^Ty*ZJDsKUBR-deUG+olRjd75{j}1%U58gA!@PD>p^{Vnr@I>FU3OLMPtIjJq+q9zpLUtqQ(A4seNDdk z#p_&=Wz{WRt|JjvKq-1LhOi?MS)}aExo=9{iU^51ocl|E$2*rJR_ze&>t~9y${)Ln#q}@97O|qPV$|QrhCHCbqG-CNqFne z9YskaJat;$ZK*S-6~IOSXb><`fc^6EjfSEak9m-CtT{aTtADey=IV416`29t^+&E% zqjjCD+qL?R1Um`8{8>j0$NlC6hm^u9l#|y)RAxG-hvmdcFv#drB{=LOq4bUy#aJfKAF}f?WZb~c_U5j*K0%e85SVrVR*FjZ`-T6s*$X6JFRR6}wN6eu>E|d1>fFHJZVdUB z4cHMN?GG#C5g^TIFBE~#)yZct9UZ6Mbi79!q2mfl7Wz4%L(%jSzq1ImhaL7!5mS%; zbPRhN-JMAi?oFj9^7sk2$TWzMG{)*SDFCPDszcpQV(P=!FH$q(K|^L-)>5{7seZ5; zWgZF2;+mUKRHuvQ;iXCcu50KIbmfn^KeFY^#CZOA77f!t-d~woyNkXJLR^_OU3R

{AvE_Lv8Jal5U!?Aw=bHDJqn3=;%rA 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 literal 0 HcmV?d00001 diff --git a/assets/images/task/icon-complete-gradient.png b/assets/images/task/icon-complete-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd9fd0b527c1f00fa1cd2d7bba52a23564c11d6 GIT binary patch literal 11480 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 PGLnL*SHfj literal 0 HcmV?d00001 diff --git a/assets/images/task/icon-skip.png b/assets/images/task/icon-skip.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3ceceb4f1bd70531f2e3d95beb357eefae18ff GIT binary patch literal 21853 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 p{D+V4`%-ym_y5KTO-+13*Cyt)em=3pcJf literal 0 HcmV?d00001 diff --git a/components/TodoCard.tsx b/components/GoalCard.tsx similarity index 70% rename from components/TodoCard.tsx rename to components/GoalCard.tsx index 28010ef..b7ea643 100644 --- a/components/TodoCard.tsx +++ b/components/GoalCard.tsx @@ -5,26 +5,24 @@ import dayjs from 'dayjs'; import React from 'react'; import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -export interface TodoItem { +export interface GoalItem { id: string; title: string; description?: string; time: string; category: 'workout' | 'finance' | 'personal' | 'work' | 'health'; - isCompleted?: boolean; priority?: 'high' | 'medium' | 'low'; } -interface TodoCardProps { - item: TodoItem; - onPress?: (item: TodoItem) => void; - onToggleComplete?: (item: TodoItem) => void; +interface GoalCardProps { + item: GoalItem; + onPress?: (item: GoalItem) => void; } const { width: screenWidth } = Dimensions.get('window'); const CARD_WIDTH = (screenWidth - 60) * 0.65; // 显示1.5张卡片 -const getCategoryIcon = (category: TodoItem['category']) => { +const getCategoryIcon = (category: GoalItem['category']) => { switch (category) { case 'workout': return 'fitness-outline'; @@ -41,7 +39,7 @@ const getCategoryIcon = (category: TodoItem['category']) => { } }; -const getCategoryColor = (category: TodoItem['category']) => { +const getCategoryColor = (category: GoalItem['category']) => { switch (category) { case 'workout': return '#FF6B6B'; @@ -58,7 +56,7 @@ const getCategoryColor = (category: TodoItem['category']) => { } }; -const getPriorityColor = (priority: TodoItem['priority']) => { +const getPriorityColor = (priority: GoalItem['priority']) => { switch (priority) { case 'high': return '#FF4757'; @@ -71,7 +69,7 @@ const getPriorityColor = (priority: TodoItem['priority']) => { } }; -export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) { +export function GoalCard({ item, onPress }: GoalCardProps) { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; @@ -80,7 +78,6 @@ export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) { const priorityColor = getPriorityColor(item.priority); const timeFormatted = dayjs(item.time).format('HH:mm'); - const isToday = dayjs(item.time).isSame(dayjs(), 'day'); return ( {item.category} - + {item.priority && ( + + )} {/* 主要内容 */} @@ -111,7 +110,7 @@ export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) { )} - {/* 底部时间和完成状态 */} + {/* 底部时间 */} - + {timeFormatted} - - onToggleComplete?.(item)} - > - {item.isCompleted && ( - - )} - - - {/* 完成状态遮罩 */} - {item.isCompleted && ( - - - - )} ); } @@ -217,23 +194,4 @@ const styles = StyleSheet.create({ fontWeight: '500', marginLeft: 4, }, - completeButton: { - width: 24, - height: 24, - borderRadius: 12, - borderWidth: 1.5, - justifyContent: 'center', - alignItems: 'center', - }, - completedOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: 'rgba(255, 255, 255, 0.9)', - borderRadius: 20, - justifyContent: 'center', - alignItems: 'center', - }, }); diff --git a/components/TodoCarousel.tsx b/components/GoalCarousel.tsx similarity index 80% rename from components/TodoCarousel.tsx rename to components/GoalCarousel.tsx index 0c63530..ceb979f 100644 --- a/components/TodoCarousel.tsx +++ b/components/GoalCarousel.tsx @@ -2,26 +2,25 @@ import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import React, { useRef } from 'react'; import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { TodoCard, TodoItem } from './TodoCard'; +import { GoalCard, GoalItem } from './GoalCard'; -interface TodoCarouselProps { - todos: TodoItem[]; - onTodoPress?: (item: TodoItem) => void; - onToggleComplete?: (item: TodoItem) => void; +interface GoalCarouselProps { + goals: GoalItem[]; + onGoalPress?: (item: GoalItem) => void; } const { width: screenWidth } = Dimensions.get('window'); -export function TodoCarousel({ todos, onTodoPress, onToggleComplete }: TodoCarouselProps) { +export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; const scrollViewRef = useRef(null); - if (!todos || todos.length === 0) { + if (!goals || goals.length === 0) { return ( - 今天暂无待办事项 + 今天暂无目标 ); @@ -39,12 +38,11 @@ export function TodoCarousel({ todos, onTodoPress, onToggleComplete }: TodoCarou contentContainerStyle={styles.scrollContent} style={styles.scrollView} > - {todos.map((item, index) => ( - ( + ))} {/* 占位符,确保最后一张卡片有足够的滑动空间 */} @@ -53,7 +51,7 @@ export function TodoCarousel({ todos, onTodoPress, onToggleComplete }: TodoCarou {/* 底部指示器 */} {/* - {todos.map((_, index) => ( + {goals.map((_, index) => ( { + const [testResults, setTestResults] = useState([]); + + const addResult = (result: string) => { + setTestResults(prev => [...prev, `${new Date().toLocaleTimeString()}: ${result}`]); + }; + + const testDailyGoalNotification = async () => { + try { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每日运动目标', + repeatType: 'daily', + frequency: 1, + hasReminder: true, + reminderTime: '09:00', + }, + '测试用户' + ); + addResult(`每日目标通知创建成功,ID: ${notificationIds.join(', ')}`); + } catch (error) { + addResult(`每日目标通知创建失败: ${error}`); + } + }; + + const testWeeklyGoalNotification = async () => { + try { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每周运动目标', + repeatType: 'weekly', + frequency: 1, + hasReminder: true, + reminderTime: '10:00', + customRepeatRule: { + weekdays: [1, 3, 5], // 周一、三、五 + }, + }, + '测试用户' + ); + addResult(`每周目标通知创建成功,ID: ${notificationIds.join(', ')}`); + } catch (error) { + addResult(`每周目标通知创建失败: ${error}`); + } + }; + + const testMonthlyGoalNotification = async () => { + try { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每月运动目标', + repeatType: 'monthly', + frequency: 1, + hasReminder: true, + reminderTime: '11:00', + customRepeatRule: { + dayOfMonth: [1, 15], // 每月1号和15号 + }, + }, + '测试用户' + ); + addResult(`每月目标通知创建成功,ID: ${notificationIds.join(', ')}`); + } catch (error) { + addResult(`每月目标通知创建失败: ${error}`); + } + }; + + const testGoalAchievementNotification = async () => { + try { + await GoalNotificationHelpers.sendGoalAchievementNotification('测试用户', '每日运动目标'); + addResult('目标达成通知发送成功'); + } catch (error) { + addResult(`目标达成通知发送失败: ${error}`); + } + }; + + const testCancelGoalNotifications = async () => { + try { + await GoalNotificationHelpers.cancelGoalNotifications('每日运动目标'); + addResult('目标通知取消成功'); + } catch (error) { + addResult(`目标通知取消失败: ${error}`); + } + }; + + const clearResults = () => { + setTestResults([]); + }; + + return ( + + + 目标通知测试 + + + + 测试每日目标通知 + + + + 测试每周目标通知 + + + + 测试每月目标通知 + + + + 测试目标达成通知 + + + + 取消目标通知 + + + + 清除结果 + + + + + 测试结果: + {testResults.map((result, index) => ( + + {result} + + ))} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + }, + scrollView: { + flex: 1, + }, + title: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 20, + textAlign: 'center', + }, + buttonContainer: { + gap: 10, + marginBottom: 20, + }, + button: { + backgroundColor: '#6366F1', + padding: 15, + borderRadius: 10, + alignItems: 'center', + }, + clearButton: { + backgroundColor: '#EF4444', + padding: 15, + borderRadius: 10, + alignItems: 'center', + }, + buttonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + resultsContainer: { + flex: 1, + }, + resultsTitle: { + fontSize: 18, + fontWeight: '600', + marginBottom: 10, + }, + resultText: { + fontSize: 14, + marginBottom: 5, + padding: 10, + backgroundColor: '#F3F4F6', + borderRadius: 5, + }, +}); diff --git a/components/TaskCard.tsx b/components/TaskCard.tsx index 49eb94f..8f664e6 100644 --- a/components/TaskCard.tsx +++ b/components/TaskCard.tsx @@ -156,7 +156,7 @@ export const TaskCard: React.FC = ({ onPress={handleCompleteTask} > @@ -168,7 +168,11 @@ export const TaskCard: React.FC = ({ style={styles.skipIconContainer} onPress={handleSkipTask} > - + )} @@ -197,10 +201,6 @@ export const TaskCard: React.FC = ({ {getStatusText(task.status)} - - - {getPriorityText(task.status)} - {/* 进度条 */} @@ -225,33 +225,6 @@ export const TaskCard: React.FC = ({ {task.currentCount}/{task.targetCount} - - {/* 底部信息 */} - - - {/* 团队成员头像 */} - - - - - - - - - - - {formatDate(task.startDate)} - - - - 2 - - - ); }; @@ -291,19 +264,17 @@ const styles = StyleSheet.create({ width: 32, height: 32, borderRadius: 16, - backgroundColor: '#7A5AF8', alignItems: 'center', justifyContent: 'center', + backgroundColor: '#F3F4F6', }, skipIconContainer: { width: 32, height: 32, - borderRadius: 16, + borderRadius: 16, backgroundColor: '#F3F4F6', alignItems: 'center', justifyContent: 'center', - borderWidth: 1, - borderColor: '#E5E7EB', }, taskIcon: { width: 20, @@ -431,7 +402,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, - backgroundColor: '#F3F4F6', }, infoTagText: { fontSize: 12, diff --git a/components/TimelineSchedule.tsx b/components/TimelineSchedule.tsx index c201734..5baaaa8 100644 --- a/components/TimelineSchedule.tsx +++ b/components/TimelineSchedule.tsx @@ -4,14 +4,14 @@ import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { TodoItem } from './TodoCard'; +import { GoalItem } from './GoalCard'; interface TimelineEvent { id: string; title: string; startTime: string; endTime?: string; - category: TodoItem['category']; + category: GoalItem['category']; isCompleted?: boolean; color?: string; } @@ -52,7 +52,7 @@ const getEventStyle = (event: TimelineEvent) => { }; // 获取分类颜色 -const getCategoryColor = (category: TodoItem['category']) => { +const getCategoryColor = (category: GoalItem['category']) => { switch (category) { case 'workout': return '#FF6B6B'; diff --git a/docs/goal-notification-implementation.md b/docs/goal-notification-implementation.md new file mode 100644 index 0000000..ed47b10 --- /dev/null +++ b/docs/goal-notification-implementation.md @@ -0,0 +1,223 @@ +# 目标通知功能实现文档 + +## 概述 + +本功能实现了根据用户创建目标时选择的频率和开始时间,自动创建本地定时推送通知。当用户创建目标并开启提醒功能时,系统会根据目标的重复类型(每日、每周、每月)和提醒时间,自动安排相应的本地推送通知。 + +## 功能特性 + +### ✅ 已实现功能 + +- [x] 根据目标重复类型创建定时推送 +- [x] 支持每日重复通知 +- [x] 支持每周重复通知(可自定义星期几) +- [x] 支持每月重复通知(可自定义日期) +- [x] 支持自定义提醒时间 +- [x] 目标达成通知 +- [x] 取消特定目标的通知 +- [x] 通知点击处理 +- [x] 开发环境测试功能 + +## 技术实现 + +### 1. 通知服务扩展 (services/notifications.ts) + +#### 新增方法 + +```typescript +/** + * 安排日历重复通知(支持每日、每周、每月) + */ +async scheduleCalendarRepeatingNotification( + notification: NotificationData, + options: { + type: 'daily' | 'weekly' | 'monthly'; + hour: number; + minute: number; + weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型 + dayOfMonth?: number; // 1-31,仅用于monthly类型 + } +): Promise +``` + +#### 通知处理扩展 + +```typescript +// 处理目标提醒通知点击 +else if (data?.type === 'goal_reminder') { + console.log('用户点击了目标提醒通知', data); + // 这里可以添加导航到目标页面的逻辑 +} +``` + +### 2. 目标通知辅助函数 (utils/notificationHelpers.ts) + +#### 核心方法 + +```typescript +/** + * 根据目标设置创建定时推送 + */ +static async scheduleGoalNotifications( + goalData: { + title: string; + repeatType: 'daily' | 'weekly' | 'monthly'; + frequency: number; + hasReminder: boolean; + reminderTime?: string; + customRepeatRule?: { + weekdays?: number[]; + dayOfMonth?: number[]; + }; + startTime?: number; + }, + userName: string +): Promise + +/** + * 取消特定目标的所有通知 + */ +static async cancelGoalNotifications(goalTitle: string): Promise +``` + +#### 支持的重复类型 + +1. **每日重复** + - 使用 `scheduleCalendarRepeatingNotification` 的 `daily` 类型 + - 每天在指定时间发送通知 + +2. **每周重复** + - 支持自定义星期几(如周一、三、五) + - 为每个选中的星期几创建单独的通知 + - 使用 `scheduleCalendarRepeatingNotification` 的 `weekly` 类型 + +3. **每月重复** + - 支持自定义日期(如每月1号和15号) + - 为每个选中的日期创建单独的通知 + - 使用 `scheduleCalendarRepeatingNotification` 的 `monthly` 类型 + +### 3. 目标创建页面集成 (app/(tabs)/goals.tsx) + +#### 创建目标后的通知设置 + +```typescript +// 创建目标成功后,设置定时推送 +try { + const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: goalData.title, + repeatType: goalData.repeatType, + frequency: goalData.frequency, + hasReminder: goalData.hasReminder, + reminderTime: goalData.reminderTime, + customRepeatRule: goalData.customRepeatRule, + startTime: goalData.startTime, + }, + userName + ); + + console.log(`目标"${goalData.title}"的定时推送已创建,通知ID:`, notificationIds); +} catch (notificationError) { + console.error('创建目标定时推送失败:', notificationError); + // 通知创建失败不影响目标创建的成功 +} +``` + +## 使用示例 + +### 1. 创建每日目标通知 + +```typescript +const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每日运动目标', + repeatType: 'daily', + frequency: 1, + hasReminder: true, + reminderTime: '09:00', + }, + '张三' +); +``` + +### 2. 创建每周目标通知 + +```typescript +const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每周运动目标', + repeatType: 'weekly', + frequency: 1, + hasReminder: true, + reminderTime: '10:00', + customRepeatRule: { + weekdays: [1, 3, 5], // 周一、三、五 + }, + }, + '张三' +); +``` + +### 3. 创建每月目标通知 + +```typescript +const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: '每月运动目标', + repeatType: 'monthly', + frequency: 1, + hasReminder: true, + reminderTime: '11:00', + customRepeatRule: { + dayOfMonth: [1, 15], // 每月1号和15号 + }, + }, + '张三' +); +``` + +### 4. 发送目标达成通知 + +```typescript +await GoalNotificationHelpers.sendGoalAchievementNotification('张三', '每日运动目标'); +``` + +### 5. 取消目标通知 + +```typescript +await GoalNotificationHelpers.cancelGoalNotifications('每日运动目标'); +``` + +## 测试功能 + +### 开发环境测试按钮 + +在开发环境下,目标页面会显示一个"测试通知"按钮,可以快速测试各种通知类型: + +- 每日目标通知测试 +- 每周目标通知测试 +- 目标达成通知测试 + +### 测试组件 + +创建了 `GoalNotificationTest` 组件,提供完整的测试界面: + +```typescript +import { GoalNotificationTest } from '@/components/GoalNotificationTest'; +``` + +## 注意事项 + +1. **权限要求**: 需要用户授予通知权限才能正常工作 +2. **平台限制**: Expo Notifications 的重复通知功能有一定限制,我们使用日历重复通知来绕过这些限制 +3. **时间处理**: 所有时间都基于用户设备的本地时间 +4. **错误处理**: 通知创建失败不会影响目标创建的成功 +5. **通知管理**: 每个目标的通知都有唯一的标识,可以单独取消 + +## 未来改进 + +1. **通知模板**: 支持更多样化的通知内容模板 +2. **智能提醒**: 根据用户行为调整提醒时间 +3. **批量管理**: 支持批量管理多个目标的通知 +4. **通知历史**: 记录和显示通知发送历史 +5. **自定义声音**: 支持自定义通知声音 diff --git a/docs/goal-notification-summary.md b/docs/goal-notification-summary.md new file mode 100644 index 0000000..b15ee90 --- /dev/null +++ b/docs/goal-notification-summary.md @@ -0,0 +1,121 @@ +# 目标通知功能实现总结 + +## 实现概述 + +已成功实现了根据用户创建目标时选择的频率和开始时间,自动创建本地定时推送通知的功能。 + +## 主要功能 + +### ✅ 已完成功能 + +1. **目标创建后自动设置通知** + - 在用户创建目标成功后,自动根据目标设置创建定时推送 + - 支持每日、每周、每月三种重复类型 + - 支持自定义提醒时间 + +2. **多种重复类型支持** + - **每日重复**: 每天在指定时间发送通知 + - **每周重复**: 支持自定义星期几(如周一、三、五) + - **每月重复**: 支持自定义日期(如每月1号和15号) + +3. **通知管理功能** + - 目标达成通知 + - 取消特定目标的通知 + - 通知点击处理 + +4. **开发测试功能** + - 开发环境下的测试按钮 + - 完整的测试组件 + +## 技术实现 + +### 核心文件 + +1. **services/notifications.ts** + - 扩展了通知服务,添加了 `scheduleCalendarRepeatingNotification` 方法 + - 支持日历重复通知(每日、每周、每月) + +2. **utils/notificationHelpers.ts** + - 添加了 `GoalNotificationHelpers` 类 + - 实现了 `scheduleGoalNotifications` 方法 + - 实现了 `cancelGoalNotifications` 方法 + +3. **app/(tabs)/goals.tsx** + - 在目标创建成功后调用通知设置 + - 添加了开发环境测试按钮 + +4. **components/GoalNotificationTest.tsx** + - 创建了完整的测试组件 + +### 关键代码 + +```typescript +// 创建目标后的通知设置 +const notificationIds = await GoalNotificationHelpers.scheduleGoalNotifications( + { + title: goalData.title, + repeatType: goalData.repeatType, + frequency: goalData.frequency, + hasReminder: goalData.hasReminder, + reminderTime: goalData.reminderTime, + customRepeatRule: goalData.customRepeatRule, + startTime: goalData.startTime, + }, + userName +); +``` + +## 使用流程 + +1. **用户创建目标** + - 设置目标标题、描述 + - 选择重复类型(每日/每周/每月) + - 设置频率 + - 开启提醒并设置提醒时间 + - 选择自定义重复规则(如星期几、日期) + +2. **系统自动创建通知** + - 目标创建成功后,系统自动调用通知设置 + - 根据重复类型创建相应的定时推送 + - 返回通知ID用于后续管理 + +3. **通知触发** + - 在指定时间自动发送通知 + - 用户点击通知可进行相应操作 + +## 测试验证 + +### 开发环境测试 + +在开发环境下,目标页面会显示"测试通知"按钮,可以测试: + +- 每日目标通知 +- 每周目标通知(自定义星期几) +- 目标达成通知 + +### 测试组件 + +创建了 `GoalNotificationTest` 组件,提供完整的测试界面,包括: + +- 各种通知类型的测试按钮 +- 测试结果显示 +- 错误处理 + +## 注意事项 + +1. **权限要求**: 需要用户授予通知权限 +2. **平台兼容**: 使用 Expo Notifications 的日历重复功能 +3. **错误处理**: 通知创建失败不影响目标创建 +4. **时间处理**: 基于用户设备本地时间 + +## 后续优化建议 + +1. **通知模板**: 支持更丰富的通知内容 +2. **智能提醒**: 根据用户行为调整提醒时间 +3. **批量管理**: 支持批量管理多个目标的通知 +4. **通知历史**: 记录和显示通知发送历史 +5. **自定义声音**: 支持自定义通知声音 + +## 总结 + +目标通知功能已完全实现,能够根据用户的目标设置自动创建本地定时推送,支持多种重复类型和自定义规则,并提供了完整的测试和错误处理机制。 diff --git a/services/notifications.ts b/services/notifications.ts index 5c29ecb..e4cdf3d 100644 --- a/services/notifications.ts +++ b/services/notifications.ts @@ -107,6 +107,10 @@ export class NotificationService { } else if (data?.type === 'mood_checkin') { // 处理心情打卡提醒 console.log('用户点击了心情打卡提醒'); + } else if (data?.type === 'goal_reminder') { + // 处理目标提醒通知 + console.log('用户点击了目标提醒通知', data); + // 这里可以添加导航到目标页面的逻辑 } } @@ -228,6 +232,78 @@ export class NotificationService { } } + /** + * 安排日历重复通知(支持每日、每周、每月) + */ + async scheduleCalendarRepeatingNotification( + notification: NotificationData, + options: { + type: 'daily' | 'weekly' | 'monthly'; + hour: number; + minute: number; + weekdays?: number[]; // 0-6,0为周日,仅用于weekly类型 + dayOfMonth?: number; // 1-31,仅用于monthly类型 + } + ): Promise { + 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; + } + } + /** * 取消特定通知 */ diff --git a/utils/notificationHelpers.ts b/utils/notificationHelpers.ts index 6fa67b4..749a743 100644 --- a/utils/notificationHelpers.ts +++ b/utils/notificationHelpers.ts @@ -1,4 +1,4 @@ -import { notificationService, NotificationData } from '../services/notifications'; +import { NotificationData, notificationService } from '../services/notifications'; /** * 运动相关的通知辅助函数 @@ -104,6 +104,164 @@ export class GoalNotificationHelpers { reminderDate ); } + + /** + * 根据目标设置创建定时推送 + * @param goalData 目标数据 + * @param userName 用户名 + * @returns 通知ID数组 + */ + static async scheduleGoalNotifications( + goalData: { + title: string; + repeatType: 'daily' | 'weekly' | 'monthly'; + frequency: number; + hasReminder: boolean; + reminderTime?: string; + customRepeatRule?: { + weekdays?: number[]; + dayOfMonth?: number[]; + }; + startTime?: number; + }, + userName: string + ): Promise { + const notificationIds: string[] = []; + + // 如果没有开启提醒,直接返回 + if (!goalData.hasReminder || !goalData.reminderTime) { + console.log('目标未开启提醒或未设置提醒时间'); + return notificationIds; + } + + try { + // 解析提醒时间 + const [hours, minutes] = goalData.reminderTime.split(':').map(Number); + + // 创建通知内容 + const notification: NotificationData = { + title: '目标提醒', + body: `${userName},该完成您的目标"${goalData.title}"了!`, + data: { + type: 'goal_reminder', + goalTitle: goalData.title, + repeatType: goalData.repeatType, + frequency: goalData.frequency + }, + sound: true, + priority: 'high', + }; + + // 根据重复类型创建不同的通知 + switch (goalData.repeatType) { + case 'daily': + // 每日重复 - 使用日历重复通知 + const dailyId = await notificationService.scheduleCalendarRepeatingNotification( + notification, + { + type: 'daily', + hour: hours, + minute: minutes, + } + ); + notificationIds.push(dailyId); + console.log(`已安排每日目标提醒,通知ID:${dailyId}`); + break; + + case 'weekly': + // 每周重复 - 为每个选中的星期几创建单独的通知 + if (goalData.customRepeatRule?.weekdays && goalData.customRepeatRule.weekdays.length > 0) { + for (const weekday of goalData.customRepeatRule.weekdays) { + const weeklyId = await notificationService.scheduleCalendarRepeatingNotification( + notification, + { + type: 'weekly', + hour: hours, + minute: minutes, + weekdays: [weekday], + } + ); + notificationIds.push(weeklyId); + console.log(`已安排每周目标提醒,星期${weekday},通知ID:${weeklyId}`); + } + } else { + // 默认每周重复 + const weeklyId = await notificationService.scheduleCalendarRepeatingNotification( + notification, + { + type: 'weekly', + hour: hours, + minute: minutes, + } + ); + notificationIds.push(weeklyId); + console.log(`已安排每周目标提醒,通知ID:${weeklyId}`); + } + break; + + case 'monthly': + // 每月重复 - 为每个选中的日期创建单独的通知 + if (goalData.customRepeatRule?.dayOfMonth && goalData.customRepeatRule.dayOfMonth.length > 0) { + for (const dayOfMonth of goalData.customRepeatRule.dayOfMonth) { + const monthlyId = await notificationService.scheduleCalendarRepeatingNotification( + notification, + { + type: 'monthly', + hour: hours, + minute: minutes, + dayOfMonth: dayOfMonth, + } + ); + notificationIds.push(monthlyId); + console.log(`已安排每月目标提醒,${dayOfMonth}号,通知ID:${monthlyId}`); + } + } else { + // 默认每月重复 + const monthlyId = await notificationService.scheduleCalendarRepeatingNotification( + notification, + { + type: 'monthly', + hour: hours, + minute: minutes, + dayOfMonth: 1, + } + ); + notificationIds.push(monthlyId); + console.log(`已安排每月目标提醒,通知ID:${monthlyId}`); + } + break; + } + + console.log(`目标"${goalData.title}"的定时推送已创建完成,共${notificationIds.length}个通知`); + return notificationIds; + + } catch (error) { + console.error('创建目标定时推送失败:', error); + throw error; + } + } + + + + /** + * 取消特定目标的所有通知 + */ + static async cancelGoalNotifications(goalTitle: string): Promise { + try { + const notifications = await notificationService.getAllScheduledNotifications(); + + for (const notification of notifications) { + if (notification.content.data?.type === 'goal_reminder' && + notification.content.data?.goalTitle === goalTitle) { + await notificationService.cancelNotification(notification.identifier); + console.log(`已取消目标"${goalTitle}"的通知:${notification.identifier}`); + } + } + } catch (error) { + console.error('取消目标通知失败:', error); + throw error; + } + } } /**