Files
digital-pilates/app/mood/edit.tsx
2025-10-14 16:31:19 +08:00

535 lines
14 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 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 { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
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, useRef, useState } from 'react';
import {
Alert, Image,
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
export default function MoodEditScreen() {
const safeAreaTop = useSafeAreaTop()
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 scrollViewRef = useRef<ScrollView>(null);
const textInputRef = useRef<TextInput>(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]);
// 键盘事件监听器
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
// 键盘出现时,延迟滚动到文本输入框
setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100);
});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
// 键盘隐藏时,可以进行必要的调整
});
return () => {
keyboardDidShowListener?.remove();
keyboardDidHideListener?.remove();
};
}, []);
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} />
<View style={styles.safeArea} >
<HeaderBar
title={existingMood ? '编辑心情' : '记录心情'}
onBack={() => router.back()}
withSafeTop={false}
transparent={true}
tone="light"
/>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.keyboardAvoidingView}
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
>
<ScrollView
ref={scrollViewRef}
style={styles.content}
contentContainerStyle={[styles.scrollContent, {
paddingTop: safeAreaTop
}]}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* 日期显示 */}
<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={280}
height={12}
/>
</View>
{/* 心情描述 */}
<View style={styles.descriptionSection}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.diarySubtitle}></Text>
<TextInput
ref={textInputRef}
style={styles.descriptionInput}
placeholder={`今天的心情如何?
你经历过什么特别的事情吗?
有什么让你开心的事?
或者,有什么让你感到困扰?
写下你的感受,让这些时刻成为你珍贵的记忆...`}
placeholderTextColor="#a8a8a8"
value={description}
onChangeText={setDescription}
multiline
maxLength={1000}
textAlignVertical="top"
onFocus={() => {
// 当文本输入框获得焦点时,滚动到输入框
setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 300);
}}
/>
<Text style={styles.characterCount}>{description.length}/1000</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* 底部按钮 */}
<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={20} color="#f95555" />
</TouchableOpacity>
)}
</View>
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
decorativeCircle1: {
position: 'absolute',
top: 30,
right: 15,
width: 45,
height: 45,
borderRadius: 22.5,
backgroundColor: '#7a5af8',
opacity: 0.06,
},
decorativeCircle2: {
position: 'absolute',
bottom: -10,
left: -10,
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: '#7a5af8',
opacity: 0.04,
},
safeArea: {
flex: 1,
},
keyboardAvoidingView: {
flex: 1,
},
content: {
flex: 1,
},
scrollContent: {
paddingBottom: 100, // 为底部按钮留出空间
},
dateSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 12,
borderRadius: 16,
padding: 16,
alignItems: 'center',
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
dateTitle: {
fontSize: 20,
fontWeight: '700',
color: '#192126',
},
moodSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 12,
marginTop: 0,
borderRadius: 16,
padding: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#192126',
marginBottom: 16,
},
moodOptions: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
moodOption: {
width: '18%',
alignItems: 'center',
paddingVertical: 12,
marginBottom: 8,
borderRadius: 12,
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.12,
shadowRadius: 3,
elevation: 2,
},
moodImage: {
width: 32,
height: 32,
marginBottom: 6,
},
moodLabel: {
fontSize: 12,
color: '#192126',
fontWeight: '500',
},
intensitySection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 12,
marginTop: 0,
borderRadius: 16,
padding: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
descriptionSection: {
backgroundColor: 'rgba(255,255,255,0.95)',
margin: 12,
marginTop: 0,
borderRadius: 16,
padding: 16,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
diarySubtitle: {
fontSize: 13,
color: '#666',
fontWeight: '500',
marginBottom: 12,
lineHeight: 18,
},
descriptionInput: {
borderWidth: 1.5,
borderColor: 'rgba(122,90,248,0.2)',
borderRadius: 10,
padding: 12,
fontSize: 14,
minHeight: 120,
textAlignVertical: 'top',
backgroundColor: 'rgba(122,90,248,0.02)',
color: '#192126',
lineHeight: 20,
},
characterCount: {
fontSize: 12,
color: '#777f8c',
textAlign: 'right',
marginTop: 8,
fontWeight: '500',
},
footer: {
padding: 12,
position: 'absolute',
bottom: 20,
right: 8,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
},
saveButton: {
backgroundColor: '#7a5af8',
borderRadius: 10,
paddingVertical: 10,
paddingHorizontal: 20,
alignItems: 'center',
marginLeft: 8,
shadowColor: '#7a5af8',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 3,
elevation: 2,
},
deleteIconButton: {
width: 32,
height: 32,
borderRadius: 16,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 8,
},
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: 13,
fontWeight: '600',
},
deleteButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '600',
},
});