import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors, palette } from '@/constants/Colors'; import { useI18n } from '@/hooks/useI18n'; 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 STEP_KEYS: MedicationRecognitionTask['status'][] = [ 'analyzing_product', 'analyzing_suitability', 'analyzing_ingredients', 'analyzing_effects', ]; export default function MedicationAiProgressScreen() { const { t } = useI18n(); 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 steps = useMemo(() => STEP_KEYS.map(key => ({ key, label: t(`medications.aiProgress.steps.${key}`) })), [t]); const currentStepIndex = useMemo(() => { if (!task) return 0; const idx = STEP_KEYS.indexOf(task.status as any); if (idx >= 0) return idx; if (task.status === 'completed') return STEP_KEYS.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 || t('medications.aiProgress.errors.default')); setShowErrorModal(true); } } catch (err: any) { console.error('[MEDICATION_AI] status failed', err); setError(err?.message || t('medications.aiProgress.errors.queryFailed')); } 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 / 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)}% {steps.map((step, index) => { const active = index === currentStepIndex; const done = index < currentStepIndex; return ( {step.label} ); })} {task?.status === 'completed' && ( {t('medications.aiProgress.steps.completed')} )} {loading ? : null} {error ? {error} : null} {/* 识别提示弹窗 */} e.stopPropagation()} style={styles.errorModalContainer} > {/* 标题 */} {t('medications.aiProgress.modal.title')} {/* 提示信息 */} {errorMessage} {/* 重新拍摄按钮 */} {isLiquidGlassAvailable() ? ( {t('medications.aiProgress.modal.retry')} ) : ( {t('medications.aiProgress.modal.retry')} )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, heroCard: { marginHorizontal: 20, marginTop: 24, borderRadius: 24, backgroundColor: Colors.light.card, padding: 16, shadowColor: Colors.light.text, shadowOpacity: 0.08, shadowRadius: 18, shadowOffset: { width: 0, height: 10 }, }, heroImageWrapper: { height: 230, borderRadius: 18, overflow: 'hidden', backgroundColor: palette.gray[50], }, heroImage: { width: '100%', height: '100%', }, heroPlaceholder: { flex: 1, backgroundColor: palette.gray[50], }, // 深色蒙版层,让点阵更清晰可见 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: Colors.light.background, shadowColor: Colors.light.primary, shadowOpacity: 0.9, shadowRadius: 6, shadowOffset: { width: 0, height: 0 }, }, progressRow: { height: 8, backgroundColor: palette.gray[50], borderRadius: 10, marginTop: 14, overflow: 'hidden', }, progressBar: { height: '100%', borderRadius: 10, backgroundColor: Colors.light.primary, }, progressText: { marginTop: 8, fontSize: 14, fontWeight: '700', color: Colors.light.text, textAlign: 'right', }, stepList: { marginTop: 24, marginHorizontal: 24, gap: 14, }, stepRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, bullet: { width: 14, height: 14, borderRadius: 7, backgroundColor: palette.gray[50], }, bulletActive: { backgroundColor: Colors.light.primary, }, bulletDone: { backgroundColor: Colors.light.success, }, stepLabel: { fontSize: 15, color: Colors.light.textMuted, }, stepLabelActive: { color: Colors.light.text, fontWeight: '700', }, stepLabelDone: { color: Colors.light.successDark, fontWeight: '700', }, loadingBox: { marginTop: 30, alignItems: 'center', gap: 12, }, errorText: { color: Colors.light.danger, fontSize: 14, }, // Modal 样式 modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(15, 23, 42, 0.4)', }, errorModalContainer: { width: SCREEN_WIDTH - 48, backgroundColor: Colors.light.card, borderRadius: 28, overflow: 'hidden', shadowColor: Colors.light.primary, 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: palette.purple[50], alignItems: 'center', justifyContent: 'center', }, errorModalTitle: { fontSize: 22, fontWeight: '700', color: Colors.light.text, marginBottom: 16, textAlign: 'center', }, errorMessageBox: { backgroundColor: palette.purple[25], borderRadius: 16, padding: 20, marginBottom: 28, width: '100%', borderWidth: 1, borderColor: palette.purple[200], }, errorMessageText: { fontSize: 15, lineHeight: 24, color: Colors.light.textSecondary, textAlign: 'center', }, retryButton: { borderRadius: 16, overflow: 'hidden', shadowColor: Colors.light.primary, 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: Colors.light.onPrimary, }, });