From 23aa15f76e54ab1552754955e85ada58f5eb2fb1 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Sun, 24 Aug 2025 09:46:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增后台任务管理器,支持任务的注册、执行和状态监控 - 实现自定义Hook,简化后台任务的使用和管理 - 添加示例任务,包括数据同步、健康数据更新和通知检查等 - 更新文档,详细描述后台任务系统的实现和使用方法 - 优化相关组件,确保用户体验和界面一致性 --- app.json | 13 +- app/(tabs)/statistics.tsx | 18 +- app/_layout.tsx | 16 +- app/mood/calendar.tsx | 42 +-- app/mood/edit.tsx | 113 ++----- app/task-list.tsx | 3 - components/GoalCard.tsx | 6 +- components/MoodIntensitySlider.tsx | 298 +++++++++++++++++++ docs/background-tasks-implementation.md | 284 ++++++++++++++++++ hooks/useBackgroundTasks.ts | 109 +++++++ ios/Podfile.lock | 18 ++ ios/digitalpilates.xcodeproj/project.pbxproj | 2 + ios/digitalpilates/Info.plist | 4 + package-lock.json | 33 ++ package.json | 6 +- services/backgroundTaskManager.ts | 263 ++++++++++++++++ services/backgroundTasks.ts | 180 +++++++++++ 17 files changed, 1289 insertions(+), 119 deletions(-) create mode 100644 components/MoodIntensitySlider.tsx create mode 100644 docs/background-tasks-implementation.md create mode 100644 hooks/useBackgroundTasks.ts create mode 100644 services/backgroundTaskManager.ts create mode 100644 services/backgroundTasks.ts diff --git a/app.json b/app.json index d6cb212..bb105a3 100644 --- a/app.json +++ b/app.json @@ -17,7 +17,9 @@ "NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。", "NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。", "NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。", - "UIBackgroundModes": ["remote-notification"] + "UIBackgroundModes": [ + "remote-notification" + ] } }, "android": { @@ -62,12 +64,15 @@ { "icon": "./assets/images/Sealife.jpeg", "color": "#ffffff", - "sounds": ["./assets/sounds/notification.wav"] + "sounds": [ + "./assets/sounds/notification.wav" + ] } - ] + ], + "expo-background-task" ], "experiments": { "typedRoutes": true } } -} \ No newline at end of file +} diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index b293d49..60e8e2c 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -13,6 +13,7 @@ import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; +import { useBackgroundTasks } from '@/hooks/useBackgroundTasks'; import { useColorScheme } from '@/hooks/useColorScheme'; import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords'; import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice'; @@ -131,7 +132,7 @@ export default function ExploreScreen() { // 营养数据状态 const [nutritionSummary, setNutritionSummary] = useState(null); const [isNutritionLoading, setIsNutritionLoading] = useState(false); - + const { registerTask } = useBackgroundTasks(); // 心情相关状态 const dispatch = useAppDispatch(); const [isMoodLoading, setIsMoodLoading] = useState(false); @@ -299,6 +300,21 @@ export default function ExploreScreen() { }, [selectedIndex]) ); + useEffect(() => { + // 注册任务 + registerTask({ + id: 'health-data-task', + name: 'health-data-task', + handler: async () => { + try { + await loadHealthData(); + } catch (error) { + console.error('健康数据任务执行失败:', error); + } + }, + }); + }, []); + // 日期点击时,加载对应日期数据 const onSelectDate = (index: number, date: Date) => { setSelectedIndex(index); diff --git a/app/_layout.tsx b/app/_layout.tsx index 2755dcf..b060fce 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -2,14 +2,14 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; -import 'react-native-reanimated'; -import 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import 'react-native-reanimated'; import PrivacyConsentModal from '@/components/PrivacyConsentModal'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { clearAiCoachSessionCache } from '@/services/aiCoachSession'; +import { backgroundTaskManager } from '@/services/backgroundTaskManager'; import { store } from '@/store'; import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice'; import React from 'react'; @@ -31,7 +31,18 @@ function Bootstrapper({ children }: { children: React.ReactNode }) { setUserDataLoaded(true); }; + const initializeBackgroundTasks = async () => { + try { + await backgroundTaskManager.initialize(); + + console.log('后台任务管理器初始化成功'); + } catch (error) { + console.error('后台任务管理器初始化失败:', error); + } + }; + loadUserData(); + initializeBackgroundTasks(); // 冷启动时清空 AI 教练会话缓存 clearAiCoachSessionCache(); }, [dispatch]); @@ -96,6 +107,7 @@ export default function RootLayout() { + diff --git a/app/mood/calendar.tsx b/app/mood/calendar.tsx index d88a838..93a2025 100644 --- a/app/mood/calendar.tsx +++ b/app/mood/calendar.tsx @@ -8,7 +8,7 @@ import { selectLatestMoodRecordByDate } from '@/store/moodSlice'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useFocusEffect, useLocalSearchParams } from 'expo-router'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Dimensions, SafeAreaView, @@ -56,6 +56,14 @@ export default function MoodCalendarScreen() { const dispatch = useAppDispatch(); const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData(); + // 使用 useRef 来存储函数引用,避免依赖循环 + const fetchMoodRecordsRef = useRef(fetchMoodRecords); + const fetchMoodHistoryRecordsRef = useRef(fetchMoodHistoryRecords); + + // 更新 ref 值 + fetchMoodRecordsRef.current = fetchMoodRecords; + fetchMoodHistoryRecordsRef.current = fetchMoodHistoryRecords; + const { selectedDate } = params; const initialDate = selectedDate ? dayjs(selectedDate as string).toDate() : new Date(); @@ -66,9 +74,9 @@ export default function MoodCalendarScreen() { const moodRecords = useAppSelector(state => state.mood.moodRecords); // 获取选中日期的数据 - const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null; const selectedDateMood = useAppSelector(state => { - if (!selectedDateString) return null; + if (!selectedDay) return null; + const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD'); return selectLatestMoodRecordByDate(selectedDateString)(state); }); @@ -80,29 +88,24 @@ export default function MoodCalendarScreen() { const { calendar, today, month, year } = generateCalendarData(currentMonth); // 加载整个月份的心情数据 - const loadMonthMoodData = async (targetMonth: Date) => { + const loadMonthMoodData = useCallback(async (targetMonth: Date) => { try { const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD'); const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD'); - - const historyData = await fetchMoodHistoryRecords({ startDate, endDate }); - - // 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中 - // 不需要额外的处理 + await fetchMoodHistoryRecordsRef.current({ startDate, endDate }); } catch (error) { console.error('加载月份心情数据失败:', error); } - }; + }, []); // 加载选中日期的心情记录 - const loadDailyMoodCheckins = async (dateString: string) => { + const loadDailyMoodCheckins = useCallback(async (dateString: string) => { try { - await fetchMoodRecords(dateString); - // 不需要手动设置 selectedDateMood,因为它现在从 Redux store 中获取 + await fetchMoodRecordsRef.current(dateString); } catch (error) { console.error('加载心情记录失败:', error); } - }; + }, []); // 初始化选中日期 useEffect(() => { @@ -112,15 +115,16 @@ export default function MoodCalendarScreen() { setSelectedDay(date.date()); const dateString = date.format('YYYY-MM-DD'); loadDailyMoodCheckins(dateString); + loadMonthMoodData(date.toDate()); } else { const today = new Date(); setCurrentMonth(today); setSelectedDay(today.getDate()); const dateString = dayjs().format('YYYY-MM-DD'); loadDailyMoodCheckins(dateString); + loadMonthMoodData(today); } - loadMonthMoodData(currentMonth); - }, [selectedDate]); + }, [selectedDate, loadDailyMoodCheckins, loadMonthMoodData]); // 监听页面焦点变化,当从编辑页面返回时刷新数据 useFocusEffect( @@ -129,14 +133,14 @@ export default function MoodCalendarScreen() { const refreshData = async () => { if (selectedDay) { const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD'); - await fetchMoodRecords(selectedDateString); + await fetchMoodRecordsRef.current(selectedDateString); } const startDate = dayjs(currentMonth).startOf('month').format('YYYY-MM-DD'); const endDate = dayjs(currentMonth).endOf('month').format('YYYY-MM-DD'); - await fetchMoodHistoryRecords({ startDate, endDate }); + await fetchMoodHistoryRecordsRef.current({ startDate, endDate }); }; refreshData(); - }, [currentMonth, selectedDay, fetchMoodRecords, fetchMoodHistoryRecords]) + }, [currentMonth, selectedDay]) ); // 月份切换函数 diff --git a/app/mood/edit.tsx b/app/mood/edit.tsx index 60af7b8..ae6cc92 100644 --- a/app/mood/edit.tsx +++ b/app/mood/edit.tsx @@ -1,28 +1,29 @@ +import MoodIntensitySlider from '@/components/MoodIntensitySlider'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { getMoodOptions, MoodType } from '@/services/moodCheckins'; import { - createMoodRecord, - deleteMoodRecord, - fetchDailyMoodCheckins, - selectMoodRecordsByDate, - updateMoodRecord + createMoodRecord, + deleteMoodRecord, + fetchDailyMoodCheckins, + selectMoodRecordsByDate, + updateMoodRecord } from '@/store/moodSlice'; import dayjs from 'dayjs'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { - Alert, - SafeAreaView, - ScrollView, - StyleSheet, - Text, - TextInput, - TouchableOpacity, - View, + Alert, + SafeAreaView, + ScrollView, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, } from 'react-native'; export default function MoodEditScreen() { @@ -133,28 +134,8 @@ export default function MoodEditScreen() { ); }; - const renderIntensitySlider = () => { - return ( - - 心情强度: {intensity} - - {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((level) => ( - = level && styles.intensityDotActive - ]} - onPress={() => setIntensity(level)} - /> - ))} - - - 轻微 - 强烈 - - - ); + const handleIntensityChange = (value: number) => { + setIntensity(value); }; // 使用统一的渐变背景色 @@ -210,12 +191,17 @@ export default function MoodEditScreen() { {/* 心情强度选择 */} - {selectedMood && ( - - 心情强度 - {renderIntensitySlider()} - - )} + + 心情强度 + + + {/* 心情描述 */} {selectedMood && ( @@ -383,50 +369,7 @@ const styles = StyleSheet.create({ shadowRadius: 12, elevation: 6, }, - intensityContainer: { - alignItems: 'center', - }, - intensityLabel: { - fontSize: 18, - fontWeight: '700', - color: '#192126', - marginBottom: 16, - }, - intensitySlider: { - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - marginBottom: 12, - paddingHorizontal: 10, - }, - intensityDot: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: 'rgba(122,90,248,0.1)', - borderWidth: 2, - borderColor: 'rgba(122,90,248,0.2)', - }, - intensityDotActive: { - backgroundColor: '#7a5af8', - borderColor: '#7a5af8', - shadowColor: '#7a5af8', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.2, - shadowRadius: 4, - elevation: 2, - }, - intensityLabels: { - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - paddingHorizontal: 10, - }, - intensityLabelText: { - fontSize: 13, - color: '#5d6676', - fontWeight: '500', - }, + descriptionSection: { backgroundColor: 'rgba(255,255,255,0.95)', margin: 16, diff --git a/app/task-list.tsx b/app/task-list.tsx index 7ba8d89..74e6db9 100644 --- a/app/task-list.tsx +++ b/app/task-list.tsx @@ -256,18 +256,15 @@ const styles = StyleSheet.create({ // 日期选择器样式 dateSelector: { paddingHorizontal: 20, - paddingVertical: 16, }, taskListContainer: { flex: 1, - backgroundColor: 'rgba(255, 255, 255, 0.95)', borderTopLeftRadius: 24, borderTopRightRadius: 24, overflow: 'hidden', }, taskList: { paddingHorizontal: 20, - paddingTop: 20, paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20, }, emptyState: { diff --git a/components/GoalCard.tsx b/components/GoalCard.tsx index b81cc2b..07d6b70 100644 --- a/components/GoalCard.tsx +++ b/components/GoalCard.tsx @@ -216,8 +216,8 @@ const styles = StyleSheet.create({ elevation: 2, }, goalIcon: { - width: 40, - height: 40, + width: 32, + height: 32, borderRadius: 20, backgroundColor: '#F3F4F6', alignItems: 'center', @@ -243,7 +243,7 @@ const styles = StyleSheet.create({ marginRight: 12, }, goalTitle: { - fontSize: 16, + fontSize: 14, fontWeight: '600', color: '#1F2937', marginBottom: 8, diff --git a/components/MoodIntensitySlider.tsx b/components/MoodIntensitySlider.tsx new file mode 100644 index 0000000..f32bc1b --- /dev/null +++ b/components/MoodIntensitySlider.tsx @@ -0,0 +1,298 @@ +import * as Haptics from 'expo-haptics'; +import { LinearGradient } from 'expo-linear-gradient'; +import React from 'react'; +import { + StyleSheet, + Text, + View, +} from 'react-native'; +import { + PanGestureHandler, + PanGestureHandlerGestureEvent, +} from 'react-native-gesture-handler'; +import Animated, { + interpolate, + interpolateColor, + runOnJS, + useAnimatedGestureHandler, + useAnimatedStyle, + useSharedValue, + withSpring, +} from 'react-native-reanimated'; + +interface MoodIntensitySliderProps { + value: number; + onValueChange: (value: number) => void; + min?: number; + max?: number; + step?: number; + width?: number; + height?: number; +} + +export default function MoodIntensitySlider({ + value, + onValueChange, + min = 1, + max = 10, + step = 1, + width = 320, + height = 16, // 更粗的进度条 +}: MoodIntensitySliderProps) { + const translateX = useSharedValue(0); + const sliderWidth = width - 40; // 减去thumb的宽度 + const thumbSize = 36; // 更大的thumb + + // 计算初始位置 + React.useEffect(() => { + const initialPosition = ((value - min) / (max - min)) * sliderWidth; + translateX.value = withSpring(initialPosition); + }, [value, min, max, sliderWidth, translateX]); + + const triggerHaptics = () => { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + }; + + const gestureHandler = useAnimatedGestureHandler< + PanGestureHandlerGestureEvent, + { startX: number; lastValue: number } + >({ + onStart: (_, context) => { + context.startX = translateX.value; + context.lastValue = value; + runOnJS(triggerHaptics)(); + }, + onActive: (event, context) => { + const newX = context.startX + event.translationX; + const clampedX = Math.max(0, Math.min(sliderWidth, newX)); + translateX.value = clampedX; + + // 计算当前值 + const currentValue = Math.round((clampedX / sliderWidth) * (max - min) + min); + + // 当值改变时触发震动和回调 + if (currentValue !== context.lastValue) { + context.lastValue = currentValue; + runOnJS(triggerHaptics)(); + runOnJS(onValueChange)(currentValue); + } + }, + onEnd: () => { + // 计算最终值并吸附到最近的步长 + const currentValue = Math.round((translateX.value / sliderWidth) * (max - min) + min); + const snapPosition = ((currentValue - min) / (max - min)) * sliderWidth; + + translateX.value = withSpring(snapPosition); + runOnJS(triggerHaptics)(); + runOnJS(onValueChange)(currentValue); + }, + }); + + const thumbStyle = useAnimatedStyle(() => { + const scale = interpolate( + translateX.value, + [0, sliderWidth], + [1, 1.05] + ); + + return { + transform: [ + { translateX: translateX.value }, + { scale: withSpring(scale) } + ], + }; + }); + + const progressStyle = useAnimatedStyle(() => { + const progressWidth = translateX.value + thumbSize / 2; + return { + width: progressWidth, + }; + }); + + // 动态颜色配置 - 根据进度变化颜色 + const getProgressColors = (progress: number) => { + if (progress <= 0.25) { + return ['#22c55e', '#84cc16'] as const; // 绿色到浅绿色 + } else if (progress <= 0.5) { + return ['#84cc16', '#eab308'] as const; // 浅绿色到黄色 + } else if (progress <= 0.75) { + return ['#eab308', '#f97316'] as const; // 黄色到橙色 + } else { + return ['#f97316', '#ef4444'] as const; // 橙色到红色 + } + }; + + const progressColorsStyle = useAnimatedStyle(() => { + const progress = translateX.value / sliderWidth; + return { + backgroundColor: interpolateColor( + progress, + [0, 0.25, 0.5, 0.75, 1], + ['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444'] + ), + }; + }); + + return ( + + + {/* 背景轨道 - 更粗的灰色轨道 */} + + + + + {/* 进度条 - 动态颜色 */} + + + + + {/* 可拖拽的thumb */} + + + {/* */} + + + + + + {/* 标签 */} + + 轻微 + 强烈 + + + {/* 刻度 */} + + {Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => ( + + + + ))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + paddingVertical: 16, + }, + sliderContainer: { + height: 50, + justifyContent: 'center', + position: 'relative', + paddingHorizontal: 20, + }, + track: { + position: 'absolute', + left: 20, + right: 20, + borderRadius: 8, + overflow: 'hidden', + }, + trackGradient: { + flex: 1, + borderRadius: 8, + }, + progress: { + position: 'absolute', + left: 20, + borderRadius: 8, + overflow: 'hidden', + }, + progressGradient: { + flex: 1, + borderRadius: 8, + }, + thumb: { + position: 'absolute', + left: 22, + elevation: 6, + overflow: 'hidden', + }, + thumbGradient: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + thumbInner: { + width: 16, + height: 32, + borderRadius: 8, + backgroundColor: '#ffffff', + shadowColor: '#ffffff', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2, + elevation: 2, + }, + valueContainer: { + marginTop: 20, + marginBottom: 12, + paddingHorizontal: 16, + paddingVertical: 10, + backgroundColor: '#7a5af8', + borderRadius: 16, + shadowColor: '#7a5af8', + shadowOffset: { width: 0, height: 3 }, + shadowOpacity: 0.3, + shadowRadius: 6, + elevation: 6, + }, + valueText: { + fontSize: 20, + fontWeight: '800', + color: '#ffffff', + textAlign: 'center', + minWidth: 28, + }, + labelsContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 8, + paddingHorizontal: 20, + }, + labelText: { + fontSize: 14, + color: '#5d6676', + fontWeight: '600', + }, + scaleContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 16, + paddingHorizontal: 20, + }, + scaleItem: { + alignItems: 'center', + flex: 1, + }, + scaleMark: { + width: 2, + height: 10, + backgroundColor: '#e5e7eb', + borderRadius: 1, + }, + scaleMarkActive: { + backgroundColor: '#7a5af8', + width: 3, + height: 12, + }, +}); diff --git a/docs/background-tasks-implementation.md b/docs/background-tasks-implementation.md new file mode 100644 index 0000000..1b4736d --- /dev/null +++ b/docs/background-tasks-implementation.md @@ -0,0 +1,284 @@ +# 后台任务系统实现文档 + +## 概述 + +本项目已成功集成iOS后台任务支持,使用Expo官方的 `expo-task-manager` 和 `expo-background-task` 库。该系统提供了完整的后台任务管理功能,支持任务注册、执行、状态监控等。 + +## 技术栈 + +- **expo-task-manager**: Expo官方后台任务管理库 +- **expo-background-task**: Expo官方后台任务库 +- **React Native**: 跨平台移动应用框架 +- **TypeScript**: 类型安全的JavaScript超集 + +## 文件结构 + +``` +services/ +├── backgroundTaskManager.ts # 后台任务管理器核心逻辑 +├── backgroundTasks.ts # 示例任务定义 +hooks/ +├── useBackgroundTasks.ts # 后台任务自定义Hook +components/ +├── BackgroundTaskTest.tsx # 后台任务测试组件 +``` + +## 核心功能 + +### 1. 后台任务管理器 (services/backgroundTaskManager.ts) + +#### 主要特性 +- **单例模式**: 确保全局只有一个任务管理器实例 +- **任务注册**: 支持注册自定义后台任务 +- **状态管理**: 完整的任务状态跟踪和持久化 +- **错误处理**: 完善的错误处理和日志记录 +- **后台获取**: 自动注册后台获取任务 + +#### 核心方法 + +```typescript +// 初始化后台任务管理器 +await backgroundTaskManager.initialize(); + +// 注册自定义任务 +await backgroundTaskManager.registerTask({ + id: 'my-task', + name: '我的任务', + handler: async (data) => { + // 您的任务逻辑 + console.log('执行任务:', data); + }, + options: { + minimumInterval: 300, // 5分钟最小间隔 + stopOnTerminate: false, + startOnBoot: true, + } +}); + +// 手动执行任务 +await backgroundTaskManager.executeTask('my-task', { customData: 'value' }); + +// 执行所有任务 +const results = await backgroundTaskManager.executeAllTasks(); + +// 获取任务状态 +const status = backgroundTaskManager.getTaskStatus('my-task'); +``` + +### 2. 自定义Hook (hooks/useBackgroundTasks.ts) + +#### 主要特性 +- **状态管理**: 管理任务状态和初始化状态 +- **自动初始化**: 组件挂载时自动初始化任务管理器 +- **便捷接口**: 提供简化的任务操作方法 +- **实时更新**: 任务状态实时更新 + +#### 使用示例 + +```typescript +const { + isInitialized, + taskStatuses, + registeredTasks, + registerTask, + executeTask, + executeAllTasks, +} = useBackgroundTasks(); + +// 注册任务 +await registerTask({ + id: 'data-sync', + name: '数据同步', + handler: async () => { + // 数据同步逻辑 + } +}); + +// 执行任务 +await executeTask('data-sync'); +``` + +### 3. 示例任务 (services/backgroundTasks.ts) + +#### 预定义任务类型 +- **数据同步任务**: 同步用户数据、运动记录等 +- **健康数据更新任务**: 更新步数、心率等健康数据 +- **通知检查任务**: 检查是否需要发送通知 +- **缓存清理任务**: 清理过期缓存文件 +- **用户行为分析任务**: 分析用户使用模式 + +#### 创建自定义任务 + +```typescript +import { createCustomTask } from '@/services/backgroundTasks'; + +const myTask = createCustomTask( + 'my-custom-task', + '我的自定义任务', + async (data) => { + // 您的任务逻辑 + console.log('执行自定义任务:', data); + }, + { + minimumInterval: 120, // 2分钟 + stopOnTerminate: false, + startOnBoot: true, + } +); +``` + +## 使用指南 + +### 1. 基本使用 + +```typescript +import { useBackgroundTasks } from '@/hooks/useBackgroundTasks'; +import { createCustomTask } from '@/services/backgroundTasks'; + +const MyComponent = () => { + const { registerTask, executeTask } = useBackgroundTasks(); + + const handleCreateTask = async () => { + const task = createCustomTask( + 'my-task', + '我的任务', + async (data) => { + // 实现您的后台任务逻辑 + console.log('后台任务执行中...'); + + // 例如:数据同步 + await syncUserData(); + + // 例如:健康数据更新 + await updateHealthData(); + + // 例如:发送通知 + await checkAndSendNotifications(); + } + ); + + await registerTask(task); + }; + + const handleExecuteTask = async () => { + await executeTask('my-task', { customData: 'value' }); + }; + + return ( + +