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;

  // 用于取消请求的Completer
  final 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,
  });

  @override
  State<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();
      },
    );
  }

  @override
  Widget 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,
  });

  @override
  State<UploadingImageWidget> createState() => _UploadingImageWidgetState();
}

class _UploadingImageWidgetState extends State<UploadingImageWidget> {
  // 是否上传失败
  bool isUploadFail = false;
  double cruProgress = 0.0;
  String httpPath = "";

  // 正在上传中
  bool isUploading = false;

  @override
  void 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);
  }

  @override
  Widget 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,
  });

  @override
  State<UploadingFileWidget> createState() => _UploadingFileWidgetState();
}

class _UploadingFileWidgetState extends State<UploadingFileWidget> {
  // 是否上传失败
  bool isUploadFail = false;
  // 正在上传中
  bool isUploading = false;
  double cruProgress = 0.0;
  String httpPath = "";

  Function? cancelSendItems;

  @override
  void 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);
  }

  @override
  Widget 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,
        ),
      );
    }
  }

  @override
  Widget 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(() {});
                                },
                              ),

支持撤销文件上传

断点续传功能和秒传

在原有的基础上改动如下

所有上传接口都加上文件的MD5 值,分片的几个接口将uploadId改为整个文件的MD5

新增MD5的方法 依旧使用crypto 库

crypto | Dart PackageImplementations of SHA, MD5, and HMAC cryptographic functions.icon-default.png?t=N7T8https://pub-web.flutter-io.cn/packages/crypto

 static staticCalculateMD5(File file) async {
    if (!file.existsSync()) {
      print('File "$file" does not exist.');
      return;
    }
    Digest digest = await md5.bind(file.openRead()).first;
    return digest.toString();
  }

 app 使用

  FormData formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(path),
      'md5': await FileUtils.staticCalculateMD5(File(path)),
    });

web端使用

static staticCalculateMD5Stream(List<int> input) async {
    Digest digest = md5.convert(input);
    return digest.toString();
  }
FormData formData = FormData.fromMap({
      'file': multipartFile,
      'md5': await FileUtils.staticCalculateMD5Stream(fileBytes),
    });

分片接口改动如下

1、初始化分片接口

新增参数 md5 文件的MD5  这样就可以验证文件是否上传过,如果上传直接返回文件地址

未上传完 会返回已上传的分片 然后对求出未上传的分片进行继续上传 

逻辑修改如下

    String filePath = aa['data']['filePath'] ?? "";
    List<SliceChunkItem> sendSliceConfig = config;

    int finalUploadSliceCount = 0;
    // 说明是上传过的文件 直接秒传
    if (filePath.isNotEmpty) {
      onSendProgress?.call(1, 1);
      return aa;
    } else {
      List partList = (aa['data']['partList'] ?? []);
      List<SliceChunkItem> filterPartList = partList
          .map<SliceChunkItem>((e) => SliceChunkItem.fromJson(e))
          .toList();

      for (var it in filterPartList) {
        int idx = config.indexWhere((e) => e.partNumber == it.partNumber);
        if (idx != -1) {
          config[idx] = it;
        }
      }

      sendSliceConfig =
          config.where((e) => !(e.end == 0 && e.start == 0)).toList();

      if (sendSliceConfig.isNotEmpty) {
        finalUploadSliceCount = filterPartList.length;
        onSendProgress?.call(finalUploadSliceCount, config.length);
      }
    }

2、分片的接口 新增了文件的MD5

 完成接口一样的新增md5

最后最新的分片上传接口逻辑如下

 

// 文件分片上传
  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();

    String curMd5Code = utils.hash;
    if (curMd5Code.isEmpty) {
      curMd5Code = await FileUtils.staticCalculateMD5(file);
    }

    final aa = await multipartUploadInit(
      fileName: fileName,
      checksumType: FileUtils.hashMethod,
      md5: curMd5Code,
    );
    String filePath = aa['data']['filePath'] ?? "";
    List<SliceChunkItem> sendSliceConfig = config;

    int finalUploadSliceCount = 0;
    // 说明是上传过的文件 直接秒传
    if (filePath.isNotEmpty) {
      onSendProgress?.call(1, 1);
      return aa;
    } else {
      List partList = (aa['data']['partList'] ?? []);
      List<SliceChunkItem> filterPartList = partList
          .map<SliceChunkItem>((e) => SliceChunkItem.fromJson(e))
          .toList();

      for (var it in filterPartList) {
        int idx = config.indexWhere((e) => e.partNumber == it.partNumber);
        if (idx != -1) {
          config[idx] = it;
        }
      }

      sendSliceConfig =
          config.where((e) => !(e.end == 0 && e.start == 0)).toList();

      if (sendSliceConfig.isNotEmpty) {
        finalUploadSliceCount = filterPartList.length;
        onSendProgress?.call(finalUploadSliceCount, config.length);
      }
    }

    // String uploadId = aa['data']['uploadId'];
    "[分片上传] aa $aa".w();

    FormData formData;
    await utils.sendItems(
        config: sendSliceConfig,
        concurrentLimit: 4,
        callback: (item) async {
          // item.setUploadId(uploadId);
          "[分片上传] bb 开始上传 partNumber ${item.partNumber} ".w();
          var fileBytes = await utils.getRange(item.start ?? 0, item.end ?? 0);
          item.checksum = utils.calculateSHA1FormList(fileBytes);
          // 直接传递数组fileBytes 给dio 会导致内存崩溃
          formData = FormData.fromMap({
            'file': MultipartFile.fromBytes(fileBytes, filename: "11"),
            'partNumber': item.partNumber,
            'checksum': item.checksum,
            'md5': curMd5Code,
          });
          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,
      md5: curMd5Code,
      partList: config.map((e) => e.toJson()).toList(),
    );

    // String filePath = cc["data"]["file_path"];

    "[分片上传] cc $cc".w();
    return cc;
  }

上传组件发现删除存在bug 修复如下

UploadingFileWidget 组件使用key: ValueKey('image_$i'),进行标识

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,
            key: ValueKey('image_$i'),
          );
        },
      ),

在Flutter中,Key可以是任何类型的对象,但最常用的是 GlobalKey、ValueKey 和 ObjectKey。

  1. GlobalKey: 用于在整个应用程序中唯一标识一个Widget。当你需要在程序的不同部分引用同一个组件时,可以使用GlobalKey。比如,如果你需要通过GlobalKey在一个页面中的某个组件,可以使用GlobalKey。

  2. ValueKey: 用于基于值的比较,可以根据给定的值来标识Widget,在列表、集合或父子关系中非常有用。比如,如果你有一个具有一组项目的列表,并且需要标识这些项目,可以使用ValueKey,并以项目的唯一标识符作为值。

  3. ObjectKey: 与ValueKey类似,可以根据对象的身份来标识Widget。如果你需要根据对象的身份来标识Widget,而不是基于对象的值,可以使用ObjectKey。

除了这些内置的Key类型,你也可以创建自定义的Key类,只要它们遵循Key的方法和属性即可。

在你的情况下,你可以使用GlobalKey来保证每个UploadingImageWidget都有唯一的标识,这样就可以避免状态混淆的问题。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux常见的指令

目录 01. ls 指令02. pwd命令03. cd 指令04. touch指令05.mkdir指令&#xff08;重要&#xff09;&#xff1a;06.rmdir指令 && rm 指令&#xff08;重要&#xff09;&#xff1a;07.man指令&#xff08;重要&#xff09;&#xff1a;08.cp指令&#xff08;重要&#x…

人工智能讲师AI讲师大模型讲师叶梓介绍及大语言模型技术原理与实践提纲

叶梓&#xff0c;上海交通大学计算机专业博士毕业&#xff0c;高级工程师。主研方向&#xff1a;数据挖掘、机器学习、人工智能。历任国内知名上市IT企业的AI技术总监、资深技术专家&#xff0c;市级行业大数据平台技术负责人。 长期负责城市信息化智能平台的建设工作&#xff…

JUC并发编程学习与实践

文章目录 学习资料创建和运行线程方法一&#xff1a;直接使用Thread方法二&#xff1a;使用Runnable配合Thread方法三&#xff1a;FutureTask配合Thread 线程的常见方法start与runsleep与yield线程的优先级 join方法详解interrupt线程打断interrupt线程打断后&#xff0c;线程不…

4.7 Verilog 循环语句

关键词&#xff1a;while, for, repeat, forever Verilog 循环语句有 4 种类型&#xff0c;分别是 while&#xff0c;for&#xff0c;repeat&#xff0c;和 forever 循环。循环语句只能在 always 或 initial 块中使用&#xff0c;但可以包含延迟表达式。 while 循环 while 循…

科普|什么是数据脱敏

在当今数字化的时代&#xff0c;数据已经成为企业的重要资产和核心竞争力。然而&#xff0c;随着数据量的不断增加&#xff0c;数据安全和隐私保护问题也日益突出。 什么是数据脱敏呢&#xff1f; 数据脱敏&#xff0c;也称为数据去隐私化或数据匿名化&#xff0c;是一种将敏感…

electron学习和新建窗口

首先我们要先下载electron npm install --save-dev electron 建立入口文件main.js 新建一个入口文件 main.js&#xff0c;然后导入eletron新建一个窗口。 const { app, BrowserWindow, ipcMain } require("electron"); const path require("path");func…

JavaWeb——002JS Vue快速入门

目录 一、JS快速入门​编辑 1、什么是JavaScript?​编辑 2、JS引入方式​编辑 2.1、示例代码 3、JS基础语法 3.1、书写语法 3.2、变量​编辑 3.3、数据类型 3.4、运算符​编辑 3.5、流程控制语句​编辑 4、JS函数 4.1、第一种函数定义方式 function funcName(参数…

C#知识点-15(匿名函数、使用委托进行窗体传值、反射)

匿名函数 概念&#xff1a;没有名字的函数&#xff0c;一般情况下只调用一次。它的本质就是一个方法&#xff0c;虽然我们没有定义这个方法&#xff0c;但是编译器会把匿名函数编译成一个方法 public delegate void Del1();//无参数无返回值的委托public delegate void Del2(s…

Linux 安装RocketMQ

官网&#xff1a; https://rocketmq.apache.org/zh/安装RocketMQ 5.2.0 wget https://dist.apache.org/repos/dist/release/rocketmq/5.2.0/rocketmq-all-5.2.0-bin-release.zip unzip rocketmq-all-5.2.0-bin-release.zip#启动之前修改jvm启动内存 cd bin #修改&#xff1a;…

车辆管理系统设计与实践

车辆管理系统是针对车辆信息、行驶记录、维护保养等进行全面管理的系统。本文将介绍车辆管理系统的设计原则、技术架构以及实践经验&#xff0c;帮助读者了解如何构建一个高效、稳定的车辆管理系统。 1. 系统设计原则 在设计车辆管理系统时&#xff0c;需要遵循以下设计原则&…

顺序表经典算法及其相关思考

27. 移除元素 - 力扣&#xff08;LeetCode&#xff09; 思路一 利用顺序表中的SLDestroy函数的思想&#xff0c;遇到等于val值的就挪动 思路二 双指针法&#xff1a;不停的将和val不相等的数字往前放。此时的des更像一个空数组&#xff0c;里面存放的都是和val不相等、能够存…

【Rust敲门砖】 Windows环境下配置及安装环境

一、安装C环境 rust底层是依赖C环境的连接器&#xff0c;所以需要先安装C/C编译环境, 有两种选择:安装微软的msvc或者安装mingw/cygwin。 如果使用msvc的Visual Studio&#xff0c;只需要安装好C/C编译环境,然后一路默认就行了&#xff0c;缺点是体积比较大&#xff0c;下载安…

YOLO v9 思路复现 + 全流程优化

YOLO v9 思路复现 全流程优化 提出背景&#xff1a;深层网络的 信息丢失、梯度流偏差YOLO v9 设计逻辑可编程梯度信息&#xff08;PGI&#xff09;&#xff1a;使用PGI改善训练过程广义高效层聚合网络&#xff08;GELAN&#xff09;&#xff1a;使用GELAN改进架构 对比其他解法…

day16_map课后练习 - 参考答案

文章目录 day16_课后练习第1题第2题第3题第4题第5题第6题 day16_课后练习 第1题 开发提示&#xff1a;可以使用Map&#xff0c;key是字母&#xff0c;value是该字母的次数 效果演示&#xff1a;例如&#xff1a;String str “Your future depends on your dreams, so go to …

KafKa3.x基础

来源&#xff1a;B站 目录 定义消息队列传统消息队列的应用场景消息队列的两种模式 Kafka 基础架构Kafka 命令行操作主题命令行操作生产者命令行操作消费者命令行操作 Kafka 生产者生产者消息发送流程发送原理生产者重要参数列表 异步发送 API普通异步发送带回调函数的异步发送…

【springBoot】springAOP

AOP的概述 AOP是面向切面编程。切面就是指某一类特定的问题&#xff0c;所以AOP也可以理解为面向特定方法编程。AOP是一种思想&#xff0c;拦截器&#xff0c;统一数据返回和统一异常处理是AOP思想的一种实现。简单来说&#xff1a;AOP是一种思想&#xff0c;对某一类事务的集…

(提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战

文章目录 &#xff08;提供数据集下载&#xff09;基于大语言模型LangChain与ChatGLM3-6B本地知识库调优&#xff1a;数据集优化、参数调整、提示词Prompt优化本地知识库目标操作步骤问答测试的预设问题原始数据情况数据集优化&#xff1a;预处理&#xff0c;先后准备了三份数据…

C#使用一个泛型方法操作不同数据类型的数组

目录 一、泛型方法及其存在的意义 二 、实例 1.源码 2.生成效果 再发一个泛型方法的示例。 一、泛型方法及其存在的意义 实际应用中&#xff0c;查找或遍历数组中的值时&#xff0c;有时因为数组类型的不同&#xff0c;需要对不同的数组进行操作&#xff0c;那么,可以使用…

Java学习-21 网络编程

什么是网络编程&#xff1f; 可以让设备中的程序与网络上其他设备中的程序进行数据交互&#xff08;实现网络通信的&#xff09; 基本的通信架构 基本的通信架构有2种形式: CS架构(Client客户端/Server服务端) BS架构(Browser浏览器/Server服务端)。 网络通信三要素 IP …

ATCoder Beginnner Contest 341 A~G

A.Print 341&#xff08;模拟&#xff09; 题意&#xff1a; 给定一个正整数 N N N&#xff0c;输出由 N N N个0和 ( N 1 ) (N1) (N1)个1交替组成的字符串。 分析&#xff1a; 按题意模拟即可 代码&#xff1a; #include<bits/stdc.h>using namespace std;int mai…