flutter 文件上传组件和大文件分片上传

文件分片上传

资料

https://www.cnblogs.com/caijinglong/p/11558389.html

使用分段上传来上传和复制对象 - Amazon Simple Storage Service

因为公司使用的是亚马逊的s3桶  下面是查阅资料获得的

亚马逊s3桶的文件上传分片

分段上分为三个步骤:开始上传、上传对象分段,以及在上传所有分段后完成分段上传。在收到完成分段上传请求后,Amazon S3 会利用上传的分段创建对象,然后您可以像在您的存储桶中访问任何其他对象一样访问该对象。

您可以列出所有正在执行的分段上传,或者获取为特定分段上传操作上传的分段列表。以上每个操作都在本节中进行了说明。

分段上传开始

当您发送请求以开始分段上传时,Amazon S3 将返回具有上传 ID 的响应,此 ID 是分段上传的唯一标识符。无论您何时上传分段、列出分段、完成上传或停止上传,您都必须包括此上传 ID。如果您想要提供描述已上传的对象的任何元数据,必须在请求中提供它以开始分段上传。

分段上传

上传分段时,除了指定上传 ID,还必须指定分段编号。您可以选择 1 和 10000 之间的任意分段编号。分段编号在您正在上传的对象中唯一地识别分段及其位置。您选择的分段编号不必是连续序列(例如,它可以是 1、5 和 14)。如果您使用之前上传的分段的同一分段编号上传新分段,则之前上传的分段将被覆盖。

无论您何时上传分段,Amazon S3 都将在其响应中返回实体标签 (ETag) 标头。对于每个分段上传,您必须记录分段编号和 ETag 值。所有对象分段上传的 ETag 值将保持不变,但将为每个分段分配不同的分段号。您必须在随后的请求中包括这些值以完成分段上传。

分段上传完成

完成分段上传时,Amazon S3 通过按升序的分段编号规范化分段来创建对象。如果在开始分段上传请求中提供了任何对象元数据,则 Amazon S3 会将该元数据与对象相关联。成功完成请求后,分段将不再存在。

完成分段上传请求必须包括上传 ID 以及分段编号和相应的 ETag 值的列表。Amazon S3 响应包括可唯一地识别组合对象数据的 ETag。此 ETag 无需成为对象数据的 MD5 哈希。

文件分片基本原理

前端使用插件获取到本地选择的文件,判断文件的大小,超过设置的限制数,就进行大文件分片上传逻辑

第一步

进行文件分片 下面这个方法 返回的是大文件分片后的开始索引和结束索引

 List<List<int>> sliceFileIntoChunks(int fileSize, int sliceMinSize, int sliceMaxCount) {List<List<int>> slices = [];int start = 0;while (start < fileSize) {int end = start + sliceMinSize;if (end > fileSize || slices.length + 1 >= sliceMaxCount) {end = fileSize;}slices.add([start, end]);start = end;}return slices;}

第二步

将分片信息 生成新的配置对象 配置对象会导出 分片的json

List<SliceChunkItem> config = await utils.getJsonFromSplitFileIntoChunks();
// 切片项
class SliceChunkItem {// 切片所在文件的位置final int start;final int end;final int partNumber;String uploadId = "";String tag = "";String checksum = "";List<int> fileBytes = [];MultipartFile? multipartFile;SliceChunkItem({required this.start,required this.end,required this.partNumber,});setUploadId(id) {uploadId = id;}setTag(id) {tag = id;}Map<String, dynamic> toJson() {return {"partNumber": partNumber,"tag": tag,"checksum": checksum,};}List<int> toClunk() {return fileBytes;}
}

第三步

接口发送,分为三个接口,第一个为初始化接口、第二个为分片上传接口、第三个为文件合成接口(因为需求原因 希望存的是一个完整的文件,且不做分片下载功能)

第一个接口 传递了文件名 和加密的类型 因为是亚马逊 hashMethod= SHA1

String fileName = file.path.split('/').last;
  final aa = await multipartUploadInit(fileName: fileName, checksumType: FileUtils.hashMethod);

第二个接口 因为需要并发去发分片 

分片使用FormData进行存 其他信息也加在FormData中

static multipartUpload({required FormData formData,}) async {final res = await dio.post(Url.multipartUpload,data: formData,options: Options(method: "post",contentType: "multipart/form-data",sendTimeout: const Duration(days: 5),receiveTimeout: const Duration(days: 5),),);return res.data;}
  // 同时对分片进行并发Future sendItems({required List<SliceChunkItem> config,required int concurrentLimit,required Function(SliceChunkItem) callback,}) async {return await Future.wait(config.map((item) async {// 判断是否需要取消请求if (cancelCompleter.isCompleted) {throw 'Requests are cancelled';}// 控制并发数量while (count >= concurrentLimit) {await Future.delayed(const Duration(milliseconds: 100));}count++;try {await callback(item);} finally {count--;}}));}

