feat: 更新心情记录功能和界面
- 调整启动画面中的图片宽度,提升视觉效果 - 移除引导页面相关组件,简化应用结构 - 新增心情统计页面,支持用户查看和分析心情数据 - 优化心情卡片组件,增强用户交互体验 - 更新登录页面标题,提升品牌一致性 - 新增心情日历和编辑功能,支持用户记录和管理心情
This commit is contained in:
586
app/mood/calendar.tsx
Normal file
586
app/mood/calendar.tsx
Normal 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',
|
||||
},
|
||||
|
||||
});
|
||||
444
app/mood/edit.tsx
Normal file
444
app/mood/edit.tsx
Normal file
@@ -0,0 +1,444 @@
|
||||
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
|
||||
} 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,
|
||||
} from 'react-native';
|
||||
|
||||
export default function MoodEditScreen() {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
const params = useLocalSearchParams();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { date, moodId } = params;
|
||||
const selectedDate = date as string || dayjs().format('YYYY-MM-DD');
|
||||
|
||||
const [selectedMood, setSelectedMood] = useState<MoodType | ''>('');
|
||||
const [intensity, setIntensity] = useState(5);
|
||||
const [description, setDescription] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [existingMood, setExistingMood] = useState<any>(null);
|
||||
|
||||
const moodOptions = getMoodOptions();
|
||||
|
||||
// 从 Redux 获取数据
|
||||
const moodRecords = useAppSelector(selectMoodRecordsByDate(selectedDate));
|
||||
const loading = useAppSelector(state => state.mood.loading);
|
||||
|
||||
// 初始化数据
|
||||
useEffect(() => {
|
||||
// 加载当前日期的心情记录
|
||||
dispatch(fetchDailyMoodCheckins(selectedDate));
|
||||
}, [selectedDate, dispatch]);
|
||||
|
||||
// 当 moodRecords 更新时,查找现有记录
|
||||
useEffect(() => {
|
||||
if (moodId && moodRecords.length > 0) {
|
||||
const mood = moodRecords.find((c: any) => c.id === moodId) || moodRecords[0];
|
||||
setExistingMood(mood);
|
||||
setSelectedMood(mood.moodType);
|
||||
setIntensity(mood.intensity);
|
||||
setDescription(mood.description || '');
|
||||
}
|
||||
}, [moodId, moodRecords]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedMood) {
|
||||
Alert.alert('提示', '请选择心情');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (existingMood) {
|
||||
// 更新现有记录
|
||||
await dispatch(updateMoodRecord({
|
||||
id: existingMood.id,
|
||||
moodType: selectedMood,
|
||||
intensity,
|
||||
description: description.trim() || undefined,
|
||||
})).unwrap();
|
||||
} else {
|
||||
// 创建新记录
|
||||
await dispatch(createMoodRecord({
|
||||
moodType: selectedMood,
|
||||
intensity,
|
||||
description: description.trim() || undefined,
|
||||
checkinDate: selectedDate,
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
Alert.alert('成功', existingMood ? '心情记录已更新' : '心情记录已保存', [
|
||||
{ text: '确定', onPress: () => router.back() }
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('保存心情失败:', error);
|
||||
Alert.alert('错误', '保存心情失败,请重试');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!existingMood) return;
|
||||
|
||||
Alert.alert(
|
||||
'确认删除',
|
||||
'确定要删除这条心情记录吗?',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await dispatch(deleteMoodRecord({ id: existingMood.id })).unwrap();
|
||||
|
||||
Alert.alert('成功', '心情记录已删除', [
|
||||
{ text: '确定', onPress: () => router.back() }
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('删除心情失败:', error);
|
||||
Alert.alert('错误', '删除心情失败,请重试');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
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 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}>
|
||||
{existingMood ? '编辑心情' : '记录心情'}
|
||||
</Text>
|
||||
<View style={styles.headerSpacer} />
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content}>
|
||||
{/* 日期显示 */}
|
||||
<View style={styles.dateSection}>
|
||||
<Text style={styles.dateTitle}>
|
||||
{dayjs(selectedDate).format('YYYY年M月D日')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 心情选择 */}
|
||||
<View style={styles.moodSection}>
|
||||
<Text style={styles.sectionTitle}>选择心情</Text>
|
||||
<View style={styles.moodOptions}>
|
||||
{moodOptions.map((mood, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[
|
||||
styles.moodOption,
|
||||
selectedMood === mood.type && styles.selectedMoodOption
|
||||
]}
|
||||
onPress={() => setSelectedMood(mood.type)}
|
||||
>
|
||||
<Text style={styles.moodEmoji}>{mood.emoji}</Text>
|
||||
<Text style={styles.moodLabel}>{mood.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 心情强度选择 */}
|
||||
{selectedMood && (
|
||||
<View style={styles.intensitySection}>
|
||||
<Text style={styles.sectionTitle}>心情强度</Text>
|
||||
{renderIntensitySlider()}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 心情描述 */}
|
||||
{selectedMood && (
|
||||
<View style={styles.descriptionSection}>
|
||||
<Text style={styles.sectionTitle}>心情描述(可选)</Text>
|
||||
<TextInput
|
||||
style={styles.descriptionInput}
|
||||
placeholder="描述一下你的心情..."
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
maxLength={200}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
<Text style={styles.characterCount}>{description.length}/200</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<View style={styles.footer}>
|
||||
{existingMood && (
|
||||
<TouchableOpacity
|
||||
style={[styles.deleteButton, isDeleting && styles.disabledButton]}
|
||||
onPress={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Text style={styles.deleteButtonText}>
|
||||
{isDeleting ? '删除中...' : '删除记录'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[styles.saveButton, (!selectedMood || isLoading) && styles.disabledButton]}
|
||||
onPress={handleSave}
|
||||
disabled={!selectedMood || isLoading}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</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,
|
||||
},
|
||||
dateSection: {
|
||||
backgroundColor: '#fff',
|
||||
margin: 16,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
dateTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#192126',
|
||||
},
|
||||
moodSection: {
|
||||
backgroundColor: '#fff',
|
||||
margin: 16,
|
||||
marginTop: 0,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 16,
|
||||
},
|
||||
moodOptions: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
moodOption: {
|
||||
width: '30%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
marginBottom: 16,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#f8f8f8',
|
||||
},
|
||||
selectedMoodOption: {
|
||||
backgroundColor: '#e8f5e8',
|
||||
borderWidth: 2,
|
||||
borderColor: '#4CAF50',
|
||||
},
|
||||
moodEmoji: {
|
||||
fontSize: 24,
|
||||
marginBottom: 8,
|
||||
},
|
||||
moodLabel: {
|
||||
fontSize: 14,
|
||||
color: '#333',
|
||||
},
|
||||
intensitySection: {
|
||||
backgroundColor: '#fff',
|
||||
margin: 16,
|
||||
marginTop: 0,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
intensityContainer: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
intensityLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
marginBottom: 12,
|
||||
},
|
||||
intensitySlider: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
},
|
||||
intensityDot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#ddd',
|
||||
},
|
||||
intensityDotActive: {
|
||||
backgroundColor: '#4CAF50',
|
||||
},
|
||||
intensityLabels: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
},
|
||||
intensityLabelText: {
|
||||
fontSize: 12,
|
||||
color: '#666',
|
||||
},
|
||||
descriptionSection: {
|
||||
backgroundColor: '#fff',
|
||||
margin: 16,
|
||||
marginTop: 0,
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
},
|
||||
descriptionInput: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
minHeight: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
characterCount: {
|
||||
fontSize: 12,
|
||||
color: '#999',
|
||||
textAlign: 'right',
|
||||
marginTop: 4,
|
||||
},
|
||||
footer: {
|
||||
padding: 16,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: '#4CAF50',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
deleteButton: {
|
||||
backgroundColor: '#F44336',
|
||||
borderRadius: 12,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
disabledButton: {
|
||||
backgroundColor: '#ccc',
|
||||
},
|
||||
saveButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user