feat: 更新心情记录功能和界面

- 调整启动画面中的图片宽度,提升视觉效果
- 移除引导页面相关组件,简化应用结构
- 新增心情统计页面,支持用户查看和分析心情数据
- 优化心情卡片组件,增强用户交互体验
- 更新登录页面标题,提升品牌一致性
- 新增心情日历和编辑功能,支持用户记录和管理心情
This commit is contained in:
richarjiang
2025-08-21 17:59:22 +08:00
parent a7607e0f74
commit 72e75b602e
24 changed files with 2964 additions and 1238 deletions

586
app/mood/calendar.tsx Normal file
View File

@@ -0,0 +1,586 @@
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}>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<Text style={styles.backButton}></Text>
</TouchableOpacity>
<Text style={styles.headerTitle}></Text>
<View style={styles.headerSpacer} />
</View>
<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,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingVertical: 16,
},
backButton: {
fontSize: 24,
color: '#666',
},
headerTitle: {
fontSize: 20,
fontWeight: '600',
color: '#333',
flex: 1,
textAlign: 'center',
},
headerSpacer: {
width: 24,
},
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: '#4CAF50',
},
dayButtonToday: {
borderWidth: 2,
borderColor: '#4CAF50',
},
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: '#4CAF50',
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: '#4CAF50',
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: '#4CAF50',
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',
},
});