diff --git a/lib/app/core/domain/entities/upload_result.dart b/lib/app/core/domain/entities/upload_result.dart new file mode 100644 index 0000000..b7e6be8 --- /dev/null +++ b/lib/app/core/domain/entities/upload_result.dart @@ -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, + }); +} diff --git a/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart index 6fafca5..ee719f4 100644 --- a/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart +++ b/lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart @@ -1,22 +1,10 @@ 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'; -/// 上传操作的进度和结果模型 -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; 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 6ad5613..4f3b9b8 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart @@ -1,6 +1,7 @@ // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart import 'package:get/get.dart'; +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/get_enterprise_list_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; 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 index aae988d..7d74904 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart +++ b/lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/domain/entities/upload_result.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'; diff --git a/lib/app/features/problem/data/model/problem_dto.dart b/lib/app/features/problem/data/model/problem_dto.dart index 6fa970e..4a9a228 100644 --- a/lib/app/features/problem/data/model/problem_dto.dart +++ b/lib/app/features/problem/data/model/problem_dto.dart @@ -1,144 +1,115 @@ -// import 'dart:convert'; - -// 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/models/problem_sync_status.dart'; - -// /// 问题的数据模型。 -// /// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 -// class ProblemRemoteDto { -// /// 问题的唯一标识符,可空。 -// final String id; - -// /// 企业id -// final String? enterpriseId; - -// /// 问题的详细描述。 -// final String description; - -// /// 问题发生的位置。 -// final String location; - -// /// 问题的图片元数据列表。 -// final List imageUrls; - -// /// 问题创建的时间。 -// final DateTime creationTime; - -// /// 问题创建id -// final String creatorId; - -// /// 问题的同步状态,默认为未同步。 -// final ProblemSyncStatus syncStatus; - -// /// 最后修改时间 -// final DateTime lastModifiedTime; - -// /// 相关的审查任务ID,可空。 -// final String? censorTaskId; - -// /// 绑定的附加数据,可空。 -// final String? bindData; - -// /// 问题是否已被检查,默认为false。 -// final bool isChecked; - -// ProblemRemoteDto({ -// required this.id, -// required this.description, -// required this.location, -// 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, -// }); - -// /// copyWith 方法,用于创建对象的副本并修改指定字段 -// ProblemRemoteDto copyWith({ -// String? id, -// String? enterpriseId, -// String? description, -// String? location, -// List? imageUrls, -// DateTime? creationTime, -// String? creatorId, -// DateTime? lastModifiedTime, -// ProblemSyncStatus? syncStatus, -// bool? isDeleted, -// String? censorTaskId, -// String? bindData, -// bool? isChecked, -// }) { -// return ProblemRemoteDto( -// id: id ?? this.id, -// enterpriseId: enterpriseId ?? this.enterpriseId, -// description: description ?? this.description, -// location: location ?? this.location, -// imageUrls: imageUrls ?? this.imageUrls, -// creationTime: creationTime ?? this.creationTime, -// creatorId: creatorId ?? this.creatorId, -// lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime, -// syncStatus: syncStatus ?? this.syncStatus, -// censorTaskId: censorTaskId ?? this.censorTaskId, -// bindData: bindData ?? this.bindData, -// isChecked: isChecked ?? this.isChecked, -// ); -// } - -// /// 转换为JSON字符串 -// Map toJson() { -// return { -// 'id': id, -// 'enterpriseId': enterpriseId, -// 'description': description, -// 'location': location, -// 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), -// 'creationTime': creationTime.toIso8601String(), -// 'creatorId': creatorId, -// 'lastModifiedTime': lastModifiedTime.toIso8601String(), -// 'censorTaskId': censorTaskId, -// 'bindData': bindData, -// }.withoutNullOrEmptyValues; -// } - -// /// 从Map创建对象,用于从SQLite读取 -// factory ProblemRemoteDto.fromJson(Map map) { -// // 处理imageUrls的转换 -// List imageUrlsList = []; -// if (map['imageUrls'] != null) { -// try { -// final List 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, -// ); -// } -// } +import 'dart:convert'; + +import 'package:problem_check_system/app/core/extensions/map_extensions.dart'; +import 'package:problem_check_system/app/core/domain/entities/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) +/// +/// 这个模型专门用于与远程 API 进行数据交互。 +/// 它的字段和结构【严格匹配】服务器 API 定义的 JSON 格式。 +class ProblemDto { + final String? id; + final String? title; + final String? location; + final String? censorTaskId; + final String? rowId; + final String? bindData; + final List? imageUrls; + final String? creationTime; + final String? companyId; + + ProblemDto({ + this.id, + this.title, + this.location, + this.censorTaskId, + this.rowId, + this.bindData, + this.imageUrls, + this.creationTime, + this.companyId, + }); + + + factory ProblemDto.fromModel(ProblemModel model) { + return ProblemDto( + id: model.id, + title: model.description, + location: model.location, + bindData: model.bindData, + imageUrls: List.from(jsonDecode(model.imageUrls)), + creationTime: model.creationTime, + companyId: model.enterpriseId, + ); + } + ProblemModel toModel() { + return ProblemModel( + id: + + ); + } + + // ======================================================================= + // 新增方法 + // ======================================================================= + + /// [新增] fromJson 工厂构造函数:从 JSON Map 创建 EnterpriseDto 实例。 + /// + /// 当从服务器接收到 JSON 数据时,调用此方法将其转换为 Dart 对象。 + factory ProblemDto.fromJson(Map json) { + final creationTime = DateTime.parse(json['creationTime'] as String); + final creatorId = json['creatorId'] as String; + final lastModTimeStr = json['lastModificationTime'] as String?; + return ProblemDto( + // 必须存在的字段 + id: json['id'] as String, + creationTime: creationTime, + creatorId: creatorId, + lastModificationTime: lastModTimeStr != null + ? DateTime.parse(lastModTimeStr) + : creationTime, + lastModifierId: json['lastModifierId'] as String? ?? creatorId, + companyName: json['companyName'] as String, + companyType: json['companyType'] as String? ?? "生产", + + // 可选字段 + companyScope: json['companyScope'] as String?, + mainPrincipalName: json['mainPrincipalName'] as String?, + mainPrincipalPhone: json['mainPrincipalPhone'] as String?, + securityPrincipalName: json['securityPrincipalName'] as String?, + securityPrincipalPhone: json['securityPrincipalPhone'] as String?, + companyAddress: json['companyAddress'] as String?, + majorHazard: json['majorHazard'] as String?, + ); + } + + /// [新增] toJson 方法:将 EnterpriseDto 实例转换为 JSON Map。 + /// + /// 当需要将数据发送到服务器时,调用此方法将其转换为 JSON 格式。 + Map toJson() { + final jsonMap = { + 'id': id, + // 使用 toIso8601String() 是将 DateTime 转换为标准化字符串的最佳实践 + 'creationTime': creationTime.toIso8601String(), + 'creatorId': creatorId, + 'lastModificationTime': lastModificationTime?.toIso8601String(), + 'lastModifierId': lastModifierId, + 'companyName': companyName, + 'companyType': companyType, + 'companyScope': companyScope, + 'mainPrincipalName': mainPrincipalName, + 'mainPrincipalPhone': mainPrincipalPhone, + 'securityPrincipalName': securityPrincipalName, + 'securityPrincipalPhone': securityPrincipalPhone, + 'companyAddress': companyAddress, + 'majorHazard': majorHazard, + }; + return jsonMap.withoutNullOrEmptyValues; + } + + /// [核心] 将 DTO (网络数据) 转换为 Model (本地/业务模型)。 + /// + /// 这个转换是数据流入应用内部的关键步骤。 +} diff --git a/lib/app/features/problem/data/repositories/problem_repository_impl.dart b/lib/app/features/problem/data/repositories/problem_repository_impl.dart index fdcdb81..c6a7fb0 100644 --- a/lib/app/features/problem/data/repositories/problem_repository_impl.dart +++ b/lib/app/features/problem/data/repositories/problem_repository_impl.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart'; import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/features/problem/domain/entities/problem_bind_status.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart'; @@ -117,4 +116,29 @@ class ProblemRepository implements IProblemRepository { // 3. 操作成功后,返回传入的实体,确认操作完成 return problem; } + // TODO: 上传问题时必须检查企业是否已经上传 + @override + Future syncProblemToServer(ProblemEntity problem) { + // 将 Domain 层的纯净实体转换为 Data 层的 Model + final problemModel = ProblemModel.fromEntity(problem); + EnterpriseDto syncedDto; + // **核心调度逻辑** + // Repository 像一个指挥官,根据情况向不同的士兵(DataSource)下达命令 + switch (enterprise.syncStatus) { + case SyncStatus.pendingCreate: + syncedDto = await remoteDataSource.createEnterprise(enterpriseModel); + break; + case SyncStatus.pendingUpdate: + syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel); + break; + case SyncStatus.pendingDelete: + await remoteDataSource.deleteEnterprise(enterprise.id); + return enterprise; + // 对于其他状态,此方法不执行任何网络操作 + case SyncStatus.synced: + case SyncStatus.untracked: + return enterprise; + } + return syncedDto.toModel().toEntity(); + } } diff --git a/lib/app/features/problem/domain/repositories/problem_repository.dart b/lib/app/features/problem/domain/repositories/problem_repository.dart index c3c4119..e2b7c86 100644 --- a/lib/app/features/problem/domain/repositories/problem_repository.dart +++ b/lib/app/features/problem/domain/repositories/problem_repository.dart @@ -12,4 +12,6 @@ abstract class IProblemRepository { Future addProblem(ProblemEntity problem); Future updateProblem(ProblemEntity problem); Future deleteProblem(String id); + // 同步问题到服务器 + Future syncProblemToServer(ProblemEntity problem); } diff --git a/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart b/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart new file mode 100644 index 0000000..1b35e69 --- /dev/null +++ b/lib/app/features/problem/domain/usecases/upload_problems_usecase.dart @@ -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 call({ + required List 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); + } +} diff --git a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart index 655a1e4..c7493a4 100644 --- a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart @@ -25,6 +25,6 @@ class ProblemUploadBinding extends BaseBindings { @override void register5Controllers() { - Get.lazyPut(() => ProblemUploadController()); + // Get.lazyPut(() => ProblemUploadController()); } } diff --git a/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart index 85e1170..da7caeb 100644 --- a/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart @@ -1,13 +1,128 @@ -import 'dart:ui'; +// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart -class ProblemUploadController { - int get selectedCount => 10; +import 'package:get/get.dart'; +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 = [].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(); // 用于存储上传结果 - 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 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 _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(); + } + } } diff --git a/lib/app/features/problem/presentation/pages/problem_upload_page.dart b/lib/app/features/problem/presentation/pages/problem_upload_page.dart index 5279937..2b53699 100644 --- a/lib/app/features/problem/presentation/pages/problem_upload_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_upload_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get_state_manager/src/simple/get_view.dart'; import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; class ProblemUploadPage extends GetView { @@ -10,57 +12,110 @@ class ProblemUploadPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: UploadAppBar( - selectedCount: controller.selectedCount, - allSelected: controller.allSelected.value, - buttonVisible: controller.unUploadedProblems.isNotEmpty, - onButtonPressed: controller.selectAll, - ), - body: _buildBody(), - bottomNavigationBar: _buildBottomBar(), - ); - } - - Widget _buildBody() { - return Center( - child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)), - ); - // return Obx(() { - // if (controller.unUploadedProblems.isEmpty) { - // return Center( - // child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)), - // ); - // } - // return ProblemListPage( - // problemsToShow: controller.unUploadedProblems, - // viewType: ProblemCardViewType.checkbox, - // ); - // }); - } - - Widget _buildBottomBar() { - return Container( - padding: EdgeInsets.all(16.w), - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Obx( + () => UploadAppBar( + selectedCount: controller.selectedEnterprises.length, + allSelected: controller.allSelected, + buttonVisible: controller.enterprises.isNotEmpty, + onButtonPressed: () => controller.toggleSelectAll(), + ), + ), ), - child: Obx( - () => ElevatedButton( - onPressed: controller.selectedCount > 0 - ? controller.handleUpload - : null, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), + body: _buildProblemList(), + bottomNavigationBar: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: Obx( + () => ElevatedButton( + onPressed: controller.selectedEnterprises.isNotEmpty + ? controller.confirmUpload + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + minimumSize: Size(double.infinity, 48.h), ), - minimumSize: Size(double.infinity, 48.h), + child: Text('确认上传 (${controller.selectedEnterprises.length})'), ), - child: Text('点击上传 (${controller.selectedCount})'), ), ), ); } + + Widget _buildProblemList() { + // 使用 Obx 包裹以监听 controller 中所有 Rx 变量的变化 + return Obx(() { + // 在列表为空且仍在加载时显示加载指示器 + if (controller.isLoading.value && controller.enterprises.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + // 在加载完成但列表为空时显示提示信息 + if (controller.enterprises.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.folder_off_outlined, + size: 60.sp, + color: Colors.grey[400], + ), + SizedBox(height: 16.h), + Text( + '没有找到相关问题', + style: TextStyle(fontSize: 16.sp, color: Colors.grey[600]), + ), + ], + ), + ); + } + + // 使用下拉刷新包裹列表 + return RefreshIndicator( + onRefresh: () async => controller.fetchPendingUploads(), + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemCount: controller.enterprises.length, + itemBuilder: (context, index) { + final item = controller.enterprises[index]; + // 核心: 从 base controller 获取选中状态 + + return Obx(() { + final isSelected = controller.selectedEnterprises.contains(item); + + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: UnifiedEnterpriseCard( + enterpriseListItem: item, + isSelected: isSelected, + // 根据外部传入的 itemMode 决定卡片内部的 mode + // --- [核心] 将卡片的所有交互事件转发给 controller 的抽象方法 --- + onTap: () => controller.onSelectionChanged(item), + actions: Padding( + // 给 Checkbox 自身的点击区域和视觉留出空间 + padding: EdgeInsets.only(right: 8.w), + child: Checkbox( + value: isSelected, + activeColor: Theme.of(context).primaryColor, + onChanged: (value) { + controller.onSelectionChanged(item); + }, + ), + ), + ), + ); + }); + }, + ), + ); + }); + } }