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

news2024/12/28 21:03:43

文件分片上传

资料

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

支持撤销文件上传

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

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

相关文章

【漏洞复现-通达OA】通达OA WHERE_STR 存在前台SQL注入漏洞

一、漏洞简介 通达OA(Office Anywhere网络智能办公系统)是由北京通达信科科技有限公司自主研发的协同办公自动化软件,是与中国企业管理实践相结合形成的综合管理办公平台。通达OA WHERE_STR存在前台SQL注入漏洞,攻击者可通过该漏洞获取数据库敏感信息。 二、影响版本 ●…

全网最详细的从0到1的turbo pnpm monorepo的前端工程化项目[vitePress篇]

全网最详细的从0到1的turbo pnpm monorepo的前端工程化项目[vitePress篇] 前言选型为什么选择VitePress安装VitePress运行优化默认UI使用自定义UI编辑自定义布局编写home页面组件编写page页面组件 结语 前言 一个好的工程化项目&#xff0c;必然有一个好的文档管理&#xff0c;…

【Go-Zero】goctl生成model层后报错Unresolved reference ‘ErrNotFound‘解决方案

【Go-Zero】goctl生成model层后报错Unresolved reference ErrNotFound’解决方案 大家好 我是寸铁&#x1f44a; 总结了一篇goctl生成model层后报错Unresolved reference ErrNotFound’报错解决方案的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题背景 大家好&#xff…

Sora 的工作原理

原文&#xff1a;How Sora Works (And What It Means) 作者&#xff1a; DAN SHIPPER OpenAI 的新型文本到视频模型为电影制作开启了新篇章 DALL-E 提供的插图。 让我们先明确一点&#xff0c;我们不会急急忙忙慌乱。我们不会预测乌托邦或预言灾难。我们要保持冷静并... 你…

java面试多线程篇

文章说明 在文章中对所有的面试题都进行了难易程度和出现频率的等级说明 星数越多代表权重越大&#xff0c;最多五颗星&#xff08;☆☆☆☆☆&#xff09; 最少一颗星&#xff08;☆&#xff09; 1.线程的基础知识 1.1 线程和进程的区别&#xff1f; 难易程度&#xff1a;☆☆…

Filterajax

1.Filter概念 概念:表示过滤器,是JavaWeb三大组件(Servlet,Filter,Listener)之一;过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能.过滤器可以完成一些通用操作比如:登录添加购物车,视频广告,敏感字符处理等等... 2.Filter快速入门 3.Listener 4.Ajax学习 1.使用场…

移动通信相关知识学习笔记

一、移动通信架构简图 移动无线的接入网是专指各种基站设备。核心网就是各种交换机。 二、无线信号基本原理 无线网络中&#xff0c;使用AP设备和天线来实现有线和无线信号互相转换。如上图所示&#xff0c;有线网络侧的数据从AP设备的有线接口进入AP后&#xff0c;经AP处理为…

一.重新回炉Spring Framework: 理解Spring IoC

1. 写在前面的话 说实话&#xff0c;从事java开发工作时间也不短了&#xff0c;对于Spring Framework&#xff0c;也是天天用&#xff0c;这期间也碰到了很多问题&#xff0c;也解决了很多问题。可是&#xff0c;总感觉对Spring Framework还是一知半解&#xff0c;不能有个更加…

PCIe学习笔记(2)错误处理和AER/DPC功能

文章目录 PCIe ErrorAER (Advanced Error Reporting)DPC (Downstream Port Containment) 处理器上错误通常可分为detected和undetected error。Undetected errors可能变得良性(benign)&#xff0c;也可能导致系统故障如silent data corruptions (SDC)。Detected errors则又可分…

2024024期传足14场胜负前瞻

2024024期赛事由亚冠5场&#xff0c;欧冠4场、英超1场、英冠4场组成。售止时间为2月20日&#xff08;周二&#xff09;17点30分&#xff0c;敬请留意&#xff1a; 本期中深盘中等&#xff0c;1.5以下赔率5场&#xff0c;1.5-2.0赔率5场&#xff0c;其他场次是平半盘、平盘。本期…

Django后端开发——ORM

文章目录 参考资料ORM-基础字段及选项字段类型练习——添加模型类应用bookstore下的models.py数据库迁移——同步至mysqlmysql中查看效果 字段选项Meta类定义示例&#xff1a;改表名应用bookstore下的models.py终端效果 练习——改表名字段选项修改应用bookstore下的models.py终…

DVWA 靶场之 Brute Force-LowMedium(前期配置铺垫与渗透方法及源码分析)

首先登录 DVWA 靶场 DVWA 默认的用户有5个&#xff0c;用户名及密码如下&#xff1a; admin/passwordgordonb/abc1231337/charleypablo/letmeinsmithy/password 难度等级设置为 low &#xff0c;我们先从最简单的开始 来到 Brute Force&#xff08;暴力破解&#xff09; 我们可…

手写myscrapy(二)

我们看一下scrapy的系统架构设计方法和思路&#xff1a; 模块化设计&#xff1a; Scrapy采用模块化设计&#xff0c;将整个系统划分为多个独立的模块&#xff0c;包括引擎&#xff08;Engine&#xff09;、调度器&#xff08;Scheduler&#xff09;、下载器&#xff08;Downl…

目录IO 2月19日学习笔记

1. lseek off_t lseek(int fd, off_t offset, int whence); 功能: 重新设定文件描述符的偏移量 参数: fd:文件描述符 offset:偏移量 whence: SEEK_SET 文件开头 SEE…

C++ 浮点数二分 数的三次方根

给定一个浮点数 n &#xff0c;求它的三次方根。 输入格式 共一行&#xff0c;包含一个浮点数 n 。 输出格式 共一行&#xff0c;包含一个浮点数&#xff0c;表示问题的解。 注意&#xff0c;结果保留 6 位小数。 数据范围 −10000≤n≤10000 输入样例&#xff1a; 1000.00…

面试题:链表相交

链表相交 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 思路 这个题目有2个思路&#xff0c;我先说容易想到的思路 对齐链表…

极速提升测试效率:揭秘Web自动化三大等待技巧!

三种等待方式 简介 在实际工作中等待机制可以保证代码的稳定性&#xff0c;保证代码不会受网速、电脑性能等条件的约束。 等待就是当运行代码时&#xff0c;如果页面的渲染速度跟不上代码的运行速度&#xff0c;就需要人为的去限制代码执行的速度。 在做 Web 自动化时&…

Jetpack 之Glance+Compose实现一个小组件

Glance&#xff0c;官方对其解释是使用 Jetpack Compose 样式的 API 构建远程 Surface 的布局&#xff0c;通俗的讲就是使用Compose风格的API来搭建小插件布局&#xff0c;其最新版本是2022年2月23日更新的1.0.0-alpha03。众所周知&#xff0c;Compose样式的API与原生差别不小&…

测试开发【Mock平台】13基础:拦截器服务实现(四) 简单规则匹配逻辑

【Mock平台】为系列测试开发教程&#xff0c;从0到1编码带你一步步使用Spring Boot 和 Antd React框架完成搭建一个测试工具平台&#xff0c;希望作为一个实战项目对各位的测试开发学习之路有帮助&#xff0c;关注公众号发送“mock”获取github项目源码地址&#xff0c;大奇一个…