import { fetchApi } from '@/lib/server-api-util'; import { defaultExifData, ExifData, ImagesuploaderProps } 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 { UploadUrlResponse } from './file-uploader'; import UploadPreview from './preview'; // 在文件顶部添加这些类型 type UploadTask = { file: File; metadata: { isCompressed: string; type: string; isThumbnail?: string; [key: string]: any; }; }; type FileUploadItem = { id: string; name: string; progress: number; status: 'pending' | 'uploading' | 'done' | 'error'; error: string | null; type: 'image' | 'video'; thumbnail: string | null; }; type ConfirmUpload = { file_id: string; upload_url: string; name: string; size: number; content_type: string; file_path: string; }; type UploadResult = { originalUrl?: string; compressedUrl: string; file: File | null; exif: any; originalFile: ConfirmUpload; compressedFile: ConfirmUpload; thumbnail: string; thumbnailFile: File; }; export const ImagesUploader: React.FC = ({ children, style, compressQuality = 0.8, maxWidth = 2048, maxHeight = 2048, preserveExif = true, onUploadComplete, multipleChoice = false, showPreview = true, fileType = ['images'], }) => { const [isLoading, setIsLoading] = useState(false); const [uploadQueue, setUploadQueue] = useState([]); // 请求权限 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 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 uploadWithProgress = async (file: File, metadata: any): Promise => { let timeoutId: number try { console.log("Starting upload for file:", file.name, "size:", file.size, "type:", file.type); // 检查文件大小 const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB if (file.size > MAX_FILE_SIZE) { throw new Error(`文件大小超过限制 (${(MAX_FILE_SIZE / 1024 / 1024).toFixed(1)}MB)`); } const uploadUrlData = await getUploadUrl(file, {}); console.log("Got upload URL for:", file.name); return new Promise((resolve, reject) => { try { // 设置超时 timeoutId = setTimeout(() => { reject(new Error('上传超时,请检查网络连接')); }, 30000); // 上传文件 const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrlData.upload_url, true); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) { const progress = Math.round((event.loaded / event.total) * 100); console.log(`Upload progress for ${file.name}: ${progress}%`); } }; xhr.onload = async () => { clearTimeout(timeoutId!); if (xhr.status >= 200 && xhr.status < 300) { try { const result = await confirmUpload(uploadUrlData.file_id); resolve({ ...result, file_id: uploadUrlData.file_id, upload_url: uploadUrlData.upload_url, }); } catch (error) { reject(error); } } else { reject(new Error(`上传失败,状态码: ${xhr.status}`)); } }; xhr.onerror = () => { clearTimeout(timeoutId!); reject(new Error('网络错误,请检查网络连接')); }; xhr.send(file); } catch (error) { clearTimeout(timeoutId!); reject(error); } }); } catch (error) { console.error('Error in uploadWithProgress:', { error, fileName: file?.name, fileSize: file?.size, fileType: file?.type }); throw error; } }; // 处理单个资源 const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise => { const fileId = `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const isVideo = asset.type === 'video'; const uploadResults: UploadResult = { originalUrl: undefined, compressedUrl: '', file: null, exif: {}, originalFile: {} as ConfirmUpload, compressedFile: {} as ConfirmUpload, thumbnail: '', thumbnailFile: {} as File, }; // 创建上传项 const newFileItem: FileUploadItem = { id: fileId, name: asset.fileName || 'file', progress: 0, status: 'uploading' as const, error: null, type: isVideo ? 'video' : 'image', thumbnail: null, }; setUploadQueue(prev => [...prev, newFileItem]); const updateProgress = (progress: number) => { setUploadQueue(prev => prev.map(item => item.id === fileId ? { ...item, progress } : item ) ); }; try { let file: File; let thumbnailFile: File | null = null; let exifData: ExifData = { ...defaultExifData }; if (isVideo) { // 处理视频文件 file = new File( [await (await fetch(asset.uri)).blob()], `video_${Date.now()}.mp4`, { type: 'video/mp4' } ); // 生成视频缩略图 const thumbnailResult = await ImageManipulator.manipulateAsync( asset.uri, [{ resize: { width: 300 } }], { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG } ); thumbnailFile = new File( [await (await fetch(thumbnailResult.uri)).blob()], `thumb_${Date.now()}.jpg`, { type: 'image/jpeg' } ); } else { // 处理图片 const [originalResponse, compressedFileResult] = await Promise.all([ fetch(asset.uri), ImageManipulator.manipulateAsync( asset.uri, [{ resize: { width: 800 } }], { compress: 0.7, format: ImageManipulator.SaveFormat.JPEG } ) ]); // 如果保留 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); } } } const originalBlob = await originalResponse.blob(); const compressedBlob = await compressedFileResult.file; file = new File( [originalBlob], `original_${Date.now()}_${asset.fileName || 'photo.jpg'}`, { type: asset.mimeType || 'image/jpeg' } ); thumbnailFile = new File( [compressedBlob], `compressed_${Date.now()}_${asset.fileName || 'photo.jpg'}`, { type: 'image/jpeg' } ); } // 准备上传任务 const uploadTasks: UploadTask[] = [ { file, metadata: { isCompressed: 'false', type: isVideo ? 'video' : 'image', ...(isVideo ? {} : exifData) } } ]; if (thumbnailFile) { uploadTasks.push({ file: thumbnailFile, metadata: { isCompressed: 'true', type: 'image', isThumbnail: 'true' } }); } // 顺序上传文件 const uploadResultsList = []; for (const task of uploadTasks) { try { const result = await uploadWithProgress(task.file, task.metadata); uploadResultsList.push(result); } catch (error) { console.error('Upload failed:', error); throw error; } } // 处理上传结果 const [mainUpload, thumbnailUpload] = uploadResultsList; uploadResults.originalFile = mainUpload; uploadResults.compressedFile = thumbnailUpload || mainUpload; uploadResults.thumbnail = thumbnailUpload?.upload_url || ''; uploadResults.thumbnailFile = thumbnailFile; // 更新上传状态 updateProgress(100); setUploadQueue(prev => prev.map(item => item.id === fileId ? { ...item, status: 'done' as const, progress: 100, thumbnail: uploadResults.thumbnail } : item ) ); // 添加到素材库 if (uploadResults.originalFile?.file_id) { await addMaterial( uploadResults.originalFile.file_id, uploadResults.thumbnail ); } return uploadResults; } catch (error) { console.error('Error processing file:', error); setUploadQueue(prev => prev.map(item => item.id === fileId ? { ...item, status: 'error' as const, error: error instanceof Error ? error.message : '上传失败' } : item ) ); return null; } }; // 处理所有选中的图片 const processAssets = async (assets: ImagePicker.ImagePickerAsset[]): Promise => { // 设置最大并发数 const CONCURRENCY_LIMIT = 3; const results: UploadResult[] = []; // 分批处理资源 for (let i = 0; i < assets.length; i += CONCURRENCY_LIMIT) { const batch = assets.slice(i, i + CONCURRENCY_LIMIT); // 并行处理当前批次的所有资源 const batchResults = await Promise.allSettled( batch.map(asset => processSingleAsset(asset)) ); // 收集成功的结果 for (const result of batchResults) { if (result.status === 'fulfilled' && result.value) { results.push(result.value); } } // 添加小延迟,避免过多占用系统资源 if (i + CONCURRENCY_LIMIT < assets.length) { await new Promise(resolve => setTimeout(resolve, 100)); } } return results; }; // 处理图片选择 const pickImage = async () => { try { setIsLoading(true); const hasPermission = await requestPermissions(); console.log("hasPermission", hasPermission); if (!hasPermission) return; const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: fileType, allowsMultipleSelection: multipleChoice, quality: 1, exif: preserveExif, }); console.log("result", result?.assets); if (result.canceled || !result.assets) { setIsLoading(false); return; } try { const uploadResults = await processAssets(result.assets); // 所有文件处理完成后的回调 // @ts-ignore onUploadComplete?.(uploadResults?.map((item, index) => { return { ...item, preview: result?.assets?.[index]?.uri } })); } catch (error) { Alert.alert('错误', '部分文件处理失败,请重试'); } finally { setIsLoading(false); } } catch (error) { Alert.alert('错误', '选择图片时出错,请重试'); } finally { setIsLoading(false); } }; // 在组件卸载时清理已完成的上传 useEffect(() => { return () => { // 只保留未完成的上传项 setUploadQueue(prev => prev.filter(item => item.status === 'uploading' || item.status === 'pending' ) ); }; }, []); return ( {children ? ( {children} ) : (