feat: 新增语音记录饮食功能与开发者调试模块
- 集成 @react-native-voice/voice 实现中文语音识别,支持“一句话记录”餐食 - 新增语音录制页面,含波形动画、音量反馈与识别结果确认 - FloatingFoodOverlay 新增语音入口,打通拍照/库/语音三种记录方式 - 添加麦克风与语音识别权限描述(iOS Info.plist 与 Android manifest) - 实现开发者模式:连续三次点击用户名激活,含日志查看、导出与清除 - 新增 logger 工具类,统一日志存储(AsyncStorage)与按级别输出 - 重构 BackgroundTaskManager 为单例并支持 Promise 初始化,避免重复注册 - 移除 sleep-detail 多余渐变背景,改用 ThemedView 统一主题 - 新增通用 haptic 反馈函数,支持多种震动类型(iOS only) - 升级 expo-background-task、expo-notifications、expo-task-manager 至兼容版本
This commit is contained in:
312
app/developer/logs.tsx
Normal file
312
app/developer/logs.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import { log, logger, LogEntry } 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 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('错误', '导出日志失败');
|
||||
}
|
||||
};
|
||||
|
||||
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={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>
|
||||
}
|
||||
/>
|
||||
</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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user