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

444
app/mood/edit.tsx Normal file
View 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',
},
});