Files
digital-pilates/app/mood/calendar.tsx
richarjiang acb3907344 Refactor: Remove background task management and related hooks
- Deleted `useBackgroundTasks.ts` hook and its associated logic for managing background tasks.
- Removed `backgroundTaskManager.ts` service and all related task definitions and registrations.
- Cleaned up `Podfile.lock` and `package.json` to remove unused dependencies related to background tasks.
- Updated iOS project files to eliminate references to removed background task components.
- Added new background fetch identifier in `Info.plist` for future use.
2025-09-05 09:47:49 +08:00

708 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useMoodData } from '@/hooks/useMoodData';
import { getMoodOptions } from '@/services/moodCheckins';
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, useRef, useState } from 'react';
import {
Dimensions, Image, SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
const { width } = Dimensions.get('window');
// 心情日历数据生成函数
const generateCalendarData = (targetDate: Date) => {
const year = targetDate.getFullYear();
const month = targetDate.getMonth();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const firstDayOfWeek = new Date(year, month, 1).getDay();
const calendar = [];
const weeks = [];
// 添加空白日期
for (let i = 0; i < firstDayOfWeek; i++) {
weeks.push(null);
}
// 添加实际日期
for (let day = 1; day <= daysInMonth; day++) {
weeks.push(day);
}
// 按周分组
for (let i = 0; i < weeks.length; i += 7) {
calendar.push(weeks.slice(i, i + 7));
}
return { calendar, today: new Date().getDate(), month: month + 1, year };
};
export default function MoodCalendarScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const params = useLocalSearchParams();
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();
const [currentMonth, setCurrentMonth] = useState(initialDate);
const [selectedDay, setSelectedDay] = useState<number | null>(null);
// 使用 Redux store 中的数据
const moodRecords = useAppSelector(state => state.mood.moodRecords);
// 获取选中日期的数据
const selectedDateMood = useAppSelector(state => {
if (!selectedDay) return null;
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
return selectLatestMoodRecordByDate(selectedDateString)(state);
});
const moodOptions = getMoodOptions();
const weekDays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
// 生成当前月份的日历数据
const { calendar, today, month, year } = generateCalendarData(currentMonth);
// 加载整个月份的心情数据
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');
await fetchMoodHistoryRecordsRef.current({ startDate, endDate });
} catch (error) {
console.error('加载月份心情数据失败:', error);
}
}, []);
// 加载选中日期的心情记录
const loadDailyMoodCheckins = useCallback(async (dateString: string) => {
try {
await fetchMoodRecordsRef.current(dateString);
} catch (error) {
console.error('加载心情记录失败:', error);
}
}, []);
// 初始化选中日期
useEffect(() => {
if (selectedDate) {
const date = dayjs(selectedDate as string);
setCurrentMonth(date.toDate());
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);
}
}, [selectedDate, loadDailyMoodCheckins, loadMonthMoodData]);
// 监听页面焦点变化,当从编辑页面返回时刷新数据
useFocusEffect(
useCallback(() => {
// 当页面获得焦点时,刷新当前月份的数据和选中日期的数据
const refreshData = async () => {
if (selectedDay) {
const selectedDateString = dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD');
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 fetchMoodHistoryRecordsRef.current({ startDate, endDate });
};
refreshData();
}, [currentMonth, selectedDay])
);
// 月份切换函数
const goToPreviousMonth = () => {
const newMonth = new Date(currentMonth);
newMonth.setMonth(newMonth.getMonth() - 1);
setCurrentMonth(newMonth);
setSelectedDay(null);
loadMonthMoodData(newMonth);
};
const goToNextMonth = () => {
const newMonth = new Date(currentMonth);
newMonth.setMonth(newMonth.getMonth() + 1);
setCurrentMonth(newMonth);
setSelectedDay(null);
loadMonthMoodData(newMonth);
};
// 日期选择函数
const onSelectDate = (day: number) => {
setSelectedDay(day);
const selectedDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
loadDailyMoodCheckins(selectedDateString);
};
// 跳转到心情编辑页面
const openMoodEdit = () => {
const selectedDateString = selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
const moodId = selectedDateMood?.id;
router.push({
pathname: '/mood/edit',
params: {
date: selectedDateString,
...(moodId && { moodId })
}
});
};
const renderMoodRing = (day: number | null, isSelected: boolean) => {
if (!day) return null;
// 检查该日期是否有心情记录 - 现在从 Redux store 中获取
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
const dayRecords = moodRecords[dayDateString] || [];
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
const isToday = day === new Date().getDate() &&
month === new Date().getMonth() + 1 &&
year === new Date().getFullYear();
if (moodRecord) {
const mood = moodOptions.find(m => m.type === moodRecord.moodType);
return (
<View style={isToday ? styles.todayMoodIconContainer : styles.moodIconContainer}>
<View style={styles.moodIcon}>
<Image
source={mood?.image}
style={styles.moodIconImage}
/>
</View>
</View>
);
}
return (
<View style={isToday ? styles.todayDefaultMoodIcon : styles.defaultMoodIcon}>
</View>
);
};
return (
<View style={styles.container}>
<LinearGradient
colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<SafeAreaView style={styles.safeArea}>
<HeaderBar
title="心情日历"
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
tone="light"
/>
<ScrollView style={styles.content}>
{/* 日历视图 */}
<View style={styles.calendar}>
{/* 月份导航 */}
<View style={styles.monthNavigation}>
<TouchableOpacity
style={styles.navButton}
onPress={goToPreviousMonth}
>
<Text style={styles.navButtonText}></Text>
</TouchableOpacity>
<Text style={styles.monthTitle}>{year}{monthNames[month - 1]}</Text>
<TouchableOpacity
style={styles.navButton}
onPress={goToNextMonth}
>
<Text style={styles.navButtonText}></Text>
</TouchableOpacity>
</View>
<View style={styles.weekHeader}>
{weekDays.map((day, index) => (
<Text key={index} style={styles.weekDay}>{day}</Text>
))}
</View>
{calendar.map((week, weekIndex) => (
<View key={weekIndex} style={styles.weekRow}>
{week.map((day, dayIndex) => {
const isSelected = day === selectedDay;
const isToday = day === today && month === new Date().getMonth() + 1 && year === new Date().getFullYear();
const isFutureDate = Boolean(day && dayjs(currentMonth).date(day).isAfter(dayjs(), 'day'));
return (
<View key={dayIndex} style={styles.dayContainer}>
{day && (
<TouchableOpacity
style={[
styles.dayButton,
isSelected && styles.dayButtonSelected,
isToday && styles.dayButtonToday
]}
onPress={() => !isFutureDate && day && onSelectDate(day)}
disabled={isFutureDate}
>
<View style={styles.dayContent}>
<Text style={[
styles.dayNumber,
isSelected && styles.dayNumberSelected,
isToday && styles.dayNumberToday,
isFutureDate && styles.dayNumberDisabled
]}>
{day.toString().padStart(2, '0')}
</Text>
{renderMoodRing(day, isSelected)}
</View>
</TouchableOpacity>
)}
</View>
);
})}
</View>
))}
</View>
{/* 选中日期的记录 */}
<View style={styles.selectedDateSection}>
<View style={styles.selectedDateHeader}>
<Text style={styles.selectedDateTitle}>
{selectedDay ? dayjs(currentMonth).date(selectedDay).format('YYYY年M月D日') : '请选择日期'}
</Text>
<TouchableOpacity
style={styles.addMoodButton}
onPress={openMoodEdit}
>
<Text style={styles.addMoodButtonText}></Text>
</TouchableOpacity>
</View>
{selectedDay ? (
selectedDateMood ? (
<TouchableOpacity
style={styles.moodRecord}
onPress={openMoodEdit}
>
<View style={styles.recordIcon}>
<View style={styles.moodIcon}>
<Image
source={moodOptions.find(m => m.type === selectedDateMood.moodType)?.image}
style={styles.moodIconImage}
/>
</View>
</View>
<View style={styles.recordContent}>
<Text style={styles.recordMood}>
{moodOptions.find(m => m.type === selectedDateMood.moodType)?.label}
</Text>
<Text style={styles.recordIntensity}>: {selectedDateMood.intensity}</Text>
{selectedDateMood.description && (
<Text style={styles.recordDescription}>{selectedDateMood.description}</Text>
)}
</View>
<View style={styles.spacer} />
<Text style={styles.recordTime}>
{dayjs(selectedDateMood.createdAt).format('HH:mm')}
</Text>
</TouchableOpacity>
) : (
<View style={styles.emptyRecord}>
<Text style={styles.emptyRecordText}></Text>
<Text style={styles.emptyRecordSubtext}>"记录"</Text>
</View>
)
) : (
<View style={styles.emptyRecord}>
<Text style={styles.emptyRecordText}></Text>
<Text style={styles.emptyRecordSubtext}>"记录"</Text>
</View>
)}
</View>
</ScrollView>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 40,
right: 20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#7a5af8',
opacity: 0.08,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#7a5af8',
opacity: 0.04,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
},
calendar: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16,
borderRadius: 20,
padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
},
monthNavigation: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
},
navButton: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: 'rgba(122,90,248,0.1)',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
navButtonText: {
fontSize: 24,
color: '#7a5af8',
fontWeight: '700',
},
monthTitle: {
fontSize: 20,
fontWeight: '800',
color: '#192126',
},
weekHeader: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 20,
},
weekDay: {
fontSize: 13,
color: '#5d6676',
textAlign: 'center',
width: (width - 96) / 7,
fontWeight: '600',
},
weekRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 16,
},
dayContainer: {
width: (width - 96) / 7,
alignItems: 'center',
},
dayButton: {
width: 44,
height: 44,
borderRadius: 22,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
backgroundColor: 'transparent',
},
dayButtonSelected: {
backgroundColor: '#FFFFFF',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 6,
elevation: 4,
},
dayButtonToday: {
borderWidth: 2,
borderColor: '#7a5af8',
},
dayContent: {
position: 'relative',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
dayNumber: {
fontSize: 14,
color: '#777f8c',
fontWeight: '600',
position: 'absolute',
top: 2,
zIndex: 1,
},
dayNumberSelected: {
color: '#192126',
fontWeight: '700',
},
dayNumberToday: {
color: '#7a5af8',
fontWeight: '700',
},
dayNumberDisabled: {
color: '#c0c4ca',
},
moodIconContainer: {
position: 'absolute',
bottom: 2,
width: 22,
height: 22,
borderRadius: 11,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 1,
},
todayMoodIconContainer: {
position: 'absolute',
bottom: 1,
width: 20,
height: 20,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
moodIcon: {
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: 'rgba(255,255,255,0.95)',
justifyContent: 'center',
alignItems: 'center',
},
moodIconImage: {
width: 28,
height: 28,
borderRadius: 9,
},
defaultMoodIcon: {
position: 'absolute',
bottom: 2,
width: 22,
height: 22,
borderRadius: 11,
borderWidth: 1.5,
borderColor: 'rgba(122,90,248,0.3)',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(122,90,248,0.05)',
},
todayDefaultMoodIcon: {
position: 'absolute',
bottom: 1,
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 1.5,
borderColor: 'rgba(122,90,248,0.4)',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(122,90,248,0.08)',
},
moodRingContainer: {
position: 'absolute',
bottom: 2,
width: 22,
height: 22,
justifyContent: 'center',
alignItems: 'center',
},
moodIntensityText: {
fontSize: 8,
fontWeight: '800',
textAlign: 'center',
position: 'absolute',
zIndex: 1,
textShadowColor: 'rgba(0,0,0,0.3)',
textShadowOffset: { width: 0, height: 0.5 },
textShadowRadius: 1,
},
selectedDateSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 16,
marginTop: 0,
borderRadius: 20,
padding: 20,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 6,
},
selectedDateHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
selectedDateTitle: {
fontSize: 22,
fontWeight: '800',
color: '#192126',
},
addMoodButton: {
paddingHorizontal: 20,
height: 36,
borderRadius: 18,
backgroundColor: '#7a5af8',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
elevation: 3,
},
addMoodButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '700',
},
moodRecord: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingVertical: 16,
backgroundColor: 'rgba(122,90,248,0.05)',
borderRadius: 16,
paddingHorizontal: 16,
},
recordIcon: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: '#e9e7f1ff',
justifyContent: 'center',
alignItems: 'center',
marginRight: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
elevation: 2,
},
recordContent: {
flex: 1,
},
recordMood: {
fontSize: 18,
color: '#192126',
fontWeight: '700',
marginBottom: 4,
},
recordIntensity: {
fontSize: 14,
color: '#5d6676',
marginTop: 2,
fontWeight: '500',
},
recordDescription: {
fontSize: 14,
color: '#5d6676',
marginTop: 6,
fontStyle: 'italic',
lineHeight: 20,
},
spacer: {
flex: 1,
},
recordTime: {
fontSize: 14,
color: '#777f8c',
fontWeight: '500',
},
emptyRecord: {
alignItems: 'center',
paddingVertical: 32,
},
emptyRecordText: {
fontSize: 16,
color: '#5d6676',
marginBottom: 8,
fontWeight: '600',
},
emptyRecordSubtext: {
fontSize: 13,
color: '#777f8c',
textAlign: 'center',
lineHeight: 18,
},
});