From 96f1ef1efdbf3c096af1ee8f57d1417c026faf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Mon, 8 Sep 2025 17:28:12 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E6=A0=B9=E6=8D=AE=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E8=BF=9B=E8=A1=8C=E7=89=A9=E7=90=86=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E4=B8=8E=E9=80=BB=E8=BE=91=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/constants/api_endpoints.dart | 1 - lib/data/models/operation.dart | 10 - lib/data/models/problem_model.dart | 52 +--- lib/data/models/problem_sync_status.dart | 98 +++++++ lib/data/models/sync_status.dart | 7 - lib/data/providers/sqlite_provider.dart | 97 +++---- lib/data/repositories/problem_repository.dart | 200 +++----------- .../controllers/problem_controller.dart | 250 +++++++++++++++--- .../controllers/problem_form_controller.dart | 10 +- .../problem/views/problem_list_page.dart | 62 +++-- .../problem/views/widgets/problem_card.dart | 30 +-- 11 files changed, 470 insertions(+), 347 deletions(-) delete mode 100644 lib/data/models/operation.dart create mode 100644 lib/data/models/problem_sync_status.dart delete mode 100644 lib/data/models/sync_status.dart diff --git a/lib/core/utils/constants/api_endpoints.dart b/lib/core/utils/constants/api_endpoints.dart index 163d5d9..29b6f26 100644 --- a/lib/core/utils/constants/api_endpoints.dart +++ b/lib/core/utils/constants/api_endpoints.dart @@ -11,7 +11,6 @@ abstract class ApiEndpoints { // 定义 Memorandum 相关的端点 static const String getProblem = '/api/Memorandum'; static const String postProblem = '/api/Memorandum'; - static const String deleteProblem = '/api/Memorandum'; static String putProblemById(String id) => '/api/Memorandum/$id'; static String deleteProblemById(String id) => '/api/Memorandum/$id'; diff --git a/lib/data/models/operation.dart b/lib/data/models/operation.dart deleted file mode 100644 index 7658b1a..0000000 --- a/lib/data/models/operation.dart +++ /dev/null @@ -1,10 +0,0 @@ -enum Operation { - /// 创建 - create, - - /// 修改 - update, - - /// 删除 - delete, -} diff --git a/lib/data/models/problem_model.dart b/lib/data/models/problem_model.dart index eb6c8d0..8c30f5f 100644 --- a/lib/data/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -1,8 +1,7 @@ import 'dart:convert'; import 'package:problem_check_system/data/models/image_metadata_model.dart'; -import 'package:problem_check_system/data/models/operation.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:uuid/uuid.dart'; /// 问题的数据模型。 @@ -24,13 +23,10 @@ class Problem { final DateTime creationTime; /// 问题的同步状态,默认为未同步。 - final SyncStatus syncStatus; + final ProblemSyncStatus syncStatus; - /// 对问题的操作类型,默认为创建。 - final Operation operation; - - /// 是否逻辑删除 - final bool isDeleted; + /// 最后修改时间 + final DateTime lastModifiedTime; /// 相关的审查任务ID,可空。 final String? censorTaskId; @@ -41,38 +37,19 @@ class Problem { /// 问题是否已被检查,默认为false。 final bool isChecked; - /// 静态对象uuid - static final Uuid _uuid = Uuid(); - Problem({ this.id, required this.description, required this.location, required this.imageUrls, required this.creationTime, - this.syncStatus = SyncStatus.notSynced, - this.operation = Operation.create, - this.isDeleted = false, + required this.lastModifiedTime, + this.syncStatus = ProblemSyncStatus.pendingCreate, this.censorTaskId, this.bindData, this.isChecked = false, }); - /// 工厂方法:创建新问题(自动生成ID) - factory Problem.create({ - required String description, - required String location, - required List imageUrls, - }) { - return Problem( - id: _uuid.v4(), - description: description, - location: location, - imageUrls: imageUrls, - creationTime: DateTime.now(), - ); - } - /// copyWith 方法,用于创建对象的副本并修改指定字段 Problem copyWith({ String? id, @@ -80,8 +57,8 @@ class Problem { String? location, List? imageUrls, DateTime? creationTime, - SyncStatus? syncStatus, - Operation? operation, + DateTime? lastModifiedTime, + ProblemSyncStatus? syncStatus, bool? isDeleted, String? censorTaskId, String? bindData, @@ -93,9 +70,8 @@ class Problem { location: location ?? this.location, imageUrls: imageUrls ?? this.imageUrls, creationTime: creationTime ?? this.creationTime, + lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime, syncStatus: syncStatus ?? this.syncStatus, - operation: operation ?? this.operation, - isDeleted: isDeleted ?? this.isDeleted, censorTaskId: censorTaskId ?? this.censorTaskId, bindData: bindData ?? this.bindData, isChecked: isChecked ?? this.isChecked, @@ -110,9 +86,8 @@ class Problem { 'location': location, 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), 'creationTime': creationTime.millisecondsSinceEpoch, + 'lastModifiedTime': lastModifiedTime.millisecondsSinceEpoch, 'syncStatus': syncStatus.index, - 'operation': operation.index, - 'isDeleted': isDeleted ? 1 : 0, 'censorTaskId': censorTaskId, 'bindData': bindData, 'isChecked': isChecked ? 1 : 0, @@ -139,9 +114,10 @@ class Problem { location: map['location'], imageUrls: imageUrlsList, creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']), - syncStatus: SyncStatus.values[map['syncStatus']], - operation: Operation.values[map['operation']], - isDeleted: map['isDeleted'] == 1, + lastModifiedTime: DateTime.fromMillisecondsSinceEpoch( + map['lastModifiedTime'], + ), + syncStatus: ProblemSyncStatus.values[map['syncStatus']], censorTaskId: map['censorTaskId'], bindData: map['bindData'], isChecked: map['isChecked'] == 1, diff --git a/lib/data/models/problem_sync_status.dart b/lib/data/models/problem_sync_status.dart new file mode 100644 index 0000000..a37252e --- /dev/null +++ b/lib/data/models/problem_sync_status.dart @@ -0,0 +1,98 @@ +import 'package:problem_check_system/data/models/image_metadata_model.dart'; +import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:uuid/uuid.dart'; + +enum ProblemSyncStatus { + /// 未跟踪 - 需要被移除的记录(如本地删除但从未同步过) + untracked, + + /// 已同步 - 与服务器完全一致(类似git的unmodified) + synced, + + /// 待创建 - 新问题,需要上传到服务器(类似git的untracked → staged) + pendingCreate, + + /// 待更新 - 已修改的问题,需要更新到服务器(类似git的modified → staged) + pendingUpdate, + + /// 待删除 - 已标记删除,需要从服务器删除(类似git的deleted → staged) + pendingDelete, +} + +/// 问题状态管理器 - 类似 git add/git commit +class ProblemStateManager { + /// 静态对象uuid + static final Uuid _uuid = Uuid(); + + /// 创建新问题(类似创建新文件) + static Problem createNewProblem({ + required String description, + required String location, + required List imageUrls, + }) { + return Problem( + id: _uuid.v4(), + description: description, + location: location, + imageUrls: imageUrls, + creationTime: DateTime.now(), + lastModifiedTime: DateTime.now(), + syncStatus: ProblemSyncStatus.pendingCreate, + ); + } + + /// 修改问题内容(类似编辑文件) + static Problem modifyProblem(Problem problem) { + final newStatus = problem.syncStatus == ProblemSyncStatus.synced + ? ProblemSyncStatus + .pendingUpdate // 已同步的改为待更新 + : problem.syncStatus; // 保持原有待处理状态 + + return problem.copyWith( + syncStatus: newStatus, + lastModifiedTime: DateTime.now(), + ); + } + + /// 标记问题为删除 + static Problem markForDeletion(Problem problem) { + switch (problem.syncStatus) { + case ProblemSyncStatus.pendingCreate: + // 待创建的问题 → 未跟踪(直接移除) + return problem.copyWith( + syncStatus: ProblemSyncStatus.untracked, + lastModifiedTime: DateTime.now(), + ); + case ProblemSyncStatus.synced: + case ProblemSyncStatus.pendingUpdate: + // 已同步或待更新的问题 → 待删除(需要服务器操作) + return problem.copyWith( + syncStatus: ProblemSyncStatus.pendingDelete, + lastModifiedTime: DateTime.now(), + ); + case ProblemSyncStatus.untracked: + case ProblemSyncStatus.pendingDelete: + // 已经是删除相关状态,无需变化 + return problem; + } + } + + /// 撤销删除(类似 git reset) + static Problem undoDeletion(Problem problem) { + if (problem.syncStatus == ProblemSyncStatus.pendingDelete) { + return problem.copyWith( + syncStatus: ProblemSyncStatus.pendingUpdate, + lastModifiedTime: DateTime.now(), + ); + } + return problem; + } + + /// 同步成功后的状态更新(类似 git commit 成功) + static Problem markAsSynced(Problem problem) { + return problem.copyWith( + syncStatus: ProblemSyncStatus.synced, + lastModifiedTime: DateTime.now(), + ); + } +} diff --git a/lib/data/models/sync_status.dart b/lib/data/models/sync_status.dart deleted file mode 100644 index 4e6ed2e..0000000 --- a/lib/data/models/sync_status.dart +++ /dev/null @@ -1,7 +0,0 @@ -enum SyncStatus { - /// 已同步,本地无修改 - synced, - - /// 未同步到服务器 - notSynced, -} diff --git a/lib/data/providers/sqlite_provider.dart b/lib/data/providers/sqlite_provider.dart index 5031d9d..463189c 100644 --- a/lib/data/providers/sqlite_provider.dart +++ b/lib/data/providers/sqlite_provider.dart @@ -1,7 +1,7 @@ // sqlite_provider.dart import 'package:get/get.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; @@ -50,9 +50,8 @@ class SQLiteProvider extends GetxService { location TEXT NOT NULL, imageUrls TEXT NOT NULL, creationTime INTEGER NOT NULL, + lastModifiedTime INTEGER NOT NULL, syncStatus INTEGER NOT NULL, - operation INTEGER NOT NULL, - isDeleted INTEGER NOT NULL, censorTaskId TEXT, bindData TEXT, isChecked INTEGER NOT NULL @@ -90,14 +89,9 @@ class SQLiteProvider extends GetxService { /// 插入问题记录,并设置同步状态为未同步 Future insertProblem(Problem problem) async { try { - // 确保插入的问题同步状态为未同步 - final problemToInsert = problem.copyWith( - syncStatus: SyncStatus.notSynced, - ); - final result = await _database.insert( _tableName, - problemToInsert.toMap(), + problem.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); @@ -105,30 +99,28 @@ class SQLiteProvider extends GetxService { return result; } catch (e) { Get.log('插入问题失败(ID: ${problem.id}):$e', isError: true); - return 0; + throw Exception(''); } } - /// 逻辑删除问题记录,并设置同步状态为未同步 - Future deleteProblem(String id) async { + /// 从数据库物理删除问题记录 + Future deleteProblem(String problemId) async { try { - final result = await _database.update( + final result = await _database.delete( _tableName, - { - 'isDeleted': 1, - 'syncStatus': SyncStatus.notSynced.index, // 设置为未同步 - }, where: 'id = ?', - whereArgs: [id], + whereArgs: [problemId], ); if (result > 0) { - Get.log('问题逻辑删除成功,ID: $id'); + Get.log('问题删除成功,ID: $problemId'); + } else { + Get.log('未找到要删除的问题,ID: $problemId'); } return result; } catch (e) { - Get.log('逻辑删除问题失败(ID: $id):$e', isError: true); + Get.log('删除问题失败(ID: $problemId):$e', isError: true); return 0; } } @@ -154,32 +146,32 @@ class SQLiteProvider extends GetxService { } } - /// 获取需要同步的问题记录(所有同步状态为未同步的记录) - Future> getProblemsForSync() async { - try { - final results = await _database.query( - _tableName, - where: 'syncStatus = ?', - whereArgs: [SyncStatus.notSynced.index], - orderBy: 'creationTime ASC', - ); - - Get.log('找到 ${results.length} 条需要同步的记录'); - return results.map((json) => Problem.fromMap(json)).toList(); - } catch (e) { - Get.log('获取待同步问题失败:$e', isError: true); - return []; - } - } + // /// 获取需要同步的问题记录(所有同步状态为未同步的记录) + // Future> getProblemsForSync() async { + // try { + // final results = await _database.query( + // _tableName, + // where: 'syncStatus = ?', + // whereArgs: [SyncStatus.notSynced.index], + // orderBy: 'creationTime ASC', + // ); + + // Get.log('找到 ${results.length} 条需要同步的记录'); + // return results.map((json) => Problem.fromMap(json)).toList(); + // } catch (e) { + // Get.log('获取待同步问题失败:$e', isError: true); + // return []; + // } + // } /// 标记问题为已同步(在同步成功后调用) Future markAsSynced(String id) async { try { final result = await _database.update( _tableName, - {'syncStatus': SyncStatus.synced.index}, - where: 'id = ? AND syncStatus = ?', - whereArgs: [id, SyncStatus.notSynced.index], + {'syncStatus': ProblemSyncStatus.synced.index}, + where: 'id = ?', + whereArgs: [id], ); if (result > 0) { @@ -198,7 +190,7 @@ class SQLiteProvider extends GetxService { try { final results = await _database.query( _tableName, - where: 'id = ? AND isDeleted = 0', + where: 'id = ?', whereArgs: [id], limit: 1, ); @@ -216,17 +208,11 @@ class SQLiteProvider extends GetxService { DateTime? endDate, String? syncStatus, String? bindStatus, - bool includeDeleted = false, }) async { try { final whereClauses = []; final whereArgs = []; - // 删除状态筛选 - if (!includeDeleted) { - whereClauses.add('isDeleted = 0'); - } - // 时间范围筛选 if (startDate != null) { whereClauses.add('creationTime >= ?'); @@ -240,12 +226,17 @@ class SQLiteProvider extends GetxService { // 同步状态筛选 if (syncStatus != null && syncStatus != '全部') { - final statusValue = syncStatus == '已上传' - ? SyncStatus.synced.index - : SyncStatus.notSynced.index; - - whereClauses.add('syncStatus = ?'); - whereArgs.add(statusValue); + 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); + } } // 绑定状态筛选 diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index abf559b..630eda8 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -1,17 +1,10 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; -import 'package:get/get.dart' hide MultipartFile, FormData; -import 'package:problem_check_system/core/extensions/http_response_extension.dart'; +import 'package:get/get.dart' hide MultipartFile, FormData, Response; import 'package:problem_check_system/core/utils/constants/api_endpoints.dart'; -import 'package:problem_check_system/data/models/image_status.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; -import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/data/providers/connectivity_provider.dart'; import 'package:problem_check_system/data/providers/http_provider.dart'; import 'package:problem_check_system/data/providers/sqlite_provider.dart'; -import 'package:problem_check_system/data/repositories/file_repository.dart'; /// 问题仓库,负责处理问题数据的本地持久化。 /// 它封装了底层数据库操作,为业务逻辑层提供一个简洁的接口。 @@ -19,7 +12,6 @@ class ProblemRepository extends GetxService { final SQLiteProvider sqliteProvider; final HttpProvider httpProvider; final ConnectivityProvider connectivityProvider; - final FileRepository fileRepository = Get.find(); RxBool get isOnline => connectivityProvider.isOnline; @@ -30,18 +22,8 @@ class ProblemRepository extends GetxService { }); /// 更新本地数据库中的一个问题。 - /// 如果问题存在则更新,如果不存在则插入。 Future updateProblem(Problem problem) async { - // 检查问题是否存在,通常通过ID判断 - final existingProblem = await sqliteProvider.getProblemById(problem.id!); - - if (existingProblem != null) { - // 问题已存在,执行更新操作 - await sqliteProvider.updateProblem(problem); - } else { - // 问题不存在,执行插入操作 - await sqliteProvider.insertProblem(problem); - } + await sqliteProvider.updateProblem(problem); } /// 通用查询方法,根据可选的筛选条件获取问题列表。 @@ -66,151 +48,51 @@ class ProblemRepository extends GetxService { await sqliteProvider.insertProblem(problem); } - Future deleteProblem(String id) async { - await sqliteProvider.deleteProblem(id); + Future deleteProblem(String problemId) async { + await sqliteProvider.deleteProblem(problemId); } - // * /api/Objects/association/${file.name} - /// 上传单个问题及其所有关联的图片。 - Future uploadProblem( - Problem problem, { - required CancelToken cancelToken, - required void Function(double progress) onProgress, - }) async { - try { - // 找到需要上传的图片 - final newImages = problem.imageUrls - .where((img) => img.status == ImageStatus.local) - .toList(); - - final totalFilesToUpload = newImages.length; - int filesUploadedCount = 0; - - // 1. 上传所有状态为 ImageStatus.local 的新图片 - final List remoteUrls = []; - for (var image in newImages) { - // 修正:当上传被取消时,抛出异常而不是无返回值的 return - 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); // 确保图片上传进度为100% - - // 2. 构建 API payload - final List finalRemoteUrls = []; - int newImageIndex = 0; - for (var image in problem.imageUrls) { - if (image.status == ImageStatus.synced) { - finalRemoteUrls.add(image.remoteUrl!); - } else if (image.status == ImageStatus.local) { - finalRemoteUrls.add(remoteUrls[newImageIndex]); - newImageIndex++; - } - } - - final apiPayload = { - 'id': problem.id, - 'title': problem.description, - 'location': problem.location, - 'imageUrls': finalRemoteUrls, - 'creationTime': problem.creationTime.toUtc().toIso8601String(), - }; - - // 3. 发送给服务器 - final response = await httpProvider.post( - ApiEndpoints.postProblem, - data: apiPayload, - cancelToken: cancelToken, - ); - - // 4. 处理服务器响应,并更新本地模型状态 - if (response.isSuccess) { - final List updatedImageMetadata = []; - int uploadedUrlIndex = 0; - for (var image in problem.imageUrls) { - if (image.status == ImageStatus.local) { - updatedImageMetadata.add( - ImageMetadata( - localPath: image.localPath, - remoteUrl: remoteUrls[uploadedUrlIndex], - status: ImageStatus.synced, - ), - ); - uploadedUrlIndex++; - } else { - updatedImageMetadata.add(image); - } - } - - // 返回一个包含新同步状态和更新后图片列表的对象 - return problem.copyWith( - syncStatus: SyncStatus.synced, - imageUrls: updatedImageMetadata, - ); - } else { - throw Exception('问题上传失败,状态码: ${response.statusCode}'); - } - } on DioException { - rethrow; - } + /// getAll + Future getAll() async { + final response = await httpProvider.get(ApiEndpoints.getProblem); + return response; } - /// 新增:上传问题列表。 - /// 遍历问题列表,并计算总进度。 - 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); - }, - ); + /// post + Future post( + Map apiPayload, + CancelToken cancelToken, + ) async { + // 3. 发送给服务器 + final response = await httpProvider.post( + ApiEndpoints.postProblem, + data: apiPayload, + cancelToken: cancelToken, + ); + return response; + } - sqliteProvider.updateProblem(updatedProblem); - // updatedProblems.add(updatedProblem); - } - // return updatedProblems; - } on DioException { - rethrow; - } + /// put + Future put( + Map apiPayload, + CancelToken cancelToken, + ) async { + // 3. 发送给服务器 + final response = await httpProvider.post( + ApiEndpoints.postProblem, + data: apiPayload, + cancelToken: cancelToken, + ); + return response; } - getProblemsForSync() { - return sqliteProvider.getProblemsForSync(); + /// 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/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index 525713c..de95db1 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -4,10 +4,14 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart' hide MultipartFile, FormData; +import 'package:get/get.dart' hide MultipartFile, FormData, Response; import 'package:flutter/material.dart'; import 'package:problem_check_system/app/routes/app_routes.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; +import 'package:problem_check_system/core/extensions/http_response_extension.dart'; +import 'package:problem_check_system/data/models/image_metadata_model.dart'; +import 'package:problem_check_system/data/models/image_status.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; +import 'package:problem_check_system/data/repositories/file_repository.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/models/date_range_enum.dart'; @@ -17,6 +21,7 @@ class ProblemController extends GetxController with GetSingleTickerProviderStateMixin { /// 依赖问题数据 final ProblemRepository problemRepository; + final FileRepository fileRepository = Get.find(); /// 最近问题列表 final RxList problems = [].obs; @@ -39,7 +44,7 @@ class ProblemController extends GetxController /// 选中未上传的数量 int get selectedUnUploadCount => _selectedProblems - .where((p) => p.syncStatus == SyncStatus.notSynced) + .where((p) => p.syncStatus != ProblemSyncStatus.synced) .length; // 在 ProblemController 中添加 @@ -68,7 +73,15 @@ class ProblemController extends GetxController final Rx historyStartTime = DateTime.now() .subtract(const Duration(days: 7)) .obs; - final Rx historyEndTime = DateTime.now().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; @@ -147,7 +160,7 @@ class ProblemController extends GetxController showUploadProgressDialog(); try { - await problemRepository.uploadProblems( + await uploadProblems( _selectedProblems.toList(), // 转换为列表 cancelToken: _cancelToken, onProgress: (progress) { @@ -226,14 +239,187 @@ class ProblemController extends GetxController // Get.snackbar('提示', '上传已取消'); } - void uploadProblems() async { - // if (selectedUnUploadedProblems.isEmpty) return; - // // 实际的上传逻辑,例如调用 API - // // 上传完成后,清空列表或更新状态 - // selectedUnUploadedProblems.clear(); - // for (var problem in selectedUnUploadedProblems) { - // await uploadProblem(problem); - // } + /// 新增:上传问题列表。 + /// 遍历问题列表,并计算总进度。 + 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); + }, + ); + + problemRepository.updateProblem(updatedProblem); + // updatedProblems.add(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.local) + .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(apiPayload!, cancelToken); + break; + case ProblemSyncStatus.pendingDelete: + response = await problemRepository.delete(problem.id!, cancelToken); + break; + } + + // 4. 处理服务器响应 + if (response.isSuccess) { + // 更新图片状态(仅对创建和更新操作) + final updatedImageMetadata = + problem.syncStatus != ProblemSyncStatus.pendingDelete + ? _updateImageMetadata(problem.imageUrls, remoteUrls) + : problem.imageUrls; + + // 返回同步完成的对象,操作类型重置为none + return problem.copyWith( + syncStatus: ProblemSyncStatus.synced, // 同步完成,重置为none + imageUrls: updatedImageMetadata, + ); + } 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.local) { + 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.local) { + updatedImageMetadata.add( + ImageMetadata( + localPath: image.localPath, + remoteUrl: newRemoteUrls[uploadedUrlIndex], + status: ImageStatus.synced, + ), + ); + uploadedUrlIndex++; + } else { + updatedImageMetadata.add(image); + } + } + + return updatedImageMetadata; } // #endregion @@ -406,12 +592,14 @@ class ProblemController extends GetxController loadUnUploadedProblems(); } - // 新增方法:查询所有未上传的问题 + // 查询所有未上传的问题 Future loadUnUploadedProblems() async { isLoading.value = true; try { // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 - unUploadedProblems.value = await problemRepository.getProblemsForSync(); + unUploadedProblems.value = await problemRepository.getProblems( + syncStatus: '未上传', + ); } catch (e) { Get.snackbar('错误', '加载未上传问题失败: $e'); } finally { @@ -419,33 +607,21 @@ class ProblemController extends GetxController } } - // Future addProblem(Problem problem) async { - // try { - // await problemRepository.insertProblem(problem); - // loadProblems(); - // } catch (e) { - // Get.snackbar('错误', '保存问题失败: $e'); - // rethrow; - // } - // } - - // Future updateProblem(Problem problem) async { - // try { - // await problemRepository.updateProblem(problem); - // loadProblems(); - // } catch (e) { - // Get.snackbar('错误', '更新问题失败: $e'); - // rethrow; - // } - // } - + /// 删除问题 + /// 控制器中可以添加逻辑 Future deleteProblem(Problem problem) async { try { - if (problem.id != null) { + final deleteProblem = ProblemStateManager.markForDeletion(problem); + if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) { + // 直接删除问题和图片 await problemRepository.deleteProblem(problem.id!); await _deleteProblemImages(problem); - loadProblems(); + } else { + // 更新状态 + await problemRepository.updateProblem(deleteProblem); } + + loadProblems(); } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); rethrow; diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart index 8c40182..3a4b671 100644 --- a/lib/modules/problem/controllers/problem_form_controller.dart +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -8,6 +8,7 @@ import 'package:problem_check_system/data/models/image_status.dart'; import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'dart:io'; import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; class ProblemFormController extends GetxController { @@ -141,25 +142,26 @@ class ProblemFormController extends GetxController { 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(updatedProblem); + await problemRepository.updateProblem(modifyProblem); Get.back(result: true); // 返回成功结果 Get.snackbar('成功', '问题已更新'); } else { // 新增模式:创建新问题 - final problem = Problem.create( + final newProblem = ProblemStateManager.createNewProblem( description: descriptionController.text, location: locationController.text, imageUrls: imagePaths, ); - await problemRepository.insertProblem(problem); + await problemRepository.insertProblem(newProblem); Get.back(result: true); // 返回成功结果 Get.snackbar('成功', '问题已保存'); } diff --git a/lib/modules/problem/views/problem_list_page.dart b/lib/modules/problem/views/problem_list_page.dart index 8654e00..3ce8055 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: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'; import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; @@ -37,40 +38,55 @@ class ProblemListPage extends GetView { } Widget _buildSwipeableProblemCard(Problem problem) { - // 只有在视图类型是 buttons 时,才启用滑动删除 + // 对于所有视图类型,如果是待删除状态,都禁用交互 + final bool isPendingDelete = + problem.syncStatus == ProblemSyncStatus.pendingDelete; + if (viewType == ProblemCardViewType.buttons) { - return Dismissible( - key: Key(problem.id ?? UniqueKey().toString()), - 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( + // 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, // 非选择模式,默认false - ), - ); + isSelected: false, + ); + } } else { + // 其他视图类型(list、grid等):使用 Obx 监听选中状态 return Obx(() { - // 使用 Obx 来监听选中状态的变化 final isSelected = controller.selectedProblems.contains(problem); return ProblemCard( key: ValueKey(problem.id), problem: problem, viewType: viewType, - isSelected: isSelected, // 传递选中状态 + isSelected: isSelected, onChanged: (problem, isChecked) { controller.updateProblemSelection(problem, isChecked); }, diff --git a/lib/modules/problem/views/widgets/problem_card.dart b/lib/modules/problem/views/widgets/problem_card.dart index f3975e3..9471e70 100644 --- a/lib/modules/problem/views/widgets/problem_card.dart +++ b/lib/modules/problem/views/widgets/problem_card.dart @@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:problem_check_system/app/routes/app_routes.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; +import 'package:problem_check_system/data/models/problem_sync_status.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; @@ -29,7 +29,8 @@ class ProblemCard extends StatelessWidget { @override Widget build(BuildContext context) { // 根据是否已删除决定卡片的颜色 - final bool isDeleted = problem.isDeleted; + final bool isDeleted = + problem.syncStatus == ProblemSyncStatus.pendingDelete; final Color cardColor = isDeleted ? Colors.grey[300]! : Theme.of(context).cardColor; @@ -101,19 +102,20 @@ class ProblemCard extends StatelessWidget { Wrap( spacing: 8, children: [ - ...problem.isDeleted + ...problem.syncStatus == ProblemSyncStatus.pendingDelete ? [ TDTag( '已删除', - theme: TDTagTheme.danger, + isLight: true, + theme: TDTagTheme.defaultTheme, textColor: isDeleted ? Colors.grey[700] : null, - backgroundColor: isDeleted - ? Colors.grey[400] - : null, + // backgroundColor: isDeleted + // ? Colors.grey[400] + // : null, ), ] : [ - problem.syncStatus == SyncStatus.synced + problem.syncStatus == ProblemSyncStatus.synced ? TDTag( '已上传', isLight: true, @@ -232,13 +234,11 @@ class ProblemCard extends StatelessWidget { padding: EdgeInsets.only(right: 16.w), child: Checkbox( value: isSelected, - onChanged: isDeleted - ? null - : (bool? value) { - if (value != null) { - onChanged?.call(problem, value); - } - }, + onChanged: (bool? value) { + if (value != null) { + onChanged?.call(problem, value); + } + }, ), ); }