335 lines
10 KiB
TypeScript
335 lines
10 KiB
TypeScript
import { addMaterial, confirmUpload, getUploadUrl } from '@/lib/background-uploader/api';
|
||
import { ConfirmUpload, FileUploadItem, UploadResult, UploadTask } from '@/lib/background-uploader/types';
|
||
import { uploadFileWithProgress } from '@/lib/background-uploader/uploader';
|
||
import { compressImage } from '@/lib/image-process/imageCompress';
|
||
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 UploadPreview from './preview';
|
||
|
||
export const ImagesUploader: React.FC<ImagesuploaderProps> = ({
|
||
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<FileUploadItem[]>([]);
|
||
|
||
// 请求权限
|
||
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;
|
||
};
|
||
|
||
// 处理单个资源
|
||
const processSingleAsset = async (asset: ImagePicker.ImagePickerAsset): Promise<UploadResult | null> => {
|
||
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,
|
||
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 {
|
||
// 处理图片,主图和缩略图都用 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 });
|
||
await uploadFileWithProgress(task.file, uploadUrlData.upload_url, updateProgress, 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: 'done' 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<UploadResult[]> => {
|
||
// 设置最大并发数
|
||
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 (
|
||
<View style={[style]}>
|
||
{children ? (
|
||
<TouchableOpacity onPress={pickImage} disabled={isLoading} activeOpacity={0.7}>
|
||
{children}
|
||
</TouchableOpacity>
|
||
) : (
|
||
<Button
|
||
title={isLoading ? '处理中...' : '选择图片'}
|
||
onPress={pickImage}
|
||
disabled={isLoading}
|
||
/>
|
||
)}
|
||
|
||
{/* 上传预览 */}
|
||
{showPreview && <UploadPreview items={uploadQueue} />}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default ImagesUploader; |