Browse Source

feat : 上传问题

dev
徐振升 6 hours ago
parent
commit
ea8958e186
  1. 34
      lib/app/core/controllers/i_upload_controller.dart
  2. 8
      lib/app/core/pages/widgets/upload_progress_dialog.dart
  3. 26
      lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart
  4. 29
      lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart
  5. 4
      lib/app/features/enterprise/domain/repositories/enterprise_repository.dart
  6. 13
      lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart
  7. 15
      lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
  8. 16
      lib/app/features/problem/data/datasources/problem_local_data_source.dart
  9. 118
      lib/app/features/problem/data/datasources/problem_remote_data_source.dart
  10. 145
      lib/app/features/problem/data/model/problem_dto.dart
  11. 3
      lib/app/features/problem/data/model/problem_filter_dto.dart
  12. 70
      lib/app/features/problem/data/repositories/problem_repository_impl.dart
  13. 6
      lib/app/features/problem/domain/entities/problem_filter_params.dart
  14. 2
      lib/app/features/problem/domain/repositories/problem_repository.dart
  15. 2
      lib/app/features/problem/domain/usecases/add_problem_usecase.dart
  16. 2
      lib/app/features/problem/domain/usecases/delete_problem.dart
  17. 2
      lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart
  18. 2
      lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart
  19. 10
      lib/app/features/problem/domain/usecases/update_problem_usecase.dart
  20. 131
      lib/app/features/problem/domain/usecases/upload_problems_usecase.dart
  21. 2
      lib/app/features/problem/presentation/bindings/problem_form_binding.dart
  22. 51
      lib/app/features/problem/presentation/bindings/problem_list_binding.dart
  23. 55
      lib/app/features/problem/presentation/bindings/problem_upload_binding.dart
  24. 43
      lib/app/features/problem/presentation/controllers/problem_form_controller.dart
  25. 58
      lib/app/features/problem/presentation/controllers/problem_upload_controller.dart
  26. 24
      lib/app/features/problem/presentation/pages/problem_upload_page.dart

34
lib/app/core/controllers/i_upload_controller.dart

@ -0,0 +1,34 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.dart';
///
///
///
/// 使 UI ( UploadProgressDialog)
///
abstract class IUploadController {
// --- (State Properties) ---
///
RxBool get isUploading;
/// (0.0 to 1.0)
RxDouble get uploadProgress;
///
RxInt get uploadedCount;
///
RxInt get totalToUpload;
///
Rx<UploadResult?> get uploadResult;
// --- (Action Methods) ---
///
void cancelUpload();
/// ()
void closeUploadDialog();
}

8
lib/app/features/enterprise/presentation/pages/widgets/upload_progress_dialog.dart → lib/app/core/pages/widgets/upload_progress_dialog.dart

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.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';
class UploadProgressDialog extends GetView<EnterpriseUploadController> { class UploadProgressDialog extends StatelessWidget {
const UploadProgressDialog({super.key}); final IUploadController controller;
const UploadProgressDialog({super.key, required this.controller});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

26
lib/app/features/enterprise/data/datasources/enterprise_local_data_source.dart

@ -23,6 +23,10 @@ abstract class EnterpriseLocalDataSource {
// [] // []
Future<void> upsertEnterprise(EnterpriseModel enterprise); Future<void> upsertEnterprise(EnterpriseModel enterprise);
Future<void> cacheEnterprises(List<EnterpriseModel> enterprises); Future<void> cacheEnterprises(List<EnterpriseModel> enterprises);
Future getEnterpriseById(String enterpriseId);
Future<void> deleteEnterprise(String id) async {}
} }
class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource { class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource {
@ -210,4 +214,26 @@ class EnterpriseLocalDataSourceImpl implements EnterpriseLocalDataSource {
conflictAlgorithm: ConflictAlgorithm.replace, // replace conflictAlgorithm: ConflictAlgorithm.replace, // replace
); );
} }
@override
Future getEnterpriseById(String enterpriseId) async {
final db = await _databaseService.database;
final List<Map<String, dynamic>> maps = await db.query(
_tableName,
where: 'id = ?',
whereArgs: [enterpriseId],
limit: 1, //
);
if (maps.isNotEmpty) {
return maps.first;
}
return null;
}
@override
Future<void> deleteEnterprise(String id) {
//TODO
throw UnimplementedError();
}
} }

29
lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart

@ -98,14 +98,23 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository {
syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel); syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel);
break; break;
case SyncStatus.pendingDelete: case SyncStatus.pendingDelete:
//
await remoteDataSource.deleteEnterprise(enterprise.id); await remoteDataSource.deleteEnterprise(enterprise.id);
return enterprise; //
// await localDataSource.deleteEnterprise(enterprise.id);
//
return enterprise.copyWith(syncStatus: SyncStatus.synced);
case SyncStatus.synced: case SyncStatus.synced:
case SyncStatus.untracked: case SyncStatus.untracked:
return enterprise; return enterprise;
} }
return syncedDto.toModel().toEntity(); // [] DTO Model
// ID'synced'
final syncedModel = syncedDto.toModel(); //
await localDataSource.updateEnterprise(syncedModel);
// Model Entity
return syncedModel.toEntity();
} }
@override @override
@ -192,4 +201,18 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository {
final enterprises = models.map((model) => model.toEntity()).toList(); final enterprises = models.map((model) => model.toEntity()).toList();
return enterprises; return enterprises;
} }
@override
Future<Enterprise?> getEnterpriseById(String enterpriseId) async {
// 1. ID (Map)
final problemMap = await localDataSource.getEnterpriseById(enterpriseId);
// 2. null
if (problemMap == null) {
return null;
}
// 3. DTO
return EnterpriseModel.fromMap(problemMap).toEntity();
}
} }

4
lib/app/features/enterprise/domain/repositories/enterprise_repository.dart

