feat: add nutrition and mood reminder settings

- Implemented nutrition and mood reminder toggles in notification settings screen.
- Added corresponding utility functions for managing nutrition and mood reminder preferences.
- Updated user preferences interface to include nutrition and mood reminder states.
- Enhanced localization for new reminder settings and alerts.
- Incremented iOS app version to 1.0.30.
This commit is contained in:
2025-11-23 22:47:54 +08:00
parent bcb910140e
commit 8cbf6be50a
7 changed files with 359 additions and 151 deletions

View File

@@ -24,7 +24,7 @@ import { createWaterRecordAction } from '@/store/waterSlice';
import { loadActiveFastingSchedule } from '@/utils/fasting';
import { initializeHealthPermissions } from '@/utils/health';
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
import { getWaterReminderSettings } from '@/utils/userPreferences';
import { getMoodReminderEnabled, getNutritionReminderEnabled, getWaterReminderSettings } from '@/utils/userPreferences';
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
import React, { useEffect } from 'react';
import { AppState, AppStateStatus } from 'react-native';
@@ -221,40 +221,57 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
try {
logger.info('📢 开始批量注册通知提醒...');
// 并行注册所有通知,提高效率
await Promise.all([
// 营养提醒
NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() =>
logger.info('✅ 午餐提醒已注册')
),
NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() =>
logger.info('✅ 晚餐提醒已注册')
),
// 心情提醒
MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() =>
logger.info('✅ 心情提醒已注册')
),
// 喝水提醒 - 需要先检查设置
getWaterReminderSettings().then(settings => {
if (settings.enabled) {
// 如果使用的是自定义提醒scheduleCustomWaterReminders 会被调用(通常在设置页面保存时)
// 但为了保险起见,这里也可以根据设置类型来决定调用哪个
// 目前逻辑似乎是 scheduleRegularWaterReminders 是默认的/旧的逻辑?
// 查看 notificationHelpers.tsscheduleRegularWaterReminders 是每2小时一次的固定逻辑
// 而 scheduleCustomWaterReminders 是根据用户设置的时间段和间隔
// 如果用户开启了提醒,应该使用 scheduleCustomWaterReminders
WaterNotificationHelpers.scheduleCustomWaterReminders(profile.name || '用户', settings).then(() =>
logger.info('✅ 自定义喝水提醒已注册')
);
} else {
logger.info(' 用户未开启喝水提醒,跳过注册');
}
}),
// 获取用户偏好设置
const [nutritionReminderEnabled, moodReminderEnabled, waterSettings] = await Promise.all([
getNutritionReminderEnabled(),
getMoodReminderEnabled(),
getWaterReminderSettings(),
]);
// 准备所有通知注册任务
const notificationTasks = [];
// 营养提醒 - 根据用户设置决定是否注册
if (nutritionReminderEnabled) {
notificationTasks.push(
NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() =>
logger.info('✅ 午餐提醒已注册')
),
NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() =>
logger.info('✅ 晚餐提醒已注册')
)
);
} else {
logger.info(' 用户未开启营养提醒,跳过注册');
}
// 心情提醒 - 根据用户设置决定是否注册
if (moodReminderEnabled) {
notificationTasks.push(
MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() =>
logger.info('✅ 心情提醒已注册')
)
);
} else {
logger.info(' 用户未开启心情提醒,跳过注册');
}
// 喝水提醒 - 根据用户设置决定是否注册
if (waterSettings.enabled) {
notificationTasks.push(
WaterNotificationHelpers.scheduleCustomWaterReminders(profile.name || '用户', waterSettings).then(() =>
logger.info('✅ 自定义喝水提醒已注册')
)
);
} else {
logger.info(' 用户未开启喝水提醒,跳过注册');
}
// 并行执行所有通知注册任务
if (notificationTasks.length > 0) {
await Promise.all(notificationTasks);
}
// 检查断食通知(如果有活跃计划)
const fastingSchedule = store.getState().fasting.activeSchedule;
if (fastingSchedule) {

View File

@@ -3,9 +3,13 @@ import { useI18n } from '@/hooks/useI18n';
import { useNotifications } from '@/hooks/useNotifications';
import {
getMedicationReminderEnabled,
getMoodReminderEnabled,
getNotificationEnabled,
getNutritionReminderEnabled,
setMedicationReminderEnabled,
setNotificationEnabled
setMoodReminderEnabled,
setNotificationEnabled,
setNutritionReminderEnabled
} from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
@@ -24,17 +28,23 @@ export default function NotificationSettingsScreen() {
// 通知设置状态
const [notificationEnabled, setNotificationEnabledState] = useState(false);
const [medicationReminderEnabled, setMedicationReminderEnabledState] = useState(false);
const [nutritionReminderEnabled, setNutritionReminderEnabledState] = useState(false);
const [moodReminderEnabled, setMoodReminderEnabledState] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// 加载通知设置
const loadNotificationSettings = useCallback(async () => {
try {
const [notification, medicationReminder] = await Promise.all([
const [notification, medicationReminder, nutritionReminder, moodReminder] = await Promise.all([
getNotificationEnabled(),
getMedicationReminderEnabled(),
getNutritionReminderEnabled(),
getMoodReminderEnabled(),
]);
setNotificationEnabledState(notification);
setMedicationReminderEnabledState(medicationReminder);
setNutritionReminderEnabledState(nutritionReminder);
setMoodReminderEnabledState(moodReminder);
} catch (error) {
console.error('Failed to load notification settings:', error);
} finally {
@@ -87,9 +97,13 @@ export default function NotificationSettingsScreen() {
// 关闭推送,保存用户偏好设置
await setNotificationEnabled(false);
setNotificationEnabledState(false);
// 关闭总开关时,也关闭药品提醒
// 关闭总开关时,也关闭所有提醒
await setMedicationReminderEnabled(false);
setMedicationReminderEnabledState(false);
await setNutritionReminderEnabled(false);
setNutritionReminderEnabledState(false);
await setMoodReminderEnabled(false);
setMoodReminderEnabledState(false);
} catch (error) {
console.error('Failed to disable push notifications:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.saveFailed'));
@@ -118,6 +132,48 @@ export default function NotificationSettingsScreen() {
}
};
// 处理营养通知提醒开关变化
const handleNutritionReminderToggle = async (value: boolean) => {
try {
await setNutritionReminderEnabled(value);
setNutritionReminderEnabledState(value);
if (value) {
// 发送测试通知
await sendNotification({
title: t('notificationSettings.alerts.nutritionReminderEnabled.title'),
body: t('notificationSettings.alerts.nutritionReminderEnabled.body'),
sound: true,
priority: 'high',
});
}
} catch (error) {
console.error('Failed to set nutrition reminder:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.nutritionReminderFailed'));
}
};
// 处理心情通知提醒开关变化
const handleMoodReminderToggle = async (value: boolean) => {
try {
await setMoodReminderEnabled(value);
setMoodReminderEnabledState(value);
if (value) {
// 发送测试通知
await sendNotification({
title: t('notificationSettings.alerts.moodReminderEnabled.title'),
body: t('notificationSettings.alerts.moodReminderEnabled.body'),
sound: true,
priority: 'high',
});
}
} catch (error) {
console.error('Failed to set mood reminder:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.moodReminderFailed'));
}
};
// 返回按钮
const BackButton = () => (
<TouchableOpacity
@@ -247,6 +303,34 @@ export default function NotificationSettingsScreen() {
</View>
</View>
{/* 营养提醒部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.nutritionReminder')}</Text>
<View style={styles.card}>
<SwitchItem
title={t('notificationSettings.items.nutritionReminder.title')}
description={t('notificationSettings.items.nutritionReminder.description')}
value={nutritionReminderEnabled}
onValueChange={handleNutritionReminderToggle}
disabled={!notificationEnabled}
/>
</View>
</View>
{/* 心情提醒部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.moodReminder')}</Text>
<View style={styles.card}>
<SwitchItem
title={t('notificationSettings.items.moodReminder.title')}
description={t('notificationSettings.items.moodReminder.description')}
value={moodReminderEnabled}
onValueChange={handleMoodReminderToggle}
disabled={!notificationEnabled}
/>
</View>
</View>
{/* 说明部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.description')}</Text>

View File

@@ -29,6 +29,7 @@ const WORKOUT_TYPES = [
{ key: 'walking', label: '步行' },
{ key: 'other', label: '其他运动' },
];
const WORKOUT_TYPE_KEYS = WORKOUT_TYPES.map(type => type.key);
export default function WorkoutNotificationSettingsScreen() {
const safeAreaTop = useSafeAreaTop()
@@ -80,16 +81,18 @@ export default function WorkoutNotificationSettingsScreen() {
};
const handleWorkoutTypeToggle = (workoutType: string) => {
const currentTypes = preferences.enabledWorkoutTypes;
let newTypes: string[];
const currentTypes = preferences.enabledWorkoutTypes.length === 0
? [...WORKOUT_TYPE_KEYS] // 空数组表示全部启用,先展开成完整列表,避免影响其他开关的当前状态
: [...preferences.enabledWorkoutTypes];
if (currentTypes.includes(workoutType)) {
newTypes = currentTypes.filter(type => type !== workoutType);
} else {
newTypes = [...currentTypes, workoutType];
}
const nextTypes = currentTypes.includes(workoutType)
? currentTypes.filter(type => type !== workoutType)
: [...currentTypes, workoutType];
savePreferences({ enabledWorkoutTypes: newTypes });
// 如果全部类型都开启,回退为空数组表示“全部启用”,以保持原有存储约定
const normalizedTypes = nextTypes.length === WORKOUT_TYPE_KEYS.length ? [] : nextTypes;
savePreferences({ enabledWorkoutTypes: normalizedTypes });
};
const handleReset = () => {
@@ -345,4 +348,4 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '600',
},
});
});