|
@@ -0,0 +1,440 @@
|
|
|
+// uploadFileField.dart
|
|
|
+import 'dart:io';
|
|
|
+import 'package:cs_resources/constants/color_constants.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:plugin_basic/basic_export.dart';
|
|
|
+import 'package:plugin_platform/engine/toast/toast_engine.dart';
|
|
|
+import 'package:shared/utils/log_utils.dart';
|
|
|
+import 'package:shared/utils/util.dart';
|
|
|
+import 'package:widgets/ext/ex_widget.dart';
|
|
|
+import 'package:widgets/my_text_view.dart';
|
|
|
+import 'package:file_picker/file_picker.dart';
|
|
|
+
|
|
|
+class FileFieldItem {
|
|
|
+ final String fieldKey; // 字段key
|
|
|
+ final String? fieldLabel; // 文件字段名称
|
|
|
+ late List<String>? fieldPathValueList; // 文件字段值
|
|
|
+ final String? fieldPlaceholder; // 文件字段提示
|
|
|
+ final String? maxSize; // 文件大小限制 如 '500B' '1KB' ‘1MB’
|
|
|
+ final bool multiple; // 是否允许多文件上传
|
|
|
+ final bool canUpload; // 是否可上传
|
|
|
+ final bool disabled; // 是否禁用
|
|
|
+ final bool preview; // 是否可以点击预览
|
|
|
+ final bool download; // 是否可以下载
|
|
|
+ final List<String> allowedFileTypes; // 允许的文件类型
|
|
|
+ final Function(List<File>)? onFilesSelected; // 文件选择回调
|
|
|
+ final Function(File)? onFileRemoved; // 文件删除回调
|
|
|
+ final Function(File)? onFilePreview; // 文件预览回调
|
|
|
+ final Function(File)? onFileDownload; // 文件下载回调
|
|
|
+
|
|
|
+ FileFieldItem({
|
|
|
+ required this.fieldKey,
|
|
|
+ this.fieldLabel,
|
|
|
+ fieldPathValueList,
|
|
|
+ fieldPlaceholder,
|
|
|
+ this.maxSize,
|
|
|
+ multiple,
|
|
|
+ canUpload,
|
|
|
+ disabled,
|
|
|
+ preview,
|
|
|
+ download, // 默认不启用下载
|
|
|
+ allowedFileTypes,
|
|
|
+ this.onFilesSelected,
|
|
|
+ this.onFileRemoved,
|
|
|
+ this.onFilePreview,
|
|
|
+ this.onFileDownload,
|
|
|
+ }): this.fieldPathValueList = fieldPathValueList!=null?fieldPathValueList?.cast<String>()?? []: null,
|
|
|
+ this.fieldPlaceholder = fieldPlaceholder?? 'Choose File',
|
|
|
+ this.canUpload = canUpload?? true,
|
|
|
+ this.multiple = multiple?? false,
|
|
|
+ this.disabled = disabled?? false,
|
|
|
+ this.preview = preview?? false,
|
|
|
+ this.download = download?? false,
|
|
|
+ this.allowedFileTypes = allowedFileTypes ?? ['*'];
|
|
|
+}
|
|
|
+
|
|
|
+class UploadFileField extends StatefulWidget {
|
|
|
+ final FileFieldItem fileField;
|
|
|
+
|
|
|
+ const UploadFileField({
|
|
|
+ Key? key,
|
|
|
+ required this.fileField,
|
|
|
+ }) : super(key: key);
|
|
|
+
|
|
|
+ @override
|
|
|
+ _UploadFileFieldState createState() => _UploadFileFieldState();
|
|
|
+}
|
|
|
+
|
|
|
+class _UploadFileFieldState extends State<UploadFileField> {
|
|
|
+ String? _filedInputText;
|
|
|
+ List<File> _selectedFiles = [];
|
|
|
+ String? _errorMessage;
|
|
|
+
|
|
|
+ @override
|
|
|
+ void initState() {
|
|
|
+ super.initState();
|
|
|
+
|
|
|
+ if(widget.fileField.multiple){
|
|
|
+ // 多选
|
|
|
+ if(widget.fileField.fieldPathValueList!=null && widget.fileField.fieldPathValueList!.isNotEmpty){
|
|
|
+ _filedInputText = widget.fileField.fieldPathValueList!.map((url) {
|
|
|
+ if(url.isEmpty) return '';
|
|
|
+ try {
|
|
|
+ return Uri.parse(url).pathSegments.last;
|
|
|
+ } catch (e) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }).where((element) => element.isNotEmpty).join(",");
|
|
|
+ }
|
|
|
+ }else {
|
|
|
+ // 单选
|
|
|
+ if (widget.fileField.fieldPathValueList != null &&
|
|
|
+ widget.fileField.fieldPathValueList!.isNotEmpty) {
|
|
|
+ try {
|
|
|
+ String firstUrl = widget.fileField.fieldPathValueList!.first;
|
|
|
+ if (firstUrl.isNotEmpty) {
|
|
|
+ _filedInputText = Uri.parse(firstUrl).pathSegments.last;
|
|
|
+ } else {
|
|
|
+ _filedInputText = "";
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ _filedInputText = "";
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ _filedInputText = "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将大小字符串转换为字节 B
|
|
|
+ int _parseSizeToBytes(String maxSize) {
|
|
|
+ final RegExp regex = RegExp(r'(\d+)([KMGT]?B)', caseSensitive: false);
|
|
|
+ final match = regex.firstMatch(maxSize.toUpperCase());
|
|
|
+
|
|
|
+ if (match == null) return 0;
|
|
|
+
|
|
|
+ final number = int.parse(match.group(1)!);
|
|
|
+ final unit = match.group(2);
|
|
|
+
|
|
|
+ switch (unit) {
|
|
|
+ case 'KB':
|
|
|
+ return number * 1024;
|
|
|
+ case 'MB':
|
|
|
+ return number * 1024 * 1024;
|
|
|
+ case 'GB':
|
|
|
+ return number * 1024 * 1024 * 1024;
|
|
|
+ case 'TB':
|
|
|
+ return number * 1024 * 1024 * 1024 * 1024;
|
|
|
+ default:
|
|
|
+ return number;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void _pickFiles() async {
|
|
|
+ if (widget.fileField.disabled) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ FilePickerResult? result = await FilePicker.platform.pickFiles(
|
|
|
+ allowMultiple: widget.fileField.multiple,
|
|
|
+ type: FileType.custom,
|
|
|
+ // allowedExtensions: widget.fileField.allowedFileTypes,
|
|
|
+ allowedExtensions: ['jpg', 'png', 'jpeg', 'pdf', 'docx','mp4'],
|
|
|
+ );
|
|
|
+
|
|
|
+ Log.d("--选取的文件结果---- ${result}");
|
|
|
+ if (result != null) {
|
|
|
+ if(!widget.fileField.multiple){
|
|
|
+ // 清空上次_selectedFiles
|
|
|
+ _selectedFiles = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ List<PlatformFile> files = result.files;
|
|
|
+ _validateAndSetFiles(files);
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ setState(() {
|
|
|
+ _errorMessage = '文件选择失败';
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void _validateAndSetFiles(List<PlatformFile> platFormFiles) {
|
|
|
+ Log.d("开始验证和设置文件--${platFormFiles}-");
|
|
|
+ setState(() {
|
|
|
+ _errorMessage = null;
|
|
|
+ });
|
|
|
+
|
|
|
+ Log.d("验证文件大小和类型----${widget.fileField.multiple}---");
|
|
|
+ // 检查是否超过最大文件数量
|
|
|
+ if (!widget.fileField.multiple && (_selectedFiles.length + platFormFiles.length) > 1) {
|
|
|
+ setState(() {
|
|
|
+ _errorMessage = '只允许上传一个文件';
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Log.d("检查文件大小和类型---类型:${widget.fileField.allowedFileTypes}----大小限制:${widget.fileField.maxSize}-");
|
|
|
+
|
|
|
+ // 检查文件类型
|
|
|
+ if (!widget.fileField.allowedFileTypes.contains('*')) {
|
|
|
+ bool allowTypeRes = _checkAllowFileType(platFormFiles);
|
|
|
+ Log.d("允许文件类型结果--${allowTypeRes}");
|
|
|
+ if (!allowTypeRes) {
|
|
|
+ ToastEngine.show("${_errorMessage}");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Log.d("所有文件类型验证通过");
|
|
|
+
|
|
|
+ // 检查文件大小
|
|
|
+ if(widget.fileField.maxSize != null){
|
|
|
+ Log.d("开始检查文件大小");
|
|
|
+ final maxSize = _parseSizeToBytes(widget.fileField.maxSize!);
|
|
|
+ Log.d("文件大小限制字节数:${maxSize} B");
|
|
|
+ bool isAlLowFileSizeRes = _checkAllowFileSize(platFormFiles, maxSize);
|
|
|
+ Log.d("所有文件大小验证结果--${isAlLowFileSizeRes}");
|
|
|
+ if(!isAlLowFileSizeRes){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setState(() {
|
|
|
+ List<File> files = platFormFiles.map((file) {
|
|
|
+ return File(file.path!);
|
|
|
+ }).toList();
|
|
|
+ if (widget.fileField.multiple) {
|
|
|
+ _selectedFiles.addAll(files);
|
|
|
+ } else {
|
|
|
+ _selectedFiles = files.take(1).toList();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ Log.d("当前选中的文件:$_selectedFiles");
|
|
|
+ if(widget.fileField.multiple && !widget.fileField.disabled){
|
|
|
+ setState(() {
|
|
|
+ _filedInputText = _selectedFiles.map((file) {
|
|
|
+ return Uri.parse(file.path).pathSegments.last;
|
|
|
+ }).join(",");
|
|
|
+ });
|
|
|
+ }else {
|
|
|
+ setState(() {
|
|
|
+ _filedInputText = Uri.parse(_selectedFiles.first.path).pathSegments.last;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ widget.fileField.onFilesSelected?.call(_selectedFiles);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小
|
|
|
+ bool _checkAllowFileSize(List<PlatformFile> platFormFiles, int maxSize){
|
|
|
+ for (var platFormFile in platFormFiles) {
|
|
|
+ // 检查文件大小
|
|
|
+ if (platFormFile.size > maxSize) {
|
|
|
+ setState(() {
|
|
|
+ _errorMessage = '文件大小超过限制 (${widget.fileField.maxSize})';
|
|
|
+ });
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Log.d("文件大小检查通过---");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件类型
|
|
|
+ bool _checkAllowFileType(List<PlatformFile> platFormFiles){
|
|
|
+ for (var platFormFile in platFormFiles) {
|
|
|
+ String filePath = platFormFile.path!;
|
|
|
+ String fileName = filePath.substring(filePath.lastIndexOf("/") + 1);
|
|
|
+ String fileSuffix = fileName.substring(fileName.lastIndexOf(".") + 1);
|
|
|
+ // 截取文件后缀进行判断
|
|
|
+ if (!widget.fileField.allowedFileTypes.contains(fileSuffix)) {
|
|
|
+ setState(() {
|
|
|
+ _errorMessage = "文件类型错误,请选择${widget.fileField.allowedFileTypes.join(",")}文件";
|
|
|
+ });
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Log.d("文件类型检查通过---");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ void _removeFile(File file) {
|
|
|
+ setState(() {
|
|
|
+ _selectedFiles.remove(file);
|
|
|
+ _errorMessage = null;
|
|
|
+ });
|
|
|
+ widget.fileField.onFileRemoved?.call(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ void _previewFile(File file) {
|
|
|
+ if (!widget.fileField.preview || widget.fileField.disabled) return;
|
|
|
+
|
|
|
+ // 如果有自定义预览回调,使用回调
|
|
|
+ if (widget.fileField.onFilePreview != null) {
|
|
|
+ widget.fileField.onFilePreview!(file);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认预览行为 - 显示一个简单的预览对话框
|
|
|
+ _showDefaultPreview(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ void _downloadFile(File file) {
|
|
|
+ if (!widget.fileField.download || widget.fileField.disabled) return;
|
|
|
+
|
|
|
+ // 如果有自定义下载回调,使用回调
|
|
|
+ if (widget.fileField.onFileDownload != null) {
|
|
|
+ widget.fileField.onFileDownload!(file);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认下载行为
|
|
|
+ _showDefaultDownload(file);
|
|
|
+ }
|
|
|
+
|
|
|
+ void _showDefaultPreview(File file) {
|
|
|
+ showDialog(
|
|
|
+ context: context,
|
|
|
+ builder: (BuildContext context) {
|
|
|
+ return AlertDialog(
|
|
|
+ title: Text(file.path.split('/').last),
|
|
|
+ content: SizedBox(
|
|
|
+ width: double.maxFinite,
|
|
|
+ child: Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ const Icon(Icons.insert_drive_file, size: 60, color: Colors.blue),
|
|
|
+ const SizedBox(height: 16),
|
|
|
+ Text('文件路径: ${file.path}'),
|
|
|
+ Text('文件大小: ${(file.lengthSync() / 1024).toStringAsFixed(2)} KB'),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ actions: [
|
|
|
+ TextButton(
|
|
|
+ onPressed: () => Navigator.of(context).pop(),
|
|
|
+ child: const Text('关闭'),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ void _showDefaultDownload(File file) {
|
|
|
+ // 下载
|
|
|
+ // ScaffoldMessenger.of(context).showSnackBar(
|
|
|
+ // SnackBar(
|
|
|
+ // content: Text('开始下载文件: ${file.path.split('/').last}'),
|
|
|
+ // duration: const Duration(seconds: 2),
|
|
|
+ // ),
|
|
|
+ // );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ //附件文件
|
|
|
+ MyTextView(
|
|
|
+ widget.fileField.fieldLabel??'-',
|
|
|
+ fontSize: 15,
|
|
|
+ isFontRegular: true,
|
|
|
+ textColor: Colors.white,
|
|
|
+ marginTop: 15,
|
|
|
+ ),
|
|
|
+
|
|
|
+ // 显示和选择附件
|
|
|
+ Container(
|
|
|
+ padding: const EdgeInsets.only(left: 16),
|
|
|
+ margin: const EdgeInsets.only(top: 10),
|
|
|
+ height: 45,
|
|
|
+ decoration: BoxDecoration(
|
|
|
+ color: const Color(0xFF4DCFF6).withOpacity(widget.fileField.disabled ? 0.5 : 0.2),
|
|
|
+ borderRadius: const BorderRadius.all(Radius.circular(5)),
|
|
|
+ ),
|
|
|
+ child: Row(
|
|
|
+ mainAxisSize: MainAxisSize.max,
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
+ mainAxisAlignment: MainAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ MyTextView(
|
|
|
+ '${_filedInputText}',
|
|
|
+ fontSize: 14,
|
|
|
+ hint: "Choose File".tr,
|
|
|
+ textHintColor: ColorConstants.textGrayAECAE5,
|
|
|
+ isFontMedium: true,
|
|
|
+ textColor: ColorConstants.white,
|
|
|
+ ).expanded(),
|
|
|
+
|
|
|
+ // 上传附件的图标
|
|
|
+ Visibility(
|
|
|
+ visible: widget.fileField.canUpload && !widget.fileField.disabled,
|
|
|
+ child: MyTextView(
|
|
|
+ 'Upload'.tr,
|
|
|
+ boxHeight: 45,
|
|
|
+ textAlign: TextAlign.center,
|
|
|
+ boxWidth: 90,
|
|
|
+ cornerRadius: 5,
|
|
|
+ onClick: _pickFiles,
|
|
|
+ textColor: Colors.white,
|
|
|
+ fontSize: 15,
|
|
|
+ fontWeight: FontWeight.w400,
|
|
|
+ backgroundColor: ColorConstants.textGreen0AC074,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget _buildFileItem(File file) {
|
|
|
+ return Container(
|
|
|
+ margin: const EdgeInsets.only(bottom: 4),
|
|
|
+ child: Row(
|
|
|
+ children: [
|
|
|
+ const Icon(Icons.insert_drive_file, size: 16, color: Colors.blue),
|
|
|
+ const SizedBox(width: 8),
|
|
|
+ Expanded(
|
|
|
+ child: GestureDetector(
|
|
|
+ onTap: () => _previewFile(file),
|
|
|
+ child: Text(
|
|
|
+ file.path.split('/').last,
|
|
|
+ style: TextStyle(
|
|
|
+ fontSize: 14,
|
|
|
+ color: widget.fileField.preview && !widget.fileField.disabled
|
|
|
+ ? Colors.blue
|
|
|
+ : Colors.black,
|
|
|
+ decoration: widget.fileField.preview && !widget.fileField.disabled
|
|
|
+ ? TextDecoration.underline
|
|
|
+ : TextDecoration.none,
|
|
|
+ ),
|
|
|
+ overflow: TextOverflow.ellipsis,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ if (widget.fileField.download && !widget.fileField.disabled)
|
|
|
+ IconButton(
|
|
|
+ icon: const Icon(Icons.download, size: 18, color: Colors.green),
|
|
|
+ onPressed: () => _downloadFile(file),
|
|
|
+ padding: const EdgeInsets.all(0),
|
|
|
+ constraints: const BoxConstraints(),
|
|
|
+ ),
|
|
|
+ if (!widget.fileField.disabled)
|
|
|
+ IconButton(
|
|
|
+ icon: const Icon(Icons.delete, size: 18, color: Colors.red),
|
|
|
+ onPressed: () => _removeFile(file),
|
|
|
+ padding: const EdgeInsets.all(0),
|
|
|
+ constraints: const BoxConstraints(),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|