import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { getMedicationRecognitionStatus } from '@/services/medications'; import { MedicationRecognitionTask } from '@/types/medication'; import { Ionicons } from '@expo/vector-icons'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Animated, Dimensions, Modal, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const STATUS_STEPS: { key: MedicationRecognitionTask['status']; label: string }[] = [ { key: 'analyzing_product', label: '正在进行产品分析...' }, { key: 'analyzing_suitability', label: '正在检测适宜人群...' }, { key: 'analyzing_ingredients', label: '正在评估成分信息...' }, { key: 'analyzing_effects', label: '正在生成安全建议...' }, ]; export default function MedicationAiProgressScreen() { const { taskId, cover } = useLocalSearchParams<{ taskId?: string; cover?: string }>(); const insets = useSafeAreaInsets(); const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showErrorModal, setShowErrorModal] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const navigatingRef = useRef(false); const pollingTimerRef = useRef | null>(null); // 动画值:上下浮动和透明度 const floatAnim = useRef(new Animated.Value(0)).current; const opacityAnim = useRef(new Animated.Value(0.3)).current; const currentStepIndex = useMemo(() => { if (!task) return 0; const idx = STATUS_STEPS.findIndex((step) => step.key === task.status); if (idx >= 0) return idx; if (task.status === 'completed') return STATUS_STEPS.length; return 0; }, [task]); const fetchStatus = async () => { if (!taskId || navigatingRef.current) return; try { const data = await getMedicationRecognitionStatus(taskId as string); setTask(data); setError(null); // 识别成功,跳转到详情页 if (data.status === 'completed' && data.result && !navigatingRef.current) { navigatingRef.current = true; // 清除轮询 if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } router.replace({ pathname: '/medications/[medicationId]', params: { medicationId: 'ai-draft', aiTaskId: data.taskId, cover: (cover as string) || data.result.photoUrl || '', }, }); } // 识别失败,停止轮询并显示错误弹窗 if (data.status === 'failed' && !navigatingRef.current) { navigatingRef.current = true; // 清除轮询 if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } // 显示错误提示弹窗 setErrorMessage(data.errorMessage || '识别失败,请重新拍摄'); setShowErrorModal(true); } } catch (err: any) { console.error('[MEDICATION_AI] status failed', err); setError(err?.message || '查询失败,请稍后再试'); } finally { setLoading(false); } }; // 处理重新拍摄 const handleRetry = () => { setShowErrorModal(false); router.back(); }; useEffect(() => { fetchStatus(); pollingTimerRef.current = setInterval(fetchStatus, 2400); return () => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } }; }, [taskId]); // 启动浮动和闪烁动画 - 更快的动画速度 useEffect(() => { // 上下浮动动画 - 加快速度 const floatAnimation = Animated.loop( Animated.sequence([ Animated.timing(floatAnim, { toValue: -10, duration: 1000, useNativeDriver: true, }), Animated.timing(floatAnim, { toValue: 0, duration: 1000, useNativeDriver: true, }), ]) ); // 透明度闪烁动画 - 加快速度,增加对比度 const opacityAnimation = Animated.loop( Animated.sequence([ Animated.timing(opacityAnim, { toValue: 1, duration: 800, useNativeDriver: true, }), Animated.timing(opacityAnim, { toValue: 0.4, duration: 800, useNativeDriver: true, }), ]) ); floatAnimation.start(); opacityAnimation.start(); return () => { floatAnimation.stop(); opacityAnimation.stop(); }; }, []); const progress = task?.progress ?? Math.min(100, (currentStepIndex / STATUS_STEPS.length) * 100 + 10); return ( router.back()} transparent /> {cover ? ( ) : ( )} {/* 识别中的点阵网格动画效果 - 带深色蒙版 */} {task?.status !== 'completed' && task?.status !== 'failed' && ( <> {/* 深色半透明蒙版层,让点阵更清晰 */} {/* 渐变蒙版边框,增加视觉层次 */} {/* 点阵网格动画 */} {Array.from({ length: 11 }).map((_, idx) => ( {Array.from({ length: 11 }).map((__, jdx) => ( ))} ))} )} {Math.round(progress)}% {STATUS_STEPS.map((step, index) => { const active = index === currentStepIndex; const done = index < currentStepIndex; return ( {step.label} ); })} {task?.status === 'completed' && ( 识别完成,正在载入详情... )} {loading ? : null} {error ? {error} : null} {/* 识别提示弹窗 */} e.stopPropagation()} style={styles.errorModalContainer} > {/* 标题 */} 需要重新拍摄 {/* 提示信息 */} {errorMessage} {/* 重新拍摄按钮 */} {isLiquidGlassAvailable() ? ( 重新拍摄 ) : ( 重新拍摄 )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, heroCard: { marginHorizontal: 20, marginTop: 24, borderRadius: 24, backgroundColor: '#fff', padding: 16, shadowColor: '#0f172a', shadowOpacity: 0.08, shadowRadius: 18, shadowOffset: { width: 0, height: 10 }, }, heroImageWrapper: { height: 230, borderRadius: 18, overflow: 'hidden', backgroundColor: '#e2e8f0', }, heroImage: { width: '100%', height: '100%', }, heroPlaceholder: { flex: 1, backgroundColor: '#e2e8f0', }, // 深色蒙版层,让点阵更清晰可见 overlayMask: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, backgroundColor: 'rgba(15, 23, 42, 0.35)', }, // 渐变边框效果 gradientBorder: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, borderRadius: 18, }, // 点阵网格容器 dottedGrid: { position: 'absolute', left: 16, right: 16, top: 16, bottom: 16, justifyContent: 'space-between', }, dotRow: { flexDirection: 'row', justifyContent: 'space-between', }, // 单个点样式 - 更明亮和更大的发光效果 dot: { width: 5, height: 5, borderRadius: 2.5, backgroundColor: '#FFFFFF', shadowColor: '#0ea5e9', shadowOpacity: 0.9, shadowRadius: 6, shadowOffset: { width: 0, height: 0 }, }, progressRow: { height: 8, backgroundColor: '#f1f5f9', borderRadius: 10, marginTop: 14, overflow: 'hidden', }, progressBar: { height: '100%', borderRadius: 10, backgroundColor: '#0ea5e9', }, progressText: { marginTop: 8, fontSize: 14, fontWeight: '700', color: '#0f172a', textAlign: 'right', }, stepList: { marginTop: 24, marginHorizontal: 24, gap: 14, }, stepRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, bullet: { width: 14, height: 14, borderRadius: 7, backgroundColor: '#e2e8f0', }, bulletActive: { backgroundColor: '#0ea5e9', }, bulletDone: { backgroundColor: '#22c55e', }, stepLabel: { fontSize: 15, color: '#94a3b8', }, stepLabelActive: { color: '#0f172a', fontWeight: '700', }, stepLabelDone: { color: '#16a34a', fontWeight: '700', }, loadingBox: { marginTop: 30, alignItems: 'center', gap: 12, }, errorText: { color: '#ef4444', fontSize: 14, }, // Modal 样式 modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(15, 23, 42, 0.4)', }, errorModalContainer: { width: SCREEN_WIDTH - 48, backgroundColor: '#FFFFFF', borderRadius: 28, overflow: 'hidden', shadowColor: '#0ea5e9', shadowOpacity: 0.15, shadowRadius: 24, shadowOffset: { width: 0, height: 8 }, elevation: 8, }, errorModalContent: { padding: 32, alignItems: 'center', }, errorIconContainer: { marginBottom: 24, }, errorIconCircle: { width: 96, height: 96, borderRadius: 48, backgroundColor: 'rgba(14, 165, 233, 0.08)', alignItems: 'center', justifyContent: 'center', }, errorModalTitle: { fontSize: 22, fontWeight: '700', color: '#0f172a', marginBottom: 16, textAlign: 'center', }, errorMessageBox: { backgroundColor: '#f0f9ff', borderRadius: 16, padding: 20, marginBottom: 28, width: '100%', borderWidth: 1, borderColor: 'rgba(14, 165, 233, 0.2)', }, errorMessageText: { fontSize: 15, lineHeight: 24, color: '#475569', textAlign: 'center', }, retryButton: { borderRadius: 16, overflow: 'hidden', shadowColor: '#0ea5e9', shadowOpacity: 0.25, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 6, }, retryButtonGradient: { paddingVertical: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, retryButtonText: { fontSize: 18, fontWeight: '700', color: '#FFFFFF', }, });