feat(medications): 重构药品通知系统并添加独立设置页面
- 创建药品通知服务模块,统一管理药品提醒通知的调度和取消 - 新增独立的通知设置页面,支持总开关和药品提醒开关分离控制 - 重构药品详情页面,移除频率编辑功能到独立页面 - 优化药品添加流程,支持拍照和相册选择图片 - 改进通知权限检查和错误处理机制 - 更新用户偏好设置,添加药品提醒开关配置
This commit is contained in:
388
app/notification-settings.tsx
Normal file
388
app/notification-settings.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
import { ThemedText } from '@/components/ThemedText';
|
||||
import { useAuthGuard } from '@/hooks/useAuthGuard';
|
||||
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 { pushIfAuthedElseLogin } = useAuthGuard();
|
||||
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('加载通知设置失败:', 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: '通知已开启',
|
||||
body: '您将收到应用通知和提醒',
|
||||
sound: true,
|
||||
priority: 'normal',
|
||||
});
|
||||
} else {
|
||||
// 系统权限被拒绝,不更新用户偏好设置
|
||||
Alert.alert(
|
||||
'权限被拒绝',
|
||||
'请在系统设置中开启通知权限,然后再尝试开启推送功能',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{ text: '去设置', onPress: () => Linking.openSettings() }
|
||||
]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('开启推送通知失败:', error);
|
||||
Alert.alert('错误', '请求通知权限失败');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// 关闭推送,保存用户偏好设置
|
||||
await setNotificationEnabled(false);
|
||||
setNotificationEnabledState(false);
|
||||
// 关闭总开关时,也关闭药品提醒
|
||||
await setMedicationReminderEnabled(false);
|
||||
setMedicationReminderEnabledState(false);
|
||||
} catch (error) {
|
||||
console.error('关闭推送通知失败:', error);
|
||||
Alert.alert('错误', '保存设置失败');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理药品通知提醒开关变化
|
||||
const handleMedicationReminderToggle = async (value: boolean) => {
|
||||
try {
|
||||
await setMedicationReminderEnabled(value);
|
||||
setMedicationReminderEnabledState(value);
|
||||
|
||||
if (value) {
|
||||
// 发送测试通知
|
||||
await sendNotification({
|
||||
title: '药品提醒已开启',
|
||||
body: '您将在用药时间收到提醒通知',
|
||||
sound: true,
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('设置药品提醒失败:', error);
|
||||
Alert.alert('错误', '保存设置失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 返回按钮
|
||||
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}>加载中...</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}>通知设置</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* 通知设置部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>通知设置</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title="消息推送"
|
||||
description="开启后将接收应用通知"
|
||||
value={notificationEnabled}
|
||||
onValueChange={handleNotificationToggle}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 药品提醒部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>药品提醒</Text>
|
||||
<View style={styles.card}>
|
||||
<SwitchItem
|
||||
title="药品通知提醒"
|
||||
description="在用药时间接收提醒通知"
|
||||
value={medicationReminderEnabled}
|
||||
onValueChange={handleMedicationReminderToggle}
|
||||
disabled={!notificationEnabled}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 说明部分 */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>说明</Text>
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.description}>
|
||||
• 消息推送是所有通知的总开关{'\n'}
|
||||
• 药品通知提醒需要在消息推送开启后才能使用{'\n'}
|
||||
• 您可以在系统设置中管理通知权限{'\n'}
|
||||
• 关闭消息推送将停止所有应用通知
|
||||
</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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user