158 lines
5.0 KiB
Dart
158 lines
5.0 KiB
Dart
|
|
// 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<String, String> uploadPathEnum; // 对应 UPLOAD_FILE_PATH_ENUM
|
|||
|
|
final Set<String> 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<UploadFileItem>
|
|||
|
|
/// - single: bool (default true)
|
|||
|
|
/// - params: Map<String, dynamic> (must contain 'type')
|
|||
|
|
///
|
|||
|
|
/// 返回 UploadResult:
|
|||
|
|
/// - single: 返回 filePath 字段
|
|||
|
|
/// - batch(single==false): 返回 id 字段(对应 foreignKey)
|
|||
|
|
Future<UploadResult> uploadFile({
|
|||
|
|
required List<UploadFileItem>? files,
|
|||
|
|
bool single = true,
|
|||
|
|
required Map<String, dynamic>? 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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|