Files
digital-pilates/app/notification-settings.tsx
richarjiang d282abd146 feat(background): 增强iOS后台任务系统,添加processing任务类型支持
- 添加新的processing任务标识符到iOS配置文件
- 重构BackgroundTaskBridge支持不同任务类型(refresh/processing)
- 增强后台任务日志记录和调试信息
- 修复任务类型配置不匹配问题
- 改进任务调度逻辑和错误处理机制
- 添加任务执行时间戳记录用于调试
- 移除notification-settings中未使用的AuthGuard依赖
2025-11-13 17:12:57 +08:00

385 lines
11 KiB
TypeScript

import { ThemedText } from '@/components/ThemedText';
import { useI18n } from '@/hooks/useI18n';
import { useNotifications } from '@/hooks/useNotifications';
import {
getMedicationReminderEnabled,
getNotificationEnabled,
setMedicationReminderEnabled,
setNotificationEnabled
} from '@/utils/userPreferences';
import { Ionicons } from '@expo/vector-icons';
import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect';
import { LinearGradient } from 'expo-linear-gradient';
import { router, useFocusEffect } from 'expo-router';
import React, { useCallback, useState } from 'react';
import { Alert, Linking, ScrollView, StatusBar, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function NotificationSettingsScreen() {
const insets = useSafeAreaInsets();
const { t } = useI18n();
const { requestPermission, sendNotification } = useNotifications();
const isLgAvailable = isLiquidGlassAvailable();
// 通知设置状态
const [notificationEnabled, setNotificationEnabledState] = useState(false);
const [medicationReminderEnabled, setMedicationReminderEnabledState] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// 加载通知设置
const loadNotificationSettings = useCallback(async () => {
try {
const [notification, medicationReminder] = await Promise.all([
getNotificationEnabled(),
getMedicationReminderEnabled(),
]);
setNotificationEnabledState(notification);
setMedicationReminderEnabledState(medicationReminder);
} catch (error) {
console.error('Failed to load notification settings:', error);
} finally {
setIsLoading(false);
}
}, []);
// 页面聚焦时加载设置
useFocusEffect(
useCallback(() => {
loadNotificationSettings();
}, [loadNotificationSettings])
);
// 处理总通知开关变化
const handleNotificationToggle = async (value: boolean) => {
if (value) {
try {
// 先检查系统权限
const status = await requestPermission();
if (status === 'granted') {
// 系统权限获取成功,保存用户偏好设置
await setNotificationEnabled(true);
setNotificationEnabledState(true);
// 发送测试通知
await sendNotification({
title: t('notificationSettings.alerts.notificationsEnabled.title'),
body: t('notificationSettings.alerts.notificationsEnabled.body'),
sound: true,
priority: 'normal',
});
} else {
// 系统权限被拒绝,不更新用户偏好设置
Alert.alert(
t('notificationSettings.alerts.permissionDenied.title'),
t('notificationSettings.alerts.permissionDenied.message'),
[
{ text: t('notificationSettings.alerts.permissionDenied.cancel'), style: 'cancel' },
{ text: t('notificationSettings.alerts.permissionDenied.goToSettings'), onPress: () => Linking.openSettings() }
]
);
}
} catch (error) {
console.error('Failed to enable push notifications:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.message'));
}
} else {
try {
// 关闭推送,保存用户偏好设置
await setNotificationEnabled(false);
setNotificationEnabledState(false);
// 关闭总开关时,也关闭药品提醒
await setMedicationReminderEnabled(false);
setMedicationReminderEnabledState(false);
} catch (error) {
console.error('Failed to disable push notifications:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.saveFailed'));
}
}
};
// 处理药品通知提醒开关变化
const handleMedicationReminderToggle = async (value: boolean) => {
try {
await setMedicationReminderEnabled(value);
setMedicationReminderEnabledState(value);
if (value) {
// 发送测试通知
await sendNotification({
title: t('notificationSettings.alerts.medicationReminderEnabled.title'),
body: t('notificationSettings.alerts.medicationReminderEnabled.body'),
sound: true,
priority: 'high',
});
}
} catch (error) {
console.error('Failed to set medication reminder:', error);
Alert.alert(t('notificationSettings.alerts.error.title'), t('notificationSettings.alerts.error.medicationReminderFailed'));
}
};
// 返回按钮
const BackButton = () => (
<TouchableOpacity
onPress={() => router.back()}
style={styles.backButton}
activeOpacity={0.7}
>
{isLgAvailable ? (
<GlassView
style={styles.glassButton}
glassEffectStyle="clear"
tintColor="rgba(255, 255, 255, 0.3)"
isInteractive={true}
>
<Ionicons name="chevron-back" size={24} color="#333" />
</GlassView>
) : (
<View style={[styles.glassButton, styles.fallbackButton]}>
<Ionicons name="chevron-back" size={24} color="#333" />
</View>
)}
</TouchableOpacity>
);
// 开关项组件
const SwitchItem = ({
title,
description,
value,
onValueChange,
disabled = false
}: {
title: string;
description: string;
value: boolean;
onValueChange: (value: boolean) => void;
disabled?: boolean;
}) => (
<View style={styles.switchItem}>
<View style={styles.switchItemLeft}>
<Text style={styles.switchItemTitle}>{title}</Text>
<Text style={styles.switchItemDescription}>{description}</Text>
</View>
<Switch
value={value}
onValueChange={onValueChange}
disabled={disabled}
trackColor={{ false: '#E5E5E5', true: '#9370DB' }}
thumbColor="#FFFFFF"
style={styles.switch}
/>
</View>
);
if (isLoading) {
return (
<View style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
<LinearGradient
colors={['#f5e5fbff', '#e5fcfeff', '#eefdffff', '#e6f6fcff']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
/>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>{t('notificationSettings.loading')}</Text>
</View>
</View>
);
}
return (
<View style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor="transparent" translucent />
{/* 背景渐变 */}
<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} />
<ScrollView
style={styles.scrollView}
contentContainerStyle={{
paddingTop: insets.top + 20,
paddingBottom: insets.bottom + 20,
paddingHorizontal: 16,
}}
showsVerticalScrollIndicator={false}
>
{/* 头部 */}
<View style={styles.header}>
<BackButton />
<ThemedText style={styles.title}>{t('notificationSettings.title')}</ThemedText>
</View>
{/* 通知设置部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.notifications')}</Text>
<View style={styles.card}>
<SwitchItem
title={t('notificationSettings.items.pushNotifications.title')}
description={t('notificationSettings.items.pushNotifications.description')}
value={notificationEnabled}
onValueChange={handleNotificationToggle}
/>
</View>
</View>
{/* 药品提醒部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.medicationReminder')}</Text>
<View style={styles.card}>
<SwitchItem
title={t('notificationSettings.items.medicationReminder.title')}
description={t('notificationSettings.items.medicationReminder.description')}
value={medicationReminderEnabled}
onValueChange={handleMedicationReminderToggle}
disabled={!notificationEnabled}
/>
</View>
</View>
{/* 说明部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('notificationSettings.sections.description')}</Text>
<View style={styles.card}>
<Text style={styles.description}>
{t('notificationSettings.description.text')}
</Text>
</View>
</View>
</ScrollView>
</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,
},
scrollView: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 16,
color: '#666',
},
header: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 24,
},
backButton: {
marginRight: 16,
},
glassButton: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
fallbackButton: {
backgroundColor: 'rgba(255, 255, 255, 0.9)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.3)',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2C3E50',
},
section: {
marginBottom: 24,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#2C3E50',
marginBottom: 12,
paddingHorizontal: 4,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
overflow: 'hidden',
},
switchItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 16,
paddingHorizontal: 16,
},
switchItemLeft: {
flex: 1,
marginRight: 16,
},
switchItemTitle: {
fontSize: 16,
fontWeight: '500',
color: '#2C3E50',
marginBottom: 4,
},
switchItemDescription: {
fontSize: 14,
color: '#6C757D',
lineHeight: 20,
},
switch: {
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
},
description: {
fontSize: 14,
color: '#6C757D',
lineHeight: 22,
paddingVertical: 16,
paddingHorizontal: 16,
},
});