Compare commits

..

40 Commits
master ... dev

Author SHA1 Message Date
徐振升 5e39240e6e fix : 多次更新对话框未消失 1 hour ago
徐振升 038d0553f8 fix : 图片仓库依赖注入失效 2 hours ago
徐振升 d0ac707a12 feat : 同步服务器数据到本地 4 hours ago
徐振升 65601485fa feat : 同步服务器问题到本地 22 hours ago
徐振升 db8594f83a feat : 下拉同步服务器数据 1 day ago
徐振升 2d2f9d2d5c feat : 本地同步到服务器 1 day ago
徐振升 72d07da128 todo : 上传问题的删除、更新 2 days ago
徐振升 96f1ef1efd feat : 根据问题状态进行物理删除与逻辑删除 2 days ago
徐振升 b614000b77 feat : 图片删除效果 2 days ago
徐振升 240bf0d69d feat : 上传问题id 2 days ago
徐振升 dddfc44d19 feat : 上传问题 4 days ago
徐振升 3bf85e0495 style : 更新页面布局 4 days ago
徐振升 223e49adad refactor : 使用GetView小部件 4 days ago
徐振升 4d50b91a16 feat : 查询功能 4 days ago
徐振升 34a8b997fc feat : 查询筛选布局优化 6 days ago
徐振升 48de460804 refactor : 优化查询下拉列表,上传问题页面,数据库查询。 6 days ago
徐振升 cafa2a2412 feat : sqlite int 类型 toMap,fromMap 1 week ago
徐振升 52cc2e11fd feat : 不可变设计模式 1 week ago
徐振升 dca5b93924 feat : 当前与历史问题列表查询条件 1 week ago
徐振升 b99c1826e9 feat : 查询问题 2 weeks ago
徐振升 faa42e07e1 fix : 登录认证提示未知错误 2 weeks ago
徐振升 72c871ca9b refactor : imageUrls 修改我 imageMetadata 2 weeks ago
徐振升 b7bff3048c fix : 问题仓库未能正确继承服务类 2 weeks ago
徐振升 58d4b668d9 fix : 修正所有警告 2 weeks ago
徐振升 49bb72a185 refactor : authController修改为loginController 2 weeks ago
徐振升 714bebc485 fate : 优化代码可读性 2 weeks ago
徐振升 1dfc578217 refactor : 优化框架逻辑 2 weeks ago
徐振升 7af9347b7e fate : 我的,姓名,头像,email 2 weeks ago
徐振升 9cdfd8da42 refactor : DioProvider 2 weeks ago
徐振升 eaa836205b fate : 上传问题页面 2 weeks ago
徐振升 17a1791d92 fate : 问题上传页面 problem_upload_page 2 weeks ago
徐振升 a2b8abd934 fate : 悬浮贴靠按钮 2 weeks ago
徐振升 5020451a84 refactor : 问题页悬浮按钮 2 weeks ago
徐振升 697e61ab3a refactor : 用户控制器 2 weeks ago
徐振升 b083dd0f83 refactor : 登录页 2 weeks ago
徐振升 34447b0826 fate : 监听联网状态 2 weeks ago
徐振升 1397875a11 新增:离线登录 2 weeks ago
徐振升 2e5b115a8d 重构代码 3 weeks ago
徐振升 2267d6fc91 新增:本地数据库,新增问题,本地存储 3 weeks ago
徐振升 2ba07bde25 新增:保存问题文件 3 weeks ago
  1. 2
      .vscode/settings.json
  2. 25
      README.md
  3. 4
      analysis_options.yaml
  4. 15
      android/app/src/main/AndroidManifest.xml
  5. 5
      android/gradle.properties
  6. BIN
      assets/images/background.png
  7. 3
      devtools_options.yaml
  8. 49
      lib/app/bindings/initial_binding.dart
  9. 43
      lib/app/routes/app_pages.dart
  10. 14
      lib/app/routes/app_routes.dart
  11. 16
      lib/core/extensions/http_response_extension.dart
  12. 19
      lib/core/utils/constants/api_endpoints.dart
  13. 53
      lib/data/models/auth_model.dart
  14. 47
      lib/data/models/image_metadata_model.dart
  15. 14
      lib/data/models/image_status.dart
  16. 132
      lib/data/models/problem_model.dart
  17. 98
      lib/data/models/problem_sync_status.dart
  18. 24
      lib/data/models/server_problem.dart
  19. 315
      lib/data/models/server_problem.freezed.dart
  20. 41
      lib/data/models/server_problem.g.dart
  21. 81
      lib/data/models/user/organization.dart
  22. 15
      lib/data/models/user/page.dart
  23. 15
      lib/data/models/user/role.dart
  24. 81
      lib/data/models/user/user.dart
  25. 62
      lib/data/providers/connectivity_provider.dart
  26. 258
      lib/data/providers/http_provider.dart
  27. 271
      lib/data/providers/sqlite_provider.dart
  28. 121
      lib/data/repositories/auth_repository.dart
  29. 65
      lib/data/repositories/file_repository.dart
  30. 8
      lib/data/repositories/image_repository.dart
  31. 202
      lib/data/repositories/image_repository_impl.dart
  32. 135
      lib/data/repositories/problem_repository.dart
  33. 67
      lib/main.dart
  34. 12
      lib/modules/auth/bindings/login_binding.dart
  35. 99
      lib/modules/auth/controllers/login_controller.dart
  36. 189
      lib/modules/auth/views/login_page.dart
  37. 25
      lib/modules/home/bindings/home_binding.dart
  38. 21
      lib/modules/home/controllers/home_controller.dart
  39. 21
      lib/modules/home/home_controller.dart
  40. 17
      lib/modules/home/views/home_page.dart
  41. 4
      lib/modules/login/models/login_model.dart
  42. 16
      lib/modules/login/view_models/login_controller.dart
  43. 175
      lib/modules/login/views/login_page.dart
  44. 0
      lib/modules/login/views/my_page.dart
  45. 0
      lib/modules/login/views/problem_page.dart
  46. 13
      lib/modules/my/bindings/change_password_binding.dart
  47. 54
      lib/modules/my/controllers/change_password_controller.dart
  48. 54
      lib/modules/my/controllers/my_controller.dart
  49. 155
      lib/modules/my/views/change_password.dart
  50. 222
      lib/modules/my/views/my_page.dart
  51. 23
      lib/modules/problem/bindings/problem_form_binding.dart
  52. 76
      lib/modules/problem/components/date_picker_button.dart
  53. 874
      lib/modules/problem/controllers/problem_controller.dart
  54. 257
      lib/modules/problem/controllers/problem_form_controller.dart
  55. 35
      lib/modules/problem/controllers/sync_progress_state.dart
  56. 139
      lib/modules/problem/problem_card.dart
  57. 43
      lib/modules/problem/problem_list_page.dart
  58. 145
      lib/modules/problem/problem_page.dart
  59. 343
      lib/modules/problem/views/problem_form_page.dart
  60. 181
      lib/modules/problem/views/problem_list_page.dart
  61. 165
      lib/modules/problem/views/problem_page.dart
  62. 93
      lib/modules/problem/views/problem_upload_page.dart
  63. 66
      lib/modules/problem/views/widgets/current_filter_bar.dart
  64. 0
      lib/modules/problem/views/widgets/custom_button.dart
  65. 78
      lib/modules/problem/views/widgets/custom_filter_dropdown.dart
  66. 97
      lib/modules/problem/views/widgets/history_filter_bar.dart
  67. 68
      lib/modules/problem/views/widgets/models/date_range_enum.dart
  68. 23
      lib/modules/problem/views/widgets/models/dropdown_option.dart
  69. 247
      lib/modules/problem/views/widgets/problem_card.dart
  70. 40
      lib/modules/problem/views/widgets/sync_progress_dialog.dart
  71. 6
      macos/Flutter/GeneratedPluginRegistrant.swift
  72. 595
      pubspec.lock
  73. 20
      pubspec.yaml
  74. 6
      windows/flutter/generated_plugin_registrant.cc
  75. 2
      windows/flutter/generated_plugins.cmake

2
.vscode/settings.json vendored

@ -1,3 +1,3 @@
{ {
"cSpell.words": ["Getx", "tdesign"] "cSpell.words": ["fenix", "Getx", "tdesign"]
} }

25
README.md

@ -1,3 +1,26 @@
# problem_check_system # problem_check_system
A new Flutter project. 系统架构为MVVM + 仓库模式
这个应用需要实现以下功能:
离线登录系统
问题数据收集(描述、位置、图片等)
本地数据存储
有网络时手动上传功能
技术栈
Flutter SDK
GetX (状态管理、路由管理、依赖注入)
SQFlite (本地数据库)
Image Picker (图片选择)
Geolocator (位置信息)
HTTP/Dio (网络请求)

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

15
android/app/src/main/AndroidManifest.xml

@ -1,6 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 对于 Android 10 (API 29) 及以上版本,需要添加以下权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application <application
android:label="problem_check_system" android:label="现场问题检查"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

5
android/gradle.properties

@ -1,3 +1,8 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=7890
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7890

BIN
assets/images/background.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 85 KiB

3
devtools_options.yaml

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

49
lib/app/bindings/initial_binding.dart

@ -0,0 +1,49 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
import 'package:problem_check_system/data/providers/sqlite_provider.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
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_impl.dart';
import 'package:problem_check_system/data/repositories/problem_repository.dart';
class InitialBinding implements Bindings {
@override
void dependencies() {
_registerCoreServices();
_registerRepositories();
}
void _registerCoreServices() {
///
Get.put<GetStorage>(GetStorage(), permanent: true);
Get.put<HttpProvider>(HttpProvider());
Get.put<SQLiteProvider>(SQLiteProvider());
Get.put<ConnectivityProvider>(ConnectivityProvider());
}
void _registerRepositories() {
Get.lazyPut<FileRepository>(() => FileRepository());
Get.lazyPut<ImageRepository>(
() => ImageRepositoryImpl(httpProvider: Get.find<HttpProvider>()),
);
///
Get.lazyPut<AuthRepository>(
() => AuthRepository(
httpProvider: Get.find<HttpProvider>(),
storage: Get.find<GetStorage>(),
connectivityProvider: Get.find<ConnectivityProvider>(),
),
);
Get.lazyPut<ProblemRepository>(
() => ProblemRepository(
sqliteProvider: Get.find<SQLiteProvider>(),
httpProvider: Get.find<HttpProvider>(),
connectivityProvider: Get.find<ConnectivityProvider>(),
),
);
}
}

43
lib/app/routes/app_pages.dart

@ -0,0 +1,43 @@
import 'package:get/get.dart';
import 'package:problem_check_system/modules/home/bindings/home_binding.dart';
import 'package:problem_check_system/modules/home/views/home_page.dart';
import 'package:problem_check_system/modules/auth/bindings/login_binding.dart';
import 'package:problem_check_system/modules/auth/views/login_page.dart';
import 'package:problem_check_system/modules/my/bindings/change_password_binding.dart';
import 'package:problem_check_system/modules/my/views/change_password.dart';
import 'package:problem_check_system/modules/problem/bindings/problem_form_binding.dart';
import 'package:problem_check_system/modules/problem/views/problem_form_page.dart';
import 'package:problem_check_system/modules/problem/views/problem_upload_page.dart';
import 'app_routes.dart';
abstract class AppPages {
// GetPage
static final routes = <GetPage>[
GetPage(
name: AppRoutes.home,
page: () => const HomePage(),
binding: HomeBinding(),
),
//
GetPage(
name: AppRoutes.login,
page: () => const LoginPage(),
binding: LoginBinding(),
),
GetPage(
name: AppRoutes.changePassword,
page: () => const ChangePasswordPage(),
binding: ChangePasswordBinding(),
),
GetPage(
name: AppRoutes.problemUpload,
page: () => const ProblemUploadPage(),
),
GetPage(
name: AppRoutes.problemForm,
page: () => const ProblemFormPage(),
binding: ProblemFormBinding(),
),
];
}

14
lib/app/routes/app_routes.dart

@ -0,0 +1,14 @@
abstract class AppRoutes {
// 使 const
static const home = '/home';
static const login = '/login';
static const my = '/my';
static const changePassword = '/changePassword';
// #region
static const problem = '/problem';
static const problemUpload = '/problemUpload';
static const problemForm = '/problemForm';
// #endregion
}

16
lib/core/extensions/http_response_extension.dart

@ -0,0 +1,16 @@
// core/extensions/http_response_extension.dart
import 'package:dio/dio.dart';
extension HttpResponseExtension on Response {
bool get isSuccess {
return statusCode != null && statusCode! >= 200 && statusCode! < 300;
}
bool get isClientError {
return statusCode != null && statusCode! >= 400 && statusCode! < 500;
}
bool get isServerError {
return statusCode != null && statusCode! >= 500 && statusCode! < 600;
}
}

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

@ -0,0 +1,19 @@
// lib/data/api_endpoints.dart
abstract class ApiEndpoints {
static const String baseUrl = 'https://xhdev.anxincloud.cn';
// Accounts
static const String postLogin = '/api/Accounts/SignIn';
static const String postRefreshToken = '/api/Accounts/RefreshToken';
static const String getUserProfile = '/api/Accounts/Profile';
static const String patchPassword = '/api/Accounts/ChangePassword';
// Memorandum
static const String getProblems = '/api/Memorandum';
static const String postProblem = '/api/Memorandum';
static String putProblemById(String id) => '/api/Memorandum/$id';
static String deleteProblemById(String id) => '/api/Memorandum/$id';
//
static const String postUploadFile = '/api/Objects/association';
}

53
lib/data/models/auth_model.dart

@ -0,0 +1,53 @@
///
class LoginRequest {
final String username;
final String password;
final String wechatJsCode;
LoginRequest({
required this.username,
required this.password,
this.wechatJsCode = "",
});
Map<String, dynamic> toJson() {
return {
'username': username,
'password': password,
'wechatJsCode': wechatJsCode,
};
}
// Map LoginRequest
factory LoginRequest.fromJson(Map<String, dynamic> json) {
return LoginRequest(
username: json['username'] as String,
password: json['password'] as String,
wechatJsCode: json['wechatJsCode'] as String,
);
}
}
///
class LoginResponse {
final String token;
final String refreshToken;
final int expires;
final String name;
LoginResponse({
required this.token,
required this.refreshToken,
required this.expires,
required this.name,
});
factory LoginResponse.fromJson(Map<String, dynamic> json) {
return LoginResponse(
token: json['token'] ?? '',
refreshToken: json['refresh_token'] ?? '',
expires: json['expires'] ?? '',
name: json['name'] ?? '',
);
}
}

47
lib/data/models/image_metadata_model.dart

