17 changed files with 516 additions and 76 deletions
@ -0,0 +1,10 @@
|
||||
extension MapExtension on Map<String, dynamic> { |
||||
/// 返回一个新的 Map,其中已移除了所有值为 null 或空字符串的键值对。 |
||||
Map<String, dynamic> get withoutNullOrEmptyValues { |
||||
final newMap = Map<String, dynamic>.from(this); |
||||
newMap.removeWhere( |
||||
(key, value) => value == null || (value is String && value.isEmpty), |
||||
); |
||||
return newMap; |
||||
} |
||||
} |
||||
@ -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<void> createEnterprise(EnterpriseModel enterprise); |
||||
|
||||
/// 调用 API 更新一个已有的企业 |
||||
Future<void> updateEnterprise(EnterpriseModel enterprise); |
||||
|
||||
/// 调用 API 删除一个企业 |
||||
Future<void> deleteEnterprise(String enterpriseId); |
||||
} |
||||
|
||||
class EnterpriseRemoteDataSourceImpl implements EnterpriseRemoteDataSource { |
||||
// 在这里实现与远程服务器交互的具体方法 |
||||
final HttpProvider http; |
||||
const EnterpriseRemoteDataSourceImpl({required this.http}); |
||||
|
||||
static const String enterprisesEndpoint = '/api/Companies'; |
||||
@override |
||||
Future<void> 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<void> deleteEnterprise(String enterpriseId) async { |
||||
try { |
||||
await http.delete('$enterprisesEndpoint/$enterpriseId'); |
||||
} catch (e) { |
||||
rethrow; |
||||
} |
||||
} |
||||
|
||||
@override |
||||
Future<void> 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; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -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<String, dynamic> toJson() { |
||||
final originalMap = { |
||||
'companyName': companyName, |
||||
'companyType': companyType, |
||||
'companyScope': companyScope, |
||||
'mainPrincipalName': mainPrincipalName, |
||||
'mainPrincipalPhone': mainPrincipalPhone, |
||||
'securityPrincipalName': securityPrincipalName, |
||||
'securityPrincipalPhone': securityPrincipalPhone, |
||||
'companyAddress': companyAddress, |
||||
'detail': detail, |
||||
}; |
||||
return originalMap.withoutNullOrEmptyValues; |
||||
} |
||||
} |
||||
@ -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<UploadResult> call({ |
||||
required List<EnterpriseListItem> 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); |
||||
} |
||||
} |
||||
@ -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<EnterpriseUploadController> { |
||||
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('完成'), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue