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

支持撤销文件上传

断点续传功能和秒传

在原有的基础上改动如下

所有上传接口都加上文件的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都有唯一的标识,这样就可以避免状态混淆的问题。

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

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

相关文章

CSP-J 2023 T3 一元二次方程

文章目录 题目题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 题目传送门题解思路总代码 提交结果尾声 题目 题目背景 众所周知&#xff0c;对一元二次方程 a x 2 b x c 0 , ( a ≠ 0 ) ax ^ 2 bx c 0, (a \neq 0) ax2bxc0,(a0)&#xff0c;可…

STM32G030C8T6:定时器1ms中断(以64MHz外部晶振为例)

本专栏记录STM32开发各个功能的详细过程&#xff0c;方便自己后续查看&#xff0c;当然也供正在入门STM32单片机的兄弟们参考&#xff1b; 本小节的目标是&#xff0c;系统主频64 MHZ,采用高速外部晶振&#xff0c;通过定时器3 每秒中断控制 PB9 引脚输出高低电平&#xff0c;从…

低代码开发:推动互联网企业数字化转型的关键因素

联网行业作为我国数字经济发展的核心驱动力&#xff0c;在推动国家数字化转型中扮演着至关重要的角色。与其他传统行业相比&#xff0c;互联网企业面临更加紧迫的数字化转型需求&#xff0c;因为它们需要不断适应快速变化的市场环境和技术趋势。 然而&#xff0c;由于互联网企业…

MFC 皮肤库配置

1.创建MFC 对话框 2.添加皮肤资源 添加资源 添加头文件 关闭SDL检测 添加静态库文件 修改字符集 添加头文件 将皮肤中的ssk文件加载到初始化实例中 > 运行即可

【寸铁的刷题笔记】树、dfs、bfs、回溯、递归(一)

【寸铁的刷题笔记】树、dfs、bfs、回溯、递归(一) 大家好 我是寸铁&#x1f44a; 总结了一篇刷题关于树、dfs、bfs、回溯、递归的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 105. 从前序与中序遍历序列构造二叉树 模拟分析图 代码实现 /*** Definition for a binary tre…

HarmonyOS—添加/删除Module

Module是应用/服务的基本功能单元&#xff0c;包含了源代码、资源文件、第三方库及应用/服务配置文件&#xff0c;每一个Module都可以独立进行编译和运行。一个HarmonyOS应用/服务通常会包含一个或多个Module&#xff0c;因此&#xff0c;可以在工程中创建多个Module&#xff0…

如何利用内网穿透工具在企业微信开发者中心实现本地接口服务回调

文章目录 1. Windows安装Cpolar2. 创建Cpolar域名3. 创建企业微信应用4. 定义回调本地接口5. 回调和可信域名接口校验6. 设置固定Cpolar域名7. 使用固定域名校验 企业微信开发者在应用的开发测试阶段&#xff0c;应用服务通常是部署在开发环境&#xff0c;在有数据回调的开发场…

前端知识复习

1.symbol类型 Symbol 是 ECMAScript 6 中引入的一种新的基本数据类型&#xff0c;它表示独一无二的值。Symbol 值是通过 Symbol() 函数创建的。 Symbol 值具有以下特点&#xff1a; 独一无二性&#xff08;唯一性&#xff09;&#xff1a;每个通过 Symbol() 函数创建的 Symb…

十三:集合

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 01、Java 集合框架概述1.1、集合框架与数组的对比及概述1.2、集合框架涉及到的API 02、Collection接口方法2.1、Collection接口中的常用方法12.2、Collection接口中…

在idea中配置Tomcat

1.在idea中点击右上角 2.点击Edit Configurations,点击加号 3.向下拉找到Tomcat Server下的Local,点一下 点击Configure 找到tomcat文件路径,选择apache-tomcat-8.5.63(8.5.63是我的版本号) 选择好路径后点ok就配置好了 总步骤:

代码随想录算法训练营第一天

