2025-08-05 13:52:49 +08:00

331 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { requestLocationPermission, requestMediaLibraryPermission } from '@/components/owner/utils';
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 { PermissionService } from '@/lib/PermissionService';
import { createVideoThumbnailFile } from '@/lib/video-process/videoThumbnail';
import * as ImagePicker from 'expo-image-picker';
import * as MediaLibrary from 'expo-media-library';
import React, { useEffect, useState } from 'react';
import { 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 [files, setFiles] = useState<FileUploadItem[]>([]);
const [uploadQueue, setUploadQueue] = useState<FileUploadItem[]>([]);
// 处理单个资源
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,
uri: asset.uri,
previewUrl: asset.uri, // 使用 asset.uri 作为初始预览
preview: 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<UploadResult[]> => {
// 设置最大并发数
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 (
<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;