// upload_file_service.dart import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:image_picker/image_picker.dart'; /// 上传结果 - 简单包装 class UploadResult { final String? filePath; final String? id; UploadResult({this.filePath, this.id}); } /// 可接受的上传文件项:支持 File、XFile,或已有的 filePath(表示无需重新上传) class UploadFileItem { final File? file; final XFile? xfile; final String? filePath; // 已有文件的返回路径(老文件) UploadFileItem({this.file, this.xfile, this.filePath}); } /// UploadFileService: 可被 Provider/Consumer 或直接持有 class UploadFileService extends ChangeNotifier { final Dio dio; final Map uploadPathEnum; // 对应 UPLOAD_FILE_PATH_ENUM final Set uploadTypeEnum; // 对应 UPLOAD_FILE_TYPE_ENUM 的取值集合 bool _loading = false; bool get loading => _loading; UploadFileService({ Dio? dioInstance, required this.uploadPathEnum, required this.uploadTypeEnum, }) : dio = dioInstance ?? Dio(); void _setLoading(bool v) { _loading = v; notifyListeners(); } /// options: /// - files: List /// - single: bool (default true) /// - params: Map (must contain 'type') /// /// 返回 UploadResult: /// - single: 返回 filePath 字段 /// - batch(single==false): 返回 id 字段(对应 foreignKey) Future uploadFile({ required List? files, bool single = true, required Map? params, }) async { if (params == null) { throw ArgumentError('请传入 options.params'); } final type = params['type']; if (type == null) { throw ArgumentError('请传入 options.params.type'); } if (!uploadTypeEnum.contains(type)) { throw ArgumentError('传入的 type 不在 UPLOAD_FILE_TYPE_ENUM 中'); } final path = uploadPathEnum[type]; if (path == null || path.isEmpty) { throw ArgumentError('未找到 type $type 对应的 path'); } if (!single && (params['foreignKey'] == null)) { throw ArgumentError('single 为 false 时,options.params.foreignKey 必需'); } _setLoading(true); try { final fileItems = files ?? []; // 如果没有文件则直接返回(与原逻辑一致) if (fileItems.isEmpty) { return single ? UploadResult(filePath: '') : UploadResult(id: ''); } // 检查是否有真正需要上传的文件(File 或 XFile) final needUpload = fileItems.where((it) => it.file != null || it.xfile != null).toList(); // 如果没有实际要上传的文件,返回老文件(files[0].filePath 或 params.foreignKey) if (needUpload.isEmpty) { _setLoading(false); return single ? UploadResult(filePath: fileItems[0].filePath ?? '') : UploadResult(id: params['foreignKey']?.toString() ?? ''); } // 构建 FormData final formData = FormData(); // 添加文件字段 - 同名 "files"(与后端约定) for (final it in needUpload) { String filePath; if (it.file != null) { filePath = it.file!.path; } else if (it.xfile != null) { // XFile.path 在 web 上不是本地文件(注意),这里只为移动端/桌面可用 filePath = it.xfile!.path; } else { continue; } final filename = filePath.split(Platform.pathSeparator).last; final multipart = await MultipartFile.fromFile(filePath, filename: filename); formData.files.add(MapEntry('files', multipart)); } // 添加 params 字段(全部作为字符串) params.forEach((k, v) { if (v == null) return; formData.fields.add(MapEntry(k, v.toString())); }); // 添加 path 字段 formData.fields.add(MapEntry('path', path)); // 选择 URL:单文件/批量 final url = single ? '/basicInfo/imgFiles/save' : '/basicInfo/imgFiles/batchSave'; final response = await dio.post( url, data: formData, options: Options( contentType: 'multipart/form-data', // 如果需要加额外 headers 可以在这里传入 ), ); // 解析响应(假定后端返回结构与原 JS 一致:res.data.filePath / res.data.foreignKey) final resData = response.data; if (single) { final String? returnedPath = (resData is Map && resData['filePath'] != null) ? resData['filePath'].toString() : null; return UploadResult(filePath: returnedPath); } else { final String? foreignKey = (resData is Map && resData['foreignKey'] != null) ? resData['foreignKey'].toString() : null; return UploadResult(id: foreignKey); } } catch (e) { // 将错误上抛,调用方可 catch rethrow; } finally { _setLoading(false); } } }