- 在相机界面新增“拍摄示例”弹窗,展示正确/错误拍摄对比图 - 底部控制栏增加相册选择按钮与帮助按钮 - 优化控制栏布局为左右分布,提升操作便捷性 - 移除 food-recognition 中冗余的 isUploading 状态,简化上传流程
747 lines
21 KiB
TypeScript
747 lines
21 KiB
TypeScript
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||
import { Colors } from '@/constants/Colors';
|
||
import { useAppDispatch } from '@/hooks/redux';
|
||
import { useCosUpload } from '@/hooks/useCosUpload';
|
||
import { recognizeFood } from '@/services/foodRecognition';
|
||
import { saveRecognitionResult, setError, setLoading } from '@/store/foodRecognitionSlice';
|
||
import { Ionicons } from '@expo/vector-icons';
|
||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import {
|
||
ActivityIndicator,
|
||
Alert,
|
||
Animated,
|
||
Easing,
|
||
Image,
|
||
ScrollView,
|
||
StyleSheet,
|
||
Text,
|
||
TouchableOpacity,
|
||
View
|
||
} from 'react-native';
|
||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||
|
||
export default function FoodRecognitionScreen() {
|
||
const router = useRouter();
|
||
const params = useLocalSearchParams<{
|
||
imageUri?: string;
|
||
mealType?: string;
|
||
}>();
|
||
|
||
const { imageUri, mealType } = params;
|
||
const { upload } = useCosUpload();
|
||
const [showRecognitionProcess, setShowRecognitionProcess] = useState(false);
|
||
const [recognitionLogs, setRecognitionLogs] = useState<string[]>([]);
|
||
const [currentStep, setCurrentStep] = useState<'idle' | 'uploading' | 'recognizing' | 'completed' | 'failed'>('idle');
|
||
const dispatch = useAppDispatch();
|
||
|
||
// 动画引用
|
||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||
const slideAnim = useRef(new Animated.Value(50)).current;
|
||
const progressAnim = useRef(new Animated.Value(0)).current;
|
||
const pulseAnim = useRef(new Animated.Value(1)).current;
|
||
|
||
// 启动动画效果
|
||
useEffect(() => {
|
||
if (showRecognitionProcess) {
|
||
// 进入动画
|
||
Animated.parallel([
|
||
Animated.timing(fadeAnim, {
|
||
toValue: 1,
|
||
duration: 600,
|
||
easing: Easing.out(Easing.cubic),
|
||
useNativeDriver: true,
|
||
}),
|
||
Animated.timing(slideAnim, {
|
||
toValue: 0,
|
||
duration: 600,
|
||
easing: Easing.out(Easing.cubic),
|
||
useNativeDriver: true,
|
||
})
|
||
]).start();
|
||
|
||
// 启动进度条动画
|
||
if (currentStep === 'uploading' || currentStep === 'recognizing') {
|
||
Animated.timing(progressAnim, {
|
||
toValue: currentStep === 'uploading' ? 0.5 : 1,
|
||
duration: 2000,
|
||
easing: Easing.inOut(Easing.ease),
|
||
useNativeDriver: false,
|
||
}).start();
|
||
}
|
||
|
||
// 脉冲动画
|
||
const pulseAnimation = Animated.loop(
|
||
Animated.sequence([
|
||
Animated.timing(pulseAnim, {
|
||
toValue: 1.1,
|
||
duration: 800,
|
||
easing: Easing.inOut(Easing.sin),
|
||
useNativeDriver: true,
|
||
}),
|
||
Animated.timing(pulseAnim, {
|
||
toValue: 1,
|
||
duration: 800,
|
||
easing: Easing.inOut(Easing.sin),
|
||
useNativeDriver: true,
|
||
})
|
||
])
|
||
);
|
||
|
||
if (currentStep === 'uploading' || currentStep === 'recognizing') {
|
||
pulseAnimation.start();
|
||
} else {
|
||
pulseAnimation.stop();
|
||
pulseAnim.setValue(1);
|
||
}
|
||
} else {
|
||
fadeAnim.setValue(0);
|
||
slideAnim.setValue(50);
|
||
progressAnim.setValue(0);
|
||
}
|
||
}, [showRecognitionProcess, currentStep]);
|
||
|
||
const addLog = (message: string) => {
|
||
setRecognitionLogs(prev => [...prev, message]);
|
||
};
|
||
|
||
const handleConfirm = async () => {
|
||
if (!imageUri) return;
|
||
|
||
// 按钮动画效果
|
||
Animated.sequence([
|
||
Animated.timing(scaleAnim, {
|
||
toValue: 0.95,
|
||
duration: 100,
|
||
useNativeDriver: true,
|
||
}),
|
||
Animated.timing(scaleAnim, {
|
||
toValue: 1,
|
||
duration: 100,
|
||
useNativeDriver: true,
|
||
})
|
||
]).start();
|
||
|
||
try {
|
||
setShowRecognitionProcess(true);
|
||
setRecognitionLogs([]);
|
||
setCurrentStep('uploading');
|
||
dispatch(setLoading(true));
|
||
|
||
addLog('📤 正在上传图片到云端...');
|
||
|
||
// 上传图片到 COS
|
||
const { url } = await upload(
|
||
{ uri: imageUri, name: 'food-image.jpg', type: 'image/jpeg' },
|
||
{ prefix: 'food-images/' }
|
||
);
|
||
|
||
addLog('✅ 图片上传完成');
|
||
addLog('🤖 AI大模型分析中...');
|
||
setCurrentStep('recognizing');
|
||
|
||
// 调用食物识别 API
|
||
const recognitionResult = await recognizeFood({
|
||
imageUrls: [url]
|
||
});
|
||
|
||
console.log('食物识别结果:', recognitionResult);
|
||
|
||
if (!recognitionResult.isFoodDetected) {
|
||
addLog('❌ 识别失败:未检测到食物');
|
||
addLog(`💭 ${recognitionResult.nonFoodMessage || recognitionResult.analysisText}`);
|
||
setCurrentStep('failed');
|
||
return;
|
||
}
|
||
|
||
addLog('✅ AI分析完成');
|
||
addLog(`🎯 识别置信度: ${recognitionResult.confidence}%`);
|
||
addLog(`🍽️ 识别到 ${recognitionResult.items.length} 种食物`);
|
||
|
||
setCurrentStep('completed');
|
||
|
||
// 生成唯一的识别ID
|
||
const recognitionId = `recognition_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||
|
||
// 保存识别结果到 Redux
|
||
dispatch(saveRecognitionResult({
|
||
id: recognitionId,
|
||
result: recognitionResult
|
||
}));
|
||
|
||
// 延迟跳转,让用户看到完成状态
|
||
setTimeout(() => {
|
||
router.replace(`/food/analysis-result?imageUri=${encodeURIComponent(url)}&mealType=${mealType}&recognitionId=${recognitionId}`);
|
||
}, 1500);
|
||
|
||
} catch (error) {
|
||
console.warn('食物识别失败', error);
|
||
addLog('❌ 识别过程出错');
|
||
addLog(`💥 ${error instanceof Error ? error.message : '未知错误'}`);
|
||
setCurrentStep('failed');
|
||
dispatch(setError('食物识别失败,请重试'));
|
||
} finally {
|
||
dispatch(setLoading(false));
|
||
}
|
||
};
|
||
|
||
const handleRetry = () => {
|
||
setShowRecognitionProcess(false);
|
||
setCurrentStep('idle');
|
||
setRecognitionLogs([]);
|
||
dispatch(setError(null));
|
||
|
||
router.back()
|
||
};
|
||
|
||
const handleGoBack = () => {
|
||
if (showRecognitionProcess && currentStep !== 'failed') {
|
||
Alert.alert(
|
||
'正在识别中',
|
||
'识别过程尚未完成,确定要返回吗?',
|
||
[
|
||
{ text: '继续识别', style: 'cancel' },
|
||
{ text: '返回', style: 'destructive', onPress: () => router.back() }
|
||
]
|
||
);
|
||
} else {
|
||
router.back();
|
||
}
|
||
};
|
||
|
||
if (!imageUri) {
|
||
return (
|
||
<SafeAreaView style={styles.container}>
|
||
<HeaderBar
|
||
title="食物识别"
|
||
onBack={() => router.back()}
|
||
/>
|
||
<View style={styles.errorContainer}>
|
||
<Text style={styles.errorText}>未找到图片</Text>
|
||
</View>
|
||
</SafeAreaView>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<View style={styles.container}>
|
||
<HeaderBar
|
||
title={showRecognitionProcess ? "食物识别" : "确认食物"}
|
||
onBack={handleGoBack}
|
||
/>
|
||
|
||
{/* 主要内容区域 */}
|
||
<ScrollView style={styles.contentContainer} showsVerticalScrollIndicator={false}>
|
||
{!showRecognitionProcess ? (
|
||
// 确认界面
|
||
<>
|
||
{/* 照片卡片 */}
|
||
<View style={styles.photoCard}>
|
||
<View style={styles.photoFrame}>
|
||
<Image
|
||
source={{ uri: imageUri }}
|
||
style={styles.photoImage}
|
||
resizeMode="cover"
|
||
/>
|
||
|
||
{/* 餐次标签叠加 */}
|
||
{mealType && (
|
||
<View style={styles.mealTypeBadge}>
|
||
<Text style={styles.mealTypeBadgeText}>
|
||
{getMealTypeLabel(mealType)}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
</View>
|
||
</View>
|
||
|
||
{/* AI 识别说明卡片 */}
|
||
<View style={styles.infoCard}>
|
||
<View style={styles.infoHeader}>
|
||
<View style={styles.aiIconContainer}>
|
||
<Ionicons name="sparkles" size={20} color={Colors.light.primary} />
|
||
</View>
|
||
<Text style={styles.infoTitle}>智能食物识别</Text>
|
||
</View>
|
||
<Text style={styles.infoDescription}>
|
||
AI 将分析您的照片,识别食物种类、估算营养成分,为您生成详细的营养分析报告
|
||
</Text>
|
||
</View>
|
||
|
||
{/* 底部按钮区域 */}
|
||
<View style={styles.bottomContainer}>
|
||
<Animated.View style={[styles.confirmButtonContainer, { transform: [{ scale: scaleAnim }] }]}>
|
||
<TouchableOpacity
|
||
style={styles.confirmButton}
|
||
onPress={handleConfirm}
|
||
activeOpacity={0.8}
|
||
>
|
||
<View style={styles.confirmButtonContent}>
|
||
<Ionicons name="scan" size={20} color={Colors.light.onPrimary} style={{ marginRight: 8 }} />
|
||
<Text style={styles.confirmButtonText}>开始智能识别</Text>
|
||
</View>
|
||
</TouchableOpacity>
|
||
</Animated.View>
|
||
</View>
|
||
</>
|
||
) : (
|
||
// 识别过程界面
|
||
<Animated.View style={[styles.recognitionContainer, {
|
||
opacity: fadeAnim,
|
||
transform: [{ translateY: slideAnim }]
|
||
}]}>
|
||
{/* 照片缩略图卡片 */}
|
||
<View style={styles.thumbnailCard}>
|
||
<Image
|
||
source={{ uri: imageUri }}
|
||
style={styles.thumbnailImage}
|
||
resizeMode="cover"
|
||
/>
|
||
<View style={styles.thumbnailInfo}>
|
||
{mealType && (
|
||
<View style={styles.thumbnailMealType}>
|
||
<Text style={styles.thumbnailMealTypeText}>
|
||
{getMealTypeLabel(mealType)}
|
||
</Text>
|
||
</View>
|
||
)}
|
||
<Text style={styles.thumbnailTitle}>AI 识别中...</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 进度指示卡片 */}
|
||
<View style={styles.progressCard}>
|
||
<View style={styles.progressHeader}>
|
||
<Animated.View style={[styles.statusIconAnimated, { transform: [{ scale: pulseAnim }] }]}>
|
||
<View style={[styles.statusIcon, {
|
||
backgroundColor: currentStep === 'uploading' || currentStep === 'recognizing' ? Colors.light.primary :
|
||
currentStep === 'completed' ? Colors.light.success :
|
||
currentStep === 'failed' ? Colors.light.danger : Colors.light.neutral200
|
||
}]}>
|
||
{currentStep === 'uploading' || currentStep === 'recognizing' ? (
|
||
<ActivityIndicator size="small" color={Colors.light.onPrimary} />
|
||
) : currentStep === 'completed' ? (
|
||
<Ionicons name="checkmark" size={20} color={Colors.light.onPrimary} />
|
||
) : currentStep === 'failed' ? (
|
||
<Ionicons name="close" size={20} color={Colors.light.onPrimary} />
|
||
) : null}
|
||
</View>
|
||
</Animated.View>
|
||
<View style={styles.progressInfo}>
|
||
<Text style={styles.statusText}>{
|
||
currentStep === 'idle' ? '准备中' :
|
||
currentStep === 'uploading' ? '上传图片中' :
|
||
currentStep === 'recognizing' ? 'AI 分析中' :
|
||
currentStep === 'completed' ? '识别完成' :
|
||
currentStep === 'failed' ? '识别失败' : ''
|
||
}</Text>
|
||
<Text style={styles.statusSubtext}>{
|
||
currentStep === 'uploading' ? '正在将图片上传到云端处理...' :
|
||
currentStep === 'recognizing' ? '智能模型正在分析食物成分...' :
|
||
currentStep === 'completed' ? '即将跳转到分析结果页面' :
|
||
currentStep === 'failed' ? '请检查网络连接或重新拍照' : ''
|
||
}</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 进度条 */}
|
||
{(currentStep === 'uploading' || currentStep === 'recognizing') && (
|
||
<View style={styles.progressBarContainer}>
|
||
<View style={styles.progressBarBackground}>
|
||
<Animated.View
|
||
style={[styles.progressBarFill, {
|
||
width: progressAnim.interpolate({
|
||
inputRange: [0, 1],
|
||
outputRange: ['0%', '100%'],
|
||
})
|
||
}]}
|
||
/>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 识别日志卡片 */}
|
||
<View style={styles.logCard}>
|
||
<View style={styles.logHeader}>
|
||
<Ionicons name="document-text-outline" size={18} color={Colors.light.primary} />
|
||
<Text style={styles.logTitle}>进度</Text>
|
||
</View>
|
||
<ScrollView
|
||
style={styles.logScrollView}
|
||
contentContainerStyle={styles.logContent}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{recognitionLogs.map((log, index) => (
|
||
<Animated.View
|
||
key={index}
|
||
style={[styles.logItem, {
|
||
opacity: fadeAnim,
|
||
transform: [{ translateX: slideAnim }]
|
||
}]}
|
||
>
|
||
<Text style={styles.logText}>{log}</Text>
|
||
</Animated.View>
|
||
))}
|
||
{recognitionLogs.length === 0 && (
|
||
<Text style={styles.logPlaceholder}>等待处理开始...</Text>
|
||
)}
|
||
</ScrollView>
|
||
</View>
|
||
|
||
{/* 重试按钮 */}
|
||
{currentStep === 'failed' && (
|
||
<View style={styles.bottomContainer}>
|
||
<TouchableOpacity
|
||
style={styles.retryButton}
|
||
onPress={handleRetry}
|
||
activeOpacity={0.8}
|
||
>
|
||
<Ionicons name="refresh" size={18} color={Colors.light.onPrimary} style={{ marginRight: 8 }} />
|
||
<Text style={styles.retryButtonText}>返回重新拍照</Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
)}
|
||
</Animated.View>
|
||
)}
|
||
</ScrollView>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
// 获取餐次标签
|
||
function getMealTypeLabel(mealType: string): string {
|
||
const mealTypeMap: Record<string, string> = {
|
||
breakfast: '早餐',
|
||
lunch: '午餐',
|
||
dinner: '晚餐',
|
||
snack: '加餐',
|
||
};
|
||
return mealTypeMap[mealType] || '未知餐次';
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: Colors.light.pageBackgroundEmphasis,
|
||
},
|
||
contentContainer: {
|
||
flex: 1,
|
||
paddingHorizontal: 16,
|
||
paddingTop: 16,
|
||
},
|
||
|
||
// 照片卡片样式
|
||
photoCard: {
|
||
backgroundColor: Colors.light.card,
|
||
borderRadius: 24,
|
||
padding: 20,
|
||
marginBottom: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 8 },
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 20,
|
||
elevation: 8,
|
||
},
|
||
photoFrame: {
|
||
width: '100%',
|
||
aspectRatio: 1,
|
||
borderRadius: 20,
|
||
overflow: 'hidden',
|
||
backgroundColor: Colors.light.neutral100,
|
||
position: 'relative',
|
||
},
|
||
photoImage: {
|
||
width: '100%',
|
||
height: '100%',
|
||
},
|
||
mealTypeBadge: {
|
||
position: 'absolute',
|
||
top: 16,
|
||
right: 16,
|
||
paddingHorizontal: 14,
|
||
paddingVertical: 8,
|
||
backgroundColor: 'rgba(122, 90, 248, 0.95)',
|
||
borderRadius: 20,
|
||
backdropFilter: 'blur(10px)',
|
||
},
|
||
mealTypeBadgeText: {
|
||
color: Colors.light.onPrimary,
|
||
fontSize: 13,
|
||
fontWeight: '700',
|
||
letterSpacing: 0.3,
|
||
},
|
||
|
||
// 信息卡片样式
|
||
infoCard: {
|
||
backgroundColor: Colors.light.card,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
marginBottom: 24,
|
||
shadowColor: Colors.light.primary,
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 16,
|
||
elevation: 4,
|
||
borderWidth: 1,
|
||
borderColor: Colors.light.heroSurfaceTint,
|
||
},
|
||
infoHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
},
|
||
aiIconContainer: {
|
||
width: 32,
|
||
height: 32,
|
||
borderRadius: 16,
|
||
backgroundColor: Colors.light.heroSurfaceTint,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 12,
|
||
},
|
||
infoTitle: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: Colors.light.text,
|
||
letterSpacing: 0.2,
|
||
},
|
||
infoDescription: {
|
||
fontSize: 14,
|
||
color: Colors.light.textSecondary,
|
||
lineHeight: 22,
|
||
letterSpacing: 0.1,
|
||
},
|
||
|
||
// 按钮样式
|
||
bottomContainer: {
|
||
paddingBottom: 40,
|
||
paddingTop: 8,
|
||
},
|
||
confirmButtonContainer: {},
|
||
confirmButton: {
|
||
backgroundColor: Colors.light.primary,
|
||
paddingVertical: 18,
|
||
paddingHorizontal: 32,
|
||
borderRadius: 28,
|
||
alignItems: 'center',
|
||
shadowColor: Colors.light.primary,
|
||
shadowOffset: { width: 0, height: 8 },
|
||
shadowOpacity: 0.35,
|
||
shadowRadius: 20,
|
||
elevation: 8,
|
||
},
|
||
confirmButtonContent: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
},
|
||
confirmButtonText: {
|
||
color: Colors.light.onPrimary,
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
letterSpacing: 0.4,
|
||
},
|
||
|
||
// 识别过程容器
|
||
recognitionContainer: {
|
||
flex: 1,
|
||
},
|
||
|
||
// 缩略图卡片
|
||
thumbnailCard: {
|
||
backgroundColor: Colors.light.card,
|
||
borderRadius: 20,
|
||
padding: 16,
|
||
marginBottom: 16,
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
elevation: 4,
|
||
},
|
||
thumbnailImage: {
|
||
width: 70,
|
||
height: 70,
|
||
borderRadius: 16,
|
||
},
|
||
thumbnailInfo: {
|
||
flex: 1,
|
||
marginLeft: 16,
|
||
},
|
||
thumbnailMealType: {
|
||
alignSelf: 'flex-start',
|
||
paddingHorizontal: 12,
|
||
paddingVertical: 6,
|
||
backgroundColor: Colors.light.primary,
|
||
borderRadius: 16,
|
||
marginBottom: 6,
|
||
},
|
||
thumbnailMealTypeText: {
|
||
color: Colors.light.onPrimary,
|
||
fontSize: 11,
|
||
fontWeight: '700',
|
||
letterSpacing: 0.2,
|
||
},
|
||
thumbnailTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '600',
|
||
color: Colors.light.text,
|
||
letterSpacing: 0.1,
|
||
},
|
||
|
||
// 进度卡片
|
||
progressCard: {
|
||
backgroundColor: Colors.light.card,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
marginBottom: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 6 },
|
||
shadowOpacity: 0.08,
|
||
shadowRadius: 16,
|
||
elevation: 6,
|
||
},
|
||
progressHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'flex-start',
|
||
},
|
||
statusIconAnimated: {},
|
||
statusIcon: {
|
||
width: 48,
|
||
height: 48,
|
||
borderRadius: 24,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
marginRight: 16,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: 0.1,
|
||
shadowRadius: 8,
|
||
elevation: 3,
|
||
},
|
||
progressInfo: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
},
|
||
statusText: {
|
||
fontSize: 18,
|
||
fontWeight: '700',
|
||
color: Colors.light.text,
|
||
marginBottom: 4,
|
||
letterSpacing: 0.2,
|
||
},
|
||
statusSubtext: {
|
||
fontSize: 14,
|
||
color: Colors.light.textSecondary,
|
||
lineHeight: 20,
|
||
letterSpacing: 0.1,
|
||
},
|
||
|
||
// 进度条
|
||
progressBarContainer: {
|
||
marginTop: 20,
|
||
},
|
||
progressBarBackground: {
|
||
width: '100%',
|
||
height: 8,
|
||
backgroundColor: Colors.light.neutral100,
|
||
borderRadius: 4,
|
||
overflow: 'hidden',
|
||
},
|
||
progressBarFill: {
|
||
height: '100%',
|
||
backgroundColor: Colors.light.primary,
|
||
borderRadius: 4,
|
||
},
|
||
|
||
// 日志卡片
|
||
logCard: {
|
||
backgroundColor: Colors.light.card,
|
||
borderRadius: 20,
|
||
padding: 20,
|
||
marginBottom: 16,
|
||
flex: 1,
|
||
minHeight: 200,
|
||
shadowColor: '#000',
|
||
shadowOffset: { width: 0, height: 4 },
|
||
shadowOpacity: 0.06,
|
||
shadowRadius: 12,
|
||
elevation: 4,
|
||
},
|
||
logHeader: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
marginBottom: 16,
|
||
},
|
||
logTitle: {
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
color: Colors.light.text,
|
||
marginLeft: 8,
|
||
letterSpacing: 0.2,
|
||
},
|
||
logScrollView: {
|
||
flex: 1,
|
||
backgroundColor: Colors.light.heroSurfaceTint,
|
||
borderRadius: 16,
|
||
paddingHorizontal: 16,
|
||
paddingVertical: 12,
|
||
},
|
||
logContent: {
|
||
flexGrow: 1,
|
||
},
|
||
logItem: {
|
||
paddingVertical: 6,
|
||
paddingHorizontal: 4,
|
||
},
|
||
logText: {
|
||
fontSize: 14,
|
||
color: Colors.light.text,
|
||
lineHeight: 22,
|
||
letterSpacing: 0.1,
|
||
},
|
||
logPlaceholder: {
|
||
fontSize: 14,
|
||
color: Colors.light.textMuted,
|
||
fontStyle: 'italic',
|
||
textAlign: 'center',
|
||
marginTop: 40,
|
||
},
|
||
|
||
// 重试按钮
|
||
retryButton: {
|
||
backgroundColor: Colors.light.primary,
|
||
paddingVertical: 18,
|
||
paddingHorizontal: 32,
|
||
borderRadius: 28,
|
||
alignItems: 'center',
|
||
flexDirection: 'row',
|
||
justifyContent: 'center',
|
||
shadowColor: Colors.light.primary,
|
||
shadowOffset: { width: 0, height: 8 },
|
||
shadowOpacity: 0.35,
|
||
shadowRadius: 20,
|
||
elevation: 8,
|
||
},
|
||
retryButtonText: {
|
||
color: Colors.light.onPrimary,
|
||
fontSize: 16,
|
||
fontWeight: '700',
|
||
letterSpacing: 0.4,
|
||
},
|
||
|
||
// 通用样式
|
||
errorContainer: {
|
||
flex: 1,
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
},
|
||
errorText: {
|
||
fontSize: 16,
|
||
color: Colors.light.textMuted,
|
||
textAlign: 'center',
|
||
},
|
||
}); |