import { Ionicons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import * as ImagePicker from 'expo-image-picker'; import { useRouter } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { Alert, Image, Linking, Platform, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Colors } from '@/constants/Colors'; type PoseView = 'front' | 'side' | 'back'; type UploadState = { front?: string | null; side?: string | null; back?: string | null; }; type Sample = { uri: string; correct: boolean }; const SAMPLES: Record = { front: [ { uri: 'https://images.unsplash.com/photo-1594737625785-c6683fc87c73?w=400&q=80&auto=format', correct: true }, { uri: 'https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?w=400&q=80&auto=format', correct: false }, { uri: 'https://images.unsplash.com/photo-1571019614242-c5c5dee9f50b?w=400&q=80&auto=format', correct: false }, ], side: [ { uri: 'https://images.unsplash.com/photo-1554463529-e27854014799?w=400&q=80&auto=format', correct: true }, { uri: 'https://images.unsplash.com/photo-1596357395104-5bcae0b1a5eb?w=400&q=80&auto=format', correct: false }, { uri: 'https://images.unsplash.com/photo-1526506118085-60ce8714f8c5?w=400&q=80&auto=format', correct: false }, ], back: [ { uri: 'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?w=400&q=80&auto=format', correct: true }, { uri: 'https://images.unsplash.com/photo-1571721797421-f4c9f2b13107?w=400&q=80&auto=format', correct: false }, { uri: 'https://images.unsplash.com/photo-1518611012118-696072aa579a?w=400&q=80&auto=format', correct: false }, ], }; export default function AIPostureAssessmentScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const theme = Colors.dark; const [uploadState, setUploadState] = useState({}); const canStart = useMemo( () => Boolean(uploadState.front && uploadState.side && uploadState.back), [uploadState] ); const [cameraPerm, setCameraPerm] = useState(null); const [libraryPerm, setLibraryPerm] = useState(null); const [libraryAccess, setLibraryAccess] = useState<'all' | 'limited' | 'none' | null>(null); const [cameraCanAsk, setCameraCanAsk] = useState(null); const [libraryCanAsk, setLibraryCanAsk] = useState(null); useEffect(() => { (async () => { const cam = await ImagePicker.getCameraPermissionsAsync(); const lib = await ImagePicker.getMediaLibraryPermissionsAsync(); setCameraPerm(cam.status); setLibraryPerm(lib.status); setLibraryAccess( (lib as any).accessPrivileges ?? (lib.status === 'granted' ? 'all' : 'none') ); setCameraCanAsk(cam.canAskAgain); setLibraryCanAsk(lib.canAskAgain); })(); }, []); async function requestAllPermissions() { try { const cam = await ImagePicker.requestCameraPermissionsAsync(); const lib = await ImagePicker.requestMediaLibraryPermissionsAsync(); setCameraPerm(cam.status); setLibraryPerm(lib.status); setLibraryAccess( (lib as any).accessPrivileges ?? (lib.status === 'granted' ? 'all' : 'none') ); setCameraCanAsk(cam.canAskAgain); setLibraryCanAsk(lib.canAskAgain); const libGranted = lib.status === 'granted' || (lib as any).accessPrivileges === 'limited'; if (cam.status !== 'granted' || !libGranted) { Alert.alert( '权限未完全授予', '请在系统设置中授予相机与相册权限以完成上传', [ { text: '取消', style: 'cancel' }, { text: '去设置', onPress: () => Linking.openSettings() }, ] ); } } catch { } } async function requestPermissionAndPick(source: 'camera' | 'library', key: PoseView) { try { if (source === 'camera') { const resp = await ImagePicker.requestCameraPermissionsAsync(); setCameraPerm(resp.status); setCameraCanAsk(resp.canAskAgain); if (resp.status !== 'granted') { Alert.alert( '权限不足', '需要相机权限以拍摄照片', resp.canAskAgain ? [{ text: '好的' }] : [ { text: '取消', style: 'cancel' }, { text: '去设置', onPress: () => Linking.openSettings() }, ] ); return; } const result = await ImagePicker.launchCameraAsync({ allowsEditing: true, quality: 0.8, aspect: [3, 4], }); if (!result.canceled) { setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null })); } } else { const resp = await ImagePicker.requestMediaLibraryPermissionsAsync(); setLibraryPerm(resp.status); setLibraryAccess( (resp as any).accessPrivileges ?? (resp.status === 'granted' ? 'all' : 'none') ); setLibraryCanAsk(resp.canAskAgain); const libGranted = resp.status === 'granted' || (resp as any).accessPrivileges === 'limited'; if (!libGranted) { Alert.alert( '权限不足', '需要相册权限以选择照片', resp.canAskAgain ? [{ text: '好的' }] : [ { text: '取消', style: 'cancel' }, { text: '去设置', onPress: () => Linking.openSettings() }, ] ); return; } const result = await ImagePicker.launchImageLibraryAsync({ allowsEditing: true, quality: 0.8, aspect: [3, 4], }); if (!result.canceled) { setUploadState((s) => ({ ...s, [key]: result.assets[0]?.uri ?? null })); } } } catch (e) { Alert.alert('发生错误', '选择图片失败,请重试'); } } function handleStart() { if (!canStart) return; // TODO: 调用后端或进入分析页面 Alert.alert('开始测评', '已收集三视角照片,准备开始AI体态分析'); } return ( {/* Header */} router.back()} style={styles.backButton} > AI体态测评 {/* Permissions Banner (iOS 优先提示) */} {Platform.OS === 'ios' && ( (cameraPerm !== 'granted' || !(libraryPerm === 'granted' || libraryAccess === 'limited')) && ( 需要相机与相册权限 授权后可拍摄或选择三视角全身照片用于AI体态测评。 {((cameraCanAsk ?? true) || (libraryCanAsk ?? true)) ? ( 一键授权 ) : ( Linking.openSettings()}> 去设置开启 )} requestPermissionAndPick('library', 'front')}> 稍后再说 ) )} {/* Intro */} 上传标准姿势照片 请依次上传正面、侧面与背面全身照。保持光线均匀、背景简洁,身体立正自然放松。 {/* Upload sections */} requestPermissionAndPick('camera', 'front')} onPickLibrary={() => requestPermissionAndPick('library', 'front')} samples={SAMPLES.front} /> requestPermissionAndPick('camera', 'side')} onPickLibrary={() => requestPermissionAndPick('library', 'side')} samples={SAMPLES.side} /> requestPermissionAndPick('camera', 'back')} onPickLibrary={() => requestPermissionAndPick('library', 'back')} samples={SAMPLES.back} /> {/* Bottom CTA */} {canStart ? '开始测评' : '请先完成三视角上传'} ); } function UploadTile({ label, value, onPickCamera, onPickLibrary, samples, }: { label: string; value?: string | null; onPickCamera: () => void; onPickLibrary: () => void; samples: Sample[]; }) { return ( {label} {value ? ( 可长按替换 ) : ( 需上传此视角 )} {value ? ( ) : ( 拍摄或选择照片 点击拍摄,长按从相册选择 )} 示例 {samples.map((s, idx) => ( {s.correct ? '正确示范' : '错误示范'} ))} ); } const styles = StyleSheet.create({ screen: { flex: 1, }, permBanner: { marginTop: 12, marginHorizontal: 16, padding: 14, borderRadius: 16, backgroundColor: 'rgba(255,255,255,0.04)' }, permTitle: { color: '#ECEDEE', fontSize: 16, fontWeight: '700', }, permDesc: { color: 'rgba(255,255,255,0.75)', marginTop: 6, fontSize: 13, }, permActions: { flexDirection: 'row', gap: 10, marginTop: 10, }, permPrimary: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 14, height: 40, borderRadius: 12, backgroundColor: '#BBF246', }, permPrimaryText: { color: '#192126', fontSize: 14, fontWeight: '800', }, permSecondary: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 14, height: 40, borderRadius: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.18)', }, permSecondaryText: { color: 'rgba(255,255,255,0.85)', fontSize: 14, fontWeight: '700', }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, }, backButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(255,255,255,0.06)', }, headerTitle: { fontSize: 22, color: '#ECEDEE', fontWeight: '700', }, introBox: { marginTop: 12, paddingHorizontal: 20, gap: 10, }, title: { fontSize: 26, color: '#ECEDEE', fontWeight: '800', }, description: { fontSize: 15, lineHeight: 22, color: 'rgba(255,255,255,0.75)', }, section: { marginTop: 16, paddingHorizontal: 16, gap: 12, }, sectionHeader: { paddingHorizontal: 4, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, sectionTitle: { color: '#ECEDEE', fontSize: 18, fontWeight: '700', }, retakeHint: { color: 'rgba(255,255,255,0.55)', fontSize: 13, }, uploader: { height: 220, borderRadius: 18, borderWidth: 1, borderStyle: 'dashed', borderColor: 'rgba(255,255,255,0.18)', backgroundColor: '#1E262C', overflow: 'hidden', }, preview: { width: '100%', height: '100%', }, placeholder: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8, }, plusBadge: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', backgroundColor: '#BBF246', }, placeholderTitle: { color: '#ECEDEE', fontSize: 16, fontWeight: '700', }, placeholderDesc: { color: 'rgba(255,255,255,0.65)', fontSize: 12, }, sampleBox: { marginTop: 8, borderRadius: 16, padding: 12, backgroundColor: 'rgba(255,255,255,0.04)', }, sampleTitle: { color: 'rgba(255,255,255,0.8)', fontSize: 14, marginBottom: 8, fontWeight: '600', }, sampleRow: { flexDirection: 'row', gap: 10, }, sampleItem: { flex: 1, }, sampleImg: { width: '100%', height: 90, borderRadius: 12, backgroundColor: '#111', }, sampleTag: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 8, marginTop: 6, }, sampleTagText: { color: '#192126', fontSize: 12, fontWeight: '700', }, bottomCtaWrap: { position: 'absolute', left: 16, right: 16, bottom: 0, }, bottomCta: { height: 64, borderRadius: 32, alignItems: 'center', justifyContent: 'center', }, bottomCtaText: { fontSize: 18, fontWeight: '800', }, });