diff --git a/lib/core/utils/constants/api_endpoints.dart b/lib/core/utils/constants/api_endpoints.dart index 29b6f26..16e3963 100644 --- a/lib/core/utils/constants/api_endpoints.dart +++ b/lib/core/utils/constants/api_endpoints.dart @@ -9,7 +9,7 @@ abstract class ApiEndpoints { static const String patchPassword = '/api/Accounts/ChangePassword'; // 定义 Memorandum 相关的端点 - static const String getProblem = '/api/Memorandum'; + static const String getProblems = '/api/Memorandum'; static const String postProblem = '/api/Memorandum'; static String putProblemById(String id) => '/api/Memorandum/$id'; static String deleteProblemById(String id) => '/api/Memorandum/$id'; diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index e99c76a..44d87ec 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart' hide MultipartFile, FormData, Response; +import 'package:problem_check_system/core/extensions/http_response_extension.dart'; import 'package:problem_check_system/core/utils/constants/api_endpoints.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/data/providers/connectivity_provider.dart'; @@ -52,10 +53,37 @@ class ProblemRepository extends GetxService { await sqliteProvider.deleteProblem(problemId); } - /// getAll - Future getAll() async { - final response = await httpProvider.get(ApiEndpoints.getProblem); - return response; + // 在ProblemRepository中添加 + Future> fetchProblemsFromServer({ + DateTime? startTime, + DateTime? endTime, + int? pageNumber, + int? pageSize, + CancelToken? cancelToken, + }) async { + try { + final response = await httpProvider.get( + ApiEndpoints.getProblems, + queryParameters: { + 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) { + // 假设服务器返回的是Problem对象的列表 + final List data = response.data; + return data.map((json) => Problem.fromJson(json)).toList(); + } else { + throw Exception('拉取问题失败: ${response.statusCode}'); + } + } on DioException catch (e) { + throw Exception('拉取问题失败: $e'); + } } /// post diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index a5f6350..8ec5927 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -169,7 +169,7 @@ class ProblemController extends GetxController ); Get.back(); - Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM); + Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.TOP); // 上传成功后清空选中状态 clearSelection(); @@ -180,17 +180,17 @@ class ProblemController extends GetxController } on DioException catch (e) { Get.back(); if (CancelToken.isCancel(e)) { - Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.BOTTOM); + Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.TOP); } else { Get.snackbar( '上传失败', '错误: ${e.message}', - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, ); } } catch (e) { Get.back(); - Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.BOTTOM); + Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.TOP); } } @@ -366,12 +366,14 @@ class ProblemController extends GetxController // 4. 处理服务器响应 if (response.isSuccess) { + final problem = Problem.fromJson(response.data); // 更新图片状态(仅对创建和更新操作) final updatedImageMetadata = problem.syncStatus != ProblemSyncStatus.pendingDelete ? _updateImageMetadata(problem.imageUrls, remoteUrls) : problem.imageUrls; + Get.log(problem.lastModifiedTime.toUtc().toIso8601String()); // 返回同步完成的对象,操作类型重置为none return problem.copyWith( syncStatus: problem.syncStatus != ProblemSyncStatus.pendingDelete @@ -435,6 +437,89 @@ class ProblemController extends GetxController // #endregion + // #region 问题同步 + + Future pullDataFromServer() async { + isLoading.value = true; + try { + // 1. 从服务器获取最新数据 + final List serverProblems = await problemRepository + .fetchProblemsFromServer(); + + // 2. 获取本地数据 + final List localProblems = await problemRepository.getProblems(); + + // 3. 同步策略:以服务器数据为准,合并本地未上传的更改 + await _syncProblems(serverProblems, localProblems); + + // 4. 重新加载本地问题列表 + await loadProblems(); + + Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); + } catch (e) { + Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP); + } finally { + isLoading.value = false; + } + } + + /// 同步服务器和本地数据 + Future _syncProblems( + List serverProblems, + List localProblems, + ) async { + // 创建映射以便快速查找 + final Map serverProblemsMap = { + for (var problem in serverProblems.where((p) => p.id != null)) + problem.id!: problem, + }; + + final Map localProblemsMap = { + for (var problem in localProblems.where((p) => p.id != null)) + problem.id!: problem, + }; + + // 处理服务器有但本地没有的数据(新增) + for (final serverProblem in serverProblems) { + if (serverProblem.id != null && + !localProblemsMap.containsKey(serverProblem.id)) { + // 服务器新增的问题,添加到本地 + await problemRepository.insertProblem(serverProblem); + } + } + + // 处理本地有但服务器没有的数据(删除) + for (final localProblem in localProblems) { + if (localProblem.id != null && + !serverProblemsMap.containsKey(localProblem.id)) { + // 服务器已删除的问题,从本地删除 + await problemRepository.deleteProblem(localProblem.id!); + } + } + + // 处理双方都有的数据(更新) + for (final serverProblem in serverProblems) { + if (serverProblem.id != null && + localProblemsMap.containsKey(serverProblem.id)) { + final localProblem = localProblemsMap[serverProblem.id]!; + + // 只有当本地数据已同步时才更新(避免覆盖本地未上传的更改) + if (localProblem.syncStatus == ProblemSyncStatus.synced) { + // 比较更新时间,使用最新的数据 + final serverUpdated = serverProblem.lastModifiedTime; + final localUpdated = localProblem.lastModifiedTime; + + if (serverUpdated.isAfter(localUpdated)) { + // 服务器数据更新,更新本地数据 + await problemRepository.updateProblem(serverProblem); + } + } + // 如果本地有未同步的更改,保留本地更改(下次上传时会同步到服务器) + } + } + } + // #endregion + // #region 悬浮按钮 /// floatingButton更新位置 void updateFabUploadPosition(Offset delta) { diff --git a/lib/modules/problem/views/problem_list_page.dart b/lib/modules/problem/views/problem_list_page.dart index 3ce8055..ac75db2 100644 --- a/lib/modules/problem/views/problem_list_page.dart +++ b/lib/modules/problem/views/problem_list_page.dart @@ -1,6 +1,7 @@ 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/data/models/problem_sync_status.dart'; import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; @@ -23,16 +24,32 @@ class ProblemListPage extends GetView { return const Center(child: CircularProgressIndicator()); } - return 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); + 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); + }, + ), ); }); } diff --git a/pubspec.lock b/pubspec.lock index f010752..a48b1f2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -106,7 +106,7 @@ packages: source: hosted version: "2.1.1" easy_refresh: - dependency: transitive + dependency: "direct main" description: name: easy_refresh sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" diff --git a/pubspec.yaml b/pubspec.yaml index 0fe9fe4..2a3615f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: connectivity_plus: ^6.1.5 crypto: ^3.0.6 dio: ^5.9.0 + easy_refresh: ^3.4.0 flutter: sdk: flutter flutter_localizations: