Files
digital-pilates/app/mood/edit.tsx
richarjiang 83b77615cf feat: Enhance Oxygen Saturation Card with health permissions and loading state management
feat(i18n): Add common translations and mood-related strings in English and Chinese

fix(i18n): Update metabolism titles for consistency in health translations

chore: Update Podfile.lock to include SDWebImage 5.21.4 and other dependency versions

refactor(moodCheckins): Improve mood configuration retrieval with optional translation support

refactor(sleepHealthKit): Replace useI18n with direct i18n import for sleep quality descriptions
2025-11-28 23:48:38 +08:00

531 lines
14 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 { useI18n } from '@/hooks/useI18n';
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 { t } = useI18n();
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(t);
// 从 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(t('common.alert'), t('mood.edit.alerts.selectMood'));
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(t('common.success'), existingMood ? t('mood.edit.alerts.updateSuccess') : t('mood.edit.alerts.saveSuccess'), [
{ text: t('common.confirm'), onPress: () => router.back() }
]);
} catch (error) {
console.error('保存心情失败:', error);
Alert.alert(t('common.error'), t('mood.edit.alerts.saveError'));
} finally {
setIsLoading(false);
}
};
const handleDelete = async () => {
if (!existingMood) return;
Alert.alert(
t('mood.edit.alerts.confirmDeleteTitle'),
t('mood.edit.alerts.confirmDelete'),
[
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('common.delete'),
style: 'destructive',
onPress: async () => {
try {
setIsDeleting(true);
await dispatch(deleteMoodRecord({ id: existingMood.id })).unwrap();
Alert.alert(t('common.success'), t('mood.edit.alerts.deleteSuccess'), [
{ text: t('common.confirm'), onPress: () => router.back() }
]);
} catch (error) {
console.error('删除心情失败:', error);
Alert.alert(t('common.error'), t('mood.edit.alerts.deleteError'));
} 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 ? t('mood.edit.editTitle') : t('mood.edit.title')}
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(t('mood.edit.dateFormat'))}
</Text>
</View>
{/* 心情选择 */}
<View style={styles.moodSection}>
<Text style={styles.sectionTitle}>{t('mood.edit.selectMood')}</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}>{t('mood.edit.intensity')}</Text>
<MoodIntensitySlider
value={intensity}
onValueChange={handleIntensityChange}
min={1}
max={10}
width={280}
height={12}
/>
</View>
{/* 心情描述 */}
<View style={styles.descriptionSection}>
<Text style={styles.sectionTitle}>{t('mood.edit.diary')}</Text>
<Text style={styles.diarySubtitle}>{t('mood.edit.diarySubtitle')}</Text>
<TextInput
ref={textInputRef}
style={styles.descriptionInput}
placeholder={t('mood.edit.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 ? t('mood.edit.saving') : existingMood ? t('mood.edit.update') : t('mood.edit.save')}
</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',
},
});