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

328 lines
8.2 KiB
TypeScript

import { FastingNotificationTestPanel } from '@/components/developer/FastingNotificationTestPanel';
import { log, LogEntry, logger } from '@/utils/logger';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React, { useEffect, useState } from 'react';
import {
Alert,
FlatList,
RefreshControl,
Share,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function LogsScreen() {
const router = useRouter();
const insets = useSafeAreaInsets();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [showTestPanel, setShowTestPanel] = useState(false);
const loadLogs = async () => {
try {
const allLogs = await logger.getAllLogs();
// 按时间倒序排列,最新的在前面
setLogs(allLogs.reverse());
} catch (error) {
log.error('加载日志失败', error);
}
};
const onRefresh = async () => {
setRefreshing(true);
await loadLogs();
setRefreshing(false);
};
const handleClearLogs = () => {
Alert.alert(
'清除日志',
'确定要清除所有日志吗?此操作不可恢复。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: async () => {
await logger.clearLogs();
setLogs([]);
log.info('日志已清除');
},
},
]
);
};
const handleExportLogs = async () => {
try {
const exportData = await logger.exportLogs();
await Share.share({
message: exportData,
title: '应用日志导出',
});
} catch (error) {
log.error('导出日志失败', error);
Alert.alert('错误', '导出日志失败');
}
};
const handleTestNotifications = () => {
setShowTestPanel(true);
};
useEffect(() => {
loadLogs();
// 添加测试日志
log.info('进入日志页面');
}, []);
const formatTimestamp = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
};
const getLevelColor = (level: string) => {
switch (level) {
case 'ERROR':
return '#FF4444';
case 'WARN':
return '#FF8800';
case 'INFO':
return '#0088FF';
case 'DEBUG':
return '#888888';
default:
return '#333333';
}
};
const getLevelIcon = (level: string) => {
switch (level) {
case 'ERROR':
return 'close-circle';
case 'WARN':
return 'warning';
case 'INFO':
return 'information-circle';
case 'DEBUG':
return 'bug';
default:
return 'ellipse';
}
};
const renderLogItem = ({ item }: { item: LogEntry }) => (
<View style={styles.logItem}>
<View style={styles.logHeader}>
<View style={styles.logLevelContainer}>
<Ionicons
name={getLevelIcon(item.level) as any}
size={16}
color={getLevelColor(item.level)}
style={styles.logIcon}
/>
<Text style={[styles.logLevel, { color: getLevelColor(item.level) }]}>
{item.level}
</Text>
</View>
<Text style={styles.timestamp}>{formatTimestamp(item.timestamp)}</Text>
</View>
<Text style={styles.logMessage}>{item.message}</Text>
{item.data && (
<Text style={styles.logData}>{JSON.stringify(item.data, null, 2)}</Text>
)}
</View>
);
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
<Ionicons name="chevron-back" size={24} color="#2C3E50" />
</TouchableOpacity>
<Text style={styles.title}> ({logs.length})</Text>
<View style={styles.headerActions}>
<TouchableOpacity onPress={handleTestNotifications} style={styles.actionButton}>
<Ionicons name="notifications-outline" size={20} color="#FF8800" />
</TouchableOpacity>
<TouchableOpacity onPress={handleExportLogs} style={styles.actionButton}>
<Ionicons name="share-outline" size={20} color="#9370DB" />
</TouchableOpacity>
<TouchableOpacity onPress={handleClearLogs} style={styles.actionButton}>
<Ionicons name="trash-outline" size={20} color="#FF4444" />
</TouchableOpacity>
</View>
</View>
{/* Logs List */}
<FlatList
data={logs}
renderItem={renderLogItem}
keyExtractor={(item) => item.id}
style={styles.logsList}
contentContainerStyle={styles.logsContent}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Ionicons name="document-text-outline" size={48} color="#CCCCCC" />
<Text style={styles.emptyText}></Text>
<TouchableOpacity
style={styles.testButton}
onPress={() => {
log.debug('测试调试日志');
log.info('测试信息日志');
log.warn('测试警告日志');
log.error('测试错误日志');
setTimeout(loadLogs, 100);
}}
>
<Text style={styles.testButtonText}></Text>
</TouchableOpacity>
</View>
}
/>
{/* 断食通知测试面板 */}
{showTestPanel && (
<FastingNotificationTestPanel
onClose={() => setShowTestPanel(false)}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F8F9FA',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
backButton: {
width: 40,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: 'bold',
color: '#2C3E50',
flex: 1,
marginLeft: 8,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
},
actionButton: {
width: 36,
height: 36,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 8,
},
logsList: {
flex: 1,
},
logsContent: {
padding: 16,
},
logItem: {
backgroundColor: '#FFFFFF',
padding: 12,
marginBottom: 8,
borderRadius: 8,
borderLeftWidth: 3,
borderLeftColor: '#E5E7EB',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
logHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
},
logLevelContainer: {
flexDirection: 'row',
alignItems: 'center',
},
logIcon: {
marginRight: 4,
},
logLevel: {
fontSize: 12,
fontWeight: 'bold',
fontFamily: 'monospace',
},
timestamp: {
fontSize: 11,
color: '#9CA3AF',
fontFamily: 'monospace',
},
logMessage: {
fontSize: 14,
color: '#374151',
lineHeight: 20,
fontFamily: 'monospace',
},
logData: {
fontSize: 12,
color: '#6B7280',
marginTop: 8,
padding: 8,
backgroundColor: '#F3F4F6',
borderRadius: 4,
fontFamily: 'monospace',
},
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 48,
},
emptyText: {
fontSize: 16,
color: '#9CA3AF',
marginTop: 12,
marginBottom: 24,
},
testButton: {
backgroundColor: '#9370DB',
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
},
testButtonText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
});