● 今日学习的文章链接和视频链接 ● 自己看到题目的第一想法 1. 704二分法&#xff1a; 方法一&#xff1a; 整个数组是 左闭右闭区间 [ ] left指针指向数组开始下标&#xff0c; right 指针指向数组最后下表nums.size()-1, mid为 (leftright) /2循环条件 left<rightnu…

小折叠也能成为主力机,全新小折叠旗舰华为Pocket 2正式发布

2024年2月22日&#xff0c;华为在三亚举办华为Pocket 2时尚盛典&#xff0c;正式发布其全新小折叠旗舰华为Pocket 2。一直以来&#xff0c;华为致力于萃取各界艺术灵感&#xff0c;不断探寻科技美学的可能性&#xff0c;华为Pocket系列更是秉承将奢雅美学与尖端科技融为一体的理…

探索Redis是否为单线程的奥秘(文末送书)

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. Redis中的多线程二. I/O多线程三. Redis中的多进程四. 结论五. 书籍推荐5.1 书…

第一个Qt程序中的秘密

创建第一个程序 首先我们打开Qt Creator 打开文件->New Projects... 菜单&#xff0c;创建我们的第一个Qt项目 选择 Qt Widgets Application&#xff0c;点击选择...按钮 之后&#xff0c;输入项目名称QtLearning&#xff0c;并选择创建路径&#xff0c; 在build system中选…

ConnectWise ScreenConnect 身份验证绕过漏洞复现可RCE(CVE-2024-1709)

0x01 产品简介 ConnectWise ScreenConnect ,是一款自托管的远程桌面软件应用,该款软件允许用户自行托管,可以在自己的服务器、个人电脑、虚拟机或虚拟专用服务器上运行。 0x02 漏洞概述 ConnectWise ScreenConnect低于23.9.8 版本的产品中,SetupWizard.aspx接口处存在身…

[newstarctf2023] --RE wp

AndroGenshin: rc4加密表&#xff0c;base64换表&#xff1a; 脚本梭就行 python username b"genshinimpact" base64_table [125, 239, 101, 151, 77, 163, 163, 110, 58, 230, 186, 206, 84, 84, 189, 193, 30, 63, 104, 178, 130, 211,164, 94, 75, 16, 32, 33…

发布 rust 源码包 (crates.io)

rust 编程语言的包 (或者 库, library) 叫做 crate, 也就是软件中的一个组件. 一个完整的软件通常由多个 crate 组成, rust 编译器 (rustc) 一次编译一整个 crate, 不同的 crate 可以同时并行编译. rust 官方有一个集中发布开源包的网站 crates.io. 发布在这上面的 crate 可以…

vue3框架组件自动导入unplugin-vue-components

1.安装 npm i unplugin-vue-components -save-dev 2.配置 我这里用的是Vue CLI&#xff0c;所以要在vue.config.js文件中添加配置&#xff0c;官网中有写不同打包工具的配置写法 框架我使用的是Element Plus&#xff0c;使用前去官网查看自己的框架是否支持&#xff0c;主流…

LLM之RAG实战(二十七)| 如何评估RAG系统

有没有想过今天的一些应用程序是如何看起来几乎神奇地智能的&#xff1f;这种魔力很大一部分来自于一种叫做RAG和LLM的东西。把RAG&#xff08;Retrieval Augmented Generation&#xff09;想象成人工智能世界里聪明的书呆子&#xff0c;它会挖掘大量信息&#xff0c;准确地找到…

电脑黑屏什么都不显示怎么办 电脑开机黑屏不显示任何东西的4种解决办法

相信有很多网友都有经历电脑开机黑屏不显示任何东西&#xff0c;找了很多方法都没处理好&#xff0c;其实关于这个的问题&#xff0c;首先还是要了解清楚开机黑屏的原因&#xff0c;才能够对症下药&#xff0c;下面大家可以跟小编一起来看看怎么解决吧 电脑开机黑屏不显示任何…