Files
digital-pilates/app/medications/ai-progress.tsx
richarjiang bca6670390 Add Chinese translations for medication management and personal settings
- Introduced new translation files for medication, personal, and weight management in Chinese.
- Updated the main index file to include the new translation modules.
- Enhanced the medication type definitions to include 'ointment'.
- Refactored workout type labels to utilize i18n for better localization support.
- Improved sleep quality descriptions and recommendations with i18n integration.
2025-11-28 17:29:51 +08:00

522 lines
15 KiB
TypeScript

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<MedicationRecognitionTask | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showErrorModal, setShowErrorModal] = useState(false);
const [errorMessage, setErrorMessage] = useState<string>('');
const navigatingRef = useRef(false);
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | 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 (
<SafeAreaView style={styles.container}>
<LinearGradient colors={[palette.gray[25], palette.gray[50]]} style={StyleSheet.absoluteFill} />
<HeaderBar title={t('medications.aiProgress.title')} onBack={() => router.back()} transparent />
<View style={{ height: insets.top }} />
<View style={styles.heroCard}>
<View style={styles.heroImageWrapper}>
{cover ? (
<Image source={{ uri: cover }} style={styles.heroImage} contentFit="cover" />
) : (
<View style={styles.heroPlaceholder} />
)}
{/* 识别中的点阵网格动画效果 - 带深色蒙版 */}
{task?.status !== 'completed' && task?.status !== 'failed' && (
<>
{/* 深色半透明蒙版层,让点阵更清晰 */}
<View style={styles.overlayMask} />
{/* 渐变蒙版边框,增加视觉层次 */}
<LinearGradient
colors={[Colors.light.primary + '4D', Colors.light.accentPurple + '33', 'transparent']}
style={styles.gradientBorder}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{/* 点阵网格动画 */}
<Animated.View
style={[
styles.dottedGrid,
{
transform: [{ translateY: floatAnim }],
opacity: opacityAnim,
}
]}
>
{Array.from({ length: 11 }).map((_, idx) => (
<View key={idx} style={styles.dotRow}>
{Array.from({ length: 11 }).map((__, jdx) => (
<View key={`${idx}-${jdx}`} style={styles.dot} />
))}
</View>
))}
</Animated.View>
</>
)}
</View>
<View style={styles.progressRow}>
<View style={[styles.progressBar, { width: `${progress}%` }]} />
</View>
<Text style={styles.progressText}>{Math.round(progress)}%</Text>
</View>
<View style={styles.stepList}>
{steps.map((step, index) => {
const active = index === currentStepIndex;
const done = index < currentStepIndex;
return (
<View key={step.key} style={styles.stepRow}>
<View style={[styles.bullet, done && styles.bulletDone, active && styles.bulletActive]} />
<Text style={[styles.stepLabel, active && styles.stepLabelActive, done && styles.stepLabelDone]}>
{step.label}
</Text>
</View>
);
})}
{task?.status === 'completed' && (
<View style={styles.stepRow}>
<View style={[styles.bullet, styles.bulletDone]} />
<Text style={[styles.stepLabel, styles.stepLabelDone]}>{t('medications.aiProgress.steps.completed')}</Text>
</View>
)}
</View>
<View style={styles.loadingBox}>
{loading ? <ActivityIndicator color={Colors.light.primary} /> : null}
{error ? <Text style={styles.errorText}>{error}</Text> : null}
</View>
{/* 识别提示弹窗 */}
<Modal
visible={showErrorModal}
transparent={true}
animationType="fade"
onRequestClose={handleRetry}
>
<TouchableOpacity
style={styles.modalOverlay}
activeOpacity={1}
onPress={handleRetry}
>
<TouchableOpacity
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={styles.errorModalContainer}
>
<View style={styles.errorModalContent}>
{/* 标题 */}
<Text style={styles.errorModalTitle}>{t('medications.aiProgress.modal.title')}</Text>
{/* 提示信息 */}
<View style={styles.errorMessageBox}>
<Text style={styles.errorMessageText}>{errorMessage}</Text>
</View>
{/* 重新拍摄按钮 */}
<TouchableOpacity
onPress={handleRetry}
activeOpacity={0.8}
style={{ width: '100%' }}
>
{isLiquidGlassAvailable() ? (
<GlassView
style={styles.retryButton}
glassEffectStyle="regular"
tintColor={Colors.light.primary}
isInteractive={true}
>
<LinearGradient
colors={[Colors.light.primary, Colors.light.accentPurple]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.retryButtonGradient}
>
<Ionicons name="camera" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}>{t('medications.aiProgress.modal.retry')}</Text>
</LinearGradient>
</GlassView>
) : (
<View style={styles.retryButton}>
<LinearGradient
colors={[Colors.light.primary, Colors.light.accentPurple]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.retryButtonGradient}
>
<Ionicons name="camera" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}>{t('medications.aiProgress.modal.retry')}</Text>
</LinearGradient>
</View>
)}
</TouchableOpacity>
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
</SafeAreaView>
);
}
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,
},
});