@ -0,0 +1,47 @@
// image_metadata_model.dart
import 'package:problem_check_system/data/models/image_status.dart';
class ImageMetadata {
final String localPath;
final String? remoteUrl;
final ImageStatus status;
ImageMetadata({
required this.localPath,
this.remoteUrl,
required this.status,
});
// For saving to SQL
Map<String, dynamic> toMap() {
return {
'localPath': localPath,
'remoteUrl': remoteUrl,
'status': status.index,
};
}
// For reading from SQL
factory ImageMetadata.fromMap(Map<String, dynamic> map) {
return ImageMetadata(
localPath: map['localPath'] as String,
remoteUrl: map['remoteUrl'] as String?,
status: ImageStatus.values[map['status'] as int],
);
}
/// Creates a new [ImageMetadata] instance with optional new values.
///
/// The original object remains unchanged.
ImageMetadata copyWith({
String? localPath,
String? remoteUrl,
ImageStatus? status,
}) {
return ImageMetadata(
localPath: localPath ?? this.localPath,
remoteUrl: remoteUrl ?? this.remoteUrl,
status: status ?? this.status,
);
}
}

14
lib/data/models/image_status.dart

@ -0,0 +1,14 @@
///
enum ImageStatus {
///
synced,
//
pendingUpload,
///
pendingDeleted,
///
pendingDownload,
}

132
lib/data/models/problem_model.dart

@ -0,0 +1,132 @@
import 'dart:convert';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
///
///
class Problem {
///
final String id;
///
final String description;
///
final String location;
///
final List<ImageMetadata> imageUrls;
///
final DateTime creationTime;
///
final ProblemSyncStatus syncStatus;
///
final DateTime lastModifiedTime;
/// ID
final String? censorTaskId;
///
final String? bindData;
/// false
final bool isChecked;
Problem({
required this.id,
required this.description,
required this.location,
required this.imageUrls,
required this.creationTime,
required this.lastModifiedTime,
this.syncStatus = ProblemSyncStatus.pendingCreate,
this.censorTaskId,
this.bindData,
this.isChecked = false,
});
/// copyWith
Problem copyWith({
String? id,
String? description,
String? location,
List<ImageMetadata>? imageUrls,
DateTime? creationTime,
DateTime? lastModifiedTime,
ProblemSyncStatus? syncStatus,
bool? isDeleted,
String? censorTaskId,
String? bindData,
bool? isChecked,
}) {
return Problem(
id: id ?? this.id,
description: description ?? this.description,
location: location ?? this.location,
imageUrls: imageUrls ?? this.imageUrls,
creationTime: creationTime ?? this.creationTime,
lastModifiedTime: lastModifiedTime ?? this.lastModifiedTime,
syncStatus: syncStatus ?? this.syncStatus,
censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData,
isChecked: isChecked ?? this.isChecked,
);
}
/// MapSQLite存储
Map<String, dynamic> toMap() {
return {
'id': id,
'description': description,
'location': location,
'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()),
'creationTime': creationTime.millisecondsSinceEpoch,
'lastModifiedTime': lastModifiedTime.millisecondsSinceEpoch,
'syncStatus': syncStatus.index,
'censorTaskId': censorTaskId,
'bindData': bindData,
'isChecked': isChecked ? 1 : 0,
};
}
/// Map创建对象SQLite读取
factory Problem.fromMap(Map<String, dynamic> map) {
// imageUrls的转换
List<ImageMetadata> imageUrlsList = [];
if (map['imageUrls'] != null) {
try {
final List<dynamic> imageList = json.decode(map['imageUrls']);
imageUrlsList = imageList.map((e) => ImageMetadata.fromMap(e)).toList();
} catch (e) {
//
imageUrlsList = [];
}
}
return Problem(
id: map['id'],
description: map['description'],
location: map['location'],
imageUrls: imageUrlsList,
creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']),
lastModifiedTime: DateTime.fromMillisecondsSinceEpoch(
map['lastModifiedTime'],
),
syncStatus: ProblemSyncStatus.values[map['syncStatus']],
censorTaskId: map['censorTaskId'],
bindData: map['bindData'],
isChecked: map['isChecked'] == 1,
);
}
/// JSON字符串
String toJson() => json.encode(toMap());
/// JSON字符串创建对象
factory Problem.fromJson(String source) =>
Problem.fromMap(json.decode(source));
}

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

24
lib/data/models/server_problem.dart

@ -0,0 +1,24 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'server_problem.freezed.dart';
part 'server_problem.g.dart';
@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<String>? imageUrls,
required DateTime creationTime,
required String creatorId,
DateTime? lastModificationTime,
String? lastModifierId,
}) = _ServerProblem;
factory ServerProblem.fromJson(Map<String, Object?> json) =>
_$ServerProblemFromJson(json);
}

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,
};

81
lib/data/models/user/organization.dart

@ -0,0 +1,81 @@
class Organization {
String? id;
String? name;
dynamic organizationTypes;
dynamic newAuth;
String? otherAuth;
String? psmAuth;
String? dangerAuth;
dynamic parentId;
int? order;
bool? enabled;
String? level;
String? code;
List<dynamic>? organizationTypeIds;
String? creationTime;
dynamic creatorId;
DateTime? lastModificationTime;
String? lastModifierId;
Organization({
this.id,
this.name,
this.organizationTypes,
this.newAuth,
this.otherAuth,
this.psmAuth,
this.dangerAuth,
this.parentId,
this.order,
this.enabled,
this.level,
this.code,
this.organizationTypeIds,
this.creationTime,
this.creatorId,
this.lastModificationTime,
this.lastModifierId,
});
factory Organization.fromJson(Map<String, dynamic> json) => Organization(
id: json['id'] as String?,
name: json['name'] as String?,
organizationTypes: json['organizationTypes'] as dynamic,
newAuth: json['newAuth'] as dynamic,
otherAuth: json['otherAuth'] as String?,
psmAuth: json['psmAuth'] as String?,
dangerAuth: json['dangerAuth'] as String?,
parentId: json['parentId'] as dynamic,
order: json['order'] as int?,
enabled: json['enabled'] as bool?,
level: json['level'] as String?,
code: json['code'] as String?,
organizationTypeIds: json['organizationTypeIds'] as List<dynamic>?,
creationTime: json['creationTime'] as String?,
creatorId: json['creatorId'] as dynamic,
lastModificationTime: json['lastModificationTime'] == null
? null
: DateTime.parse(json['lastModificationTime'] as String),
lastModifierId: json['lastModifierId'] as String?,
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'organizationTypes': organizationTypes,
'newAuth': newAuth,
'otherAuth': otherAuth,
'psmAuth': psmAuth,
'dangerAuth': dangerAuth,
'parentId': parentId,
'order': order,
'enabled': enabled,
'level': level,
'code': code,
'organizationTypeIds': organizationTypeIds,
'creationTime': creationTime,
'creatorId': creatorId,
'lastModificationTime': lastModificationTime?.toIso8601String(),
'lastModifierId': lastModifierId,
};
}

15
lib/data/models/user/page.dart

@ -0,0 +1,15 @@
class Page {
String? id;
String? name;
dynamic url;
Page({this.id, this.name, this.url});
factory Page.fromJson(Map<String, dynamic> json) => Page(
id: json['id'] as String?,
name: json['name'] as String?,
url: json['url'] as dynamic,
);
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'url': url};
}

15
lib/data/models/user/role.dart

@ -0,0 +1,15 @@
class Role {
String? id;
String? name;
dynamic desc;
Role({this.id, this.name, this.desc});
factory Role.fromJson(Map<String, dynamic> json) => Role(
id: json['id'] as String?,
name: json['name'] as String?,
desc: json['desc'] as dynamic,
);
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'desc': desc};
}

81
lib/data/models/user/user.dart

