Browse Source

feat : 同步服务器数据到本地

dev
徐振升 13 hours ago
parent
commit
d0ac707a12
  1. 4
      analysis_options.yaml
  2. 94
      lib/data/models/server_problem.dart
  3. 315
      lib/data/models/server_problem.freezed.dart
  4. 41
      lib/data/models/server_problem.g.dart
  5. 103
      lib/data/repositories/file_repository.dart
  6. 15
      lib/data/repositories/problem_repository.dart
  7. 37
      lib/modules/problem/controllers/problem_controller.dart
  8. 35
      lib/modules/problem/controllers/sync_progress_state.dart
  9. 40
      lib/modules/problem/views/widgets/sync_progress_dialog.dart
  10. 288
      pubspec.lock
  11. 5
      pubspec.yaml

4
analysis_options.yaml

@ -1 +1,5 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
errors:
invalid_annotation_target: ignore

94
lib/data/models/server_problem.dart

@ -1,88 +1,24 @@
import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@immutable part 'server_problem.freezed.dart';
class ServerProblem { part 'server_problem.g.dart';
final String id;
final String title;
final String location;
final String? censorTaskId;
final String? rowId;
final String? bindData;
final List<String>? imageUrls;
final DateTime creationTime;
final String creatorId;
final DateTime lastModificationTime;
final String lastModifierId;
const ServerProblem({ @freezed
required this.id, abstract class ServerProblem with _$ServerProblem {
required this.title, const factory ServerProblem({
required this.location, required String id,
this.censorTaskId, required String title,
this.rowId, required String location,
this.bindData,
this.imageUrls,
required this.creationTime,
required this.creatorId,
required this.lastModificationTime,
required this.lastModifierId,
});
factory ServerProblem.fromJson(Map<String, dynamic> 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<String>?,
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<String, dynamic> 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,
String? censorTaskId, String? censorTaskId,
String? rowId, String? rowId,
String? bindData, String? bindData,
List<String>? imageUrls, List<String>? imageUrls,
DateTime? creationTime, required DateTime creationTime,
String? creatorId, required String creatorId,
DateTime? lastModificationTime, DateTime? lastModificationTime,
String? lastModifierId, String? lastModifierId,
}) { }) = _ServerProblem;
return ServerProblem(
id: id ?? this.id, factory ServerProblem.fromJson(Map<String, Object?> json) =>
title: title ?? this.title, _$ServerProblemFromJson(json);
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,
);
}
} }

315
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>(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<String>? 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<ServerProblem> get copyWith => _$ServerProblemCopyWithImpl<ServerProblem>(this as ServerProblem, _$identity);
/// Serializes this ServerProblem to a JSON map.
Map<String, dynamic> 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<String>? 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<String>?,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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(TResult Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List<String>? 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 extends Object?>(TResult Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List<String>? 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 extends Object?>(TResult? Function( String id, String title, String location, String? censorTaskId, String? rowId, String? bindData, List<String>? 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<String>? imageUrls, required this.creationTime, required this.creatorId, this.lastModificationTime, this.lastModifierId}): _imageUrls = imageUrls;
factory _ServerProblem.fromJson(Map<String, dynamic> 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<String>? _imageUrls;
@override List<String>? 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<String, dynamic> 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<String>? 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<String>?,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

41
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<String, dynamic> 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<dynamic>?)
?.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<String, dynamic> _$ServerProblemToJson(_ServerProblem instance) =>
<String, dynamic>{
'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,
};

103
lib/data/repositories/file_repository.dart

@ -1,14 +1,9 @@
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; // kDebugMode debugPrint import 'package:flutter/foundation.dart'; // kDebugMode debugPrint
import 'package:get/get.dart' hide FormData, MultipartFile; import 'package:get/get.dart' hide FormData, MultipartFile;
import 'package:path/path.dart' as p; 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/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_metadata_model.dart';
import 'package:problem_check_system/data/models/image_status.dart';
import 'package:problem_check_system/data/providers/http_provider.dart'; import 'package:problem_check_system/data/providers/http_provider.dart';
class FileRepository { class FileRepository {
@ -66,102 +61,4 @@ class FileRepository {
throw Exception('图片上传发生未知错误: $e'); throw Exception('图片上传发生未知错误: $e');
} }
} }
//
Future<ImageMetadata> 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<List<ImageMetadata>> downloadImages(
List<String> imageUrls, {
CancelToken? cancelToken,
void Function(int current, int total)? onProgress,
}) async {
final List<ImageMetadata> 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';
}
}
} }

