- 在心情日历中新增心情圆环展示,显示心情强度 - 修改心情记录编辑页面,支持使用图标替代表情 - 优化心情类型配置,使用图片资源替代原有表情 - 新增多种心情图标,丰富用户选择 - 更新相关样式,提升用户体验和界面美观性 - 更新文档,详细描述新功能和使用方法
466 lines
12 KiB
TypeScript
466 lines
12 KiB
TypeScript
import MoodIntensitySlider from '@/components/MoodIntensitySlider';
|
||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
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 { Ionicons } from '@expo/vector-icons';
|
||
import dayjs from 'dayjs';
|
||
import { LinearGradient } from 'expo-linear-gradient';
|
||
import { router, useLocalSearchParams } from 'expo-router';
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
Alert, Image,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TextInput,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
|
||
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 handleIntensityChange = (value: number) => {
|
||
setIntensity(value);
|
||
};
|
||
|
||
// 使用统一的渐变背景色
|
||
const backgroundGradientColors = [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd] as const;
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<LinearGradient
|
||
colors={['#fafaff', '#f4f3ff']} // 使用紫色主题的浅色渐变
|
||
style={styles.gradientBackground}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 0, y: 1 }}
|
||
/>
|
||
|
||
{/* 装饰性圆圈 */}
|
||
<View style={styles.decorativeCircle1} />
|
||
<View style={styles.decorativeCircle2} />
|
||
<SafeAreaView style={styles.safeArea} edges={['top']}>
|
||
<HeaderBar
|
||
title={existingMood ? '编辑心情' : '记录心情'}
|
||
onBack={() => router.back()}
|
||
withSafeTop={false}
|
||
transparent={true}
|
||
tone="light"
|
||
/>
|
||
|
||
<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)}
|
||
>
|
||
<Image source={mood.image} style={styles.moodImage} />
|
||
<Text style={styles.moodLabel}>{mood.label}</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 心情强度选择 */}
|
||
<View style={styles.intensitySection}>
|
||
<Text style={styles.sectionTitle}>心情强度</Text>
|
||
<MoodIntensitySlider
|
||
value={intensity}
|
||
onValueChange={handleIntensityChange}
|
||
min={1}
|
||
max={10}
|
||
width={320}
|
||
/>
|
||
</View>
|
||
|
||
|
||
{/* 心情描述 */}
|
||
{selectedMood && (
|
||
<View style={styles.descriptionSection}>
|
||
<Text style={styles.sectionTitle}>心情描述(可选)</Text>
|
||
<TextInput
|
||
style={styles.descriptionInput}
|
||
placeholder="描述一下你的心情..."
|
||
placeholderTextColor="#777f8c"
|
||
value={description}
|
||
onChangeText={setDescription}
|
||
multiline
|
||
maxLength={200}
|
||
textAlignVertical="top"
|
||
/>
|
||
<Text style={styles.characterCount}>{description.length}/200</Text>
|
||
</View>
|
||
)}
|
||
</ScrollView>
|
||
|
||
{/* 底部按钮 */}
|
||
<View style={styles.footer}>
|
||
<View style={styles.buttonRow}>
|
||
|
||
<TouchableOpacity
|
||
style={[styles.saveButton, (!selectedMood || isLoading) && styles.disabledButton]}
|
||
onPress={handleSave}
|
||
disabled={!selectedMood || isLoading}
|
||
>
|
||
<Text style={styles.saveButtonText}>
|
||
{isLoading ? '保存中...' : existingMood ? '更新心情' : '保存心情'}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
{existingMood && (
|
||
<TouchableOpacity
|
||
style={[styles.deleteIconButton, isDeleting && styles.disabledButton]}
|
||
onPress={handleDelete}
|
||
disabled={isDeleting}
|
||
>
|
||
<Ionicons name="trash-outline" size={24} color="#f95555" />
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
</View>
|
||
</SafeAreaView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
},
|
||
gradientBackground: {
|
||
position: 'absolute',
|
||
left: 0,
|
||
right: 0,
|
||
top: 0,
|
||
bottom: 0,
|
||
},
|
||
decorativeCircle1: {
|
||
position: 'absolute',
|
||
top: 40,
|
||
right: 20,
|
||
width: 60,
|
||
height: 60,
|
||
borderRadius: 30,
|
||
backgroundColor: '#7a5af8',
|
||
opacity: 0.08,
|
||
},
|
||
decorativeCircle2: {
|
||
position: 'absolute',
|
||
bottom: -15,
|
||
left: -15,
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 20,
|
||
backgroundColor: '#7a5af8',
|
||
opacity: 0.04,
|
||
},
|
||
safeArea: {
|
||
flex: 1,
|
||
},
|
||
|
||
content: {
|
||
flex: 1,
|
||
},
|
||
dateSection: {
|
||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||
margin: 16,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
alignItems: 'center',
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
dateTitle: {
|
||
fontSize: 26,
|
||
fontWeight: '800',
|
||
color: '#192126',
|
||
},
|
||
moodSection: {
|
||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||
margin: 16,
|
||
marginTop: 0,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 20,
|
||
fontWeight: '700',
|
||
color: '#192126',
|
||
marginBottom: 20,
|
||
},
|
||
moodOptions: {
|
||
flexDirection: 'row',
|
||
flexWrap: 'wrap',
|
||
justifyContent: 'space-between',
|
||
},
|
||
moodOption: {
|
||
width: '18%',
|
||
alignItems: 'center',
|
||
paddingVertical: 16,
|
||
marginBottom: 12,
|
||
borderRadius: 16,
|
||
backgroundColor: 'rgba(122,90,248,0.05)',
|
||
borderWidth: 1,
|
||
borderColor: 'rgba(122,90,248,0.1)',
|
||
},
|
||
selectedMoodOption: {
|
||
backgroundColor: 'rgba(122,90,248,0.15)',
|
||
borderWidth: 2,
|
||
borderColor: '#7a5af8',
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.15,
|
||
shadowRadius: 4,
|
||
elevation: 2,
|
||
},
|
||
moodImage: {
|
||
width: 40,
|
||
height: 40,
|
||
marginBottom: 10,
|
||
},
|
||
moodLabel: {
|
||
fontSize: 14,
|
||
color: '#192126',
|
||
fontWeight: '600',
|
||
},
|
||
intensitySection: {
|
||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||
margin: 16,
|
||
marginTop: 0,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
|
||
descriptionSection: {
|
||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||
margin: 16,
|
||
marginTop: 0,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 12,
|
||
elevation: 6,
|
||
},
|
||
descriptionInput: {
|
||
borderWidth: 1.5,
|
||
borderColor: 'rgba(122,90,248,0.2)',
|
||
borderRadius: 12,
|
||
padding: 16,
|
||
fontSize: 16,
|
||
minHeight: 100,
|
||
textAlignVertical: 'top',
|
||
backgroundColor: 'rgba(122,90,248,0.02)',
|
||
color: '#192126',
|
||
},
|
||
characterCount: {
|
||
fontSize: 12,
|
||
color: '#777f8c',
|
||
textAlign: 'right',
|
||
marginTop: 8,
|
||
fontWeight: '500',
|
||
},
|
||
footer: {
|
||
padding: 16,
|
||
position: 'absolute',
|
||
bottom: 24,
|
||
right: 8,
|
||
},
|
||
buttonRow: {
|
||
flexDirection: 'row',
|
||
justifyContent: 'flex-end',
|
||
alignItems: 'center',
|
||
},
|
||
saveButton: {
|
||
backgroundColor: '#7a5af8',
|
||
borderRadius: 12,
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 24,
|
||
alignItems: 'center',
|
||
marginLeft: 12,
|
||
shadowColor: '#7a5af8',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.2,
|
||
shadowRadius: 4,
|
||
elevation: 3,
|
||
},
|
||
deleteIconButton: {
|
||
width: 36,
|
||
height: 36,
|
||
borderRadius: 18,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
marginLeft: 12,
|
||
},
|
||
deleteButton: {
|
||
backgroundColor: '#f95555',
|
||
borderRadius: 12,
|
||
paddingVertical: 12,
|
||
paddingHorizontal: 24,
|
||
alignItems: 'center',
|
||
shadowColor: '#f95555',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.2,
|
||
shadowRadius: 4,
|
||
elevation: 3,
|
||
},
|
||
disabledButton: {
|
||
backgroundColor: '#c0c4ca',
|
||
shadowOpacity: 0,
|
||
elevation: 0,
|
||
},
|
||
saveButtonText: {
|
||
color: '#fff',
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
},
|
||
deleteButtonText: {
|
||
color: '#fff',
|
||
fontSize: 14,
|
||
fontWeight: '600',
|
||
},
|
||
});
|