一、需求来源
最近遇到上传文档附件的需求,基于 file_picker 显示上传百分比,支持失败重连,支持失败删除;每个文档上传成功之后都会进行 url 回调。
支持功能:
- 去重,如果已经上传文档,二次选择会直接过滤,防止重复文档多次上传;;
- 点击支持音频播放、视频播放、文档预览;
效果如下:
二、使用示例
final fileUploadBoxController = NFileUploadBoxController();
final isAllUploadFinished = ValueNotifier(false);
var selectedFiles = [].map((e) => NFileUploadModel(url: e)).toList();
//...
Container(
margin:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
child: NFileUploadBox(
controller: fileUploadBoxController,
title: "上传音视频文件",
description: "支持格式...."
",单个文件大小不超过100m,最多支持10个文件",
maxCount: 9,
allowedExtensions: [
...NFileType.audio.exts,
...NFileType.video.exts,
...NFileType.doc.exts,
...NFileType.excel.exts,
...NFileType.ppt.exts,
...NFileType.pdf.exts,
],
items: selectedFiles,
// showFileSize: true,
onChanged: (List<NFileUploadModel> value) {
selectedFiles = value;
YLog.d("$widget selectedFiles: $selectedFiles");
isAllUploadFinished.value = true;
},
onCancel: () {
isAllUploadFinished.value = true;
},
onStart: () {
isAllUploadFinished.value = false;
},
),
),
[log] DLog 2024-09-07 10:19:14.092997 UploadPageDemo selectedFiles: [{"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/测试视频1.MOV","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/测试视频1.MOV","name":"测试视频1.MOV","size":2286856,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E6%B5%8B%E8%AF%95%E8%A7%86%E9%A2%911.MOV"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/陈一发儿 - 童话镇_副本.mp3","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/陈一发儿 - 童话镇_副本.mp3","name":"陈一发儿 - 童话镇_副本.mp3","size":10405608,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E9%99%88%E4%B8%80%E5%8F%91%E5%84%BF%20-%20%E7%AB%A5%E8%AF%9D%E9%95%87_%E5%89%AF%E6%9C%AC.mp3"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/Flutter闲鱼最佳实践.pdf","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/Flutter闲鱼最佳实践.pdf","name":"Flutter闲鱼最佳实践.pdf","size":21863874,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/Flutter%E9%97%B2%E9%B1%BC%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.pdf"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/研发tapd培训.pptx","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/研发tapd培训.pptx","name":"研发tapd培训.pptx","size":2982153,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E7%A0%94%E5%8F%91tapd%E5%9F%B9%E8%AE%AD.pptx"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/假设房价按年5%增幅.xlsx","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/假设房价按年5%增幅.xlsx","name":"假设房价按年5%增幅.xlsx","size":12897,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E5%81%87%E8%AE%BE%E6%88%BF%E4%BB%B7%E6%8C%89%E5%B9%B45%25%E5%A2%9E%E5%B9%85.xlsx"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/手机APP个税年度汇算操作流程2021042002.docx","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/手机APP个税年度汇算操作流程2021042002.docx","name":"手机APP个税年度汇算操作流程2021042002.docx","size":2653921,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E6%89%8B%E6%9C%BAAPP%E4%B8%AA%E7%A8%8E%E5%B9%B4%E5%BA%A6%E6%B1%87%E7%AE%97%E6%93%8D%E4%BD%9C%E6%B5%81%E7%A8%8B2021042002.docx"}}, {"url":"https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/beta/gcp/图数据库neo4j实战.pdf","file":{"path":"/Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/图数据库neo4j实战.pdf","name":"图数据库neo4j实战.pdf","size":1948037,"identifier":"file:///Users/shang/Library/Developer/CoreSimulator/Devices/0A26CC53-8AE9-4170-8BE3-9A4ECC103E32/data/Containers/Data/Application/93841387-5D9C-477F-AAAB-E43DB919E2EE/tmp/%E5%9B%BE%E6%95%B0%E6%8D%AE%E5%BA%93neo4j%E5%AE%9E%E6%88%98.pdf"}}]
三、源码
1、上传模型源码
/// 文件
class NFileUploadModel {
NFileUploadModel({
this.url,
this.assetFile,
});
/// 上传之后的文件 url
String? url;
/// 本地选择的文件实体
NPickFile? assetFile;
Map<String, dynamic> toJson() {
final data = Map<String, dynamic>();
data['url'] = url;
data['file'] = assetFile?.toJson();
return data;
}
@override
String toString() {
return jsonEncode(toJson());
}
}
/// 文件上传选择实体
class NPickFile {
NPickFile({
required this.path,
required this.name,
required this.size,
this.identifier,
});
final String path;
final String name;
final int size;
final String? identifier;
Map<String, dynamic> toJson() {
final data = Map<String, dynamic>();
data['path'] = path;
data['name'] = name;
data['size'] = size;
data['identifier'] = identifier;
return data;
}
static NPickFile fromPlatformFile(PlatformFile file) {
return NPickFile(
path: file.path ?? "",
name: file.name,
size: file.size,
identifier: file.identifier,
);
}
}
2、NFileUploadBox 源码
//
// NFileUploadBox.dart
// projects
//
// Created by shang on 2024/8/27 15:47.
// Copyright © 2024/8/27 shang. All rights reserved.
//
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:yl_ylgcp_app/extension/file_ext.dart';
import 'package:yl_ylgcp_app/extension/string_ext.dart';
import 'package:yl_ylgcp_app/routers/app_route.dart';
import 'package:yl_ylgcp_app/routers/navigator_util.dart';
import 'package:yl_ylgcp_app/theme/color.dart';
import 'package:yl_ylgcp_app/util/enum/permission_type_enum.dart';
import 'package:yl_ylgcp_app/util/phone_permission.dart';
import 'package:yl_ylgcp_app/util/tool_util.dart';
import 'package:yl_ylgcp_app/util/ylog.dart';
import 'package:yl_ylgcp_app/vender/easy_toast.dart';
import 'package:yl_ylgcp_app/vender/upload_file/n_file_upload_Item.dart';
import 'package:yl_ylgcp_app/vender/upload_file/n_file_upload_model.dart';
import 'package:yl_ylgcp_app/widget/n_pair.dart';
import 'package:yl_ylgcp_app/widget/n_text.dart';
/// 从文件存储系统选择文件
class NFileUploadBox extends StatefulWidget {
const NFileUploadBox({
super.key,
this.controller,
required this.items,
this.title = "上传文件",
this.description = "支持格式/单个文件大小限制/最大数量",
required this.onChanged,
this.onStart,
this.onCancel,
this.canEdit = true,
this.showFileSize = false,
this.maxMB = 100,
this.maxCount = 9,
this.type = FileType.custom,
this.allowMultiple = true,
required this.allowedExtensions,
this.header,
this.footer,
});
final NFileUploadBoxController? controller;
final List<NFileUploadModel> items;
final String title;
final String description;
/// 全部结束(有成功有失败 url="")或者删除完失败图片时会回调
final ValueChanged<List<NFileUploadModel>> onChanged;
/// 开始上传回调
final VoidCallback? onStart;
/// 取消
final VoidCallback? onCancel;
/// 可以编辑
final bool canEdit;
/// 做大个数
final int maxCount;
/// 最大尺寸
final int maxMB;
final FileType type;
final bool allowMultiple;
final List<String> allowedExtensions;
/// 显示文件大小
final bool showFileSize;
final Widget? header;
final Widget? footer;
@override
State<NFileUploadBox> createState() => _NFileUploadBoxState();
}
class _NFileUploadBoxState extends State<NFileUploadBox> {
late final List<NFileUploadModel> selectedModels = [];
/// 全部上传结束
final isAllUploadFinished = ValueNotifier(false);
@override
void dispose() {
widget.controller?._detach(this);
super.dispose();
}
@override
void initState() {
widget.controller?._attach(this);
selectedModels.addAll(widget.items);
super.initState();
}
@override
void didUpdateWidget(covariant NFileUploadBox oldWidget) {
final entityIds = widget.items.map((e) => e.assetFile?.path).join(",");
final oldWidgetEntityIds =
oldWidget.items.map((e) => e.assetFile?.path).join(",");
if (entityIds != oldWidgetEntityIds) {
selectedModels
..clear()
..addAll(widget.items);
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.header ??
Padding(
padding: EdgeInsets.symmetric(vertical: 5),
child: NText(
data: widget.title,
fontSize: 14,
color: fontColor737373,
),
),
...selectedModels.map((e) {
final index = selectedModels.indexOf(e);
// final fileName = (e.assetFile?.path ?? "").split("/").last;
return GestureDetector(
onTap: () => onTapItem(e),
child: NFileUploadItem(
model: e,
canEdit: widget.canEdit,
urlBlock: (url) {
final isAllFinished =
selectedModels.where((e) => e.url == null).isEmpty;
// debugPrint("isAllFinished: ${isAllFinished}");
if (isAllFinished) {
final urls = selectedModels.map((e) => e.url).toList();
debugPrint("isAllFinished urls: $urls");
widget.onChanged(selectedModels);
isAllUploadFinished.value = true;
}
},
onDelete: widget.canEdit == false
? null
: () {
debugPrint(
"onDelete: $index, length: ${selectedModels[index].assetFile?.path}");
selectedModels.remove(e);
setState(() {});
widget.onChanged(selectedModels);
},
showFileSize: widget.showFileSize,
),
);
}).toList(),
if (selectedModels.length < widget.maxCount && widget.canEdit)
buildUploadButton(
onPressed: () async {
await onPickFile(
maxMB: widget.maxMB,
maxCount: widget.maxCount,
type: widget.type,
allowMultiple: widget.allowMultiple,
allowedExtensions: widget.allowedExtensions,
onPermission: () async {
//todo: 安卓权限
bool isGranted = await PhonePermission.checkDocument(
permissionType: PermissionTypeEnum.im,
);
return isGranted;
},
);
},
),
Offstage(
offstage: !widget.canEdit,
child: widget.footer ??
Padding(
padding: const EdgeInsets.only(top: 5.0),
child: NText(
data: widget.description,
fontSize: 12,
color: fontColorB3B3B3,
),
),
),
],
);
}
Widget buildUploadButton({required VoidCallback onPressed}) {
return GestureDetector(
onTap: onPressed,
child: Container(
width: double.infinity,
height: 36,
decoration: BoxDecoration(
// color: bgColor,
border: Border.all(color: lineColor),
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: NPair(
icon: Image(
image: "icon_upload_one.png".toAssetImage(),
width: 16,
height: 16,
),
child: NText(
data: "选择文件并上传",
fontSize: 14,
color: fontColorB3B3B3,
),
),
),
);
}
/// 选择文件
Future<void> onPickFile({
int maxMB = 100,
int maxCount = 9,
FileType type = FileType.any,
bool allowMultiple = true,
required List<String> allowedExtensions,
required FutureOr<bool> Function() onPermission,
}) async {
if (!await onPermission()) {
return;
}
try {
FilePickerResult? pickerResult = await FilePicker.platform.pickFiles(
type: type,
allowMultiple: allowMultiple,
allowedExtensions: allowedExtensions,
);
final result = pickerResult?.files ?? [];
final lengthBefore = result.length;
YLog.d("result: 过滤前 ${result.length}");
result.removeWhere((el) {
final same =
selectedModels.map((e) => e.assetFile?.path).contains(el.path);
final result = same || el.size > maxMB * 1024 * 1024;
return result;
});
final lengthAfter = result.length;
YLog.d("result: 过滤后 ${result.length}");
final filterCount = lengthBefore - lengthAfter;
if (filterCount > 0) {
EasyToast.showToast("已过滤 $filterCount 个无效文件");
}
if (result.isEmpty == true) {
debugPrint("没有添加");
widget.onCancel?.call();
return;
}
widget.onStart?.call();
isAllUploadFinished.value = false;
for (final e in result) {
if (!selectedModels.contains(e)) {
selectedModels
.add(NFileUploadModel(assetFile: NPickFile.fromPlatformFile(e)));
}
}
if (selectedModels.length > maxCount) {
selectedModels.removeRange(0, selectedModels.length - maxCount);
}
setState(() {});
} catch (e) {
debugPrint("$this $e");
}
}
onTapItem(NFileUploadModel model) {
if (model.url?.startsWith("http") == true) {
final fileName = model.url?.split("/").last;
switch (model.url!.fileType) {
case NFileType.image:
{
final urls = selectedModels
.where((e) => e.url?.startsWith("http") == true)
.map((e) => e.url ?? "")
.toList();
final index = urls.indexOf(model.url ?? "");
// debugPrint("urls: ${urls.length}, $index");
FocusScope.of(context).unfocus();
ToolUtil.imagePreview(urls, index);
}
case NFileType.video:
{
Get.toNamed(APPRouter.chewiePlayerPage, arguments: {
"videoUrl": model.url,
"videoTitle": fileName,
});
}
break;
case NFileType.audio:
{
Get.toNamed(APPRouter.audioPlayPage, arguments: {
"url": model.url,
"desc": fileName,
"title": fileName,
});
}
break;
case NFileType.doc:
case NFileType.excel:
case NFileType.ppt:
case NFileType.pdf:
{
FocusScope.of(context).unfocus();
ToolUtil.openWebViewPage(model.url ?? "", title: fileName);
}
break;
default:
break;
}
}
}
}
/// AssetUploadBox 组件控制器
class NFileUploadBoxController {
_NFileUploadBoxState? _anchor;
void _attach(_NFileUploadBoxState anchor) {
_anchor = anchor;
}
void _detach(_NFileUploadBoxState anchor) {
if (_anchor == anchor) {
_anchor = null;
}
}
/// 是否全部上传结束
ValueNotifier<bool>? get isAllUploadFinished => _anchor?.isAllUploadFinished;
}
3、每个文件对应的子组件 NFileUploadItem 源码
//
// NFileUploadItem.dart
// yl_health_app
//
// Created by shang on 2023/04/30 11:19.
// Copyright © 2023/04/30 shang. All rights reserved.
//
import 'dart:io';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:yl_ylgcp_app/extension/file_ext.dart';
import 'package:yl_ylgcp_app/extension/num_ext.dart';
import 'package:yl_ylgcp_app/http/oss/upload_oss.dart';
import 'package:yl_ylgcp_app/theme/color.dart';
import 'package:yl_ylgcp_app/vender/image_service.dart';
import 'package:yl_ylgcp_app/vender/upload_file/n_file_upload_model.dart';
import 'package:yl_ylgcp_app/vender/video_service.dart';
import 'package:yl_ylgcp_app/widget/n_text.dart';
/// 上传图片单元(基于 wechat_assets_picker)
class NFileUploadItem extends StatefulWidget {
const NFileUploadItem({
super.key,
required this.model,
this.radius = 4,
this.urlBlock,
this.onDelete,
this.canEdit = true,
this.showFileSize = false,
this.borderColor = Colors.transparent,
});
final NFileUploadModel model;
/// 圆角 默认8
final double radius;
/// 上传成功获取 url 回调
final ValueChanged<String>? urlBlock;
/// 返回删除元素的 id
final VoidCallback? onDelete;
/// 显示文件大小
final bool showFileSize;
/// 边框颜色
final Color borderColor;
/// 是否可编辑 - 删除
final bool canEdit;
@override
NFileUploadItemState createState() => NFileUploadItemState();
}
class NFileUploadItemState extends State<NFileUploadItem>
with AutomaticKeepAliveClientMixin {
/// 防止触发多次上传动作
var _isLoading = false;
/// 请求成功或失败
final _successVN = ValueNotifier(true);
/// 上传进度
final _percentVN = ValueNotifier(0.0);
String? get filePath => widget.model.assetFile?.path;
String? get fileName => widget.model.assetFile?.name;
// bool? get only => widget.model.url?.startsWith("http") == true;
@override
void initState() {
super.initState();
onRefresh();
}
@override
void didUpdateWidget(covariant NFileUploadItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.model.assetFile?.path == oldWidget.model.assetFile?.path ||
widget.model.url?.startsWith("http") == true) {
return;
}
onRefresh();
}
@override
Widget build(BuildContext context) {
super.build(context);
final fileName = widget.model.assetFile?.name ??
widget.model.url?.split("/").last ??
"--";
final fileNameNew = fileName.split(".").firstOrNull ?? "--";
final ext = fileName.split(".").lastOrNull ?? "";
final nameWidget = Row(
children: [
Flexible(
child: NText(
data: fileNameNew,
fontSize: 14,
color: fontColor737373,
maxLines: 1,
),
),
NText(
data: ".$ext",
fontSize: 14,
color: fontColor737373,
maxLines: 1,
),
],
);
return Container(
margin: const EdgeInsets.only(
bottom: 9,
),
padding: const EdgeInsets.only(
left: 10,
top: 9,
bottom: 9,
right: 12,
),
decoration: const BoxDecoration(
color: bgColor,
// border: Border.all(color: Colors.blue),
borderRadius: BorderRadius.all(Radius.circular(4)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: GestureDetector(
child: nameWidget ??
NText(
data: fileName,
fontSize: 14,
color: fontColor737373,
maxLines: 1,
),
),
),
buildDelete(),
],
),
if (widget.canEdit) buildUploading(),
if (widget.showFileSize)
buildFileSizeInfo(
length: widget.model.assetFile?.size,
),
],
),
);
}
/// 右上角删除按钮
Widget buildDelete() {
if (widget.onDelete == null) {
return const SizedBox();
}
return GestureDetector(
onTap: widget.onDelete,
child: const Image(
image: AssetImage(
"assets/images/icon_trash.png",
),
width: 12,
height: 14,
),
);
}
Widget buildUploading() {
return AnimatedBuilder(
animation: Listenable.merge([
_successVN,
_percentVN,
]),
builder: (context, child) {
// YLog.d("buildUploading ${fileName}: ${_percentVN.value}");
if (_successVN.value == false) {
return buildUploadFail();
}
final value = _percentVN.value;
if (value >= 1) {
return const SizedBox();
}
final showPercent = widget.model.assetFile != null &&
(widget.model.assetFile!.size > 2 * 1024 * 1024) == true;
final desc = showPercent ? value.toStringAsPercent(2) : "上传中";
return Row(
children: [
Expanded(
child: LinearProgressIndicator(
value: _percentVN.value,
),
),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: NText(
data: desc,
fontSize: 12,
),
),
],
);
},
);
}
Widget buildUploadFail() {
return InkWell(
onTap: onRefresh,
child: const Row(
children: [
// Icon(Icons.refresh, color: Colors.red),
NText(
data: "点击重试",
fontSize: 14,
color: Colors.red,
),
],
),
);
}
Future<String?> uploadFile({
required String path,
}) async {
var res = await OssUtil.upload(
filePath: path,
onSendProgress: (int count, int total) {
final percent = (count / total);
if (percent >= 0.99) {
_percentVN.value = 0.99;
} else {
_percentVN.value = percent;
}
},
onReceiveProgress: (int count, int total) {
_percentVN.value = 1;
},
);
_percentVN.value = 1;
if (res != null) {
// debugPrint("res: $res");
return res;
}
return null;
}
onRefresh() {
if (!widget.canEdit) {
return;
}
// debugPrint("onRefresh ${widget.entity}");
final path = widget.model.assetFile?.path;
if (path == null || path.isEmpty) {
throw "文件路径为空";
}
if (_isLoading) {
debugPrint("_isLoading: $_isLoading ${widget.model.assetFile}");
return;
}
_isLoading = true;
_successVN.value = true;
compressFile(
file: File(path),
).then((file) {
return uploadFile(path: file.path);
}).then((value) {
final url = value;
if (url == null || url.isEmpty) {
_successVN.value = false;
throw "上传失败 ${widget.model.assetFile?.path}";
}
_successVN.value = true;
widget.model.url = url;
}).catchError((err) {
debugPrint("err: $err");
widget.model.url = "";
_successVN.value = false;
}).whenComplete(() {
_isLoading = false;
// LogUtil.d("${fileName}_whenComplete");
widget.urlBlock?.call(widget.model.url ?? "");
});
}
/// 压缩文件
Future<File> compressFile({required File file}) async {
final ext = file.path.split(".").last;
if (NFileType.video.exts.contains(ext)) {
return await VideoService.compressVideo(file, showToast: false);
}
if (NFileType.image.exts.contains(ext)) {
return await ImageService().compressAndGetFile(file, needLogInfo: false);
}
return file;
}
Widget buildFileSizeInfo({required int? length}) {
if (length == null) {
return const SizedBox();
}
final result = length / (1024 * 1024);
final desc = "${result.toStringAsFixed(2)}MB";
return Align(child: Text(desc));
}
@override
bool get wantKeepAlive => true;
}
4、文件类型支持
/// app 文件类型
enum NFileType {
unknown('未知', []),
image('图片', ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic']),
video(
'视频', ['mp4', 'avi', 'wmv', 'rmvb', 'mpg', 'mpeg', 'mov', '3gp', 'flv']),
audio('音频', ['mp3', 'wav', 'wma', 'amr', 'ogg']),
doc('word文档', ['doc', 'docx']),
excel('excel文档', ['xls', 'xlsx']),
ppt('ppt文档', ['ppt', 'pptx']),
pdf('ppt文档', ['pdf']);
const NFileType(this.message, this.exts);
/// 描述
final String message;
/// 类型
final List<String> exts;
}
extension FileExt on File {
/// 获取文件类型
NFileType get fileType => path.fileType;
/// length 转为 MB 描述
String get fileSizeDesc {
final length = lengthSync();
return length.fileSizeDesc;
}
/// 压缩质量
int get compressQuality {
final length = lengthSync();
return length.compressQuality;
}
}
extension FileStringExt on String {
/// 获取文件类型
NFileType get fileType {
if (!contains(".")) {
return NFileType.unknown;
}
final ext = split('.').last.toLowerCase();
for (final e in NFileType.values) {
if (e.exts.contains(ext)) {
return e;
}
}
return NFileType.unknown;
}
}
总结
1、本文是 AssetUploadDocumentBox 组件的升级版;理论上支持所有的文件类型选择;
2、NFileUploadBox 中点击子组件跳转可以更换为自己的实现
3、未解决的问题
- 如何限制最大选择数量?
- 文档是否有好的压缩库?