@ -23,7 +23,7 @@ abstract class EnterpriseRepository implements SyncableRepository<Enterprise> {
Future<Enterprise> syncEnterpriseToServer(Enterprise enterprise); Future<Enterprise> syncEnterpriseToServer(Enterprise enterprise);
Future<void> updateEnterpriseSyncStatus(String id, SyncStatus synced) async {} Future<void> updateEnterpriseSyncStatus(String id, SyncStatus synced);
// [] // []
Future<SyncResult> syncWithServer(); Future<SyncResult> syncWithServer();
@ -31,4 +31,6 @@ abstract class EnterpriseRepository implements SyncableRepository<Enterprise> {
Future<void> resolveConflictAndUpdate(Enterprise chosenEnterprise); Future<void> resolveConflictAndUpdate(Enterprise chosenEnterprise);
// //
Future<List<Enterprise>> getAllEnterprises(); Future<List<Enterprise>> getAllEnterprises();
// ID获取企业信息
Future<Enterprise?> getEnterpriseById(String enterpriseId);
} }

13
lib/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart

@ -20,7 +20,7 @@ class UploadEnterprisesUseCase {
} }
/// ///
/// todo -- (Request-Acknowledge-Update) /// TODO Repository
/// [onProgress] (Controller) /// [onProgress] (Controller)
Future<UploadResult> call({ Future<UploadResult> call({
required List<EnterpriseListItem> enterprisesToUpload, required List<EnterpriseListItem> enterprisesToUpload,
@ -45,16 +45,7 @@ class UploadEnterprisesUseCase {
final enterprise = enterprisesToUpload[i].enterprise; final enterprise = enterprisesToUpload[i].enterprise;
try { try {
// 1. await repository.syncEnterpriseToServer(enterprise);
final syncedEnterprise = await repository.syncEnterpriseToServer(
enterprise,
);
// 2.
if (enterprise.syncStatus == SyncStatus.pendingDelete) {
// repository.deleteLocalEnterprise(enterprise.id);
} else {
await repository.updateEnterprise(syncedEnterprise);
}
successCount++; successCount++;
} catch (e) { } catch (e) {
// //

15
lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart

@ -1,13 +1,15 @@
// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.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/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/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/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/core/pages/widgets/upload_progress_dialog.dart';
class EnterpriseUploadController extends GetxController { class EnterpriseUploadController extends GetxController
implements IUploadController {
final GetEnterpriseListUsecase getEnterpriseListUsecase; final GetEnterpriseListUsecase getEnterpriseListUsecase;
final UploadEnterprisesUseCase uploadEnterprisesUseCase; final UploadEnterprisesUseCase uploadEnterprisesUseCase;
@ -21,10 +23,15 @@ class EnterpriseUploadController extends GetxController {
final enterprises = <EnterpriseListItem>[].obs; final enterprises = <EnterpriseListItem>[].obs;
final selectedEnterprises = <EnterpriseListItem>{}.obs; final selectedEnterprises = <EnterpriseListItem>{}.obs;
// --- [] --- // --- [] ---
@override
final isUploading = false.obs; final isUploading = false.obs;
@override
final uploadProgress = 0.0.obs; // 0.0 1.0 final uploadProgress = 0.0.obs; // 0.0 1.0
@override
final uploadedCount = 0.obs; final uploadedCount = 0.obs;
@override
final totalToUpload = 0.obs; final totalToUpload = 0.obs;
@override
final uploadResult = Rxn<UploadResult>(); // final uploadResult = Rxn<UploadResult>(); //
bool get allSelected => bool get allSelected =>
@ -74,7 +81,7 @@ class EnterpriseUploadController extends GetxController {
} }
// 1. // 1.
Get.dialog( Get.dialog(
const UploadProgressDialog(), UploadProgressDialog(controller: this),
barrierDismissible: false, // barrierDismissible: false, //
); );
// 2. // 2.
@ -106,11 +113,13 @@ class EnterpriseUploadController extends GetxController {
} }
/// [] /// []
@override
void cancelUpload() { void cancelUpload() {
uploadEnterprisesUseCase.cancel(); uploadEnterprisesUseCase.cancel();
} }
/// [] /// []
@override
void closeUploadDialog() { void closeUploadDialog() {
Get.back(); // Get.back(); //
// true // true

16
lib/app/features/problem/data/datasources/problem_local_data_source.dart

@ -1,5 +1,4 @@
import 'package:get/get.dart'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; // SyncStatus
import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/core/services/database_service.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_filter_params.dart';
import 'package:sqflite/sqflite.dart'; // DatabaseService import 'package:sqflite/sqflite.dart'; // DatabaseService
@ -7,7 +6,7 @@ import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService
/// IProblemLocalDataSource /// IProblemLocalDataSource
/// 'problems' CRUD (, , , ) /// 'problems' CRUD (, , , )
/// (Repository) ( Sqflite) /// (Repository) ( Sqflite)
abstract class IProblemLocalDataSource { abstract class ProblemLocalDataSource {
/// ID /// ID
/// ///
/// [id] - /// [id] -
@ -40,11 +39,11 @@ abstract class IProblemLocalDataSource {
// IProblemLocalDataSource // IProblemLocalDataSource
class ProblemLocalDataSource implements IProblemLocalDataSource { class ProblemLocalDataSourceImpl implements ProblemLocalDataSource {
final DatabaseService _databaseService; final DatabaseService _databaseService;
final String _tableName = 'problems'; final String _tableName = 'problems';
ProblemLocalDataSource({required DatabaseService databaseService}) ProblemLocalDataSourceImpl({required DatabaseService databaseService})
: _databaseService = databaseService; : _databaseService = databaseService;
@override @override
@ -102,10 +101,11 @@ class ProblemLocalDataSource implements IProblemLocalDataSource {
} }
// 4. // 4.
if (filter.syncStatus != null) { if (filter.isUploaded != null) {
whereClauses.add('p.syncStatus = ?'); final operator = filter.isUploaded! ? '=' : '!=';
whereClauses.add('p.syncStatus $operator ?');
// SyncStatus // SyncStatus
arguments.add(filter.syncStatus!.name); arguments.add(SyncStatus.synced.name);
} }
// 5. // 5.

118
lib/app/features/problem/data/datasources/problem_remote_data_source.dart

@ -1 +1,117 @@
class ProblemRemoteDataSource {} import 'package:dio/dio.dart';
import 'package:problem_check_system/app/core/services/http_provider.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_dto.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_filter_dto.dart';
///
///
/// API
/// DTO (Data Transfer Objects)
abstract class ProblemRemoteDataSource {
// --- (Read/Query) ---
///
///
/// [filter]: DTO
/// ProblemDto Future
Future<List<ProblemDto>> getProblems(ProblemFilterDto filter);
/// ID
///
/// [problemId]:
/// ProblemDto Future
Future<ProblemDto> getProblemById(String problemId);
// --- (Create) ---
///
///
/// [problem]: DTO
/// ID和时间戳
Future<ProblemDto> createProblem(ProblemDto problem);
// --- (Update) ---
///
///
/// [problem]: DTO
///
Future<ProblemDto> updateProblem(ProblemDto problem);
// --- (Delete) ---
/// ID
///
/// [problemId]:
/// 使 Future<void>
Future<void> deleteProblem(String problemId);
}
class ProblemRemoteDataSourceImpl implements ProblemRemoteDataSource {
final HttpProvider http;
static const String problemsEndpoint = '/api/Memorandum';
// Dio
ProblemRemoteDataSourceImpl({required this.http});
@override
Future<List<ProblemDto>> getProblems(ProblemFilterDto filter) async {
try {
final response = await http.get(
problemsEndpoint, //
queryParameters: filter.toJson(),
);
// 'data'
final List<dynamic> problemListJson = response.data['data'];
return problemListJson.map((json) => ProblemDto.fromJson(json)).toList();
} on DioException catch (e) {
//
throw Exception('获取问题列表失败: $e');
}
}
@override
Future<ProblemDto> getProblemById(String problemId) async {
try {
final response = await http.get('$problemsEndpoint/$problemId');
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('获取问题失败: $e');
}
}
@override
Future<ProblemDto> createProblem(ProblemDto problem) async {
try {
final response = await http.post(
problemsEndpoint,
data: problem.toJson(), // DTO Map
);
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('创建问题失败: $e');
}
}
@override
Future<ProblemDto> updateProblem(ProblemDto problem) async {
try {
final response = await http.put(
'$problemsEndpoint/${problem.id}', // PUT ID URL
data: problem.toJson(),
);
return ProblemDto.fromJson(response.data);
} on DioException catch (e) {
throw Exception('更新问题失败: $e');
}
}
@override
Future<void> deleteProblem(String problemId) async {
try {
// 204 No ContentDio
await http.delete('$problemsEndpoint/$problemId');
} on DioException catch (e) {
throw Exception('删除问题失败: $e');
}
}
}

145
lib/app/features/problem/data/model/problem_dto.dart

@ -1,115 +1,108 @@
import 'dart:convert'; 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/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/core/extensions/map_extensions.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_entity.dart';
/// ProblemDto (Data Transfer Object)
///
/// API
/// API JSON
class ProblemDto { class ProblemDto {
final String? id; final String id;
final String? title; final String? title;
final String? location; final String? location;
final String? censorTaskId; final String? censorTaskId;
final String? rowId; final String? rowId;
final String? bindData; final String? bindData;
final List<String?>? imageUrls; final List<String>? imageUrls;
final String? creationTime; final String creatorId;
final String creationTime;
final String? lastModifierId;
final String? lastModificationTime;
final String? companyId; final String? companyId;
ProblemDto({ ProblemDto({
this.id, required this.id,
this.title, this.title,
this.location, this.location,
this.censorTaskId, this.censorTaskId,
this.rowId, this.rowId,
this.bindData, this.bindData,
this.imageUrls, this.imageUrls,
this.creationTime, required this.creatorId,
required this.creationTime,
this.lastModifierId,
this.lastModificationTime,
this.companyId, this.companyId,
}); });
factory ProblemDto.fromEntity(ProblemEntity entity) {
factory ProblemDto.fromModel(ProblemModel model) {
return ProblemDto( return ProblemDto(
id: model.id, id: entity.id,
title: model.description, title: entity.description,
location: model.location, location: entity.location,
bindData: model.bindData, bindData: entity.bindData,
imageUrls: List<String>.from(jsonDecode(model.imageUrls)), imageUrls: entity.imageUrls,
creationTime: model.creationTime, creationTime: entity.creationTime.toIso8601String(),
companyId: model.enterpriseId, creatorId: entity.creatorId,
companyId: entity.enterpriseId,
); );
} }
ProblemModel toModel() {
return ProblemModel(
id:
ProblemEntity toEntity() {
return ProblemEntity(
id: id,
description: title ?? '',
location: location ?? '',
imageUrls: imageUrls ?? [],
enterpriseId: companyId ?? '',
creatorId: creatorId,
creationTime: DateTime.tryParse(creationTime) ?? DateTime.now(),
lastModifierId: lastModifierId ?? creatorId,
lastModifiedTime:
DateTime.tryParse(lastModificationTime ?? '') ?? DateTime.now(),
syncStatus: SyncStatus.synced,
bindData: bindData,
); );
} }
// ======================================================================= // FIX: fromJson -
//
// =======================================================================
/// [] fromJson JSON Map EnterpriseDto
///
/// JSON Dart
factory ProblemDto.fromJson(Map<String, dynamic> json) { factory ProblemDto.fromJson(Map<String, dynamic> json) {
final creationTime = DateTime.parse(json['creationTime'] as String); final creatorIdData = json['creatorId'] as String? ?? '';
final creatorId = json['creatorId'] as String; final creationTimeData = json['creationTime'] 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? ?? "生产",
// return ProblemDto(
companyScope: json['companyScope'] as String?, id: json['id'] as String? ?? '',
mainPrincipalName: json['mainPrincipalName'] as String?, creationTime: creationTimeData,
mainPrincipalPhone: json['mainPrincipalPhone'] as String?, creatorId: creatorIdData,
securityPrincipalName: json['securityPrincipalName'] as String?, lastModificationTime:
securityPrincipalPhone: json['securityPrincipalPhone'] as String?, json['lastModificationTime'] as String? ?? creationTimeData,
companyAddress: json['companyAddress'] as String?, lastModifierId: json['lastModifierId'] as String? ?? creatorIdData,
majorHazard: json['majorHazard'] as String?, title: json['title'] as String?,
location: json['location'] as String?,
bindData: json['bindData'] as String?,
imageUrls: (json['imageUrls'] as List? ?? [])
.whereType<String>()
.toList(),
companyId: json['companyId'] as String?,
censorTaskId: json['censorTaskId'] as String?,
rowId: json['rowId'] as String?,
); );
} }
/// [] toJson EnterpriseDto JSON Map // 🟡 :
///
/// JSON
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final jsonMap = { final jsonMap = {
'id': id, //
// 使 toIso8601String() DateTime "id": id,
'creationTime': creationTime.toIso8601String(), "title": title,
'creatorId': creatorId, "location": location,
'lastModificationTime': lastModificationTime?.toIso8601String(), "bindData": bindData,
'lastModifierId': lastModifierId, "imageUrls": imageUrls,
'companyName': companyName, "creatorId": creatorId,
'companyType': companyType, "creationTime": creationTime,
'companyScope': companyScope, "companyId": companyId,
'mainPrincipalName': mainPrincipalName, "censorTaskId": censorTaskId,
'mainPrincipalPhone': mainPrincipalPhone, "rowId": rowId,
'securityPrincipalName': securityPrincipalName, "lastModifierId": lastModifierId,
'securityPrincipalPhone': securityPrincipalPhone, "lastModificationTime": lastModificationTime,
'companyAddress': companyAddress,
'majorHazard': majorHazard,
}; };
return jsonMap.withoutNullOrEmptyValues; return jsonMap.withoutNullOrEmptyValues;
} }
/// [] DTO () Model (/)
///
///
} }

3
lib/app/features/problem/data/model/problem_filter_dto.dart

@ -0,0 +1,3 @@
class ProblemFilterDto {
toJson() {}
}

70
lib/app/features/problem/data/repositories/problem_repository_impl.dart

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart'; 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/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_dto.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.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_entity.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_filter_params.dart';
@ -10,18 +12,22 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr
/// ///
/// ///
/// (ProblemEntity) (ProblemModel) /// (ProblemEntity) (ProblemModel)
class ProblemRepository implements IProblemRepository { class ProblemRepositoryImpl implements ProblemRepository {
final IProblemLocalDataSource problemLocalDataSource; // 2. final ProblemLocalDataSource localDataSource;
final ProblemRemoteDataSource remoteDataSource;
ProblemRepository(this.problemLocalDataSource); ProblemRepositoryImpl({
required this.localDataSource,
required this.remoteDataSource,
});
@override @override
Future<ProblemEntity> addProblem(ProblemEntity problem) async { Future<ProblemEntity> addProblem(ProblemEntity problem) async {
// 1. (Entity) (DTO) // 1. (Entity) (DTO)
final problemDto = ProblemModel.fromEntity(problem); final problemModel = ProblemModel.fromEntity(problem);
// 2. DTO Map // 2. DTO Map
await problemLocalDataSource.addProblem(problemDto.toMap()); await localDataSource.addProblem(problemModel.toMap());
// 3. // 3.
return problem; return problem;
@ -30,14 +36,14 @@ class ProblemRepository implements IProblemRepository {
@override @override
Future<void> deleteProblem(String id) async { Future<void> deleteProblem(String id) async {
// ID // ID
await problemLocalDataSource.deleteProblem(id); await localDataSource.deleteProblem(id);
} }
@override @override
Future<List<ProblemListItemEntity>> getAllProblemListItem({ Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter, required ProblemFilterParams filter,
}) async { }) async {
final problemDataMaps = await problemLocalDataSource.getAllProblems(filter); final problemDataMaps = await localDataSource.getAllProblems(filter);
return problemDataMaps.map((map) { return problemDataMaps.map((map) {
// 1: Map ProblemEntity // 1: Map ProblemEntity
@ -94,7 +100,7 @@ class ProblemRepository implements IProblemRepository {
@override @override
Future<ProblemEntity?> getProblemById(String id) async { Future<ProblemEntity?> getProblemById(String id) async {
// 1. ID (Map) // 1. ID (Map)
final problemMap = await problemLocalDataSource.getProblemById(id); final problemMap = await localDataSource.getProblemById(id);
// 2. null // 2. null
if (problemMap == null) { if (problemMap == null) {
@ -108,37 +114,51 @@ class ProblemRepository implements IProblemRepository {
@override @override
Future<ProblemEntity> updateProblem(ProblemEntity problem) async { Future<ProblemEntity> updateProblem(ProblemEntity problem) async {
// 1. (Entity) (DTO) // 1. (Entity) (DTO)
final problemDto = ProblemModel.fromEntity(problem); final problemModel = ProblemModel.fromEntity(problem);
// 2. DTO Map // 2. DTO Map
await problemLocalDataSource.updateProblem(problemDto.toMap()); await localDataSource.updateProblem(problemModel.toMap());
// 3. // 3.
return problem; return problem;
} }
// TODO:
@override @override
Future<ProblemEntity> syncProblemToServer(ProblemEntity problem) { Future<ProblemEntity> syncProblemToServer(ProblemEntity problem) async {
// Domain Data Model // Entity DTO
final problemModel = ProblemModel.fromEntity(problem); final problemDto = ProblemDto.fromEntity(problem);
EnterpriseDto syncedDto; ProblemDto syncedDto;
// ****
// Repository DataSource switch (problem.syncStatus) {
switch (enterprise.syncStatus) {
case SyncStatus.pendingCreate: case SyncStatus.pendingCreate:
syncedDto = await remoteDataSource.createEnterprise(enterpriseModel); syncedDto = await remoteDataSource.createProblem(problemDto);
break; break;
case SyncStatus.pendingUpdate: case SyncStatus.pendingUpdate:
syncedDto = await remoteDataSource.updateEnterprise(enterpriseModel); syncedDto = await remoteDataSource.updateProblem(problemDto);
break; break;
case SyncStatus.pendingDelete: case SyncStatus.pendingDelete:
await remoteDataSource.deleteEnterprise(enterprise.id); //
return enterprise; await remoteDataSource.deleteProblem(problemDto.id);
// //
await localDataSource.deleteProblem(problem.id);
//
return problem.copyWith(syncStatus: SyncStatus.synced);
case SyncStatus.synced: case SyncStatus.synced:
case SyncStatus.untracked: case SyncStatus.untracked:
return enterprise; return problem;
} }
return syncedDto.toModel().toEntity();
// DTO Entity
// syncedEntity ID
final syncedEntity = syncedDto.toEntity();
// Entity Model
//
final modelToSave = ProblemModel.fromEntity(syncedEntity);
await localDataSource.updateProblem(modelToSave.toMap());
// Entity
return syncedEntity;
} }
} }

6
lib/app/features/problem/domain/entities/problem_filter_params.dart

@ -1,5 +1,3 @@
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
/// ///
class ProblemFilterParams { class ProblemFilterParams {
/// () /// ()
@ -12,7 +10,7 @@ class ProblemFilterParams {
final DateTime? endTime; final DateTime? endTime;
/// () /// ()
final SyncStatus? syncStatus; final bool? isUploaded;
/// (true: , false: , null: ) /// (true: , false: , null: )
final bool? isBound; final bool? isBound;
@ -21,7 +19,7 @@ class ProblemFilterParams {
this.enterpriseName, this.enterpriseName,
this.startTime, this.startTime,
this.endTime, this.endTime,
this.syncStatus, this.isUploaded,
this.isBound, this.isBound,
}); });

2
lib/app/features/problem/domain/repositories/problem_repository.dart

@ -4,7 +4,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
/// Problem /// Problem
/// ///
abstract class IProblemRepository { abstract class ProblemRepository {
Future<List<ProblemListItemEntity>> getAllProblemListItem({ Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter, required ProblemFilterParams filter,
}); });

2
lib/app/features/problem/domain/usecases/add_problem_usecase.dart

@ -6,7 +6,7 @@ import 'package:problem_check_system/app/features/problem/domain/repositories/pr
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class AddProblemUsecase { class AddProblemUsecase {
final IProblemRepository problemRepository; final ProblemRepository problemRepository;
final AuthRepository authRepository; final AuthRepository authRepository;
final Uuid uuid; final Uuid uuid;

2
lib/app/features/problem/domain/usecases/delete_problem.dart

@ -1,7 +1,7 @@
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class DeleteProblem { class DeleteProblem {
final IProblemRepository problemRepository; final ProblemRepository problemRepository;
DeleteProblem({required this.problemRepository}); DeleteProblem({required this.problemRepository});
Future<void> call(String id) async { Future<void> call(String id) async {

2
lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart

@ -3,7 +3,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class GetAllProblemsUsecase { class GetAllProblemsUsecase {
final IProblemRepository problemRepository; final ProblemRepository problemRepository;
GetAllProblemsUsecase({required this.problemRepository}); GetAllProblemsUsecase({required this.problemRepository});

2
lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart

@ -2,7 +2,7 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class GetProblemByIdUsecase { class GetProblemByIdUsecase {
final IProblemRepository problemRepository; final ProblemRepository problemRepository;
GetProblemByIdUsecase({required this.problemRepository}); GetProblemByIdUsecase({required this.problemRepository});
Future<ProblemEntity?> call(String id) async { Future<ProblemEntity?> call(String id) async {

10
lib/app/features/problem/domain/usecases/update_problem_usecase.dart

@ -4,11 +4,11 @@ import 'package:problem_check_system/app/features/problem/domain/entities/proble
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
class UpdateProblemUsecase { class UpdateProblemUsecase {
final IProblemRepository repository; final ProblemRepository problemRepository;
final AuthRepository authRepository; final AuthRepository authRepository;
UpdateProblemUsecase({ UpdateProblemUsecase({
required this.repository, required this.problemRepository,
required this.authRepository, required this.authRepository,
}); });
@ -18,8 +18,10 @@ class UpdateProblemUsecase {
final newProblem = entity.copyWith( final newProblem = entity.copyWith(
lastModifiedTime: nowUtc, lastModifiedTime: nowUtc,
lastModifierId: userId, lastModifierId: userId,
syncStatus: SyncStatus.pendingUpdate, syncStatus: entity.syncStatus == SyncStatus.pendingCreate
? SyncStatus.pendingCreate
: SyncStatus.pendingUpdate,
); );
return await repository.updateProblem(newProblem); return await problemRepository.updateProblem(newProblem);
} }
} }

131
lib/app/features/problem/domain/usecases/upload_problems_usecase.dart

@ -1,42 +1,57 @@
import 'dart:async'; import 'dart:async';
import 'package:get/get.dart'; // GetX import 'package:get/get.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.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/problem/domain/entities/problem_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart';
import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.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';
/// ///
///
/// :
/// - UI
/// -
/// -
/// - 便 UI
/// -
class UploadProblemsUsecase { class UploadProblemsUsecase {
final ProblemRepository repository; final ProblemRepository problemRepository;
UploadProblemsUsecase({required this.repository}); final EnterpriseRepository enterpriseRepository;
UploadProblemsUsecase({
required this.problemRepository,
required this.enterpriseRepository,
});
// 使 GetX RxBool //
// Controller UseCase
final RxBool _isCancelled = false.obs; final RxBool _isCancelled = false.obs;
/// [] /// [API]
void cancel() { void cancel() {
_isCancelled.value = true; _isCancelled.value = true;
} }
/// /// [API]
/// [onProgress] (Controller) ///
/// [problemsToUpload]:
/// [onProgress]: (, )
/// [onFailure]: () (, )
Future<UploadResult> call({ Future<UploadResult> call({
required List<ProblemListItemEntity> problemsToUpload, required List<ProblemEntity> problemsToUpload,
required void Function(int uploaded, int total) onProgress, required void Function(int uploaded, int total) onProgress,
void Function(ProblemEntity failedProblem, String error)? onFailure,
}) async { }) async {
// 便 _isCancelled.value = false; // 便
_isCancelled.value = false;
int successCount = 0; int successCount = 0;
int failureCount = 0; int failureCount = 0;
final total = problemsToUpload.length; final total = problemsToUpload.length;
// [2]
final Set<String> syncedEnterpriseIdsInThisSession = {};
for (int i = 0; i < total; i++) { for (int i = 0; i < total; i++) {
// //
if (_isCancelled.value) { if (_isCancelled.value) {
//
return UploadResult( return UploadResult(
successCount: successCount, successCount: successCount,
failureCount: failureCount, failureCount: failureCount,
@ -44,28 +59,80 @@ class UploadProblemsUsecase {
); );
} }
final problem = problemsToUpload[i].problemEntity; final problem = problemsToUpload[i];
try { try {
// 1. // [3] 使
final syncedProblem = await repository.syncProblemToServer(problem); await _syncSingleProblemWithDependencies(
// 2. problem,
if (problem.syncStatus == SyncStatus.pendingDelete) { syncedEnterpriseIdsInThisSession,
// repository.deleteLocalEnterprise(enterprise.id); );
} else {
await repository.updateProblem(syncedProblem);
}
successCount++; successCount++;
} catch (e) { } catch (e) {
//
failureCount++; failureCount++;
Get.log('上传失败: ${problem.description}, 错误: $e'); final errorMessage = '问题 [${problem.description}] 上传失败';
Get.log('$errorMessage, 错误: $e');
// [1] Controller
onFailure?.call(problem, e.toString());
} }
// 3. //
onProgress(i + 1, total); onProgress(i + 1, total);
} }
// return UploadResult(
return UploadResult(successCount: successCount, failureCount: failureCount); successCount: successCount,
failureCount: failureCount,
wasCancelled: false,
);
}
/// []
Future<void> _syncSingleProblemWithDependencies(
ProblemEntity problem,
Set<String> syncedEnterpriseIds,
) async {
// --- A: ---
final enterprise = await enterpriseRepository.getEnterpriseById(
problem.enterpriseId,
);
//
if (enterprise == null) {
throw Exception('数据不一致:关联的企业 (ID: ${problem.enterpriseId}) 在本地数据库中不存在。');
}
//
final bool needsEnterpriseSync =
(enterprise.syncStatus != SyncStatus.synced &&
enterprise.syncStatus != SyncStatus.untracked);
if (needsEnterpriseSync && !syncedEnterpriseIds.contains(enterprise.id)) {
Get.log('优先同步依赖的企业 [${enterprise.name}]...');
try {
final syncedEnterprise = await enterpriseRepository
.syncEnterpriseToServer(enterprise);
Get.log('企业 [${syncedEnterprise.name}] 同步成功!');
// ID
syncedEnterpriseIds.add(syncedEnterprise.id);
} catch (e) {
//
throw Exception('其依赖的企业 [${enterprise.name}] 上传失败: $e');
}
}
// --- B: ---
//
// enterpriseId ID
//
final refreshedProblem = await problemRepository.getProblemById(problem.id);
if (refreshedProblem == null) {
throw Exception('数据不一致:在企业同步后,问题 (ID: ${problem.id}) 从本地数据库消失。');
}
Get.log('正在同步问题 [${refreshedProblem.description}]...');
await problemRepository.syncProblemToServer(refreshedProblem);
} }
} }

2
lib/app/features/problem/presentation/bindings/problem_form_binding.dart

@ -57,7 +57,7 @@ class ProblemFormBinding extends BaseBindings {
); );
Get.lazyPut( Get.lazyPut(
() => UpdateProblemUsecase( () => UpdateProblemUsecase(
repository: Get.find(), problemRepository: Get.find(),
authRepository: Get.find(), authRepository: Get.find(),
), ),
); );

51
lib/app/features/problem/presentation/bindings/problem_list_binding.dart

@ -1,35 +1,52 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/app/core/bindings/base_bindings.dart';
import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/core/services/database_service.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/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart'; import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.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/get_all_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart';
class ProblemListBinding extends Bindings { class ProblemListBinding extends BaseBindings {
@override @override
void dependencies() { void register1Services() {
// // TODO: implement register1Services
Get.lazyPut<IProblemLocalDataSource>( }
() =>
ProblemLocalDataSource(databaseService: Get.find<DatabaseService>()), @override
void register2DataSource() {
Get.lazyPut<ProblemLocalDataSource>(
() => ProblemLocalDataSourceImpl(
databaseService: Get.find<DatabaseService>(),
),
); );
// Get.lazyPut<ProblemRemoteDataSource>(
Get.lazyPut<IProblemRepository>( () => ProblemRemoteDataSourceImpl(http: Get.find()),
() => ProblemRepository(Get.find<IProblemLocalDataSource>()),
); );
// }
Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase( @override
problemRepository: Get.find<IProblemRepository>(), void register3Repositories() {
Get.lazyPut<ProblemRepository>(
() => ProblemRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
), ),
); );
}
@override
void register4Usecases() {
Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase(problemRepository: Get.find()),
);
}
/// @override
void register5Controllers() {
Get.lazyPut<ProblemListController>( Get.lazyPut<ProblemListController>(
() => ProblemListController( () => ProblemListController(getAllProblemsUsecase: Get.find()),
getAllProblemsUsecase: Get.find<GetAllProblemsUsecase>(),
),
); );
} }
} }

55
lib/app/features/problem/presentation/bindings/problem_upload_binding.dart

@ -1,5 +1,15 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/app/core/bindings/base_bindings.dart'; import 'package:problem_check_system/app/core/bindings/base_bindings.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/problem/data/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_remote_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository_impl.dart';
import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.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';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart';
class ProblemUploadBinding extends BaseBindings { class ProblemUploadBinding extends BaseBindings {
@ -10,21 +20,58 @@ class ProblemUploadBinding extends BaseBindings {
@override @override
void register2DataSource() { void register2DataSource() {
// TODO: implement register2DataSource Get.lazyPut<ProblemLocalDataSource>(
() => ProblemLocalDataSourceImpl(databaseService: Get.find()),
);
Get.lazyPut<ProblemRemoteDataSource>(
() => ProblemRemoteDataSourceImpl(http: Get.find()),
);
Get.lazyPut<EnterpriseLocalDataSource>(
() => EnterpriseLocalDataSourceImpl(databaseService: Get.find()),
);
Get.lazyPut<EnterpriseRemoteDataSource>(
() => EnterpriseRemoteDataSourceImpl(http: Get.find()),
);
} }
@override @override
void register3Repositories() { void register3Repositories() {
// TODO: implement register3Repositories Get.lazyPut<ProblemRepository>(
() => ProblemRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
),
);
Get.lazyPut<EnterpriseRepository>(
() => EnterpriseRepositoryImpl(
localDataSource: Get.find(),
remoteDataSource: Get.find(),
networkStatusService: Get.find(),
uuid: Get.find(),
),
);
} }
@override @override
void register4Usecases() { void register4Usecases() {
// TODO: implement register4Usecases Get.lazyPut<GetAllProblemsUsecase>(
() => GetAllProblemsUsecase(problemRepository: Get.find()),
);
Get.lazyPut<UploadProblemsUsecase>(
() => UploadProblemsUsecase(
problemRepository: Get.find(),
enterpriseRepository: Get.find(),
),
);
} }
@override @override
void register5Controllers() { void register5Controllers() {
// Get.lazyPut(() => ProblemUploadController()); Get.lazyPut(
() => ProblemUploadController(
getAllProblemsUsecase: Get.find(),
uploadProblemsUsecase: Get.find(),
),
);
} }
} }

43
lib/app/features/problem/presentation/controllers/problem_form_controller.dart

@ -251,40 +251,49 @@ class ProblemFormController extends GetxController {
} }
Get.back(result: true); // Get.back(result: true); //
} catch (e) { } catch (e) {
Get.log('$e');
Get.snackbar('错误', '保存问题失败: $e'); Get.snackbar('错误', '保存问题失败: $e');
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
//
Future<List<String>> _saveImagesToLocal() async { Future<List<String>> _saveImagesToLocal() async {
final List<String> imagePaths = []; final List<String> finalImagePaths = [];
final directory = await getApplicationDocumentsDirectory(); final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images'); final imagesDir = Directory('${directory.path}/problem_images');
//
if (!await imagesDir.exists()) { if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true); await imagesDir.create(recursive: true);
} }
//
for (var image in selectedImages) { for (var image in selectedImages) {
try { // : XFile
final String fileName = '${Uuid().v4()}_${path.basename(image.name)}'; //
final String imagePath = '${imagesDir.path}/$fileName'; if (image.path.startsWith(directory.path)) {
final File imageFile = File(imagePath); // 使
finalImagePaths.add(image.path);
// } else {
final imageBytes = await image.readAsBytes(); //
await imageFile.writeAsBytes(imageBytes); try {
// ( .jpg)
imagePaths.add(imagePath); final String extension = path.extension(image.path);
} catch (e) { //
throw Exception(e); final String newFileName = '${const Uuid().v4()}$extension';
final String newImagePath = path.join(imagesDir.path, newFileName);
//
await image.saveTo(newImagePath); // XFile.saveTo()
finalImagePaths.add(newImagePath);
} catch (e) {
//
throw Exception('保存新图片失败: ${image.path}. 错误: $e');
}
} }
} }
return imagePaths; return finalImagePaths;
} }
// / // /

58
lib/app/features/problem/presentation/controllers/problem_upload_controller.dart

@ -1,14 +1,16 @@
// lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/app/core/controllers/i_upload_controller.dart';
import 'package:problem_check_system/app/core/domain/entities/upload_result.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/core/pages/widgets/upload_progress_dialog.dart';
import 'package:problem_check_system/app/features/enterprise/domain/usecases/upload_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.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/entities/problem_list_item_entity.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/get_all_problems_usecase.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/usecases/upload_problems_usecase.dart';
class ProblemUploadController extends GetxController { class ProblemUploadController extends GetxController
implements IUploadController {
final GetAllProblemsUsecase getAllProblemsUsecase; final GetAllProblemsUsecase getAllProblemsUsecase;
final UploadProblemsUsecase uploadProblemsUsecase; final UploadProblemsUsecase uploadProblemsUsecase;
@ -19,18 +21,22 @@ class ProblemUploadController extends GetxController {
// --- --- // --- ---
final isLoading = false.obs; final isLoading = false.obs;
final enterprises = <EnterpriseListItem>[].obs; final problems = <ProblemListItemEntity>[].obs;
final selectedEnterprises = <EnterpriseListItem>{}.obs; final selectedProblems = <ProblemListItemEntity>{}.obs;
// --- [] --- // --- [] ---
@override
final isUploading = false.obs; final isUploading = false.obs;
@override
final uploadProgress = 0.0.obs; // 0.0 1.0 final uploadProgress = 0.0.obs; // 0.0 1.0
@override
final uploadedCount = 0.obs; final uploadedCount = 0.obs;
@override
final totalToUpload = 0.obs; final totalToUpload = 0.obs;
@override
final uploadResult = Rxn<UploadResult>(); // final uploadResult = Rxn<UploadResult>(); //
bool get allSelected => bool get allSelected =>
enterprises.isNotEmpty && problems.isNotEmpty && problems.length == selectedProblems.length;
enterprises.length == selectedEnterprises.length;
@override @override
void onInit() { void onInit() {
@ -38,20 +44,20 @@ class ProblemUploadController extends GetxController {
fetchPendingUploads(); fetchPendingUploads();
} }
void onSelectionChanged(EnterpriseListItem enterprise) { void onSelectionChanged(ProblemListItemEntity problem) {
if (selectedEnterprises.contains(enterprise)) { if (selectedProblems.contains(problem)) {
selectedEnterprises.remove(enterprise); selectedProblems.remove(problem);
} else { } else {
selectedEnterprises.add(enterprise); selectedProblems.add(problem);
} }
} }
void toggleSelectAll() { void toggleSelectAll() {
if (allSelected) { if (allSelected) {
selectedEnterprises.clear(); selectedProblems.clear();
} else { } else {
// //
selectedEnterprises.addAll(enterprises); selectedProblems.addAll(problems);
} }
} }
@ -59,8 +65,10 @@ class ProblemUploadController extends GetxController {
Future<void> fetchPendingUploads() async { Future<void> fetchPendingUploads() async {
isLoading.value = true; isLoading.value = true;
try { try {
final data = await getEnterpriseListUsecase(isUploaded: false); final data = await getAllProblemsUsecase(
enterprises.assignAll(data); filter: ProblemFilterParams(isUploaded: false),
);
problems.assignAll(data);
} catch (e) { } catch (e) {
Get.snackbar('错误', '加载待上传列表失败: $e'); Get.snackbar('错误', '加载待上传列表失败: $e');
} finally { } finally {
@ -69,13 +77,13 @@ class ProblemUploadController extends GetxController {
} }
void confirmUpload() { void confirmUpload() {
if (selectedEnterprises.isEmpty) { if (selectedProblems.isEmpty) {
Get.snackbar('提示', '请至少选择一个企业进行上传'); Get.snackbar('提示', '请至少选择一个问题进行上传');
return; return;
} }
// 1. // 1.
Get.dialog( Get.dialog(
const UploadProgressDialog(), UploadProgressDialog(controller: this),
barrierDismissible: false, // barrierDismissible: false, //
); );
// 2. // 2.
@ -88,12 +96,14 @@ class ProblemUploadController extends GetxController {
isUploading.value = true; isUploading.value = true;
uploadProgress.value = 0.0; uploadProgress.value = 0.0;
uploadResult.value = null; uploadResult.value = null;
totalToUpload.value = selectedEnterprises.length; totalToUpload.value = selectedProblems.length;
uploadedCount.value = 0; uploadedCount.value = 0;
// UseCase onProgress // UseCase onProgress
final result = await uploadEnterprisesUseCase.call( final result = await uploadProblemsUsecase(
enterprisesToUpload: selectedEnterprises.toList(), problemsToUpload: selectedProblems
.map((item) => item.problemEntity)
.toList(),
onProgress: (uploaded, total) { onProgress: (uploaded, total) {
// //
uploadedCount.value = uploaded; uploadedCount.value = uploaded;
@ -107,11 +117,13 @@ class ProblemUploadController extends GetxController {
} }
/// [] /// []
@override
void cancelUpload() { void cancelUpload() {
uploadEnterprisesUseCase.cancel(); uploadProblemsUsecase.cancel();
} }
/// [] /// []
@override
void closeUploadDialog() { void closeUploadDialog() {
Get.back(); // Get.back(); //
// true // true

24
lib/app/features/problem/presentation/pages/problem_upload_page.dart

@ -3,8 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.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: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/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'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart';
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/problem_card.dart';
class ProblemUploadPage extends GetView<ProblemUploadController> { class ProblemUploadPage extends GetView<ProblemUploadController> {
const ProblemUploadPage({super.key}); const ProblemUploadPage({super.key});
@ -16,9 +16,9 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
preferredSize: const Size.fromHeight(kToolbarHeight), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx( child: Obx(
() => UploadAppBar( () => UploadAppBar(
selectedCount: controller.selectedEnterprises.length, selectedCount: controller.selectedProblems.length,
allSelected: controller.allSelected, allSelected: controller.allSelected,
buttonVisible: controller.enterprises.isNotEmpty, buttonVisible: controller.problems.isNotEmpty,
onButtonPressed: () => controller.toggleSelectAll(), onButtonPressed: () => controller.toggleSelectAll(),
), ),
), ),
@ -32,7 +32,7 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
), ),
child: Obx( child: Obx(
() => ElevatedButton( () => ElevatedButton(
onPressed: controller.selectedEnterprises.isNotEmpty onPressed: controller.selectedProblems.isNotEmpty
? controller.confirmUpload ? controller.confirmUpload
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -43,7 +43,7 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
), ),
minimumSize: Size(double.infinity, 48.h), minimumSize: Size(double.infinity, 48.h),
), ),
child: Text('确认上传 (${controller.selectedEnterprises.length})'), child: Text('确认上传 (${controller.selectedProblems.length})'),
), ),
), ),
), ),
@ -54,11 +54,11 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
// 使 Obx controller Rx // 使 Obx controller Rx
return Obx(() { return Obx(() {
// //
if (controller.isLoading.value && controller.enterprises.isEmpty) { if (controller.isLoading.value && controller.problems.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// //
if (controller.enterprises.isEmpty) { if (controller.problems.isEmpty) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -83,18 +83,18 @@ class ProblemUploadPage extends GetView<ProblemUploadController> {
onRefresh: () async => controller.fetchPendingUploads(), onRefresh: () async => controller.fetchPendingUploads(),
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.enterprises.length, itemCount: controller.problems.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = controller.enterprises[index]; final item = controller.problems[index];
// : base controller // : base controller
return Obx(() { return Obx(() {
final isSelected = controller.selectedEnterprises.contains(item); final isSelected = controller.selectedProblems.contains(item);
return Padding( return Padding(
padding: EdgeInsets.only(bottom: 12.h), padding: EdgeInsets.only(bottom: 12.h),
child: UnifiedEnterpriseCard( child: ProblemCard(
enterpriseListItem: item, problemListItem: item,
isSelected: isSelected, isSelected: isSelected,
// itemMode mode // itemMode mode
// --- [] controller --- // --- [] controller ---

Loading…
Cancel
Save