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