From cac0993b504cf845064fabcf4118b1551beb0818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Thu, 30 Oct 2025 16:18:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor=20:=20=E9=97=AE=E9=A2=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E8=BF=9B=E8=A1=8C=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/core/models/form_mode.dart | 10 + lib/app/core/models/problem_sync_status.dart | 2 +- lib/app/core/models/sync_status.dart | 65 +- .../core/repositories/auth_repository.dart | 2 +- lib/app/core/routes/app_pages.dart | 17 +- lib/app/core/services/database_service.dart | 36 +- .../usecases/add_enterprise_usecase.dart | 4 +- .../enterprise_form_controller.dart | 12 +- .../enterprise_list_controller.dart | 2 +- .../widgets/unified_enterprise_card.dart | 1 - .../features/home/bindings/home_binding.dart | 33 +- .../home/controllers/home_controller.dart | 4 +- .../controllers/navigation_controller.dart | 4 +- .../problem_local_data_source.dart | 145 ++ .../datasources/problem_local_datasource.dart | 123 -- .../problem_remote_data_source.dart | 1 + .../problem/data/model/problem_local_dto.dart | 103 + .../data/model/problem_remote_dto.dart | 144 ++ .../data/repositories/problem_repository.dart | 252 +-- .../repositories/problem_repository_impl.dart | 81 + .../domain/entities/problem_entity.dart | 67 +- .../repositoies/problem_repository.dart | 19 - .../repositories/problem_repository.dart | 16 + .../domain/usecases/add_problem_usecase.dart | 40 + .../domain/usecases/delete_problem.dart | 10 + .../usecases/get_all_problems_usecase.dart | 22 + .../usecases/get_problem_by_id_usecase.dart | 11 + .../domain/usecases/update_problem.dart | 22 + .../bindings/problem_binding.dart | 62 +- .../bindings/problem_form_binding.dart | 34 +- .../bindings/problem_list_binding.dart | 35 + .../bindings/problem_upload_binding.dart | 8 + .../controllers/problem_controller.dart | 1946 ++++++++--------- .../controllers/problem_form_controller.dart | 142 +- .../controllers/problem_list_controller.dart | 208 ++ .../problem_upload_controller.dart | 13 + .../models/problem_card_model.dart | 0 .../models/problem_form_model.dart | 13 + .../{views => pages}/problem_form_page.dart | 130 +- .../presentation/pages/problem_list_page.dart | 182 ++ .../{views => pages}/problem_upload_page.dart | 31 +- .../widgets/custom_button.dart | 0 .../widgets/custom_filter_dropdown.dart | 2 +- .../widgets/models/date_range_enum.dart | 2 +- .../widgets/models/dropdown_option.dart | 0 .../widgets/problem_card.dart | 10 +- .../widgets/sync_progress_dialog.dart | 0 .../presentation/views/problem_page.dart | 94 - .../views/widgets/current_filter_bar.dart | 66 - .../views/widgets/history_filter_bar.dart | 97 - .../views/widgets/problem_list_page.dart | 181 -- 51 files changed, 2508 insertions(+), 1996 deletions(-) create mode 100644 lib/app/core/models/form_mode.dart create mode 100644 lib/app/features/problem/data/datasources/problem_local_data_source.dart delete mode 100644 lib/app/features/problem/data/datasources/problem_local_datasource.dart create mode 100644 lib/app/features/problem/data/datasources/problem_remote_data_source.dart create mode 100644 lib/app/features/problem/data/model/problem_local_dto.dart create mode 100644 lib/app/features/problem/data/model/problem_remote_dto.dart create mode 100644 lib/app/features/problem/data/repositories/problem_repository_impl.dart delete mode 100644 lib/app/features/problem/domain/repositoies/problem_repository.dart create mode 100644 lib/app/features/problem/domain/repositories/problem_repository.dart create mode 100644 lib/app/features/problem/domain/usecases/add_problem_usecase.dart create mode 100644 lib/app/features/problem/domain/usecases/delete_problem.dart create mode 100644 lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart create mode 100644 lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart create mode 100644 lib/app/features/problem/domain/usecases/update_problem.dart create mode 100644 lib/app/features/problem/presentation/bindings/problem_list_binding.dart create mode 100644 lib/app/features/problem/presentation/bindings/problem_upload_binding.dart create mode 100644 lib/app/features/problem/presentation/controllers/problem_list_controller.dart create mode 100644 lib/app/features/problem/presentation/controllers/problem_upload_controller.dart create mode 100644 lib/app/features/problem/presentation/models/problem_card_model.dart create mode 100644 lib/app/features/problem/presentation/models/problem_form_model.dart rename lib/app/features/problem/presentation/{views => pages}/problem_form_page.dart (79%) create mode 100644 lib/app/features/problem/presentation/pages/problem_list_page.dart rename lib/app/features/problem/presentation/{views => pages}/problem_upload_page.dart (69%) rename lib/app/features/problem/presentation/{views => pages}/widgets/custom_button.dart (100%) rename lib/app/features/problem/presentation/{views => pages}/widgets/custom_filter_dropdown.dart (97%) rename lib/app/features/problem/presentation/{views => pages}/widgets/models/date_range_enum.dart (96%) rename lib/app/features/problem/presentation/{views => pages}/widgets/models/dropdown_option.dart (100%) rename lib/app/features/problem/presentation/{views => pages}/widgets/problem_card.dart (96%) rename lib/app/features/problem/presentation/{views => pages}/widgets/sync_progress_dialog.dart (100%) delete mode 100644 lib/app/features/problem/presentation/views/problem_page.dart delete mode 100644 lib/app/features/problem/presentation/views/widgets/current_filter_bar.dart delete mode 100644 lib/app/features/problem/presentation/views/widgets/history_filter_bar.dart delete mode 100644 lib/app/features/problem/presentation/views/widgets/problem_list_page.dart diff --git a/lib/app/core/models/form_mode.dart b/lib/app/core/models/form_mode.dart new file mode 100644 index 0000000..5a0ad56 --- /dev/null +++ b/lib/app/core/models/form_mode.dart @@ -0,0 +1,10 @@ +enum FormMode { + /// 新增 + add, + + /// 编辑 + edit, + + /// 查看 + view, +} diff --git a/lib/app/core/models/problem_sync_status.dart b/lib/app/core/models/problem_sync_status.dart index 0a0967f..6031ca7 100644 --- a/lib/app/core/models/problem_sync_status.dart +++ b/lib/app/core/models/problem_sync_status.dart @@ -41,7 +41,7 @@ class ProblemStateManager extends GetxController { location: location, imageUrls: imageUrls, creationTime: DateTime.now().toUtc(), - creatorId: authRepository.getUserId()!, + creatorId: authRepository.getUserId(), lastModifiedTime: DateTime.now().toUtc(), syncStatus: ProblemSyncStatus.pendingCreate, ); diff --git a/lib/app/core/models/sync_status.dart b/lib/app/core/models/sync_status.dart index c635cd5..4a304f3 100644 --- a/lib/app/core/models/sync_status.dart +++ b/lib/app/core/models/sync_status.dart @@ -1,60 +1,17 @@ +// SyncStatus 的现代化重构 import 'package:flutter/material.dart'; enum SyncStatus { - /// 未跟踪 - 需要被移除的记录(如本地删除但从未同步过) - untracked, + untracked('未跟踪', Colors.grey), + synced('已同步', Colors.green), + pendingCreate('待新建', Colors.blue), + pendingUpdate('待更新', Colors.orange), + pendingDelete('待删除', Colors.red); - /// 已同步 - 与服务器完全一致(类似git的unmodified) - synced, + final String displayName; + final Color displayColor; - /// 待创建 - 新问题,需要上传到服务器(类似git的untracked → staged) - pendingCreate, - - /// 待更新 - 已修改的问题,需要更新到服务器(类似git的modified → staged) - pendingUpdate, - - /// 待删除 - 已标记删除,需要从服务器删除(类似git的deleted → staged) - pendingDelete, -} - -/// 为 SyncStatus 枚举添加扩展功能 -extension SyncStatusExtension on SyncStatus { - /// 返回一个用户可读的描述字符串 - String get displayName { - switch (this) { - case SyncStatus.synced: - return '已同步'; - case SyncStatus.pendingCreate: - return '待新建'; - case SyncStatus.pendingUpdate: - return '待更新'; - case SyncStatus.pendingDelete: - return '待删除'; - case SyncStatus.untracked: - return '未跟踪'; - // 添加一个 default 是一个好习惯,以防未来添加新的枚举值而忘记更新这里 - default: - return '未知状态'; - } - } - - /// [可选] 返回一个与状态对应的颜色,用于UI显示 - Color get displayColor { - switch (this) { - case SyncStatus.synced: - return Colors.green; - case SyncStatus.pendingCreate: - return Colors.blue; - case SyncStatus.pendingUpdate: - return Colors.orange; - case SyncStatus.pendingDelete: - return Colors.red; - case SyncStatus.untracked: - return Colors.grey; - default: - return Colors.black; - } - } + const SyncStatus(this.displayName, this.displayColor); } /// 一个抽象接口,定义了所有“可同步”实体的共同特征。 @@ -62,6 +19,6 @@ extension SyncStatusExtension on SyncStatus { abstract class SyncableEntity { String get id; SyncStatus get syncStatus; - DateTime? get lastModifiedTime; - String? get lastModifierId; + DateTime get lastModifiedTime; + String get lastModifierId; } diff --git a/lib/app/core/repositories/auth_repository.dart b/lib/app/core/repositories/auth_repository.dart index 3280c63..5c7832d 100644 --- a/lib/app/core/repositories/auth_repository.dart +++ b/lib/app/core/repositories/auth_repository.dart @@ -43,7 +43,7 @@ class AuthRepository extends GetxService { storage.write(_userId, id); } - String? getUserId() { + String getUserId() { return storage.read(_userId); } diff --git a/lib/app/core/routes/app_pages.dart b/lib/app/core/routes/app_pages.dart index d9b7f37..80454e2 100644 --- a/lib/app/core/routes/app_pages.dart +++ b/lib/app/core/routes/app_pages.dart @@ -16,11 +16,12 @@ import 'package:problem_check_system/app/features/auth/bindings/login_binding.da import 'package:problem_check_system/app/features/auth/views/login_page.dart'; import 'package:problem_check_system/app/features/my/bindings/change_password_binding.dart'; import 'package:problem_check_system/app/features/my/views/change_password.dart'; -import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_binding.dart'; import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_form_binding.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/problem_form_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/problem_upload_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_list_binding.dart'; +import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_upload_binding.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/problem_form_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/problem_list_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/problem_upload_page.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_info_page.dart'; import 'app_routes.dart'; @@ -34,7 +35,7 @@ abstract class AppPages { binding: BindingsBuilder(() { NavigationBinding().dependencies(); EnterpriseListBinding().dependencies(); - ProblemBinding().dependencies(); + ProblemListBinding().dependencies(); ProfileBinding().dependencies(); }), ), @@ -56,8 +57,8 @@ abstract class AppPages { ), GetPage( name: AppRoutes.problem, - page: () => const ProblemPage(), - binding: ProblemBinding(), + page: () => const ProblemListPage(), + binding: ProblemListBinding(), ), GetPage( name: AppRoutes.my, @@ -67,7 +68,7 @@ abstract class AppPages { GetPage( name: AppRoutes.problemUpload, page: () => const ProblemUploadPage(), - binding: ProblemBinding(), + binding: ProblemUploadBinding(), ), GetPage( name: AppRoutes.problemForm, diff --git a/lib/app/core/services/database_service.dart b/lib/app/core/services/database_service.dart index a77d7af..57896a6 100644 --- a/lib/app/core/services/database_service.dart +++ b/lib/app/core/services/database_service.dart @@ -2,21 +2,33 @@ import 'package:get/get.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; -/// 创建问题表的脚本 +/// 创建问题表的脚本 (最佳实践版本) const String _createProblemsTable = ''' CREATE TABLE problems( id TEXT PRIMARY KEY, enterpriseId TEXT NOT NULL, description TEXT NOT NULL, location TEXT NOT NULL, - imageUrls TEXT NOT NULL, - creationTime INTEGER NOT NULL, + + -- [建议三] 为 imageUrls 添加默认值,增强健壮性 + imageUrls TEXT NOT NULL DEFAULT '[]', + creatorId TEXT NOT NULL, - lastModifiedTime INTEGER NOT NULL, - syncStatus INTEGER NOT NULL, - censorTaskId TEXT, - bindData TEXT, - isChecked INTEGER NOT NULL + creationTime TEXT NOT NULL, + lastModifierId TEXT NOT NULL, + lastModifiedTime TEXT NOT NULL, + + -- [建议一] 使用 CHECK 约束保证 syncStatus 的数据完整性 + syncStatus TEXT NOT NULL CHECK(syncStatus IN ( + 'untracked', + 'synced', + 'pendingCreate', + 'pendingUpdate', + 'pendingDelete' + )), + + bindData TEXT + -- 注意:SQL 语句中,最后一列后面不能有逗号 ) '''; @@ -81,6 +93,14 @@ class DatabaseService extends GetxService { // 创建问题表 await db.execute(_createProblemsTable); Get.log('`problems` 表创建成功'); + // [建议二] 为常用查询字段创建索引,提升性能 + await db.execute( + 'CREATE INDEX idx_problems_creationTime ON problems(creationTime)', + ); + await db.execute( + 'CREATE INDEX idx_problems_syncStatus ON problems(syncStatus)', + ); + Get.log('`problems` 表的索引创建成功'); // 创建企业表 await db.execute(_createEnterprisesTable); diff --git a/lib/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart b/lib/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart index bf3a4a3..930d9b6 100644 --- a/lib/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart +++ b/lib/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart @@ -34,9 +34,9 @@ class AddEnterpriseUsecase { contactPhone: contactPhone, majorHazardsDescription: majorHazardsDescription, lastModifiedTime: DateTime.now(), - lastModifierId: authRepository.getUserId()!, + lastModifierId: authRepository.getUserId(), creationTime: DateTime.now(), - creatorId: authRepository.getUserId()!, + creatorId: authRepository.getUserId(), syncStatus: SyncStatus.pendingCreate, ); await repository.addEnterprise(enterprise); diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart index 08adbc1..e7c9286 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart @@ -1,21 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/app/core/models/company_enum.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/add_enterprise_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/editor_enterprise_usecase.dart'; -enum FormMode { - /// 新增 - add, - - /// 编辑 - edit, - - /// 查看 - view, -} - class EnterpriseFormController extends GetxController { final AddEnterpriseUsecase? addEnterpriseUsecase; final EditEnterpriseUsecase? editEnterpriseUsecase; diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart index 124765a..d8c8b83 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; import 'package:problem_check_system/app/core/models/company_enum.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_conflict.dart'; @@ -11,7 +12,6 @@ import 'package:problem_check_system/app/features/enterprise/domain/entities/ent 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/resolve_conflict_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/sync_enterprises_usecase.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart'; /// ----------------------------------------------------------------------------- /// [具体实现] 企业管理列表控制器 diff --git a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart index 4c41339..4845c2f 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart +++ b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; -import 'package:problem_check_system/app/core/models/sync_status.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; // [重构 1] 新的统一卡片组件 diff --git a/lib/app/features/home/bindings/home_binding.dart b/lib/app/features/home/bindings/home_binding.dart index e0c3937..98a86a9 100644 --- a/lib/app/features/home/bindings/home_binding.dart +++ b/lib/app/features/home/bindings/home_binding.dart @@ -1,31 +1,26 @@ import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; -import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; import 'package:problem_check_system/app/features/home/controllers/home_controller.dart'; -import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; class HomeBinding implements Bindings { @override void dependencies() { /// 注册主页控制器 Get.lazyPut(() => HomeController()); - Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); - // Get.lazyPut(() => EnterpriseListController()); + // Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); + // // Get.lazyPut(() => EnterpriseListController()); - /// 注册问题控制器 - Get.lazyPut( - () => ProblemController( - problemRepository: Get.find(), - problemStateManager: Get.find(), - ), - fenix: true, - ); + // /// 注册问题控制器 + // Get.lazyPut( + // () => ProblemController( + // problemRepository: Get.find(), + // problemStateManager: Get.find(), + // ), + // fenix: true, + // ); - /// 注册我的控制器 - Get.lazyPut( - () => MyController(authRepository: Get.find()), - ); + // /// 注册我的控制器 + // Get.lazyPut( + // () => MyController(authRepository: Get.find()), + // ); } } diff --git a/lib/app/features/home/controllers/home_controller.dart b/lib/app/features/home/controllers/home_controller.dart index d9b7880..9387a1a 100644 --- a/lib/app/features/home/controllers/home_controller.dart +++ b/lib/app/features/home/controllers/home_controller.dart @@ -2,14 +2,14 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; import 'package:problem_check_system/app/features/my/views/my_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/problem_list_page.dart'; class HomeController extends GetxController { var selectedIndex = 0.obs; // 页面列表 final List pages = [ const EnterpriseListPage(), - const ProblemPage(), + const ProblemListPage(), const MyPage(), ]; diff --git a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart index 9a7fddc..402e8ac 100644 --- a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart +++ b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart @@ -7,7 +7,7 @@ import 'package:problem_check_system/app/features/enterprise/presentation/contro import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; import 'package:problem_check_system/app/features/home/pages/home_page.dart'; import 'package:problem_check_system/app/features/my/views/my_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/problem_list_page.dart'; class NavigationController extends GetxController { var selectedIndex = 0.obs; @@ -30,7 +30,7 @@ class NavigationController extends GetxController { final List pages = const [ HomePage(), EnterpriseListPage(), - ProblemPage(), + ProblemListPage(), MyPage(), ]; final navigationItems = const [ diff --git a/lib/app/features/problem/data/datasources/problem_local_data_source.dart b/lib/app/features/problem/data/datasources/problem_local_data_source.dart new file mode 100644 index 0000000..b442ec0 --- /dev/null +++ b/lib/app/features/problem/data/datasources/problem_local_data_source.dart @@ -0,0 +1,145 @@ +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/models/sync_status.dart'; // 导入 SyncStatus +import 'package:problem_check_system/app/core/services/database_service.dart'; +import 'package:sqflite/sqflite.dart'; // 导入你的 DatabaseService + +/// IProblemLocalDataSource 定义了问题本地数据源的接口。 +/// 它抽象了所有与本地数据库中 'problems' 表相关的 CRUD (创建, 读取, 更新, 删除) 操作。 +/// 仓库层 (Repository) 将通过这个接口与数据源交互,而无需关心底层的数据库实现 (如 Sqflite)。 +abstract class IProblemLocalDataSource { + /// 根据 ID 从数据库获取一个问题。 + /// + /// [id] - 要查询的问题的唯一标识符。 + /// 返回一个 Map,如果找不到则返回 null。 + Future?> getProblemById(String id); + + /// 从数据库获取所有问题,并可选择性地进行过滤。 + /// + /// [startDate] - 筛选创建时间晚于此日期的问题。 + /// [endDate] - 筛选创建时间早于此日期的问题。 + /// [syncStatus] - 筛选具有特定同步状态的问题。 + /// [bindStatus] - (示例) 筛选具有特定绑定状态的问题。 + Future>> getAllProblems({ + DateTime? startDate, + DateTime? endDate, + String? syncStatus, + String? bindStatus, + }); + + /// 向数据库中添加一个新问题。 + /// + /// [problemMap] - 包含问题数据的 Map,其键应与 'problems' 表的列名匹配。 + Future addProblem(Map problemMap); + + /// 更新数据库中的一个现有问题。 + /// + /// [problemMap] - 包含要更新的问题数据的 Map。它必须包含 'id' 键。 + Future updateProblem(Map problemMap); + + /// 根据 ID 从数据库中删除一个问题。 + /// + /// [id] - 要删除的问题的唯一标识符。 + Future deleteProblem(String id); +} + +// 假设 IProblemLocalDataSource 接口定义在同一个文件中或已导入 + +class ProblemLocalDataSource implements IProblemLocalDataSource { + final DatabaseService _databaseService; + final String _tableName = 'problems'; + + ProblemLocalDataSource({required DatabaseService databaseService}) + : _databaseService = databaseService; + + @override + Future addProblem(Map problemMap) async { + final db = await _databaseService.database; + await db.insert( + _tableName, + problemMap, + // 如果插入的 ID 已存在,则替换它,这使得 add 同时具有 update 的功能,非常稳健。 + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + @override + Future deleteProblem(String id) async { + final db = await _databaseService.database; + await db.delete(_tableName, where: 'id = ?', whereArgs: [id]); + } + + @override + Future>> getAllProblems({ + DateTime? startDate, + DateTime? endDate, + String? syncStatus, + String? bindStatus, // 注意:你的表结构中没有 bindStatus,这里我们忽略它 + }) async { + final db = await _databaseService.database; + + // 动态构建 WHERE 查询语句 + List whereClauses = []; + List whereArgs = []; + + if (startDate != null) { + // 数据库中存储的是 INTEGER (毫秒时间戳) + whereClauses.add('creationTime >= ?'); + whereArgs.add(startDate.millisecondsSinceEpoch); + } + if (endDate != null) { + whereClauses.add('creationTime <= ?'); + whereArgs.add(endDate.millisecondsSinceEpoch); + } + if (syncStatus != null) { + // 数据库中存储的是 INTEGER (枚举的 index) + // 我们需要将仓库层传来的字符串转回枚举的 index + try { + final status = SyncStatus.values.byName(syncStatus); + whereClauses.add('syncStatus = ?'); + whereArgs.add(status.index); + } catch (e) { + // 如果传来一个无效的 status 字符串,则忽略此过滤器 + Get.log('无效的 syncStatus 过滤器: $syncStatus', isError: true); + } + } + + final String? whereString = whereClauses.isEmpty + ? null + : whereClauses.join(' AND '); + + final List> maps = await db.query( + _tableName, + where: whereString, + whereArgs: whereArgs, + orderBy: 'creationTime DESC', // 通常按创建时间降序排列 + ); + return maps; + } + + @override + Future?> getProblemById(String id) async { + final db = await _databaseService.database; + final List> maps = await db.query( + _tableName, + where: 'id = ?', + whereArgs: [id], + limit: 1, // 限制只返回一条记录 + ); + + if (maps.isNotEmpty) { + return maps.first; + } + return null; + } + + @override + Future updateProblem(Map problemMap) async { + final db = await _databaseService.database; + await db.update( + _tableName, + problemMap, + where: 'id = ?', + whereArgs: [problemMap['id']], + ); + } +} diff --git a/lib/app/features/problem/data/datasources/problem_local_datasource.dart b/lib/app/features/problem/data/datasources/problem_local_datasource.dart deleted file mode 100644 index 4edef34..0000000 --- a/lib/app/features/problem/data/datasources/problem_local_datasource.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/core/services/database_service.dart'; -import 'package:sqflite/sqflite.dart'; - -const String _tableName = 'problems'; - -/// 数据源抽象接口 -abstract class ProblemLocalDataSource { - Future insertProblem(Problem problem); - Future updateProblem(Problem problem); - Future deleteProblem(String problemId); - Future getProblemById(String id); - Future> getProblems({ - DateTime? startDate, - DateTime? endDate, - String? syncStatus, - String? bindStatus, - }); -} - -/// 数据源的具体实现 -class ProblemLocalDataSourceImpl implements ProblemLocalDataSource { - final DatabaseService _databaseService; - - ProblemLocalDataSourceImpl({required DatabaseService databaseService}) - : _databaseService = databaseService; - - @override - Future insertProblem(Problem problem) async { - final db = await _databaseService.database; - return await db.insert( - _tableName, - problem.toMap(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - - @override - Future updateProblem(Problem problem) async { - final db = await _databaseService.database; - return await db.update( - _tableName, - problem.toMap(), - where: 'id = ?', - whereArgs: [problem.id], - ); - } - - @override - Future deleteProblem(String problemId) async { - final db = await _databaseService.database; - return await db.delete(_tableName, where: 'id = ?', whereArgs: [problemId]); - } - - @override - Future getProblemById(String id) async { - final db = await _databaseService.database; - final results = await db.query( - _tableName, - where: 'id = ?', - whereArgs: [id], - limit: 1, - ); - return results.isNotEmpty ? Problem.fromMap(results.first) : null; - } - - @override - Future> getProblems({ - DateTime? startDate, - DateTime? endDate, - String? syncStatus, - String? bindStatus, - }) async { - final db = await _databaseService.database; - final whereClauses = []; - final whereArgs = []; - - // 时间范围筛选 - if (startDate != null) { - whereClauses.add('creationTime >= ?'); - whereArgs.add(startDate.millisecondsSinceEpoch); - } - - if (endDate != null) { - whereClauses.add('creationTime <= ?'); - whereArgs.add(endDate.millisecondsSinceEpoch); - } - - // 同步状态筛选 - if (syncStatus != null && syncStatus != '全部') { - if (syncStatus == '未上传') { - whereClauses.add('syncStatus IN (?, ?, ?)'); - whereArgs.addAll([ - ProblemSyncStatus.pendingCreate.index, - ProblemSyncStatus.pendingUpdate.index, - ProblemSyncStatus.pendingDelete.index, - ]); - } else { - whereClauses.add('syncStatus = ?'); - whereArgs.add(ProblemSyncStatus.synced.index); - } - } - - // 绑定状态筛选 - if (bindStatus != null && bindStatus != '全部') { - if (bindStatus == '已绑定') { - whereClauses.add('bindData IS NOT NULL AND bindData != ""'); - } else { - whereClauses.add('(bindData IS NULL OR bindData = "")'); - } - } - - final results = await db.query( - _tableName, - where: whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null, - whereArgs: whereArgs.isEmpty ? null : whereArgs, - orderBy: 'creationTime DESC', - ); - - return results.map((json) => Problem.fromMap(json)).toList(); - } -} diff --git a/lib/app/features/problem/data/datasources/problem_remote_data_source.dart b/lib/app/features/problem/data/datasources/problem_remote_data_source.dart new file mode 100644 index 0000000..5aa6760 --- /dev/null +++ b/lib/app/features/problem/data/datasources/problem_remote_data_source.dart @@ -0,0 +1 @@ +class ProblemRemoteDataSource {} diff --git a/lib/app/features/problem/data/model/problem_local_dto.dart b/lib/app/features/problem/data/model/problem_local_dto.dart new file mode 100644 index 0000000..51d43df --- /dev/null +++ b/lib/app/features/problem/data/model/problem_local_dto.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; + +import 'package:problem_check_system/app/core/models/sync_status.dart'; +import '../../domain/entities/problem_entity.dart'; + +/// ProblemLocalDto (重构版) +/// +/// 这个版本精确匹配了重构后的 `problems` 表结构。 +/// 它负责处理领域实体 (Entity) 与数据库 Map 之间的数据格式转换, +/// 并遵循了将时间和枚举存为可读、健壮的 TEXT 格式的最佳实践。 +class ProblemLocalDto { + final String id; + final String enterpriseId; + final String description; + final String location; + final String imageUrls; + final String creatorId; + final String creationTime; + final String lastModifierId; + final String lastModifiedTime; + final SyncStatus syncStatus; + final String? bindData; + + ProblemLocalDto({ + required this.id, + required this.enterpriseId, + required this.description, + required this.location, + required this.imageUrls, + required this.creatorId, + required this.creationTime, + required this.lastModifierId, + required this.lastModifiedTime, + required this.syncStatus, + required this.bindData, + }); + + Map toMap() { + return { + 'id': id, + 'enterpriseId': enterpriseId, + 'description': description, + 'location': location, + 'imageUrls': imageUrls, + 'creatorId': creatorId, + 'creationTime': creationTime, + 'lastModifierId': lastModifierId, + 'lastModifiedTime': lastModifiedTime, + 'syncStatus': syncStatus.name, + 'bindData': bindData, + }; + } + + factory ProblemLocalDto.fromMap(Map map) { + return ProblemLocalDto( + id: map['id'] as String, + enterpriseId: map['enterpriseId'] as String, + description: map['description'] as String, + location: map['location'] as String, + imageUrls: map['imageUrls'] as String, + creatorId: map['creatorId'] as String, + creationTime: map['creationTime'] as String, + lastModifierId: map['lastModifierId'], + lastModifiedTime: map['lastModifiedTime'] as String, + syncStatus: SyncStatus.values.byName(map['syncStatus'] as String), + bindData: map['bindData'] as String?, + ); + } + + /// 从领域实体 ProblemEntity 创建 DTO 实例。 + factory ProblemLocalDto.fromEntity(ProblemEntity entity) { + return ProblemLocalDto( + id: entity.id, + enterpriseId: entity.enterpriseId, + description: entity.description, + location: entity.location, + imageUrls: jsonEncode(entity.imageUrls), + creatorId: entity.creatorId, + creationTime: entity.creationTime.toUtc().toIso8601String(), + lastModifierId: entity.lastModifierId, + lastModifiedTime: entity.lastModifiedTime.toUtc().toIso8601String(), + syncStatus: entity.syncStatus, + bindData: entity.bindData, + ); + } + + /// 将 DTO 实例转换回领域实体 ProblemEntity。 + ProblemEntity toEntity() { + return ProblemEntity( + id: id, + enterpriseId: enterpriseId, + description: description, + location: location, + imageUrls: List.from(jsonDecode(imageUrls)), + creatorId: creatorId, + creationTime: DateTime.parse(creationTime), + lastModifierId: lastModifierId, + lastModifiedTime: DateTime.parse(lastModifiedTime), + syncStatus: syncStatus, + bindData: bindData, + ); + } +} diff --git a/lib/app/features/problem/data/model/problem_remote_dto.dart b/lib/app/features/problem/data/model/problem_remote_dto.dart new file mode 100644 index 0000000..e03862e --- /dev/null +++ b/lib/app/features/problem/data/model/problem_remote_dto.dart @@ -0,0 +1,144 @@ +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, + ); + } +} diff --git a/lib/app/features/problem/data/repositories/problem_repository.dart b/lib/app/features/problem/data/repositories/problem_repository.dart index 299edbb..34afd80 100644 --- a/lib/app/features/problem/data/repositories/problem_repository.dart +++ b/lib/app/features/problem/data/repositories/problem_repository.dart @@ -1,139 +1,139 @@ -import 'package:dio/dio.dart'; -import 'package:get/get.dart' hide MultipartFile, FormData, Response; -import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; -import 'package:problem_check_system/app/core/utils/constants/api_endpoints.dart'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/core/models/server_problem.dart'; -import 'package:problem_check_system/app/core/services/network_status_service.dart'; -import 'package:problem_check_system/app/core/services/http_provider.dart'; -import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; -import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_datasource.dart'; +// import 'package:dio/dio.dart'; +// import 'package:get/get.dart' hide MultipartFile, FormData, Response; +// import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; +// import 'package:problem_check_system/app/core/utils/constants/api_endpoints.dart'; +// import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; +// import 'package:problem_check_system/app/core/models/server_problem.dart'; +// import 'package:problem_check_system/app/core/services/network_status_service.dart'; +// import 'package:problem_check_system/app/core/services/http_provider.dart'; +// import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; +// import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart'; -/// 问题仓库,负责处理问题数据的本地持久化。 -/// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 -class ProblemRepository extends GetxService { - final ProblemLocalDataSource problemLocalDataSource; - final HttpProvider httpProvider; - final NetworkStatusService networkStatusService; - final AuthRepository authRepository; +// /// 问题仓库,负责处理问题数据的本地持久化。 +// /// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 +// class ProblemRepository extends GetxService { +// final ProblemLocalDataSource problemLocalDataSource; +// final HttpProvider httpProvider; +// final NetworkStatusService networkStatusService; +// final AuthRepository authRepository; - RxBool get isOnline => networkStatusService.isOnline; +// RxBool get isOnline => networkStatusService.isOnline; - ProblemRepository({ - required this.problemLocalDataSource, - required this.httpProvider, - required this.networkStatusService, - required this.authRepository, - }); +// ProblemRepository({ +// required this.problemLocalDataSource, +// required this.httpProvider, +// required this.networkStatusService, +// required this.authRepository, +// }); - /// 更新本地数据库中的一个问题。 - Future updateProblem(Problem problem) async { - await problemLocalDataSource.updateProblem(problem); - } +// /// 更新本地数据库中的一个问题。 +// Future updateProblem(Problem problem) async { +// await problemLocalDataSource.updateProblem(problem); +// } - /// 通用查询方法,根据可选的筛选条件获取问题列表。 - /// - `startDate`/`endDate`:筛选创建时间范围。 - /// - `syncStatus`:筛选上传状态('已上传', '未上传', '全部')。 - /// - `bindStatus`:筛选绑定状态('已绑定', '未绑定', '全部')。 - Future getProblems({ - DateTime? startDate, - DateTime? endDate, - String? syncStatus, - String? bindStatus, - }) async { - return await problemLocalDataSource.getProblems( - startDate: startDate, - endDate: endDate, - syncStatus: syncStatus, - bindStatus: bindStatus, - ); - } +// /// 通用查询方法,根据可选的筛选条件获取问题列表。 +// /// - `startDate`/`endDate`:筛选创建时间范围。 +// /// - `syncStatus`:筛选上传状态('已上传', '未上传', '全部')。 +// /// - `bindStatus`:筛选绑定状态('已绑定', '未绑定', '全部')。 +// Future getProblems({ +// DateTime? startDate, +// DateTime? endDate, +// String? syncStatus, +// String? bindStatus, +// }) async { +// return await problemLocalDataSource.getProblems( +// startDate: startDate, +// endDate: endDate, +// syncStatus: syncStatus, +// bindStatus: bindStatus, +// ); +// } - Future insertProblem(Problem problem) async { - await problemLocalDataSource.insertProblem(problem); - } +// Future insertProblem(Problem problem) async { +// await problemLocalDataSource.insertProblem(problem); +// } - Future deleteProblem(String problemId) async { - await problemLocalDataSource.deleteProblem(problemId); - } +// Future deleteProblem(String problemId) async { +// await problemLocalDataSource.deleteProblem(problemId); +// } - // 在ProblemRepository中添加 - Future> fetchProblemsFromServer({ - DateTime? startTime, - DateTime? endTime, - int? pageNumber, - int? pageSize, - CancelToken? cancelToken, - }) async { - try { - final response = await httpProvider.get( - ApiEndpoints.getProblems, - queryParameters: { - 'creatorId': authRepository.getUserId(), - if (startTime != null) - 'StartTime': startTime.toUtc().toIso8601String(), - if (endTime != null) 'EndTime': endTime.toUtc().toIso8601String(), - if (pageNumber != null) 'pageNumber': pageNumber, - if (pageSize != null) 'pageSize': pageSize, - }, - cancelToken: cancelToken, - ); +// // 在ProblemRepository中添加 +// Future> fetchProblemsFromServer({ +// DateTime? startTime, +// DateTime? endTime, +// int? pageNumber, +// int? pageSize, +// CancelToken? cancelToken, +// }) async { +// try { +// final response = await httpProvider.get( +// ApiEndpoints.getProblems, +// queryParameters: { +// 'creatorId': authRepository.getUserId(), +// if (startTime != null) +// 'StartTime': startTime.toUtc().toIso8601String(), +// if (endTime != null) 'EndTime': endTime.toUtc().toIso8601String(), +// if (pageNumber != null) 'pageNumber': pageNumber, +// if (pageSize != null) 'pageSize': pageSize, +// }, +// cancelToken: cancelToken, +// ); - if (response.isSuccess) { - // Dio 会自动解析 JSON,response.data 已经是 Map 或 List - final Map data = response.data; - final List items = data['items']; +// if (response.isSuccess) { +// // Dio 会自动解析 JSON,response.data 已经是 Map 或 List +// final Map data = response.data; +// final List items = data['items']; - // 使用 Freezed 生成的 fromJson 方法 - return items.map((item) => ServerProblem.fromJson(item)).toList(); - } else { - throw Exception('拉取问题失败: ${response.statusCode}'); - } - } on DioException catch (e) { - Get.log("Dio 异常$e"); - rethrow; - } catch (e) { - Get.log("解析失败:$e"); - rethrow; - } - } +// // 使用 Freezed 生成的 fromJson 方法 +// return items.map((item) => ServerProblem.fromJson(item)).toList(); +// } else { +// throw Exception('拉取问题失败: ${response.statusCode}'); +// } +// } on DioException catch (e) { +// Get.log("Dio 异常$e"); +// rethrow; +// } catch (e) { +// Get.log("解析失败:$e"); +// rethrow; +// } +// } - /// post - Future post( - Map apiPayload, - CancelToken cancelToken, - ) async { - // 3. 发送给服务器 - final response = await httpProvider.post( - ApiEndpoints.postProblem, - data: apiPayload, - cancelToken: cancelToken, - ); - return response; - } +// /// post +// Future post( +// Map apiPayload, +// CancelToken cancelToken, +// ) async { +// // 3. 发送给服务器 +// final response = await httpProvider.post( +// ApiEndpoints.postProblem, +// data: apiPayload, +// cancelToken: cancelToken, +// ); +// return response; +// } - /// put - Future put( - String id, - Map apiPayload, - CancelToken cancelToken, - ) async { - // 3. 发送给服务器 - final response = await httpProvider.put( - ApiEndpoints.putProblemById(id), - data: apiPayload, - cancelToken: cancelToken, - ); - return response; - } +// /// put +// Future put( +// String id, +// Map apiPayload, +// CancelToken cancelToken, +// ) async { +// // 3. 发送给服务器 +// final response = await httpProvider.put( +// ApiEndpoints.putProblemById(id), +// data: apiPayload, +// cancelToken: cancelToken, +// ); +// return response; +// } - /// delete - Future delete(String id, CancelToken cancelToken) async { - // 3. 发送给服务器 - final response = await httpProvider.delete( - ApiEndpoints.deleteProblemById(id), - cancelToken: cancelToken, - ); - return response; - } -} +// /// delete +// Future delete(String id, CancelToken cancelToken) async { +// // 3. 发送给服务器 +// final response = await httpProvider.delete( +// ApiEndpoints.deleteProblemById(id), +// cancelToken: cancelToken, +// ); +// return response; +// } +// } diff --git a/lib/app/features/problem/data/repositories/problem_repository_impl.dart b/lib/app/features/problem/data/repositories/problem_repository_impl.dart new file mode 100644 index 0000000..f60b9e2 --- /dev/null +++ b/lib/app/features/problem/data/repositories/problem_repository_impl.dart @@ -0,0 +1,81 @@ +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_local_dto.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'; + +/// 问题仓库,负责处理问题数据的本地持久化。 +/// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 +/// 它的核心工作是在领域实体 (ProblemEntity) 和数据传输对象 (ProblemLocalDto) 之间进行转换。 +class ProblemRepository implements IProblemRepository { + final IProblemLocalDataSource problemLocalDataSource; // 2. 依赖于数据源的抽象 + + ProblemRepository(this.problemLocalDataSource); + + @override + Future addProblem(ProblemEntity problem) async { + // 1. 将领域实体 (Entity) 转换为本地数据传输对象 (DTO) + final problemDto = ProblemLocalDto.fromEntity(problem); + + // 2. 调用数据源的方法,将 DTO 转换为 Map 进行存储 + await problemLocalDataSource.addProblem(problemDto.toMap()); + + // 3. 操作成功后,返回传入的实体,确认操作完成 + return problem; + } + + @override + Future deleteProblem(String id) async { + // 直接将 ID 传递给数据源进行删除操作 + await problemLocalDataSource.deleteProblem(id); + } + + @override + Future> getAllProblems({ + DateTime? startDate, + DateTime? endDate, + String? syncStatus, + String? bindStatus, + }) async { + // 1. 调用数据源获取所有问题的原始数据 (List of Maps) + final problemMaps = await problemLocalDataSource.getAllProblems( + // 将参数直接透传给数据源 + startDate: startDate, + endDate: endDate, + syncStatus: syncStatus, + bindStatus: bindStatus, + ); + + // 2. 将每个 Map 转换为 DTO,然后再转换为领域实体 (Entity) + final problems = problemMaps + .map((map) => ProblemLocalDto.fromMap(map).toEntity()) + .toList(); + + return problems; + } + + @override + Future getProblemById(String id) async { + // 1. 调用数据源通过 ID 获取问题的原始数据 (Map) + final problemMap = await problemLocalDataSource.getProblemById(id); + + // 2. 如果数据不存在,则返回 null + if (problemMap == null) { + return null; + } + + // 3. 如果数据存在,则先转换为 DTO,再转换为领域实体并返回 + return ProblemLocalDto.fromMap(problemMap).toEntity(); + } + + @override + Future updateProblem(ProblemEntity problem) async { + // 1. 将领域实体 (Entity) 转换为本地数据传输对象 (DTO) + final problemDto = ProblemLocalDto.fromEntity(problem); + + // 2. 调用数据源的方法,将 DTO 转换为 Map 进行更新 + await problemLocalDataSource.updateProblem(problemDto.toMap()); + + // 3. 操作成功后,返回传入的实体,确认操作完成 + return problem; + } +} diff --git a/lib/app/features/problem/domain/entities/problem_entity.dart b/lib/app/features/problem/domain/entities/problem_entity.dart index 1539219..bd1802f 100644 --- a/lib/app/features/problem/domain/entities/problem_entity.dart +++ b/lib/app/features/problem/domain/entities/problem_entity.dart @@ -6,19 +6,74 @@ class ProblemEntity implements SyncableEntity { final String id; @override - final DateTime? lastModifiedTime; + final DateTime lastModifiedTime; @override - final String? lastModifierId; + final String lastModifierId; @override final SyncStatus syncStatus; - // 2. 在构造函数中初始化这些字段 + /// 企业名称 + final String enterpriseId; + + /// 问题描述 + final String description; + + /// 所在位置 + final String location; + + /// 问题图片 + final List imageUrls; + + /// 创建时间 + final DateTime creationTime; + + /// 创建人 + final String creatorId; + + /// 绑定信息 + final String? bindData; + ProblemEntity({ required this.id, - this.lastModifiedTime, - this.lastModifierId, - this.syncStatus = SyncStatus.pendingCreate, + required this.lastModifiedTime, + required this.lastModifierId, + required this.syncStatus, + required this.enterpriseId, + required this.description, + required this.location, + required this.imageUrls, + required this.creationTime, + required this.creatorId, + this.bindData, }); + + ProblemEntity copyWith({ + String? id, + DateTime? lastModifiedTime, + String? lastModifierId, + SyncStatus? syncStatus, + String? enterpriseId, + String? description, + String? location, + List? imageUrls, + DateTime? creationTime, + String? creatorId, + String? bindData, + }) { + return ProblemEntity( + id: id ?? this.id, + syncStatus: syncStatus ?? this.syncStatus, + lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime, + lastModifierId: lastModifierId ?? this.lastModifierId, + enterpriseId: enterpriseId ?? this.enterpriseId, + description: description ?? this.description, + location: location ?? this.location, + imageUrls: imageUrls ?? this.imageUrls, + creationTime: creationTime ?? this.creationTime, + creatorId: creatorId ?? this.creatorId, + bindData: bindData ?? this.bindData, + ); + } } diff --git a/lib/app/features/problem/domain/repositoies/problem_repository.dart b/lib/app/features/problem/domain/repositoies/problem_repository.dart deleted file mode 100644 index fa4b59f..0000000 --- a/lib/app/features/problem/domain/repositoies/problem_repository.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; - -/// Problem 仓库的抽象接口 -/// 定义了业务逻辑层需要的数据操作。 -abstract class ProblemRepository { - Future addProblem(Problem problem); - Future updateProblem(Problem problem); - Future deleteProblem(String problemId); - Future getProblemById(String id); - - Future markAsSynced(String id); - - Future> getProblems({ - DateTime? startDate, - DateTime? endDate, - String? syncStatus, // 业务逻辑层使用字符串,更直观 - String? bindStatus, - }); -} diff --git a/lib/app/features/problem/domain/repositories/problem_repository.dart b/lib/app/features/problem/domain/repositories/problem_repository.dart new file mode 100644 index 0000000..478547c --- /dev/null +++ b/lib/app/features/problem/domain/repositories/problem_repository.dart @@ -0,0 +1,16 @@ +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; + +/// Problem 仓库的抽象接口 +/// 定义了业务逻辑层需要的数据操作。 +abstract class IProblemRepository { + Future> getAllProblems({ + DateTime? startDate, + DateTime? endDate, + String? syncStatus, + String? bindStatus, + }); + Future getProblemById(String id); + Future addProblem(ProblemEntity problem); + Future updateProblem(ProblemEntity problem); + Future deleteProblem(String id); +} diff --git a/lib/app/features/problem/domain/usecases/add_problem_usecase.dart b/lib/app/features/problem/domain/usecases/add_problem_usecase.dart new file mode 100644 index 0000000..da8afa2 --- /dev/null +++ b/lib/app/features/problem/domain/usecases/add_problem_usecase.dart @@ -0,0 +1,40 @@ +import 'package:problem_check_system/app/core/models/sync_status.dart'; +import 'package:problem_check_system/app/core/repositories/auth_repository.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:uuid/uuid.dart'; + +class AddProblemUsecase { + final IProblemRepository problemRepository; + final AuthRepository authRepository; + final Uuid uuid; + + AddProblemUsecase({ + required this.problemRepository, + required this.authRepository, + required this.uuid, + }); + + Future call({ + required String enterpriseId, + required String description, + required String location, + required List imageUrls, + }) async { + final nowUtc = DateTime.now().toUtc(); + final userId = authRepository.getUserId(); + final newProblem = ProblemEntity( + id: uuid.v4(), + description: description, + location: location, + imageUrls: imageUrls, + lastModifiedTime: nowUtc, + lastModifierId: userId, + syncStatus: SyncStatus.pendingCreate, + enterpriseId: enterpriseId, + creationTime: nowUtc, + creatorId: userId, + ); + return await problemRepository.addProblem(newProblem); + } +} diff --git a/lib/app/features/problem/domain/usecases/delete_problem.dart b/lib/app/features/problem/domain/usecases/delete_problem.dart new file mode 100644 index 0000000..391d95f --- /dev/null +++ b/lib/app/features/problem/domain/usecases/delete_problem.dart @@ -0,0 +1,10 @@ +import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; + +class DeleteProblem { + final IProblemRepository problemRepository; + + DeleteProblem({required this.problemRepository}); + Future call(String id) async { + await problemRepository.deleteProblem(id); + } +} diff --git a/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart b/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart new file mode 100644 index 0000000..62c81e9 --- /dev/null +++ b/lib/app/features/problem/domain/usecases/get_all_problems_usecase.dart @@ -0,0 +1,22 @@ +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'; + +class GetAllProblemsUsecase { + final IProblemRepository problemRepository; + + GetAllProblemsUsecase({required this.problemRepository}); + + Future> call({ + DateTime? startDate, + DateTime? endDate, + String? syncStatus, + String? bindStatus, + }) async { + return await problemRepository.getAllProblems( + startDate: startDate, + endDate: endDate, + syncStatus: syncStatus, + bindStatus: bindStatus, + ); + } +} diff --git a/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart b/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart new file mode 100644 index 0000000..459a3db --- /dev/null +++ b/lib/app/features/problem/domain/usecases/get_problem_by_id_usecase.dart @@ -0,0 +1,11 @@ +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'; + +class GetProblemByIdUsecase { + final IProblemRepository problemRepository; + + GetProblemByIdUsecase({required this.problemRepository}); + Future call(String id) async { + return await problemRepository.getProblemById(id); + } +} diff --git a/lib/app/features/problem/domain/usecases/update_problem.dart b/lib/app/features/problem/domain/usecases/update_problem.dart new file mode 100644 index 0000000..8a1c4f4 --- /dev/null +++ b/lib/app/features/problem/domain/usecases/update_problem.dart @@ -0,0 +1,22 @@ +import 'package:problem_check_system/app/core/models/sync_status.dart'; +import 'package:problem_check_system/app/core/repositories/auth_repository.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'; + +class UpdateProblem { + final IProblemRepository repository; + final AuthRepository authRepository; + + UpdateProblem({required this.repository, required this.authRepository}); + + Future call(ProblemEntity entity) async { + final nowUtc = DateTime.now().toUtc(); + final userId = authRepository.getUserId(); + final newProblem = entity.copyWith( + lastModifiedTime: nowUtc, + lastModifierId: userId, + syncStatus: SyncStatus.pendingUpdate, + ); + return await repository.updateProblem(newProblem); + } +} diff --git a/lib/app/features/problem/presentation/bindings/problem_binding.dart b/lib/app/features/problem/presentation/bindings/problem_binding.dart index 6ef7436..62915ba 100644 --- a/lib/app/features/problem/presentation/bindings/problem_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_binding.dart @@ -1,37 +1,29 @@ -import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/core/services/database_service.dart'; -import 'package:problem_check_system/app/core/services/http_provider.dart'; -import 'package:problem_check_system/app/core/services/network_status_service.dart'; -import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_datasource.dart'; -import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; +// import 'package:get/get.dart'; +// import 'package:problem_check_system/app/core/models/problem_sync_status.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/repositories/problem_repository_impl.dart'; +// import 'package:problem_check_system/app/features/problem/domain/repositories/problem_repository.dart'; -class ProblemBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => ProblemLocalDataSourceImpl( - databaseService: Get.find(), - ), - ); - Get.lazyPut( - () => ProblemRepository( - problemLocalDataSource: Get.find(), - httpProvider: Get.find(), - networkStatusService: Get.find(), - authRepository: Get.find(), - ), - ); - Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); +// class ProblemBinding extends Bindings { +// @override +// void dependencies() { +// Get.lazyPut( +// () => +// ProblemLocalDataSource(databaseService: Get.find()), +// ); +// Get.lazyPut( +// () => ProblemRepository(Get.find()), +// ); +// Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); - /// 注册问题控制器 - Get.lazyPut( - () => ProblemController( - problemRepository: Get.find(), - problemStateManager: Get.find(), - ), - fenix: true, - ); - } -} +// /// 注册问题控制器 +// Get.lazyPut( +// () => ProblemController( +// problemRepository: Get.find(), +// problemStateManager: Get.find(), +// ), +// fenix: true, +// ); +// } +// } diff --git a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart index 2404e65..42ddb5e 100644 --- a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart @@ -1,27 +1,33 @@ import 'package:get/get.dart'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_form_controller.dart'; class ProblemFormBinding extends Bindings { @override void dependencies() { - final dynamic arguments = Get.arguments; - final bool readOnly = Get.parameters['isReadOnly'] == 'true'; + // final dynamic arguments = Get.arguments; + // final bool readOnly = Get.parameters['isReadOnly'] == 'true'; - Problem? problem; - if (arguments != null && arguments is Problem) { - problem = arguments; + ProblemEntity? problem; + FormMode formMode = FormMode.view; + + if (Get.arguments is Map) { + final arguments = Get.arguments as Map; + + // 设置模式 + if (arguments.containsKey('mode')) { + formMode = arguments['mode'] as FormMode; + } + + // 如果是编辑或查看模式,需要填充数据 + if (arguments.containsKey('data')) { + problem = arguments['data'] as ProblemEntity?; + } } - Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); Get.lazyPut( - () => ProblemFormController( - problemRepository: Get.find(), - problemStateManager: Get.find(), - problem: problem, - isReadOnly: readOnly, - ), + () => ProblemFormController(problem: problem, formMode: formMode), ); } } diff --git a/lib/app/features/problem/presentation/bindings/problem_list_binding.dart b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart new file mode 100644 index 0000000..2ad2e47 --- /dev/null +++ b/lib/app/features/problem/presentation/bindings/problem_list_binding.dart @@ -0,0 +1,35 @@ +import 'package:get/get.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/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/presentation/controllers/problem_list_controller.dart'; + +class ProblemListBinding extends Bindings { + @override + void dependencies() { + // 数据 + Get.lazyPut( + () => + ProblemLocalDataSource(databaseService: Get.find()), + ); + // 仓库 + Get.lazyPut( + () => ProblemRepository(Get.find()), + ); + // 用例 + Get.lazyPut( + () => GetAllProblemsUsecase( + problemRepository: Get.find(), + ), + ); + + /// 控制器 + Get.lazyPut( + () => ProblemListController( + getAllProblemsUsecase: Get.find(), + ), + ); + } +} diff --git a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart new file mode 100644 index 0000000..3c2dc69 --- /dev/null +++ b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart @@ -0,0 +1,8 @@ +import 'package:get/get.dart'; + +class ProblemUploadBinding extends Bindings { + @override + void dependencies() { + // TODO: implement dependencies + } +} diff --git a/lib/app/features/problem/presentation/controllers/problem_controller.dart b/lib/app/features/problem/presentation/controllers/problem_controller.dart index ed53478..3967c94 100644 --- a/lib/app/features/problem/presentation/controllers/problem_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_controller.dart @@ -1,973 +1,973 @@ -// modules/problem/controllers/problem_controller.dart -import 'dart:developer'; -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart' hide MultipartFile, FormData, Response; -import 'package:flutter/material.dart'; -import 'package:problem_check_system/app/core/routes/app_routes.dart'; -import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; -import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; -import 'package:problem_check_system/app/core/models/image_status.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/core/models/server_problem.dart'; -import 'package:problem_check_system/app/core/repositories/file_repository.dart'; -import 'package:problem_check_system/app/core/repositories/image_repository.dart'; -import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/sync_progress_state.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/date_range_enum.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/dropdown_option.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/sync_progress_dialog.dart'; - -class ProblemController extends GetxController - with GetSingleTickerProviderStateMixin { - /// 依赖问题数据 - final ProblemRepository problemRepository; - final ProblemStateManager problemStateManager; - final FileRepository fileRepository = Get.find(); - - /// 最近问题列表 - final RxList problems = [].obs; - - /// 历史问题列表 - final RxList historyProblems = [].obs; - - /// 未上传的问题列表 - final RxList unUploadedProblems = [].obs; - final Rx allSelected = false.obs; - final RxDouble uploadProgress = 0.0.obs; - // Dio 的取消令牌,用于取消正在进行的请求 - late CancelToken _cancelToken; - - final RxSet _selectedProblems = {}.obs; - - Set get selectedProblems => _selectedProblems; - - int get selectedCount => _selectedProblems.length; - - /// 选中未上传的数量 - int get selectedUnUploadCount => _selectedProblems - .where((p) => p.syncStatus != ProblemSyncStatus.synced) - .length; - - // 在 ProblemController 中添加 - // 添加日期范围选项列表 - List get dateRangeOptions { - return DateRange.values.map((range) => range.toDropdownOption()).toList(); - } - - final List uploadOptions = const [ - DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), - DropdownOption(label: '已上传', value: '已上传', icon: Icons.cloud_done), - DropdownOption(label: '未上传', value: '未上传', icon: Icons.cloud_off), - ]; - - final List bindOptions = const [ - DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), - DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link), - DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off), - ]; - - final Rx currentDateRange = DateRange.oneWeek.obs; - final RxString currentUploadFilter = '全部'.obs; - final RxString currentBindFilter = '全部'.obs; - - // 历史问题列表筛选条件 - final Rx historyStartTime = DateTime.now() - .subtract(const Duration(days: 365)) - .obs; - final Rx historyEndTime = DateTime( - DateTime.now().year, - DateTime.now().month, - DateTime.now().day, - 23, - 59, - 59, - 999, - ).obs; - final RxString historyUploadFilter = '全部'.obs; - final RxString historyBindFilter = '全部'.obs; - - /// 是否加载中 - final RxBool isLoading = false.obs; - - late TabController tabController; - - /// floatingButton 拖动 - final double _fabSize = 56.0; - final double _edgePaddingX = 27.0.w; - final double _edgePaddingY = 111.0.h; - final fabUploadPosition = Offset(337.0, 703.7).obs; - - /// get 选中的 - RxBool get isOnline => problemRepository.isOnline; - - ProblemController({ - required this.problemRepository, - required this.problemStateManager, - }); - - @override - void onInit() { - super.onInit(); - - tabController = TabController(length: 2, vsync: this); - tabController.addListener(_onTabChanged); - loadProblems(); - // 查询未上传问题 - // loadUnUploadedProblems(); - } - - @override - void onClose() { - tabController.dispose(); - super.onClose(); - } - - // #region 问题上传 - - void updateProblemSelection(Problem problem, bool isChecked) { - if (isChecked) { - _selectedProblems.add(problem); - } else { - _selectedProblems.remove(problem); - } - // 更新全选状态 - allSelected.value = _selectedProblems.length == unUploadedProblems.length; - } - - void selectAll() { - if (allSelected.value) { - // 如果已经是全选,则取消全选 - _selectedProblems.clear(); - } else { - // 如果是取消全选,则选择所有 - _selectedProblems.addAll(unUploadedProblems); - } - allSelected.value = !allSelected.value; - } - - // 上传完成后清空选中状态 - void clearSelection() { - _selectedProblems.clear(); - allSelected.value = false; - } - - // 在 handleUpload 方法中,上传完成后调用 clearSelection - Future handleUpload() async { - if (_selectedProblems.isEmpty) { - Get.snackbar('提示', '请选择要上传的问题'); - return; - } - - uploadProgress.value = 0.0; - _cancelToken = CancelToken(); - - showUploadProgressDialog(); - - try { - await uploadProblems( - _selectedProblems.toList(), // 转换为列表 - cancelToken: _cancelToken, - onProgress: (progress) { - uploadProgress.value = progress; - }, - ); - - Get.back(); - Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.TOP); - - // 上传成功后清空选中状态 - clearSelection(); - // 重新加载未上传的问题列表 - loadUnUploadedProblems(); - // 重新加载problems - loadProblems(); - } on DioException catch (e) { - Get.back(); - if (CancelToken.isCancel(e)) { - Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.TOP); - } else { - Get.snackbar( - '上传失败', - '错误: ${e.message}', - snackPosition: SnackPosition.TOP, - ); - } - } catch (e) { - Get.back(); - Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.TOP); - } - } - - /// 显示上传对话框 - void showUploadProgressDialog() { - // 显示对话框 - Get.defaultDialog( - title: '上传问题中...', - content: Obx(() { - // final progress = (uploadProgress.value * 100).toInt(); - return Column( - // mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 16.h), - LinearProgressIndicator( - value: uploadProgress.value, - backgroundColor: Colors.grey[300], - valueColor: const AlwaysStoppedAnimation(Colors.blue), - ), - SizedBox(height: 16.h), - // Text('已完成: $progress%'), - Text('已上传: $selectedUnUploadCount / $selectedCount'), - ], - ); - }), - barrierDismissible: false, // 防止用户点击外部关闭对话框 - // 添加一个 "取消" 按钮 - cancel: ElevatedButton( - onPressed: () { - // 调用 controller 中的取消方法 - cancelUpload(); - }, - child: Text('取消', style: TextStyle(color: Colors.red)), - ), - ); - // 启动上传逻辑 - // ... - } - - void cancelUpload() { - // 在这里实现你的取消逻辑 - // 1. 停止上传任务(例如,取消 HTTP 请求) - // 2. 将上传进度重置为0 - uploadProgress.value = 0.0; - // 3. 关闭对话框 - Get.back(); - // 4. 可以添加一个提示,例如: - // Get.snackbar('提示', '上传已取消'); - } - - /// 新增:上传问题列表。 - /// 遍历问题列表,并计算总进度。 - Future uploadProblems( - List problems, { - required CancelToken cancelToken, - required void Function(double progress) onProgress, - }) async { - final int totalProblems = problems.length; - // final List updatedProblems = []; - - try { - for (int i = 0; i < totalProblems; i++) { - // 如果取消令牌被触发,停止并返回 - if (cancelToken.isCancelled) { - break; - } - - final problemToUpload = problems[i]; - - // 传递一个子进度回调,用于计算单个问题的进度 - final updatedProblem = await uploadProblem( - problemToUpload, - cancelToken: cancelToken, - onProgress: (progress) { - // 计算总体进度:(已完成的问题数 + 当前问题的进度) / 总问题数 - final overallProgress = (i + progress) / totalProblems; - onProgress(overallProgress); - }, - ); - - if (updatedProblem.syncStatus == ProblemSyncStatus.untracked) { - problemRepository.deleteProblem(updatedProblem.id); - } else { - problemRepository.updateProblem(updatedProblem); - } - } - // return updatedProblems; - } on DioException { - rethrow; - } - } - - /// 上传单个问题及其所有关联的图片。 - /// 上传单个问题及其所有关联的图片,根据操作类型执行不同逻辑 - Future uploadProblem( - Problem problem, { - required CancelToken cancelToken, - required void Function(double progress) onProgress, - }) async { - try { - // 检查操作类型有效性 - if (problem.syncStatus == ProblemSyncStatus.synced || - problem.syncStatus == ProblemSyncStatus.untracked) { - throw Exception('问题已同步,无需再次同步'); - } - - // 1. 上传图片(仅对创建和更新操作) - final List remoteUrls = []; - if (problem.syncStatus != ProblemSyncStatus.pendingDelete) { - final newImages = problem.imageUrls - .where((img) => img.status == ImageStatus.pendingUpload) - .toList(); - - final totalFilesToUpload = newImages.length; - int filesUploadedCount = 0; - - for (var image in newImages) { - if (cancelToken.isCancelled) { - throw DioException( - requestOptions: RequestOptions(path: ''), - type: DioExceptionType.cancel, - error: '上传已取消', - ); - } - - final url = await fileRepository.uploadImage( - image.localPath, - cancelToken: cancelToken, - onSendProgress: (sent, total) { - double overallProgress = - (filesUploadedCount + (sent / total)) / totalFilesToUpload; - onProgress(overallProgress); - }, - ); - remoteUrls.add(url); - filesUploadedCount++; - } - onProgress(1.0); - } - - // 2. 构建 API payload(删除操作不需要完整payload) - final apiPayload = problem.syncStatus != ProblemSyncStatus.pendingDelete - ? { - 'id': problem.id, - 'title': problem.description, - 'location': problem.location, - 'imageUrls': _buildFinalRemoteUrls(problem.imageUrls, remoteUrls), - 'creationTime': problem.creationTime.toUtc().toIso8601String(), - } - : null; - - // 3. 根据操作类型调用不同的API - late final Response response; - - switch (problem.syncStatus) { - case ProblemSyncStatus.untracked: - case ProblemSyncStatus.synced: - throw Exception('无效的操作类型: none'); - case ProblemSyncStatus.pendingCreate: - response = await problemRepository.post(apiPayload!, cancelToken); - break; - case ProblemSyncStatus.pendingUpdate: - response = await problemRepository.put( - problem.id, - apiPayload!, - cancelToken, - ); - break; - case ProblemSyncStatus.pendingDelete: - response = await problemRepository.delete(problem.id, cancelToken); - break; - } - - // 4. 处理服务器响应 - if (response.isSuccess) { - if (problem.syncStatus != ProblemSyncStatus.pendingDelete) { - final serverProblem = ServerProblem.fromJson(response.data); - // 更新图片状态(仅对创建和更新操作) - final updatedImageMetadata = _updateImageMetadata( - problem.imageUrls, - remoteUrls, - ); - // 返回同步完成的对象,操作类型重置为none - return problem.copyWith( - syncStatus: ProblemSyncStatus.synced, - imageUrls: updatedImageMetadata, - lastModifiedTime: serverProblem.lastModificationTime, - ); - } else { - // 删除逻辑 - return problem.copyWith(syncStatus: ProblemSyncStatus.untracked); - } - } else { - throw Exception('操作失败,状态码: ${response.statusCode}'); - } - } on DioException { - rethrow; - } - } - - /// 辅助方法:构建最终的远程URL列表 - List _buildFinalRemoteUrls( - List images, - List newRemoteUrls, - ) { - final List finalRemoteUrls = []; - int newImageIndex = 0; - - for (var image in images) { - if (image.status == ImageStatus.synced) { - finalRemoteUrls.add(image.remoteUrl!); - } else if (image.status == ImageStatus.pendingUpload) { - finalRemoteUrls.add(newRemoteUrls[newImageIndex]); - newImageIndex++; - } - } - - return finalRemoteUrls; - } - - /// 辅助方法:更新图片元数据状态 - List _updateImageMetadata( - List images, - List newRemoteUrls, - ) { - final List updatedImageMetadata = []; - int uploadedUrlIndex = 0; - - for (var image in images) { - if (image.status == ImageStatus.pendingUpload) { - updatedImageMetadata.add( - ImageMetadata( - localPath: image.localPath, - remoteUrl: newRemoteUrls[uploadedUrlIndex], - status: ImageStatus.synced, - ), - ); - uploadedUrlIndex++; - } else { - updatedImageMetadata.add(image); - } - } - - return updatedImageMetadata; - } - - // #endregion - - // #region 问题同步 - - final SyncProgressState syncProgress = SyncProgressState(); - - Future pullDataFromServer() async { - isLoading.value = true; - - // 显示进度对话框 - Get.dialog( - SyncProgressDialog(progressState: syncProgress), - barrierDismissible: false, - ); - - try { - const int totalSteps = 5; - syncProgress.startSync(totalSteps); - - // 1. 从服务器获取最新数据 - syncProgress.updateProgress('正在从服务器获取数据...', 1); - final List serverProblems = await problemRepository - .fetchProblemsFromServer(pageNumber: 1, pageSize: 99); - - // 2. 获取本地数据 - syncProgress.updateProgress('正在获取本地数据...', 2); - final List localProblems = await problemRepository.getProblems(); - - // 3. 同步策略:以服务器数据为准,保留本地未同步的更改 - syncProgress.updateProgress('正在同步数据...', 3); - final List downloadedProblems = await _syncProblems( - serverProblems, - localProblems, - ); - - // 4. 启动图片下载任务 - syncProgress.updateProgress('正在下载图片...', 4); - await downloadImagesForProblems(downloadedProblems); - - // 5. 重新加载本地问题列表 - syncProgress.updateProgress('正在重新加载数据...', 5); - await loadProblems(); - - syncProgress.completeSync(); - - // 明确关闭对话框 - Get.back(closeOverlays: true); - - Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); - } catch (e) { - syncProgress.errorSync(e.toString()); - - // 错误时也确保关闭 - Get.back(closeOverlays: true); - Get.log('错误: $e'); - Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP); - } finally { - isLoading.value = false; - } - } - - /// 异步下载问题的图片,返回下载完成的任务 - Future downloadImagesForProblems(List problems) async { - if (problems.isEmpty) return; - - final imageRepository = Get.find(); - final List> downloadFutures = []; - - for (final problem in problems) { - // 为每个问题创建下载任务 - final downloadFuture = _downloadProblemImages(problem, imageRepository); - downloadFutures.add(downloadFuture); - } - - // 等待所有问题图片下载完成 - await Future.wait(downloadFutures); - } - - /// 下载单个问题的所有图片 - Future _downloadProblemImages( - Problem problem, - ImageRepository imageRepository, - ) async { - try { - final List downloadedImages = []; - final List> imageFutures = []; - - for (final imageMeta in problem.imageUrls) { - if (imageMeta.remoteUrl != null && imageMeta.remoteUrl!.isNotEmpty) { - // 为每张图片创建下载任务 - final imageFuture = - _downloadSingleImage(imageMeta, problem.id, imageRepository).then( - (downloadedImage) { - if (downloadedImage != null) { - downloadedImages.add(downloadedImage); - } - }, - ); - - imageFutures.add(imageFuture); - } - } - - // 等待当前问题的所有图片下载完成 - await Future.wait(imageFutures); - - // 更新问题的图片数据 - if (downloadedImages.isNotEmpty) { - final updatedProblem = problem.copyWith(imageUrls: downloadedImages); - await problemRepository.updateProblem(updatedProblem); - } - } catch (e) { - Get.log('下载问题 ${problem.id} 的图片失败: $e'); - rethrow; // 重新抛出异常,让调用方知道失败 - } - } - - /// 下载单张图片 - Future _downloadSingleImage( - ImageMetadata imageMeta, - String problemId, - ImageRepository imageRepository, - ) async { - try { - final bool isDownloaded = await imageRepository.isImageDownloaded( - imageMeta.remoteUrl!, - problemId, - ); - - String localPath; - if (isDownloaded) { - localPath = (await imageRepository.getLocalImagePath( - imageMeta.remoteUrl!, - problemId, - ))!; - } else { - localPath = await imageRepository.downloadImage( - imageMeta.remoteUrl!, - problemId, - ); - } - - return imageMeta.copyWith( - localPath: localPath, - status: ImageStatus.synced, - ); - } catch (e) { - Get.log('下载图片 ${imageMeta.remoteUrl} 失败: $e'); - return null; // 单张图片失败不影响其他图片 - } - } - // /// 异步下载问题的图片 - // void _downloadImagesForProblems(List problems) { - // if (problems.isEmpty) return; - - // // 在后台执行图片下载 - // Future(() async { - // final imageRepository = Get.find(); // 使用GetX获取实例 - - // for (final problem in problems) { - // try { - // final List downloadedImages = []; - - // for (final imageMeta in problem.imageUrls) { - // if (imageMeta.remoteUrl != null && - // imageMeta.remoteUrl!.isNotEmpty) { - // // 检查是否已下载 - // final bool isDownloaded = await imageRepository.isImageDownloaded( - // imageMeta.remoteUrl!, - // problem.id, - // ); - - // String localPath; - // if (isDownloaded) { - // // 如果已下载,获取本地路径 - // localPath = (await imageRepository.getLocalImagePath( - // imageMeta.remoteUrl!, - // problem.id, - // ))!; - // } else { - // // 下载图片到本地 - // localPath = await imageRepository.downloadImage( - // imageMeta.remoteUrl!, - // problem.id, - // ); - // } - - // // 更新图片元数据 - // final downloadedImage = imageMeta.copyWith( - // localPath: localPath, - // status: ImageStatus.synced, - // ); - // downloadedImages.add(downloadedImage); - // } - // } - - // // 更新问题的图片数据 - // if (downloadedImages.isNotEmpty) { - // final updatedProblem = problem.copyWith( - // imageUrls: downloadedImages, - // ); - // await problemRepository.updateProblem(updatedProblem); - // } - // } catch (e) { - // Get.log('下载问题 ${problem.id} 的图片失败: $e'); - // } - // } - // }); - // } - - /// 同步服务器和本地数据,返回需要下载图片的问题列表 - Future> _syncProblems( - List serverProblems, - List localProblems, - ) async { - final List needDownloadImages = []; - - // 创建映射以便快速查找 - final Map serverProblemsMap = { - for (var problem in serverProblems) problem.id: problem, - }; - - final Map localProblemsMap = { - for (var problem in localProblems) problem.id: problem, - }; - - // 处理服务器有但本地没有的数据(新增) - for (final serverProblem in serverProblems) { - if (!localProblemsMap.containsKey(serverProblem.id)) { - // 服务器新增的问题,添加到本地 - final newProblem = _convertServerProblemToLocal(serverProblem); - await problemRepository.insertProblem(newProblem); - needDownloadImages.add(newProblem); - } - } - - // 处理本地有但服务器没有的数据(删除) - for (final localProblem in localProblems) { - if (!serverProblemsMap.containsKey(localProblem.id)) { - // 只有已同步的数据才从本地删除,未同步的数据保留 - if (localProblem.syncStatus == ProblemSyncStatus.synced) { - await problemRepository.deleteProblem(localProblem.id); - } - // 如果是未同步的数据(pendingCreate/pendingUpdate),保留在本地等待上传 - } - } - - // 处理双方都有的数据(更新) - for (final serverProblem in serverProblems) { - if (localProblemsMap.containsKey(serverProblem.id)) { - final localProblem = localProblemsMap[serverProblem.id]!; - - // 只有当本地数据已同步时才更新(避免覆盖本地未上传的更改) - if (localProblem.syncStatus == ProblemSyncStatus.synced) { - // 比较更新时间,使用最新的数据 - final serverUpdated = serverProblem.lastModificationTime; - final localUpdated = localProblem.lastModifiedTime; - - if (serverUpdated != null && - serverUpdated.millisecondsSinceEpoch > - localUpdated.millisecondsSinceEpoch) { - // 服务器数据更新,更新本地数据 - final updatedProblem = _convertServerProblemToLocal(serverProblem); - await problemRepository.updateProblem(updatedProblem); - needDownloadImages.add(updatedProblem); - } - } - // 如果本地有未同步的更改,保留本地更改(下次上传时会同步到服务器) - } - } - - return needDownloadImages; - } - - /// 将服务器问题转换为本地问题模型 - Problem _convertServerProblemToLocal(ServerProblem serverProblem) { - // 转换图片URL为ImageMetadata列表(初始状态为待下载) - final List imageMetadatas = (serverProblem.imageUrls ?? []) - .map( - (url) => ImageMetadata( - remoteUrl: url, - localPath: '', // 初始为空,等待下载 - status: ImageStatus.pendingDownload, // 标记为待下载状态 - ), - ) - .toList(); - - return Problem( - id: serverProblem.id, - description: serverProblem.title, - location: serverProblem.location, - imageUrls: imageMetadatas, - creationTime: serverProblem.creationTime, - creatorId: serverProblem.creatorId, - lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(), - syncStatus: ProblemSyncStatus.synced, // 来自服务器的数据标记为已同步 - censorTaskId: serverProblem.censorTaskId, - bindData: serverProblem.bindData, - isChecked: false, // 默认未检查 - ); - } - // #endregion - - // #region 悬浮按钮 - /// floatingButton更新位置 - void updateFabUploadPosition(Offset delta) { - final screenWidth = ScreenUtil().screenWidth; - final screenHeight = ScreenUtil().screenHeight; - - Offset newPosition = fabUploadPosition.value + delta; - - // 限制水平范围:按钮左边缘与屏幕左边缘的距离 - double clampedDx = newPosition.dx.clamp( - _edgePaddingX, - screenWidth - _fabSize - _edgePaddingX, - ); - - // 限制垂直范围:按钮上边缘与屏幕上边缘的距离 - double clampedDy = newPosition.dy.clamp( - _edgePaddingY, - screenHeight - _fabSize - _edgePaddingY, - ); - - fabUploadPosition.value = Offset(clampedDx, clampedDy); - } - - /// floatingButton 贴靠 - void snapToEdge() { - final screenWidth = ScreenUtil().screenWidth; - - // 获取当前按钮的水平中心点 - final buttonCenterDx = fabUploadPosition.value.dx + _fabSize / 2; - - double newDx; - - // 判断按钮中心点位于屏幕的左半部分还是右半部分 - if (buttonCenterDx < screenWidth / 2) { - // 贴靠到左侧,按钮左边缘与屏幕左边缘距离为 _edgePaddingX - newDx = _edgePaddingX; - } else { - // 贴靠到右侧,按钮右边缘与屏幕右边缘距离为 _edgePaddingX - newDx = screenWidth - _fabSize - _edgePaddingX; - } - - // 关键:只更新水平位置,垂直位置保持不变 - fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy); - } - - // #endregion - - // #region ta按钮 - void _onTabChanged() { - if (!tabController.indexIsChanging) { - loadProblems(); - } - } - - // #endregion - // 添加筛选方法 - // 更新日期范围的方法 - void updateCurrentDateRange(String rangeValue) { - final newRange = rangeValue.toDateRange(); - if (newRange != null) { - currentDateRange.value = newRange; - loadProblems(); // 重新加载数据 - } - } - - void updateCurrentUpload(String value) { - currentUploadFilter.value = value; - loadProblems(); // 重新加载数据 - } - - void updateCurrentBind(String value) { - currentBindFilter.value = value; - loadProblems(); // 重新加载数据 - } - - // 添加筛选方法 - /// 显示日期选择器 - Future selectDateRange(BuildContext context) async { - final initialDateRange = DateTimeRange( - start: historyStartTime.value, - end: historyEndTime.value, - ); - - final DateTimeRange? picked = await showDateRangePicker( - context: context, - firstDate: DateTime(2024, 8, 1), // 可选的最早日期 - lastDate: DateTime(2101), // 可选的最晚日期 - initialDateRange: initialDateRange, - ); - - if (picked != null) { - // 处理用户选择的日期范围 - historyStartTime.value = picked.start; - historyEndTime.value = DateTime( - picked.end.year, - picked.end.month, - picked.end.day, - 23, - 59, - 59, - 999, - ); - loadProblems(); - log('选择的日期范围是: ${picked.start} 到 ${picked.end}'); - } - } - - void updateHistoryUpload(String value) { - historyUploadFilter.value = value; - loadProblems(); // 重新加载数据 - } - - void updateHistoryBind(String value) { - historyBindFilter.value = value; - loadProblems(); // 重新加载数据 - } - - /// 加载 - Future loadProblems() async { - isLoading.value = true; - try { - // 根据 Tab 索引设置查询参数的默认值 - final bool isProblemListTab = tabController.index == 0; - - final DateTime startDate = isProblemListTab - ? currentDateRange.value.startDate - : historyStartTime.value; - - final DateTime endDate = isProblemListTab - ? currentDateRange.value.endDate - : historyEndTime.value; - - final String uploadStatus = isProblemListTab - ? currentUploadFilter.value - : historyUploadFilter.value; - - final String bindStatus = isProblemListTab - ? currentBindFilter.value - : historyBindFilter.value; - - // 只执行一次数据库查询 - final loadedProblems = await problemRepository.getProblems( - startDate: startDate, - endDate: endDate, - syncStatus: uploadStatus, - bindStatus: bindStatus, - ); - - // 根据 Tab 索引将数据分配给正确的列表 - if (isProblemListTab) { - problems.assignAll(loadedProblems); - } else { - historyProblems.assignAll(loadedProblems); - } - } catch (e) { - Get.snackbar('错误', '加载问题失败: $e'); - } finally { - isLoading.value = false; - } - } - - /// 显示上传页面 - void showUploadPage() { - Get.toNamed(AppRoutes.problemUpload); - clearSelection(); - loadUnUploadedProblems(); - } - - // 查询所有未上传的问题 - Future loadUnUploadedProblems() async { - isLoading.value = true; - try { - // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 - unUploadedProblems.value = await problemRepository.getProblems( - syncStatus: '未上传', - ); - } catch (e) { - Get.snackbar('错误', '加载未上传问题失败: $e'); - } finally { - isLoading.value = false; - } - } - - /// 删除问题 - /// 控制器中可以添加逻辑 - Future deleteProblem(Problem problem) async { - try { - final deleteProblem = problemStateManager.markForDeletion(problem); - if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) { - // 直接删除问题和图片 - await problemRepository.deleteProblem(problem.id); - await _deleteProblemImages(problem); - } else { - // 更新状态 - await problemRepository.updateProblem(deleteProblem); - } - - loadProblems(); - } catch (e) { - Get.snackbar('错误', '删除问题失败: $e'); - rethrow; - } - } - - // 删除本地文件 - Future _deleteProblemImages(Problem problem) async { - for (var imagePath in problem.imageUrls) { - try { - final file = File(imagePath.localPath); - if (await file.exists()) { - await file.delete(); - } - } catch (e) { - throw Exception(e); - } - } - } - - Future toProblemFormPageAndRefresh({Problem? problem}) async { - await Get.toNamed(AppRoutes.problemForm, arguments: problem); - loadProblems(); - } -} +// // modules/problem/controllers/problem_controller.dart +// import 'dart:developer'; +// import 'dart:io'; + +// import 'package:dio/dio.dart'; +// import 'package:flutter_screenutil/flutter_screenutil.dart'; +// import 'package:get/get.dart' hide MultipartFile, FormData, Response; +// import 'package:flutter/material.dart'; +// import 'package:problem_check_system/app/core/routes/app_routes.dart'; +// import 'package:problem_check_system/app/core/extensions/http_response_extension.dart'; +// import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; +// import 'package:problem_check_system/app/core/models/image_status.dart'; +// import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; +// import 'package:problem_check_system/app/core/models/server_problem.dart'; +// import 'package:problem_check_system/app/core/repositories/file_repository.dart'; +// import 'package:problem_check_system/app/core/repositories/image_repository.dart'; +// import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; +// import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; +// import 'package:problem_check_system/app/features/problem/presentation/controllers/sync_progress_state.dart'; +// import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/models/date_range_enum.dart'; +// import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/models/dropdown_option.dart'; +// import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/sync_progress_dialog.dart'; + +// class ProblemController extends GetxController +// with GetSingleTickerProviderStateMixin { +// /// 依赖问题数据 +// final ProblemRepository problemRepository; +// final ProblemStateManager problemStateManager; +// final FileRepository fileRepository = Get.find(); + +// /// 最近问题列表 +// final RxList problems = [].obs; + +// /// 历史问题列表 +// final RxList historyProblems = [].obs; + +// /// 未上传的问题列表 +// final RxList unUploadedProblems = [].obs; +// final Rx allSelected = false.obs; +// final RxDouble uploadProgress = 0.0.obs; +// // Dio 的取消令牌,用于取消正在进行的请求 +// late CancelToken _cancelToken; + +// final RxSet _selectedProblems = {}.obs; + +// Set get selectedProblems => _selectedProblems; + +// int get selectedCount => _selectedProblems.length; + +// /// 选中未上传的数量 +// int get selectedUnUploadCount => _selectedProblems +// .where((p) => p.syncStatus != ProblemSyncStatus.synced) +// .length; + +// // 在 ProblemController 中添加 +// // 添加日期范围选项列表 +// List get dateRangeOptions { +// return DateRange.values.map((range) => range.toDropdownOption()).toList(); +// } + +// final List uploadOptions = const [ +// DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), +// DropdownOption(label: '已上传', value: '已上传', icon: Icons.cloud_done), +// DropdownOption(label: '未上传', value: '未上传', icon: Icons.cloud_off), +// ]; + +// final List bindOptions = const [ +// DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), +// DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link), +// DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off), +// ]; + +// final Rx currentDateRange = DateRange.oneWeek.obs; +// final RxString currentUploadFilter = '全部'.obs; +// final RxString currentBindFilter = '全部'.obs; + +// // 历史问题列表筛选条件 +// final Rx historyStartTime = DateTime.now() +// .subtract(const Duration(days: 365)) +// .obs; +// final Rx historyEndTime = DateTime( +// DateTime.now().year, +// DateTime.now().month, +// DateTime.now().day, +// 23, +// 59, +// 59, +// 999, +// ).obs; +// final RxString historyUploadFilter = '全部'.obs; +// final RxString historyBindFilter = '全部'.obs; + +// /// 是否加载中 +// final RxBool isLoading = false.obs; + +// late TabController tabController; + +// /// floatingButton 拖动 +// final double _fabSize = 56.0; +// final double _edgePaddingX = 27.0.w; +// final double _edgePaddingY = 111.0.h; +// final fabUploadPosition = Offset(337.0, 703.7).obs; + +// /// get 选中的 +// RxBool get isOnline => problemRepository.isOnline; + +// ProblemController({ +// required this.problemRepository, +// required this.problemStateManager, +// }); + +// @override +// void onInit() { +// super.onInit(); + +// tabController = TabController(length: 2, vsync: this); +// tabController.addListener(_onTabChanged); +// loadProblems(); +// // 查询未上传问题 +// // loadUnUploadedProblems(); +// } + +// @override +// void onClose() { +// tabController.dispose(); +// super.onClose(); +// } + +// // #region 问题上传 + +// void updateProblemSelection(Problem problem, bool isChecked) { +// if (isChecked) { +// _selectedProblems.add(problem); +// } else { +// _selectedProblems.remove(problem); +// } +// // 更新全选状态 +// allSelected.value = _selectedProblems.length == unUploadedProblems.length; +// } + +// void selectAll() { +// if (allSelected.value) { +// // 如果已经是全选,则取消全选 +// _selectedProblems.clear(); +// } else { +// // 如果是取消全选,则选择所有 +// _selectedProblems.addAll(unUploadedProblems); +// } +// allSelected.value = !allSelected.value; +// } + +// // 上传完成后清空选中状态 +// void clearSelection() { +// _selectedProblems.clear(); +// allSelected.value = false; +// } + +// // 在 handleUpload 方法中,上传完成后调用 clearSelection +// Future handleUpload() async { +// if (_selectedProblems.isEmpty) { +// Get.snackbar('提示', '请选择要上传的问题'); +// return; +// } + +// uploadProgress.value = 0.0; +// _cancelToken = CancelToken(); + +// showUploadProgressDialog(); + +// try { +// await uploadProblems( +// _selectedProblems.toList(), // 转换为列表 +// cancelToken: _cancelToken, +// onProgress: (progress) { +// uploadProgress.value = progress; +// }, +// ); + +// Get.back(); +// Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.TOP); + +// // 上传成功后清空选中状态 +// clearSelection(); +// // 重新加载未上传的问题列表 +// loadUnUploadedProblems(); +// // 重新加载problems +// loadProblems(); +// } on DioException catch (e) { +// Get.back(); +// if (CancelToken.isCancel(e)) { +// Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.TOP); +// } else { +// Get.snackbar( +// '上传失败', +// '错误: ${e.message}', +// snackPosition: SnackPosition.TOP, +// ); +// } +// } catch (e) { +// Get.back(); +// Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.TOP); +// } +// } + +// /// 显示上传对话框 +// void showUploadProgressDialog() { +// // 显示对话框 +// Get.defaultDialog( +// title: '上传问题中...', +// content: Obx(() { +// // final progress = (uploadProgress.value * 100).toInt(); +// return Column( +// // mainAxisSize: MainAxisSize.min, +// children: [ +// SizedBox(height: 16.h), +// LinearProgressIndicator( +// value: uploadProgress.value, +// backgroundColor: Colors.grey[300], +// valueColor: const AlwaysStoppedAnimation(Colors.blue), +// ), +// SizedBox(height: 16.h), +// // Text('已完成: $progress%'), +// Text('已上传: $selectedUnUploadCount / $selectedCount'), +// ], +// ); +// }), +// barrierDismissible: false, // 防止用户点击外部关闭对话框 +// // 添加一个 "取消" 按钮 +// cancel: ElevatedButton( +// onPressed: () { +// // 调用 controller 中的取消方法 +// cancelUpload(); +// }, +// child: Text('取消', style: TextStyle(color: Colors.red)), +// ), +// ); +// // 启动上传逻辑 +// // ... +// } + +// void cancelUpload() { +// // 在这里实现你的取消逻辑 +// // 1. 停止上传任务(例如,取消 HTTP 请求) +// // 2. 将上传进度重置为0 +// uploadProgress.value = 0.0; +// // 3. 关闭对话框 +// Get.back(); +// // 4. 可以添加一个提示,例如: +// // Get.snackbar('提示', '上传已取消'); +// } + +// /// 新增:上传问题列表。 +// /// 遍历问题列表,并计算总进度。 +// Future uploadProblems( +// List problems, { +// required CancelToken cancelToken, +// required void Function(double progress) onProgress, +// }) async { +// final int totalProblems = problems.length; +// // final List updatedProblems = []; + +// try { +// for (int i = 0; i < totalProblems; i++) { +// // 如果取消令牌被触发,停止并返回 +// if (cancelToken.isCancelled) { +// break; +// } + +// final problemToUpload = problems[i]; + +// // 传递一个子进度回调,用于计算单个问题的进度 +// final updatedProblem = await uploadProblem( +// problemToUpload, +// cancelToken: cancelToken, +// onProgress: (progress) { +// // 计算总体进度:(已完成的问题数 + 当前问题的进度) / 总问题数 +// final overallProgress = (i + progress) / totalProblems; +// onProgress(overallProgress); +// }, +// ); + +// if (updatedProblem.syncStatus == ProblemSyncStatus.untracked) { +// problemRepository.deleteProblem(updatedProblem.id); +// } else { +// problemRepository.updateProblem(updatedProblem); +// } +// } +// // return updatedProblems; +// } on DioException { +// rethrow; +// } +// } + +// /// 上传单个问题及其所有关联的图片。 +// /// 上传单个问题及其所有关联的图片,根据操作类型执行不同逻辑 +// Future uploadProblem( +// Problem problem, { +// required CancelToken cancelToken, +// required void Function(double progress) onProgress, +// }) async { +// try { +// // 检查操作类型有效性 +// if (problem.syncStatus == ProblemSyncStatus.synced || +// problem.syncStatus == ProblemSyncStatus.untracked) { +// throw Exception('问题已同步,无需再次同步'); +// } + +// // 1. 上传图片(仅对创建和更新操作) +// final List remoteUrls = []; +// if (problem.syncStatus != ProblemSyncStatus.pendingDelete) { +// final newImages = problem.imageUrls +// .where((img) => img.status == ImageStatus.pendingUpload) +// .toList(); + +// final totalFilesToUpload = newImages.length; +// int filesUploadedCount = 0; + +// for (var image in newImages) { +// if (cancelToken.isCancelled) { +// throw DioException( +// requestOptions: RequestOptions(path: ''), +// type: DioExceptionType.cancel, +// error: '上传已取消', +// ); +// } + +// final url = await fileRepository.uploadImage( +// image.localPath, +// cancelToken: cancelToken, +// onSendProgress: (sent, total) { +// double overallProgress = +// (filesUploadedCount + (sent / total)) / totalFilesToUpload; +// onProgress(overallProgress); +// }, +// ); +// remoteUrls.add(url); +// filesUploadedCount++; +// } +// onProgress(1.0); +// } + +// // 2. 构建 API payload(删除操作不需要完整payload) +// final apiPayload = problem.syncStatus != ProblemSyncStatus.pendingDelete +// ? { +// 'id': problem.id, +// 'title': problem.description, +// 'location': problem.location, +// 'imageUrls': _buildFinalRemoteUrls(problem.imageUrls, remoteUrls), +// 'creationTime': problem.creationTime.toUtc().toIso8601String(), +// } +// : null; + +// // 3. 根据操作类型调用不同的API +// late final Response response; + +// switch (problem.syncStatus) { +// case ProblemSyncStatus.untracked: +// case ProblemSyncStatus.synced: +// throw Exception('无效的操作类型: none'); +// case ProblemSyncStatus.pendingCreate: +// response = await problemRepository.post(apiPayload!, cancelToken); +// break; +// case ProblemSyncStatus.pendingUpdate: +// response = await problemRepository.put( +// problem.id, +// apiPayload!, +// cancelToken, +// ); +// break; +// case ProblemSyncStatus.pendingDelete: +// response = await problemRepository.delete(problem.id, cancelToken); +// break; +// } + +// // 4. 处理服务器响应 +// if (response.isSuccess) { +// if (problem.syncStatus != ProblemSyncStatus.pendingDelete) { +// final serverProblem = ServerProblem.fromJson(response.data); +// // 更新图片状态(仅对创建和更新操作) +// final updatedImageMetadata = _updateImageMetadata( +// problem.imageUrls, +// remoteUrls, +// ); +// // 返回同步完成的对象,操作类型重置为none +// return problem.copyWith( +// syncStatus: ProblemSyncStatus.synced, +// imageUrls: updatedImageMetadata, +// lastModifiedTime: serverProblem.lastModificationTime, +// ); +// } else { +// // 删除逻辑 +// return problem.copyWith(syncStatus: ProblemSyncStatus.untracked); +// } +// } else { +// throw Exception('操作失败,状态码: ${response.statusCode}'); +// } +// } on DioException { +// rethrow; +// } +// } + +// /// 辅助方法:构建最终的远程URL列表 +// List _buildFinalRemoteUrls( +// List images, +// List newRemoteUrls, +// ) { +// final List finalRemoteUrls = []; +// int newImageIndex = 0; + +// for (var image in images) { +// if (image.status == ImageStatus.synced) { +// finalRemoteUrls.add(image.remoteUrl!); +// } else if (image.status == ImageStatus.pendingUpload) { +// finalRemoteUrls.add(newRemoteUrls[newImageIndex]); +// newImageIndex++; +// } +// } + +// return finalRemoteUrls; +// } + +// /// 辅助方法:更新图片元数据状态 +// List _updateImageMetadata( +// List images, +// List newRemoteUrls, +// ) { +// final List updatedImageMetadata = []; +// int uploadedUrlIndex = 0; + +// for (var image in images) { +// if (image.status == ImageStatus.pendingUpload) { +// updatedImageMetadata.add( +// ImageMetadata( +// localPath: image.localPath, +// remoteUrl: newRemoteUrls[uploadedUrlIndex], +// status: ImageStatus.synced, +// ), +// ); +// uploadedUrlIndex++; +// } else { +// updatedImageMetadata.add(image); +// } +// } + +// return updatedImageMetadata; +// } + +// // #endregion + +// // #region 问题同步 + +// final SyncProgressState syncProgress = SyncProgressState(); + +// Future pullDataFromServer() async { +// isLoading.value = true; + +// // 显示进度对话框 +// Get.dialog( +// SyncProgressDialog(progressState: syncProgress), +// barrierDismissible: false, +// ); + +// try { +// const int totalSteps = 5; +// syncProgress.startSync(totalSteps); + +// // 1. 从服务器获取最新数据 +// syncProgress.updateProgress('正在从服务器获取数据...', 1); +// final List serverProblems = await problemRepository +// .fetchProblemsFromServer(pageNumber: 1, pageSize: 99); + +// // 2. 获取本地数据 +// syncProgress.updateProgress('正在获取本地数据...', 2); +// final List localProblems = await problemRepository.getProblems(); + +// // 3. 同步策略:以服务器数据为准,保留本地未同步的更改 +// syncProgress.updateProgress('正在同步数据...', 3); +// final List downloadedProblems = await _syncProblems( +// serverProblems, +// localProblems, +// ); + +// // 4. 启动图片下载任务 +// syncProgress.updateProgress('正在下载图片...', 4); +// await downloadImagesForProblems(downloadedProblems); + +// // 5. 重新加载本地问题列表 +// syncProgress.updateProgress('正在重新加载数据...', 5); +// await loadProblems(); + +// syncProgress.completeSync(); + +// // 明确关闭对话框 +// Get.back(closeOverlays: true); + +// Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); +// } catch (e) { +// syncProgress.errorSync(e.toString()); + +// // 错误时也确保关闭 +// Get.back(closeOverlays: true); +// Get.log('错误: $e'); +// Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP); +// } finally { +// isLoading.value = false; +// } +// } + +// /// 异步下载问题的图片,返回下载完成的任务 +// Future downloadImagesForProblems(List problems) async { +// if (problems.isEmpty) return; + +// final imageRepository = Get.find(); +// final List> downloadFutures = []; + +// for (final problem in problems) { +// // 为每个问题创建下载任务 +// final downloadFuture = _downloadProblemImages(problem, imageRepository); +// downloadFutures.add(downloadFuture); +// } + +// // 等待所有问题图片下载完成 +// await Future.wait(downloadFutures); +// } + +// /// 下载单个问题的所有图片 +// Future _downloadProblemImages( +// Problem problem, +// ImageRepository imageRepository, +// ) async { +// try { +// final List downloadedImages = []; +// final List> imageFutures = []; + +// for (final imageMeta in problem.imageUrls) { +// if (imageMeta.remoteUrl != null && imageMeta.remoteUrl!.isNotEmpty) { +// // 为每张图片创建下载任务 +// final imageFuture = +// _downloadSingleImage(imageMeta, problem.id, imageRepository).then( +// (downloadedImage) { +// if (downloadedImage != null) { +// downloadedImages.add(downloadedImage); +// } +// }, +// ); + +// imageFutures.add(imageFuture); +// } +// } + +// // 等待当前问题的所有图片下载完成 +// await Future.wait(imageFutures); + +// // 更新问题的图片数据 +// if (downloadedImages.isNotEmpty) { +// final updatedProblem = problem.copyWith(imageUrls: downloadedImages); +// await problemRepository.updateProblem(updatedProblem); +// } +// } catch (e) { +// Get.log('下载问题 ${problem.id} 的图片失败: $e'); +// rethrow; // 重新抛出异常,让调用方知道失败 +// } +// } + +// /// 下载单张图片 +// Future _downloadSingleImage( +// ImageMetadata imageMeta, +// String problemId, +// ImageRepository imageRepository, +// ) async { +// try { +// final bool isDownloaded = await imageRepository.isImageDownloaded( +// imageMeta.remoteUrl!, +// problemId, +// ); + +// String localPath; +// if (isDownloaded) { +// localPath = (await imageRepository.getLocalImagePath( +// imageMeta.remoteUrl!, +// problemId, +// ))!; +// } else { +// localPath = await imageRepository.downloadImage( +// imageMeta.remoteUrl!, +// problemId, +// ); +// } + +// return imageMeta.copyWith( +// localPath: localPath, +// status: ImageStatus.synced, +// ); +// } catch (e) { +// Get.log('下载图片 ${imageMeta.remoteUrl} 失败: $e'); +// return null; // 单张图片失败不影响其他图片 +// } +// } +// // /// 异步下载问题的图片 +// // void _downloadImagesForProblems(List problems) { +// // if (problems.isEmpty) return; + +// // // 在后台执行图片下载 +// // Future(() async { +// // final imageRepository = Get.find(); // 使用GetX获取实例 + +// // for (final problem in problems) { +// // try { +// // final List downloadedImages = []; + +// // for (final imageMeta in problem.imageUrls) { +// // if (imageMeta.remoteUrl != null && +// // imageMeta.remoteUrl!.isNotEmpty) { +// // // 检查是否已下载 +// // final bool isDownloaded = await imageRepository.isImageDownloaded( +// // imageMeta.remoteUrl!, +// // problem.id, +// // ); + +// // String localPath; +// // if (isDownloaded) { +// // // 如果已下载,获取本地路径 +// // localPath = (await imageRepository.getLocalImagePath( +// // imageMeta.remoteUrl!, +// // problem.id, +// // ))!; +// // } else { +// // // 下载图片到本地 +// // localPath = await imageRepository.downloadImage( +// // imageMeta.remoteUrl!, +// // problem.id, +// // ); +// // } + +// // // 更新图片元数据 +// // final downloadedImage = imageMeta.copyWith( +// // localPath: localPath, +// // status: ImageStatus.synced, +// // ); +// // downloadedImages.add(downloadedImage); +// // } +// // } + +// // // 更新问题的图片数据 +// // if (downloadedImages.isNotEmpty) { +// // final updatedProblem = problem.copyWith( +// // imageUrls: downloadedImages, +// // ); +// // await problemRepository.updateProblem(updatedProblem); +// // } +// // } catch (e) { +// // Get.log('下载问题 ${problem.id} 的图片失败: $e'); +// // } +// // } +// // }); +// // } + +// /// 同步服务器和本地数据,返回需要下载图片的问题列表 +// Future> _syncProblems( +// List serverProblems, +// List localProblems, +// ) async { +// final List needDownloadImages = []; + +// // 创建映射以便快速查找 +// final Map serverProblemsMap = { +// for (var problem in serverProblems) problem.id: problem, +// }; + +// final Map localProblemsMap = { +// for (var problem in localProblems) problem.id: problem, +// }; + +// // 处理服务器有但本地没有的数据(新增) +// for (final serverProblem in serverProblems) { +// if (!localProblemsMap.containsKey(serverProblem.id)) { +// // 服务器新增的问题,添加到本地 +// final newProblem = _convertServerProblemToLocal(serverProblem); +// await problemRepository.insertProblem(newProblem); +// needDownloadImages.add(newProblem); +// } +// } + +// // 处理本地有但服务器没有的数据(删除) +// for (final localProblem in localProblems) { +// if (!serverProblemsMap.containsKey(localProblem.id)) { +// // 只有已同步的数据才从本地删除,未同步的数据保留 +// if (localProblem.syncStatus == ProblemSyncStatus.synced) { +// await problemRepository.deleteProblem(localProblem.id); +// } +// // 如果是未同步的数据(pendingCreate/pendingUpdate),保留在本地等待上传 +// } +// } + +// // 处理双方都有的数据(更新) +// for (final serverProblem in serverProblems) { +// if (localProblemsMap.containsKey(serverProblem.id)) { +// final localProblem = localProblemsMap[serverProblem.id]!; + +// // 只有当本地数据已同步时才更新(避免覆盖本地未上传的更改) +// if (localProblem.syncStatus == ProblemSyncStatus.synced) { +// // 比较更新时间,使用最新的数据 +// final serverUpdated = serverProblem.lastModificationTime; +// final localUpdated = localProblem.lastModifiedTime; + +// if (serverUpdated != null && +// serverUpdated.millisecondsSinceEpoch > +// localUpdated.millisecondsSinceEpoch) { +// // 服务器数据更新,更新本地数据 +// final updatedProblem = _convertServerProblemToLocal(serverProblem); +// await problemRepository.updateProblem(updatedProblem); +// needDownloadImages.add(updatedProblem); +// } +// } +// // 如果本地有未同步的更改,保留本地更改(下次上传时会同步到服务器) +// } +// } + +// return needDownloadImages; +// } + +// /// 将服务器问题转换为本地问题模型 +// Problem _convertServerProblemToLocal(ServerProblem serverProblem) { +// // 转换图片URL为ImageMetadata列表(初始状态为待下载) +// final List imageMetadatas = (serverProblem.imageUrls ?? []) +// .map( +// (url) => ImageMetadata( +// remoteUrl: url, +// localPath: '', // 初始为空,等待下载 +// status: ImageStatus.pendingDownload, // 标记为待下载状态 +// ), +// ) +// .toList(); + +// return Problem( +// id: serverProblem.id, +// description: serverProblem.title, +// location: serverProblem.location, +// imageUrls: imageMetadatas, +// creationTime: serverProblem.creationTime, +// creatorId: serverProblem.creatorId, +// lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(), +// syncStatus: ProblemSyncStatus.synced, // 来自服务器的数据标记为已同步 +// censorTaskId: serverProblem.censorTaskId, +// bindData: serverProblem.bindData, +// isChecked: false, // 默认未检查 +// ); +// } +// // #endregion + +// // #region 悬浮按钮 +// /// floatingButton更新位置 +// void updateFabUploadPosition(Offset delta) { +// final screenWidth = ScreenUtil().screenWidth; +// final screenHeight = ScreenUtil().screenHeight; + +// Offset newPosition = fabUploadPosition.value + delta; + +// // 限制水平范围:按钮左边缘与屏幕左边缘的距离 +// double clampedDx = newPosition.dx.clamp( +// _edgePaddingX, +// screenWidth - _fabSize - _edgePaddingX, +// ); + +// // 限制垂直范围:按钮上边缘与屏幕上边缘的距离 +// double clampedDy = newPosition.dy.clamp( +// _edgePaddingY, +// screenHeight - _fabSize - _edgePaddingY, +// ); + +// fabUploadPosition.value = Offset(clampedDx, clampedDy); +// } + +// /// floatingButton 贴靠 +// void snapToEdge() { +// final screenWidth = ScreenUtil().screenWidth; + +// // 获取当前按钮的水平中心点 +// final buttonCenterDx = fabUploadPosition.value.dx + _fabSize / 2; + +// double newDx; + +// // 判断按钮中心点位于屏幕的左半部分还是右半部分 +// if (buttonCenterDx < screenWidth / 2) { +// // 贴靠到左侧,按钮左边缘与屏幕左边缘距离为 _edgePaddingX +// newDx = _edgePaddingX; +// } else { +// // 贴靠到右侧,按钮右边缘与屏幕右边缘距离为 _edgePaddingX +// newDx = screenWidth - _fabSize - _edgePaddingX; +// } + +// // 关键:只更新水平位置,垂直位置保持不变 +// fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy); +// } + +// // #endregion + +// // #region ta按钮 +// void _onTabChanged() { +// if (!tabController.indexIsChanging) { +// loadProblems(); +// } +// } + +// // #endregion +// // 添加筛选方法 +// // 更新日期范围的方法 +// void updateCurrentDateRange(String rangeValue) { +// final newRange = rangeValue.toDateRange(); +// if (newRange != null) { +// currentDateRange.value = newRange; +// loadProblems(); // 重新加载数据 +// } +// } + +// void updateCurrentUpload(String value) { +// currentUploadFilter.value = value; +// loadProblems(); // 重新加载数据 +// } + +// void updateCurrentBind(String value) { +// currentBindFilter.value = value; +// loadProblems(); // 重新加载数据 +// } + +// // 添加筛选方法 +// /// 显示日期选择器 +// Future selectDateRange(BuildContext context) async { +// final initialDateRange = DateTimeRange( +// start: historyStartTime.value, +// end: historyEndTime.value, +// ); + +// final DateTimeRange? picked = await showDateRangePicker( +// context: context, +// firstDate: DateTime(2024, 8, 1), // 可选的最早日期 +// lastDate: DateTime(2101), // 可选的最晚日期 +// initialDateRange: initialDateRange, +// ); + +// if (picked != null) { +// // 处理用户选择的日期范围 +// historyStartTime.value = picked.start; +// historyEndTime.value = DateTime( +// picked.end.year, +// picked.end.month, +// picked.end.day, +// 23, +// 59, +// 59, +// 999, +// ); +// loadProblems(); +// log('选择的日期范围是: ${picked.start} 到 ${picked.end}'); +// } +// } + +// void updateHistoryUpload(String value) { +// historyUploadFilter.value = value; +// loadProblems(); // 重新加载数据 +// } + +// void updateHistoryBind(String value) { +// historyBindFilter.value = value; +// loadProblems(); // 重新加载数据 +// } + +// /// 加载 +// Future loadProblems() async { +// isLoading.value = true; +// try { +// // 根据 Tab 索引设置查询参数的默认值 +// final bool isProblemListTab = tabController.index == 0; + +// final DateTime startDate = isProblemListTab +// ? currentDateRange.value.startDate +// : historyStartTime.value; + +// final DateTime endDate = isProblemListTab +// ? currentDateRange.value.endDate +// : historyEndTime.value; + +// final String uploadStatus = isProblemListTab +// ? currentUploadFilter.value +// : historyUploadFilter.value; + +// final String bindStatus = isProblemListTab +// ? currentBindFilter.value +// : historyBindFilter.value; + +// // 只执行一次数据库查询 +// final loadedProblems = await problemRepository.getProblems( +// startDate: startDate, +// endDate: endDate, +// syncStatus: uploadStatus, +// bindStatus: bindStatus, +// ); + +// // 根据 Tab 索引将数据分配给正确的列表 +// if (isProblemListTab) { +// problems.assignAll(loadedProblems); +// } else { +// historyProblems.assignAll(loadedProblems); +// } +// } catch (e) { +// Get.snackbar('错误', '加载问题失败: $e'); +// } finally { +// isLoading.value = false; +// } +// } + +// /// 显示上传页面 +// void showUploadPage() { +// Get.toNamed(AppRoutes.problemUpload); +// clearSelection(); +// loadUnUploadedProblems(); +// } + +// // 查询所有未上传的问题 +// Future loadUnUploadedProblems() async { +// isLoading.value = true; +// try { +// // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 +// unUploadedProblems.value = await problemRepository.getProblems( +// syncStatus: '未上传', +// ); +// } catch (e) { +// Get.snackbar('错误', '加载未上传问题失败: $e'); +// } finally { +// isLoading.value = false; +// } +// } + +// /// 删除问题 +// /// 控制器中可以添加逻辑 +// Future deleteProblem(Problem problem) async { +// try { +// final deleteProblem = problemStateManager.markForDeletion(problem); +// if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) { +// // 直接删除问题和图片 +// await problemRepository.deleteProblem(problem.id); +// await _deleteProblemImages(problem); +// } else { +// // 更新状态 +// await problemRepository.updateProblem(deleteProblem); +// } + +// loadProblems(); +// } catch (e) { +// Get.snackbar('错误', '删除问题失败: $e'); +// rethrow; +// } +// } + +// // 删除本地文件 +// Future _deleteProblemImages(Problem problem) async { +// for (var imagePath in problem.imageUrls) { +// try { +// final file = File(imagePath.localPath); +// if (await file.exists()) { +// await file.delete(); +// } +// } catch (e) { +// throw Exception(e); +// } +// } +// } + +// Future toProblemFormPageAndRefresh({Problem? problem}) async { +// await Get.toNamed(AppRoutes.problemForm, arguments: problem); +// loadProblems(); +// } +// } diff --git a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart index b8559fa..8fa2a1c 100644 --- a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart @@ -6,42 +6,39 @@ import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; import 'package:problem_check_system/app/core/models/image_status.dart'; import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; import 'dart:io'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; +import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:uuid/uuid.dart'; class ProblemFormController extends GetxController { - final Problem? problem; + final RxBool isLoading = false.obs; + final ProblemEntity? problem; late Map bindInfo; - final bool isReadOnly; - final ProblemRepository problemRepository; - final ProblemStateManager problemStateManager; + final FormMode formMode; + + bool get isReadOnly { + return formMode == FormMode.view; + } + + final titleName = "问题表单页".obs; + final TextEditingController descriptionController = TextEditingController(); final TextEditingController locationController = TextEditingController(); final RxList selectedImages = [].obs; - final RxBool isLoading = false.obs; // 使用依赖注入,便于测试 - ProblemFormController({ - required this.problemRepository, - required this.problemStateManager, - this.problem, - this.isReadOnly = false, - }) { + ProblemFormController({this.problem, this.formMode = FormMode.view}) { if (problem != null) { if (problem!.bindData != null) { bindInfo = jsonDecode(problem!.bindData!); } descriptionController.text = problem!.description; locationController.text = problem!.location; - final imagePaths = problem!.imageUrls - .map((meta) => XFile(meta.localPath)) - .toList(); + final imagePaths = problem!.imageUrls.map((e) => XFile(e)).toList(); selectedImages.assignAll(imagePaths); } else { descriptionController.clear(); @@ -50,11 +47,31 @@ class ProblemFormController extends GetxController { } } - // @override - // void onInit() { - // super.onInit(); + @override + void onInit() { + super.onInit(); + } + + @override + void onClose() { + descriptionController.dispose(); + locationController.dispose(); + super.onClose(); + } - // } + void updatePageTitle() { + switch (formMode) { + case FormMode.add: + titleName.value = '新增问题'; + break; + case FormMode.edit: + titleName.value = '修改问题'; + break; + case FormMode.view: + titleName.value = '查看问题'; + break; + } + } // 改进的 pickImage 方法 Future pickImage(ImageSource source) async { @@ -146,44 +163,44 @@ class ProblemFormController extends GetxController { /// 保存图片 Future saveProblem() async { - if (!_validateForm()) { - return; - } - - isLoading.value = true; - - try { - // 保存图片到本地 - final List imagePaths = await _saveImagesToLocal(); - - if (problem != null) { - // 修改问题 - final updatedProblem = problem!.copyWith( - description: descriptionController.text, - location: locationController.text, - imageUrls: imagePaths, - ); - // 如果原问题是待创建的,修改后仍然应该是创建操作 - final modifyProblem = problemStateManager.modifyProblem(updatedProblem); - - await problemRepository.updateProblem(modifyProblem); - } else { - // 创建新问题 - final newProblem = problemStateManager.createNewProblem( - description: descriptionController.text, - location: locationController.text, - imageUrls: imagePaths, - ); - - await problemRepository.insertProblem(newProblem); - } - Get.back(result: true); // 返回成功结果 - Get.snackbar('成功', '问题已更新'); - } catch (e) { - Get.snackbar('错误', '保存问题失败: $e'); - } finally { - isLoading.value = false; - } + // if (!_validateForm()) { + // return; + // } + + // isLoading.value = true; + + // try { + // // 保存图片到本地 + // final List imagePaths = await _saveImagesToLocal(); + + // if (problem != null) { + // // 修改问题 + // final updatedProblem = problem!.copyWith( + // description: descriptionController.text, + // location: locationController.text, + // imageUrls: imagePaths, + // ); + // // 如果原问题是待创建的,修改后仍然应该是创建操作 + // final modifyProblem = problemStateManager.modifyProblem(updatedProblem); + + // await problemRepository.updateProblem(modifyProblem); + // } else { + // // 创建新问题 + // final newProblem = problemStateManager.createNewProblem( + // description: descriptionController.text, + // location: locationController.text, + // imageUrls: imagePaths, + // ); + + // await problemRepository.insertProblem(newProblem); + // } + // Get.back(result: true); // 返回成功结果 + // Get.snackbar('成功', '问题已更新'); + // } catch (e) { + // Get.snackbar('错误', '保存问题失败: $e'); + // } finally { + // isLoading.value = false; + // } } // 保存图片到本地存储 @@ -261,11 +278,4 @@ class ProblemFormController extends GetxController { locationController.text != problem!.location || selectedImages.length != problem!.imageUrls.length; } - - @override - void onClose() { - descriptionController.dispose(); - locationController.dispose(); - super.onClose(); - } } diff --git a/lib/app/features/problem/presentation/controllers/problem_list_controller.dart b/lib/app/features/problem/presentation/controllers/problem_list_controller.dart new file mode 100644 index 0000000..304a587 --- /dev/null +++ b/lib/app/features/problem/presentation/controllers/problem_list_controller.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/models/company_enum.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; +import 'package:problem_check_system/app/core/routes/app_routes.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; +import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; + +class ProblemListController extends GetxController { + final GetAllProblemsUsecase getAllProblemsUsecase; + // final SyncEnterprisesUsecase syncEnterprisesUsecase; // 新增 + // final ResolveConflictUsecase resolveConflictUsecase; // 新增 + + ProblemListController({ + required this.getAllProblemsUsecase, + // required this.syncEnterprisesUsecase, + // required this.resolveConflictUsecase, + }); + + // --- 实现基类中定义的属性 --- + final enterpriseList = [].obs; + final isLoading = false.obs; + final isSyncing = false.obs; + + final nameController = TextEditingController(); + final selectedType = Rx(null); + final startDate = Rx(null); + final endDate = Rx(null); + final selectedEnterprises = {}.obs; + final ExpansibleController expansibleController = ExpansibleController(); + + @override + void onInit() { + // 页面初始化时,启动完整的“同步-再加载”流程 + // loadAndSyncEnterprises(); + search(); + super.onInit(); + } + + @override + void onClose() { + nameController.dispose(); + expansibleController.dispose(); + super.onClose(); + } + + void search() {} + + // --- 实现基类中定义的方法 --- + // 核心流程方法 + // Future loadAndSyncEnterprises() async { + // try { + // isLoading(true); + // isSyncing(true); + + // // 步骤 1: 执行同步 + // final syncResult = await syncEnterprisesUsecase(); + + // // 步骤 2: 处理冲突 + // if (syncResult.hasConflicts) { + // // 如果有冲突,则逐个弹窗让用户选择 + // for (final conflict in syncResult.conflicts) { + // final chosenVersion = await _showConflictDialog(conflict); + // if (chosenVersion != null) { + // // 用户做出了选择,更新本地数据 + // await resolveConflictUsecase(chosenVersion); + // } + // } + // } + + // isSyncing(false); + + // // 步骤 3: 所有同步和冲突解决完毕后,从本地加载最终数据 + // await loadEnterprises(); + // } catch (e) { + // Get.snackbar('错误', '操作失败: $e'); + // } finally { + // isLoading(false); + // isSyncing(false); + // } + // } + + // // [修改后] 弹出冲突选择对话框的辅助方法 + // Future _showConflictDialog(EnterpriseConflict conflict) { + // return Get.dialog( + // AlertDialog( + // title: Text('数据冲突'), + // content: Column( + // mainAxisSize: MainAxisSize.min, + // crossAxisAlignment: CrossAxisAlignment.stretch, + // children: [ + // Text('${conflict.localVersion.name} 服务器上的数据与本地数据不一致,请选择要保留的版本。'), + // const SizedBox(height: 24), + + // // --- 本地版本选择区 --- + // Row( + // children: [ + // // 使用 Expanded 让主按钮填充可用空间 + // Expanded( + // child: ElevatedButton( + // child: Text( + // '使用客户端版本\n(修改于: ${conflict.localVersion.lastModifiedTime.toLocal().toDateTimeString2()})', + // textAlign: TextAlign.center, + // ), + // onPressed: () => Get.back(result: conflict.localVersion), + // ), + // ), + // const SizedBox(width: 8), // 按钮间的间距 + // // 查看详情按钮 + // IconButton( + // icon: const Icon(Icons.info_outline), + // tooltip: '查看客户端版本详情', + // onPressed: () => navigateToDetailsView(conflict.localVersion), + // ), + // ], + // ), + // const SizedBox(height: 8), + + // // --- 服务器版本选择区 --- + // Row( + // children: [ + // Expanded( + // child: ElevatedButton( + // style: ElevatedButton.styleFrom( + // backgroundColor: Get.theme.colorScheme.primary, + // foregroundColor: Get.theme.colorScheme.onPrimary, + // ), + // child: Text( + // '使用服务器版本\n(修改于: ${conflict.serverVersion.lastModifiedTime.toLocal().toDateTimeString2()})', + // textAlign: TextAlign.center, + // ), + // onPressed: () => Get.back(result: conflict.serverVersion), + // ), + // ), + // const SizedBox(width: 8), + // IconButton( + // icon: const Icon(Icons.info_outline), + // tooltip: '查看服务器版本详情', + // onPressed: () => + // navigateToDetailsView(conflict.serverVersion), + // ), + // ], + // ), + // ], + // ), + // ), + // // 设置为 false,防止用户点击对话框外部意外关闭它 + // barrierDismissible: false, + // ); + // } + + // Future loadEnterprises() async { + // expansibleController.collapse(); + // isLoading.value = true; + // try { + // final result = await getEnterpriseListUsecase.call( + // name: nameController.text, + // type: selectedType.value?.displayText, + // startDate: startDate.value, + // endDate: endDate.value, + // ); + // enterpriseList.assignAll(result); + // } catch (e) { + // Get.snackbar('错误', '加载企业列表失败: $e'); + // } finally { + // isLoading.value = false; + // } + // } + + // void clearFilters() { + // nameController.clear(); + // selectedType.value = null; + // startDate.value = null; + // endDate.value = null; + // loadEnterprises(); + // } + + /// 导航到问题表单页面 + Future navigateToProblemForm({ + Enterprise? enterprise, + FormMode? fromMode, + }) async { + final result = await Get.toNamed( + AppRoutes.problemForm, + arguments: {'data': enterprise, 'mode': fromMode}, + ); + if (result == true) { + search(); + Get.snackbar( + '操作成功', + '问题信息已更新', + backgroundColor: Colors.green[600], + colorText: Colors.white, + icon: const Icon(Icons.check_circle, color: Colors.white), + duration: const Duration(seconds: 3), + ); + } + } + + /// 导航到企业问题列表 + // Future navigateToEnterpriseInfoPage(Enterprise enterprise) async { + // await Get.toNamed( + // AppRoutes.enterpriseInfo, + // arguments: {'data': enterprise, 'mode': FormMode.view}, + // ); + // } +} diff --git a/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart new file mode 100644 index 0000000..85e1170 --- /dev/null +++ b/lib/app/features/problem/presentation/controllers/problem_upload_controller.dart @@ -0,0 +1,13 @@ +import 'dart:ui'; + +class ProblemUploadController { + int get selectedCount => 10; + + get allSelected => null; + + get unUploadedProblems => null; + + VoidCallback? get selectAll => null; + + get handleUpload => null; +} diff --git a/lib/app/features/problem/presentation/models/problem_card_model.dart b/lib/app/features/problem/presentation/models/problem_card_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/app/features/problem/presentation/models/problem_form_model.dart b/lib/app/features/problem/presentation/models/problem_form_model.dart new file mode 100644 index 0000000..8f62ed9 --- /dev/null +++ b/lib/app/features/problem/presentation/models/problem_form_model.dart @@ -0,0 +1,13 @@ +class ProblemFormModel { + final String enterpriseName; + final String description; + final String location; + final List imageUrls; + + ProblemFormModel({ + required this.enterpriseName, + required this.description, + required this.location, + required this.imageUrls, + }); +} diff --git a/lib/app/features/problem/presentation/views/problem_form_page.dart b/lib/app/features/problem/presentation/pages/problem_form_page.dart similarity index 79% rename from lib/app/features/problem/presentation/views/problem_form_page.dart rename to lib/app/features/problem/presentation/pages/problem_form_page.dart index 7c3aee9..a683b7f 100644 --- a/lib/app/features/problem/presentation/views/problem_form_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_form_page.dart @@ -14,76 +14,76 @@ class ProblemFormPage extends GetView { Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( - titleName: controller.isReadOnly - ? '问题详情' - : (controller.problem == null ? '新增问题' : '编辑问题'), + titleName: controller.titleName.value, leadingVisible: true, ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (controller.problem?.bindData != null) - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - '项目名称', - controller.bindInfo['projectName'], - ), - _buildDivider(), - _buildSection( - '企业名称', - controller.bindInfo['companyName'], - ), - _buildDivider(), - _buildSection( - '要素名称', - controller.bindInfo['censorElementName'], - ), - _buildDivider(), - _buildSection( - '评估内容', - controller.bindInfo['pgContent'], - ), - _buildDivider(), - _buildSection( - '建议项', - controller.bindInfo['suggestion'], - ), - _buildDivider(), - _buildSection('问题类型', ''), - _buildDivider(), - _buildSection('工艺设备名称', ''), - ], + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + if (controller.problem?.bindData != null) + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSection( + '项目名称', + controller.bindInfo['projectName'], + ), + _buildDivider(), + _buildSection( + '企业名称', + controller.bindInfo['companyName'], + ), + _buildDivider(), + _buildSection( + '要素名称', + controller.bindInfo['censorElementName'], + ), + _buildDivider(), + _buildSection( + '评估内容', + controller.bindInfo['pgContent'], + ), + _buildDivider(), + _buildSection( + '建议项', + controller.bindInfo['suggestion'], + ), + _buildDivider(), + _buildSection('问题类型', ''), + _buildDivider(), + _buildSection('工艺设备名称', ''), + ], + ), ), + _buildInputCard( + title: '问题描述', + textController: controller.descriptionController, + hintText: '请输入问题描述', ), - _buildInputCard( - title: '问题描述', - textController: controller.descriptionController, - hintText: '请输入问题描述', - ), - _buildInputCard( - title: '所在位置', - textController: controller.locationController, - hintText: '请输入问题所在位置', - ), - _buildImageCard(context), - ], + _buildInputCard( + title: '所在位置', + textController: controller.locationController, + hintText: '请输入问题所在位置', + ), + _buildImageCard(context), + ], + ), ), ), - ), - // 只有在非只读模式下才显示底部操作按钮 - if (!controller.isReadOnly) _bottomButton(), - ], + // 只有在非只读模式下才显示底部操作按钮 + if (!controller.isReadOnly) _bottomButton(), + ], + ), ), ); } @@ -349,7 +349,7 @@ class ProblemFormPage extends GetView { Widget _bottomButton() { return Container( width: 375.w, - padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.only( diff --git a/lib/app/features/problem/presentation/pages/problem_list_page.dart b/lib/app/features/problem/presentation/pages/problem_list_page.dart new file mode 100644 index 0000000..5c4b652 --- /dev/null +++ b/lib/app/features/problem/presentation/pages/problem_list_page.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/models/form_mode.dart'; +import 'package:problem_check_system/app/core/pages/widgets/custom_app_bar.dart'; +import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; +import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart'; + +class ProblemListPage extends GetView { + const ProblemListPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + titleName: '企业列表', + actionsVisible: true, + onAddPressed: () { + controller.navigateToProblemForm(fromMode: FormMode.add); + }, + ), + body: Text("问题列表"), + ); + // return Obx(() { + // if (true) { + // return const Center(child: CircularProgressIndicator()); + // } + + // return EasyRefresh( + // header: ClassicHeader( + // dragText: '下拉刷新'.tr, + // armedText: '释放开始'.tr, + // readyText: '刷新中...'.tr, + // processingText: '刷新中...'.tr, + // processedText: '成功了'.tr, + // noMoreText: 'No more'.tr, + // failedText: '失败'.tr, + // messageText: '最后更新于 %T'.tr, + // ), + // onRefresh: () async { + // // 调用控制器的刷新方法 + // await controller.pullDataFromServer(); + // }, + // child: ListView.builder( + // padding: EdgeInsets.symmetric(horizontal: 17.w), + // itemCount: problemsToShow.length, + // itemBuilder: (context, index) { + // // if (index == problemsToShow.length) { + // // return SizedBox(height: 79.5.h); + // // } + // final problem = problemsToShow[index]; + // return _buildSwipeableProblemCard(problem); + // }, + // ), + // ); + // }); + } + + // Widget _buildSwipeableProblemCard(Problem problem) { + // // 对于所有视图类型,如果是待删除状态,都禁用交互 + // final bool isPendingDelete = + // problem.syncStatus == ProblemSyncStatus.pendingDelete; + + // if (viewType == ProblemCardViewType.buttons) { + // // buttons 视图类型:有条件启用滑动删除 + // if (!isPendingDelete) { + // // 非待删除状态:启用滑动删除 + // return Dismissible( + // key: ValueKey('${problem.id}-${problem.syncStatus}'), + // direction: DismissDirection.endToStart, + // background: Container( + // color: Colors.red, + // alignment: Alignment.centerRight, + // padding: EdgeInsets.only(right: 20.w), + // child: Icon(Icons.delete, color: Colors.white, size: 30.sp), + // ), + // confirmDismiss: (direction) async { + // return await _showDeleteConfirmationDialog(problem); + // }, + // onDismissed: (direction) { + // // controller.deleteProblem(problem); + // Get.snackbar('成功', '问题已删除'); + // }, + // child: ProblemCard( + // key: ValueKey(problem.id), + // problem: problem, + // viewType: viewType, + // isSelected: false, + // ), + // ); + // } else { + // // 待删除状态:显示普通卡片(无滑动功能) + // return ProblemCard( + // key: ValueKey(problem.id), + // problem: problem, + // viewType: viewType, + // isSelected: false, + // ); + // } + // } else { + // // 其他视图类型(list、grid等):使用 Obx 监听选中状态 + // return Obx(() { + // final isSelected = controller.selectedProblems.contains(problem); + // return ProblemCard( + // key: ValueKey(problem.id), + // problem: problem, + // viewType: viewType, + // isSelected: isSelected, + // onChanged: (problem, isChecked) { + // controller.updateProblemSelection(problem, isChecked); + // }, + // ); + // }); + // } + // } + + Future _showDeleteConfirmationDialog(Problem problem) async { + // 确保在返回前关闭可能存在的snackbar + if (Get.isSnackbarOpen) { + Get.closeCurrentSnackbar(); + } + return await Get.bottomSheet( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + const Text( + '确认删除', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + '确定要删除这个问题吗?此操作不可撤销。', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Get.back(result: true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + '删除', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () => Get.back(result: false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + '取消', + style: TextStyle(color: Colors.grey[700], fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ) ?? + false; + } +} diff --git a/lib/app/features/problem/presentation/views/problem_upload_page.dart b/lib/app/features/problem/presentation/pages/problem_upload_page.dart similarity index 69% rename from lib/app/features/problem/presentation/views/problem_upload_page.dart rename to lib/app/features/problem/presentation/pages/problem_upload_page.dart index 51c1b98..5279937 100644 --- a/lib/app/features/problem/presentation/views/problem_upload_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_upload_page.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_list_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart'; +import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart'; -class ProblemUploadPage extends GetView { +class ProblemUploadPage extends GetView { const ProblemUploadPage({super.key}); @override @@ -24,17 +22,20 @@ class ProblemUploadPage extends GetView { } Widget _buildBody() { - return Obx(() { - if (controller.unUploadedProblems.isEmpty) { - return Center( - child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)), - ); - } - return ProblemListPage( - problemsToShow: controller.unUploadedProblems, - viewType: ProblemCardViewType.checkbox, - ); - }); + 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() { diff --git a/lib/app/features/problem/presentation/views/widgets/custom_button.dart b/lib/app/features/problem/presentation/pages/widgets/custom_button.dart similarity index 100% rename from lib/app/features/problem/presentation/views/widgets/custom_button.dart rename to lib/app/features/problem/presentation/pages/widgets/custom_button.dart diff --git a/lib/app/features/problem/presentation/views/widgets/custom_filter_dropdown.dart b/lib/app/features/problem/presentation/pages/widgets/custom_filter_dropdown.dart similarity index 97% rename from lib/app/features/problem/presentation/views/widgets/custom_filter_dropdown.dart rename to lib/app/features/problem/presentation/pages/widgets/custom_filter_dropdown.dart index 340ff50..f7e669a 100644 --- a/lib/app/features/problem/presentation/views/widgets/custom_filter_dropdown.dart +++ b/lib/app/features/problem/presentation/pages/widgets/custom_filter_dropdown.dart @@ -1,7 +1,7 @@ // widgets/custom_filter_dropdown.dart import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/dropdown_option.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/models/dropdown_option.dart'; class CustomFilterDropdown extends StatelessWidget { final String title; diff --git a/lib/app/features/problem/presentation/views/widgets/models/date_range_enum.dart b/lib/app/features/problem/presentation/pages/widgets/models/date_range_enum.dart similarity index 96% rename from lib/app/features/problem/presentation/views/widgets/models/date_range_enum.dart rename to lib/app/features/problem/presentation/pages/widgets/models/date_range_enum.dart index a32b03f..b1d5f59 100644 --- a/lib/app/features/problem/presentation/views/widgets/models/date_range_enum.dart +++ b/lib/app/features/problem/presentation/pages/widgets/models/date_range_enum.dart @@ -1,6 +1,6 @@ // models/date_range_enum.dart import 'package:flutter/material.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/models/dropdown_option.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/models/dropdown_option.dart'; enum DateRange { threeDays, oneWeek, oneMonth } diff --git a/lib/app/features/problem/presentation/views/widgets/models/dropdown_option.dart b/lib/app/features/problem/presentation/pages/widgets/models/dropdown_option.dart similarity index 100% rename from lib/app/features/problem/presentation/views/widgets/models/dropdown_option.dart rename to lib/app/features/problem/presentation/pages/widgets/models/dropdown_option.dart diff --git a/lib/app/features/problem/presentation/views/widgets/problem_card.dart b/lib/app/features/problem/presentation/pages/widgets/problem_card.dart similarity index 96% rename from lib/app/features/problem/presentation/views/widgets/problem_card.dart rename to lib/app/features/problem/presentation/pages/widgets/problem_card.dart index 3d54ef0..259aa8a 100644 --- a/lib/app/features/problem/presentation/views/widgets/problem_card.dart +++ b/lib/app/features/problem/presentation/pages/widgets/problem_card.dart @@ -5,18 +5,18 @@ import 'package:intl/intl.dart'; import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/custom_button.dart'; +import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/custom_button.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'dart:io'; // 添加文件操作支持 // 定义枚举类型 enum ProblemCardViewType { buttons, checkbox } -class ProblemCard extends GetView { +class ProblemCard extends StatelessWidget { final Problem problem; final ProblemCardViewType viewType; final Function(Problem, bool)? onChanged; + final Function()? onTap; final bool isSelected; // 改为必需参数 const ProblemCard({ @@ -24,6 +24,7 @@ class ProblemCard extends GetView { required this.problem, this.viewType = ProblemCardViewType.buttons, this.onChanged, + this.onTap, required this.isSelected, // 改为必需参数 }); @@ -213,7 +214,8 @@ class ProblemCard extends GetView { CustomButton( text: '修改', onTap: () { - controller.toProblemFormPageAndRefresh(problem: problem); + onTap; + // controller.toProblemFormPageAndRefresh(problem: problem); }, ), if (!isDeleted) SizedBox(width: 8.w), diff --git a/lib/app/features/problem/presentation/views/widgets/sync_progress_dialog.dart b/lib/app/features/problem/presentation/pages/widgets/sync_progress_dialog.dart similarity index 100% rename from lib/app/features/problem/presentation/views/widgets/sync_progress_dialog.dart rename to lib/app/features/problem/presentation/pages/widgets/sync_progress_dialog.dart diff --git a/lib/app/features/problem/presentation/views/problem_page.dart b/lib/app/features/problem/presentation/views/problem_page.dart deleted file mode 100644 index e8c2987..0000000 --- a/lib/app/features/problem/presentation/views/problem_page.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/pages/widgets/custom_app_bar.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_list_page.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/current_filter_bar.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart'; - -class ProblemPage extends GetView { - const ProblemPage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: CustomAppBar( - titleName: '问题列表', - actionsVisible: true, - onAddPressed: () => controller.toProblemFormPageAndRefresh(), - ), - body: Column( - children: [ - CurrentFilterBar(), - Expanded( - child: // 使用通用列表组件 - ProblemListPage( - problemsToShow: controller.problems, - viewType: ProblemCardViewType.buttons, - ), - ), - ], - ), - - // floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, - // 使用 Stack 统一管理所有浮动按钮 - // floatingActionButton: Stack( - // children: [ - // // 固定位置的 "添加" 按钮 - // // 使用 Align 和 Positioned - // Align( - // alignment: Alignment.bottomCenter, - // child: Padding( - // padding: EdgeInsets.only(bottom: 24.h), // 底部间距 - // child: FloatingActionButton( - // heroTag: "btn_add", - // onPressed: () { - // controller.toProblemFormPageAndRefresh(); - // }, - // shape: const CircleBorder(), - // backgroundColor: Colors.blue[300], - // foregroundColor: Colors.white, - // child: const Icon(Icons.add), - // ), - // ), - // ), - - // // 可拖动的 "上传" 按钮 - // Obx(() { - // final isOnline = controller.isOnline.value; - // return Positioned( - // // 使用正确的坐标,left/right 对应 dx,top/bottom 对应 dy - // left: controller.fabUploadPosition.value.dx, - // top: controller.fabUploadPosition.value.dy, - // child: GestureDetector( - // onPanUpdate: (details) { - // // 调用控制器中的方法来更新位置 - // controller.updateFabUploadPosition(details.delta); - // }, - // onPanEnd: (details) { - // // 拖动结束后调用吸附方法 - // controller.snapToEdge(); - // }, - // child: FloatingActionButton( - // heroTag: "btn_upload", - // onPressed: isOnline - // ? () => controller.showUploadPage() - // : null, - // foregroundColor: Colors.white, - // backgroundColor: isOnline - // ? Colors.red[300] - // : Colors.grey[400], - // child: Icon( - // isOnline - // ? Icons.file_upload_outlined - // : Icons.cloud_off_outlined, - // ), - // ), - // ), - // ); - // }), - // ], - // ), - ); - } -} diff --git a/lib/app/features/problem/presentation/views/widgets/current_filter_bar.dart b/lib/app/features/problem/presentation/views/widgets/current_filter_bar.dart deleted file mode 100644 index dca78f2..0000000 --- a/lib/app/features/problem/presentation/views/widgets/current_filter_bar.dart +++ /dev/null @@ -1,66 +0,0 @@ -// widgets/compact_filter_bar.dart -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; - -import 'custom_filter_dropdown.dart'; - -class CurrentFilterBar extends GetView { - final EdgeInsetsGeometry? padding; - - const CurrentFilterBar({super.key, this.padding}); - - @override - Widget build(BuildContext context) { - return Container( - padding: padding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h), - color: Colors.grey[50], - child: Row( - children: [ - // 日期范围筛选 - ...[ - Obx( - () => CustomFilterDropdown( - title: '时间范围', - options: controller.dateRangeOptions, - selectedValue: controller.currentDateRange.value.name, - onChanged: controller.updateCurrentDateRange, - width: 100.w, - showBorder: false, - ), - ), - ], - - // 上传状态筛选 - ...[ - Obx( - () => CustomFilterDropdown( - title: '上传状态', - options: controller.uploadOptions, - selectedValue: controller.currentUploadFilter.value, - onChanged: controller.updateCurrentUpload, - width: 100.w, - showBorder: false, - ), - ), - ], - - // 绑定状态筛选 - ...[ - Obx( - () => CustomFilterDropdown( - title: '绑定状态', - options: controller.bindOptions, - selectedValue: controller.currentBindFilter.value, - onChanged: controller.updateCurrentBind, - width: 100.w, - showBorder: false, - ), - ), - ], - ], - ), - ); - } -} diff --git a/lib/app/features/problem/presentation/views/widgets/history_filter_bar.dart b/lib/app/features/problem/presentation/views/widgets/history_filter_bar.dart deleted file mode 100644 index 8b11719..0000000 --- a/lib/app/features/problem/presentation/views/widgets/history_filter_bar.dart +++ /dev/null @@ -1,97 +0,0 @@ -// widgets/compact_filter_bar.dart -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; - -import 'custom_filter_dropdown.dart'; - -class HistoryFilterBar extends GetView { - final EdgeInsetsGeometry? padding; - - const HistoryFilterBar({super.key, this.padding}); - - @override - Widget build(BuildContext context) { - return Container( - padding: padding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h), - color: Colors.grey[50], - child: Row( - children: [ - // 显示日期选择 - ...[ - SizedBox( - width: 110.w, - // decoration: BoxDecoration( - // border: Border.all(color: Colors.grey.shade300), - // borderRadius: BorderRadius.circular(8.r), - // ), - child: TextButton( - onPressed: () { - controller.selectDateRange(context); - }, - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 4.h, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.r), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.date_range, - size: 16.sp, - color: Colors.grey[700], - ), - SizedBox(width: 4.w), - - Text( - '选择日期', - style: TextStyle( - fontSize: 14.sp, - color: Colors.black87, - fontWeight: FontWeight.normal, - ), - ), - ], - ), - ), - ), - ], - - // 上传状态筛选 - ...[ - Obx( - () => CustomFilterDropdown( - title: '上传状态', - options: controller.uploadOptions, - selectedValue: controller.historyUploadFilter.value, - onChanged: controller.updateHistoryUpload, - width: 100.w, - showBorder: false, - ), - ), - ], - - // 绑定状态筛选 - ...[ - Obx( - () => CustomFilterDropdown( - title: '绑定状态', - options: controller.bindOptions, - selectedValue: controller.historyBindFilter.value, - onChanged: controller.updateHistoryBind, - width: 100.w, - showBorder: false, - ), - ), - ], - ], - ), - ); - } -} diff --git a/lib/app/features/problem/presentation/views/widgets/problem_list_page.dart b/lib/app/features/problem/presentation/views/widgets/problem_list_page.dart deleted file mode 100644 index 81b20b8..0000000 --- a/lib/app/features/problem/presentation/views/widgets/problem_list_page.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:easy_refresh/easy_refresh.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; -import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart'; -import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart'; - -class ProblemListPage extends GetView { - final RxList problemsToShow; - final ProblemCardViewType viewType; - - const ProblemListPage({ - super.key, - required this.problemsToShow, - this.viewType = ProblemCardViewType.buttons, - }); - - @override - Widget build(BuildContext context) { - return Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - - return EasyRefresh( - header: ClassicHeader( - dragText: '下拉刷新'.tr, - armedText: '释放开始'.tr, - readyText: '刷新中...'.tr, - processingText: '刷新中...'.tr, - processedText: '成功了'.tr, - noMoreText: 'No more'.tr, - failedText: '失败'.tr, - messageText: '最后更新于 %T'.tr, - ), - onRefresh: () async { - // 调用控制器的刷新方法 - await controller.pullDataFromServer(); - }, - child: ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 17.w), - itemCount: problemsToShow.length, - itemBuilder: (context, index) { - // if (index == problemsToShow.length) { - // return SizedBox(height: 79.5.h); - // } - final problem = problemsToShow[index]; - return _buildSwipeableProblemCard(problem); - }, - ), - ); - }); - } - - Widget _buildSwipeableProblemCard(Problem problem) { - // 对于所有视图类型,如果是待删除状态,都禁用交互 - final bool isPendingDelete = - problem.syncStatus == ProblemSyncStatus.pendingDelete; - - if (viewType == ProblemCardViewType.buttons) { - // buttons 视图类型:有条件启用滑动删除 - if (!isPendingDelete) { - // 非待删除状态:启用滑动删除 - return Dismissible( - key: ValueKey('${problem.id}-${problem.syncStatus}'), - direction: DismissDirection.endToStart, - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - padding: EdgeInsets.only(right: 20.w), - child: Icon(Icons.delete, color: Colors.white, size: 30.sp), - ), - confirmDismiss: (direction) async { - return await _showDeleteConfirmationDialog(problem); - }, - onDismissed: (direction) { - controller.deleteProblem(problem); - Get.snackbar('成功', '问题已删除'); - }, - child: ProblemCard( - key: ValueKey(problem.id), - problem: problem, - viewType: viewType, - isSelected: false, - ), - ); - } else { - // 待删除状态:显示普通卡片(无滑动功能) - return ProblemCard( - key: ValueKey(problem.id), - problem: problem, - viewType: viewType, - isSelected: false, - ); - } - } else { - // 其他视图类型(list、grid等):使用 Obx 监听选中状态 - return Obx(() { - final isSelected = controller.selectedProblems.contains(problem); - return ProblemCard( - key: ValueKey(problem.id), - problem: problem, - viewType: viewType, - isSelected: isSelected, - onChanged: (problem, isChecked) { - controller.updateProblemSelection(problem, isChecked); - }, - ); - }); - } - } - - Future _showDeleteConfirmationDialog(Problem problem) async { - // 确保在返回前关闭可能存在的snackbar - if (Get.isSnackbarOpen) { - Get.closeCurrentSnackbar(); - } - return await Get.bottomSheet( - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 16), - const Text( - '确认删除', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Text( - '确定要删除这个问题吗?此操作不可撤销。', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - ), - const SizedBox(height: 24), - ElevatedButton( - onPressed: () => Get.back(result: true), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: const Text( - '删除', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), - const SizedBox(height: 8), - TextButton( - onPressed: () => Get.back(result: false), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: Text( - '取消', - style: TextStyle(color: Colors.grey[700], fontSize: 16), - ), - ), - const SizedBox(height: 16), - ], - ), - ), - ), - ) ?? - false; - } -}