import { fetchApi } from '@/lib/server-api-util'; import { ConfirmUpload, defaultExifData, ExifData, ImagesPickerProps, UploadResult } from '@/types/upload'; import * as ImageManipulator from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import * as Location from 'expo-location'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; import * as Progress from 'react-native-progress'; import { FileStatus, UploadUrlResponse } from './file-uploader'; export const ImagesPicker: React.FC = ({ children, style, onPickImage, compressQuality = 0.8, maxWidth = 2048, maxHeight = 2048, preserveExif = true, onUploadComplete, onProgress, }) => { const [isLoading, setIsLoading] = useState(false); // 请求权限 const requestPermissions = async () => { if (Platform.OS !== 'web') { const { status: mediaStatus } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (mediaStatus !== 'granted') { Alert.alert('需要媒体库权限', '请允许访问媒体库以选择图片'); return false; } const { status: locationStatus } = await Location.requestForegroundPermissionsAsync();; if (locationStatus !== 'granted') { Alert.alert('需要位置权限', '需要位置权限才能获取图片位置信息'); } } return true; }; // 获取上传URL const getUploadUrl = async (file: File, metadata: Record = {}): Promise => { const body = { filename: file.name, content_type: file.type, file_size: file.size, metadata: { ...metadata, originalName: file.name, fileType: 'image', isCompressed: metadata.isCompressed || 'false', }, }; return await fetchApi("/file/generate-upload-url", { method: 'POST', body: JSON.stringify(body) }); }; // 向服务端confirm上传 const confirmUpload = async (file_id: string): Promise => await fetchApi('/file/confirm-upload', { method: 'POST', body: JSON.stringify({ file_id }) }); // 新增素材 const addMaterial = async (file: string, compressFile: string) => { await fetchApi('/material', { method: 'POST', body: JSON.stringify([{ "file_id": file, "preview_file_id": compressFile }]) }).catch((error) => { // console.log(error); }) } // 上传文件到URL const uploadFileToUrl = async (file: File, uploadUrl: string, onProgress: (progress: number) => void): Promise => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); // 进度监听 xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); onProgress(progress); } }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(); } else { reject(new Error(`Upload failed with status ${xhr.status}`)); } }; xhr.onerror = () => { reject(new Error('Network error during upload')); }; xhr.send(file); }); }; // 上传进度 const [currentFileStatus, setCurrentFileStatus] = useState({ file: null as unknown as File, status: 'pending', progress: 0 }); // 使用函数更新文件状态,确保每次更新都是原子的 const updateFileStatus = (updates: Partial) => { setCurrentFileStatus((original) => ({ ...original, ...updates })) }; // 上传文件 const uploadFile = async (file: File, metadata: Record = {}): Promise => { try { // 初始化上传状态 updateFileStatus({ status: 'uploading', progress: 1 }); // 添加小延迟,确保初始状态能被看到 await new Promise(resolve => setTimeout(resolve, 300)); // 获取上传URL const { upload_url, file_id } = await getUploadUrl(file, metadata); // 上传文件到URL await uploadFileToUrl( file, upload_url, (progress) => { // 将实际进度映射到 60%-90% 区间 const mappedProgress = 60 + (progress * 0.3); updateFileStatus({ progress: Math.round(mappedProgress) }); } ); // 确认上传到服务器 const fileData = confirmUpload(file_id) // 将fileData, upload_url,file_id 传递出去 return { ...fileData, upload_url, file_id } } catch (error) { console.error('上传文件时出错:', error); updateFileStatus({ status: 'error', error: error instanceof Error ? error.message : '上传失败' }); throw new Error('文件上传失败'); } }; // 压缩并处理图片 const processImage = async (uri: string, fileName: string, mimeType: string) => { try { // 压缩图片 const manipResult = await ImageManipulator.manipulateAsync( uri, [ { resize: { width: maxWidth, height: maxHeight, }, }, ], { compress: compressQuality, format: ImageManipulator.SaveFormat.JPEG, base64: false, } ); // 获取压缩后的图片数据 const response = await fetch(manipResult.uri); const blob = await response.blob(); // 创建文件对象 const file = new File([blob], `compressed_${Date.now()}_${fileName}`, { type: mimeType, }); return { file, uri: manipResult.uri }; } catch (error) { console.error('图片压缩失败:', error); throw new Error('图片处理失败'); } }; // 处理图片选择 const pickImage = async () => { try { setIsLoading(true); const hasPermission = await requestPermissions(); if (!hasPermission) return; const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsMultipleSelection: false, quality: 1, exif: preserveExif, }); if (result.canceled || !result.assets?.[0]) { setIsLoading(false); return; } const asset = result.assets[0]; try { // 获取原图文件 const originalResponse = await fetch(asset.uri); const originalBlob = await originalResponse.blob(); const originalFile = new File( [originalBlob], `original_${Date.now()}_${asset.fileName || 'photo.jpg'}`, { type: asset.mimeType || 'image/jpeg' } ) as File; // 压缩并处理图片 const { file: compressedFile } = await processImage( asset.uri, asset.fileName || 'photo.jpg', asset.mimeType || 'image/jpeg' ); let exifData: ExifData = { ...defaultExifData }; // 如果保留 EXIF 数据,则获取 if (preserveExif && asset.exif) { exifData = { ...exifData, ...asset.exif }; if (asset.uri && Platform.OS !== 'web') { try { const mediaAsset = await MediaLibrary.getAssetInfoAsync(asset.uri); if (mediaAsset.exif) { exifData = { ...exifData, ...mediaAsset.exif }; } if (mediaAsset.location) { exifData.GPSLatitude = mediaAsset.location.latitude; exifData.GPSLongitude = mediaAsset.location.longitude; } } catch (error) { console.warn('从媒体库获取 EXIF 数据失败:', error); } } } // 调用 onPickImage 回调 onPickImage?.(compressedFile, exifData); // 上传文件 const uploadResults: UploadResult = { originalUrl: undefined, compressedUrl: '', file: compressedFile, exifData, originalFile: {} as ConfirmUpload, compressedFile: {} as ConfirmUpload, }; try { // 上传压缩后的图片 const compressedResult = await uploadFile(compressedFile, { isCompressed: 'true', ...exifData, }); uploadResults.originalFile = compressedResult; // 上传原图 const originalResult = await uploadFile(originalFile, { isCompressed: 'false', ...exifData, }); uploadResults.compressedFile = originalResult; // 添加到素材库 await addMaterial(uploadResults.originalFile?.file_id, uploadResults.compressedFile?.file_id); // 等待一些时间再标记为成功 await new Promise(resolve => setTimeout(resolve, 300)); // 更新状态为成功 await updateFileStatus({ status: 'success', progress: 100, id: uploadResults.originalFile?.file_id }); // 调用上传完成回调 onUploadComplete?.(uploadResults); } catch (error) { updateFileStatus({ status: 'error', progress: 0, id: uploadResults.originalFile?.file_id }); throw error; // 重新抛出错误,让外层 catch 处理 } } catch (error) { Alert.alert('错误', '处理图片时出错'); } } catch (error) { Alert.alert('错误', '选择图片时出错,请重试'); } finally { setIsLoading(false); } }; useEffect(() => { if (onProgress) { onProgress(currentFileStatus); } }, [currentFileStatus.progress]); return ( {children ? ( {children} ) : (