15
lib/data/repositories/problem_repository.dart

@ -76,14 +76,21 @@ class ProblemRepository extends GetxService {
); );
if (response.isSuccess) { if (response.isSuccess) {
// Problem对象的列表 // Dio JSONresponse.data Map List
final List<dynamic> data = response.data; final Map<String, dynamic> data = response.data;
return data.map((json) => ServerProblem.fromJson(json)).toList(); final List<dynamic> items = data['items'];
// 使 Freezed fromJson
return items.map((item) => ServerProblem.fromJson(item)).toList();
} else { } else {
throw Exception('拉取问题失败: ${response.statusCode}'); throw Exception('拉取问题失败: ${response.statusCode}');
} }
} on DioException catch (e) { } on DioException catch (e) {
throw Exception('拉取问题失败: $e'); Get.log("Dio 异常$e");
rethrow;
} catch (e) {
Get.log("解析失败:$e");
rethrow;
} }
} }

37
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/image_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/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/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/models/dropdown_option.dart';
import 'package:problem_check_system/modules/problem/views/widgets/sync_progress_dialog.dart';
class ProblemController extends GetxController class ProblemController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
@ -73,7 +75,7 @@ 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: 365))
.obs; .obs;
final Rx<DateTime> historyEndTime = DateTime( final Rx<DateTime> historyEndTime = DateTime(
DateTime.now().year, DateTime.now().year,
@ -440,18 +442,33 @@ class ProblemController extends GetxController
// #endregion // #endregion
// #region // #region
// TODO
final SyncProgressState syncProgress = SyncProgressState();
Future<void> pullDataFromServer() async { Future<void> pullDataFromServer() async {
isLoading.value = true; isLoading.value = true;
//
Get.dialog(
SyncProgressDialog(progressState: syncProgress),
barrierDismissible: false,
);
try { try {
const int totalSteps = 4;
syncProgress.startSync(totalSteps);
// 1. // 1.
syncProgress.updateProgress('正在从服务器获取数据...', 1);
final List<ServerProblem> serverProblems = await problemRepository final List<ServerProblem> serverProblems = await problemRepository
.fetchProblemsFromServer(); .fetchProblemsFromServer(pageNumber: 1, pageSize: 99);
// 2. // 2.
syncProgress.updateProgress('正在获取本地数据...', 2);
final List<Problem> localProblems = await problemRepository.getProblems(); final List<Problem> localProblems = await problemRepository.getProblems();
// 3. // 3.
syncProgress.updateProgress('正在同步数据...', 3);
final List<Problem> downloadedProblems = await _syncProblems( final List<Problem> downloadedProblems = await _syncProblems(
serverProblems, serverProblems,
localProblems, localProblems,
@ -461,10 +478,20 @@ class ProblemController extends GetxController
_downloadImagesForProblems(downloadedProblems); _downloadImagesForProblems(downloadedProblems);
// 5. // 5.
syncProgress.updateProgress('正在重新加载数据...', 4);
await loadProblems(); await loadProblems();
syncProgress.completeSync();
//
Get.back();
Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP); Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP);
} catch (e) { } catch (e) {
syncProgress.errorSync(e.toString());
//
Get.back();
Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP); Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP);
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -578,7 +605,7 @@ class ProblemController extends GetxController
final serverUpdated = serverProblem.lastModificationTime; final serverUpdated = serverProblem.lastModificationTime;
final localUpdated = localProblem.lastModifiedTime; final localUpdated = localProblem.lastModifiedTime;
if (serverUpdated.isAfter(localUpdated)) { if (serverUpdated != null && serverUpdated.isAfter(localUpdated)) {
// //
final updatedProblem = _convertServerProblemToLocal(serverProblem); final updatedProblem = _convertServerProblemToLocal(serverProblem);
await problemRepository.updateProblem(updatedProblem); await problemRepository.updateProblem(updatedProblem);
@ -611,7 +638,7 @@ class ProblemController extends GetxController
location: serverProblem.location, location: serverProblem.location,
imageUrls: imageMetadatas, imageUrls: imageMetadatas,
creationTime: serverProblem.creationTime, creationTime: serverProblem.creationTime,
lastModifiedTime: serverProblem.lastModificationTime, lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(),
syncStatus: ProblemSyncStatus.synced, // syncStatus: ProblemSyncStatus.synced, //
censorTaskId: serverProblem.censorTaskId, censorTaskId: serverProblem.censorTaskId,
bindData: serverProblem.bindData, bindData: serverProblem.bindData,

35
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';
}
}

40
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<Color>(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),
),
],
),
),
);
}
}

