Files
digital-pilates/app/mood/edit.tsx
richarjiang 72e75b602e feat: 更新心情记录功能和界面
- 调整启动画面中的图片宽度,提升视觉效果
- 移除引导页面相关组件,简化应用结构
- 新增心情统计页面,支持用户查看和分析心情数据
- 优化心情卡片组件,增强用户交互体验
- 更新登录页面标题,提升品牌一致性
- 新增心情日历和编辑功能,支持用户记录和管理心情
2025-08-21 17:59:22 +08:00

445 lines
12 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 { 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',
},
});