Files
digital-pilates/app/mood/calendar.tsx
richarjiang 098c65b23e feat: 更新心情相关页面和组件
- 在心情统计、日历和编辑页面中引入 HeaderBar 组件,提升界面一致性和用户体验
- 移除冗余的头部视图代码,简化组件结构
- 更新心情日历和编辑页面的样式,使用新的颜色常量,增强视觉效果
- 优化心情统计页面的加载状态显示,提升用户交互体验
2025-08-21 19:09:02 +08:00

568 lines
16 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 { useColorScheme } from '@/hooks/useColorScheme';
import { useMoodData } from '@/hooks/useMoodData';
import { getMoodOptions } from '@/services/moodCheckins';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useLocalSearchParams } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Dimensions,
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 { fetchMoodRecords, fetchMoodHistoryRecords } = useMoodData();
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);
const [selectedDateMood, setSelectedDateMood] = useState<any>(null);
const [moodRecords, setMoodRecords] = useState<Record<string, any[]>>({});
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);
// 初始化选中日期
useEffect(() => {
if (selectedDate) {
const date = dayjs(selectedDate as string);
setCurrentMonth(date.toDate());
setSelectedDay(date.date());
const dateString = date.format('YYYY-MM-DD');
loadDailyMoodCheckins(dateString);
} else {
const today = new Date();
setCurrentMonth(today);
setSelectedDay(today.getDate());
const dateString = dayjs().format('YYYY-MM-DD');
loadDailyMoodCheckins(dateString);
}
loadMonthMoodData(currentMonth);
}, [selectedDate]);
// 加载整个月份的心情数据
const loadMonthMoodData = 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 });
// 将历史记录按日期分组
const monthData: Record<string, any[]> = {};
historyData.forEach(checkin => {
const date = checkin.checkinDate;
if (!monthData[date]) {
monthData[date] = [];
}
monthData[date].push(checkin);
});
setMoodRecords(monthData);
} catch (error) {
console.error('加载月份心情数据失败:', error);
}
};
// 加载选中日期的心情记录
const loadDailyMoodCheckins = async (dateString: string) => {
try {
const checkins = await fetchMoodRecords(dateString);
if (checkins.length > 0) {
setSelectedDateMood(checkins[0]); // 取最新的记录
} else {
setSelectedDateMood(null);
}
} catch (error) {
console.error('加载心情记录失败:', error);
setSelectedDateMood(null);
}
};
// 月份切换函数
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 renderMoodIcon = (day: number | null, isSelected: boolean) => {
if (!day) return null;
// 检查该日期是否有心情记录
const dayDateString = dayjs(currentMonth).date(day).format('YYYY-MM-DD');
const dayRecords = moodRecords[dayDateString] || [];
const moodRecord = dayRecords.length > 0 ? dayRecords[0] : null;
if (moodRecord) {
const mood = moodOptions.find(m => m.type === moodRecord.moodType);
return (
<View style={[styles.moodIconContainer, { backgroundColor: mood?.color }]}>
<View style={styles.moodIcon}>
<Text style={styles.moodEmoji}>{mood?.emoji || '😊'}</Text>
</View>
</View>
);
}
return (
<View style={styles.defaultMoodIcon}>
<Text style={styles.defaultMoodEmoji}>😊</Text>
</View>
);
};
// 使用统一的渐变背景色
const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const;
return (
<View style={styles.container}>
<LinearGradient
colors={backgroundGradientColors}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<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>
{renderMoodIcon(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}>
<Text style={styles.moodEmoji}>
{moodOptions.find(m => m.type === selectedDateMood.moodType)?.emoji || '😊'}
</Text>
</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,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
},
calendar: {
backgroundColor: '#fff',
margin: 16,
borderRadius: 16,
padding: 16,
},
monthNavigation: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
navButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#f8f9fa',
justifyContent: 'center',
alignItems: 'center',
},
navButtonText: {
fontSize: 20,
color: '#333',
fontWeight: '600',
},
monthTitle: {
fontSize: 18,
fontWeight: '700',
color: '#192126',
},
weekHeader: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 16,
},
weekDay: {
fontSize: 14,
color: '#666',
textAlign: 'center',
width: (width - 96) / 7,
},
weekRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: 16,
},
dayContainer: {
width: (width - 96) / 7,
alignItems: 'center',
},
dayButton: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 8,
},
dayButtonSelected: {
backgroundColor: Colors.light.accentGreen,
},
dayButtonToday: {
borderWidth: 2,
borderColor: Colors.light.accentGreen,
},
dayContent: {
position: 'relative',
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
dayNumber: {
fontSize: 14,
color: '#999',
fontWeight: '500',
position: 'absolute',
top: 2,
zIndex: 1,
},
dayNumberSelected: {
color: '#FFFFFF',
fontWeight: '600',
},
dayNumberToday: {
color: Colors.light.accentGreen,
fontWeight: '600',
},
dayNumberDisabled: {
color: '#ccc',
},
moodIconContainer: {
position: 'absolute',
bottom: 2,
width: 20,
height: 20,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
moodIcon: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: 'rgba(255,255,255,0.9)',
justifyContent: 'center',
alignItems: 'center',
},
moodEmoji: {
fontSize: 12,
},
defaultMoodIcon: {
position: 'absolute',
bottom: 2,
width: 20,
height: 20,
borderRadius: 10,
borderWidth: 1,
borderColor: '#ddd',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
},
defaultMoodEmoji: {
fontSize: 10,
opacity: 0.3,
},
selectedDateSection: {
backgroundColor: '#fff',
margin: 16,
marginTop: 0,
borderRadius: 16,
padding: 16,
},
selectedDateHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
selectedDateTitle: {
fontSize: 20,
fontWeight: '700',
color: '#192126',
},
addMoodButton: {
paddingHorizontal: 16,
height: 32,
borderRadius: 16,
backgroundColor: Colors.light.accentGreen,
justifyContent: 'center',
alignItems: 'center',
},
addMoodButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
moodRecord: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingVertical: 12,
},
recordIcon: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: Colors.light.accentGreen,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
recordContent: {
flex: 1,
},
recordMood: {
fontSize: 16,
color: '#333',
fontWeight: '500',
},
recordIntensity: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
recordDescription: {
fontSize: 14,
color: '#666',
marginTop: 4,
fontStyle: 'italic',
},
spacer: {
flex: 1,
},
recordTime: {
fontSize: 14,
color: '#999',
},
emptyRecord: {
alignItems: 'center',
paddingVertical: 20,
},
emptyRecordText: {
fontSize: 16,
color: '#666',
marginBottom: 8,
},
emptyRecordSubtext: {
fontSize: 12,
color: '#999',
},
});