Compare commits

..

No commits in common. 'dev' and 'master' have entirely different histories.
dev ... master

  1. 2
      .vscode/settings.json
  2. 25
      README.md
  3. 15
      android/app/src/main/AndroidManifest.xml
  4. 5
      android/gradle.properties
  5. BIN
      assets/images/background.png
  6. 3
      devtools_options.yaml
  7. 49
      lib/app/bindings/initial_binding.dart
  8. 43
      lib/app/routes/app_pages.dart
  9. 14
      lib/app/routes/app_routes.dart
  10. 16
      lib/core/extensions/http_response_extension.dart
  11. 19
      lib/core/utils/constants/api_endpoints.dart
  12. 53
      lib/data/models/auth_model.dart
  13. 47
      lib/data/models/image_metadata_model.dart
  14. 14
      lib/data/models/image_status.dart
  15. 132
      lib/data/models/problem_model.dart
  16. 98
      lib/data/models/problem_sync_status.dart
  17. 88
      lib/data/models/server_problem.dart
  18. 81
      lib/data/models/user/organization.dart
  19. 15
      lib/data/models/user/page.dart
  20. 15
      lib/data/models/user/role.dart
  21. 81
      lib/data/models/user/user.dart
  22. 62
      lib/data/providers/connectivity_provider.dart
  23. 258
      lib/data/providers/http_provider.dart
  24. 271
      lib/data/providers/sqlite_provider.dart
  25. 121
      lib/data/repositories/auth_repository.dart
  26. 167
      lib/data/repositories/file_repository.dart
  27. 8
      lib/data/repositories/image_repository.dart
  28. 202
      lib/data/repositories/image_repository_impl.dart
  29. 128
      lib/data/repositories/problem_repository.dart
  30. 67
      lib/main.dart
  31. 12
      lib/modules/auth/bindings/login_binding.dart
  32. 99
      lib/modules/auth/controllers/login_controller.dart
  33. 189
      lib/modules/auth/views/login_page.dart
  34. 25
      lib/modules/home/bindings/home_binding.dart
  35. 21
      lib/modules/home/controllers/home_controller.dart
  36. 21
      lib/modules/home/home_controller.dart
  37. 17
      lib/modules/home/home_page.dart
  38. 4
      lib/modules/login/models/login_model.dart
  39. 16
      lib/modules/login/view_models/login_controller.dart
  40. 175
      lib/modules/login/views/login_page.dart
  41. 0
      lib/modules/login/views/my_page.dart
  42. 0
      lib/modules/login/views/problem_page.dart
  43. 13
      lib/modules/my/bindings/change_password_binding.dart
  44. 54
      lib/modules/my/controllers/change_password_controller.dart
  45. 54
      lib/modules/my/controllers/my_controller.dart
  46. 155
      lib/modules/my/views/change_password.dart
  47. 222
      lib/modules/my/views/my_page.dart
  48. 23
      lib/modules/problem/bindings/problem_form_binding.dart
  49. 76
      lib/modules/problem/components/date_picker_button.dart
  50. 845
      lib/modules/problem/controllers/problem_controller.dart
  51. 257
      lib/modules/problem/controllers/problem_form_controller.dart
  52. 0
      lib/modules/problem/custom_button.dart
  53. 139
      lib/modules/problem/problem_card.dart
  54. 43
      lib/modules/problem/problem_list_page.dart
  55. 145
      lib/modules/problem/problem_page.dart
  56. 343
      lib/modules/problem/views/problem_form_page.dart
  57. 177
      lib/modules/problem/views/problem_list_page.dart
  58. 165
      lib/modules/problem/views/problem_page.dart
  59. 93
      lib/modules/problem/views/problem_upload_page.dart
  60. 66
      lib/modules/problem/views/widgets/current_filter_bar.dart
  61. 78
      lib/modules/problem/views/widgets/custom_filter_dropdown.dart
  62. 97
      lib/modules/problem/views/widgets/history_filter_bar.dart
  63. 68
      lib/modules/problem/views/widgets/models/date_range_enum.dart
  64. 23
      lib/modules/problem/views/widgets/models/dropdown_option.dart
  65. 247
      lib/modules/problem/views/widgets/problem_card.dart
  66. 6
      macos/Flutter/GeneratedPluginRegistrant.swift
  67. 307
      pubspec.lock
  68. 15
      pubspec.yaml
  69. 6
      windows/flutter/generated_plugin_registrant.cc
  70. 2
      windows/flutter/generated_plugins.cmake

2
.vscode/settings.json vendored

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

