11 changed files with 453 additions and 213 deletions
@ -0,0 +1,12 @@ |
|||||||
|
/// 上传操作的进度和结果模型 |
||||||
|
class UploadResult { |
||||||
|
final int successCount; |
||||||
|
final int failureCount; |
||||||
|
final bool wasCancelled; |
||||||
|
|
||||||
|
UploadResult({ |
||||||
|
this.successCount = 0, |
||||||
|
this.failureCount = 0, |
||||||
|
this.wasCancelled = false, |
||||||
|
}); |
||||||
|
} |
||||||
@ -1,144 +1,115 @@ |
|||||||
// import 'dart:convert'; |
import 'dart:convert'; |
||||||
|
|
||||||
// import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; |
import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; |
||||||
// import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; |
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; |
||||||
// import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; |
import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; |
||||||
|
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; |
||||||
// /// 问题的数据模型。 |
|
||||||
// /// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 |
/// ProblemDto (Data Transfer Object) |
||||||
// class ProblemRemoteDto { |
/// |
||||||
// /// 问题的唯一标识符,可空。 |
/// 这个模型专门用于与远程 API 进行数据交互。 |
||||||
// final String id; |
/// 它的字段和结构【严格匹配】服务器 API 定义的 JSON 格式。 |
||||||
|
class ProblemDto { |
||||||
// /// 企业id |
final String? id; |
||||||
// final String? enterpriseId; |
final String? title; |
||||||
|
final String? location; |
||||||
// /// 问题的详细描述。 |
final String? censorTaskId; |
||||||
// final String description; |
final String? rowId; |
||||||
|
final String? bindData; |
||||||
// /// 问题发生的位置。 |
final List<String?>? imageUrls; |
||||||
// final String location; |
final String? creationTime; |
||||||
|
final String? companyId; |
||||||
// /// 问题的图片元数据列表。 |
|
||||||
// final List<String> imageUrls; |
ProblemDto({ |
||||||
|
this.id, |
||||||
// /// 问题创建的时间。 |
this.title, |
||||||
// final DateTime creationTime; |
this.location, |
||||||
|
this.censorTaskId, |
||||||
// /// 问题创建id |
this.rowId, |
||||||
// final String creatorId; |
this.bindData, |
||||||
|
this.imageUrls, |
||||||
// /// 问题的同步状态,默认为未同步。 |
this.creationTime, |
||||||
// final ProblemSyncStatus syncStatus; |
this.companyId, |
||||||
|
}); |
||||||
// /// 最后修改时间 |
|
||||||
// final DateTime lastModifiedTime; |
|
||||||
|
factory ProblemDto.fromModel(ProblemModel model) { |
||||||
// /// 相关的审查任务ID,可空。 |
return ProblemDto( |
||||||
// final String? censorTaskId; |
id: model.id, |
||||||
|
title: model.description, |
||||||
// /// 绑定的附加数据,可空。 |
location: model.location, |
||||||
// final String? bindData; |
bindData: model.bindData, |
||||||
|
imageUrls: List<String>.from(jsonDecode(model.imageUrls)), |
||||||
// /// 问题是否已被检查,默认为false。 |
creationTime: model.creationTime, |
||||||
// final bool isChecked; |
companyId: model.enterpriseId, |
||||||
|
); |
||||||
// ProblemRemoteDto({ |
} |
||||||
// required this.id, |
ProblemModel toModel() { |
||||||
// required this.description, |
return ProblemModel( |
||||||
// required this.location, |
id: |
||||||
// required this.imageUrls, |
|
||||||
// required this.creationTime, |
); |
||||||
// required this.creatorId, |
} |
||||||
// required this.lastModifiedTime, |
|
||||||
// this.syncStatus = ProblemSyncStatus.pendingCreate, |
// ======================================================================= |
||||||
// this.censorTaskId, |
// 新增方法 |
||||||
// this.bindData, |
// ======================================================================= |
||||||
// this.isChecked = false, |
|
||||||
// this.enterpriseId, |
/// [新增] fromJson 工厂构造函数:从 JSON Map 创建 EnterpriseDto 实例。 |
||||||
// }); |
/// |
||||||
|
/// 当从服务器接收到 JSON 数据时,调用此方法将其转换为 Dart 对象。 |
||||||
// /// copyWith 方法,用于创建对象的副本并修改指定字段 |
factory ProblemDto.fromJson(Map<String, dynamic> json) { |
||||||
// ProblemRemoteDto copyWith({ |
final creationTime = DateTime.parse(json['creationTime'] as String); |
||||||
// String? id, |
final creatorId = json['creatorId'] as String; |
||||||
// String? enterpriseId, |
final lastModTimeStr = json['lastModificationTime'] as String?; |
||||||
// String? description, |
return ProblemDto( |
||||||
// String? location, |
// 必须存在的字段 |
||||||
// List<ImageMetadata>? imageUrls, |
id: json['id'] as String, |
||||||
// DateTime? creationTime, |
creationTime: creationTime, |
||||||
// String? creatorId, |
creatorId: creatorId, |
||||||
// DateTime? lastModifiedTime, |
lastModificationTime: lastModTimeStr != null |
||||||
// ProblemSyncStatus? syncStatus, |
? DateTime.parse(lastModTimeStr) |
||||||
// bool? isDeleted, |
: creationTime, |
||||||
// String? censorTaskId, |
lastModifierId: json['lastModifierId'] as String? ?? creatorId, |
||||||
// String? bindData, |
companyName: json['companyName'] as String, |
||||||
// bool? isChecked, |
companyType: json['companyType'] as String? ?? "生产", |
||||||
// }) { |
|
||||||
// return ProblemRemoteDto( |
// 可选字段 |
||||||
// id: id ?? this.id, |
companyScope: json['companyScope'] as String?, |
||||||
// enterpriseId: enterpriseId ?? this.enterpriseId, |
mainPrincipalName: json['mainPrincipalName'] as String?, |
||||||
// description: description ?? this.description, |
mainPrincipalPhone: json['mainPrincipalPhone'] as String?, |
||||||
// location: location ?? this.location, |
securityPrincipalName: json['securityPrincipalName'] as String?, |
||||||
// imageUrls: imageUrls ?? this.imageUrls, |
securityPrincipalPhone: json['securityPrincipalPhone'] as String?, |
||||||
// creationTime: creationTime ?? this.creationTime, |
companyAddress: json['companyAddress'] as String?, |
||||||
// creatorId: creatorId ?? this.creatorId, |
majorHazard: json['majorHazard'] as String?, |
||||||
// lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime, |
); |
||||||
// syncStatus: syncStatus ?? this.syncStatus, |
} |
||||||
// censorTaskId: censorTaskId ?? this.censorTaskId, |
|
||||||
// bindData: bindData ?? this.bindData, |
/// [新增] toJson 方法:将 EnterpriseDto 实例转换为 JSON Map。 |
||||||
// isChecked: isChecked ?? this.isChecked, |
/// |
||||||
// ); |
/// 当需要将数据发送到服务器时,调用此方法将其转换为 JSON 格式。 |
||||||
// } |
Map<String, dynamic> toJson() { |
||||||
|
final jsonMap = { |
||||||
// /// 转换为JSON字符串 |
'id': id, |
||||||
// Map<String, dynamic> toJson() { |
// 使用 toIso8601String() 是将 DateTime 转换为标准化字符串的最佳实践 |
||||||
// return { |
'creationTime': creationTime.toIso8601String(), |
||||||
// 'id': id, |
'creatorId': creatorId, |
||||||
// 'enterpriseId': enterpriseId, |
'lastModificationTime': lastModificationTime?.toIso8601String(), |
||||||
// 'description': description, |
'lastModifierId': lastModifierId, |
||||||
// 'location': location, |
'companyName': companyName, |
||||||
// 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), |
'companyType': companyType, |
||||||
// 'creationTime': creationTime.toIso8601String(), |
'companyScope': companyScope, |
||||||
// 'creatorId': creatorId, |
'mainPrincipalName': mainPrincipalName, |
||||||
// 'lastModifiedTime': lastModifiedTime.toIso8601String(), |
'mainPrincipalPhone': mainPrincipalPhone, |
||||||
// 'censorTaskId': censorTaskId, |
'securityPrincipalName': securityPrincipalName, |
||||||
// 'bindData': bindData, |
'securityPrincipalPhone': securityPrincipalPhone, |
||||||
// }.withoutNullOrEmptyValues; |
'companyAddress': companyAddress, |
||||||
// } |
'majorHazard': majorHazard, |
||||||
|
}; |
||||||
// /// 从Map创建对象,用于从SQLite读取 |
return jsonMap.withoutNullOrEmptyValues; |
||||||
// factory ProblemRemoteDto.fromJson(Map<String, dynamic> map) { |
} |
||||||
// // 处理imageUrls的转换 |
|
||||||
// List<ImageMetadata> imageUrlsList = []; |
/// [核心] 将 DTO (网络数据) 转换为 Model (本地/业务模型)。 |
||||||
// if (map['imageUrls'] != null) { |
/// |
||||||
// try { |
/// 这个转换是数据流入应用内部的关键步骤。 |
||||||
// final List<dynamic> imageList = json.decode(map['imageUrls']); |
} |
||||||
// imageUrlsList = imageList.map((e) => ImageMetadata.fromMap(e)).toList(); |
|
||||||
// } catch (e) { |
|
||||||
// // 如果解析失败,保持空列表 |
|
||||||
// imageUrlsList = []; |
|
||||||
// } |
|
||||||
// } |
|
||||||
|
|
||||||
// return ProblemRemoteDto( |
|
||||||
// id: map['id'], |
|
||||||
// enterpriseId: map['companyId'], |
|
||||||
// description: map['description'], |
|
||||||
// location: map['location'], |
|
||||||
// imageUrls: imageUrlsList, |
|
||||||
// creationTime: DateTime.fromMillisecondsSinceEpoch( |
|
||||||
// map['creationTime'], |
|
||||||
// isUtc: true, |
|
||||||
// ), |
|
||||||
// creatorId: map['creatorId'], |
|
||||||
// lastModifiedTime: DateTime.fromMillisecondsSinceEpoch( |
|
||||||
// map['lastModifiedTime'], |
|
||||||
// isUtc: true, |
|
||||||
// ), |
|
||||||
// syncStatus: ProblemSyncStatus.values[map['syncStatus']], |
|
||||||
// censorTaskId: map['censorTaskId'], |
|
||||||
// bindData: map['bindData'], |
|
||||||
// isChecked: map['isChecked'] == 1, |
|
||||||
// ); |
|
||||||
// } |
|
||||||
// } |
|
||||||
|
|||||||
@ -0,0 +1,71 @@ |
|||||||
|
import 'dart:async'; |
||||||
|
import 'package:get/get.dart'; // 引入 GetX 用于创建可观察对象 |
||||||
|
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart'; |
||||||
|
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; |
||||||
|
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; |
||||||
|
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; |
||||||
|
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart'; |
||||||
|
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart'; |
||||||
|
|
||||||
|
/// 用例:上传一个或多个问题实体,支持进度回调和取消 |
||||||
|
class UploadProblemsUsecase { |
||||||
|
final ProblemRepository repository; |
||||||
|
UploadProblemsUsecase({required this.repository}); |
||||||
|
|
||||||
|
// 使用 GetX 的 RxBool 来创建一个可观察的取消标志 |
||||||
|
// 这样 Controller 可以改变它,UseCase 可以监听它 |
||||||
|
final RxBool _isCancelled = false.obs; |
||||||
|
|
||||||
|
/// [核心] 提供一个公开的方法来触发取消 |
||||||
|
void cancel() { |
||||||
|
_isCancelled.value = true; |
||||||
|
} |
||||||
|
|
||||||
|
/// 执行上传操作 |
||||||
|
/// [onProgress] 是一个回调函数,用于将进度实时报告给调用者 (Controller) |
||||||
|
Future<UploadResult> call({ |
||||||
|
required List<ProblemListItemEntity> problemsToUpload, |
||||||
|
required void Function(int uploaded, int total) onProgress, |
||||||
|
}) async { |
||||||
|
// 重置取消标志,以便用例可以被复用 |
||||||
|
_isCancelled.value = false; |
||||||
|
int successCount = 0; |
||||||
|
int failureCount = 0; |
||||||
|
final total = problemsToUpload.length; |
||||||
|
|
||||||
|
for (int i = 0; i < total; i++) { |
||||||
|
// 在每次循环开始前,检查是否已被取消 |
||||||
|
if (_isCancelled.value) { |
||||||
|
// 如果已取消,立即中断并返回当前结果 |
||||||
|
return UploadResult( |
||||||
|
successCount: successCount, |
||||||
|
failureCount: failureCount, |
||||||
|
wasCancelled: true, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
final problem = problemsToUpload[i].problemEntity; |
||||||
|
try { |
||||||
|
// 1. 调用通用的同步方法 |
||||||
|
final syncedProblem = await repository.syncProblemToServer(problem); |
||||||
|
// 2. 同步成功后,更新本地状态 |
||||||
|
if (problem.syncStatus == SyncStatus.pendingDelete) { |
||||||
|
// repository.deleteLocalEnterprise(enterprise.id); |
||||||
|
} else { |
||||||
|
await repository.updateProblem(syncedProblem); |
||||||
|
} |
||||||
|
successCount++; |
||||||
|
} catch (e) { |
||||||
|
// 记录失败,但继续上传下一个 |
||||||
|
failureCount++; |
||||||
|
Get.log('上传失败: ${problem.description}, 错误: $e'); |
||||||
|
} |
||||||
|
|
||||||
|
// 3. 通过回调报告进度 |
||||||
|
onProgress(i + 1, total); |
||||||
|
} |
||||||
|
|
||||||
|
// 所有任务完成 |
||||||
|
return UploadResult(successCount: successCount, failureCount: failureCount); |
||||||
|
} |
||||||
|
} |
||||||
@ -1,13 +1,128 @@ |
|||||||
import 'dart:ui'; |
// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart |
||||||
|
|
||||||
class ProblemUploadController { |
import 'package:get/get.dart'; |
||||||
int get selectedCount => 10; |
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart'; |
||||||
|
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; |
||||||
|
import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; |
||||||
|
import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart'; |
||||||
|
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; |
||||||
|
import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart'; |
||||||
|
|
||||||
get allSelected => null; |
class ProblemUploadController extends GetxController { |
||||||
|
final GetAllProblemsUsecase getAllProblemsUsecase; |
||||||
|
final UploadProblemsUsecase uploadProblemsUsecase; |
||||||
|
|
||||||
get unUploadedProblems => null; |
ProblemUploadController({ |
||||||
|
required this.getAllProblemsUsecase, |
||||||
|
required this.uploadProblemsUsecase, |
||||||
|
}); |
||||||
|
|
||||||
VoidCallback? get selectAll => null; |
// --- 实现基类中定义的属性 --- |
||||||
|
final isLoading = false.obs; |
||||||
|
final enterprises = <EnterpriseListItem>[].obs; |
||||||
|
final selectedEnterprises = <EnterpriseListItem>{}.obs; |
||||||
|
// --- [新增] 上传状态管理的属性 --- |
||||||
|
final isUploading = false.obs; |
||||||
|
final uploadProgress = 0.0.obs; // 进度,0.0 到 1.0 |
||||||
|
final uploadedCount = 0.obs; |
||||||
|
final totalToUpload = 0.obs; |
||||||
|
final uploadResult = Rxn<UploadResult>(); // 用于存储上传结果 |
||||||
|
|
||||||
get handleUpload => null; |
bool get allSelected => |
||||||
|
enterprises.isNotEmpty && |
||||||
|
enterprises.length == selectedEnterprises.length; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
fetchPendingUploads(); |
||||||
|
} |
||||||
|
|
||||||
|
void onSelectionChanged(EnterpriseListItem enterprise) { |
||||||
|
if (selectedEnterprises.contains(enterprise)) { |
||||||
|
selectedEnterprises.remove(enterprise); |
||||||
|
} else { |
||||||
|
selectedEnterprises.add(enterprise); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void toggleSelectAll() { |
||||||
|
if (allSelected) { |
||||||
|
selectedEnterprises.clear(); |
||||||
|
} else { |
||||||
|
// 如果是取消全选,则选择所有 |
||||||
|
selectedEnterprises.addAll(enterprises); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 从数据源获取待上传的企业列表 |
||||||
|
Future<void> fetchPendingUploads() async { |
||||||
|
isLoading.value = true; |
||||||
|
try { |
||||||
|
final data = await getEnterpriseListUsecase(isUploaded: false); |
||||||
|
enterprises.assignAll(data); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '加载待上传列表失败: $e'); |
||||||
|
} finally { |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void confirmUpload() { |
||||||
|
if (selectedEnterprises.isEmpty) { |
||||||
|
Get.snackbar('提示', '请至少选择一个企业进行上传'); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 1. 显示上传对话框 |
||||||
|
Get.dialog( |
||||||
|
const UploadProgressDialog(), |
||||||
|
barrierDismissible: false, // 禁止点击外部关闭 |
||||||
|
); |
||||||
|
// 2. 开始执行上传 |
||||||
|
_startUpload(); |
||||||
|
} |
||||||
|
|
||||||
|
/// [新增] 实际执行上传的私有方法 |
||||||
|
Future<void> _startUpload() async { |
||||||
|
// 重置状态 |
||||||
|
isUploading.value = true; |
||||||
|
uploadProgress.value = 0.0; |
||||||
|
uploadResult.value = null; |
||||||
|
totalToUpload.value = selectedEnterprises.length; |
||||||
|
uploadedCount.value = 0; |
||||||
|
|
||||||
|
// 调用 UseCase,并传入 onProgress 回调 |
||||||
|
final result = await uploadEnterprisesUseCase.call( |
||||||
|
enterprisesToUpload: selectedEnterprises.toList(), |
||||||
|
onProgress: (uploaded, total) { |
||||||
|
// 更新进度状态 |
||||||
|
uploadedCount.value = uploaded; |
||||||
|
uploadProgress.value = uploaded / total; |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
// 上传结束 |
||||||
|
isUploading.value = false; |
||||||
|
uploadResult.value = result; |
||||||
|
} |
||||||
|
|
||||||
|
/// [新增] 取消上传 |
||||||
|
void cancelUpload() { |
||||||
|
uploadEnterprisesUseCase.cancel(); |
||||||
|
} |
||||||
|
|
||||||
|
/// [新增] 关闭对话框并处理后续事宜 |
||||||
|
void closeUploadDialog() { |
||||||
|
Get.back(); // 关闭对话框 |
||||||
|
// 如果上传不是被取消的,并且有成功项,则认为操作成功,返回 true 通知列表刷新 |
||||||
|
if (uploadResult.value != null && |
||||||
|
!uploadResult.value!.wasCancelled && |
||||||
|
uploadResult.value!.successCount > 0) { |
||||||
|
Get.back(result: true); // 关闭上传选择页面 |
||||||
|
} else { |
||||||
|
// 否则,只关闭对话框,停留在当前页面 |
||||||
|
// 可能需要刷新一下列表,以显示哪些上传失败了 |
||||||
|
fetchPendingUploads(); |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue