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 { |
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