feat(fasting): 重构断食通知系统并增强可靠性

- 新增 useFastingNotifications hook 统一管理通知状态和同步逻辑
- 实现四阶段通知提醒:开始前30分钟、开始时、结束前30分钟、结束时
- 添加通知验证机制,确保通知正确设置和避免重复
- 新增 NotificationErrorAlert 组件显示通知错误并提供重试选项
- 实现断食计划持久化存储,应用重启后自动恢复
- 添加开发者测试面板用于验证通知系统可靠性
- 优化通知同步策略,支持选择性更新减少不必要的操作
- 修复个人页面编辑按钮样式问题
- 更新应用版本号至 1.0.18
This commit is contained in:
richarjiang
2025-10-14 15:05:11 +08:00
parent e03b2b3032
commit cf069f3537
18 changed files with 1565 additions and 242 deletions

View File

@@ -0,0 +1,229 @@
import { FASTING_PLANS, FastingPlan } from '@/constants/Fasting';
import { fastingNotificationTester } from '@/utils/fastingNotificationTest';
import React, { useState } from 'react';
import { Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface FastingNotificationTestPanelProps {
onClose: () => void;
}
export const FastingNotificationTestPanel: React.FC<FastingNotificationTestPanelProps> = ({
onClose,
}) => {
const [isRunning, setIsRunning] = useState(false);
const [testResults, setTestResults] = useState<Array<{
testName: string;
passed: boolean;
message: string;
timestamp: Date;
}>>([]);
const runTest = async (plan: FastingPlan) => {
setIsRunning(true);
setTestResults([]);
try {
const results = await fastingNotificationTester.runFullTestSuite(plan);
setTestResults(results);
const passedCount = results.filter(r => r.passed).length;
const totalCount = results.length;
Alert.alert(
'测试完成',
`测试结果: ${passedCount}/${totalCount} 通过`,
[{ text: '确定', onPress: () => { } }]
);
} catch (error) {
Alert.alert(
'测试失败',
`测试过程中发生错误: ${error instanceof Error ? error.message : '未知错误'}`,
[{ text: '确定', onPress: () => { } }]
);
} finally {
setIsRunning(false);
}
};
const clearResults = () => {
setTestResults([]);
fastingNotificationTester.clearResults();
};
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}></Text>
<TouchableOpacity style={styles.closeButton} onPress={onClose}>
<Text style={styles.closeButtonText}>×</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
{FASTING_PLANS.map((plan) => (
<TouchableOpacity
key={plan.id}
style={[styles.planButton, isRunning && styles.disabledButton]}
onPress={() => runTest(plan)}
disabled={isRunning}
>
<Text style={styles.planButtonText}>{plan.title}</Text>
<Text style={styles.planSubtitle}>{plan.subtitle}</Text>
</TouchableOpacity>
))}
</View>
{testResults.length > 0 && (
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}></Text>
<TouchableOpacity style={styles.clearButton} onPress={clearResults}>
<Text style={styles.clearButtonText}></Text>
</TouchableOpacity>
</View>
{testResults.map((result, index) => (
<View key={index} style={[
styles.resultItem,
result.passed ? styles.passedResult : styles.failedResult
]}>
<Text style={styles.resultTestName}>{result.testName}</Text>
<Text style={styles.resultMessage}>{result.message}</Text>
<Text style={styles.resultTime}>
{result.timestamp.toLocaleTimeString()}
</Text>
</View>
))}
</View>
)}
{isRunning && (
<View style={styles.section}>
<Text style={styles.runningText}>...</Text>
</View>
)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#333333',
},
closeButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F0F0F0',
justifyContent: 'center',
alignItems: 'center',
},
closeButtonText: {
fontSize: 18,
fontWeight: '600',
color: '#666666',
},
content: {
flex: 1,
padding: 16,
},
section: {
marginBottom: 24,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333333',
marginBottom: 12,
},
planButton: {
backgroundColor: '#FFFFFF',
padding: 12,
borderRadius: 8,
marginBottom: 8,
borderWidth: 1,
borderColor: '#E0E0E0',
},
disabledButton: {
opacity: 0.5,
},
planButtonText: {
fontSize: 14,
fontWeight: '500',
color: '#333333',
},
planSubtitle: {
fontSize: 12,
color: '#666666',
marginTop: 2,
},
clearButton: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#F0F0F0',
borderRadius: 4,
},
clearButtonText: {
fontSize: 12,
color: '#666666',
},
resultItem: {
padding: 12,
borderRadius: 8,
marginBottom: 8,
borderLeftWidth: 4,
},
passedResult: {
backgroundColor: '#F0F9F0',
borderLeftColor: '#4CAF50',
},
failedResult: {
backgroundColor: '#FFF3F3',
borderLeftColor: '#F44336',
},
resultTestName: {
fontSize: 14,
fontWeight: '500',
color: '#333333',
},
resultMessage: {
fontSize: 12,
color: '#666666',
marginTop: 2,
},
resultTime: {
fontSize: 10,
color: '#999999',
marginTop: 4,
},
runningText: {
fontSize: 14,
color: '#666666',
textAlign: 'center',
fontStyle: 'italic',
},
});

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface NotificationErrorAlertProps {
error: string | null;
onRetry?: () => void;
onDismiss?: () => void;
}
export const NotificationErrorAlert: React.FC<NotificationErrorAlertProps> = ({
error,
onRetry,
onDismiss,
}) => {
if (!error) return null;
return (
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}></Text>
<Text style={styles.message}>{error}</Text>
<View style={styles.actions}>
{onRetry && (
<TouchableOpacity style={styles.button} onPress={onRetry}>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
)}
{onDismiss && (
<TouchableOpacity style={[styles.button, styles.dismissButton]} onPress={onDismiss}>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#FEF2F2',
borderLeftWidth: 4,
borderLeftColor: '#EF4444',
marginHorizontal: 20,
marginVertical: 8,
borderRadius: 8,
},
content: {
padding: 12,
},
title: {
fontSize: 14,
fontWeight: '600',
color: '#DC2626',
marginBottom: 4,
},
message: {
fontSize: 13,
color: '#7F1D1D',
marginBottom: 8,
lineHeight: 18,
},
actions: {
flexDirection: 'row',
gap: 8,
},
button: {
paddingHorizontal: 12,
paddingVertical: 6,
backgroundColor: '#DC2626',
borderRadius: 6,
},
dismissButton: {
backgroundColor: '#9CA3AF',
},
buttonText: {
fontSize: 12,
fontWeight: '500',
color: '#FFFFFF',
},
});