import { addMaterial, confirmUpload, getUploadUrl } from '@/lib/background-uploader/api'; import { ConfirmUpload, ExifData, FileUploadItem, ImagesuploaderProps, UploadResult, UploadTask, defaultExifData } from '@/lib/background-uploader/types'; import { uploadFileWithProgress } from '@/lib/background-uploader/uploader'; import { compressImage } from '@/lib/image-process/imageCompress'; import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail'; import * as ImagePicker from 'expo-image-picker'; import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils'; import { PermissionService } from '@/lib/PermissionService'; import * as MediaLibrary from 'expo-media-library'; import React, { useEffect, useState } from 'react'; import { Alert, Button, Platform, TouchableOpacity, View } from 'react-native'; import UploadPreview from './preview'; 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 [files, setFiles] = useState([]); const [uploadQueue, setUploadQueue] = useState([]); // 处理单个资源 const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise => { console.log("asset111111", asset); 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, uri: asset.uri, previewUrl: asset.uri, // 使用 asset.uri 作为初始预览 name: asset.fileName || 'file', progress: 0, status: 'uploading', error: undefined, type: isVideo ? 'video' : 'image', thumbnail: undefined, }; 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' } ); // 使用复用函数生成视频缩略图 thumbnailFile = await createVideoThumbnailFile(asset, 300); } else { // 处理图片,主图和缩略图都用 compressImage 方法 // 主图压缩(按 maxWidth/maxHeight/compressQuality) const { file: compressedFile } = await compressImage(asset.uri, maxWidth); // 缩略图压缩(宽度800) const { file: thumbFile } = await compressImage(asset.uri, 800); // 如果保留 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); } } } // 用压缩后主图作为上传主文件 file = compressedFile as File; // 用缩略图文件作为预览 thumbnailFile = thumbFile as File; } // 准备上传任务 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 { // 统一通过 lib 的 uploadFileWithProgress 实现上传 const uploadUrlData = await getUploadUrl(task.file, { ...task.metadata, GPSVersionID: undefined }); const taskIndex = uploadTasks.indexOf(task); const totalTasks = uploadTasks.length; const baseProgress = (taskIndex / totalTasks) * 100; await uploadFileWithProgress( task.file, uploadUrlData.upload_url, (progress) => { const taskProgress = progress.total > 0 ? (progress.loaded / progress.total) * (100 / totalTasks) : 0; updateProgress(baseProgress + taskProgress); }, 30000 ); const result = await confirmUpload(uploadUrlData.file_id); uploadResultsList.push({ ...result, file_id: uploadUrlData.file_id, upload_url: uploadUrlData.upload_url }); } 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: 'success' as const, progress: 100, thumbnail: uploadResults.thumbnail } : item ) ); // 添加到素材库 if (uploadResults.originalFile?.file_id) { await addMaterial( uploadResults.originalFile.file_id, uploadResults.compressedFile?.file_id ); } 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[] = []; // 分批处理资源,优化并发处理 const processBatch = async (batch: ImagePicker.ImagePickerAsset[]) => { 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); } } }; // 使用 Promise.all 并行处理所有批次 const batches = []; for (let i = 0; i < assets.length; i += CONCURRENCY_LIMIT) { batches.push(assets.slice(i, i + CONCURRENCY_LIMIT)); } // 并行处理所有批次,但限制并发数量 for (let i = 0; i < batches.length; i += CONCURRENCY_LIMIT) { const batchGroup = batches.slice(i, i + CONCURRENCY_LIMIT); await Promise.all(batchGroup.map(processBatch)); } return results; }; // 处理图片选择 const pickImage = async () => { try { setIsLoading(true); const hasMediaPermission = await requestMediaLibraryPermission(); if (!hasMediaPermission) { setIsLoading(false); return; } // 请求位置权限,但不强制要求 await requestLocationPermission(); 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) { PermissionService.show({ title: '错误', message: '部分文件处理失败,请重试' }); } finally { setIsLoading(false); } } catch (error) { PermissionService.show({ title: '错误', message: '选择图片时出错,请重试' }); } finally { setIsLoading(false); } }; // 在组件卸载时清理已完成的上传 useEffect(() => { return () => { // 只保留未完成的上传项 setUploadQueue(prev => prev.filter(item => item.status === 'uploading' || item.status === 'pending' ) ); }; }, []); return ( {children ? ( {children} ) : (