Browse Source

feat : 根据问题状态进行物理删除与逻辑删除

dev
徐振升 1 day ago
parent
commit
96f1ef1efd
  1. 1
      lib/core/utils/constants/api_endpoints.dart
  2. 10
      lib/data/models/operation.dart
  3. 52
      lib/data/models/problem_model.dart
  4. 98
      lib/data/models/problem_sync_status.dart
  5. 7
      lib/data/models/sync_status.dart
  6. 97
      lib/data/providers/sqlite_provider.dart
  7. 200
      lib/data/repositories/problem_repository.dart
  8. 250
      lib/modules/problem/controllers/problem_controller.dart
  9. 10
      lib/modules/problem/controllers/problem_form_controller.dart
  10. 62
      lib/modules/problem/views/problem_list_page.dart
  11. 30
      lib/modules/problem/views/widgets/problem_card.dart

1
lib/core/utils/constants/api_endpoints.dart

@ -11,7 +11,6 @@ abstract class ApiEndpoints {
// Memorandum // Memorandum
static const String getProblem = '/api/Memorandum'; static const String getProblem = '/api/Memorandum';
static const String postProblem = '/api/Memorandum'; static const String postProblem = '/api/Memorandum';
static const String deleteProblem = '/api/Memorandum';
static String putProblemById(String id) => '/api/Memorandum/$id'; static String putProblemById(String id) => '/api/Memorandum/$id';
static String deleteProblemById(String id) => '/api/Memorandum/$id'; static String deleteProblemById(String id) => '/api/Memorandum/$id';

10
lib/data/models/operation.dart

@ -1,10 +0,0 @@
enum Operation {
///
create,
///
update,
///
delete,
}

52
lib/data/models/problem_model.dart

@ -1,8 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:problem_check_system/data/models/image_metadata_model.dart'; 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/problem_sync_status.dart';
import 'package:problem_check_system/data/models/sync_status.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
/// ///
@ -24,13 +23,10 @@ class Problem {
final DateTime creationTime; final DateTime creationTime;
/// ///
final SyncStatus syncStatus; final ProblemSyncStatus syncStatus;
/// ///
final Operation operation; final DateTime lastModifiedTime;
///
final bool isDeleted;
/// ID /// ID
final String? censorTaskId; final String? censorTaskId;
@ -41,38 +37,19 @@ class Problem {
/// false /// false
final bool isChecked; final bool isChecked;
/// uuid
static final Uuid _uuid = Uuid();
Problem({ Problem({
this.id, this.id,
required this.description, required this.description,
required this.location, required this.location,
required this.imageUrls, required this.imageUrls,
required this.creationTime, required this.creationTime,
this.syncStatus = SyncStatus.notSynced, required this.lastModifiedTime,
this.operation = Operation.create, this.syncStatus = ProblemSyncStatus.pendingCreate,
this.isDeleted = false,
this.censorTaskId, this.censorTaskId,
this.bindData, this.bindData,
this.isChecked = false, this.isChecked = false,
}); });
/// ID
factory Problem.create({
required String description,
required String location,
required List<ImageMetadata> imageUrls,
}) {
return Problem(
id: _uuid.v4(),
description: description,
location: location,
imageUrls: imageUrls,
creationTime: DateTime.now(),
);
}
/// copyWith /// copyWith
Problem copyWith({ Problem copyWith({
String? id, String? id,
@ -80,8 +57,8 @@ class Problem {
String? location, String? location,
List<ImageMetadata>? imageUrls, List<ImageMetadata>? imageUrls,
DateTime? creationTime, DateTime? creationTime,
SyncStatus? syncStatus, DateTime? lastModifiedTime,
Operation? operation, ProblemSyncStatus? syncStatus,
bool? isDeleted, bool? isDeleted,
String? censorTaskId, String? censorTaskId,
String? bindData, String? bindData,
@ -93,9 +70,8 @@ class Problem {
location: location ?? this.location, location: location ?? this.location,
imageUrls: imageUrls ?? this.imageUrls, imageUrls: imageUrls ?? this.imageUrls,
creationTime: creationTime ?? this.creationTime, creationTime: creationTime ?? this.creationTime,
lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime,
syncStatus: syncStatus ?? this.syncStatus, syncStatus: syncStatus ?? this.syncStatus,
operation: operation ?? this.operation,
isDeleted: isDeleted ?? this.isDeleted,
censorTaskId: censorTaskId ?? this.censorTaskId, censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData, bindData: bindData ?? this.bindData,
isChecked: isChecked ?? this.isChecked, isChecked: isChecked ?? this.isChecked,
@ -110,9 +86,8 @@ class Problem {
'location': location, 'location': location,
'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()),
'creationTime': creationTime.millisecondsSinceEpoch, 'creationTime': creationTime.millisecondsSinceEpoch,
'lastModifiedTime': lastModifiedTime.millisecondsSinceEpoch,
'syncStatus': syncStatus.index, 'syncStatus': syncStatus.index,
'operation': operation.index,
'isDeleted': isDeleted ? 1 : 0,
'censorTaskId': censorTaskId, 'censorTaskId': censorTaskId,
'bindData': bindData, 'bindData': bindData,
'isChecked': isChecked ? 1 : 0, 'isChecked': isChecked ? 1 : 0,
@ -139,9 +114,10 @@ class Problem {
location: map['location'], location: map['location'],
imageUrls: imageUrlsList, imageUrls: imageUrlsList,
creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']), creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']),
syncStatus: SyncStatus.values[map['syncStatus']], lastModifiedTime: DateTime.fromMillisecondsSinceEpoch(
operation: Operation.values[map['operation']], map['lastModifiedTime'],
isDeleted: map['isDeleted'] == 1, ),
syncStatus: ProblemSyncStatus.values[map['syncStatus']],
censorTaskId: map['censorTaskId'], censorTaskId: map['censorTaskId'],
bindData: map['bindData'], bindData: map['bindData'],
isChecked: map['isChecked'] == 1, isChecked: map['isChecked'] == 1,

98
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<ImageMetadata> 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(),
);
}
}

7
lib/data/models/sync_status.dart

@ -1,7 +0,0 @@
enum SyncStatus {
///
synced,
///
notSynced,
}

97
lib/data/providers/sqlite_provider.dart

@ -1,7 +1,7 @@
// sqlite_provider.dart // sqlite_provider.dart
import 'package:get/get.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:problem_check_system/data/models/problem_model.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -50,9 +50,8 @@ class SQLiteProvider extends GetxService {
location TEXT NOT NULL, location TEXT NOT NULL,
imageUrls TEXT NOT NULL, imageUrls TEXT NOT NULL,
creationTime INTEGER NOT NULL, creationTime INTEGER NOT NULL,
lastModifiedTime INTEGER NOT NULL,
syncStatus INTEGER NOT NULL, syncStatus INTEGER NOT NULL,
operation INTEGER NOT NULL,
isDeleted INTEGER NOT NULL,
censorTaskId TEXT, censorTaskId TEXT,
bindData TEXT, bindData TEXT,
isChecked INTEGER NOT NULL isChecked INTEGER NOT NULL
@ -90,14 +89,9 @@ class SQLiteProvider extends GetxService {
/// ///
Future<int> insertProblem(Problem problem) async { Future<int> insertProblem(Problem problem) async {
try { try {
//
final problemToInsert = problem.copyWith(
syncStatus: SyncStatus.notSynced,
);
final result = await _database.insert( final result = await _database.insert(
_tableName, _tableName,
problemToInsert.toMap(), problem.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
@ -105,30 +99,28 @@ class SQLiteProvider extends GetxService {
return result; return result;
} catch (e) { } catch (e) {
Get.log('插入问题失败(ID: ${problem.id}):$e', isError: true); Get.log('插入问题失败(ID: ${problem.id}):$e', isError: true);
return 0; throw Exception('');
} }
} }
/// ///
Future<int> deleteProblem(String id) async { Future<int> deleteProblem(String problemId) async {
try { try {
final result = await _database.update( final result = await _database.delete(
_tableName, _tableName,
{
'isDeleted': 1,
'syncStatus': SyncStatus.notSynced.index, //
},
where: 'id = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [problemId],
); );
if (result > 0) { if (result > 0) {
Get.log('问题逻辑删除成功,ID: $id'); Get.log('问题删除成功,ID: $problemId');
} else {
Get.log('未找到要删除的问题,ID: $problemId');
} }
return result; return result;
} catch (e) { } catch (e) {
Get.log('逻辑删除问题失败(ID: $id):$e', isError: true); Get.log('删除问题失败(ID: $problemId):$e', isError: true);
return 0; return 0;
} }
} }
@ -154,32 +146,32 @@ class SQLiteProvider extends GetxService {
} }
} }
/// // ///
Future<List<Problem>> getProblemsForSync() async { // Future<List<Problem>> getProblemsForSync() async {
try { // try {
final results = await _database.query( // final results = await _database.query(
_tableName, // _tableName,
where: 'syncStatus = ?', // where: 'syncStatus = ?',
whereArgs: [SyncStatus.notSynced.index], // whereArgs: [SyncStatus.notSynced.index],
orderBy: 'creationTime ASC', // orderBy: 'creationTime ASC',
); // );
Get.log('找到 ${results.length} 条需要同步的记录'); // Get.log('找到 ${results.length} 条需要同步的记录');
return results.map((json) => Problem.fromMap(json)).toList(); // return results.map((json) => Problem.fromMap(json)).toList();
} catch (e) { // } catch (e) {
Get.log('获取待同步问题失败:$e', isError: true); // Get.log('获取待同步问题失败:$e', isError: true);
return []; // return [];
} // }
} // }
/// ///
Future<int> markAsSynced(String id) async { Future<int> markAsSynced(String id) async {
try { try {
final result = await _database.update( final result = await _database.update(
_tableName, _tableName,
{'syncStatus': SyncStatus.synced.index}, {'syncStatus': ProblemSyncStatus.synced.index},
where: 'id = ? AND syncStatus = ?', where: 'id = ?',
whereArgs: [id, SyncStatus.notSynced.index], whereArgs: [id],
); );
if (result > 0) { if (result > 0) {
@ -198,7 +190,7 @@ class SQLiteProvider extends GetxService {
try { try {
final results = await _database.query( final results = await _database.query(
_tableName, _tableName,
where: 'id = ? AND isDeleted = 0', where: 'id = ?',
whereArgs: [id], whereArgs: [id],
limit: 1, limit: 1,
); );
@ -216,17 +208,11 @@ class SQLiteProvider extends GetxService {
DateTime? endDate, DateTime? endDate,
String? syncStatus, String? syncStatus,
String? bindStatus, String? bindStatus,
bool includeDeleted = false,
}) async { }) async {
try { try {
final whereClauses = <String>[]; final whereClauses = <String>[];
final whereArgs = <dynamic>[]; final whereArgs = <dynamic>[];
//
if (!includeDeleted) {
whereClauses.add('isDeleted = 0');
}
// //
if (startDate != null) { if (startDate != null) {
whereClauses.add('creationTime >= ?'); whereClauses.add('creationTime >= ?');
@ -240,12 +226,17 @@ class SQLiteProvider extends GetxService {
// //
if (syncStatus != null && syncStatus != '全部') { if (syncStatus != null && syncStatus != '全部') {
final statusValue = syncStatus == '已上传' if (syncStatus == '未上传') {
? SyncStatus.synced.index whereClauses.add('syncStatus IN (?, ?, ?)');
: SyncStatus.notSynced.index; whereArgs.addAll([
ProblemSyncStatus.pendingCreate.index,
whereClauses.add('syncStatus = ?'); ProblemSyncStatus.pendingUpdate.index,
whereArgs.add(statusValue); ProblemSyncStatus.pendingDelete.index,
]);
} else {
whereClauses.add('syncStatus = ?');
whereArgs.add(ProblemSyncStatus.synced.index);
}
} }
// //

200
lib/data/repositories/problem_repository.dart

@ -1,17 +1,10 @@
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:get/get.dart' hide MultipartFile, FormData; 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/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/models/problem_model.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.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/http_provider.dart';
import 'package:problem_check_system/data/providers/sqlite_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 SQLiteProvider sqliteProvider;
final HttpProvider httpProvider; final HttpProvider httpProvider;
final ConnectivityProvider connectivityProvider; final ConnectivityProvider connectivityProvider;
final FileRepository fileRepository = Get.find<FileRepository>();
RxBool get isOnline => connectivityProvider.isOnline; RxBool get isOnline => connectivityProvider.isOnline;
@ -30,18 +22,8 @@ class ProblemRepository extends GetxService {
}); });
/// ///
///
Future<void> updateProblem(Problem problem) async { Future<void> updateProblem(Problem problem) async {
// ID判断 await sqliteProvider.updateProblem(problem);
final existingProblem = await sqliteProvider.getProblemById(problem.id!);
if (existingProblem != null) {
//
await sqliteProvider.updateProblem(problem);
} else {
//
await sqliteProvider.insertProblem(problem);
}
} }
/// ///
@ -66,151 +48,51 @@ class ProblemRepository extends GetxService {
await sqliteProvider.insertProblem(problem); await sqliteProvider.insertProblem(problem);
} }
Future<void> deleteProblem(String id) async { Future<void> deleteProblem(String problemId) async {
await sqliteProvider.deleteProblem(id); await sqliteProvider.deleteProblem(problemId);
} }
// * /api/Objects/association/${file.name} /// getAll
/// Future<Response> getAll() async {
Future<Problem> uploadProblem( final response = await httpProvider.get(ApiEndpoints.getProblem);
Problem problem, { return response;
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<String> 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<String> 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<ImageMetadata> 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;
}
} }
/// /// post
/// Future<Response> post(
Future<void> uploadProblems( Map<String, Object?> apiPayload,
List<Problem> problems, { CancelToken cancelToken,
required CancelToken cancelToken, ) async {
required void Function(double progress) onProgress, // 3.
}) async { final response = await httpProvider.post(
final int totalProblems = problems.length; ApiEndpoints.postProblem,
// final List<Problem> updatedProblems = []; data: apiPayload,
cancelToken: cancelToken,
try { );
for (int i = 0; i < totalProblems; i++) { return response;
// }
if (cancelToken.isCancelled) {
break;
}
final problemToUpload = problems[i];
//
final updatedProblem = await uploadProblem(
problemToUpload,
cancelToken: cancelToken,
onProgress: (progress) {
// ( + ) /
final overallProgress = (i + progress) / totalProblems;
onProgress(overallProgress);
},
);
sqliteProvider.updateProblem(updatedProblem); /// put
// updatedProblems.add(updatedProblem); Future<Response> put(
} Map<String, Object?> apiPayload,
// return updatedProblems; CancelToken cancelToken,
} on DioException { ) async {
rethrow; // 3.
} final response = await httpProvider.post(
ApiEndpoints.postProblem,
data: apiPayload,
cancelToken: cancelToken,
);
return response;
} }
getProblemsForSync() { /// delete
return sqliteProvider.getProblemsForSync(); Future<Response> delete(String id, CancelToken cancelToken) async {
// 3.
final response = await httpProvider.delete(
ApiEndpoints.deleteProblemById(id),
cancelToken: cancelToken,
);
return response;
} }
} }

250
lib/modules/problem/controllers/problem_controller.dart

@ -4,10 +4,14 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_screenutil/flutter_screenutil.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:flutter/material.dart';
import 'package:problem_check_system/app/routes/app_routes.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/repositories/problem_repository.dart';
import 'package:problem_check_system/data/models/problem_model.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'; import 'package:problem_check_system/modules/problem/views/widgets/models/date_range_enum.dart';
@ -17,6 +21,7 @@ class ProblemController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
/// ///
final ProblemRepository problemRepository; final ProblemRepository problemRepository;
final FileRepository fileRepository = Get.find<FileRepository>();
/// ///
final RxList<Problem> problems = <Problem>[].obs; final RxList<Problem> problems = <Problem>[].obs;
@ -39,7 +44,7 @@ class ProblemController extends GetxController
/// ///
int get selectedUnUploadCount => _selectedProblems int get selectedUnUploadCount => _selectedProblems
.where((p) => p.syncStatus == SyncStatus.notSynced) .where((p) => p.syncStatus != ProblemSyncStatus.synced)
.length; .length;
// ProblemController // ProblemController
@ -68,7 +73,15 @@ class ProblemController extends GetxController
final Rx<DateTime> historyStartTime = DateTime.now() final Rx<DateTime> historyStartTime = DateTime.now()
.subtract(const Duration(days: 7)) .subtract(const Duration(days: 7))
.obs; .obs;
final Rx<DateTime> historyEndTime = DateTime.now().obs; final Rx<DateTime> historyEndTime = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
23,
59,
59,
999,
).obs;
final RxString historyUploadFilter = '全部'.obs; final RxString historyUploadFilter = '全部'.obs;
final RxString historyBindFilter = '全部'.obs; final RxString historyBindFilter = '全部'.obs;
@ -147,7 +160,7 @@ class ProblemController extends GetxController
showUploadProgressDialog(); showUploadProgressDialog();
try { try {
await problemRepository.uploadProblems( await uploadProblems(
_selectedProblems.toList(), // _selectedProblems.toList(), //
cancelToken: _cancelToken, cancelToken: _cancelToken,
onProgress: (progress) { onProgress: (progress) {
@ -226,14 +239,187 @@ class ProblemController extends GetxController
// Get.snackbar('提示', '上传已取消'); // Get.snackbar('提示', '上传已取消');
} }
void uploadProblems() async { ///
// if (selectedUnUploadedProblems.isEmpty) return; ///
// // API Future<void> uploadProblems(
// // List<Problem> problems, {
// selectedUnUploadedProblems.clear(); required CancelToken cancelToken,
// for (var problem in selectedUnUploadedProblems) { required void Function(double progress) onProgress,
// await uploadProblem(problem); }) async {
// } final int totalProblems = problems.length;
// final List<Problem> 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<Problem> 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<String> 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 payloadpayload
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<String> _buildFinalRemoteUrls(
List<ImageMetadata> images,
List<String> newRemoteUrls,
) {
final List<String> 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<ImageMetadata> _updateImageMetadata(
List<ImageMetadata> images,
List<String> newRemoteUrls,
) {
final List<ImageMetadata> 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 // #endregion
@ -406,12 +592,14 @@ class ProblemController extends GetxController
loadUnUploadedProblems(); loadUnUploadedProblems();
} }
// //
Future<void> loadUnUploadedProblems() async { Future<void> loadUnUploadedProblems() async {
isLoading.value = true; isLoading.value = true;
try { try {
// _localDatabase.getProblems '未上传' // _localDatabase.getProblems '未上传'
unUploadedProblems.value = await problemRepository.getProblemsForSync(); unUploadedProblems.value = await problemRepository.getProblems(
syncStatus: '未上传',
);
} catch (e) { } catch (e) {
Get.snackbar('错误', '加载未上传问题失败: $e'); Get.snackbar('错误', '加载未上传问题失败: $e');
} finally { } finally {
@ -419,33 +607,21 @@ class ProblemController extends GetxController
} }
} }
// Future<void> addProblem(Problem problem) async { ///
// try { ///
// await problemRepository.insertProblem(problem);
// loadProblems();
// } catch (e) {
// Get.snackbar('错误', '保存问题失败: $e');
// rethrow;
// }
// }
// Future<void> updateProblem(Problem problem) async {
// try {
// await problemRepository.updateProblem(problem);
// loadProblems();
// } catch (e) {
// Get.snackbar('错误', '更新问题失败: $e');
// rethrow;
// }
// }
Future<void> deleteProblem(Problem problem) async { Future<void> deleteProblem(Problem problem) async {
try { try {
if (problem.id != null) { final deleteProblem = ProblemStateManager.markForDeletion(problem);
if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) {
//
await problemRepository.deleteProblem(problem.id!); await problemRepository.deleteProblem(problem.id!);
await _deleteProblemImages(problem); await _deleteProblemImages(problem);
loadProblems(); } else {
//
await problemRepository.updateProblem(deleteProblem);
} }
loadProblems();
} catch (e) { } catch (e) {
Get.snackbar('错误', '删除问题失败: $e'); Get.snackbar('错误', '删除问题失败: $e');
rethrow; rethrow;

10
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 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'dart:io'; import 'dart:io';
import 'package:problem_check_system/data/models/problem_model.dart'; 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'; import 'package:problem_check_system/data/repositories/problem_repository.dart';
class ProblemFormController extends GetxController { class ProblemFormController extends GetxController {
@ -141,25 +142,26 @@ class ProblemFormController extends GetxController {
final List<ImageMetadata> imagePaths = await _saveImagesToLocal(); final List<ImageMetadata> imagePaths = await _saveImagesToLocal();
if (problem != null) { if (problem != null) {
//
final updatedProblem = problem!.copyWith( final updatedProblem = problem!.copyWith(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
); );
//
final modifyProblem = ProblemStateManager.modifyProblem(updatedProblem);
await problemRepository.updateProblem(updatedProblem); await problemRepository.updateProblem(modifyProblem);
Get.back(result: true); // Get.back(result: true); //
Get.snackbar('成功', '问题已更新'); Get.snackbar('成功', '问题已更新');
} else { } else {
// //
final problem = Problem.create( final newProblem = ProblemStateManager.createNewProblem(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
); );
await problemRepository.insertProblem(problem); await problemRepository.insertProblem(newProblem);
Get.back(result: true); // Get.back(result: true); //
Get.snackbar('成功', '问题已保存'); Get.snackbar('成功', '问题已保存');
} }

62
lib/modules/problem/views/problem_list_page.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.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/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
@ -37,40 +38,55 @@ class ProblemListPage extends GetView<ProblemController> {
} }
Widget _buildSwipeableProblemCard(Problem problem) { Widget _buildSwipeableProblemCard(Problem problem) {
// buttons //
final bool isPendingDelete =
problem.syncStatus == ProblemSyncStatus.pendingDelete;
if (viewType == ProblemCardViewType.buttons) { if (viewType == ProblemCardViewType.buttons) {
return Dismissible( // buttons
key: Key(problem.id ?? UniqueKey().toString()), if (!isPendingDelete) {
direction: DismissDirection.endToStart, //
background: Container( return Dismissible(
color: Colors.red, key: ValueKey('${problem.id}-${problem.syncStatus}'),
alignment: Alignment.centerRight, direction: DismissDirection.endToStart,
padding: EdgeInsets.only(right: 20.w), background: Container(
child: Icon(Icons.delete, color: Colors.white, size: 30.sp), color: Colors.red,
), alignment: Alignment.centerRight,
confirmDismiss: (direction) async { padding: EdgeInsets.only(right: 20.w),
return await _showDeleteConfirmationDialog(problem); child: Icon(Icons.delete, color: Colors.white, size: 30.sp),
}, ),
onDismissed: (direction) { confirmDismiss: (direction) async {
controller.deleteProblem(problem); return await _showDeleteConfirmationDialog(problem);
Get.snackbar('成功', '问题已删除'); },
}, onDismissed: (direction) {
child: ProblemCard( controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除');
},
child: ProblemCard(
key: ValueKey(problem.id),
problem: problem,
viewType: viewType,
isSelected: false,
),
);
} else {
//
return ProblemCard(
key: ValueKey(problem.id), key: ValueKey(problem.id),
problem: problem, problem: problem,
viewType: viewType, viewType: viewType,
isSelected: false, // false isSelected: false,
), );
); }
} else { } else {
// listgrid等使 Obx
return Obx(() { return Obx(() {
// 使 Obx
final isSelected = controller.selectedProblems.contains(problem); final isSelected = controller.selectedProblems.contains(problem);
return ProblemCard( return ProblemCard(
key: ValueKey(problem.id), key: ValueKey(problem.id),
problem: problem, problem: problem,
viewType: viewType, viewType: viewType,
isSelected: isSelected, // isSelected: isSelected,
onChanged: (problem, isChecked) { onChanged: (problem, isChecked) {
controller.updateProblemSelection(problem, isChecked); controller.updateProblemSelection(problem, isChecked);
}, },

30
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:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:problem_check_system/app/routes/app_routes.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/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart'; import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
@ -29,7 +29,8 @@ class ProblemCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// //
final bool isDeleted = problem.isDeleted; final bool isDeleted =
problem.syncStatus == ProblemSyncStatus.pendingDelete;
final Color cardColor = isDeleted final Color cardColor = isDeleted
? Colors.grey[300]! ? Colors.grey[300]!
: Theme.of(context).cardColor; : Theme.of(context).cardColor;
@ -101,19 +102,20 @@ class ProblemCard extends StatelessWidget {
Wrap( Wrap(
spacing: 8, spacing: 8,
children: [ children: [
...problem.isDeleted ...problem.syncStatus == ProblemSyncStatus.pendingDelete
? [ ? [
TDTag( TDTag(
'已删除', '已删除',
theme: TDTagTheme.danger, isLight: true,
theme: TDTagTheme.defaultTheme,
textColor: isDeleted ? Colors.grey[700] : null, textColor: isDeleted ? Colors.grey[700] : null,
backgroundColor: isDeleted // backgroundColor: isDeleted
? Colors.grey[400] // ? Colors.grey[400]
: null, // : null,
), ),
] ]
: [ : [
problem.syncStatus == SyncStatus.synced problem.syncStatus == ProblemSyncStatus.synced
? TDTag( ? TDTag(
'已上传', '已上传',
isLight: true, isLight: true,
@ -232,13 +234,11 @@ class ProblemCard extends StatelessWidget {
padding: EdgeInsets.only(right: 16.w), padding: EdgeInsets.only(right: 16.w),
child: Checkbox( child: Checkbox(
value: isSelected, value: isSelected,
onChanged: isDeleted onChanged: (bool? value) {
? null if (value != null) {
: (bool? value) { onChanged?.call(problem, value);
if (value != null) { }
onChanged?.call(problem, value); },
}
},
), ),
); );
} }

Loading…
Cancel
Save