Files
digital-pilates/app/water/reminder-settings.tsx
richarjiang fbe0c92f0f feat(i18n): 全面实现应用核心功能模块的国际化支持
- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块
- 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本
- 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示
- 完善登录页、注销流程及权限申请弹窗的双语提示信息
- 优化部分页面的 UI 细节与字体样式以适配多语言显示
2025-11-27 17:54:36 +08:00

634 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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 { useColorScheme } from '@/hooks/useColorScheme';
import { WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterReminderSettings, setWaterReminderSettings as saveWaterReminderSettings } from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { Picker } from '@react-native-picker/picker';
import { LinearGradient } from 'expo-linear-gradient';
import { router } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
Switch,
Text,
TouchableOpacity,
View
} from 'react-native';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { useI18n } from '@/hooks/useI18n';
import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding';
const WaterReminderSettings: React.FC = () => {
const { t } = useI18n();
const safeAreaTop = useSafeAreaTop()
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const [startTimePickerVisible, setStartTimePickerVisible] = useState(false);
const [endTimePickerVisible, setEndTimePickerVisible] = useState(false);
// 喝水提醒相关状态
const [waterReminderSettings, setWaterReminderSettings] = useState({
enabled: false,
startTime: '08:00',
endTime: '22:00',
interval: 60,
});
// 时间选择器临时值
const [tempStartHour, setTempStartHour] = useState(8);
const [tempEndHour, setTempEndHour] = useState(22);
// 打开开始时间选择器
const openStartTimePicker = () => {
const currentHour = parseInt(waterReminderSettings.startTime.split(':')[0]);
setTempStartHour(currentHour);
setStartTimePickerVisible(true);
};
// 打开结束时间选择器
const openEndTimePicker = () => {
const currentHour = parseInt(waterReminderSettings.endTime.split(':')[0]);
setTempEndHour(currentHour);
setEndTimePickerVisible(true);
};
// 确认开始时间选择
const confirmStartTime = () => {
const newStartTime = `${String(tempStartHour).padStart(2, '0')}:00`;
// 检查时间合理性
if (isValidTimeRange(newStartTime, waterReminderSettings.endTime)) {
setWaterReminderSettings(prev => ({
...prev,
startTime: newStartTime
}));
setStartTimePickerVisible(false);
} else {
Alert.alert(
t('waterReminderSettings.alerts.timeValidation.title'),
t('waterReminderSettings.alerts.timeValidation.startTimeInvalid'),
[{ text: t('waterReminderSettings.buttons.confirm') }]
);
}
};
// 确认结束时间选择
const confirmEndTime = () => {
const newEndTime = `${String(tempEndHour).padStart(2, '0')}:00`;
// 检查时间合理性
if (isValidTimeRange(waterReminderSettings.startTime, newEndTime)) {
setWaterReminderSettings(prev => ({
...prev,
endTime: newEndTime
}));
setEndTimePickerVisible(false);
} else {
Alert.alert(
t('waterReminderSettings.alerts.timeValidation.title'),
t('waterReminderSettings.alerts.timeValidation.endTimeInvalid'),
[{ text: t('waterReminderSettings.buttons.confirm') }]
);
}
};
// 验证时间范围是否有效
const isValidTimeRange = (startTime: string, endTime: string): boolean => {
const [startHour] = startTime.split(':').map(Number);
const [endHour] = endTime.split(':').map(Number);
// 支持跨天的情况,如果结束时间小于开始时间,认为是跨天有效的
if (endHour < startHour) {
return true; // 跨天情况,如 22:00 到 08:00
}
// 同一天内,结束时间必须大于开始时间
return endHour > startHour;
};
// 处理喝水提醒配置保存
const handleWaterReminderSave = async () => {
try {
// 保存设置到本地存储
await saveWaterReminderSettings(waterReminderSettings);
// 设置或取消通知
// 这里使用 "用户" 作为默认用户名,实际项目中应该从用户状态获取
const userName = '用户';
await WaterNotificationHelpers.scheduleCustomWaterReminders(userName, waterReminderSettings);
if (waterReminderSettings.enabled) {
const timeInfo = `${waterReminderSettings.startTime}-${waterReminderSettings.endTime}`;
const intervalInfo = `${waterReminderSettings.interval}${t('waterReminderSettings.labels.minutes')}`;
Alert.alert(
t('waterReminderSettings.alerts.success.enabled'),
t('waterReminderSettings.alerts.success.enabledMessage', {
timeRange: timeInfo,
interval: intervalInfo
}),
[{ text: t('waterReminderSettings.buttons.confirm'), onPress: () => router.back() }]
);
} else {
Alert.alert(
t('waterReminderSettings.alerts.success.disabled'),
t('waterReminderSettings.alerts.success.disabledMessage'),
[{ text: t('waterReminderSettings.buttons.confirm'), onPress: () => router.back() }]
);
}
} catch (error) {
console.error('保存喝水提醒设置失败:', error);
Alert.alert(
t('waterReminderSettings.alerts.error.title'),
t('waterReminderSettings.alerts.error.message')
);
}
};
// 加载用户偏好设置
useEffect(() => {
const loadUserPreferences = async () => {
try {
// 加载喝水提醒设置
const reminderSettings = await getWaterReminderSettings();
setWaterReminderSettings(reminderSettings);
// 初始化时间选择器临时值
const startHour = parseInt(reminderSettings.startTime.split(':')[0]);
const endHour = parseInt(reminderSettings.endTime.split(':')[0]);
setTempStartHour(startHour);
setTempEndHour(endHour);
} catch (error) {
console.error('加载用户偏好设置失败:', error);
}
};
loadUserPreferences();
}, []);
return (
<View style={styles.container}>
{/* 背景渐变 */}
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<HeaderBar
title={t('waterReminderSettings.title')}
onBack={() => {
router.back();
}}
/>
<KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, {
paddingTop: safeAreaTop
}]}
showsVerticalScrollIndicator={false}
>
{/* 开启/关闭提醒 */}
<View style={styles.waterReminderSection}>
<View style={styles.waterReminderSectionHeader}>
<View style={styles.waterReminderSectionTitleContainer}>
<Ionicons name="notifications-outline" size={20} color={colorTokens.text} />
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>{t('waterReminderSettings.sections.notifications')}</Text>
</View>
<Switch
value={waterReminderSettings.enabled}
onValueChange={(enabled) => setWaterReminderSettings(prev => ({ ...prev, enabled }))}
trackColor={{ false: '#E5E5E5', true: '#3498DB' }}
thumbColor={waterReminderSettings.enabled ? '#FFFFFF' : '#FFFFFF'}
/>
</View>
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
{t('waterReminderSettings.descriptions.notifications')}
</Text>
</View>
{/* 时间段设置 */}
{waterReminderSettings.enabled && (
<>
<View style={styles.waterReminderSection}>
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>{t('waterReminderSettings.sections.timeRange')}</Text>
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
{t('waterReminderSettings.descriptions.timeRange')}
</Text>
<View style={styles.timeRangeContainer}>
{/* 开始时间 */}
<View style={styles.timePickerContainer}>
<Text style={[styles.timeLabel, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.startTime')}</Text>
<Pressable
style={[styles.timePicker, { backgroundColor: 'white' }]}
onPress={openStartTimePicker}
>
<Text style={[styles.timePickerText, { color: colorTokens.text }]}>{waterReminderSettings.startTime}</Text>
<Ionicons name="chevron-down" size={16} color={colorTokens.textSecondary} style={styles.timePickerIcon} />
</Pressable>
</View>
{/* 结束时间 */}
<View style={styles.timePickerContainer}>
<Text style={[styles.timeLabel, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.endTime')}</Text>
<Pressable
style={[styles.timePicker, { backgroundColor: 'white' }]}
onPress={openEndTimePicker}
>
<Text style={[styles.timePickerText, { color: colorTokens.text }]}>{waterReminderSettings.endTime}</Text>
<Ionicons name="chevron-down" size={16} color={colorTokens.textSecondary} style={styles.timePickerIcon} />
</Pressable>
</View>
</View>
</View>
{/* 提醒间隔设置 */}
<View style={styles.waterReminderSection}>
<Text style={[styles.waterReminderSectionTitle, { color: colorTokens.text }]}>{t('waterReminderSettings.sections.interval')}</Text>
<Text style={[styles.waterReminderSectionDesc, { color: colorTokens.textSecondary }]}>
{t('waterReminderSettings.descriptions.interval')}
</Text>
<View style={styles.intervalContainer}>
<View style={styles.intervalPickerContainer}>
<Picker
selectedValue={waterReminderSettings.interval}
onValueChange={(interval) => setWaterReminderSettings(prev => ({ ...prev, interval }))}
style={styles.intervalPicker}
>
{[30, 45, 60, 90, 120, 150, 180].map(interval => (
<Picker.Item key={interval} label={`${interval}${t('waterReminderSettings.labels.minutes')}`} value={interval} />
))}
</Picker>
</View>
</View>
</View>
</>
)}
{/* 保存按钮 */}
<View style={styles.saveButtonContainer}>
<TouchableOpacity
style={[styles.saveButton, { backgroundColor: colorTokens.primary }]}
onPress={handleWaterReminderSave}
activeOpacity={0.8}
>
<Text style={[styles.saveButtonText, { color: colorTokens.onPrimary }]}>{t('waterReminderSettings.labels.saveSettings')}</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
{/* 开始时间选择器弹窗 */}
<Modal
visible={startTimePickerVisible}
transparent
animationType="fade"
onRequestClose={() => setStartTimePickerVisible(false)}
>
<Pressable style={styles.modalBackdrop} onPress={() => setStartTimePickerVisible(false)} />
<View style={styles.timePickerModalSheet}>
<View style={styles.modalHandle} />
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.startTime')}</Text>
<View style={styles.timePickerContent}>
<View style={styles.timePickerSection}>
<Text style={[styles.timePickerLabel, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.hours')}</Text>
<View style={styles.hourPickerContainer}>
<Picker
selectedValue={tempStartHour}
onValueChange={(hour) => setTempStartHour(hour)}
style={styles.hourPicker}
>
{Array.from({ length: 24 }, (_, i) => (
<Picker.Item key={i} label={`${String(i).padStart(2, '0')}:00`} value={i} />
))}
</Picker>
</View>
</View>
<View style={styles.timeRangePreview}>
<Text style={[styles.timeRangePreviewLabel, { color: colorTokens.textSecondary }]}>{t('waterReminderSettings.labels.timeRangePreview')}</Text>
<Text style={[styles.timeRangePreviewText, { color: colorTokens.text }]}>
{String(tempStartHour).padStart(2, '0')}:00 - {waterReminderSettings.endTime}
</Text>
{!isValidTimeRange(`${String(tempStartHour).padStart(2, '0')}:00`, waterReminderSettings.endTime) && (
<Text style={styles.timeRangeWarning}> {t('waterReminderSettings.alerts.timeValidation.startTimeInvalid')}</Text>
)}
</View>
</View>
<View style={styles.modalActions}>
<Pressable
onPress={() => setStartTimePickerVisible(false)}
style={[styles.modalBtn, { backgroundColor: 'white' }]}
>
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>{t('waterReminderSettings.buttons.cancel')}</Text>
</Pressable>
<Pressable
onPress={confirmStartTime}
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>{t('waterReminderSettings.buttons.confirm')}</Text>
</Pressable>
</View>
</View>
</Modal>
{/* 结束时间选择器弹窗 */}
<Modal
visible={endTimePickerVisible}
transparent
animationType="fade"
onRequestClose={() => setEndTimePickerVisible(false)}
>
<Pressable style={styles.modalBackdrop} onPress={() => setEndTimePickerVisible(false)} />
<View style={styles.timePickerModalSheet}>
<View style={styles.modalHandle} />
<Text style={[styles.modalTitle, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.endTime')}</Text>
<View style={styles.timePickerContent}>
<View style={styles.timePickerSection}>
<Text style={[styles.timePickerLabel, { color: colorTokens.text }]}>{t('waterReminderSettings.labels.hours')}</Text>
<View style={styles.hourPickerContainer}>
<Picker
selectedValue={tempEndHour}
onValueChange={(hour) => setTempEndHour(hour)}
style={styles.hourPicker}
>
{Array.from({ length: 24 }, (_, i) => (
<Picker.Item key={i} label={`${String(i).padStart(2, '0')}:00`} value={i} />
))}
</Picker>
</View>
</View>
<View style={styles.timeRangePreview}>
<Text style={[styles.timeRangePreviewLabel, { color: colorTokens.textSecondary }]}>{t('waterReminderSettings.labels.timeRangePreview')}</Text>
<Text style={[styles.timeRangePreviewText, { color: colorTokens.text }]}>
{waterReminderSettings.startTime} - {String(tempEndHour).padStart(2, '0')}:00
</Text>
{!isValidTimeRange(waterReminderSettings.startTime, `${String(tempEndHour).padStart(2, '0')}:00`) && (
<Text style={styles.timeRangeWarning}> {t('waterReminderSettings.alerts.timeValidation.endTimeInvalid')}</Text>
)}
</View>
</View>
<View style={styles.modalActions}>
<Pressable
onPress={() => setEndTimePickerVisible(false)}
style={[styles.modalBtn, { backgroundColor: 'white' }]}
>
<Text style={[styles.modalBtnText, { color: colorTokens.text }]}>{t('waterReminderSettings.buttons.cancel')}</Text>
</Pressable>
<Pressable
onPress={confirmEndTime}
style={[styles.modalBtn, styles.modalBtnPrimary, { backgroundColor: colorTokens.primary }]}
>
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary, { color: colorTokens.onPrimary }]}>{t('waterReminderSettings.buttons.confirm')}</Text>
</Pressable>
</View>
</View>
</Modal>
</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: '#0EA5E9',
opacity: 0.1,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
keyboardAvoidingView: {
flex: 1,
},
scrollView: {
flex: 1,
},
scrollContent: {
padding: 20,
},
waterReminderSection: {
marginBottom: 32,
},
waterReminderSectionHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
},
waterReminderSectionTitleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
waterReminderSectionTitle: {
fontSize: 18,
fontWeight: '600',
},
waterReminderSectionDesc: {
fontSize: 14,
lineHeight: 20,
marginTop: 4,
},
timeRangeContainer: {
flexDirection: 'row',
gap: 16,
marginTop: 16,
},
timePickerContainer: {
flex: 1,
},
timeLabel: {
fontSize: 14,
fontWeight: '500',
marginBottom: 8,
},
timePicker: {
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
},
timePickerText: {
fontSize: 16,
fontWeight: '500',
},
timePickerIcon: {
opacity: 0.6,
},
intervalContainer: {
marginTop: 16,
},
intervalPickerContainer: {
backgroundColor: 'white',
borderRadius: 8,
overflow: 'hidden',
},
intervalPicker: {
height: 200,
},
saveButtonContainer: {
marginTop: 20,
marginBottom: 40,
},
saveButton: {
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
saveButtonText: {
fontSize: 16,
fontWeight: '600',
},
modalBackdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
timePickerModalSheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: 16,
backgroundColor: '#FFFFFF',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: '60%',
shadowColor: '#000000',
shadowOffset: { width: 0, height: -2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 16,
},
modalHandle: {
width: 36,
height: 4,
backgroundColor: '#E0E0E0',
borderRadius: 2,
alignSelf: 'center',
marginBottom: 20,
},
modalTitle: {
fontSize: 20,
fontWeight: '600',
textAlign: 'center',
marginBottom: 20,
},
timePickerContent: {
flex: 1,
marginBottom: 20,
},
timePickerSection: {
marginBottom: 20,
},
timePickerLabel: {
fontSize: 16,
fontWeight: '600',
marginBottom: 12,
textAlign: 'center',
},
hourPickerContainer: {
backgroundColor: '#F8F9FA',
borderRadius: 8,
overflow: 'hidden',
},
hourPicker: {
height: 160,
},
timeRangePreview: {
backgroundColor: '#F0F8FF',
borderRadius: 8,
padding: 16,
marginTop: 16,
alignItems: 'center',
},
timeRangePreviewLabel: {
fontSize: 12,
fontWeight: '500',
marginBottom: 4,
},
timeRangePreviewText: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
},
timeRangeWarning: {
fontSize: 12,
color: '#FF6B6B',
textAlign: 'center',
lineHeight: 18,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
gap: 12,
},
modalBtn: {
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 10,
minWidth: 80,
alignItems: 'center',
},
modalBtnPrimary: {
// backgroundColor will be set dynamically
},
modalBtnText: {
fontSize: 16,
fontWeight: '600',
},
modalBtnTextPrimary: {
// color will be set dynamically
},
});
export default WaterReminderSettings;