feat(fasting): 重构断食通知系统并增强可靠性
- 新增 useFastingNotifications hook 统一管理通知状态和同步逻辑 - 实现四阶段通知提醒:开始前30分钟、开始时、结束前30分钟、结束时 - 添加通知验证机制,确保通知正确设置和避免重复 - 新增 NotificationErrorAlert 组件显示通知错误并提供重试选项 - 实现断食计划持久化存储,应用重启后自动恢复 - 添加开发者测试面板用于验证通知系统可靠性 - 优化通知同步策略,支持选择性更新减少不必要的操作 - 修复个人页面编辑按钮样式问题 - 更新应用版本号至 1.0.18
This commit is contained in:
229
components/developer/FastingNotificationTestPanel.tsx
Normal file
229
components/developer/FastingNotificationTestPanel.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user