使用

    await utils.sendItems(config: config,concurrentLimit: 5,callback: (item) async {item.setUploadId(uploadId);"[分片上传] bb 开始上传 partNumber ${item.partNumber} ".w();var fileBytes = await utils.getRange(item.start, item.end);item.checksum = utils.calculateSHA1FormList(fileBytes);// 直接传递数组fileBytes 给dio 会导致内存崩溃formData = FormData.fromMap({'file': MultipartFile.fromBytes(fileBytes, filename: "11"),'partNumber': item.partNumber,'checksum': item.checksum,'uploadId': item.uploadId,});final b = await multipartUpload(formData: formData);finalUploadSliceCount++;onSendProgress?.call(finalUploadSliceCount, config.length);"[分片上传] bb 结束上传 partNumber ${item.partNumber} $b".w();String tag = b["data"]["tag"];item.setTag(tag);});

第三个接口

  final cc = await multipartUploadComplete(// checksum: checksum,uploadId: uploadId,partList: config.map((e) => e.toJson()).toList(),);

大文件加载到内存问题

1、读大文件的时候

去拿文件的句柄 然后通过移动获取不同的文件段

file.openRead();

2、读取后存储问题

如果将获取的分片数据直接存到一个类里面,这样的操作会导致内存被撑爆,

必须发送接口的时候再进行 文件指针方式进行文件数据读取 然后发送接口后直接释放

所有分片上传的接口 做了 读取大文件分片数据的逻辑操作

3、对大文件分片 进行哈希计算

错误代码示范

 // ShA 1 进行文件哈希Future<String> calculateSHA1() async {if (await file.exists()) {List<int> contents = await file.readAsBytes();Digest sha1Result = sha1.convert(contents);return sha1Result.toString();} else {throw const FileSystemException('File not found');}}

修改后的代码 (对内存基本没影响)

 Future<String> calculateSHA1() async {if (await file.exists()) {Digest value = await sha1.bind(file.openRead()).first;return value.toString();} else {throw const FileSystemException('File not found');}}

出现的问题 (就是内存被撑爆的原因)

E/DartVM (24105): Exhausted heap space, trying to allocate 67108872 bytes. E/flutter (24105): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Out of Memory

出现问题的地方 (分片完成后 直接加载每个分片到内存中了 所有导致内容崩溃)

如下面代码 将获得的分片数据 存在了内存中 ,如果文件过大 就会被撑爆

Future<List<SliceChunkItem>> getJsonFromSplitFileIntoChunks() async {List<SliceChunkItem> sliceChunkList = [];// int i = 0;int partNumber = 1;// List<int> chunks = splitFileIntoChunks();// for (var v in chunks) {//   sliceChunkList.add(//     SliceChunkItem(//       start: i,//       end: v + i,//       fileBytes: await getRange(i, v + i),//       partNumber: partNumber,//     ),//   );//   i = v;//   partNumber++;// }List<List<int>> chunks =sliceFileIntoChunks(file.lengthSync(), sliceMinSize, sliceMaxCount);"[分片上传] ${chunks.length}".w();for (List<int> v in chunks) {sliceChunkList.add(SliceChunkItem(start: v[0],end: v[1],fileBytes: await getRange(v[0], v[1]),partNumber: partNumber,),);partNumber++;}return sliceChunkList;}

修改如下

发送的时候 再去进行获取 fileBytes 和 checksum;

完整代码如下

大文件上传工具类

import 'dart:async';
import 'dart:convert';
import 'dart:io';import 'package:LS/common/extension/custom_ext.dart';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';class MyCompleter<T> {MyCompleter();Completer<T> completer = Completer();Future<T> get future => completer.future;void reply(T result) {if (!completer.isCompleted) {completer.complete(result);}}
}class FileUtils {File file;// 文件哈希String hash = "";static String hashMethod = "SHA1";// 每个切片最小的大小int sliceMinSize = 1024 * 1024 * 10; // 10MB// 最大的切片数量int sliceMaxCount = 10000;// 限制并发数量的计数器int count = 0;// 用于取消请求的Completerfinal cancelCompleter = Completer<void>();FileUtils(this.file) {// 后端改动不需要l// // 默认后台进行文件求哈希// backstageCalculateSHA1();}// 读取文件的某个范围返回Future<List<int>> getRange(int start, int end) async {if (start < 0) {throw RangeError.range(start, 0, file.lengthSync());}if (end > file.lengthSync()) {throw RangeError.range(end, 0, file.lengthSync());}final c = MyCompleter<List<int>>();List<int> result = [];file.openRead(start, end).listen((data) {result.addAll(data);}).onDone(() {c.reply(result);});return c.future;}Stream<List<int>> getRangeStream(int start, int end) {if (start < 0) {throw RangeError.range(start, 0, file.lengthSync());}if (end > file.lengthSync()) {throw RangeError.range(end, 0, file.lengthSync());}return file.openRead(start, end);}// 读取文件的前n个字节返回List<int> splitFileIntoChunks() {final size = file.lengthSync();int chunkSize = size ~/ sliceMaxCount;chunkSize = chunkSize < sliceMinSize ? sliceMinSize : chunkSize;List<int> chunkSizes = [];int currentPosition = 0;while (currentPosition < size) {int remainingSize = size - currentPosition;int currentChunkSize =remainingSize > chunkSize ? chunkSize : remainingSize;chunkSizes.add(currentChunkSize);currentPosition += currentChunkSize;}return chunkSizes;}List<List<int>> sliceFileIntoChunks(int fileSize, int sliceMinSize, int sliceMaxCount) {List<List<int>> slices = [];int start = 0;while (start < fileSize) {int end = start + sliceMinSize;if (end > fileSize || slices.length + 1 >= sliceMaxCount) {end = fileSize;}slices.add([start, end]);start = end;}return slices;}Future<List<SliceChunkItem>> getJsonFromSplitFileIntoChunks() async {List<SliceChunkItem> sliceChunkList = [];int partNumber = 1;List<List<int>> chunks =sliceFileIntoChunks(file.lengthSync(), sliceMinSize, sliceMaxCount);"[分片上传] 当前分片 ${chunks.length}".w();for (List<int> v in chunks) {sliceChunkList.add(SliceChunkItem(start: v[0],end: v[1],partNumber: partNumber,),);partNumber++;}return sliceChunkList;}// 将M 单位转为基本单位字节static int mToSize(int m) {return 1024 * 1024 * m;}// ShA 1 进行文件哈希Future<String> calculateSHA1(Stream<List<int>> stream) async {Digest digest = await sha1.bind(stream).first;return base64.encode(digest.bytes);}// 将数组数据重新组合成文件////// 测试使用 将分片合成一个文件 写到本地// String appDocDir = (await getDownloadsDirectory())?.path ?? "";// String filePath = '$appDocDir/new.zip';// await FileUtils.mergeChunksIntoFile(//     config.map((e) => e.toClunk()).toList(), filePath);static Future<void> mergeChunksIntoFile(List<List<int>> chunks, String outputPath) async {File outputFile = File(outputPath);outputFile.createSync();IOSink output = outputFile.openWrite(mode: FileMode.writeOnlyAppend);"将数组数据重新组合成文件 a".w();for (List<int> chunk in chunks) {output.add(chunk);"将数组数据重新组合成文件 b".w();}"将数组数据重新组合成文件 c".w();await output.close();}String calculateSHA1FormList(List<int> data) {Digest digest = sha1.convert(data);return base64.encode(digest.bytes);}static String staticCalculateSHA1FormList(List<int> data) {Digest digest = sha1.convert(data);return base64.encode(digest.bytes);}// 后台进行对文件的哈希// backstageCalculateSHA1() async {//   hash = await calculateSHA1();//   return hash;// }// 同时对分片进行并发Future sendItems({required List<SliceChunkItem> config,required int concurrentLimit,required Function(SliceChunkItem) callback,}) async {return await Future.wait(config.map((item) async {// 判断是否需要取消请求if (cancelCompleter.isCompleted) {throw 'Requests are cancelled';}// 控制并发数量while (count >= concurrentLimit) {await Future.delayed(const Duration(milliseconds: 100));}count++;try {await callback(item);} finally {count--;}}));}// 取消所有请求cancelSendItems() {if (!cancelCompleter.isCompleted) {cancelCompleter.complete();}count = 0;"[切片上传] 取消并发成功".w();}
}// 切片项
class SliceChunkItem {// 切片所在文件的位置final int start;final int end;final int partNumber;String uploadId = "";String tag = "";String checksum = "";List<int> fileBytes = [];MultipartFile? multipartFile;SliceChunkItem({required this.start,required this.end,required this.partNumber,});setUploadId(id) {uploadId = id;}setTag(id) {tag = id;}Map<String, dynamic> toJson() {return {"partNumber": partNumber,"tag": tag,"checksum": checksum,};}List<int> toClunk() {return fileBytes;}
}

分片上传总接口

  // 文件分片上传static uploadSliceFile(String path, {ProgressCallback? onSendProgress,required Function(FileUtils) getFileUtils,}) async {File file = File(path);String fileName = file.path.split('/').last;final utils = FileUtils(file);getFileUtils.call(utils);List<SliceChunkItem> config = await utils.getJsonFromSplitFileIntoChunks();final aa = await multipartUploadInit(fileName: fileName, checksumType: FileUtils.hashMethod);String uploadId = aa['data']['uploadId'];"[分片上传] aa $aa".w();int finalUploadSliceCount = 0;FormData formData;// for (SliceChunkItem item in config) {//   item.setUploadId(uploadId);//   "[分片上传] bb 开始上传 partNumber ${item.partNumber} ".w();//   var fileBytes = await utils.getRange(item.start, item.end);//   item.checksum = utils.calculateSHA1FormList(fileBytes);//   // 直接传递数组fileBytes 给dio 会导致内存崩溃//   formData = FormData.fromMap({//     'file': MultipartFile.fromBytes(fileBytes, filename: "11"),//     'partNumber': item.partNumber,//     'checksum': item.checksum,//     'uploadId': item.uploadId,//   });//   final b = await multipartUpload(formData: formData);//   finalUploadSliceCount++;//   onSendProgress?.call(finalUploadSliceCount, config.length);//   "[分片上传] bb 结束上传 partNumber ${item.partNumber} $b".w();//   String tag = b["data"]["tag"];//   item.setTag(tag);// }await utils.sendItems(config: config,concurrentLimit: 5,callback: (item) async {item.setUploadId(uploadId);"[分片上传] bb 开始上传 partNumber ${item.partNumber} ".w();var fileBytes = await utils.getRange(item.start, item.end);item.checksum = utils.calculateSHA1FormList(fileBytes);// 直接传递数组fileBytes 给dio 会导致内存崩溃formData = FormData.fromMap({'file': MultipartFile.fromBytes(fileBytes, filename: "11"),'partNumber': item.partNumber,'checksum': item.checksum,'uploadId': item.uploadId,});final b = await multipartUpload(formData: formData);finalUploadSliceCount++;onSendProgress?.call(finalUploadSliceCount, config.length);"[分片上传] bb 结束上传 partNumber ${item.partNumber} $b".w();String tag = b["data"]["tag"];item.setTag(tag);});// 废弃 后端不需要整体文件的hash// String checksum = utils.hash;// if (checksum.isEmpty) {//   checksum = await utils.backstageCalculateSHA1();// }final cc = await multipartUploadComplete(// checksum: checksum,uploadId: uploadId,partList: config.map((e) => e.toJson()).toList(),);// String filePath = cc["data"]["file_path"];"[分片上传] cc $cc".w();return cc;}

文件上传组件代码

import 'dart:io';import 'package:LS/common/index.dart';
import 'package:LS/gen/assets.gen.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';enum UploadType {image,file,
}class UploadWidget extends StatefulWidget {final UploadType type;final Function(String, int)? onSuccess;final Function(int, List, UploadType)? onDelete;final Function()? onPickAssets;final List<String>? allowedExtensions;final int? limit;const UploadWidget({super.key,this.type = UploadType.image,this.onSuccess,this.onDelete,this.limit,this.onPickAssets,this.allowedExtensions,});@overrideState<UploadWidget> createState() => _UploadWidgetState();
}class _UploadWidgetState extends State<UploadWidget> {// 这两个是上传图片的时候存的List<Uint8List> webImageList = [];List<File> appImageList = [];// 这两个是上传文件的时候存的 文件上传只有 接口上传的时候 有不同所有只要一个List<FilePickerResult> filesList = [];FilePickerResult? files;Widget get curContain {switch (widget.type) {case UploadType.image:return uploadImage();case UploadType.file:return uploadFile();}}onPreview(String url) {if (url.isNotEmpty) {Get.to(() => ImagePreviewPage(imageUrl: url,),);}}//   选择图片pickImages() async {Uint8List? webImage;File? appImage;XFile? image = await AppToast.getLostData();if (image != null) {if (kIsWeb) {webImage = await image.readAsBytes();} else {appImage = File(image.path);}}if (webImage != null) {webImageList.add(webImage);}if (appImage != null) {appImageList.add(appImage);}widget.onPickAssets?.call();setState(() {});}bool isValidExtension(FilePickerResult files) {return files.files.every((file) {String extension = (file.extension ?? "").toLowerCase();return (widget.allowedExtensions ??['jpg','png','doc','xls','pdf','ppt','docx','xlsx','pptx']).contains(extension);});}// 选择文件pickFiles() async {files = await AppToast.getLostFileData(allowMultiple: false,allowedExtensions: (widget.allowedExtensions ??['jpg', 'png', 'doc', 'xls', 'pdf', 'ppt', 'docx', 'xlsx', 'pptx']),);if (files != null) {if (!isValidExtension(files as FilePickerResult)) {AppToast.show("请选择正确的文件格式");return;}filesList.add(files!);widget.onPickAssets?.call();setState(() {});}}// 上传图片Widget uploadImage() {return SizedBox(width: double.infinity,child: GridView.builder(physics: const NeverScrollableScrollPhysics(),shrinkWrap: true,gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),// +1 是为了添加图片按钮itemCount: widget.limit != null &&widget.limit! <=(kIsWeb ? webImageList.length : appImageList.length)? (kIsWeb ? webImageList.length : appImageList.length): (kIsWeb ? webImageList.length : appImageList.length) + 1,itemBuilder: (c, i) {if (kIsWeb) {if (i >= webImageList.length) {return addContainer(onTap: pickImages);}return UploadingImageWidget(webImage: webImageList[i],onDelete: () => onImageDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);} else {if (i >= appImageList.length) {return addContainer(onTap: pickImages);}return UploadingImageWidget(image: appImageList[i],onDelete: () => onImageDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);}},),);}// 上传文件Widget uploadFile() {return SizedBox(width: double.infinity,child: GridView.builder(physics: const NeverScrollableScrollPhysics(),shrinkWrap: true,gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),// +1 是为了添加图片按钮itemCount: widget.limit != null && widget.limit! <= filesList.length? filesList.length: (filesList.length + 1),itemBuilder: (c, i) {if (i >= filesList.length) {return addContainer(onTap: pickFiles);}return UploadingFileWidget(files: filesList[i],index: i,onDelete: () => onFileDelete(i),onSuccess: (url) => onSuccess(i, url),onPreview: onPreview,);},),);}onImageDelete(int i) {if (kIsWeb) {webImageList.removeAt(i);widget.onDelete?.call(i, webImageList, UploadType.image);} else {appImageList.removeAt(i);widget.onDelete?.call(i, appImageList, UploadType.image);}setState(() {});}onFileDelete(int i) {filesList.removeAt(i);widget.onDelete?.call(i, filesList, UploadType.file);setState(() {});}onSuccess(int i, String url) {widget.onSuccess?.call(url, i);}// 已上传完成的容器Widget hasUploadContainer() {return Container(width: 75.w,height: 75.w,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),);}// 点击添加Widget addContainer({Function? onTap,}) {return InkWell(child: Container(width: 75.w,height: 75.w,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),image: DecorationImage(image: Assets.images.uploadAdd.provider(),),),alignment: Alignment.center,),onTap: () {onTap?.call();},);}@overrideWidget build(BuildContext context) {return curContain;}
}// 上传图片 上传中的组件
class UploadingImageWidget extends StatefulWidget {final File? image;final Uint8List? webImage;final Function(String)? onSuccess;final Function()? onFail;final Function()? onDelete;final Function(String)? onPreview;const UploadingImageWidget({super.key,this.image,this.webImage,this.onSuccess,this.onFail,this.onDelete,this.onPreview,});@overrideState<UploadingImageWidget> createState() => _UploadingImageWidgetState();
}class _UploadingImageWidgetState extends State<UploadingImageWidget> {// 是否上传失败bool isUploadFail = false;double cruProgress = 0.0;String httpPath = "";// 正在上传中bool isUploading = false;@overridevoid initState() {super.initState();initUpload();}// 立即进行上传initUpload() {try {kIsWeb ? webUpload() : appUpload();} catch (e) {widget.onFail?.call();setState(() {isUploadFail = true;});}}onTapDelete() {widget.onDelete?.call();}webUpload() async {setState(() {isUploading = true;});final res = await Api.uploadFileListInt(widget.webImage as Uint8List,name: "img",onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}appUpload() async {setState(() {isUploading = true;});final res = await Api.uploadFile(widget.image!.path,onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}@overrideWidget build(BuildContext context) {return AnimatedContainer(duration: const Duration(seconds: 1),width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),child: isUploadFail? failUploadContainer(): (cruProgress == 1.0 && !isUploading? finallyUploadContainer(): beforeUploadContainer()),);}// 上传之前的容器Stack beforeUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),if (widget.image != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),Positioned.fill(child: Opacity(opacity: 0.6,child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#000000"),borderRadius: BorderRadius.circular(5.r),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,alignment: Alignment.center,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: SizedBox(width: 40.w,child: LineProgressWidget(cruProgress: cruProgress,minHeight: 5.h,color: HexColor("#CCCCCC"),showText: false,valueColor: AlwaysStoppedAnimation<Color>(HexColor("#F9DE4A")),),),),),],);}// 完成上传的容器Stack finallyUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),),if (widget.image != null)InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 上传失败重新上传failReUpload() async {cruProgress = 0.0;isUploadFail = false;setState(() {});initUpload();}// 失败上传的容器Stack failUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.webImage != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(widget.webImage as Uint8List,width: double.infinity,height: double.infinity,),),if (widget.image != null)Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(widget.image as File,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#000000").withOpacity(0.6),),alignment: Alignment.center,child: InkWell(onTap: failReUpload,child: SizedBox(width: 20.w,height: 20.w,child: Assets.images.uploadReload.image(width: 20.w),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}
}// 上传文件 上传中的组件
class UploadingFileWidget extends StatefulWidget {final int index;final FilePickerResult? files;final Function(String)? onSuccess;final Function()? onFail;final Function()? onDelete;final Function(String)? onPreview;const UploadingFileWidget({super.key,this.files,required this.index,this.onSuccess,this.onFail,this.onDelete,this.onPreview,});@overrideState<UploadingFileWidget> createState() => _UploadingFileWidgetState();
}class _UploadingFileWidgetState extends State<UploadingFileWidget> {// 是否上传失败bool isUploadFail = false;// 正在上传中bool isUploading = false;double cruProgress = 0.0;String httpPath = "";Function? cancelSendItems;@overridevoid initState() {super.initState();initUpload();}// 立即进行上传initUpload() {try {kIsWeb ? webUpload() : appUpload();} catch (e) {widget.onFail?.call();setState(() {isUploadFail = true;});}}onTapDelete() {widget.onDelete?.call();if (cancelSendItems != null) {cancelSendItems!.call();}}webUpload() async {PlatformFile curFile = widget.files!.files.first;setState(() {isUploading = true;});final res = await Api.uploadFilePlatformFile(curFile,onSendProgress: (count, total) {setState(() {cruProgress = count / total;});},);httpPath = res["data"] ?? "";setState(() {isUploading = false;});widget.onSuccess?.call(httpPath);}appUpload() async {var path = widget.files!.paths.first;int size = widget.files?.files.first.size ?? 0;if (size > FileUtils.mToSize(20)) {appSliceUpload();return;}setState(() {isUploading = true;});final res = await Api.uploadFile(path as String,onSendProgress: (count, total) async {// await Future.delayed(const Duration(seconds: 1));setState(() {cruProgress = count / total;});},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}// 大文件切片上传appSliceUpload() async {var path = widget.files!.paths.first;setState(() {isUploading = true;});final res = await Api.uploadSliceFile(path as String,onSendProgress: (count, total) async {// await Future.delayed(const Duration(seconds: 1));setState(() {cruProgress = count / total;});},getFileUtils: (utils) {"[分片上传] 获取 utils $utils".w();cancelSendItems = () => utils.cancelSendItems();},);setState(() {isUploading = false;});httpPath = res["data"] ?? "";widget.onSuccess?.call(httpPath);}@overrideWidget build(BuildContext context) {return AnimatedContainer(duration: const Duration(seconds: 1),width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#F2F4F7"),borderRadius: BorderRadius.circular(5.r),),child: isUploadFail? failUploadContainer(): (cruProgress == 1.0 && !isUploading? finallyUploadContainer(): beforeUploadContainer()),);}// 上传之前的容器Stack beforeUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned.fill(child: Opacity(opacity: 0.6,child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(color: HexColor("#000000"),borderRadius: BorderRadius.circular(5.r),),),),),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,alignment: Alignment.center,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: SizedBox(width: 40.w,child: LineProgressWidget(cruProgress: cruProgress,minHeight: 5.h,color: HexColor("#CCCCCC"),showText: false,valueColor: AlwaysStoppedAnimation<Color>(HexColor("#F9DE4A")),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 文件信息展示容器Widget fileInfoContainer(PlatformFile? file) {String fileName = file?.name ?? "";if (Utils.isImageFile(fileName)) {if (kIsWeb) {Uint8List webImageFile = file?.bytes as Uint8List;return InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.memory(webImageFile,width: double.infinity,height: double.infinity,),),);} else {File imageFile = File(file?.path ?? "");return InkWell(onTap: () => widget.onPreview?.call(httpPath),child: Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: Image.file(imageFile,width: double.infinity,height: double.infinity,fit: BoxFit.cover,),),);}}return Container(width: double.infinity,height: double.infinity,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),alignment: Alignment.center,child: Column(mainAxisSize: MainAxisSize.min,crossAxisAlignment: CrossAxisAlignment.center,mainAxisAlignment: MainAxisAlignment.center,children: [Assets.images.uploadFileIcon.image(width: 20.w),SizedBox(height: 5.h),Text(fileName,style: TextStyle(fontFamily: Font.pingFang,fontWeight: FontWeight.w500,fontSize: 12.sp,color: HexColor("#1A1A1A"),height: 1.1,),textAlign: TextAlign.center,overflow: TextOverflow.ellipsis,maxLines: 3,),],),);}// 完成上传的容器Stack finallyUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}// 上传失败重新上传failReUpload() async {cruProgress = 0.0;isUploadFail = false;setState(() {});initUpload();}// 失败上传的容器Stack failUploadContainer() {return Stack(clipBehavior: Clip.none,children: [if (widget.files?.files.first != null)fileInfoContainer(widget.files?.files.first),Positioned.fill(child: Container(width: double.infinity,height: double.infinity,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#000000").withOpacity(0.6),),alignment: Alignment.center,child: InkWell(onTap: failReUpload,child: SizedBox(width: 20.w,height: 20.w,child: Assets.images.uploadReload.image(width: 20.w),),),),),Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: onTapDelete,child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}
}// 已上传图片或文件展示
class HasUploadShowWidget extends StatelessWidget {final List<String?> urls;final Function(int)? onDelete;final bool showDelete;final Function(String)? onFileTap;final TextDirection textDirection;const HasUploadShowWidget({super.key,required this.urls,this.onDelete,this.showDelete = true,this.onFileTap,this.textDirection = TextDirection.ltr,});onTapDelete(int idx) {onDelete?.call(idx);}onPreview(String url) {if (url.isNotEmpty) {Get.to(() => ImagePreviewPage(imageUrl: url,),);}}@overrideWidget build(BuildContext context) {return SizedBox(width: double.infinity,child: Directionality(textDirection: textDirection,child: GridView.builder(shrinkWrap: true,itemCount: urls.length,physics: const NeverScrollableScrollPhysics(),gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4,crossAxisSpacing: 10,mainAxisSpacing: 10,childAspectRatio: 1,),itemBuilder: (c, i) {if (Utils.isImageFile(urls[i] ?? "")) {return Stack(clipBehavior: Clip.none,children: [InkWell(onTap: () => onPreview(urls[i] ?? ""),child: Container(width: 75.w,height: 75.w,clipBehavior: Clip.hardEdge,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),),child: urls[i] != null? CachedNetworkImage(width: 75.w,height: 75.w,imageUrl: urls[i] ?? "",fit: BoxFit.cover,): null,),),if (showDelete)Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: () => onTapDelete(i),child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}return Stack(clipBehavior: Clip.none,children: [InkWell(onTap: () => onFileTap?.call(urls[i] ?? ""),child: Container(width: 75.w,height: 75.w,clipBehavior: Clip.none,decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.r),color: HexColor("#F2F4F7"),),alignment: Alignment.center,child: Column(crossAxisAlignment: CrossAxisAlignment.center,mainAxisAlignment: MainAxisAlignment.center,children: [Assets.images.uploadFileIcon.image(width: 20.w),SizedBox(height: 5.h),Container(constraints: BoxConstraints(maxWidth: 75.w),child: Text(Utils.getFileNameFromUrl((urls[i] ?? "")),style: TextStyle(fontFamily: Font.pingFang,fontWeight: FontWeight.w500,fontSize: 12.sp,color: HexColor("#1A1A1A"),height: 1.1,),textAlign: TextAlign.center,overflow: TextOverflow.ellipsis,maxLines: 2,),),],),),),if (showDelete)Positioned(top: -4.w,right: -4.w,child: InkWell(onTap: () => onTapDelete(i),child: Container(width: 18.w,height: 18.w,decoration: const BoxDecoration(shape: BoxShape.circle,color: Colors.white,),child: Assets.images.uploadClose.image(width: 18.w),),),),],);}),),);}
}

