feat: 集成后台任务管理功能及相关组件
- 新增后台任务管理器,支持任务的注册、执行和状态监控 - 实现自定义Hook,简化后台任务的使用和管理 - 添加示例任务,包括数据同步、健康数据更新和通知检查等 - 更新文档,详细描述后台任务系统的实现和使用方法 - 优化相关组件,确保用户体验和界面一致性
This commit is contained in:
11
app.json
11
app.json
@@ -17,7 +17,9 @@
|
|||||||
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
|
||||||
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
|
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
|
||||||
"UIBackgroundModes": ["remote-notification"]
|
"UIBackgroundModes": [
|
||||||
|
"remote-notification"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
@@ -62,9 +64,12 @@
|
|||||||
{
|
{
|
||||||
"icon": "./assets/images/Sealife.jpeg",
|
"icon": "./assets/images/Sealife.jpeg",
|
||||||
"color": "#ffffff",
|
"color": "#ffffff",
|
||||||
"sounds": ["./assets/sounds/notification.wav"]
|
"sounds": [
|
||||||
|
"./assets/sounds/notification.wav"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-background-task"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||||
|
import { useBackgroundTasks } from '@/hooks/useBackgroundTasks';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
|
import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords';
|
||||||
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
@@ -131,7 +132,7 @@ export default function ExploreScreen() {
|
|||||||
// 营养数据状态
|
// 营养数据状态
|
||||||
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
|
const [nutritionSummary, setNutritionSummary] = useState<NutritionSummary | null>(null);
|
||||||
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
|
const [isNutritionLoading, setIsNutritionLoading] = useState(false);
|
||||||
|
const { registerTask } = useBackgroundTasks();
|
||||||
// 心情相关状态
|
// 心情相关状态
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [isMoodLoading, setIsMoodLoading] = useState(false);
|
const [isMoodLoading, setIsMoodLoading] = useState(false);
|
||||||
@@ -299,6 +300,21 @@ export default function ExploreScreen() {
|
|||||||
}, [selectedIndex])
|
}, [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) => {
|
const onSelectDate = (index: number, date: Date) => {
|
||||||
setSelectedIndex(index);
|
setSelectedIndex(index);
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
|||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import 'react-native-reanimated';
|
|
||||||
import 'react-native-gesture-handler';
|
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
import 'react-native-reanimated';
|
||||||
|
|
||||||
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
|
||||||
|
import { backgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||||
import { store } from '@/store';
|
import { store } from '@/store';
|
||||||
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
import { rehydrateUser, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -31,7 +31,18 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
setUserDataLoaded(true);
|
setUserDataLoaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initializeBackgroundTasks = async () => {
|
||||||
|
try {
|
||||||
|
await backgroundTaskManager.initialize();
|
||||||
|
|
||||||
|
console.log('后台任务管理器初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('后台任务管理器初始化失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
loadUserData();
|
loadUserData();
|
||||||
|
initializeBackgroundTasks();
|
||||||
// 冷启动时清空 AI 教练会话缓存
|
// 冷启动时清空 AI 教练会话缓存
|
||||||
clearAiCoachSessionCache();
|
clearAiCoachSessionCache();
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@@ -96,6 +107,7 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||||
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
<Stack.Screen name="article/[id]" options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
<Stack.Screen name="nutrition/records" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="background-tasks-test" options={{ headerShown: true, title: '后台任务测试' }} />
|
||||||
<Stack.Screen name="+not-found" />
|
<Stack.Screen name="+not-found" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style="dark" />
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
|
import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
@@ -56,6 +56,14 @@ export default function MoodCalendarScreen() {
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
|
const { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
|
||||||
|
|
||||||
|
// 使用 useRef 来存储函数引用,避免依赖循环
|
||||||
|
const fetchMoodRecordsRef = useRef(fetchMoodRecords);
|
||||||
|
const fetchMoodHistoryRecordsRef = useRef(fetchMoodHistoryRecords);
|
||||||
|
|
||||||
|
// 更新 ref 值
|
||||||
|
fetchMoodRecordsRef.current = fetchMoodRecords;
|
||||||
|
fetchMoodHistoryRecordsRef.current = fetchMoodHistoryRecords;
|
||||||
|
|
||||||
const { selectedDate } = params;
|
const { selectedDate } = params;
|
||||||
const initialDate = selectedDate ? dayjs(selectedDate as string).toDate() : new Date();
|
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 moodRecords = useAppSelector(state => state.mood.moodRecords);
|
||||||
|
|
||||||
// 获取选中日期的数据
|
// 获取选中日期的数据
|
||||||
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : null;
|
|
||||||
const selectedDateMood = useAppSelector(state => {
|
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);
|
return selectLatestMoodRecordByDate(selectedDateString)(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,29 +88,24 @@ export default function MoodCalendarScreen() {
|
|||||||
const { calendar, today, month, year } = generateCalendarData(currentMonth);
|
const { calendar, today, month, year } = generateCalendarData(currentMonth);
|
||||||
|
|
||||||
// 加载整个月份的心情数据
|
// 加载整个月份的心情数据
|
||||||
const loadMonthMoodData = async (targetMonth: Date) => {
|
const loadMonthMoodData = useCallback(async (targetMonth: Date) => {
|
||||||
try {
|
try {
|
||||||
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
const startDate = dayjs(targetMonth).startOf('month').format('YYYY-MM-DD');
|
||||||
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
const endDate = dayjs(targetMonth).endOf('month').format('YYYY-MM-DD');
|
||||||
|
await fetchMoodHistoryRecordsRef.current({ startDate, endDate });
|
||||||
const historyData = await fetchMoodHistoryRecords({ startDate, endDate });
|
|
||||||
|
|
||||||
// 历史记录已经通过 fetchMoodHistoryRecords 自动存储到 Redux store 中
|
|
||||||
// 不需要额外的处理
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载月份心情数据失败:', error);
|
console.error('加载月份心情数据失败:', error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 加载选中日期的心情记录
|
// 加载选中日期的心情记录
|
||||||
const loadDailyMoodCheckins = async (dateString: string) => {
|
const loadDailyMoodCheckins = useCallback(async (dateString: string) => {
|
||||||
try {
|
try {
|
||||||
await fetchMoodRecords(dateString);
|
await fetchMoodRecordsRef.current(dateString);
|
||||||
// 不需要手动设置 selectedDateMood,因为它现在从 Redux store 中获取
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载心情记录失败:', error);
|
console.error('加载心情记录失败:', error);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 初始化选中日期
|
// 初始化选中日期
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -112,15 +115,16 @@ export default function MoodCalendarScreen() {
|
|||||||
setSelectedDay(date.date());
|
setSelectedDay(date.date());
|
||||||
const dateString = date.format('YYYY-MM-DD');
|
const dateString = date.format('YYYY-MM-DD');
|
||||||
loadDailyMoodCheckins(dateString);
|
loadDailyMoodCheckins(dateString);
|
||||||
|
loadMonthMoodData(date.toDate());
|
||||||
} else {
|
} else {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
setCurrentMonth(today);
|
setCurrentMonth(today);
|
||||||
setSelectedDay(today.getDate());
|
setSelectedDay(today.getDate());
|
||||||
const dateString = dayjs().format('YYYY-MM-DD');
|
const dateString = dayjs().format('YYYY-MM-DD');
|
||||||
loadDailyMoodCheckins(dateString);
|
loadDailyMoodCheckins(dateString);
|
||||||
|
loadMonthMoodData(today);
|
||||||
}
|
}
|
||||||
loadMonthMoodData(currentMonth);
|
}, [selectedDate, loadDailyMoodCheckins, loadMonthMoodData]);
|
||||||
}, [selectedDate]);
|
|
||||||
|
|
||||||
// 监听页面焦点变化,当从编辑页面返回时刷新数据
|
// 监听页面焦点变化,当从编辑页面返回时刷新数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -129,14 +133,14 @@ export default function MoodCalendarScreen() {
|
|||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
if (selectedDay) {
|
if (selectedDay) {
|
||||||
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
|
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 startDate = dayjs(currentMonth).startOf('month').format('YYYY-MM-DD');
|
||||||
const endDate = dayjs(currentMonth).endOf('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();
|
refreshData();
|
||||||
}, [currentMonth, selectedDay, fetchMoodRecords, fetchMoodHistoryRecords])
|
}, [currentMonth, selectedDay])
|
||||||
);
|
);
|
||||||
|
|
||||||
// 月份切换函数
|
// 月份切换函数
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
|
import MoodIntensitySlider from '@/components/MoodIntensitySlider';
|
||||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { getMoodOptions, MoodType } from '@/services/moodCheckins';
|
import { getMoodOptions, MoodType } from '@/services/moodCheckins';
|
||||||
import {
|
import {
|
||||||
createMoodRecord,
|
createMoodRecord,
|
||||||
deleteMoodRecord,
|
deleteMoodRecord,
|
||||||
fetchDailyMoodCheckins,
|
fetchDailyMoodCheckins,
|
||||||
selectMoodRecordsByDate,
|
selectMoodRecordsByDate,
|
||||||
updateMoodRecord
|
updateMoodRecord
|
||||||
} from '@/store/moodSlice';
|
} from '@/store/moodSlice';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { router, useLocalSearchParams } from 'expo-router';
|
import { router, useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
export default function MoodEditScreen() {
|
export default function MoodEditScreen() {
|
||||||
@@ -133,28 +134,8 @@ export default function MoodEditScreen() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIntensitySlider = () => {
|
const handleIntensityChange = (value: number) => {
|
||||||
return (
|
setIntensity(value);
|
||||||
<View style={styles.intensityContainer}>
|
|
||||||
<Text style={styles.intensityLabel}>心情强度: {intensity}</Text>
|
|
||||||
<View style={styles.intensitySlider}>
|
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((level) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={level}
|
|
||||||
style={[
|
|
||||||
styles.intensityDot,
|
|
||||||
intensity >= level && styles.intensityDotActive
|
|
||||||
]}
|
|
||||||
onPress={() => setIntensity(level)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
<View style={styles.intensityLabels}>
|
|
||||||
<Text style={styles.intensityLabelText}>轻微</Text>
|
|
||||||
<Text style={styles.intensityLabelText}>强烈</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用统一的渐变背景色
|
// 使用统一的渐变背景色
|
||||||
@@ -210,12 +191,17 @@ export default function MoodEditScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 心情强度选择 */}
|
{/* 心情强度选择 */}
|
||||||
{selectedMood && (
|
<View style={styles.intensitySection}>
|
||||||
<View style={styles.intensitySection}>
|
<Text style={styles.sectionTitle}>心情强度</Text>
|
||||||
<Text style={styles.sectionTitle}>心情强度</Text>
|
<MoodIntensitySlider
|
||||||
{renderIntensitySlider()}
|
value={intensity}
|
||||||
</View>
|
onValueChange={handleIntensityChange}
|
||||||
)}
|
min={1}
|
||||||
|
max={10}
|
||||||
|
width={320}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
|
||||||
{/* 心情描述 */}
|
{/* 心情描述 */}
|
||||||
{selectedMood && (
|
{selectedMood && (
|
||||||
@@ -383,50 +369,7 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 12,
|
shadowRadius: 12,
|
||||||
elevation: 6,
|
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: {
|
descriptionSection: {
|
||||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
|
|||||||
@@ -256,18 +256,15 @@ const styles = StyleSheet.create({
|
|||||||
// 日期选择器样式
|
// 日期选择器样式
|
||||||
dateSelector: {
|
dateSelector: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 16,
|
|
||||||
},
|
},
|
||||||
taskListContainer: {
|
taskListContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
taskList: {
|
taskList: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingTop: 20,
|
|
||||||
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
|
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ const styles = StyleSheet.create({
|
|||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
goalIcon: {
|
goalIcon: {
|
||||||
width: 40,
|
width: 32,
|
||||||
height: 40,
|
height: 32,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: '#F3F4F6',
|
backgroundColor: '#F3F4F6',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -243,7 +243,7 @@ const styles = StyleSheet.create({
|
|||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
goalTitle: {
|
goalTitle: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#1F2937',
|
color: '#1F2937',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
|
|||||||
298
components/MoodIntensitySlider.tsx
Normal file
298
components/MoodIntensitySlider.tsx
Normal file
@@ -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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={[styles.sliderContainer, { width: width }]}>
|
||||||
|
{/* 背景轨道 - 更粗的灰色轨道 */}
|
||||||
|
<View style={[styles.track, { height }]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#f3f4f6', '#e5e7eb']}
|
||||||
|
style={[styles.trackGradient, { height }]}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 1, y: 0 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 进度条 - 动态颜色 */}
|
||||||
|
<Animated.View style={[styles.progress, { height }, progressStyle, progressColorsStyle]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={getProgressColors(translateX.value / sliderWidth)}
|
||||||
|
style={[styles.progressGradient, { height }]}
|
||||||
|
start={{ x: 1, y: 0 }}
|
||||||
|
end={{ x: 0, y: 0 }}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
{/* 可拖拽的thumb */}
|
||||||
|
<PanGestureHandler onGestureEvent={gestureHandler}>
|
||||||
|
<Animated.View style={[styles.thumb, { width: thumbSize, height: thumbSize }, thumbStyle]}>
|
||||||
|
{/* <LinearGradient
|
||||||
|
colors={['#ffffff', '#f8fafc']}
|
||||||
|
style={styles.thumbGradient}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
/> */}
|
||||||
|
<View style={styles.thumbInner} />
|
||||||
|
</Animated.View>
|
||||||
|
</PanGestureHandler>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 标签 */}
|
||||||
|
<View style={[styles.labelsContainer, { width: width }]}>
|
||||||
|
<Text style={styles.labelText}>轻微</Text>
|
||||||
|
<Text style={styles.labelText}>强烈</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 刻度 */}
|
||||||
|
<View style={[styles.scaleContainer, { width: width }]}>
|
||||||
|
{Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => (
|
||||||
|
<View key={num} style={styles.scaleItem}>
|
||||||
|
<View style={[styles.scaleMark, value === num && styles.scaleMarkActive]} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
284
docs/background-tasks-implementation.md
Normal file
284
docs/background-tasks-implementation.md
Normal file
@@ -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 (
|
||||||
|
<View>
|
||||||
|
<Button title="创建任务" onPress={handleCreateTask} />
|
||||||
|
<Button title="执行任务" onPress={handleExecuteTask} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 任务状态监控
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { taskStatuses, getTaskStatus } = useBackgroundTasks();
|
||||||
|
|
||||||
|
// 获取特定任务状态
|
||||||
|
const taskStatus = getTaskStatus('my-task');
|
||||||
|
console.log('任务状态:', {
|
||||||
|
isRegistered: taskStatus?.isRegistered,
|
||||||
|
executionCount: taskStatus?.executionCount,
|
||||||
|
lastExecution: taskStatus?.lastExecution,
|
||||||
|
lastError: taskStatus?.lastError,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 批量操作
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { executeAllTasks, cleanupTaskStatuses } = useBackgroundTasks();
|
||||||
|
|
||||||
|
// 执行所有任务
|
||||||
|
const results = await executeAllTasks();
|
||||||
|
console.log('执行结果:', results);
|
||||||
|
|
||||||
|
// 清理过期任务状态
|
||||||
|
await cleanupTaskStatuses();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
### iOS配置
|
||||||
|
|
||||||
|
在 `app.json` 中已配置后台模式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"expo": {
|
||||||
|
"ios": {
|
||||||
|
"infoPlist": {
|
||||||
|
"UIBackgroundModes": ["remote-notification"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后台获取配置
|
||||||
|
|
||||||
|
系统自动配置后台获取任务,支持:
|
||||||
|
- 最小间隔时间设置
|
||||||
|
- 应用终止时继续运行
|
||||||
|
- 设备重启时自动启动
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 任务设计原则
|
||||||
|
- **轻量级**: 后台任务应该快速执行,避免长时间运行
|
||||||
|
- **幂等性**: 任务应该可以重复执行而不产生副作用
|
||||||
|
- **错误处理**: 完善的错误处理和重试机制
|
||||||
|
- **资源管理**: 合理管理内存和网络资源
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
- **最小间隔**: 根据任务重要性设置合适的最小间隔
|
||||||
|
- **批量处理**: 将多个小任务合并为一个大任务
|
||||||
|
- **缓存策略**: 合理使用缓存减少重复计算
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
- **静默执行**: 后台任务应该静默执行,不打扰用户
|
||||||
|
- **状态反馈**: 通过UI显示任务执行状态
|
||||||
|
- **错误提示**: 在任务失败时提供友好的错误提示
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
使用 `BackgroundTaskTest` 组件进行功能测试:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { BackgroundTaskTest } from '@/components/BackgroundTaskTest';
|
||||||
|
|
||||||
|
// 在您的页面中使用
|
||||||
|
<BackgroundTaskTest />
|
||||||
|
```
|
||||||
|
|
||||||
|
该组件提供:
|
||||||
|
- 任务注册和取消注册测试
|
||||||
|
- 任务执行测试
|
||||||
|
- 状态监控测试
|
||||||
|
- 后台获取状态测试
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **iOS限制**: iOS对后台任务有严格限制,系统会根据电池状态和用户使用模式调整执行频率
|
||||||
|
2. **权限要求**: 某些后台任务可能需要特殊权限
|
||||||
|
3. **调试模式**: 在开发模式下,后台任务行为可能与生产环境不同
|
||||||
|
4. **网络状态**: 后台任务执行时需要考虑网络状态变化
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **任务不执行**: 检查iOS后台模式配置和任务注册状态
|
||||||
|
2. **执行频率低**: 系统会根据电池状态自动调整,这是正常行为
|
||||||
|
3. **任务被终止**: 检查任务执行时间,避免长时间运行
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. 使用 `console.log` 记录任务执行状态
|
||||||
|
2. 检查任务状态和错误信息
|
||||||
|
3. 使用Xcode查看后台任务日志
|
||||||
|
4. 测试不同的最小间隔设置
|
||||||
109
hooks/useBackgroundTasks.ts
Normal file
109
hooks/useBackgroundTasks.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { BackgroundTaskType as BackgroundTask, backgroundTaskManager, TaskStatusType as TaskStatus } from '@/services/backgroundTaskManager';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface UseBackgroundTasksReturn {
|
||||||
|
// 状态
|
||||||
|
isInitialized: boolean;
|
||||||
|
taskStatuses: TaskStatus[];
|
||||||
|
registeredTasks: BackgroundTask[];
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
registerTask: (task: BackgroundTask) => Promise<void>;
|
||||||
|
unregisterTask: (taskId: string) => Promise<void>;
|
||||||
|
executeTask: (taskId: string, data?: any) => Promise<void>;
|
||||||
|
executeAllTasks: () => Promise<{ [taskId: string]: 'success' | 'failed' }>;
|
||||||
|
getTaskStatus: (taskId: string) => TaskStatus | undefined;
|
||||||
|
cleanupTaskStatuses: () => Promise<void>;
|
||||||
|
|
||||||
|
// 后台任务状态
|
||||||
|
backgroundTaskStatus: string | null;
|
||||||
|
getBackgroundTaskStatus: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBackgroundTasks = (): UseBackgroundTasksReturn => {
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const [taskStatuses, setTaskStatuses] = useState<TaskStatus[]>([]);
|
||||||
|
const [registeredTasks, setRegisteredTasks] = useState<BackgroundTask[]>([]);
|
||||||
|
const [backgroundTaskStatus, setBackgroundTaskStatus] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
useEffect(() => {
|
||||||
|
const initialize = async () => {
|
||||||
|
try {
|
||||||
|
await backgroundTaskManager.initialize();
|
||||||
|
setIsInitialized(true);
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化后台任务失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = useCallback(() => {
|
||||||
|
setTaskStatuses(backgroundTaskManager.getAllTaskStatuses());
|
||||||
|
setRegisteredTasks(backgroundTaskManager.getRegisteredTasks());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 注册任务
|
||||||
|
const registerTask = useCallback(async (task: BackgroundTask) => {
|
||||||
|
await backgroundTaskManager.registerTask(task);
|
||||||
|
refreshData();
|
||||||
|
}, [refreshData]);
|
||||||
|
|
||||||
|
// 取消注册任务
|
||||||
|
const unregisterTask = useCallback(async (taskId: string) => {
|
||||||
|
await backgroundTaskManager.unregisterTask(taskId);
|
||||||
|
refreshData();
|
||||||
|
}, [refreshData]);
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
const executeTask = useCallback(async (taskId: string, data?: any) => {
|
||||||
|
await backgroundTaskManager.executeTask(taskId, data);
|
||||||
|
refreshData();
|
||||||
|
}, [refreshData]);
|
||||||
|
|
||||||
|
// 执行所有任务
|
||||||
|
const executeAllTasks = useCallback(async () => {
|
||||||
|
const results = await backgroundTaskManager.executeAllTasks();
|
||||||
|
refreshData();
|
||||||
|
return results;
|
||||||
|
}, [refreshData]);
|
||||||
|
|
||||||
|
// 获取任务状态
|
||||||
|
const getTaskStatus = useCallback((taskId: string) => {
|
||||||
|
return backgroundTaskManager.getTaskStatus(taskId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 清理任务状态
|
||||||
|
const cleanupTaskStatuses = useCallback(async () => {
|
||||||
|
await backgroundTaskManager.cleanupTaskStatuses();
|
||||||
|
refreshData();
|
||||||
|
}, [refreshData]);
|
||||||
|
|
||||||
|
// 获取后台任务状态
|
||||||
|
const getBackgroundTaskStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const status = await backgroundTaskManager.getBackgroundTaskStatus();
|
||||||
|
setBackgroundTaskStatus(status ? status.toString() : null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取后台任务状态失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isInitialized,
|
||||||
|
taskStatuses,
|
||||||
|
registeredTasks,
|
||||||
|
registerTask,
|
||||||
|
unregisterTask,
|
||||||
|
executeTask,
|
||||||
|
executeAllTasks,
|
||||||
|
getTaskStatus,
|
||||||
|
cleanupTaskStatuses,
|
||||||
|
backgroundTaskStatus,
|
||||||
|
getBackgroundTaskStatus,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -40,6 +40,8 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoAsset (11.1.7):
|
- ExpoAsset (11.1.7):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoBackgroundTask (0.2.8):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoBlur (14.1.5):
|
- ExpoBlur (14.1.5):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoFileSystem (18.1.11):
|
- ExpoFileSystem (18.1.11):
|
||||||
@@ -96,6 +98,9 @@ PODS:
|
|||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoWebBrowser (14.2.0):
|
- ExpoWebBrowser (14.2.0):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- EXTaskManager (13.1.6):
|
||||||
|
- ExpoModulesCore
|
||||||
|
- UMAppLoader
|
||||||
- fast_float (6.1.4)
|
- fast_float (6.1.4)
|
||||||
- FBLazyVector (0.79.5)
|
- FBLazyVector (0.79.5)
|
||||||
- fmt (11.0.2)
|
- fmt (11.0.2)
|
||||||
@@ -1949,6 +1954,7 @@ PODS:
|
|||||||
- SDWebImage/Core (~> 5.17)
|
- SDWebImage/Core (~> 5.17)
|
||||||
- Sentry/HybridSDK (8.53.2)
|
- Sentry/HybridSDK (8.53.2)
|
||||||
- SocketRocket (0.7.1)
|
- SocketRocket (0.7.1)
|
||||||
|
- UMAppLoader (5.1.3)
|
||||||
- Yoga (0.0.0)
|
- Yoga (0.0.0)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
@@ -1961,6 +1967,7 @@ DEPENDENCIES:
|
|||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||||
|
- ExpoBackgroundTask (from `../node_modules/expo-background-task/ios`)
|
||||||
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
||||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||||
@@ -1976,6 +1983,7 @@ DEPENDENCIES:
|
|||||||
- ExpoSymbols (from `../node_modules/expo-symbols/ios`)
|
- ExpoSymbols (from `../node_modules/expo-symbols/ios`)
|
||||||
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
- ExpoSystemUI (from `../node_modules/expo-system-ui/ios`)
|
||||||
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
|
||||||
|
- EXTaskManager (from `../node_modules/expo-task-manager/ios`)
|
||||||
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
|
||||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||||
@@ -2059,6 +2067,7 @@ DEPENDENCIES:
|
|||||||
- RNScreens (from `../node_modules/react-native-screens`)
|
- RNScreens (from `../node_modules/react-native-screens`)
|
||||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||||
- RNSVG (from `../node_modules/react-native-svg`)
|
- RNSVG (from `../node_modules/react-native-svg`)
|
||||||
|
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
|
||||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@@ -2097,6 +2106,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo-apple-authentication/ios"
|
:path: "../node_modules/expo-apple-authentication/ios"
|
||||||
ExpoAsset:
|
ExpoAsset:
|
||||||
:path: "../node_modules/expo-asset/ios"
|
:path: "../node_modules/expo-asset/ios"
|
||||||
|
ExpoBackgroundTask:
|
||||||
|
:path: "../node_modules/expo-background-task/ios"
|
||||||
ExpoBlur:
|
ExpoBlur:
|
||||||
:path: "../node_modules/expo-blur/ios"
|
:path: "../node_modules/expo-blur/ios"
|
||||||
ExpoFileSystem:
|
ExpoFileSystem:
|
||||||
@@ -2127,6 +2138,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo-system-ui/ios"
|
:path: "../node_modules/expo-system-ui/ios"
|
||||||
ExpoWebBrowser:
|
ExpoWebBrowser:
|
||||||
:path: "../node_modules/expo-web-browser/ios"
|
:path: "../node_modules/expo-web-browser/ios"
|
||||||
|
EXTaskManager:
|
||||||
|
:path: "../node_modules/expo-task-manager/ios"
|
||||||
fast_float:
|
fast_float:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
||||||
FBLazyVector:
|
FBLazyVector:
|
||||||
@@ -2289,6 +2302,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/@sentry/react-native"
|
:path: "../node_modules/@sentry/react-native"
|
||||||
RNSVG:
|
RNSVG:
|
||||||
:path: "../node_modules/react-native-svg"
|
:path: "../node_modules/react-native-svg"
|
||||||
|
UMAppLoader:
|
||||||
|
:path: "../node_modules/unimodules-app-loader/ios"
|
||||||
Yoga:
|
Yoga:
|
||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
@@ -2302,6 +2317,7 @@ SPEC CHECKSUMS:
|
|||||||
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
||||||
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
||||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||||
|
ExpoBackgroundTask: 6c1990438e45b5c4bbbc7d75aa6b688d53602fe8
|
||||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||||
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
||||||
@@ -2317,6 +2333,7 @@ SPEC CHECKSUMS:
|
|||||||
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
|
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
|
||||||
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
|
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
|
||||||
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
||||||
|
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
|
||||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||||
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
||||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||||
@@ -2412,6 +2429,7 @@ SPEC CHECKSUMS:
|
|||||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
|
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
|
||||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||||
|
|
||||||
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
||||||
|
|||||||
@@ -268,6 +268,7 @@
|
|||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/EXTaskManager/ExpoTaskManager_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/ExpoSystemUI/ExpoSystemUI_privacy.bundle",
|
||||||
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
"${PODS_CONFIGURATION_BUILD_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.bundle",
|
||||||
@@ -290,6 +291,7 @@
|
|||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle",
|
||||||
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoTaskManager_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoSystemUI_privacy.bundle",
|
||||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||||
|
|||||||
@@ -61,6 +61,10 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>fetch</string>
|
||||||
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -21,6 +21,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
"expo-apple-authentication": "6.4.2",
|
"expo-apple-authentication": "6.4.2",
|
||||||
|
"expo-background-task": "~0.2.8",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-constants": "~17.1.7",
|
"expo-constants": "~17.1.7",
|
||||||
"expo-font": "~13.3.2",
|
"expo-font": "~13.3.2",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-symbols": "~0.4.5",
|
"expo-symbols": "~0.4.5",
|
||||||
"expo-system-ui": "~5.0.10",
|
"expo-system-ui": "~5.0.10",
|
||||||
|
"expo-task-manager": "^13.1.6",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
@@ -7030,6 +7032,18 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-background-task": {
|
||||||
|
"version": "0.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-background-task/-/expo-background-task-0.2.8.tgz",
|
||||||
|
"integrity": "sha512-dePyskpmyDZeOtbr9vWFh+Nrse0TvF6YitJqnKcd+3P7pDMiDr1V2aT6zHdNOc5iV9vPaDJoH/zdmlarp1uHMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-task-manager": "~13.1.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-blur": {
|
"node_modules/expo-blur": {
|
||||||
"version": "14.1.5",
|
"version": "14.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.1.5.tgz",
|
||||||
@@ -7320,6 +7334,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-task-manager": {
|
||||||
|
"version": "13.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-task-manager/-/expo-task-manager-13.1.6.tgz",
|
||||||
|
"integrity": "sha512-sYNAftpIeZ+j6ur17Jo0OpSTk9ks/MDvTbrNCimXMyjIt69XXYL/kAPYf76bWuxOuN8bcJ8Ef8YvihkwFG9hDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"unimodules-app-loader": "~5.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-web-browser": {
|
"node_modules/expo-web-browser": {
|
||||||
"version": "14.2.0",
|
"version": "14.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz",
|
||||||
@@ -13759,6 +13786,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unimodules-app-loader": {
|
||||||
|
"version": "5.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/unimodules-app-loader/-/unimodules-app-loader-5.1.3.tgz",
|
||||||
|
"integrity": "sha512-nPUkwfkpJWvdOQrVvyQSUol93/UdmsCVd9Hkx9RgAevmKSVYdZI+S87W73NGKl6QbwK9L1BDSY5OrQuo8Oq15g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/unique-string": {
|
"node_modules/unique-string": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
"expo-apple-authentication": "6.4.2",
|
"expo-apple-authentication": "6.4.2",
|
||||||
|
"expo-background-task": "~0.2.8",
|
||||||
"expo-blur": "~14.1.5",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-constants": "~17.1.7",
|
"expo-constants": "~17.1.7",
|
||||||
"expo-font": "~13.3.2",
|
"expo-font": "~13.3.2",
|
||||||
@@ -32,11 +33,13 @@
|
|||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "^14.1.5",
|
"expo-linear-gradient": "^14.1.5",
|
||||||
"expo-linking": "~7.1.7",
|
"expo-linking": "~7.1.7",
|
||||||
|
"expo-notifications": "~0.31.4",
|
||||||
"expo-router": "~5.1.4",
|
"expo-router": "~5.1.4",
|
||||||
"expo-splash-screen": "~0.30.10",
|
"expo-splash-screen": "~0.30.10",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-symbols": "~0.4.5",
|
"expo-symbols": "~0.4.5",
|
||||||
"expo-system-ui": "~5.0.10",
|
"expo-system-ui": "~5.0.10",
|
||||||
|
"expo-task-manager": "^13.1.6",
|
||||||
"expo-web-browser": "~14.2.0",
|
"expo-web-browser": "~14.2.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
@@ -59,8 +62,7 @@
|
|||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "13.13.5",
|
||||||
"react-native-wheel-picker-expo": "^0.5.4",
|
"react-native-wheel-picker-expo": "^0.5.4",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0"
|
||||||
"expo-notifications": "~0.31.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
|||||||
263
services/backgroundTaskManager.ts
Normal file
263
services/backgroundTaskManager.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import * as ExpoBackgroundTask from 'expo-background-task';
|
||||||
|
import * as TaskManager from 'expo-task-manager';
|
||||||
|
|
||||||
|
// 任务类型定义
|
||||||
|
export interface BackgroundTask {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
handler: (data?: any) => Promise<void>;
|
||||||
|
options?: {
|
||||||
|
minimumInterval?: number; // 最小间隔时间(分钟)
|
||||||
|
stopOnTerminate?: boolean; // 应用终止时是否停止
|
||||||
|
startOnBoot?: boolean; // 设备重启时是否启动
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任务状态
|
||||||
|
export interface TaskStatus {
|
||||||
|
id: string;
|
||||||
|
isRegistered: boolean;
|
||||||
|
lastExecution?: Date;
|
||||||
|
nextExecution?: Date;
|
||||||
|
executionCount: number;
|
||||||
|
lastError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台任务管理器类
|
||||||
|
class BackgroundTaskManager {
|
||||||
|
private static instance: BackgroundTaskManager;
|
||||||
|
private tasks: Map<string, BackgroundTask> = new Map();
|
||||||
|
private taskStatuses: Map<string, TaskStatus> = new Map();
|
||||||
|
private isInitialized = false;
|
||||||
|
|
||||||
|
// 单例模式
|
||||||
|
public static getInstance(): BackgroundTaskManager {
|
||||||
|
if (!BackgroundTaskManager.instance) {
|
||||||
|
BackgroundTaskManager.instance = new BackgroundTaskManager();
|
||||||
|
}
|
||||||
|
return BackgroundTaskManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化后台任务管理器
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
if (this.isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isInitialized = true;
|
||||||
|
|
||||||
|
// 注册后台任务
|
||||||
|
await this.registerBackgroundTask();
|
||||||
|
|
||||||
|
// 加载已保存的任务状态
|
||||||
|
await this.loadTaskStatuses();
|
||||||
|
|
||||||
|
console.log('后台任务管理器初始化成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('后台任务管理器初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册后台任务
|
||||||
|
private async registerBackgroundTask(): Promise<void> {
|
||||||
|
const BACKGROUND_TASK = 'background-task';
|
||||||
|
|
||||||
|
console.log('注册后台任务');
|
||||||
|
// 定义后台任务
|
||||||
|
TaskManager.defineTask(BACKGROUND_TASK, async () => {
|
||||||
|
try {
|
||||||
|
console.log('开始执行后台任务');
|
||||||
|
|
||||||
|
// 执行所有注册的任务
|
||||||
|
const results = await this.executeAllTasks();
|
||||||
|
|
||||||
|
console.log('后台任务执行完成:', results);
|
||||||
|
|
||||||
|
// 返回成功状态
|
||||||
|
return ExpoBackgroundTask.BackgroundTaskResult.Success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('后台任务执行失败:', error);
|
||||||
|
return ExpoBackgroundTask.BackgroundTaskResult.Failed;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册后台任务
|
||||||
|
await ExpoBackgroundTask.registerTaskAsync(BACKGROUND_TASK, {
|
||||||
|
minimumInterval: 15, // 最小间隔60分钟
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('后台任务注册成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册自定义任务
|
||||||
|
public async registerTask(task: BackgroundTask): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 检查任务是否已存在
|
||||||
|
if (this.tasks.has(task.id)) {
|
||||||
|
console.warn(`任务 ${task.id} 已存在,将被覆盖`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存任务
|
||||||
|
this.tasks.set(task.id, task);
|
||||||
|
|
||||||
|
// 初始化任务状态
|
||||||
|
if (!this.taskStatuses.has(task.id)) {
|
||||||
|
this.taskStatuses.set(task.id, {
|
||||||
|
id: task.id,
|
||||||
|
isRegistered: true,
|
||||||
|
executionCount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存任务状态
|
||||||
|
await this.saveTaskStatuses();
|
||||||
|
|
||||||
|
console.log(`任务 ${task.id} 注册成功`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`注册任务 ${task.id} 失败:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消注册任务
|
||||||
|
public async unregisterTask(taskId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 移除任务
|
||||||
|
this.tasks.delete(taskId);
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
const status = this.taskStatuses.get(taskId);
|
||||||
|
if (status) {
|
||||||
|
status.isRegistered = false;
|
||||||
|
await this.saveTaskStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`任务 ${taskId} 取消注册成功`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`取消注册任务 ${taskId} 失败:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动执行任务
|
||||||
|
public async executeTask(taskId: string, data?: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const task = this.tasks.get(taskId);
|
||||||
|
if (!task) {
|
||||||
|
throw new Error(`任务 ${taskId} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始执行任务: ${taskId}`);
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
await task.handler(data);
|
||||||
|
|
||||||
|
// 更新任务状态
|
||||||
|
const status = this.taskStatuses.get(taskId);
|
||||||
|
if (status) {
|
||||||
|
status.lastExecution = new Date();
|
||||||
|
status.executionCount += 1;
|
||||||
|
status.lastError = undefined;
|
||||||
|
await this.saveTaskStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`任务 ${taskId} 执行成功`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`执行任务 ${taskId} 失败:`, error);
|
||||||
|
|
||||||
|
// 更新错误状态
|
||||||
|
const status = this.taskStatuses.get(taskId);
|
||||||
|
if (status) {
|
||||||
|
status.lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
await this.saveTaskStatuses();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行所有任务
|
||||||
|
public async executeAllTasks(): Promise<{ [taskId: string]: 'success' | 'failed' }> {
|
||||||
|
const results: { [taskId: string]: 'success' | 'failed' } = {};
|
||||||
|
|
||||||
|
for (const [taskId, task] of this.tasks) {
|
||||||
|
try {
|
||||||
|
await this.executeTask(taskId);
|
||||||
|
results[taskId] = 'success';
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`执行任务 ${taskId} 失败:`, error);
|
||||||
|
results[taskId] = 'failed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务状态
|
||||||
|
public getTaskStatus(taskId: string): TaskStatus | undefined {
|
||||||
|
return this.taskStatuses.get(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有任务状态
|
||||||
|
public getAllTaskStatuses(): TaskStatus[] {
|
||||||
|
return Array.from(this.taskStatuses.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取已注册的任务列表
|
||||||
|
public getRegisteredTasks(): BackgroundTask[] {
|
||||||
|
return Array.from(this.tasks.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查后台任务状态
|
||||||
|
public async getBackgroundTaskStatus(): Promise<ExpoBackgroundTask.BackgroundTaskStatus | null> {
|
||||||
|
return await ExpoBackgroundTask.getStatusAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存任务状态到本地存储
|
||||||
|
private async saveTaskStatuses(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const statuses = Array.from(this.taskStatuses.values());
|
||||||
|
await AsyncStorage.setItem('@background_task_statuses', JSON.stringify(statuses));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存任务状态失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储加载任务状态
|
||||||
|
private async loadTaskStatuses(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const statusesJson = await AsyncStorage.getItem('@background_task_statuses');
|
||||||
|
if (statusesJson) {
|
||||||
|
const statuses: TaskStatus[] = JSON.parse(statusesJson);
|
||||||
|
statuses.forEach(status => {
|
||||||
|
this.taskStatuses.set(status.id, status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载任务状态失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期的任务状态
|
||||||
|
public async cleanupTaskStatuses(): Promise<void> {
|
||||||
|
const now = new Date();
|
||||||
|
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
for (const [taskId, status] of this.taskStatuses) {
|
||||||
|
if (status.lastExecution && status.lastExecution < thirtyDaysAgo && !status.isRegistered) {
|
||||||
|
this.taskStatuses.delete(taskId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.saveTaskStatuses();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const backgroundTaskManager = BackgroundTaskManager.getInstance();
|
||||||
|
|
||||||
|
// 导出类型
|
||||||
|
export type { BackgroundTask as BackgroundTaskType, TaskStatus as TaskStatusType };
|
||||||
180
services/backgroundTasks.ts
Normal file
180
services/backgroundTasks.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { BackgroundTaskType as BackgroundTask, backgroundTaskManager } from './backgroundTaskManager';
|
||||||
|
|
||||||
|
// 示例任务:数据同步任务
|
||||||
|
export const createDataSyncTask = (): BackgroundTask => ({
|
||||||
|
id: 'data-sync-task',
|
||||||
|
name: '数据同步任务',
|
||||||
|
handler: async (data?: any) => {
|
||||||
|
console.log('开始执行数据同步任务');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现您的数据同步逻辑
|
||||||
|
// 例如:同步用户数据、运动记录、目标进度等
|
||||||
|
|
||||||
|
// 模拟数据同步过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
console.log('数据同步任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据同步任务执行失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minimumInterval: 5, // 5分钟最小间隔
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 示例任务:健康数据更新任务
|
||||||
|
export const createHealthDataUpdateTask = (): BackgroundTask => ({
|
||||||
|
id: 'health-data-update-task',
|
||||||
|
name: '健康数据更新任务',
|
||||||
|
handler: async (data?: any) => {
|
||||||
|
console.log('开始执行健康数据更新任务');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现您的健康数据更新逻辑
|
||||||
|
// 例如:更新步数、心率、体重等健康数据
|
||||||
|
|
||||||
|
// 模拟健康数据更新过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
console.log('健康数据更新任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('健康数据更新任务执行失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minimumInterval: 10, // 10分钟最小间隔
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 示例任务:通知检查任务
|
||||||
|
export const createNotificationCheckTask = (): BackgroundTask => ({
|
||||||
|
id: 'notification-check-task',
|
||||||
|
name: '通知检查任务',
|
||||||
|
handler: async (data?: any) => {
|
||||||
|
console.log('开始执行通知检查任务');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现您的通知检查逻辑
|
||||||
|
// 例如:检查是否需要发送运动提醒、目标达成通知等
|
||||||
|
|
||||||
|
// 模拟通知检查过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
console.log('通知检查任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('通知检查任务执行失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minimumInterval: 30, // 30分钟最小间隔
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 示例任务:缓存清理任务
|
||||||
|
export const createCacheCleanupTask = (): BackgroundTask => ({
|
||||||
|
id: 'cache-cleanup-task',
|
||||||
|
name: '缓存清理任务',
|
||||||
|
handler: async (data?: any) => {
|
||||||
|
console.log('开始执行缓存清理任务');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现您的缓存清理逻辑
|
||||||
|
// 例如:清理过期的图片缓存、临时文件等
|
||||||
|
|
||||||
|
// 模拟缓存清理过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
console.log('缓存清理任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('缓存清理任务执行失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minimumInterval: 86400, // 24小时最小间隔
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 示例任务:用户行为分析任务
|
||||||
|
export const createUserAnalyticsTask = (): BackgroundTask => ({
|
||||||
|
id: 'user-analytics-task',
|
||||||
|
name: '用户行为分析任务',
|
||||||
|
handler: async (data?: any) => {
|
||||||
|
console.log('开始执行用户行为分析任务');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里实现您的用户行为分析逻辑
|
||||||
|
// 例如:分析用户运动习惯、使用模式等
|
||||||
|
|
||||||
|
// 模拟用户行为分析过程
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2500));
|
||||||
|
|
||||||
|
console.log('用户行为分析任务执行完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('用户行为分析任务执行失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
minimumInterval: 60, // 1小时最小间隔
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册所有默认任务
|
||||||
|
export const registerDefaultTasks = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const tasks = [
|
||||||
|
createDataSyncTask(),
|
||||||
|
createHealthDataUpdateTask(),
|
||||||
|
createNotificationCheckTask(),
|
||||||
|
createCacheCleanupTask(),
|
||||||
|
createUserAnalyticsTask(),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const task of tasks) {
|
||||||
|
await backgroundTaskManager.registerTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('所有默认任务注册完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册默认任务失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建自定义任务的工厂函数
|
||||||
|
export const createCustomTask = (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
handler: (data?: any) => Promise<void>,
|
||||||
|
options?: {
|
||||||
|
minimumInterval?: number;
|
||||||
|
stopOnTerminate?: boolean;
|
||||||
|
startOnBoot?: boolean;
|
||||||
|
}
|
||||||
|
): BackgroundTask => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
handler,
|
||||||
|
options: {
|
||||||
|
minimumInterval: 300, // 默认5分钟
|
||||||
|
stopOnTerminate: false,
|
||||||
|
startOnBoot: true,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user