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
531 lines
14 KiB
TypeScript
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',
|
|
},
|
|
});
|