使用 文件上传 (如果上传的是图片 显示的也是图片样式)

UploadWidget(type: UploadType.file,limit: 5 - controller.lastFiles.length,onPickAssets: () {controller.curUploadCount++;},onDelete: (i, list, t) {controller.curUploadCount--;controller.state.files.removeAt(i);controller.update();},onSuccess: (url, i) {controller.curUploadCount--;controller.state.files.add(url);controller.update();},)

图片上传

UploadWidget(type: UploadType.image,limit: 9,onPickAssets: () {curUploadCount++;setState(() {});},onDelete: (i, list, t) {curUploadCount--;print("文件 -- $curUploadCount");imagesFiles.removeAt(i);setState(() {});},onSuccess: (url, i) {curUploadCount--;// controller.state.files.add(url);imagesFiles.add(url);setState(() {});},),

支持撤销文件上传

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/474875.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从概念到现实:数字孪生在智慧园区建设中的实现路径

目录 一、引言 二、数字孪生与智慧园区的关联 三、数字孪生技术的核心要素及实现路径 1、数据采集与处理 2、建立数字孪生模型 3、实时更新与优化 4、实现与智慧园区的集成 四、数字孪生技术在智慧园区中的应用场景 1、能源管理 2、安全监控 3、设施管理 五、面临的…

Spring事务实现原理

1、引言 spring的spring-tx模块提供了对事务管理支持&#xff0c;使用spring事务可以让我们从复杂的事务处理中得到解脱&#xff0c;无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。 spring事务有编程式事务和声明式事务两种实现方式。编程式事务是通过编写代码…

【LLM新概念】什么是深湖(DeepLake)

目录 一、说明二、深湖的意义三、深湖特色四、深湖与数据湖&#xff08;Deep Lake vs Data Lake&#xff09;五、深湖 vs 抱脸&#xff08;Deep Lake vs Huggingface&#xff09; 一、说明 Deep Lake 是专门用于深度学习用例的数据湖&#xff08;数据库&#xff09;&#xff0…

LeetCode 6. Z 字形变换

题目链接https://leetcode.cn/problems/zigzag-conversion/description/ 将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 "PAYPALISHIRING" 行数为 3 时&#xff0c;排列如下&#xff1a; P A …

JUC并发编程

JUC并发编程 JUC概述 JUC是java.util.concurrent包的简称&#xff0c;即Java并发编程工具包&#xff0c;目的是为了更好地支持高并发任务&#xff0c;让开发者进行多线程编程时有效减少竞争条件和死锁线程。 并发编程 一些基本概念&#xff1a; 进程与线程&#xff1a; 进…

Linux常见指令(一)

目录 一、基本指令 1.1ls指令 1.2pwd指令 1.3cd指令 1.4touch指令 1.5mkdir指令 1.6rmdir指令、rm指令 1.7man指令 1.8cp指令 1.9mv指令 1.10cat 一、基本指令 1.1ls指令 语法 &#xff1a; ls [ 选项 ][ 目录或文件 ] 功能&#xff1a;对于目录&#xff0c;该命令…

公众号主体申请上限怎么提高

一般可以申请多少个公众号&#xff1f;自2018年11月16日起&#xff0c;进行申请上限调整&#xff1a;1、同一个邮箱只能申请1个公众号&#xff1b;2、同一个手机号码可绑定5个公众号&#xff1b;3、同一身份证申请个人类型公众号数量上限为1个&#xff1b;4、同一企业、个体工商…

论文精读--MAE

BERT在Transformer的架构上进行了掩码操作&#xff0c;取得了很好的效果。如果对ViT进行掩码操作呢&#xff1f; 分成patch后灰色表示遮盖住&#xff0c;再将可见的patch输入encoder&#xff0c;把encoder得到的特征拉长放回原本在图片中的位置&#xff0c;最后由decoder去重构…

【大厂AI课学习笔记】【2.1 人工智能项目开发规划与目标】(3)数据准备初步

今天来学习数据准备。 一个AI项目要包括构建数据集、数据清理和数据融合、数据采集、特征工程、算法改进和其他步骤。 数据采集和数据清洗&#xff0c;也就是数据准备&#xff0c;要占到人工智能项目一半以上的工作量。 训练的数据量越大&#xff0c;模型越准确。 建立数据标…

vue自定义指令(图文示例)

第085个 查看专栏目录: VUE 本文章目录 示例效果图示例源代码API 参考网址 Vue 自定义指令是一种用于扩展 Vue 模板功能的强大工具。通过自定义指令&#xff0c;你可以在 Vue 模板中添加自定义的行为和逻辑&#xff0c;使模板更加灵活和可定制。 以下是对 Vue 自定义指令的详细…

【leetcode】572. 另一棵树的子树

题目链接 572. 另一棵树的子树 bool isSameTree(struct TreeNode* p, struct TreeNode* q) {if (p ! NULL && q ! NULL) {return p->val q->val // 分解比较根左右&& isSameTree(p->left, q->left)&& isSameTree(p->right, q->ri…

开发知识点-JAVA-springboot+Spring Security/Shiro

Spring Security/Shiro shiroShiro反序列化相关URLDNS链Shiro CC链Shiro CB链Shiro反序列化WAF绕过Java快速开发框架_若依——前后端分离版- 3. 登陆 springsecurity认证 Debug - postman模拟SpringBoot+SpringSecurity+dubbo图书电商后台实战-对象映射-基本属性映射SpringBoot…