288
pubspec.lock

@ -1,6 +1,22 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: 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: args:
dependency: transitive dependency: transitive
description: description:
@ -25,6 +41,54 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.2" 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: characters:
dependency: transitive dependency: transitive
description: description:
@ -33,6 +97,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.0" 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: clock:
dependency: transitive dependency: transitive
description: description:
@ -41,6 +113,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.2" 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: collection:
dependency: transitive dependency: transitive
description: description:
@ -65,6 +145,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.1" 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: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -81,6 +169,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.6" 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: dbus:
dependency: transitive dependency: transitive
description: description:
@ -129,6 +225,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" 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: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -229,6 +333,30 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: get:
dependency: "direct main" dependency: "direct main"
description: description:
@ -245,6 +373,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" 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: http:
dependency: transitive dependency: transitive
description: description:
@ -253,6 +397,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.0" 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: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -333,6 +485,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.20.2" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -365,6 +541,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.1.1" 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: matcher:
dependency: transitive dependency: transitive
description: description:
@ -405,6 +589,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.5.0" 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: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -549,6 +741,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.8" 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: pretty_dio_logger:
dependency: "direct main" dependency: "direct main"
description: description:
@ -557,11 +757,59 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.0" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: source_span:
dependency: transitive dependency: transitive
description: description:
@ -634,6 +882,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" 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: string_scanner:
dependency: transitive dependency: transitive
description: description:
@ -714,6 +970,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "15.0.0" 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: web:
dependency: transitive dependency: transitive
description: description:
@ -722,6 +986,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.1" 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: xdg_directories:
dependency: transitive dependency: transitive
description: description:
@ -738,6 +1018,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.6.1" 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: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

5
pubspec.yaml

@ -16,10 +16,12 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_screenutil: ^5.9.3 flutter_screenutil: ^5.9.3
freezed_annotation: ^3.1.0
get: ^4.7.2 get: ^4.7.2
get_storage: ^2.1.1 get_storage: ^2.1.1
image_picker: ^1.1.2 image_picker: ^1.1.2
intl: ^0.20.2 intl: ^0.20.2
json_annotation: ^4.9.0
path: ^1.9.1 path: ^1.9.1
path_provider: ^2.1.5 path_provider: ^2.1.5
permission_handler: ^12.0.1 permission_handler: ^12.0.1
@ -32,6 +34,9 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
build_runner: ^2.8.0
freezed: ^3.2.2
json_serializable: ^6.11.1
flutter: flutter:
uses-material-design: true uses-material-design: true

Loading…
Cancel
Save