@ -0,0 +1,81 @@
import 'organization.dart';
import 'page.dart';
import 'role.dart';
class User {
String? id;
String? username;
dynamic email;
String? name;
bool? enabled;
dynamic posts;
String? organizationId;
Organization? organization;
String? organizationName;
String? organizationLevel;
List<Role>? roles;
List<dynamic>? permissions;
List<Page>? pages;
dynamic company;
String? signatureImage;
User({
this.id,
this.username,
this.email,
this.name,
this.enabled,
this.posts,
this.organizationId,
this.organization,
this.organizationName,
this.organizationLevel,
this.roles,
this.permissions,
this.pages,
this.company,
this.signatureImage,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as String?,
username: json['username'] as String?,
email: json['email'] as dynamic,
name: json['name'] as String?,
enabled: json['enabled'] as bool?,
posts: json['posts'] as dynamic,
organizationId: json['organizationId'] as String?,
organization: json['organization'] == null
? null
: Organization.fromJson(json['organization'] as Map<String, dynamic>),
organizationName: json['organizationName'] as String?,
organizationLevel: json['organizationLevel'] as String?,
roles: (json['roles'] as List<dynamic>?)
?.map((e) => Role.fromJson(e as Map<String, dynamic>))
.toList(),
permissions: json['permissions'] as List<dynamic>?,
pages: (json['pages'] as List<dynamic>?)
?.map((e) => Page.fromJson(e as Map<String, dynamic>))
.toList(),
company: json['company'] as dynamic,
signatureImage: json['signatureImage'] as String?,
);
Map<String, dynamic> toJson() => {
'id': id,
'username': username,
'email': email,
'name': name,
'enabled': enabled,
'posts': posts,
'organizationId': organizationId,
'organization': organization?.toJson(),
'organizationName': organizationName,
'organizationLevel': organizationLevel,
'roles': roles?.map((e) => e.toJson()).toList(),
'permissions': permissions,
'pages': pages?.map((e) => e.toJson()).toList(),
'company': company,
'signatureImage': signatureImage,
};
}

62
lib/data/providers/connectivity_provider.dart

@ -0,0 +1,62 @@
// lib/data/providers/connectivity_provider.dart
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
class ConnectivityProvider extends GetxService {
final Connectivity _connectivity = Connectivity();
final RxBool isOnline = false.obs;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
@override
void onInit() {
super.onInit();
_initConnectivityListener();
}
@override
void onClose() {
_connectivitySubscription.cancel();
super.onClose();
}
Future<void> _initConnectivityListener() async {
_connectivitySubscription = _connectivity.onConnectivityChanged.listen((
results,
) {
final isConnected = results.any(
(result) =>
result == ConnectivityResult.mobile ||
result == ConnectivityResult.wifi ||
result == ConnectivityResult.ethernet,
);
isOnline.value = isConnected;
if (isConnected) {
Get.snackbar(
'网络状态',
'已连接到网络',
colorText: Colors.white,
backgroundColor: Colors.green,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
'网络状态',
'已断开网络连接',
colorText: Colors.white,
backgroundColor: Colors.red,
snackPosition: SnackPosition.TOP,
);
}
});
final initialResults = await _connectivity.checkConnectivity();
isOnline.value = initialResults.any(
(result) =>
result == ConnectivityResult.mobile ||
result == ConnectivityResult.wifi ||
result == ConnectivityResult.ethernet,
);
}
}

258
lib/data/providers/http_provider.dart

@ -0,0 +1,258 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart' hide Response;
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/core/utils/constants/api_endpoints.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
// DioProvider GetxService
// Dio
class HttpProvider extends GetxService {
late final Dio _dio;
@override
Future<void> onInit() async {
super.onInit();
_initDio();
}
// Dio
void _initDio() {
_dio = Dio(
BaseOptions(
baseUrl: ApiEndpoints.baseUrl,
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
//
// 1. (AuthInterceptor): token 401
// 2. (ErrorInterceptor):
// 3. (LoggerInterceptor): 便
_dio.interceptors.addAll(_getInterceptors());
}
List<Interceptor> _getInterceptors() {
return [_getErrorInterceptor(), if (kDebugMode) _getLoggerInterceptor()];
}
///
Interceptor _getLoggerInterceptor() {
return PrettyDioLogger(
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
compact: false,
maxWidth: 90,
);
}
/// Snackbar
Interceptor _getErrorInterceptor() {
return InterceptorsWrapper(
//
onRequest: (options, handler) async {
try {
// AuthRepository token
final authRepository = Get.find<AuthRepository>();
final token = authRepository.getToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
} catch (e) {
// AuthRepository
Get.snackbar(
'认证过期',
'请重新手动登录',
colorText: Colors.white,
backgroundColor: Colors.red,
snackPosition: SnackPosition.TOP,
);
}
return handler.next(options);
},
onError: (error, handler) {
//
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout) {
Get.snackbar(
'网络超时',
'请检查网络连接后重试',
colorText: Colors.white,
backgroundColor: Colors.red,
snackPosition: SnackPosition.TOP,
);
} else if (error.type == DioExceptionType.unknown) {
Get.snackbar(
'网络异常',
'请检查网络连接后重试',
colorText: Colors.white,
backgroundColor: Colors.red,
snackPosition: SnackPosition.TOP,
);
}
// AuthInterceptor 401
if (error.response != null) {
final message = _handleStatusCode(error.response!);
Get.snackbar(
'请求错误',
message,
colorText: Colors.white,
backgroundColor: Colors.red,
snackPosition: SnackPosition.TOP,
);
}
return handler.next(error);
},
);
}
/// HTTP
String _handleStatusCode(Response response) {
switch (response.statusCode) {
case 400:
return response.data?['detail'] ?? '请求参数错误';
case 401:
final authRepository = Get.find<AuthRepository>();
authRepository.clearAuthData();
Get.offAllNamed(AppRoutes.login);
return '未授权,请重新登录';
case 403:
return response.data?['detail'] ?? '访问被拒绝';
case 404:
return response.data?['detail'] ?? '请求资源不存在';
case 422:
final errors = response.data?['errors'];
if (errors != null && errors is Map && errors.isNotEmpty) {
return errors.values.first?.first?.toString() ?? '数据验证失败';
}
return response.data?['detail'] ?? '数据验证失败';
case 500:
return response.data?['detail'] ?? '服务器内部错误';
case 502:
return response.data?['detail'] ?? '网关错误';
case 503:
return response.data?['detail'] ?? '服务不可用';
default:
return response.data?['detail'] ?? '网络异常(${response.statusCode})';
}
}
void clear() {
_dio.interceptors.clear();
}
///
/// GET
Future<Response> get(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
/// POST
Future<Response> post(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// PUT
Future<Response> put(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return await _dio.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
/// DELETE
Future<Response> delete(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
return await _dio.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
///
Future<Response> download(
String urlPath,
String savePath, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
bool deleteOnError = true,
int? lengthHeader,
Object? data,
Options? requestOptions,
}) async {
return await _dio.download(
urlPath,
savePath,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}
}

271
lib/data/providers/sqlite_provider.dart

@ -0,0 +1,271 @@
// sqlite_provider.dart
import 'package:get/get.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// `SQLiteProvider` GetxService SQLite
///
class SQLiteProvider extends GetxService {
static const String _dbName = 'problems.db';
static const String _tableName = 'problems';
static const int _dbVersion = 1;
late Database _database;
@override
void onInit() {
super.onInit();
_initDatabase();
}
///
Future<void> _initDatabase() async {
try {
final databasePath = await getDatabasesPath();
final path = join(databasePath, _dbName);
_database = await openDatabase(
path,
version: _dbVersion,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
Get.log('数据库初始化成功');
} catch (e) {
Get.log('数据库初始化失败:$e', isError: true);
rethrow;
}
}
///
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $_tableName(
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
location TEXT NOT NULL,
imageUrls TEXT NOT NULL,
creationTime INTEGER NOT NULL,
lastModifiedTime INTEGER NOT NULL,
syncStatus INTEGER NOT NULL,
censorTaskId TEXT,
bindData TEXT,
isChecked INTEGER NOT NULL
)
''');
Get.log('数据库表创建成功');
}
///
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...');
//
for (int version = oldVersion + 1; version <= newVersion; version++) {
await _runMigration(db, version);
}
Get.log('数据库升级完成');
}
///
Future<void> _runMigration(Database db, int version) async {
switch (version) {
case 2:
// 2
// await db.execute('ALTER TABLE $_tableName ADD COLUMN newColumn TEXT;');
break;
//
default:
Get.log('没有找到版本 $version 的迁移脚本');
}
}
///
Future<int> insertProblem(Problem problem) async {
try {
final result = await _database.insert(
_tableName,
problem.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
Get.log('问题记录插入成功,ID: ${problem.id}');
return result;
} catch (e) {
Get.log('插入问题失败(ID: ${problem.id}):$e', isError: true);
throw Exception('');
}
}
///
Future<int> deleteProblem(String problemId) async {
try {
final result = await _database.delete(
_tableName,
where: 'id = ?',
whereArgs: [problemId],
);
if (result > 0) {
Get.log('问题删除成功,ID: $problemId');
} else {
Get.log('未找到要删除的问题,ID: $problemId');
}
return result;
} catch (e) {
Get.log('删除问题失败(ID: $problemId):$e', isError: true);
return 0;
}
}
///
Future<int> updateProblem(Problem problem) async {
try {
final result = await _database.update(
_tableName,
problem.toMap(),
where: 'id = ?',
whereArgs: [problem.id],
);
if (result > 0) {
Get.log('问题更新成功,ID: ${problem.id}');
}
return result;
} catch (e) {
Get.log('更新问题失败(ID: ${problem.id}):$e', isError: true);
return 0;
}
}
// ///
// Future<List<Problem>> getProblemsForSync() async {
// try {
// final results = await _database.query(
// _tableName,
// where: 'syncStatus = ?',
// whereArgs: [SyncStatus.notSynced.index],
// orderBy: 'creationTime ASC',
// );
// Get.log('找到 ${results.length} 条需要同步的记录');
// return results.map((json) => Problem.fromMap(json)).toList();
// } catch (e) {
// Get.log('获取待同步问题失败:$e', isError: true);
// return [];
// }
// }
///
Future<int> markAsSynced(String id) async {
try {
final result = await _database.update(
_tableName,
{'syncStatus': ProblemSyncStatus.synced.index},
where: 'id = ?',
whereArgs: [id],
);
if (result > 0) {
Get.log('问题标记为已同步,ID: $id');
}
return result;
} catch (e) {
Get.log('标记同步状态失败(ID: $id):$e', isError: true);
return 0;
}
}
/// ID获取问题记录
Future<Problem?> getProblemById(String id) async {
try {
final results = await _database.query(
_tableName,
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
return results.isNotEmpty ? Problem.fromMap(results.first) : null;
} catch (e) {
Get.log('获取问题失败(ID: $id):$e', isError: true);
return null;
}
}
///
Future<List<Problem>> getProblems({
DateTime? startDate,
DateTime? endDate,
String? syncStatus,
String? bindStatus,
}) async {
try {
final whereClauses = <String>[];
final whereArgs = <dynamic>[];
//
if (startDate != null) {
whereClauses.add('creationTime >= ?');
whereArgs.add(startDate.millisecondsSinceEpoch);
}
if (endDate != null) {
whereClauses.add('creationTime <= ?');
whereArgs.add(endDate.millisecondsSinceEpoch);
}
//
if (syncStatus != null && syncStatus != '全部') {
if (syncStatus == '未上传') {
whereClauses.add('syncStatus IN (?, ?, ?)');
whereArgs.addAll([
ProblemSyncStatus.pendingCreate.index,
ProblemSyncStatus.pendingUpdate.index,
ProblemSyncStatus.pendingDelete.index,
]);
} else {
whereClauses.add('syncStatus = ?');
whereArgs.add(ProblemSyncStatus.synced.index);
}
}
//
if (bindStatus != null && bindStatus != '全部') {
if (bindStatus == '已绑定') {
whereClauses.add('bindData IS NOT NULL AND bindData != ""');
} else {
whereClauses.add('(bindData IS NULL OR bindData = "")');
}
}
final results = await _database.query(
_tableName,
where: whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null,
whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'creationTime DESC',
);
return results.map((json) => Problem.fromMap(json)).toList();
} catch (e) {
Get.log('获取问题列表失败:$e', isError: true);
return [];
}
}
@override
void onClose() {
_database.close();
Get.log('数据库连接已关闭');
super.onClose();
}
}

121
lib/data/repositories/auth_repository.dart

@ -0,0 +1,121 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:problem_check_system/core/utils/constants/api_endpoints.dart';
import 'package:problem_check_system/data/models/auth_model.dart';
import 'package:problem_check_system/data/models/user/user.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
class AuthRepository extends GetxService {
final HttpProvider httpProvider;
final GetStorage storage;
final ConnectivityProvider connectivityProvider;
AuthRepository({
required this.httpProvider,
required this.storage,
required this.connectivityProvider,
});
static const String _tokenKey = 'token';
static const String _refreshTokenKey = 'refresh_token';
static const String _loginKey = 'user';
static const String _rememberMe = 'remember_me';
void saveToken(String token) {
storage.write(_tokenKey, token);
}
String? getToken() {
return storage.read(_tokenKey);
}
void saveRefreshToken(String refreshToken) {
storage.write(_refreshTokenKey, refreshToken);
}
String? getRefreshToken() {
return storage.read(_refreshTokenKey);
}
void addLoginKey(LoginRequest login) {
storage.write(_loginKey, login.toJson());
}
///
LoginRequest getLoginKey() {
final loginData = storage.read(_loginKey);
//
if (loginData != null) {
//
return LoginRequest.fromJson(Map<String, dynamic>.from(loginData));
}
//
return LoginRequest(username: '', password: '');
}
void removeLoginKey() {
storage.remove(_loginKey);
}
void addRememberMe(bool remembered) {
storage.write(_rememberMe, remembered);
}
bool getRememberMe() {
return storage.read(_rememberMe) ?? false;
}
void clearAuthData() {
storage.remove(_tokenKey);
storage.remove(_refreshTokenKey);
}
// 线
bool get isOnline {
return connectivityProvider.isOnline.value;
}
/// Check if a user is currently logged in by verifying the existence of a token.
bool isLoggedIn() {
final token = getToken();
return token != null && token.isNotEmpty;
}
/// Handles the user login process by calling the API and saving the response.
Future<LoginResponse> login(LoginRequest request) async {
final response = await httpProvider.post(
ApiEndpoints.postLogin,
data: request.toJson(),
);
final loginResponse = LoginResponse.fromJson(response.data);
return loginResponse;
}
/// API
Future<User> getUserProfile() async {
final response = await httpProvider.get(ApiEndpoints.getUserProfile);
// JSON Profile
return User.fromJson(response.data);
}
/// Refreshes the authentication token using the refresh token.
Future<LoginResponse> refreshToken() async {
final refreshToken = getRefreshToken();
final response = await httpProvider.post(
ApiEndpoints.postRefreshToken,
data: {'refresh_token': refreshToken},
);
final authResponse = LoginResponse.fromJson(response.data);
saveToken(authResponse.token);
saveRefreshToken(authResponse.refreshToken);
return authResponse;
}
}

65
lib/data/repositories/file_repository.dart

@ -0,0 +1,65 @@
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: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/providers/http_provider.dart';
class FileRepository extends GetxService {
final HttpProvider _httpProvider = Get.find<HttpProvider>();
/// @param imageFilePath
/// @param cancelToken
/// @param onSendProgress
/// @return URL
Future<String> uploadImage(
String imageFilePath, {
required CancelToken cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
// 1. FormData multipart/form-data
final formData = FormData.fromMap({
// 'file':
'file': await MultipartFile.fromFile(
imageFilePath,
filename: p.basename(imageFilePath),
),
});
// 2. 使 HttpProvider post
final response = await _httpProvider.post(
ApiEndpoints.postUploadFile,
data: formData,
cancelToken: cancelToken, // post
onSendProgress: onSendProgress, // post
);
// --- () ---
if (kDebugMode) {
debugPrint('服务器返回的状态码: ${response.statusCode}');
debugPrint('服务器返回的原始数据: ${response.data}');
}
// 3. URL
if (response.isSuccess) {
final Map<String, dynamic> data = response.data;
// URL 'url'
String imageUrl =
"${ApiEndpoints.baseUrl}${ApiEndpoints.postUploadFile}/${data['objectName']}";
return imageUrl;
} else {
throw Exception('上传失败,状态码: ${response.statusCode}');
}
} on DioException catch (e) {
Get.log('图片上传发生未知错误: $e');
throw Exception('图片上传失败: ${e.message}');
} catch (e) {
Get.log('图片上传发生未知错误: $e');
throw Exception('图片上传发生未知错误: $e');
}
}
}

8
lib/data/repositories/image_repository.dart

@ -0,0 +1,8 @@
// image_repository.dart
abstract class ImageRepository {
Future<String> downloadImage(String imageUrl, String problemId);
Future<bool> isImageDownloaded(String imageUrl, String problemId);
Future<String?> getLocalImagePath(String imageUrl, String problemId);
Future<void> deleteProblemImages(String problemId);
Future<void> cleanupCache({Duration maxAge = const Duration(days: 30)});
}

202
lib/data/repositories/image_repository_impl.dart

@ -0,0 +1,202 @@
// image_repository_impl.dart
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:problem_check_system/data/providers/http_provider.dart';
import 'package:problem_check_system/data/repositories/image_repository.dart';
class ImageRepositoryImpl extends GetxService implements ImageRepository {
final HttpProvider httpProvider;
ImageRepositoryImpl({required this.httpProvider});
@override
Future<String> downloadImage(String imageUrl, String problemId) async {
try {
// 1.
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String problemDirPath = path.join(
appDocDir.path,
'problems',
problemId,
'images',
);
final Directory problemDir = Directory(problemDirPath);
// 2.
if (!await problemDir.exists()) {
await problemDir.create(recursive: true);
}
// 3.
final String fileExtension = _getFileExtensionFromUrl(imageUrl);
final String fileName =
'${_generateFileNameHash(imageUrl)}.$fileExtension';
final String filePath = path.join(problemDir.path, fileName);
// 4. 使 Dio
final response = await httpProvider.download(
imageUrl,
filePath,
options: Options(
responseType: ResponseType.bytes,
followRedirects: true,
receiveTimeout: const Duration(seconds: 30),
),
onReceiveProgress: (received, total) {
if (total != -1) {
Get.log('下载进度: ${(received / total * 100).toStringAsFixed(1)}%');
}
},
);
if (response.statusCode == 200) {
//
final File file = File(filePath);
if (await file.exists()) {
return filePath;
} else {
throw Exception('文件写入失败');
}
} else {
throw Exception('下载失败: HTTP ${response.statusCode}');
}
} on DioException catch (e) {
throw Exception('图片下载失败: ${e.message}');
} catch (e) {
throw Exception('图片下载失败: $e');
}
}
@override
Future<bool> isImageDownloaded(String imageUrl, String problemId) async {
try {
final String fileExtension = _getFileExtensionFromUrl(imageUrl);
final String fileName =
'${_generateFileNameHash(imageUrl)}.$fileExtension';
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String filePath = path.join(
appDocDir.path,
'problems',
problemId,
'images',
fileName,
);
return await File(filePath).exists();
} catch (e) {
return false;
}
}
@override
Future<String?> getLocalImagePath(String imageUrl, String problemId) async {
try {
final String fileExtension = _getFileExtensionFromUrl(imageUrl);
final String fileName =
'${_generateFileNameHash(imageUrl)}.$fileExtension';
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String filePath = path.join(
appDocDir.path,
'problems',
problemId,
'images',
fileName,
);
if (await File(filePath).exists()) {
return filePath;
}
return null;
} catch (e) {
return null;
}
}
@override
Future<void> deleteProblemImages(String problemId) async {
try {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String problemDirPath = path.join(
appDocDir.path,
'problems',
problemId,
);
final Directory problemDir = Directory(problemDirPath);
if (await problemDir.exists()) {
await problemDir.delete(recursive: true);
}
} catch (e) {
Get.log('删除图片失败: $e');
}
}
@override
Future<void> cleanupCache({
Duration maxAge = const Duration(days: 30),
}) async {
try {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String problemsDirPath = path.join(appDocDir.path, 'problems');
final Directory problemsDir = Directory(problemsDirPath);
if (await problemsDir.exists()) {
final DateTime cutoffTime = DateTime.now().subtract(maxAge);
final List<FileSystemEntity> entities = await problemsDir
.list()
.toList();
for (final entity in entities) {
if (entity is Directory) {
try {
final FileStat stat = await entity.stat();
if (stat.modified.isBefore(cutoffTime)) {
await entity.delete(recursive: true);
Get.log('已清理过期目录: ${entity.path}');
}
} catch (e) {
Get.log('无法获取目录状态: ${entity.path}, 错误: $e');
//
try {
await entity.delete(recursive: true);
Get.log('强制清理目录: ${entity.path}');
} catch (deleteError) {
Get.log('删除目录失败: ${entity.path}, 错误: $deleteError');
}
}
}
}
}
} catch (e) {
Get.log('清理缓存失败: $e');
}
}
//
String _getFileExtensionFromUrl(String url) {
try {
final Uri uri = Uri.parse(url);
final String path = uri.path;
if (path.endsWith('.jpg') || path.endsWith('.jpeg')) return 'jpg';
if (path.endsWith('.png')) return 'png';
if (path.endsWith('.gif')) return 'gif';
if (path.endsWith('.webp')) return 'webp';
if (path.endsWith('.bmp')) return 'bmp';
return 'jpg';
} catch (e) {
return 'jpg';
}
}
String _generateFileNameHash(String url) {
return url.hashCode.abs().toString();
}
}

135
lib/data/repositories/problem_repository.dart

@ -0,0 +1,135 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart' hide MultipartFile, FormData, Response;
import 'package:problem_check_system/core/extensions/http_response_extension.dart';
import 'package:problem_check_system/core/utils/constants/api_endpoints.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/data/models/server_problem.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
import 'package:problem_check_system/data/providers/sqlite_provider.dart';
///
///
class ProblemRepository extends GetxService {
final SQLiteProvider sqliteProvider;
final HttpProvider httpProvider;
final ConnectivityProvider connectivityProvider;
RxBool get isOnline => connectivityProvider.isOnline;
ProblemRepository({
required this.sqliteProvider,
required this.httpProvider,
required this.connectivityProvider,
});
///
Future<void> updateProblem(Problem problem) async {
await sqliteProvider.updateProblem(problem);
}
///
/// - `startDate`/`endDate`
/// - `syncStatus`'已上传', '未上传', '全部'
/// - `bindStatus`'已绑定', '未绑定', '全部'
Future getProblems({
DateTime? startDate,
DateTime? endDate,
String? syncStatus,
String? bindStatus,
}) async {
return await sqliteProvider.getProblems(
startDate: startDate,
endDate: endDate,
syncStatus: syncStatus,
bindStatus: bindStatus,
);
}
Future<void> insertProblem(Problem problem) async {
await sqliteProvider.insertProblem(problem);
}
Future<void> deleteProblem(String problemId) async {
await sqliteProvider.deleteProblem(problemId);
}
// ProblemRepository中添加
Future<List<ServerProblem>> fetchProblemsFromServer({
DateTime? startTime,
DateTime? endTime,
int? pageNumber,
int? pageSize,
CancelToken? cancelToken,
}) async {
try {
final response = await httpProvider.get(
ApiEndpoints.getProblems,
queryParameters: {
if (startTime != null)
'StartTime': startTime.toUtc().toIso8601String(),
if (endTime != null) 'EndTime': endTime.toUtc().toIso8601String(),
if (pageNumber != null) 'pageNumber': pageNumber,
if (pageSize != null) 'pageSize': pageSize,
},
cancelToken: cancelToken,
);
if (response.isSuccess) {
// Dio JSONresponse.data Map List
final Map<String, dynamic> data = response.data;
final List<dynamic> items = data['items'];
// 使 Freezed fromJson
return items.map((item) => ServerProblem.fromJson(item)).toList();
} else {
throw Exception('拉取问题失败: ${response.statusCode}');
}
} on DioException catch (e) {
Get.log("Dio 异常$e");
rethrow;
} catch (e) {
Get.log("解析失败:$e");
rethrow;
}
}
/// post
Future<Response> post(
Map<String, Object> apiPayload,
CancelToken cancelToken,
) async {
// 3.
final response = await httpProvider.post(
ApiEndpoints.postProblem,
data: apiPayload,
cancelToken: cancelToken,
);
return response;
}
/// put
Future<Response> put(
String id,
Map<String, Object> apiPayload,
CancelToken cancelToken,
) async {
// 3.
final response = await httpProvider.put(
ApiEndpoints.putProblemById(id),
data: apiPayload,
cancelToken: cancelToken,
);
return response;
}
/// delete
Future<Response> delete(String id, CancelToken cancelToken) async {
// 3.
final response = await httpProvider.delete(
ApiEndpoints.deleteProblemById(id),
cancelToken: cancelToken,
);
return response;
}
}

67
lib/main.dart

@ -1,12 +1,26 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart'; import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:get/get_navigation/src/routes/get_route.dart'; import 'package:get_storage/get_storage.dart';
import 'package:get/get_navigation/src/routes/transitions_type.dart'; import 'package:problem_check_system/app/routes/app_pages.dart';
import 'package:problem_check_system/modules/home/home_page.dart'; import 'package:problem_check_system/app/routes/app_routes.dart'; //
import 'package:problem_check_system/modules/login/views/login_page.dart'; import 'package:problem_check_system/app/bindings/initial_binding.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
void main() async {
// Flutter Binding
WidgetsFlutterBinding.ensureInitialized();
//
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// GetStorage
await GetStorage.init();
// Add this line
await ScreenUtil.ensureScreenSize();
void main() {
runApp(const MainApp()); runApp(const MainApp());
} }
@ -15,38 +29,35 @@ class MainApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//稿,dp // 稿, dp
return ScreenUtilInit( return ScreenUtilInit(
designSize: const Size(375, 812), designSize: const Size(375, 812),
minTextAdapt: true, minTextAdapt: true,
splitScreenMode: true, splitScreenMode: true,
builder: (context, child) { builder: (context, _) {
// 使 _ child
return GetMaterialApp( return GetMaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'First Method', // --- ---
// You can use the library anywhere in the app even in theme localizationsDelegates: const [
theme: ThemeData( // Material
useMaterial3: true, GlobalMaterialLocalizations.delegate,
primarySwatch: Colors.blue, // Widgets
// textTheme: Typography.englishLike2018.apply(fontSizeFactor: 1.sp), GlobalWidgetsLocalizations.delegate,
), // iOS
initialRoute: '/', GlobalCupertinoLocalizations.delegate,
getPages: [ ],
GetPage( supportedLocales: const [
name: '/', Locale('zh', 'CN'), //
page: () => LoginPage(),
transition: Transition.cupertino,
),
GetPage(
name: '/home',
page: () => HomePage(),
transition: Transition.cupertino,
),
], ],
home: child, title: '问题检查系统', // 使
theme: ThemeData(useMaterial3: true, primarySwatch: Colors.blue),
// 使 GetX
initialRoute: AppRoutes.login, // 使
initialBinding: InitialBinding(), //
getPages: AppPages.routes, //
); );
}, },
child: LoginPage(),
); );
} }
} }

12
lib/modules/auth/bindings/login_binding.dart

@ -0,0 +1,12 @@
import 'package:get/get.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
import 'package:problem_check_system/modules/auth/controllers/login_controller.dart';
class LoginBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<LoginController>(
() => LoginController(authRepository: Get.find<AuthRepository>()),
);
}
}

99
lib/modules/auth/controllers/login_controller.dart

@ -0,0 +1,99 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/data/models/auth_model.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
class LoginController extends GetxController {
final AuthRepository _authRepository;
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
final isLoading = false.obs;
final rememberMe = false.obs;
LoginController({required AuthRepository authRepository})
: _authRepository = authRepository;
@override
void onInit() {
super.onInit();
_loadRememberedMe();
}
void _loadRememberedMe() {
rememberMe.value = _authRepository.getRememberMe();
if (rememberMe.value) {
final loginData = _authRepository.getLoginKey();
usernameController.text = loginData.username;
passwordController.text = loginData.password;
}
}
/// Check if the user is already logged in by delegating to the repository.
bool isLoggedIn() {
return _authRepository.isLoggedIn();
}
///
Future<void> login() async {
final username = usernameController.text.trim();
final password = passwordController.text.trim();
if (username.isEmpty || password.isEmpty) {
Get.snackbar('输入错误', '用户名和密码不能为空');
return;
}
isLoading.value = true;
final loginData = LoginRequest(username: username, password: password);
if (_authRepository.isOnline) {
await _onlineLogin(loginData);
} else {
_offlineLogin(loginData);
}
isLoading.value = false;
}
/// 线
Future<void> _onlineLogin(LoginRequest loginRequest) async {
try {
// post
var loginResponse = await _authRepository.login(loginRequest);
//
_authRepository.saveToken(loginResponse.token);
_authRepository.saveRefreshToken(loginResponse.refreshToken);
_authRepository.addRememberMe(rememberMe.value);
if (rememberMe.value) {
_authRepository.addLoginKey(loginRequest);
} else {
_authRepository.removeLoginKey();
}
Get.offAllNamed(AppRoutes.home);
//
debugPrint('登录成功: $loginResponse');
} on DioException catch (e) {
// DioException
// Snackbar
debugPrint('登录失败,由DioException捕获: ${e.message}');
} catch (e) {
// DioException
debugPrint('发生未知错误: $e');
}
}
void _offlineLogin(LoginRequest loginRequest) {
final loginData = _authRepository.getLoginKey();
if (loginData.username == loginRequest.username &&
loginData.password == loginRequest.password) {
Get.offAllNamed(AppRoutes.home);
Get.snackbar('离线登录成功', '您已离线登录到系统');
} else {
Get.snackbar('登录失败', '无网络连接,且无法验证本地凭证');
}
}
}

189
lib/modules/auth/views/login_page.dart

@ -0,0 +1,189 @@
// lib/modules/auth/views/login_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/auth/controllers/login_controller.dart';
class LoginPage extends GetView<LoginController> {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(body: SingleChildScrollView(child: _buildBackground()));
}
Widget _buildBackground() {
return Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.png'),
// 使 BoxFit.cover
fit: BoxFit.fitWidth,
alignment: Alignment.topCenter,
),
),
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 89.5.h),
Padding(
padding: EdgeInsets.only(left: 28.5.w),
child: Image.asset(
'assets/images/label.png',
width: 171.5.w,
height: 23.5.h,
fit: BoxFit.fitWidth,
),
),
SizedBox(height: 15.5.h),
Padding(
padding: EdgeInsets.only(left: 28.5.w),
child: Image.asset(
'assets/images/label1.png',
width: 296.5.w,
height: 35.5.h,
fit: BoxFit.fitWidth,
),
),
SizedBox(height: 56.5.h),
Center(child: _buildLoginCard()),
],
),
],
),
);
}
// _buildLoginCard TextEditingController
Widget _buildLoginCard() {
return Container(
width: 334.w,
height: 574.5.h,
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF).withValues(alpha: 153),
borderRadius: BorderRadius.all(Radius.circular(23.5.r)),
),
padding: EdgeInsets.all(24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
// 使 TextEditingController
_buildTextFieldSection(
label: '账号',
hintText: '请输入您的账号',
controller: controller.usernameController,
),
const SizedBox(height: 22),
_buildTextFieldSection(
label: '密码',
hintText: '请输入您的密码',
obscureText: true,
controller: controller.passwordController,
),
const SizedBox(height: 9.5),
_buildRememberPasswordRow(),
const SizedBox(height: 138.5),
_buildLoginButton(),
],
),
);
}
// _buildTextFieldSection onChanged
Widget _buildTextFieldSection({
required String label,
required String hintText,
required TextEditingController controller,
bool obscureText = false,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
controller: controller, // 使
obscureText: obscureText,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(),
),
),
],
);
}
Widget _buildRememberPasswordRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(
() => Checkbox(
value: controller.rememberMe.value,
onChanged: (value) => controller.rememberMe.value = value!,
),
),
Text(
'记住密码',
style: TextStyle(color: const Color(0xFF959595), fontSize: 14.sp),
),
],
);
}
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.login,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
minimumSize: Size(double.infinity, 48.h),
),
child: Ink(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Container(
constraints: BoxConstraints(minHeight: 48.h),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 24.w),
child: Obx(() {
if (controller.isLoading.value) {
return const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
);
}
return Text(
'登录',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
);
}),
),
),
),
);
}
}

25
lib/modules/home/bindings/home_binding.dart

@ -0,0 +1,25 @@
import 'package:get/get.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
import 'package:problem_check_system/data/repositories/problem_repository.dart';
import 'package:problem_check_system/modules/home/controllers/home_controller.dart';
import 'package:problem_check_system/modules/my/controllers/my_controller.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
class HomeBinding implements Bindings {
@override
void dependencies() {
///
Get.lazyPut<HomeController>(() => HomeController());
///
Get.lazyPut<ProblemController>(
() => ProblemController(problemRepository: Get.find<ProblemRepository>()),
fenix: true,
);
///
Get.lazyPut<MyController>(
() => MyController(authRepository: Get.find<AuthRepository>()),
);
}
}

21
lib/modules/home/controllers/home_controller.dart

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/my/views/my_page.dart';
import 'package:problem_check_system/modules/problem/views/problem_page.dart';
class HomeController extends GetxController {
// 使 Rx
var selectedIndex = 0.obs;
//
final List<Widget> pages = [
// const Center(child: Text('首页内容')),
const ProblemPage(), // 使 const
const MyPage(),
];
// NavigationBar
void changeIndex(int index) {
selectedIndex.value = index;
}
}

21
lib/modules/home/home_controller.dart

@ -1,21 +0,0 @@
// Logic
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/problem_page.dart';
class HomeController extends GetxController {
//
var selectedIndex = 0.obs;
// View
final List<Widget> pages = [
Center(child: Text('首页')),
ProblemPage(),
Center(child: Text('我的内容')),
];
//
void changeIndex(int index) {
selectedIndex.value = index;
}
}

17
lib/modules/home/home_page.dart → lib/modules/home/views/home_page.dart

@ -1,15 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/modules/home/home_controller.dart'; import 'package:problem_check_system/modules/home/controllers/home_controller.dart';
class HomePage extends StatelessWidget { class HomePage extends GetView<HomeController> {
const HomePage({super.key}); const HomePage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//
final HomeController controller = Get.put(HomeController());
return Obx( return Obx(
() => Scaffold( () => Scaffold(
body: controller.pages[controller.selectedIndex.value], // body: controller.pages[controller.selectedIndex.value], //
@ -17,11 +14,11 @@ class HomePage extends StatelessWidget {
selectedIndex: controller.selectedIndex.value, selectedIndex: controller.selectedIndex.value,
onDestinationSelected: controller.changeIndex, // onDestinationSelected: controller.changeIndex, //
destinations: const [ destinations: const [
NavigationDestination( // NavigationDestination(
icon: Icon(Icons.home_outlined), // icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home), // selectedIcon: Icon(Icons.home),
label: '首页', // label: '首页',
), // ),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.description_outlined), icon: Icon(Icons.description_outlined),
selectedIcon: Icon(Icons.description), selectedIcon: Icon(Icons.description),

4
lib/modules/login/models/login_model.dart

@ -1,4 +0,0 @@
class LoginModel {
String username = "";
String password = "";
}

16
lib/modules/login/view_models/login_controller.dart

@ -1,16 +0,0 @@
import 'package:get/get.dart';
class LoginController extends GetxController {
var username = ''.obs;
var password = ''.obs;
var rememberPassword = false.obs;
void login() {
if (username.isEmpty || password.isEmpty) {
Get.snackbar('错误', '请输入完整的信息');
} else {
// API
print('用户名: ${username.value}, 密码: ${password.value}');
}
}
}

175
lib/modules/login/views/login_page.dart

@ -1,175 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/login/view_models/login_controller.dart';
class LoginPage extends StatelessWidget {
final LoginController controller = Get.put(LoginController());
LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
//
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.png'),
fit: BoxFit.fitWidth,
),
),
),
//
Positioned(
left: 28.5.w,
top: 89.5.h,
child: Image.asset(
'assets/images/label.png',
width: 171.5.w,
height: 23.5.h,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 28.5.w,
top: 128.5.h,
child: Image.asset(
'assets/images/label1.png',
width: 296.5.w,
height: 35.5.h,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 20.5.w, // 20.5dp
top: 220.5.h, // 220.5dp
child: Container(
width: 334.w, // 334dp
height: 574.5.h, // 574.5dp
decoration: BoxDecoration(
color: const Color(
0xFFFFFFFF,
).withValues(alpha: 153), // 60%
borderRadius: BorderRadius.all(
Radius.circular(23.5.r), // 23.5dp圆角
),
),
padding: EdgeInsets.all(24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
'账号',
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
// labelText: '账号',
hintText: '请输入您的账号',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 22),
Text(
'密码',
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
obscureText: true,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
// labelText: '密码',
hintText: '请输入您的密码',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 9.5),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(
() => Checkbox(
value: controller.rememberPassword.value,
onChanged: (value) =>
controller.rememberPassword.value = value!,
),
),
Text(
'记住密码',
style: TextStyle(
color: const Color(0xFF959595), //
fontSize: 14.sp, //
),
),
],
),
const SizedBox(height: 138.5),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// HomePage
Get.toNamed('/home');
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const HomePage(),
// ),
// );
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), //
),
minimumSize: Size(double.infinity, 48.h), //
),
child: Ink(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Container(
constraints: BoxConstraints(minHeight: 48.h),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
vertical: 12.h,
horizontal: 24.w,
),
child: Text(
'登录',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
],
),
),
),
],
),
);
}
}

0
lib/modules/login/views/my_page.dart

0
lib/modules/login/views/problem_page.dart

13
lib/modules/my/bindings/change_password_binding.dart

@ -0,0 +1,13 @@
import 'package:get/get.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
import 'package:problem_check_system/modules/my/controllers/change_password_controller.dart';
class ChangePasswordBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<ChangePasswordController>(
() =>
ChangePasswordController(authRepository: Get.find<AuthRepository>()),
);
}
}

54
lib/modules/my/controllers/change_password_controller.dart

@ -0,0 +1,54 @@
import 'package:get/get.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
class ChangePasswordController extends GetxController {
//
var newPassword = ''.obs;
var confirmPassword = ''.obs;
var isLoading = false.obs;
final AuthRepository authRepository;
ChangePasswordController({required this.authRepository});
//
void updateNewPassword(String value) {
newPassword.value = value;
}
//
void updateConfirmPassword(String value) {
confirmPassword.value = value;
}
//
Future<void> changePassword() async {
//
if (newPassword.value.isEmpty || confirmPassword.value.isEmpty) {
Get.snackbar('错误', '密码不能为空');
return;
}
if (newPassword.value != confirmPassword.value) {
Get.snackbar('错误', '两次输入的密码不一致');
return;
}
isLoading.value = true;
try {
// final response = await _userProvider.changePassword(newPassword.value);
// if (response.statusCode == 200) {
// Get.back();
// Get.snackbar('成功', '密码修改成功');
// } else {
// Get.snackbar('失败', '密码修改失败,请重试');
// }
//
await Future.delayed(const Duration(seconds: 2));
Get.back();
Get.snackbar('成功', '密码修改成功', snackbarStatus: (status) {});
} catch (e) {
Get.snackbar('错误', '修改密码失败: ${e.toString()}');
} finally {
isLoading.value = false;
}
}
}

54
lib/modules/my/controllers/my_controller.dart

@ -0,0 +1,54 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/data/repositories/auth_repository.dart';
class MyController extends GetxController {
final AuthRepository authRepository;
MyController({required this.authRepository});
//
var userName = '张兰雪'.obs;
var userPhone = '138****8547'.obs;
var userImage = "".obs;
@override
void onInit() {
super.onInit();
_loadUserInfo();
}
// GetxController
RxBool isLoading = false.obs;
/// API加载用户信息
Future<void> _loadUserInfo() async {
// true
isLoading.value = true;
try {
//
final userProfile = await authRepository.getUserProfile();
// null使
userName.value = userProfile.name ?? "";
userPhone.value = userProfile.email ?? '138****8547';
userImage.value = userProfile.signatureImage.toString();
} on DioException catch (e) {
// DioException
// Snackbar
Get.log(e.toString());
} catch (e) {
// DioException
} finally {
isLoading.value = false;
}
}
void logout() {
authRepository.clearAuthData();
Get.offAllNamed(AppRoutes.login);
}
//
}

155
lib/modules/my/views/change_password.dart

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:problem_check_system/modules/my/controllers/change_password_controller.dart';
class ChangePasswordPage extends StatelessWidget {
const ChangePasswordPage({super.key});
@override
Widget build(BuildContext context) {
//
final ChangePasswordController controller =
Get.find<ChangePasswordController>();
return Scaffold(
appBar: _buildAppBar(),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
children: [
SizedBox(height: 16.h),
_buildInputField(
label: '新密码',
hintText: '请输入新密码',
onChanged: controller.updateNewPassword,
obscureText: true,
),
SizedBox(height: 24.h),
_buildInputField(
label: '确认新密码',
hintText: '请再次输入新密码',
onChanged: controller.updateConfirmPassword,
obscureText: true,
),
const Spacer(), //
_buildButtons(controller),
SizedBox(height: 50.h),
],
),
),
);
}
/// AppBar
AppBar _buildAppBar() {
return AppBar(
backgroundColor: const Color(0xFFF1F7FF),
elevation: 0,
centerTitle: true,
title: const Text(
'修改密码',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
onPressed: () => Get.back(),
),
);
}
///
Widget _buildInputField({
required String label,
required String hintText,
required Function(String) onChanged,
bool obscureText = false,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 25.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
SizedBox(height: 8.h),
TextField(
onChanged: onChanged,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey, fontSize: 14.sp),
border: InputBorder.none, // 线
isDense: true,
contentPadding: EdgeInsets.zero,
),
),
],
),
);
}
///
Widget _buildButtons(ChangePasswordController controller) {
return Row(
children: [
//
Expanded(
child: OutlinedButton(
onPressed: () => Get.back(),
style: OutlinedButton.styleFrom(
minimumSize: Size(160.w, 48.h),
side: const BorderSide(color: Color(0xFF5695FD)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'取消',
style: TextStyle(fontSize: 16.sp, color: const Color(0xFF5695FD)),
),
),
),
SizedBox(width: 16.w),
//
Expanded(
child: ElevatedButton(
onPressed: () {
//
controller.changePassword();
},
style: ElevatedButton.styleFrom(
minimumSize: Size(160.w, 48.h),
backgroundColor: const Color(0xFF5695FD),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'确定',
style: TextStyle(fontSize: 16.sp, color: Colors.white),
),
),
),
],
);
}
}

222
lib/modules/my/views/my_page.dart

@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/my/controllers/my_controller.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
class MyPage extends GetView<MyController> {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
// Obx listens to changes in the controller's observable state,
// such as an isLoading flag, and rebuilds the widget accordingly.
return Obx(() {
// Check if the controller is in a loading state.
// Assuming MyController has a `isLoading` RxBool variable.
if (controller.isLoading.value) {
return const Scaffold(
body: Center(
// Display a CircularProgressIndicator while loading.
child: CircularProgressIndicator(),
),
);
} else {
// If not loading, show the main content.
return Scaffold(
body: Stack(
children: [
//
_buildBackground(),
//
_buildContent(),
],
),
);
}
});
}
///
Widget _buildBackground() {
return Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 250.h,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xFF418CFC), const Color(0x713DBFFC)],
),
),
),
);
}
///
Widget _buildContent() {
return Positioned(
top: 100.h,
left: 20.w,
right: 20.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserInfoCard(),
SizedBox(height: 20.h),
_buildActionButtons(),
],
),
);
}
///
Widget _buildUserInfoCard() {
return Obx(
() => Container(
width: 335.w,
height: 106.h,
padding: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 25.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
//
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.grey[200],
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey.withValues(alpha: 102),
width: 1.w,
),
),
child: Image.network(
controller.userImage.value,
// Show a CircularProgressIndicator while the image is loading
loadingBuilder:
(
BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress,
) {
if (loadingProgress == null) {
return child;
}
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
// Show a placeholder icon if the image fails to load
errorBuilder:
(
BuildContext context,
Object exception,
StackTrace? stackTrace,
) {
return const Icon(
Icons.person,
size: 40,
color: Color(0xFFC8E0FF),
);
},
),
),
SizedBox(width: 15.w),
//
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.userName.value,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
Text(
controller.userPhone.value,
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
],
),
],
),
),
);
}
///
Widget _buildActionButtons() {
return Column(
children: [
_buildActionButton(
label: '修改密码',
onTap: () {
Get.toNamed(AppRoutes.changePassword);
},
),
SizedBox(height: 15.h),
_buildActionButton(
label: '退出登录',
isLogout: true,
onTap: () {
// AuthController 退
controller.logout();
},
),
],
);
}
///
Widget _buildActionButton({
required String label,
required VoidCallback onTap,
bool isLogout = false,
}) {
return InkWell(
onTap: onTap,
child: Container(
width: 335.w,
height: 50.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.r),
border: Border.all(color: const Color(0xFFEEEEEE)),
),
child: Text(
label,
style: TextStyle(
fontSize: 16.sp,
color: isLogout ? const Color(0xFFE50000) : Colors.black,
fontWeight: isLogout ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
}

23
lib/modules/problem/bindings/problem_form_binding.dart

@ -0,0 +1,23 @@
import 'package:get/get.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart';
class ProblemFormBinding extends Bindings {
@override
void dependencies() {
final dynamic arguments = Get.arguments;
final bool readOnly = Get.parameters['isReadOnly'] == 'true';
Problem? problem;
if (arguments != null && arguments is Problem) {
problem = arguments;
}
Get.lazyPut<ProblemFormController>(
() => ProblemFormController(
problemRepository: Get.find(),
problem: problem,
isReadOnly: readOnly,
),
);
}
}

76
lib/modules/problem/components/date_picker_button.dart

@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class DatePickerButton extends StatelessWidget {
DatePickerButton({super.key});
final DatePickerController dateController = Get.put(DatePickerController());
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
),
onPressed: () {
TDPicker.showDatePicker(
context,
title: '选择时间',
onConfirm: (selected) {
dateController.updateDateTime(selected);
Get.back();
},
useHour: true,
useMinute: true,
useSecond: true,
dateStart: [1999, 01, 01],
dateEnd: [2029, 12, 31],
initialDate: [2025, 1, 1],
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() => Text(
dateController.selectedDateTime.value.isEmpty
? "选择日期"
: dateController.selectedDateTime.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
SizedBox(width: 8),
Icon(Icons.keyboard_arrow_down, color: Colors.black),
],
),
),
],
);
}
}
class DatePickerController extends GetxController {
var selectedDateTime = ''.obs;
void updateDateTime(Map<String, int> selected) {
selectedDateTime.value =
'${selected['year'].toString().padLeft(4, '0')}-'
'${selected['month'].toString().padLeft(2, '0')}-'
'${selected['day'].toString().padLeft(2, '0')} ';
// '${selected['hour'].toString().padLeft(2, '0')}:'
// '${selected['minute'].toString().padLeft(2, '0')}:'
// '${selected['second'].toString().padLeft(2, '0')}';
}
}

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

@ -0,0 +1,874 @@
// modules/problem/controllers/problem_controller.dart
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart' hide MultipartFile, FormData, Response;
import 'package:flutter/material.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/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/models/server_problem.dart';
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 {
///
final ProblemRepository problemRepository;
final FileRepository fileRepository = Get.find<FileRepository>();
///
final RxList<Problem> problems = <Problem>[].obs;
///
final RxList<Problem> historyProblems = <Problem>[].obs;
///
final RxList<Problem> unUploadedProblems = <Problem>[].obs;
final Rx<bool> allSelected = false.obs;
final RxDouble uploadProgress = 0.0.obs;
// Dio
late CancelToken _cancelToken;
final RxSet<Problem> _selectedProblems = <Problem>{}.obs;
Set<Problem> get selectedProblems => _selectedProblems;
int get selectedCount => _selectedProblems.length;
///
int get selectedUnUploadCount => _selectedProblems
.where((p) => p.syncStatus != ProblemSyncStatus.synced)
.length;
// ProblemController
//
List<DropdownOption> get dateRangeOptions {
return DateRange.values.map((range) => range.toDropdownOption()).toList();
}
final List<DropdownOption> uploadOptions = const [
DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive),
DropdownOption(label: '已上传', value: '已上传', icon: Icons.cloud_done),
DropdownOption(label: '未上传', value: '未上传', icon: Icons.cloud_off),
];
final List<DropdownOption> bindOptions = const [
DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive),
DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link),
DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off),
];
final Rx<DateRange> currentDateRange = DateRange.oneWeek.obs;
final RxString currentUploadFilter = '全部'.obs;
final RxString currentBindFilter = '全部'.obs;
//
final Rx<DateTime> historyStartTime = DateTime.now()
.subtract(const Duration(days: 365))
.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 historyBindFilter = '全部'.obs;
///
final RxBool isLoading = false.obs;
late TabController tabController;
/// floatingButton
final double _fabSize = 56.0;
final double _edgePaddingX = 27.0.w;
final double _edgePaddingY = 111.0.h;
final fabUploadPosition = Offset(337.0, 703.7).obs;
/// get
RxBool get isOnline => problemRepository.isOnline;
ProblemController({required this.problemRepository});
@override
void onInit() {
super.onInit();
tabController = TabController(length: 2, vsync: this);
tabController.addListener(_onTabChanged);
loadProblems();
//
// loadUnUploadedProblems();
}
@override
void onClose() {
tabController.dispose();
super.onClose();
}
// #region
void updateProblemSelection(Problem problem, bool isChecked) {
if (isChecked) {
_selectedProblems.add(problem);
} else {
_selectedProblems.remove(problem);
}
//
allSelected.value = _selectedProblems.length == unUploadedProblems.length;
}
void selectAll() {
if (allSelected.value) {
//
_selectedProblems.clear();
} else {
//
_selectedProblems.addAll(unUploadedProblems);
}
allSelected.value = !allSelected.value;
}
//
void clearSelection() {
_selectedProblems.clear();
allSelected.value = false;
}
// handleUpload clearSelection
Future<void> handleUpload() async {
if (_selectedProblems.isEmpty) {
Get.snackbar('提示', '请选择要上传的问题');
return;
}
uploadProgress.value = 0.0;
_cancelToken = CancelToken();
showUploadProgressDialog();
try {
await uploadProblems(
_selectedProblems.toList(), //
cancelToken: _cancelToken,
onProgress: (progress) {
uploadProgress.value = progress;
},
);
Get.back();
Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.TOP);
//
clearSelection();
//
loadUnUploadedProblems();
// problems
loadProblems();
} on DioException catch (e) {
Get.back();
if (CancelToken.isCancel(e)) {
Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.TOP);
} else {
Get.snackbar(
'上传失败',
'错误: ${e.message}',
snackPosition: SnackPosition.TOP,
);
}
} catch (e) {
Get.back();
Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.TOP);
}
}
///
void showUploadProgressDialog() {
//
Get.defaultDialog(
title: '上传问题中...',
content: Obx(() {
// final progress = (uploadProgress.value * 100).toInt();
return Column(
// mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
LinearProgressIndicator(
value: uploadProgress.value,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
),
SizedBox(height: 16.h),
// Text('已完成: $progress%'),
Text('已上传: $selectedUnUploadCount / $selectedCount'),
],
);
}),
barrierDismissible: false, //
// "取消"
cancel: ElevatedButton(
onPressed: () {
// controller
cancelUpload();
},
child: Text('取消', style: TextStyle(color: Colors.red)),
),
);
//
// ...
}
void cancelUpload() {
//
// 1. HTTP
// 2. 0
uploadProgress.value = 0.0;
// 3.
Get.back();
// 4.
// Get.snackbar('提示', '上传已取消');
}
///
///
Future<void> uploadProblems(
List<Problem> problems, {
required CancelToken cancelToken,
required void Function(double progress) onProgress,
}) 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);
},
);
if (updatedProblem.syncStatus == ProblemSyncStatus.untracked) {
problemRepository.deleteProblem(updatedProblem.id);
} else {
problemRepository.updateProblem(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.pendingUpload)
.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(
problem.id,
apiPayload!,
cancelToken,
);
break;
case ProblemSyncStatus.pendingDelete:
response = await problemRepository.delete(problem.id, cancelToken);
break;
}
// 4.
if (response.isSuccess) {
if (problem.syncStatus != ProblemSyncStatus.pendingDelete) {
final serverProblem = ServerProblem.fromJson(response.data);
//
final updatedImageMetadata = _updateImageMetadata(
problem.imageUrls,
remoteUrls,
);
// none
return problem.copyWith(
syncStatus: ProblemSyncStatus.synced,
imageUrls: updatedImageMetadata,
lastModifiedTime: serverProblem.lastModificationTime,
);
} else {
//
return problem.copyWith(syncStatus: ProblemSyncStatus.untracked);
}
} 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.pendingUpload) {
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.pendingUpload) {
updatedImageMetadata.add(
ImageMetadata(
localPath: image.localPath,
remoteUrl: newRemoteUrls[uploadedUrlIndex],
status: ImageStatus.synced,
),
);
uploadedUrlIndex++;
} else {
updatedImageMetadata.add(image);
}
}
return updatedImageMetadata;
}
// #endregion
// #region
final SyncProgressState syncProgress = SyncProgressState();
Future<void> 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<ServerProblem> serverProblems = await problemRepository
.fetchProblemsFromServer(pageNumber: 1, pageSize: 99);
// 2.
syncProgress.updateProgress('正在获取本地数据...', 2);
final List<Problem> localProblems = await problemRepository.getProblems();
// 3.
syncProgress.updateProgress('正在同步数据...', 3);
final List<Problem> downloadedProblems = await _syncProblems(
serverProblems,
localProblems,
);
// 4.
_downloadImagesForProblems(downloadedProblems);
// 5.
syncProgress.updateProgress('正在重新加载数据...', 4);
await loadProblems();
syncProgress.completeSync();
//
Get.back(closeOverlays: true);
Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP);
} catch (e) {
syncProgress.errorSync(e.toString());
//
Get.back(closeOverlays: true);
Get.snackbar('同步失败', '错误: $e', snackPosition: SnackPosition.TOP);
} finally {
isLoading.value = false;
}
}
///
void _downloadImagesForProblems(List<Problem> problems) {
if (problems.isEmpty) return;
//
Future(() async {
final imageRepository = Get.find<ImageRepository>(); // 使GetX获取实例
for (final problem in problems) {
try {
final List<ImageMetadata> downloadedImages = [];
for (final imageMeta in problem.imageUrls) {
if (imageMeta.remoteUrl != null &&
imageMeta.remoteUrl!.isNotEmpty) {
//
final bool isDownloaded = await imageRepository.isImageDownloaded(
imageMeta.remoteUrl!,
problem.id,
);
String localPath;
if (isDownloaded) {
//
localPath = (await imageRepository.getLocalImagePath(
imageMeta.remoteUrl!,
problem.id,
))!;
} else {
//
localPath = await imageRepository.downloadImage(
imageMeta.remoteUrl!,
problem.id,
);
}
//
final downloadedImage = imageMeta.copyWith(
localPath: localPath,
status: ImageStatus.synced,
);
downloadedImages.add(downloadedImage);
}
}
//
if (downloadedImages.isNotEmpty) {
final updatedProblem = problem.copyWith(
imageUrls: downloadedImages,
);
await problemRepository.updateProblem(updatedProblem);
}
} catch (e) {
Get.log('下载问题 ${problem.id} 的图片失败: $e');
}
}
});
}
///
Future<List<Problem>> _syncProblems(
List<ServerProblem> serverProblems,
List<Problem> localProblems,
) async {
final List<Problem> needDownloadImages = [];
// 便
final Map<String, ServerProblem> serverProblemsMap = {
for (var problem in serverProblems) problem.id: problem,
};
final Map<String, Problem> localProblemsMap = {
for (var problem in localProblems) problem.id: problem,
};
//
for (final serverProblem in serverProblems) {
if (!localProblemsMap.containsKey(serverProblem.id)) {
//
final newProblem = _convertServerProblemToLocal(serverProblem);
await problemRepository.insertProblem(newProblem);
needDownloadImages.add(newProblem);
}
}
//
for (final localProblem in localProblems) {
if (!serverProblemsMap.containsKey(localProblem.id)) {
//
if (localProblem.syncStatus == ProblemSyncStatus.synced) {
await problemRepository.deleteProblem(localProblem.id);
}
// pendingCreate/pendingUpdate
}
}
//
for (final serverProblem in serverProblems) {
if (localProblemsMap.containsKey(serverProblem.id)) {
final localProblem = localProblemsMap[serverProblem.id]!;
//
if (localProblem.syncStatus == ProblemSyncStatus.synced) {
// 使
final serverUpdated = serverProblem.lastModificationTime;
final localUpdated = localProblem.lastModifiedTime;
if (serverUpdated != null && serverUpdated.isAfter(localUpdated)) {
//
final updatedProblem = _convertServerProblemToLocal(serverProblem);
await problemRepository.updateProblem(updatedProblem);
needDownloadImages.add(updatedProblem);
}
}
//
}
}
return needDownloadImages;
}
///
Problem _convertServerProblemToLocal(ServerProblem serverProblem) {
// URL为ImageMetadata列表
final List<ImageMetadata> imageMetadatas = (serverProblem.imageUrls ?? [])
.map(
(url) => ImageMetadata(
remoteUrl: url,
localPath: '', //
status: ImageStatus.pendingDownload, //
),
)
.toList();
return Problem(
id: serverProblem.id,
description: serverProblem.title,
location: serverProblem.location,
imageUrls: imageMetadatas,
creationTime: serverProblem.creationTime,
lastModifiedTime: serverProblem.lastModificationTime ?? DateTime.now(),
syncStatus: ProblemSyncStatus.synced, //
censorTaskId: serverProblem.censorTaskId,
bindData: serverProblem.bindData,
isChecked: false, //
);
}
// #endregion
// #region
/// floatingButton更新位置
void updateFabUploadPosition(Offset delta) {
final screenWidth = ScreenUtil().screenWidth;
final screenHeight = ScreenUtil().screenHeight;
Offset newPosition = fabUploadPosition.value + delta;
//
double clampedDx = newPosition.dx.clamp(
_edgePaddingX,
screenWidth - _fabSize - _edgePaddingX,
);
//
double clampedDy = newPosition.dy.clamp(
_edgePaddingY,
screenHeight - _fabSize - _edgePaddingY,
);
fabUploadPosition.value = Offset(clampedDx, clampedDy);
}
/// floatingButton
void snapToEdge() {
final screenWidth = ScreenUtil().screenWidth;
//
final buttonCenterDx = fabUploadPosition.value.dx + _fabSize / 2;
double newDx;
//
if (buttonCenterDx < screenWidth / 2) {
// _edgePaddingX
newDx = _edgePaddingX;
} else {
// _edgePaddingX
newDx = screenWidth - _fabSize - _edgePaddingX;
}
//
fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy);
}
// #endregion
// #region ta按钮
void _onTabChanged() {
if (!tabController.indexIsChanging) {
loadProblems();
}
}
// #endregion
//
//
void updateCurrentDateRange(String rangeValue) {
final newRange = rangeValue.toDateRange();
if (newRange != null) {
currentDateRange.value = newRange;
loadProblems(); //
}
}
void updateCurrentUpload(String value) {
currentUploadFilter.value = value;
loadProblems(); //
}
void updateCurrentBind(String value) {
currentBindFilter.value = value;
loadProblems(); //
}
//
///
Future<void> selectDateRange(BuildContext context) async {
final initialDateRange = DateTimeRange(
start: historyStartTime.value,
end: historyEndTime.value,
);
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2025, 8, 1), //
lastDate: DateTime(2101), //
initialDateRange: initialDateRange,
);
if (picked != null) {
//
historyStartTime.value = picked.start;
historyEndTime.value = DateTime(
picked.end.year,
picked.end.month,
picked.end.day,
23,
59,
59,
999,
);
loadProblems();
log('选择的日期范围是: ${picked.start}${picked.end}');
}
}
void updateHistoryUpload(String value) {
historyUploadFilter.value = value;
loadProblems(); //
}
void updateHistoryBind(String value) {
historyBindFilter.value = value;
loadProblems(); //
}
///
Future<void> loadProblems() async {
isLoading.value = true;
try {
// Tab
final bool isProblemListTab = tabController.index == 0;
final DateTime startDate = isProblemListTab
? currentDateRange.value.startDate
: historyStartTime.value;
final DateTime endDate = isProblemListTab
? currentDateRange.value.endDate
: historyEndTime.value;
final String uploadStatus = isProblemListTab
? currentUploadFilter.value
: historyUploadFilter.value;
final String bindStatus = isProblemListTab
? currentBindFilter.value
: historyBindFilter.value;
//
final loadedProblems = await problemRepository.getProblems(
startDate: startDate,
endDate: endDate,
syncStatus: uploadStatus,
bindStatus: bindStatus,
);
// Tab
if (isProblemListTab) {
problems.assignAll(loadedProblems);
} else {
historyProblems.assignAll(loadedProblems);
}
} catch (e) {
Get.snackbar('错误', '加载问题失败: $e');
} finally {
isLoading.value = false;
}
}
///
void showUploadPage() {
Get.toNamed(AppRoutes.problemUpload);
clearSelection();
loadUnUploadedProblems();
}
//
Future<void> loadUnUploadedProblems() async {
isLoading.value = true;
try {
// _localDatabase.getProblems '未上传'
unUploadedProblems.value = await problemRepository.getProblems(
syncStatus: '未上传',
);
} catch (e) {
Get.snackbar('错误', '加载未上传问题失败: $e');
} finally {
isLoading.value = false;
}
}
///
///
Future<void> deleteProblem(Problem problem) async {
try {
final deleteProblem = ProblemStateManager.markForDeletion(problem);
if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) {
//
await problemRepository.deleteProblem(problem.id);
await _deleteProblemImages(problem);
} else {
//
await problemRepository.updateProblem(deleteProblem);
}
loadProblems();
} catch (e) {
Get.snackbar('错误', '删除问题失败: $e');
rethrow;
}
}
//
Future<void> _deleteProblemImages(Problem problem) async {
for (var imagePath in problem.imageUrls) {
try {
final file = File(imagePath.localPath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
throw Exception(e);
}
}
}
Future<void> toProblemFormPageAndRefresh({Problem? problem}) async {
await Get.toNamed(AppRoutes.problemForm, arguments: problem);
loadProblems();
}
}

257
lib/modules/problem/controllers/problem_form_controller.dart

@ -0,0 +1,257 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as path;
import 'package:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.dart';
import 'package:problem_check_system/data/models/image_status.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'dart:io';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
import 'package:problem_check_system/data/repositories/problem_repository.dart';
import 'package:uuid/uuid.dart';
class ProblemFormController extends GetxController {
final Problem? problem;
final bool isReadOnly;
final ProblemRepository problemRepository;
final TextEditingController descriptionController = TextEditingController();
final TextEditingController locationController = TextEditingController();
final RxList<XFile> selectedImages = <XFile>[].obs;
final RxBool isLoading = false.obs;
// 使便
ProblemFormController({
required this.problemRepository,
this.problem,
this.isReadOnly = false,
}) {
if (problem != null) {
descriptionController.text = problem!.description;
locationController.text = problem!.location;
final imagePaths = problem!.imageUrls
.map((meta) => XFile(meta.localPath))
.toList();
selectedImages.assignAll(imagePaths);
} else {
descriptionController.clear();
locationController.clear();
selectedImages.clear();
}
}
// pickImage
Future<void> pickImage(ImageSource source) async {
try {
PermissionStatus status;
if (source == ImageSource.camera) {
status = await Permission.camera.request();
} else {
//
status = await Permission.photos.request();
if (!status.isGranted) {
// Android
status = await Permission.storage.request();
}
}
if (status.isGranted) {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: source,
imageQuality: 85, //
maxWidth: 1920, //
);
if (image != null) {
//
if (selectedImages.length >= 10) {
Get.snackbar('提示', '最多只能选择10张图片');
return;
}
selectedImages.add(image);
}
} else if (status.isPermanentlyDenied) {
_showPermissionPermanentlyDeniedDialog();
} else {
Get.snackbar('权限被拒绝', '需要相册权限才能选择图片');
}
} catch (e) {
Get.snackbar('错误', '选择图片失败: $e');
}
}
//
void _showPermissionPermanentlyDeniedDialog() {
Get.dialog(
AlertDialog(
title: const Text('权限被永久拒绝'),
content: const Text('需要相册权限来选择图片。请前往设置开启。'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
Get.back();
await openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
}
//
void removeImage(int index) {
selectedImages.removeAt(index);
}
//
bool _validateForm() {
if (descriptionController.text.isEmpty) {
Get.snackbar('提示', '请填写问题描述', snackPosition: SnackPosition.TOP);
return false;
}
if (locationController.text.isEmpty) {
Get.snackbar('提示', '请填写问题位置', snackPosition: SnackPosition.TOP);
return false;
}
if (selectedImages.isEmpty) {
Get.snackbar('提示', '请至少上传一张图片', snackPosition: SnackPosition.TOP);
return false;
}
return true;
}
///
Future<void> saveProblem() async {
if (!_validateForm()) {
return;
}
isLoading.value = true;
try {
//
final List<ImageMetadata> imagePaths = await _saveImagesToLocal();
if (problem != null) {
//
final updatedProblem = problem!.copyWith(
description: descriptionController.text,
location: locationController.text,
imageUrls: imagePaths,
);
//
final modifyProblem = ProblemStateManager.modifyProblem(updatedProblem);
await problemRepository.updateProblem(modifyProblem);
} else {
//
final newProblem = ProblemStateManager.createNewProblem(
description: descriptionController.text,
location: locationController.text,
imageUrls: imagePaths,
);
await problemRepository.insertProblem(newProblem);
}
Get.back(result: true); //
Get.snackbar('成功', '问题已更新');
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
} finally {
isLoading.value = false;
}
}
//
Future<List<ImageMetadata>> _saveImagesToLocal() async {
final List<ImageMetadata> imagePaths = [];
final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images');
//
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
for (var image in selectedImages) {
try {
final String fileName = '${Uuid().v4()}_${path.basename(image.name)}';
final String imagePath = '${imagesDir.path}/$fileName';
final ImageMetadata imageData = ImageMetadata(
localPath: imagePath,
status: ImageStatus.pendingUpload,
);
final File imageFile = File(imagePath);
//
final imageBytes = await image.readAsBytes();
await imageFile.writeAsBytes(imageBytes);
imagePaths.add(imageData);
} catch (e) {
throw Exception(e);
}
}
return imagePaths;
}
// /
void cancel() {
//
final hasChanges = _hasFormChanges();
if (hasChanges) {
Get.dialog(
AlertDialog(
title: const Text('放弃编辑'),
content: const Text('您有未保存的更改,确定要放弃吗?'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () {
Get.back();
Get.back(result: false); //
},
child: const Text('确定'),
),
],
),
);
} else {
Get.back(result: false);
}
}
//
bool _hasFormChanges() {
if (problem == null) {
//
return descriptionController.text.isNotEmpty ||
locationController.text.isNotEmpty ||
selectedImages.isNotEmpty;
}
//
return descriptionController.text != problem!.description ||
locationController.text != problem!.location ||
selectedImages.length != problem!.imageUrls.length;
}
@override
void onClose() {
descriptionController.dispose();
locationController.dispose();
super.onClose();
}
}

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

139
lib/modules/problem/problem_card.dart

@ -1,139 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/custom_button.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class ProblemCard extends StatelessWidget {
final bool initialBound;
final bool initialUploaded;
final ProblemCardController controller;
ProblemCard({
super.key,
required this.initialBound,
required this.initialUploaded,
}) : controller = ProblemCardController(
initialBound: initialBound,
initialUploaded: initialUploaded,
);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(vertical: 5.h, horizontal: 9.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.contain, //
),
title: Text(
'问题描述',
style: TextStyle(fontSize: 16.sp), //
),
subtitle: LayoutBuilder(
builder: (context, constraints) {
return Text(
'硫磺库内南侧地面上存放了阀门、消防水带等物品;12#库存放了脱模剂、愈合成催化剂省略字省略字省略字省略字...',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp), //
);
},
),
),
SizedBox(height: 8.h),
Row(
children: [
SizedBox(width: 16.w),
Row(
children: [
Icon(Icons.location_on, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w),
Text(
'汽车机厂房作业区1-C', //
style: TextStyle(fontSize: 12.sp),
),
],
),
SizedBox(width: 16.w),
Row(
children: [
Icon(Icons.access_time, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w),
Text(
'2025-07-31 15:30:29', //
style: TextStyle(fontSize: 12.sp),
),
],
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
SizedBox(width: 16.w),
Obx(
() => Wrap(
spacing: 8,
children: [
controller.isUploaded.value
? TDTag('已上传', isLight: true, theme: TDTagTheme.success)
: TDTag('未上传', isLight: true, theme: TDTagTheme.danger),
controller.isBound.value
? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary)
: TDTag(
'未绑定',
isLight: true,
theme: TDTagTheme.warning,
),
],
),
),
SizedBox(width: 100.w),
Row(
children: [
CustomButton(
text: '修改', //
onTap: () {
//
print('点击修改按钮');
},
),
SizedBox(width: 8.w),
CustomButton(
text: '查看', //
onTap: () {
//
print('点击查看按钮');
},
),
],
),
],
),
],
),
);
}
}
class ProblemCardController extends GetxController {
//
var isBound = false.obs;
//
var isUploaded = false.obs;
//
ProblemCardController({
bool initialBound = false,
bool initialUploaded = false,
}) {
isBound.value = initialBound;
isUploaded.value = initialUploaded;
}
}

43
lib/modules/problem/problem_list_page.dart

@ -1,43 +0,0 @@
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:tdesign_flutter/tdesign_flutter.dart';
// class DatePickerController extends GetxController {
// var selectedDateTime = ''.obs; // Reactive variable for selected date-time
// void updateDateTime(Map<String, int> selected) {
// selectedDateTime.value =
// '${selected['year'].toString().padLeft(4, '0')}-'
// '${selected['month'].toString().padLeft(2, '0')}-'
// '${selected['day'].toString().padLeft(2, '0')} '
// '${selected['hour'].toString().padLeft(2, '0')}:'
// '${selected['minute'].toString().padLeft(2, '0')}:'
// '${selected['second'].toString().padLeft(2, '0')}';
// }
// }
// class DatePickerScreen extends StatelessWidget {
// DatePickerScreen({super.key});
// @override
// Widget build(BuildContext context) {
// return GestureDetector(
// onTap: () {},
// child: Obx(
// () => buildSelectRow(
// context,
// dateController.selectedDateTime.value,
// '选择时间',
// ),
// ),
// );
// }
// Widget buildSelectRow(BuildContext context, String selected, String title) {
// return ListTile(
// title: Text(title),
// subtitle: Text(selected.isNotEmpty ? selected : '未选择'),
// );
// }
// }

145
lib/modules/problem/problem_page.dart

@ -1,145 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:problem_check_system/modules/problem/components/date_picker_button.dart';
import 'package:problem_check_system/modules/problem/problem_card.dart';
class ProblemPage extends StatelessWidget {
ProblemPage({super.key});
final List<Map<String, bool>> problemData = [
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 0,
length: 2,
child: Scaffold(
body: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 812.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 375.w,
height: 83.5.h,
alignment: Alignment.bottomLeft,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft, //
end: Alignment.centerRight, //
colors: [
Color(0xFF418CFC), //
Color(0xFF3DBFFC), //
],
),
),
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
// border: const Border(
// bottom: BorderSide(color: Colors.blue, width: 5.0),
// ),
color: const Color(0xfffff7f7),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(60),
),
),
tabs: const [
Tab(text: '问题列表'),
Tab(text: '历史问题列表'),
],
labelStyle: TextStyle(
fontFamily: 'MyFont', //
fontWeight: FontWeight.w800, //
fontSize: 14.sp, //
),
unselectedLabelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
labelColor: Colors.black, //
unselectedLabelColor: Colors.white, //
),
),
Expanded(
child: TabBarView(
children: [
Column(
children: [
Container(
margin: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [DatePickerButton(), DatePickerButton()],
),
),
Expanded(
child: Stack(
children: [
SingleChildScrollView(
child: Column(
children: [
...problemData.map((data) {
return ProblemCard(
initialBound:
data["initialBound"] ?? false,
initialUploaded:
data["initialUploaded"] ?? false,
);
}),
SizedBox(height: 64.h),
],
),
),
Positioned(
bottom: 5.h,
right: 160.5.w,
child: FloatingActionButton(
onPressed: () {
print('object');
},
shape: CircleBorder(),
backgroundColor: Colors.blue[300],
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
),
],
),
),
],
),
ProblemCard(initialBound: false, initialUploaded: false),
],
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
print('object');
},
foregroundColor: Colors.white,
backgroundColor: Colors.red[300],
child: const Icon(Icons.file_upload_outlined),
),
),
);
}
}

343
lib/modules/problem/views/problem_form_page.dart

@ -0,0 +1,343 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart';
class ProblemFormPage extends GetView<ProblemFormController> {
//
const ProblemFormPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
),
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context);
},
),
//
title: Text(
controller.isReadOnly
? '问题详情'
: (controller.problem == null ? '新增问题' : '编辑问题'),
style: const TextStyle(color: Colors.white),
),
centerTitle: true,
backgroundColor: Colors.transparent,
),
body: Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
_buildInputCard(
title: '问题描述',
textController: controller.descriptionController,
hintText: '请输入问题描述',
),
_buildInputCard(
title: '所在位置',
textController: controller.locationController,
hintText: '请输入问题所在位置',
),
_buildImageCard(context),
],
),
),
),
//
if (!controller.isReadOnly) _bottomButton(),
],
),
);
}
///
Widget _buildInputCard({
required String title,
required TextEditingController textController,
required String hintText,
}) {
return Card(
margin: EdgeInsets.all(17.w),
child: Column(
children: [
ListTile(
title: Text(
title,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: TextField(
maxLines: null,
controller: textController,
readOnly: controller.isReadOnly, //
decoration: InputDecoration(
hintText: hintText,
border: InputBorder.none,
),
),
),
],
),
);
}
///
Widget _buildImageCard(BuildContext context) {
return Card(
margin: EdgeInsets.all(17.w),
child: Column(
children: [
const ListTile(
title: Text(
'问题图片',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: _buildImageGrid(context),
),
],
),
);
}
///
Widget _buildImageGrid(BuildContext context) {
return Obx(() {
//
final bool showAddButton = !controller.isReadOnly;
final int itemCount =
controller.selectedImages.length + (showAddButton ? 1 : 0);
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.h,
childAspectRatio: 1,
),
itemCount: itemCount,
itemBuilder: (context, index) {
if (showAddButton && index == controller.selectedImages.length) {
return _buildAddImageButton(context);
}
return _buildImageItem(index);
},
);
});
}
///
Widget _buildAddImageButton(BuildContext context) {
return InkWell(
onTap: () => _showImageSourceBottomSheet(context),
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300, width: 1),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate,
size: 24.sp,
color: Colors.grey.shade600,
),
SizedBox(height: 4.h),
Text(
'添加图片',
style: TextStyle(color: Colors.grey.shade600, fontSize: 12.sp),
),
],
),
),
);
}
///
Widget _buildImageItem(int index) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(controller.selectedImages[index].path),
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
//
if (!controller.isReadOnly)
Positioned(
top: 0,
right: 0,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container(
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
padding: EdgeInsets.all(4.w),
child: Icon(Icons.close, color: Colors.white, size: 16.sp),
),
),
),
],
),
);
}
///
void _showImageSourceBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)),
),
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ... ( showModalBottomSheet )
SizedBox(height: 16.h),
Text(
'选择图片来源',
style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
),
SizedBox(height: 16.h),
Divider(height: 1.h),
ListTile(
leading: const Icon(Icons.camera_alt, color: Colors.blue),
title: const Text('拍照'),
onTap: () {
Navigator.pop(context);
controller.pickImage(ImageSource.camera);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.photo_library, color: Colors.blue),
title: const Text('从相册选择'),
onTap: () {
Navigator.pop(context);
controller.pickImage(ImageSource.gallery);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.cancel, color: Colors.grey),
title: const Text('取消', style: TextStyle(color: Colors.grey)),
onTap: () => Navigator.pop(context),
),
SizedBox(height: 8.h),
],
),
);
},
);
}
///
Widget _bottomButton() {
return Container(
width: 375.w,
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.r),
topRight: Radius.circular(12.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
padding: EdgeInsets.symmetric(vertical: 12.h),
),
onPressed: () {
Get.back();
},
child: Text(
'取消',
style: TextStyle(color: Colors.grey, fontSize: 16.sp),
),
),
),
SizedBox(width: 10.w),
Expanded(
child: Obx(
() => ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF418CFC),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
padding: EdgeInsets.symmetric(vertical: 12.h),
),
onPressed: controller.isLoading.value
? null
: controller.saveProblem,
child: controller.isLoading.value
? SizedBox(
width: 20.w,
height: 20.h,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'确定',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
),
),
),
),
],
),
);
}
}

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

@ -0,0 +1,181 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:easy_refresh/easy_refresh.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
class ProblemListPage extends GetView<ProblemController> {
final RxList<Problem> problemsToShow;
final ProblemCardViewType viewType;
const ProblemListPage({
super.key,
required this.problemsToShow,
this.viewType = ProblemCardViewType.buttons,
});
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return EasyRefresh(
header: ClassicHeader(
dragText: '下拉刷新'.tr,
armedText: '释放开始'.tr,
readyText: '刷新中...'.tr,
processingText: '刷新中...'.tr,
processedText: '成功了'.tr,
noMoreText: 'No more'.tr,
failedText: '失败'.tr,
messageText: '最后更新于 %T'.tr,
),
onRefresh: () async {
//
await controller.pullDataFromServer();
},
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 17.w),
itemCount: problemsToShow.length,
itemBuilder: (context, index) {
// if (index == problemsToShow.length) {
// return SizedBox(height: 79.5.h);
// }
final problem = problemsToShow[index];
return _buildSwipeableProblemCard(problem);
},
),
);
});
}
Widget _buildSwipeableProblemCard(Problem problem) {
//
final bool isPendingDelete =
problem.syncStatus == ProblemSyncStatus.pendingDelete;
if (viewType == ProblemCardViewType.buttons) {
// buttons
if (!isPendingDelete) {
//
return Dismissible(
key: ValueKey('${problem.id}-${problem.syncStatus}'),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20.w),
child: Icon(Icons.delete, color: Colors.white, size: 30.sp),
),
confirmDismiss: (direction) async {
return await _showDeleteConfirmationDialog(problem);
},
onDismissed: (direction) {
controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除');
},
child: ProblemCard(
key: ValueKey(problem.id),
problem: problem,
viewType: viewType,
isSelected: false,
),
);
} else {
//
return ProblemCard(
key: ValueKey(problem.id),
problem: problem,
viewType: viewType,
isSelected: false,
);
}
} else {
// listgrid等使 Obx
return Obx(() {
final isSelected = controller.selectedProblems.contains(problem);
return ProblemCard(
key: ValueKey(problem.id),
problem: problem,
viewType: viewType,
isSelected: isSelected,
onChanged: (problem, isChecked) {
controller.updateProblemSelection(problem, isChecked);
},
);
});
}
}
Future<bool> _showDeleteConfirmationDialog(Problem problem) async {
// snackbar
if (Get.isSnackbarOpen) {
Get.closeCurrentSnackbar();
}
return await Get.bottomSheet<bool>(
Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
const Text(
'确认删除',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'确定要删除这个问题吗?此操作不可撤销。',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'删除',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => Get.back(result: false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'取消',
style: TextStyle(color: Colors.grey[700], fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
),
) ??
false;
}
}

165
lib/modules/problem/views/problem_page.dart

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; // ProblemListPage
import 'package:problem_check_system/modules/problem/views/widgets/current_filter_bar.dart';
import 'package:problem_check_system/modules/problem/views/widgets/history_filter_bar.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; //
class ProblemPage extends GetView<ProblemController> {
const ProblemPage({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
initialIndex: 0,
length: 2,
child: Scaffold(
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 375.w,
height: 83.5.h,
alignment: Alignment.bottomLeft,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
),
),
child: TabBar(
controller: controller.tabController,
indicatorSize: TabBarIndicatorSize.tab,
indicator: const BoxDecoration(
color: Color(0xfff7f7f7),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(60),
),
),
tabs: const [
Tab(text: '问题列表'),
Tab(text: '历史问题列表'),
],
labelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
unselectedLabelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
labelColor: Colors.black,
unselectedLabelColor: Color(0xfff7f7f7),
),
),
Expanded(
child: TabBarView(
controller: controller.tabController,
children: [
// Tab
DecoratedBox(
decoration: BoxDecoration(color: Color(0xfff7f7f7)),
child: Column(
children: [
CurrentFilterBar(),
Expanded(
child: // 使
ProblemListPage(
problemsToShow: controller.problems,
viewType: ProblemCardViewType.buttons,
),
),
],
),
),
// Tab
DecoratedBox(
decoration: BoxDecoration(color: Color(0xfff7f7f7)),
child: Column(
children: [
HistoryFilterBar(),
Expanded(
child: // 使
ProblemListPage(
problemsToShow: controller.historyProblems,
viewType: ProblemCardViewType.buttons,
),
),
],
),
),
],
),
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
// 使 Stack
floatingActionButton: Stack(
children: [
// "添加"
// 使 Align Positioned
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(bottom: 24.h), //
child: FloatingActionButton(
heroTag: "btn_add",
onPressed: () {
controller.toProblemFormPageAndRefresh();
},
shape: const CircleBorder(),
backgroundColor: Colors.blue[300],
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
),
),
// "上传"
Obx(() {
final isOnline = controller.isOnline.value;
return Positioned(
// 使left/right dxtop/bottom dy
left: controller.fabUploadPosition.value.dx,
top: controller.fabUploadPosition.value.dy,
child: GestureDetector(
onPanUpdate: (details) {
//
controller.updateFabUploadPosition(details.delta);
},
onPanEnd: (details) {
//
controller.snapToEdge();
},
child: FloatingActionButton(
heroTag: "btn_upload",
onPressed: isOnline
? () => controller.showUploadPage()
: null,
foregroundColor: Colors.white,
backgroundColor: isOnline
? Colors.red[300]
: Colors.grey[400],
child: Icon(
isOnline
? Icons.file_upload_outlined
: Icons.cloud_off_outlined,
),
),
),
);
}),
],
),
),
);
}
}

93
lib/modules/problem/views/problem_upload_page.dart

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/modules/problem/views/problem_list_page.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
class ProblemUploadPage extends GetView<ProblemController> {
const ProblemUploadPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(),
body: _buildBody(),
bottomNavigationBar: _buildBottomBar(),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: Obx(() {
final selectedCount = controller.selectedCount;
return Text('已选择$selectedCount项');
}),
centerTitle: true,
// leading: IconButton(
// icon: Icon(Icons.close),
// onPressed: () {
// Get.back();
// controller.loadProblems();
// },
// ),
actions: [
Obx(
() => TextButton(
onPressed: controller.unUploadedProblems.isNotEmpty
? controller.selectAll
: null,
child: Text(
controller.allSelected.value ? '取消全选' : '全选',
style: TextStyle(
color: controller.unUploadedProblems.isNotEmpty
? Colors.blue
: Colors.grey,
),
),
),
),
],
);
}
Widget _buildBody() {
return Obx(() {
if (controller.unUploadedProblems.isEmpty) {
return Center(
child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)),
);
}
return ProblemListPage(
problemsToShow: controller.unUploadedProblems,
viewType: ProblemCardViewType.checkbox,
);
});
}
Widget _buildBottomBar() {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade300)),
),
child: Obx(
() => ElevatedButton(
onPressed: controller.selectedCount > 0
? controller.handleUpload
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
minimumSize: Size(double.infinity, 48.h),
),
child: Text('点击上传 (${controller.selectedCount})'),
),
),
);
}
}

66
lib/modules/problem/views/widgets/current_filter_bar.dart

@ -0,0 +1,66 @@
// widgets/compact_filter_bar.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'custom_filter_dropdown.dart';
class CurrentFilterBar extends GetView<ProblemController> {
final EdgeInsetsGeometry? padding;
const CurrentFilterBar({super.key, this.padding});
@override
Widget build(BuildContext context) {
return Container(
padding: padding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h),
color: Colors.grey[50],
child: Row(
children: [
//
...[
Obx(
() => CustomFilterDropdown(
title: '时间范围',
options: controller.dateRangeOptions,
selectedValue: controller.currentDateRange.value.name,
onChanged: controller.updateCurrentDateRange,
width: 100.w,
showBorder: false,
),
),
],
//
...[
Obx(
() => CustomFilterDropdown(
title: '上传状态',
options: controller.uploadOptions,
selectedValue: controller.currentUploadFilter.value,
onChanged: controller.updateCurrentUpload,
width: 100.w,
showBorder: false,
),
),
],
//
...[
Obx(
() => CustomFilterDropdown(
title: '绑定状态',
options: controller.bindOptions,
selectedValue: controller.currentBindFilter.value,
onChanged: controller.updateCurrentBind,
width: 100.w,
showBorder: false,
),
),
],
],
),
);
}
}

0
lib/modules/problem/custom_button.dart → lib/modules/problem/views/widgets/custom_button.dart

78
lib/modules/problem/views/widgets/custom_filter_dropdown.dart

@ -0,0 +1,78 @@
// widgets/custom_filter_dropdown.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart';
class CustomFilterDropdown extends StatelessWidget {
final String title;
final List<DropdownOption> options;
final String selectedValue;
final ValueChanged<String> onChanged;
final double? width;
final bool showBorder;
const CustomFilterDropdown({
super.key,
required this.title,
required this.options,
required this.selectedValue,
required this.onChanged,
this.width,
this.showBorder = true,
});
@override
Widget build(BuildContext context) {
return Container(
width: width ?? 120.w,
decoration: showBorder
? BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8.r),
)
: null,
// padding: EdgeInsets.only(left: 10.w),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedValue,
isExpanded: true,
icon: Icon(Icons.arrow_drop_down, size: 16.sp, color: Colors.grey),
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
fontWeight: FontWeight.normal,
),
padding: EdgeInsets.symmetric(horizontal: 10.w),
dropdownColor: Colors.white,
borderRadius: BorderRadius.circular(8.r),
items: options.map((DropdownOption option) {
return DropdownMenuItem<String>(
value: option.value,
child: Row(
children: [
if (option.icon != null)
Padding(
padding: EdgeInsets.only(right: 4.w),
child: Icon(option.icon, size: 16.sp, color: Colors.grey),
),
Expanded(
child: Text(
option.label,
style: TextStyle(fontSize: 14.sp),
// overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
onChanged(newValue);
}
},
),
),
);
}
}

97
lib/modules/problem/views/widgets/history_filter_bar.dart

@ -0,0 +1,97 @@
// widgets/compact_filter_bar.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'custom_filter_dropdown.dart';
class HistoryFilterBar extends GetView<ProblemController> {
final EdgeInsetsGeometry? padding;
const HistoryFilterBar({super.key, this.padding});
@override
Widget build(BuildContext context) {
return Container(
padding: padding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 5.h),
color: Colors.grey[50],
child: Row(
children: [
//
...[
SizedBox(
width: 110.w,
// decoration: BoxDecoration(
// border: Border.all(color: Colors.grey.shade300),
// borderRadius: BorderRadius.circular(8.r),
// ),
child: TextButton(
onPressed: () {
controller.selectDateRange(context);
},
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 4.h,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.date_range,
size: 16.sp,
color: Colors.grey[700],
),
SizedBox(width: 4.w),
Text(
'选择日期',
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
fontWeight: FontWeight.normal,
),
),
],
),
),
),
],
//
...[
Obx(
() => CustomFilterDropdown(
title: '上传状态',
options: controller.uploadOptions,
selectedValue: controller.historyUploadFilter.value,
onChanged: controller.updateHistoryUpload,
width: 100.w,
showBorder: false,
),
),
],
//
...[
Obx(
() => CustomFilterDropdown(
title: '绑定状态',
options: controller.bindOptions,
selectedValue: controller.historyBindFilter.value,
onChanged: controller.updateHistoryBind,
width: 100.w,
showBorder: false,
),
),
],
],
),
);
}
}

68
lib/modules/problem/views/widgets/models/date_range_enum.dart

@ -0,0 +1,68 @@
// models/date_range_enum.dart
import 'package:flutter/material.dart';
import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart';
enum DateRange { threeDays, oneWeek, oneMonth }
extension DateRangeExtension on DateRange {
String get displayName {
switch (this) {
case DateRange.threeDays:
return '近三天';
case DateRange.oneWeek:
return '近一周';
case DateRange.oneMonth:
return '近一月';
}
}
DateTime get startDate {
final now = DateTime.now();
switch (this) {
case DateRange.threeDays:
return now.subtract(const Duration(days: 3));
case DateRange.oneWeek:
return now.subtract(const Duration(days: 7));
case DateRange.oneMonth:
return now.subtract(const Duration(days: 30));
}
}
DateTime get endDate {
return DateTime.now();
}
}
// DateRange DropdownOption
extension DateRangeDropdown on DateRange {
DropdownOption toDropdownOption() {
return DropdownOption(
label: displayName,
value: name, // 使
icon: _getIcon(),
);
}
IconData _getIcon() {
switch (this) {
case DateRange.threeDays:
return Icons.calendar_today;
case DateRange.oneWeek:
return Icons.date_range;
case DateRange.oneMonth:
return Icons.calendar_month;
}
}
}
//
extension StringToDateRange on String {
DateRange? toDateRange() {
for (var range in DateRange.values) {
if (range.name == this) {
return range;
}
}
return null;
}
}

23
lib/modules/problem/views/widgets/models/dropdown_option.dart

@ -0,0 +1,23 @@
// models/dropdown_option.dart
import 'package:flutter/material.dart';
class DropdownOption {
final String label;
final String value;
final IconData? icon;
const DropdownOption({required this.label, required this.value, this.icon});
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is DropdownOption &&
runtimeType == other.runtimeType &&
value == other.value;
@override
int get hashCode => value.hashCode;
@override
String toString() => label;
}

247
lib/modules/problem/views/widgets/problem_card.dart

@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'dart:io'; //
//
enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends GetView<ProblemController> {
final Problem problem;
final ProblemCardViewType viewType;
final Function(Problem, bool)? onChanged;
final bool isSelected; //
const ProblemCard({
super.key,
required this.problem,
this.viewType = ProblemCardViewType.buttons,
this.onChanged,
required this.isSelected, //
});
@override
Widget build(BuildContext context) {
//
final bool isDeleted =
problem.syncStatus == ProblemSyncStatus.pendingDelete;
final Color cardColor = isDeleted
? Colors.grey[300]!
: Theme.of(context).cardColor;
final Color contentColor = isDeleted
? Colors.grey[600]!
: Theme.of(context).textTheme.bodyMedium!.color!;
return Card(
color: cardColor,
child: InkWell(
onTap: viewType == ProblemCardViewType.checkbox
? () {
onChanged?.call(problem, !isSelected);
}
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: _buildImageWidget(isDeleted), // 使
title: Text(
'问题描述',
style: TextStyle(fontSize: 16.sp, color: contentColor),
),
subtitle: LayoutBuilder(
builder: (context, constraints) {
return Text(
problem.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp, color: contentColor),
);
},
),
),
SizedBox(height: 8.h),
Row(
children: [
SizedBox(width: 16.w),
Icon(Icons.location_on, color: contentColor, size: 16.h),
SizedBox(width: 8.w),
SizedBox(
width: 100.w,
child: Text(
problem.location,
style: TextStyle(fontSize: 12.sp, color: contentColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
SizedBox(width: 16.w),
Icon(Icons.access_time, color: contentColor, size: 16.h),
SizedBox(width: 8.w),
Expanded(
child: Text(
DateFormat(
'yyyy-MM-dd HH:mm:ss',
).format(problem.creationTime),
style: TextStyle(fontSize: 12.sp, color: contentColor),
),
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
SizedBox(width: 16.w),
Wrap(
spacing: 8,
children: [
...problem.syncStatus == ProblemSyncStatus.pendingDelete
? [
TDTag(
'服务器未删除',
isLight: true,
theme: TDTagTheme.defaultTheme,
textColor: isDeleted ? Colors.grey[700] : null,
// backgroundColor: isDeleted
// ? Colors.grey[400]
// : null,
),
]
: [
problem.syncStatus == ProblemSyncStatus.synced
? TDTag(
'已上传',
isLight: true,
theme: TDTagTheme.success,
)
: TDTag(
'未上传',
isLight: true,
theme: TDTagTheme.danger,
),
problem.bindData != null &&
problem.bindData!.isNotEmpty
? TDTag(
'已绑定',
isLight: true,
theme: TDTagTheme.primary,
)
: TDTag(
'未绑定',
isLight: true,
theme: TDTagTheme.warning,
),
],
],
),
const Spacer(),
_buildBottomActions(isDeleted),
],
),
SizedBox(height: 8.h),
],
),
),
);
}
Widget _buildImageWidget(bool isDeleted) {
return AspectRatio(
aspectRatio: 1, //
child: _buildImageContent(isDeleted),
);
}
Widget _buildImageContent(bool isDeleted) {
//
if (problem.imageUrls.isEmpty || problem.imageUrls[0].localPath.isEmpty) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover, // 使 cover
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
}
final String imagePath = problem.imageUrls[0].localPath;
//
final File imageFile = File(imagePath);
if (!imageFile.existsSync()) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
}
// 使 Image.file
return Image.file(
imageFile,
fit: BoxFit.cover, // 使 cover
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
errorBuilder: (context, error, stackTrace) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
},
);
}
Widget _buildBottomActions(bool isDeleted) {
switch (viewType) {
case ProblemCardViewType.buttons:
return Row(
children: [
if (!isDeleted)
CustomButton(
text: '修改',
onTap: () {
controller.toProblemFormPageAndRefresh(problem: problem);
},
),
if (!isDeleted) SizedBox(width: 8.w),
CustomButton(
text: '查看',
onTap: () {
Get.toNamed(
AppRoutes.problemForm,
arguments: problem,
parameters: {'isReadOnly': 'true'},
);
},
),
SizedBox(width: 16.w),
],
);
case ProblemCardViewType.checkbox:
return Padding(
padding: EdgeInsets.only(right: 16.w),
child: Checkbox(
value: isSelected,
onChanged: (bool? value) {
if (value != null) {
onChanged?.call(problem, value);
}
},
),
);
}
}
}

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

6
macos/Flutter/GeneratedPluginRegistrant.swift

@ -5,8 +5,14 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import connectivity_plus
import file_selector_macos import file_selector_macos
import path_provider_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

595
pubspec.lock

@ -1,6 +1,30 @@
# 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:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -17,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:
@ -25,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:
@ -33,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:
@ -41,6 +129,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
connectivity_plus:
dependency: "direct main"
description:
name: connectivity_plus
sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.5"
connectivity_plus_platform_interface:
dependency: transitive
description:
name: connectivity_plus_platform_interface
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
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: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -49,8 +161,48 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.4+2" version: "0.3.4+2"
easy_refresh: crypto:
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
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 dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
easy_refresh:
dependency: "direct main"
description: description:
name: easy_refresh name: easy_refresh
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c" sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
@ -65,6 +217,22 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
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: file_selector_linux:
dependency: transitive dependency: transitive
description: description:
@ -97,6 +265,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.9.3+4" version: "0.9.3+4"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -110,6 +286,11 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -152,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:
@ -160,6 +365,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.7.2" version: "4.7.2"
get_storage:
dependency: "direct main"
description:
name: get_storage
sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2"
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: http:
dependency: transitive dependency: transitive
description: description:
@ -168,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:
@ -177,7 +414,7 @@ packages:
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image_picker: image_picker:
dependency: transitive dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
@ -240,6 +477,38 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.2.2" version: "0.2.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -272,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:
@ -304,8 +581,24 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
path: nm:
dependency: transitive dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
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: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -328,6 +621,118 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.0.1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -336,11 +741,75 @@ 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:
dependency: "direct main"
description:
name: pretty_dio_logger
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
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: 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:
@ -349,6 +818,54 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.0"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -365,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:
@ -373,6 +898,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.0"
tdesign_flutter: tdesign_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -413,6 +946,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"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -429,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:
@ -437,6 +986,46 @@ 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:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
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: sdks:
dart: ">=3.8.1 <4.0.0" dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

20
pubspec.yaml

@ -7,16 +7,36 @@ environment:
sdk: ^3.8.1 sdk: ^3.8.1
dependencies: dependencies:
connectivity_plus: ^6.1.5
crypto: ^3.0.6
dio: ^5.9.0
easy_refresh: ^3.4.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
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
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
pretty_dio_logger: ^1.4.0
sqflite: ^2.4.2
tdesign_flutter: ^0.2.4 tdesign_flutter: ^0.2.4
uuid: ^4.5.1
dev_dependencies: 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

6
windows/flutter/generated_plugin_registrant.cc

@ -6,9 +6,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h> #include <file_selector_windows/file_selector_windows.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar( FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows")); registry->GetRegistrarForPlugin("FileSelectorWindows"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
} }

2
windows/flutter/generated_plugins.cmake

@ -3,7 +3,9 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows file_selector_windows
permission_handler_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

Loading…
Cancel
Save