25
README.md

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

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

@ -1,19 +1,6 @@
<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
android:label="现场问题检查"
android:label="problem_check_system"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

5
android/gradle.properties

@ -1,8 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=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: 85 KiB

After

Width:  |  Height:  |  Size: 90 KiB

3
devtools_options.yaml

@ -1,3 +0,0 @@
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

@ -1,49 +0,0 @@
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

@ -1,43 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,16 +0,0 @@
// 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

@ -1,19 +0,0 @@
// 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

@ -1,53 +0,0 @@
///
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

@ -1,47 +0,0 @@
// 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

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

132
lib/data/models/problem_model.dart

@ -1,132 +0,0 @@
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

@ -1,98 +0,0 @@
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(),
);
}
}

88
lib/data/models/server_problem.dart

@ -1,88 +0,0 @@
import 'package:flutter/material.dart';
@immutable
class ServerProblem {
final String id;
final String title;
final String location;
final String? censorTaskId;
final String? rowId;
final String? bindData;
final List<String>? imageUrls;
final DateTime creationTime;
final String creatorId;
final DateTime lastModificationTime;
final String lastModifierId;
const ServerProblem({
required this.id,
required this.title,
required this.location,
this.censorTaskId,
this.rowId,
this.bindData,
this.imageUrls,
required this.creationTime,
required this.creatorId,
required this.lastModificationTime,
required this.lastModifierId,
});
factory ServerProblem.fromJson(Map<String, dynamic> json) => ServerProblem(
id: json['id'] as String,
title: json['title'] as String,
location: json['location'] as String,
censorTaskId: json['censorTaskId'] as String?,
rowId: json['rowId'] as String?,
bindData: json['bindData'] as String?,
imageUrls: json['imageUrls'] as List<String>?,
creationTime: DateTime.parse(json['creationTime'] as String),
creatorId: json['creatorId'] as String,
lastModificationTime: DateTime.parse(
json['lastModificationTime'] as String,
),
lastModifierId: json['lastModifierId'] as String,
);
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'location': location,
'censorTaskId': censorTaskId,
'rowId': rowId,
'bindData': bindData,
'imageUrls': imageUrls,
'creationTime': creationTime.toUtc().toIso8601String(),
'creatorId': creatorId,
'lastModificationTime': lastModificationTime.toUtc().toIso8601String(),
'lastModifierId': lastModifierId,
};
ServerProblem copyWith({
String? id,
String? title,
String? location,
String? censorTaskId,
String? rowId,
String? bindData,
List<String>? imageUrls,
DateTime? creationTime,
String? creatorId,
DateTime? lastModificationTime,
String? lastModifierId,
}) {
return ServerProblem(
id: id ?? this.id,
title: title ?? this.title,
location: location ?? this.location,
censorTaskId: censorTaskId ?? this.censorTaskId,
rowId: rowId ?? this.rowId,
bindData: bindData ?? this.bindData,
imageUrls: imageUrls ?? this.imageUrls,
creationTime: creationTime ?? this.creationTime,
creatorId: creatorId ?? this.creatorId,
lastModificationTime: lastModificationTime ?? this.lastModificationTime,
lastModifierId: lastModifierId ?? this.lastModifierId,
);
}
}

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

@ -1,81 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,81 +0,0 @@
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

@ -1,62 +0,0 @@
// 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

@ -1,258 +0,0 @@
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

@ -1,271 +0,0 @@
// 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

@ -1,121 +0,0 @@
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;
}
}

167
lib/data/repositories/file_repository.dart

@ -1,167 +0,0 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; // kDebugMode debugPrint
import 'package:get/get.dart' hide FormData, MultipartFile;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:problem_check_system/core/extensions/http_response_extension.dart';
import 'package:problem_check_system/core/utils/constants/api_endpoints.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'package:problem_check_system/data/models/image_status.dart';
import 'package:problem_check_system/data/providers/http_provider.dart';
class FileRepository {
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 = data['fileName'];
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');
}
}
//
Future<ImageMetadata> downloadImage(
String imageUrl, {
CancelToken? cancelToken,
void Function(int received, int total)? onReceiveProgress,
}) async {
final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images');
//
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
//
final String fileName =
'downloaded_${DateTime.now().millisecondsSinceEpoch}_${imageUrl.hashCode}${_getFileExtension(imageUrl)}';
final String imagePath = '${imagesDir.path}/$fileName';
try {
//
await _httpProvider.download(
imageUrl,
imagePath,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
//
return ImageMetadata(
localPath: imagePath,
remoteUrl: imageUrl,
status: ImageStatus.synced,
);
} catch (e) {
//
final file = File(imagePath);
if (await file.exists()) {
await file.delete();
}
rethrow;
}
}
//
Future<List<ImageMetadata>> downloadImages(
List<String> imageUrls, {
CancelToken? cancelToken,
void Function(int current, int total)? onProgress,
}) async {
final List<ImageMetadata> results = [];
int downloadedCount = 0;
for (final imageUrl in imageUrls) {
if (cancelToken?.isCancelled == true) {
break;
}
try {
final metadata = await downloadImage(
imageUrl,
cancelToken: cancelToken,
onReceiveProgress: (received, total) {
//
},
);
results.add(metadata);
//
downloadedCount++;
onProgress?.call(downloadedCount, imageUrls.length);
} catch (e) {
Get.log('Failed to download image $imageUrl: $e');
//
}
}
return results;
}
//
String _getFileExtension(String url) {
try {
final uri = Uri.parse(url);
final pathSegments = uri.pathSegments;
if (pathSegments.isNotEmpty) {
final fileName = pathSegments.last;
final dotIndex = fileName.lastIndexOf('.');
if (dotIndex != -1 && dotIndex < fileName.length - 1) {
return fileName.substring(dotIndex);
}
}
return '.jpg';
} catch (e) {
return '.jpg';
}
}
}

8
lib/data/repositories/image_repository.dart

@ -1,8 +0,0 @@
// 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

@ -1,202 +0,0 @@
// 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 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();
}
}

128
lib/data/repositories/problem_repository.dart

@ -1,128 +0,0 @@
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) {
// Problem对象的列表
final List<dynamic> data = response.data;
return data.map((json) => ServerProblem.fromJson(json)).toList();
} else {
throw Exception('拉取问题失败: ${response.statusCode}');
}
} on DioException catch (e) {
throw Exception('拉取问题失败: $e');
}
}
/// 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,26 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:get_storage/get_storage.dart';
import 'package:problem_check_system/app/routes/app_pages.dart';
import 'package:problem_check_system/app/routes/app_routes.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();
import 'package:get/get_navigation/src/routes/get_route.dart';
import 'package:get/get_navigation/src/routes/transitions_type.dart';
import 'package:problem_check_system/modules/home/home_page.dart';
import 'package:problem_check_system/modules/login/views/login_page.dart';
void main() {
runApp(const MainApp());
}
@ -29,35 +15,38 @@ class MainApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 稿, dp
//稿,dp
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, _) {
// 使 _ child
builder: (context, child) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
// --- ---
localizationsDelegates: const [
// Material
GlobalMaterialLocalizations.delegate,
// Widgets
GlobalWidgetsLocalizations.delegate,
// iOS
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'), //
title: 'First Method',
// You can use the library anywhere in the app even in theme
theme: ThemeData(
useMaterial3: true,
primarySwatch: Colors.blue,
// textTheme: Typography.englishLike2018.apply(fontSizeFactor: 1.sp),
),
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => LoginPage(),
transition: Transition.cupertino,
),
GetPage(
name: '/home',
page: () => HomePage(),
transition: Transition.cupertino,
),
],
title: '问题检查系统', // 使
theme: ThemeData(useMaterial3: true, primarySwatch: Colors.blue),
// 使 GetX
initialRoute: AppRoutes.login, // 使
initialBinding: InitialBinding(), //
getPages: AppPages.routes, //
home: child,
);
},
child: LoginPage(),
);
}
}

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

@ -1,12 +0,0 @@
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

@ -1,99 +0,0 @@
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

@ -1,189 +0,0 @@
// 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

@ -1,25 +0,0 @@
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

@ -1,21 +0,0 @@
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

@ -0,0 +1,21 @@
// 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/views/home_page.dart → lib/modules/home/home_page.dart

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

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

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

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

@ -0,0 +1,16 @@
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

@ -0,0 +1,175 @@
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

@ -1,13 +0,0 @@
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

@ -1,54 +0,0 @@
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

@ -1,54 +0,0 @@
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

@ -1,155 +0,0 @@
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

@ -1,222 +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/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

@ -1,23 +0,0 @@
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

@ -0,0 +1,76 @@
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')}';
}
}

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

@ -1,845 +0,0 @@
// 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/views/widgets/models/date_range_enum.dart';
import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.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: 7))
.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) {
final problem = Problem.fromJson(response.data);
//
final updatedImageMetadata =
problem.syncStatus != ProblemSyncStatus.pendingDelete
? _updateImageMetadata(problem.imageUrls, remoteUrls)
: problem.imageUrls;
Get.log(problem.lastModifiedTime.toUtc().toIso8601String());
// none
return problem.copyWith(
syncStatus: problem.syncStatus != ProblemSyncStatus.pendingDelete
? ProblemSyncStatus.synced
: ProblemSyncStatus.untracked, // none
imageUrls: updatedImageMetadata,
);
} else {
throw Exception('操作失败,状态码: ${response.statusCode}');
}
} on DioException {
rethrow;
}
}
/// URL列表
List<String> _buildFinalRemoteUrls(
List<ImageMetadata> images,
List<String> newRemoteUrls,
) {
final List<String> finalRemoteUrls = [];
int newImageIndex = 0;
for (var image in images) {
if (image.status == ImageStatus.synced) {
finalRemoteUrls.add(image.remoteUrl!);
} else if (image.status == ImageStatus.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
// TODO
Future<void> pullDataFromServer() async {
isLoading.value = true;
try {
// 1.
final List<ServerProblem> serverProblems = await problemRepository
.fetchProblemsFromServer();
// 2.
final List<Problem> localProblems = await problemRepository.getProblems();
// 3.
final List<Problem> downloadedProblems = await _syncProblems(
serverProblems,
localProblems,
);
// 4.
_downloadImagesForProblems(downloadedProblems);
// 5.
await loadProblems();
Get.snackbar('成功', '数据同步完成', snackPosition: SnackPosition.TOP);
} catch (e) {
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.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,
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

@ -1,257 +0,0 @@
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';
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 =
'problem_${DateTime.now().millisecondsSinceEpoch}_${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();
}
}

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

139
lib/modules/problem/problem_card.dart

@ -0,0 +1,139 @@
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

@ -0,0 +1,43 @@
// 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

@ -0,0 +1,145 @@
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

@ -1,343 +0,0 @@
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),
),
),
),
),
],
),
);
}
}

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

@ -1,177 +0,0 @@
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 {
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

@ -1,165 +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/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

@ -1,93 +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/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

@ -1,66 +0,0 @@
// 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,
),
),
],
],
),
);
}
}

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

@ -1,78 +0,0 @@
// 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

@ -1,97 +0,0 @@
// 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

@ -1,68 +0,0 @@
// 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

@ -1,23 +0,0 @@
// 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

@ -1,247 +0,0 @@
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);
}
},
),
);
}
}
}

6
macos/Flutter/GeneratedPluginRegistrant.swift

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

307
pubspec.lock

@ -1,14 +1,6 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@ -49,22 +41,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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"
cross_file:
dependency: transitive
description:
@ -73,40 +49,8 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4+2"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.6"
dbus:
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"
dependency: transitive
description:
name: easy_refresh
sha256: "486e30abfcaae66c0f2c2798a10de2298eb9dc5e0bb7e1dba9328308968cae0c"
@ -121,14 +65,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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_selector_linux:
dependency: transitive
description:
@ -161,14 +97,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct main"
description: flutter
@ -182,11 +110,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -237,14 +160,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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"
http:
dependency: transitive
description:
@ -262,7 +177,7 @@ packages:
source: hosted
version: "4.1.2"
image_picker:
dependency: "direct main"
dependency: transitive
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
@ -325,14 +240,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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"
leak_tracker:
dependency: transitive
description:
@ -397,16 +304,8 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.0"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.0"
path:
dependency: "direct main"
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@ -429,118 +328,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -549,14 +336,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
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"
sky_engine:
dependency: transitive
description: flutter
@ -570,54 +349,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -642,14 +373,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: "direct main"
description:
@ -690,14 +413,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
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:
dependency: transitive
description:
@ -722,22 +437,6 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
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"
sdks:
dart: ">=3.8.1 <4.0.0"
flutter: ">=3.29.0"

15
pubspec.yaml

@ -7,26 +7,11 @@ environment:
sdk: ^3.8.1
dependencies:
connectivity_plus: ^6.1.5
crypto: ^3.0.6
dio: ^5.9.0
easy_refresh: ^3.4.0
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_screenutil: ^5.9.3
get: ^4.7.2
get_storage: ^2.1.1
image_picker: ^1.1.2
intl: ^0.20.2
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
uuid: ^4.5.1
dev_dependencies:
flutter_test:

6
windows/flutter/generated_plugin_registrant.cc

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

2
windows/flutter/generated_plugins.cmake

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

Loading…
Cancel
Save