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