diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b3034..e07e9e9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + invalid_annotation_target: ignore diff --git a/lib/data/models/server_problem.dart b/lib/data/models/server_problem.dart index 2e1979e..d463e76 100644 --- a/lib/data/models/server_problem.dart +++ b/lib/data/models/server_problem.dart @@ -1,88 +1,24 @@ -import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; -@immutable -class ServerProblem { - final String id; - final String title; - final String location; - final String? censorTaskId; - final String? rowId; - final String? bindData; - final List? imageUrls; - final DateTime creationTime; - final String creatorId; - final DateTime lastModificationTime; - final String lastModifierId; +part 'server_problem.freezed.dart'; +part 'server_problem.g.dart'; - const ServerProblem({ - required this.id, - required this.title, - required this.location, - this.censorTaskId, - this.rowId, - this.bindData, - this.imageUrls, - required this.creationTime, - required this.creatorId, - required this.lastModificationTime, - required this.lastModifierId, - }); - - factory ServerProblem.fromJson(Map json) => ServerProblem( - id: json['id'] as String, - title: json['title'] as String, - location: json['location'] as String, - censorTaskId: json['censorTaskId'] as String?, - rowId: json['rowId'] as String?, - bindData: json['bindData'] as String?, - imageUrls: json['imageUrls'] as List?, - creationTime: DateTime.parse(json['creationTime'] as String), - creatorId: json['creatorId'] as String, - lastModificationTime: DateTime.parse( - json['lastModificationTime'] as String, - ), - lastModifierId: json['lastModifierId'] as String, - ); - - Map toJson() => { - 'id': id, - 'title': title, - 'location': location, - 'censorTaskId': censorTaskId, - 'rowId': rowId, - 'bindData': bindData, - 'imageUrls': imageUrls, - 'creationTime': creationTime.toUtc().toIso8601String(), - 'creatorId': creatorId, - 'lastModificationTime': lastModificationTime.toUtc().toIso8601String(), - 'lastModifierId': lastModifierId, - }; - - ServerProblem copyWith({ - String? id, - String? title, - String? location, +@freezed +abstract class ServerProblem with _$ServerProblem { + const factory ServerProblem({ + required String id, + required String title, + required String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, - DateTime? creationTime, - String? creatorId, + required DateTime creationTime, + required String creatorId, DateTime? lastModificationTime, String? lastModifierId, - }) { - return ServerProblem( - id: id ?? this.id, - title: title ?? this.title, - location: location ?? this.location, - censorTaskId: censorTaskId ?? this.censorTaskId, - rowId: rowId ?? this.rowId, - bindData: bindData ?? this.bindData, - imageUrls: imageUrls ?? this.imageUrls, - creationTime: creationTime ?? this.creationTime, - creatorId: creatorId ?? this.creatorId, - lastModificationTime: lastModificationTime ?? this.lastModificationTime, - lastModifierId: lastModifierId ?? this.lastModifierId, - ); - } + }) = _ServerProblem; + + factory ServerProblem.fromJson(Map json) => + _$ServerProblemFromJson(json); } diff --git a/lib/data/models/server_problem.freezed.dart b/lib/data/models/server_problem.freezed.dart new file mode 100644 index 0000000..e331dee --- /dev/null +++ b/lib/data/models/server_problem.freezed.dart @@ -0,0 +1,315 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'server_problem.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$ServerProblem { + + String get id; String get title; String get location; String? get censorTaskId; String? get rowId; String? get bindData; List? get imageUrls; DateTime get creationTime; String get creatorId; DateTime? get lastModificationTime; String? get lastModifierId; +/// Create a copy of ServerProblem +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ServerProblemCopyWith get copyWith => _$ServerProblemCopyWithImpl(this as ServerProblem, _$identity); + + /// Serializes this ServerProblem to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ServerProblem&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.location, location) || other.location == location)&&(identical(other.censorTaskId, censorTaskId) || other.censorTaskId == censorTaskId)&&(identical(other.rowId, rowId) || other.rowId == rowId)&&(identical(other.bindData, bindData) || other.bindData == bindData)&&const DeepCollectionEquality().equals(other.imageUrls, imageUrls)&&(identical(other.creationTime, creationTime) || other.creationTime == creationTime)&&(identical(other.creatorId, creatorId) || other.creatorId == creatorId)&&(identical(other.lastModificationTime, lastModificationTime) || other.lastModificationTime == lastModificationTime)&&(identical(other.lastModifierId, lastModifierId) || other.lastModifierId == lastModifierId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,title,location,censorTaskId,rowId,bindData,const DeepCollectionEquality().hash(imageUrls),creationTime,creatorId,lastModificationTime,lastModifierId); + +@override +String toString() { + return 'ServerProblem(id: $id, title: $title, location: $location, censorTaskId: $censorTaskId, rowId: $rowId, bindData: $bindData, imageUrls: $imageUrls, creationTime: $creationTime, creatorId: $creatorId, lastModificationTime: $lastModificationTime, lastModifierId: $lastModifierId)'; +} + + +} + +/// @nodoc +abstract mixin class $ServerProblemCopyWith<$Res> { + factory $ServerProblemCopyWith(ServerProblem value, $Res Function(ServerProblem) _then) = _$ServerProblemCopyWithImpl; +@useResult +$Res call({ + String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, DateTime creationTime, String creatorId, DateTime? lastModificationTime, String? lastModifierId +}); + + + + +} +/// @nodoc +class _$ServerProblemCopyWithImpl<$Res> + implements $ServerProblemCopyWith<$Res> { + _$ServerProblemCopyWithImpl(this._self, this._then); + + final ServerProblem _self; + final $Res Function(ServerProblem) _then; + +/// Create a copy of ServerProblem +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? location = null,Object? censorTaskId = freezed,Object? rowId = freezed,Object? bindData = freezed,Object? imageUrls = freezed,Object? creationTime = null,Object? creatorId = null,Object? lastModificationTime = freezed,Object? lastModifierId = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as String,censorTaskId: freezed == censorTaskId ? _self.censorTaskId : censorTaskId // ignore: cast_nullable_to_non_nullable +as String?,rowId: freezed == rowId ? _self.rowId : rowId // ignore: cast_nullable_to_non_nullable +as String?,bindData: freezed == bindData ? _self.bindData : bindData // ignore: cast_nullable_to_non_nullable +as String?,imageUrls: freezed == imageUrls ? _self.imageUrls : imageUrls // ignore: cast_nullable_to_non_nullable +as List?,creationTime: null == creationTime ? _self.creationTime : creationTime // ignore: cast_nullable_to_non_nullable +as DateTime,creatorId: null == creatorId ? _self.creatorId : creatorId // ignore: cast_nullable_to_non_nullable +as String,lastModificationTime: freezed == lastModificationTime ? _self.lastModificationTime : lastModificationTime // ignore: cast_nullable_to_non_nullable +as DateTime?,lastModifierId: freezed == lastModifierId ? _self.lastModifierId : lastModifierId // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ServerProblem]. +extension ServerProblemPatterns on ServerProblem { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ServerProblem value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ServerProblem() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ServerProblem value) $default,){ +final _that = this; +switch (_that) { +case _ServerProblem(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ServerProblem value)? $default,){ +final _that = this; +switch (_that) { +case _ServerProblem() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, DateTime creationTime, String creatorId, DateTime? lastModificationTime, String? lastModifierId)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ServerProblem() when $default != null: +return $default(_that.id,_that.title,_that.location,_that.censorTaskId,_that.rowId,_that.bindData,_that.imageUrls,_that.creationTime,_that.creatorId,_that.lastModificationTime,_that.lastModifierId);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, DateTime creationTime, String creatorId, DateTime? lastModificationTime, String? lastModifierId) $default,) {final _that = this; +switch (_that) { +case _ServerProblem(): +return $default(_that.id,_that.title,_that.location,_that.censorTaskId,_that.rowId,_that.bindData,_that.imageUrls,_that.creationTime,_that.creatorId,_that.lastModificationTime,_that.lastModifierId);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, DateTime creationTime, String creatorId, DateTime? lastModificationTime, String? lastModifierId)? $default,) {final _that = this; +switch (_that) { +case _ServerProblem() when $default != null: +return $default(_that.id,_that.title,_that.location,_that.censorTaskId,_that.rowId,_that.bindData,_that.imageUrls,_that.creationTime,_that.creatorId,_that.lastModificationTime,_that.lastModifierId);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ServerProblem implements ServerProblem { + const _ServerProblem({required this.id, required this.title, required this.location, this.censorTaskId, this.rowId, this.bindData, final List? imageUrls, required this.creationTime, required this.creatorId, this.lastModificationTime, this.lastModifierId}): _imageUrls = imageUrls; + factory _ServerProblem.fromJson(Map json) => _$ServerProblemFromJson(json); + +@override final String id; +@override final String title; +@override final String location; +@override final String? censorTaskId; +@override final String? rowId; +@override final String? bindData; + final List? _imageUrls; +@override List? get imageUrls { + final value = _imageUrls; + if (value == null) return null; + if (_imageUrls is EqualUnmodifiableListView) return _imageUrls; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + +@override final DateTime creationTime; +@override final String creatorId; +@override final DateTime? lastModificationTime; +@override final String? lastModifierId; + +/// Create a copy of ServerProblem +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ServerProblemCopyWith<_ServerProblem> get copyWith => __$ServerProblemCopyWithImpl<_ServerProblem>(this, _$identity); + +@override +Map toJson() { + return _$ServerProblemToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ServerProblem&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.location, location) || other.location == location)&&(identical(other.censorTaskId, censorTaskId) || other.censorTaskId == censorTaskId)&&(identical(other.rowId, rowId) || other.rowId == rowId)&&(identical(other.bindData, bindData) || other.bindData == bindData)&&const DeepCollectionEquality().equals(other._imageUrls, _imageUrls)&&(identical(other.creationTime, creationTime) || other.creationTime == creationTime)&&(identical(other.creatorId, creatorId) || other.creatorId == creatorId)&&(identical(other.lastModificationTime, lastModificationTime) || other.lastModificationTime == lastModificationTime)&&(identical(other.lastModifierId, lastModifierId) || other.lastModifierId == lastModifierId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,title,location,censorTaskId,rowId,bindData,const DeepCollectionEquality().hash(_imageUrls),creationTime,creatorId,lastModificationTime,lastModifierId); + +@override +String toString() { + return 'ServerProblem(id: $id, title: $title, location: $location, censorTaskId: $censorTaskId, rowId: $rowId, bindData: $bindData, imageUrls: $imageUrls, creationTime: $creationTime, creatorId: $creatorId, lastModificationTime: $lastModificationTime, lastModifierId: $lastModifierId)'; +} + + +} + +/// @nodoc +abstract mixin class _$ServerProblemCopyWith<$Res> implements $ServerProblemCopyWith<$Res> { + factory _$ServerProblemCopyWith(_ServerProblem value, $Res Function(_ServerProblem) _then) = __$ServerProblemCopyWithImpl; +@override @useResult +$Res call({ + String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List? imageUrls, DateTime creationTime, String creatorId, DateTime? lastModificationTime, String? lastModifierId +}); + + + + +} +/// @nodoc +class __$ServerProblemCopyWithImpl<$Res> + implements _$ServerProblemCopyWith<$Res> { + __$ServerProblemCopyWithImpl(this._self, this._then); + + final _ServerProblem _self; + final $Res Function(_ServerProblem) _then; + +/// Create a copy of ServerProblem +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? location = null,Object? censorTaskId = freezed,Object? rowId = freezed,Object? bindData = freezed,Object? imageUrls = freezed,Object? creationTime = null,Object? creatorId = null,Object? lastModificationTime = freezed,Object? lastModifierId = freezed,}) { + return _then(_ServerProblem( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable +as String,location: null == location ? _self.location : location // ignore: cast_nullable_to_non_nullable +as String,censorTaskId: freezed == censorTaskId ? _self.censorTaskId : censorTaskId // ignore: cast_nullable_to_non_nullable +as String?,rowId: freezed == rowId ? _self.rowId : rowId // ignore: cast_nullable_to_non_nullable +as String?,bindData: freezed == bindData ? _self.bindData : bindData // ignore: cast_nullable_to_non_nullable +as String?,imageUrls: freezed == imageUrls ? _self._imageUrls : imageUrls // ignore: cast_nullable_to_non_nullable +as List?,creationTime: null == creationTime ? _self.creationTime : creationTime // ignore: cast_nullable_to_non_nullable +as DateTime,creatorId: null == creatorId ? _self.creatorId : creatorId // ignore: cast_nullable_to_non_nullable +as String,lastModificationTime: freezed == lastModificationTime ? _self.lastModificationTime : lastModificationTime // ignore: cast_nullable_to_non_nullable +as DateTime?,lastModifierId: freezed == lastModifierId ? _self.lastModifierId : lastModifierId // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/data/models/server_problem.g.dart b/lib/data/models/server_problem.g.dart new file mode 100644 index 0000000..47770d4 --- /dev/null +++ b/lib/data/models/server_problem.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_problem.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_ServerProblem _$ServerProblemFromJson(Map json) => + _ServerProblem( + id: json['id'] as String, + title: json['title'] as String, + location: json['location'] as String, + censorTaskId: json['censorTaskId'] as String?, + rowId: json['rowId'] as String?, + bindData: json['bindData'] as String?, + imageUrls: (json['imageUrls'] as List?) + ?.map((e) => e as String) + .toList(), + creationTime: DateTime.parse(json['creationTime'] as String), + creatorId: json['creatorId'] as String, + lastModificationTime: json['lastModificationTime'] == null + ? null + : DateTime.parse(json['lastModificationTime'] as String), + lastModifierId: json['lastModifierId'] as String?, + ); + +Map _$ServerProblemToJson(_ServerProblem instance) => + { + 'id': instance.id, + 'title': instance.title, + 'location': instance.location, + 'censorTaskId': instance.censorTaskId, + 'rowId': instance.rowId, + 'bindData': instance.bindData, + 'imageUrls': instance.imageUrls, + 'creationTime': instance.creationTime.toIso8601String(), + 'creatorId': instance.creatorId, + 'lastModificationTime': instance.lastModificationTime?.toIso8601String(), + 'lastModifierId': instance.lastModifierId, + }; diff --git a/lib/data/repositories/file_repository.dart b/lib/data/repositories/file_repository.dart index 58abb29..d17813e 100644 --- a/lib/data/repositories/file_repository.dart +++ b/lib/data/repositories/file_repository.dart @@ -1,14 +1,9 @@ -import 'dart:io'; - import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; // 引入 kDebugMode 和 debugPrint import 'package:get/get.dart' hide FormData, MultipartFile; import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; 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/image_metadata_model.dart'; -import 'package:problem_check_system/data/models/image_status.dart'; import 'package:problem_check_system/data/providers/http_provider.dart'; class FileRepository { @@ -66,102 +61,4 @@ class FileRepository { throw Exception('图片上传发生未知错误: $e'); } } - - // 新增的下载方法 - Future downloadImage( - String imageUrl, { - CancelToken? cancelToken, - void Function(int received, int total)? onReceiveProgress, - }) async { - final directory = await getApplicationDocumentsDirectory(); - final imagesDir = Directory('${directory.path}/problem_images'); - - // 确保目录存在 - if (!await imagesDir.exists()) { - await imagesDir.create(recursive: true); - } - - // 生成唯一的文件名 - final String fileName = - 'downloaded_${DateTime.now().millisecondsSinceEpoch}_${imageUrl.hashCode}${_getFileExtension(imageUrl)}'; - final String imagePath = '${imagesDir.path}/$fileName'; - - try { - // 下载图片 - await _httpProvider.download( - imageUrl, - imagePath, - cancelToken: cancelToken, - onReceiveProgress: onReceiveProgress, - ); - - // 返回图片元数据 - return ImageMetadata( - localPath: imagePath, - remoteUrl: imageUrl, - status: ImageStatus.synced, - ); - } catch (e) { - // 清理可能创建的不完整文件 - final file = File(imagePath); - if (await file.exists()) { - await file.delete(); - } - rethrow; - } - } - - // 批量下载方法 - Future> downloadImages( - List imageUrls, { - CancelToken? cancelToken, - void Function(int current, int total)? onProgress, - }) async { - final List results = []; - int downloadedCount = 0; - - for (final imageUrl in imageUrls) { - if (cancelToken?.isCancelled == true) { - break; - } - - try { - final metadata = await downloadImage( - imageUrl, - cancelToken: cancelToken, - onReceiveProgress: (received, total) { - // 单个文件的进度可以在这里处理 - }, - ); - results.add(metadata); - - // 更新总体进度 - downloadedCount++; - onProgress?.call(downloadedCount, imageUrls.length); - } catch (e) { - Get.log('Failed to download image $imageUrl: $e'); - // 可以选择继续下载其他图片或抛出异常 - } - } - - return results; - } - - // 辅助方法:获取文件扩展名 - String _getFileExtension(String url) { - try { - final uri = Uri.parse(url); - final pathSegments = uri.pathSegments; - if (pathSegments.isNotEmpty) { - final fileName = pathSegments.last; - final dotIndex = fileName.lastIndexOf('.'); - if (dotIndex != -1 && dotIndex < fileName.length - 1) { - return fileName.substring(dotIndex); - } - } - return '.jpg'; - } catch (e) { - return '.jpg'; - } - } } diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index 38c96a8..e84b67b 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -76,14 +76,21 @@ class ProblemRepository extends GetxService { ); if (response.isSuccess) { - // 假设服务器返回的是Problem对象的列表 - final List data = response.data; - return data.map((json) => ServerProblem.fromJson(json)).toList(); + // 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) { - throw Exception('拉取问题失败: $e'); + Get.log("Dio 异常$e"); + rethrow; + } catch (e) { + Get.log("解析失败:$e"); + rethrow; } } diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index dabcdb7..ed3f412 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -16,8 +16,10 @@ import 'package:problem_check_system/data/repositories/file_repository.dart'; import 'package:problem_check_system/data/repositories/image_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/controllers/sync_progress_state.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/dropdown_option.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/sync_progress_dialog.dart'; class ProblemController extends GetxController with GetSingleTickerProviderStateMixin { @@ -73,7 +75,7 @@ class ProblemController extends GetxController // 历史问题列表筛选条件 final Rx historyStartTime = DateTime.now() - .subtract(const Duration(days: 7)) + .subtract(const Duration(days: 365)) .obs; final Rx historyEndTime = DateTime( DateTime.now().year, @@ -440,18 +442,33 @@ class ProblemController extends GetxController // #endregion // #region 问题同步 - // TODO 同步服务器问题到本地 + + final SyncProgressState syncProgress = SyncProgressState(); + Future pullDataFromServer() async { isLoading.value = true; + + // 显示进度对话框 + Get.dialog( + SyncProgressDialog(progressState: syncProgress), + barrierDismissible: false, + ); + try { + const int totalSteps = 4; + syncProgress.startSync(totalSteps); + // 1. 从服务器获取最新数据 + syncProgress.updateProgress('正在从服务器获取数据...', 1); final List serverProblems = await problemRepository - .fetchProblemsFromServer(); + .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, @@ -461,10 +478,20 @@ class ProblemController extends GetxController _downloadImagesForProblems(downloadedProblems); // 5. 重新加载本地问题列表 + syncProgress.updateProgress('正在重新加载数据...', 4); await loadProblems(); + syncProgress.completeSync(); + + // 关闭对话框 + Get.back(); + Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); } catch (e) { + syncProgress.errorSync(e.toString()); + + // 关闭对话框并显示错误 + Get.back(); Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP); } finally { isLoading.value = false; @@ -578,7 +605,7 @@ class ProblemController extends GetxController final serverUpdated = serverProblem.lastModificationTime; final localUpdated = localProblem.lastModifiedTime; - if (serverUpdated.isAfter(localUpdated)) { + if (serverUpdated != null && serverUpdated.isAfter(localUpdated)) { // 服务器数据更新,更新本地数据 final updatedProblem = _convertServerProblemToLocal(serverProblem); await problemRepository.updateProblem(updatedProblem); @@ -611,7 +638,7 @@ class ProblemController extends GetxController location: serverProblem.location, imageUrls: imageMetadatas, creationTime: serverProblem.creationTime, - lastModifiedTime: serverProblem.lastModificationTime, + lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(), syncStatus: ProblemSyncStatus.synced, // 来自服务器的数据标记为已同步 censorTaskId: serverProblem.censorTaskId, bindData: serverProblem.bindData, diff --git a/lib/modules/problem/controllers/sync_progress_state.dart b/lib/modules/problem/controllers/sync_progress_state.dart new file mode 100644 index 0000000..308c90b --- /dev/null +++ b/lib/modules/problem/controllers/sync_progress_state.dart @@ -0,0 +1,35 @@ +// sync_progress_state.dart +import 'package:get/get.dart'; + +class SyncProgressState { + final RxBool isSyncing = false.obs; + final RxString currentStep = ''.obs; + final RxDouble progress = 0.0.obs; + final RxInt totalSteps = 0.obs; + final RxInt completedSteps = 0.obs; + + void startSync(int totalSteps) { + isSyncing.value = true; + this.totalSteps.value = totalSteps; + completedSteps.value = 0; + progress.value = 0.0; + currentStep.value = '开始同步...'; + } + + void updateProgress(String step, int completed) { + completedSteps.value = completed; + progress.value = completed / totalSteps.value; + currentStep.value = step; + } + + void completeSync() { + isSyncing.value = false; + currentStep.value = '同步完成'; + progress.value = 1.0; + } + + void errorSync(String error) { + isSyncing.value = false; + currentStep.value = '同步失败: $error'; + } +} diff --git a/lib/modules/problem/views/widgets/sync_progress_dialog.dart b/lib/modules/problem/views/widgets/sync_progress_dialog.dart new file mode 100644 index 0000000..aee6d1c --- /dev/null +++ b/lib/modules/problem/views/widgets/sync_progress_dialog.dart @@ -0,0 +1,40 @@ +// sync_progress_dialog.dart +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/modules/problem/controllers/sync_progress_state.dart'; + +class SyncProgressDialog extends StatelessWidget { + final SyncProgressState progressState; + + const SyncProgressDialog({super.key, required this.progressState}); + + @override + Widget build(BuildContext context) { + return Obx( + () => AlertDialog( + title: const Text('数据同步中'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LinearProgressIndicator( + value: progressState.progress.value, + backgroundColor: Colors.grey[300], + valueColor: const AlwaysStoppedAnimation(Colors.blue), + ), + const SizedBox(height: 16), + Text( + progressState.currentStep.value, + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '${(progressState.progress.value * 100).toStringAsFixed(1)}%', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index a48b1f2..12d8278 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a + url: "https://pub.flutter-io.cn" + source: hosted + version: "88.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.1.1" args: dependency: transitive description: @@ -25,6 +41,54 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "5b887c55a0f734b433b3b2d89f9cd1f99eb636b17e268a5b4259258bc916504b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "804c47c936df75e1911c19a4fb8c46fa8ff2b3099b9f2b2aa4726af3774f734b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.8.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + url: "https://pub.flutter-io.cn" + source: hosted + version: "8.12.0" characters: dependency: transitive description: @@ -33,6 +97,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" clock: dependency: transitive description: @@ -41,6 +113,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.10.1" collection: dependency: transitive description: @@ -65,6 +145,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" cross_file: dependency: transitive description: @@ -81,6 +169,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" dbus: dependency: transitive description: @@ -129,6 +225,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" file_selector_linux: dependency: transitive description: @@ -229,6 +333,30 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: b6445822d9b3961a9d0f3def0b4297bd77314bdbfd1de0b62eaca27c93e9bc0f + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.0" get: dependency: "direct main" description: @@ -245,6 +373,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" http: dependency: transitive description: @@ -253,6 +397,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" http_parser: dependency: transitive description: @@ -333,6 +485,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: "33a040668b31b320aafa4822b7b1e177e163fc3c1e835c6750319d4ab23aa6fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.11.1" leak_tracker: dependency: transitive description: @@ -365,6 +541,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -405,6 +589,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" path: dependency: "direct main" description: @@ -549,6 +741,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" pretty_dio_logger: dependency: "direct main" description: @@ -557,11 +757,59 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: ccf30b0c9fbcd79d8b6f5bfac23199fb354938436f62475e14aea0f29ee0f800 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.8" source_span: dependency: transitive description: @@ -634,6 +882,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -714,6 +970,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "15.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" web: dependency: transitive description: @@ -722,6 +986,22 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: @@ -738,6 +1018,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" sdks: dart: ">=3.8.1 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2a3615f..34137a8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,12 @@ dependencies: flutter_localizations: sdk: flutter flutter_screenutil: ^5.9.3 + freezed_annotation: ^3.1.0 get: ^4.7.2 get_storage: ^2.1.1 image_picker: ^1.1.2 intl: ^0.20.2 + json_annotation: ^4.9.0 path: ^1.9.1 path_provider: ^2.1.5 permission_handler: ^12.0.1 @@ -32,6 +34,9 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + build_runner: ^2.8.0 + freezed: ^3.2.2 + json_serializable: ^6.11.1 flutter: uses-material-design: true