feat: 集成后台任务管理功能及相关组件
- 新增后台任务管理器,支持任务的注册、执行和状态监控 - 实现自定义Hook,简化后台任务的使用和管理 - 添加示例任务,包括数据同步、健康数据更新和通知检查等 - 更新文档,详细描述后台任务系统的实现和使用方法 - 优化相关组件,确保用户体验和界面一致性
This commit is contained in:
13
app.json
13
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<NutritionSummary | null>(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);
|
||||
|
||||
@@ -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() {
|
||||
<Stack.Screen name="legal/privacy-policy" options={{ headerShown: true, title: '隐私政策' }} />
|
||||
<Stack.Screen name="article/[id]" 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>
|
||||
<StatusBar style="dark" />
|
||||
|
||||
@@ -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])
|
||||
);
|
||||
|
||||
// 月份切换函数
|
||||
|
||||
@@ -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 (
|
||||
<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>
|
||||
);
|
||||
const handleIntensityChange = (value: number) => {
|
||||
setIntensity(value);
|
||||
};
|
||||
|
||||
// 使用统一的渐变背景色
|
||||
@@ -210,12 +191,17 @@ export default function MoodEditScreen() {
|
||||
</View>
|
||||
|
||||
{/* 心情强度选择 */}
|
||||
{selectedMood && (
|
||||
<View style={styles.intensitySection}>
|
||||
<Text style={styles.sectionTitle}>心情强度</Text>
|
||||
{renderIntensitySlider()}
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.intensitySection}>
|
||||
<Text style={styles.sectionTitle}>心情强度</Text>
|
||||
<MoodIntensitySlider
|
||||
value={intensity}
|
||||
onValueChange={handleIntensityChange}
|
||||
min={1}
|
||||
max={10}
|
||||
width={320}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
{/* 心情描述 */}
|
||||
{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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
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
|
||||
- ExpoAsset (11.1.7):
|
||||
- ExpoModulesCore
|
||||
- ExpoBackgroundTask (0.2.8):
|
||||
- ExpoModulesCore
|
||||
- ExpoBlur (14.1.5):
|
||||
- ExpoModulesCore
|
||||
- ExpoFileSystem (18.1.11):
|
||||
@@ -96,6 +98,9 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoWebBrowser (14.2.0):
|
||||
- ExpoModulesCore
|
||||
- EXTaskManager (13.1.6):
|
||||
- ExpoModulesCore
|
||||
- UMAppLoader
|
||||
- fast_float (6.1.4)
|
||||
- FBLazyVector (0.79.5)
|
||||
- fmt (11.0.2)
|
||||
@@ -1949,6 +1954,7 @@ PODS:
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- Sentry/HybridSDK (8.53.2)
|
||||
- SocketRocket (0.7.1)
|
||||
- UMAppLoader (5.1.3)
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
@@ -1961,6 +1967,7 @@ DEPENDENCIES:
|
||||
- Expo (from `../node_modules/expo`)
|
||||
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||
- ExpoBackgroundTask (from `../node_modules/expo-background-task/ios`)
|
||||
- ExpoBlur (from `../node_modules/expo-blur/ios`)
|
||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||
@@ -1976,6 +1983,7 @@ DEPENDENCIES:
|
||||
- ExpoSymbols (from `../node_modules/expo-symbols/ios`)
|
||||
- ExpoSystemUI (from `../node_modules/expo-system-ui/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`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||
@@ -2059,6 +2067,7 @@ DEPENDENCIES:
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||
- RNSVG (from `../node_modules/react-native-svg`)
|
||||
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
SPEC REPOS:
|
||||
@@ -2097,6 +2106,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-apple-authentication/ios"
|
||||
ExpoAsset:
|
||||
:path: "../node_modules/expo-asset/ios"
|
||||
ExpoBackgroundTask:
|
||||
:path: "../node_modules/expo-background-task/ios"
|
||||
ExpoBlur:
|
||||
:path: "../node_modules/expo-blur/ios"
|
||||
ExpoFileSystem:
|
||||
@@ -2127,6 +2138,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-system-ui/ios"
|
||||
ExpoWebBrowser:
|
||||
:path: "../node_modules/expo-web-browser/ios"
|
||||
EXTaskManager:
|
||||
:path: "../node_modules/expo-task-manager/ios"
|
||||
fast_float:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
|
||||
FBLazyVector:
|
||||
@@ -2289,6 +2302,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/@sentry/react-native"
|
||||
RNSVG:
|
||||
:path: "../node_modules/react-native-svg"
|
||||
UMAppLoader:
|
||||
:path: "../node_modules/unimodules-app-loader/ios"
|
||||
Yoga:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
@@ -2302,6 +2317,7 @@ SPEC CHECKSUMS:
|
||||
Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea
|
||||
ExpoAppleAuthentication: 8a661b6f4936affafd830f983ac22463c936dad5
|
||||
ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6
|
||||
ExpoBackgroundTask: 6c1990438e45b5c4bbbc7d75aa6b688d53602fe8
|
||||
ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9
|
||||
ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63
|
||||
ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e
|
||||
@@ -2317,6 +2333,7 @@ SPEC CHECKSUMS:
|
||||
ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859
|
||||
ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2
|
||||
ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92
|
||||
EXTaskManager: 280143f6d8e596f28739d74bf34910300dcbd4ea
|
||||
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
|
||||
FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
@@ -2412,6 +2429,7 @@ SPEC CHECKSUMS:
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
UMAppLoader: 55159b69750129faa7a51c493cb8ea55a7b64eb9
|
||||
Yoga: adb397651e1c00672c12e9495babca70777e411e
|
||||
|
||||
PODFILE CHECKSUM: 8d79b726cf7814a1ef2e250b7a9ef91c07c77936
|
||||
|
||||
@@ -268,6 +268,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_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}/ExpoSystemUI/ExpoSystemUI_privacy.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}/ExpoConstants_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}/ExpoSystemUI_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PurchasesHybridCommon.bundle",
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"expo": "~53.0.20",
|
||||
"expo-apple-authentication": "6.4.2",
|
||||
"expo-background-task": "~0.2.8",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-font": "~13.3.2",
|
||||
@@ -35,6 +36,7 @@
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.10",
|
||||
"expo-task-manager": "^13.1.6",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
@@ -7030,6 +7032,18 @@
|
||||
"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": {
|
||||
"version": "14.1.5",
|
||||
"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": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-14.2.0.tgz",
|
||||
@@ -13759,6 +13786,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"expo": "~53.0.20",
|
||||
"expo-apple-authentication": "6.4.2",
|
||||
"expo-background-task": "~0.2.8",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-font": "~13.3.2",
|
||||
@@ -32,11 +33,13 @@
|
||||
"expo-image-picker": "~16.1.4",
|
||||
"expo-linear-gradient": "^14.1.5",
|
||||
"expo-linking": "~7.1.7",
|
||||
"expo-notifications": "~0.31.4",
|
||||
"expo-router": "~5.1.4",
|
||||
"expo-splash-screen": "~0.30.10",
|
||||
"expo-status-bar": "~2.2.3",
|
||||
"expo-symbols": "~0.4.5",
|
||||
"expo-system-ui": "~5.0.10",
|
||||
"expo-task-manager": "^13.1.6",
|
||||
"expo-web-browser": "~14.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
@@ -59,8 +62,7 @@
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-wheel-picker-expo": "^0.5.4",
|
||||
"react-redux": "^9.2.0",
|
||||
"expo-notifications": "~0.31.4"
|
||||
"react-redux": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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