feat: 集成后台任务管理功能及相关组件
- 新增后台任务管理器,支持任务的注册、执行和状态监控 - 实现自定义Hook,简化后台任务的使用和管理 - 添加示例任务,包括数据同步、健康数据更新和通知检查等 - 更新文档,详细描述后台任务系统的实现和使用方法 - 优化相关组件,确保用户体验和界面一致性
This commit is contained in:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user