diff --git a/lib/app/core/extensions/map_extensions.dart b/lib/app/core/extensions/map_extensions.dart new file mode 100644 index 0000000..48b6982 --- /dev/null +++ b/lib/app/core/extensions/map_extensions.dart @@ -0,0 +1,10 @@ +extension MapExtension on Map { + /// 返回一个新的 Map,其中已移除了所有值为 null 或空字符串的键值对。 + Map get withoutNullOrEmptyValues { + final newMap = Map.from(this); + newMap.removeWhere( + (key, value) => value == null || (value is String && value.isEmpty), + ); + return newMap; + } +} diff --git a/lib/app/core/models/sync_status.dart b/lib/app/core/models/sync_status.dart index c9517cc..8e3f56c 100644 --- a/lib/app/core/models/sync_status.dart +++ b/lib/app/core/models/sync_status.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + enum SyncStatus { /// 未跟踪 - 需要被移除的记录(如本地删除但从未同步过) untracked, @@ -15,6 +17,46 @@ enum SyncStatus { pendingDelete, } +/// 为 SyncStatus 枚举添加扩展功能 +extension SyncStatusExtension on SyncStatus { + /// 返回一个用户可读的描述字符串 + String get displayName { + switch (this) { + case SyncStatus.synced: + return '已同步'; + case SyncStatus.pendingCreate: + return '未同步-待新建'; + case SyncStatus.pendingUpdate: + return '未同步-待更新'; + case SyncStatus.pendingDelete: + return '未同步-待删除'; + case SyncStatus.untracked: + return '未跟踪'; + // 添加一个 default 是一个好习惯,以防未来添加新的枚举值而忘记更新这里 + default: + return '未知状态'; + } + } + + /// [可选] 返回一个与状态对应的颜色,用于UI显示 + Color get displayColor { + switch (this) { + case SyncStatus.synced: + return Colors.green; + case SyncStatus.pendingCreate: + return Colors.blue; + case SyncStatus.pendingUpdate: + return Colors.orange; + case SyncStatus.pendingDelete: + return Colors.red; + case SyncStatus.untracked: + return Colors.grey; + default: + return Colors.black; + } + } +} + /// 一个抽象接口,定义了所有“可同步”实体的共同特征。 /// 任何需要离线同步功能的实体都应该实现这个接口。 abstract class SyncableEntity { diff --git a/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart b/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart index c56b262..79f3919 100644 --- a/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart +++ b/lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart @@ -16,9 +16,7 @@ abstract class EnterpriseLocalDataSource { bool? isUploaded, }); - Future> getUnsyncedEnterprises(); - - /// 查询企业列表 + Future updateSyncStatus(String enterpriseId, SyncStatus newStatus); } class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { @@ -146,42 +144,21 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { } @override - Future> getUnsyncedEnterprises() async { + Future updateSyncStatus( + String enterpriseId, + SyncStatus newStatus, + ) async { final db = await _databaseService.database; - - // 1. [修正] 定义需要查询的状态列表,使用枚举本身,而不是字符串。 - // 根据您的 SyncStatus 定义,这些状态都代表需要与服务器同步。 - const List unsyncedStatuses = [ - SyncStatus.pendingCreate, - SyncStatus.pendingUpdate, - SyncStatus.pendingDelete, - ]; - - // 2. [修正] 将枚举列表转换为整数索引列表,以匹配数据库中的存储方式。 - // 这是最关键的修正点。 - final List statusIndexes = unsyncedStatuses - .map((status) => status.index) - .toList(); - - // 3. [完善] 动态生成 SQL 查询中的 '?' 占位符。 - // 这样即使未来 unsyncedStatuses 列表长度变化,代码也无需修改。 - final String placeholders = List.filled( - statusIndexes.length, - '?', - ).join(','); - - // 4. 执行查询 - final List> maps = await db.query( - _tableName, - where: 'syncStatus IN ($placeholders)', // e.g., 'syncStatus IN (?,?,?)' - whereArgs: statusIndexes, // e.g., [2, 3, 4] + await db.update( + _tableName, // 你的表名 + { + 'syncStatus': newStatus.index, // 将枚举转换为整数索引存储 + 'lastModifiedTime': DateTime.now() + .toUtc() + .millisecondsSinceEpoch, // 更新修改时间 + }, + where: 'id = ?', + whereArgs: [enterpriseId], ); - - if (maps.isEmpty) { - return []; - } - - // 5. [修正] 使用正确的 fromMap 工厂构造函数来转换数据。 - return maps.map((map) => EnterpriseModel.fromMap(map)).toList(); } } diff --git a/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart b/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart index 5930c86..afe01a5 100644 --- a/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart +++ b/lib/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart @@ -1,5 +1,56 @@ -class EnterpriseRemoteDataSource {} +import 'package:problem_check_system/app/core/services/http_provider.dart'; +import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_dto.dart'; +import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; + +/// 远程数据源的抽象接口 +/// 定义了所有与企业相关的网络 API 调用 +abstract class EnterpriseRemoteDataSource { + /// 调用 API 创建一个新的企业 + /// + /// 成功时无返回,失败时应抛出异常 (如 DioException) + Future createEnterprise(EnterpriseModel enterprise); + + /// 调用 API 更新一个已有的企业 + Future updateEnterprise(EnterpriseModel enterprise); + + /// 调用 API 删除一个企业 + Future deleteEnterprise(String enterpriseId); +} class EnterpriseRemoteDataSourceImpl implements EnterpriseRemoteDataSource { - // 在这里实现与远程服务器交互的具体方法 + final HttpProvider http; + const EnterpriseRemoteDataSourceImpl({required this.http}); + + static const String enterprisesEndpoint = '/api/Companies'; + @override + Future createEnterprise(EnterpriseModel enterprise) async { + try { + final enterpriseDto = EnterpriseDto.fromModel(enterprise); + final data = enterpriseDto.toJson(); + await http.post(enterprisesEndpoint, data: data); + } catch (e) { + rethrow; + } + } + + @override + Future deleteEnterprise(String enterpriseId) async { + try { + await http.delete('$enterprisesEndpoint/$enterpriseId'); + } catch (e) { + rethrow; + } + } + + @override + Future updateEnterprise(EnterpriseModel enterprise) async { + try { + final enterpriseDto = EnterpriseDto.fromModel(enterprise); + final data = enterpriseDto.toJson(); + // 通常更新操作需要一个 ID + await http.patch('$enterprisesEndpoint/${enterprise.id}', data: data); + } catch (e) { + rethrow; + } + } } diff --git a/lib/app/features/enterprise/data/model/enterprise_dto.dart b/lib/app/features/enterprise/data/model/enterprise_dto.dart new file mode 100644 index 0000000..9aebea0 --- /dev/null +++ b/lib/app/features/enterprise/data/model/enterprise_dto.dart @@ -0,0 +1,61 @@ +import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; +import 'package:problem_check_system/app/features/enterprise/data/model/enterprise_model.dart'; + +/// EnterpriseDto (Data Transfer Object) +/// +/// 这个模型专门用于与远程 API 进行数据交互。 +/// 它的字段和结构【严格匹配】服务器 API 定义的 JSON 格式。 +class EnterpriseDto { + final String companyName; + final String companyType; + final String? companyScope; + final String? mainPrincipalName; + final String? mainPrincipalPhone; + final String? securityPrincipalName; // API 中没有,但最好加上以保持对称 + final String? securityPrincipalPhone; // API 中没有,但最好加上以保持对称 + final String? companyAddress; + final String? detail; // 对应 majorHazardsDescription + + const EnterpriseDto({ + required this.companyName, + required this.companyType, + this.companyScope, + this.mainPrincipalName, + this.mainPrincipalPhone, + this.securityPrincipalName, + this.securityPrincipalPhone, + this.companyAddress, + this.detail, + }); + + /// [核心] 工厂构造函数:从您的内部模型 `EnterpriseModel` 创建 DTO。 + /// + /// 这个转换逻辑是这个类的核心职责。 + factory EnterpriseDto.fromModel(EnterpriseModel model) { + return EnterpriseDto( + companyName: model.name, + companyType: model.type, + companyScope: model.scale, + mainPrincipalName: model.contactPerson, + mainPrincipalPhone: model.contactPhone, + companyAddress: model.address, + detail: model.majorHazardsDescription, + ); + } + + /// 将 DTO 对象转换为可以发送给服务器的 JSON (Map) 格式。 + Map toJson() { + final originalMap = { + 'companyName': companyName, + 'companyType': companyType, + 'companyScope': companyScope, + 'mainPrincipalName': mainPrincipalName, + 'mainPrincipalPhone': mainPrincipalPhone, + 'securityPrincipalName': securityPrincipalName, + 'securityPrincipalPhone': securityPrincipalPhone, + 'companyAddress': companyAddress, + 'detail': detail, + }; + return originalMap.withoutNullOrEmptyValues; + } +} diff --git a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart index 74eb945..aac44db 100644 --- a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart +++ b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart @@ -75,4 +75,37 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { // 调用数据源执行操作 await localDataSource.updateEnterprise(enterpriseModel); } + + @override + Future syncEnterpriseToServer(Enterprise enterprise) async { + // 将 Domain 层的纯净实体转换为 Data 层的 Model + final enterpriseModel = EnterpriseModel.fromEntity(enterprise); + + // **核心调度逻辑** + // Repository 像一个指挥官,根据情况向不同的士兵(DataSource)下达命令 + switch (enterprise.syncStatus) { + case SyncStatus.pendingCreate: + await remoteDataSource.createEnterprise(enterpriseModel); + break; + case SyncStatus.pendingUpdate: + await remoteDataSource.updateEnterprise(enterpriseModel); + break; + case SyncStatus.pendingDelete: + await remoteDataSource.deleteEnterprise(enterprise.id); + break; + // 对于其他状态,此方法不执行任何网络操作 + case SyncStatus.synced: + case SyncStatus.untracked: + break; + } + } + + @override + Future updateEnterpriseSyncStatus( + String enterpriseId, + SyncStatus newStatus, + ) async { + // 将指令直接转发给本地数据源 + await localDataSource.updateSyncStatus(enterpriseId, newStatus); + } } diff --git a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart index 0cc9c08..4f29afb 100644 --- a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart +++ b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart @@ -1,3 +1,4 @@ +import 'package:problem_check_system/app/core/models/sync_status.dart'; import 'package:problem_check_system/app/core/repositories/syncable_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; @@ -18,4 +19,8 @@ abstract class EnterpriseRepository implements SyncableRepository { DateTime? endDate, bool? isUploaded, }); + + Future syncEnterpriseToServer(Enterprise enterprise) async {} + + Future updateEnterpriseSyncStatus(String id, SyncStatus synced) async {} } diff --git a/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart new file mode 100644 index 0000000..dabe59c --- /dev/null +++ b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart @@ -0,0 +1,84 @@ +import 'dart:async'; +import 'package:get/get.dart'; // 引入 GetX 用于创建可观察对象 +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/models/sync_status.dart'; + +/// 上传操作的进度和结果模型 +class UploadResult { + final int successCount; + final int failureCount; + final bool wasCancelled; + + UploadResult({ + this.successCount = 0, + this.failureCount = 0, + this.wasCancelled = false, + }); +} + +/// 用例:上传一个或多个企业实体,支持进度回调和取消 +class UploadEnterprisesUseCase { + final EnterpriseRepository repository; + UploadEnterprisesUseCase({required this.repository}); + + // 使用 GetX 的 RxBool 来创建一个可观察的取消标志 + // 这样 Controller 可以改变它,UseCase 可以监听它 + final RxBool _isCancelled = false.obs; + + /// [核心] 提供一个公开的方法来触发取消 + void cancel() { + _isCancelled.value = true; + } + + /// 执行上传操作 + /// [onProgress] 是一个回调函数,用于将进度实时报告给调用者 (Controller) + Future call({ + required List enterprisesToUpload, + required void Function(int uploaded, int total) onProgress, + }) async { + // 重置取消标志,以便用例可以被复用 + _isCancelled.value = false; + int successCount = 0; + int failureCount = 0; + final total = enterprisesToUpload.length; + + for (int i = 0; i < total; i++) { + // 在每次循环开始前,检查是否已被取消 + if (_isCancelled.value) { + // 如果已取消,立即中断并返回当前结果 + return UploadResult( + successCount: successCount, + failureCount: failureCount, + wasCancelled: true, + ); + } + + final enterprise = enterprisesToUpload[i].enterprise; + try { + // 1. 调用通用的同步方法 + await repository.syncEnterpriseToServer(enterprise); + // 2. 同步成功后,更新本地状态 + if (enterprise.syncStatus == SyncStatus.pendingDelete) { + // repository.deleteLocalEnterprise(enterprise.id); // 从本地删除 + } else { + await repository.updateEnterpriseSyncStatus( + enterprise.id, + SyncStatus.synced, + ); + } + successCount++; + } catch (e) { + // 记录失败,但继续上传下一个 + failureCount++; + Get.log('上传失败: ${enterprise.name}, 错误: $e'); + } + + // 3. 通过回调报告进度 + onProgress(i + 1, total); + } + + // 所有任务完成 + return UploadResult(successCount: successCount, failureCount: failureCount); + } +} diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart index 2d532cf..7140243 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart @@ -1,7 +1,6 @@ // lib/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart'; @@ -9,33 +8,27 @@ import 'package:problem_check_system/app/features/enterprise/domain/repositories import 'package:problem_check_system/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart'; -import 'package:uuid/uuid.dart'; // 确保导入您的模型 class EnterpriseFormBinding extends Bindings { @override void dependencies() { Get.lazyPut( - () => EnterpriseLocalDataSourceImpl( - databaseService: Get.find(), - ), + () => EnterpriseLocalDataSourceImpl(databaseService: Get.find()), ); Get.lazyPut( - () => EnterpriseRemoteDataSourceImpl(), + () => EnterpriseRemoteDataSourceImpl(http: Get.find()), ); Get.lazyPut( (() => EnterpriseRepositoryImpl( - localDataSource: Get.find(), - remoteDataSource: Get.find(), + localDataSource: Get.find(), + remoteDataSource: Get.find(), )), ); Get.lazyPut( - () => AddEnterpriseUsecase( - repository: Get.find(), - uuid: Get.find(), - ), + () => AddEnterpriseUsecase(repository: Get.find(), uuid: Get.find()), ); Get.lazyPut( - () => EditEnterpriseUsecase(repository: Get.find()), + () => EditEnterpriseUsecase(repository: Get.find()), ); // 2. 将获取到的参数(可能为 null)传递给 Controller 的构造函数。 diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart index 15144f2..8900e2d 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/core/services/database_service.dart'; +import 'package:problem_check_system/app/core/services/http_provider.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart'; @@ -15,7 +16,9 @@ class EnterpriseListBinding extends Bindings { databaseService: Get.find(), ), ); - Get.put(EnterpriseRemoteDataSourceImpl()); + Get.put( + EnterpriseRemoteDataSourceImpl(http: Get.find()), + ); Get.put( EnterpriseRepositoryImpl( localDataSource: Get.find(), diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart index e623b49..6092c9c 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart @@ -1,10 +1,12 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/core/services/database_service.dart'; +import 'package:problem_check_system/app/core/services/http_provider.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; class EnterpriseUploadBinding extends Bindings { @@ -15,21 +17,28 @@ class EnterpriseUploadBinding extends Bindings { databaseService: Get.find(), ), ); - Get.put(EnterpriseRemoteDataSourceImpl()); - Get.put( + Get.put( + EnterpriseRemoteDataSourceImpl(http: Get.find()), + ); + + final enterpriseRepo = Get.put( EnterpriseRepositoryImpl( localDataSource: Get.find(), remoteDataSource: Get.find(), ), ); - Get.lazyPut( - () => GetEnterpriseListUsecase( - repository: Get.find(), - ), + + final getUsecase = Get.put( + GetEnterpriseListUsecase(repository: enterpriseRepo), + ); + final uploadUsecase = Get.put( + UploadEnterprisesUseCase(repository: enterpriseRepo), ); + Get.lazyPut( () => EnterpriseUploadController( - getEnterpriseListUsecase: Get.find(), + getEnterpriseListUsecase: getUsecase, + uploadEnterprisesUseCase: uploadUsecase, ), ); } diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart index cbeca97..75cbeb6 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart @@ -88,7 +88,14 @@ class EnterpriseListController extends GetxController { ); if (result == true) { search(); - Get.snackbar('成功', '企业信息已更新'); + Get.snackbar( + '成功', + '企业信息已更新', + backgroundColor: Colors.green[600], + colorText: Colors.white, + icon: const Icon(Icons.check_circle, color: Colors.white), + duration: const Duration(seconds: 3), + ); } } @@ -100,7 +107,14 @@ class EnterpriseListController extends GetxController { ); if (result == true) { search(); - Get.snackbar('成功', '企业信息已保存'); + Get.snackbar( + '成功', + '企业信息已创建', + backgroundColor: Colors.green[600], + colorText: Colors.white, + icon: const Icon(Icons.check_circle, color: Colors.white), + duration: const Duration(seconds: 3), + ); } } diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart index cd1a8a5..6ad5613 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart @@ -3,15 +3,28 @@ import 'package:get/get.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/get_enterprise_list_usecase.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'; class EnterpriseUploadController extends GetxController { final GetEnterpriseListUsecase getEnterpriseListUsecase; - EnterpriseUploadController({required this.getEnterpriseListUsecase}); + final UploadEnterprisesUseCase uploadEnterprisesUseCase; + + EnterpriseUploadController({ + required this.getEnterpriseListUsecase, + required this.uploadEnterprisesUseCase, + }); // --- 实现基类中定义的属性 --- final isLoading = false.obs; final enterprises = [].obs; final selectedEnterprises = {}.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(); // 用于存储上传结果 bool get allSelected => enterprises.isNotEmpty && @@ -44,7 +57,7 @@ class EnterpriseUploadController extends GetxController { Future fetchPendingUploads() async { isLoading.value = true; try { - final data = await getEnterpriseListUsecase(); + final data = await getEnterpriseListUsecase(isUploaded: false); enterprises.assignAll(data); } catch (e) { Get.snackbar('错误', '加载待上传列表失败: $e'); @@ -53,16 +66,61 @@ class EnterpriseUploadController extends GetxController { } } - /// 确认上传,并返回结果 void confirmUpload() { if (selectedEnterprises.isEmpty) { Get.snackbar('提示', '请至少选择一个企业进行上传'); return; } - // 在这里执行实际的上传业务逻辑... - Get.log('正在上传 ${selectedEnterprises.length} 个企业...'); + // 1. 显示上传对话框 + Get.dialog( + const UploadProgressDialog(), + barrierDismissible: false, // 禁止点击外部关闭 + ); + // 2. 开始执行上传 + _startUpload(); + } + + /// [新增] 实际执行上传的私有方法 + Future _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(); + } - // 操作成功后,返回 true 通知上一个页面刷新 - Get.back(result: true); + /// [新增] 关闭对话框并处理后续事宜 + void closeUploadDialog() { + Get.back(); // 关闭对话框 + // 如果上传不是被取消的,并且有成功项,则认为操作成功,返回 true 通知列表刷新 + if (uploadResult.value != null && + !uploadResult.value!.wasCancelled && + uploadResult.value!.successCount > 0) { + Get.back(result: true); // 关闭上传选择页面 + } else { + // 否则,只关闭对话框,停留在当前页面 + // 可能需要刷新一下列表,以显示哪些上传失败了 + fetchPendingUploads(); + } } } diff --git a/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart b/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart index 76e4ab0..2f73552 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart @@ -43,7 +43,7 @@ class EnterpriseUploadPage extends GetView { ), minimumSize: Size(double.infinity, 48.h), ), - child: Text('点击上传 (${controller.selectedEnterprises.length})'), + child: Text('确认上传 (${controller.selectedEnterprises.length})'), ), ), ), diff --git a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart index f5e9328..3e6c71b 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart +++ b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart @@ -120,13 +120,17 @@ class UnifiedEnterpriseCard extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 3.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.r), - border: Border.all(color: Colors.red.shade400, width: 1.w), + border: Border.all( + color: enterpriseListItem.enterprise.syncStatus.displayColor, + width: 1.w, + ), ), child: Text( - enterpriseListItem.enterprise.syncStatus == SyncStatus.synced - ? '信息已上传' - : '信息未上传', - style: TextStyle(fontSize: 7.sp, color: Colors.red.shade400), + enterpriseListItem.enterprise.syncStatus.displayName, + style: TextStyle( + fontSize: 7.sp, + color: enterpriseListItem.enterprise.syncStatus.displayColor, + ), ), ), ], diff --git a/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart b/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart new file mode 100644 index 0000000..aae988d --- /dev/null +++ b/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; + +class UploadProgressDialog extends GetView { + const UploadProgressDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + // 使用 Obx 包裹内容,使其能根据 controller 的状态变化而变化 + content: Obx(() { + // 根据是否正在上传来决定显示进度还是结果 + if (controller.isUploading.value) { + return _buildProgressIndicator(); + } else { + return _buildResultInfo(controller.uploadResult.value); + } + }), + ); + } + + // 构建进度显示的 Widget + Widget _buildProgressIndicator() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '正在上传', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + LinearProgressIndicator(value: controller.uploadProgress.value), + const SizedBox(height: 12), + Text( + '${controller.uploadedCount.value} / ${controller.totalToUpload.value}', + ), + const SizedBox(height: 20), + TextButton( + onPressed: () => controller.cancelUpload(), + child: const Text('取消', style: TextStyle(color: Colors.red)), + ), + ], + ); + } + + // 构建结果显示的 Widget + Widget _buildResultInfo(UploadResult? result) { + if (result == null) { + // 理论上不应该出现,但作为保护 + return const Column( + mainAxisSize: MainAxisSize.min, + children: [Text('未知错误')], + ); + } + + IconData icon; + Color color; + String title; + + if (result.wasCancelled) { + icon = Icons.cancel; + color = Colors.orange; + title = '上传已取消'; + } else if (result.failureCount > 0) { + icon = Icons.error; + color = Colors.red; + title = '上传部分失败'; + } else { + icon = Icons.check_circle; + color = Colors.green; + title = '上传成功'; + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 50), + const SizedBox(height: 16), + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + Text('成功: ${result.successCount}, 失败: ${result.failureCount}'), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () => controller.closeUploadDialog(), + child: const Text('完成'), + ), + ], + ); + } +} diff --git a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart index f38a22e..5c97bf8 100644 --- a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart +++ b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart @@ -99,6 +99,7 @@ class NavigationController extends GetxController { case 1: // 企业列表页面 Get.log("当前在企业页面,准备跳转到企业数据上传页..."); // 使用命名路由进行跳转,这是最佳实践 + // todo 这里应该调用 enterpriseListController 的导航方法,方便返回刷新 Get.toNamed(AppRoutes.enterpriseUpload); break; case 2: // 问题列表页面