Compare commits
No commits in common. 'dev' and 'master' have entirely different histories.
70 changed files with 663 additions and 5734 deletions
@ -1,3 +1,3 @@
|
||||
{ |
||||
"cSpell.words": ["fenix", "Getx", "tdesign"] |
||||
"cSpell.words": ["Getx", "tdesign"] |
||||
} |
||||
|
@ -1,26 +1,3 @@
|
||||
# problem_check_system |
||||
|
||||
系统架构为MVVM + 仓库模式 |
||||
|
||||
这个应用需要实现以下功能: |
||||
|
||||
离线登录系统 |
||||
|
||||
问题数据收集(描述、位置、图片等) |
||||
|
||||
本地数据存储 |
||||
|
||||
有网络时手动上传功能 |
||||
|
||||
技术栈 |
||||
Flutter SDK |
||||
|
||||
GetX (状态管理、路由管理、依赖注入) |
||||
|
||||
SQFlite (本地数据库) |
||||
|
||||
Image Picker (图片选择) |
||||
|
||||
Geolocator (位置信息) |
||||
|
||||
HTTP/Dio (网络请求) |
||||
A new Flutter project. |
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 90 KiB |
@ -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: |
@ -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>(), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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(), |
||||
), |
||||
]; |
||||
} |
@ -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 |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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'; |
||||
} |
@ -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'] ?? '', |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
} |
@ -1,14 +0,0 @@
|
||||
/// 图片的同步状态 |
||||
enum ImageStatus { |
||||
/// 已同步 |
||||
synced, |
||||
|
||||
// 待上传 |
||||
pendingUpload, |
||||
|
||||
/// 待删除 |
||||
pendingDeleted, |
||||
|
||||
/// 待下载 |
||||
pendingDownload, |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
|
||||
/// 将对象转换为Map,用于SQLite存储 |
||||
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)); |
||||
} |
@ -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(), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
}; |
||||
} |
@ -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}; |
||||
} |
@ -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}; |
||||
} |
@ -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, |
||||
}; |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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'; |
||||
} |
||||
} |
||||
} |
@ -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)}); |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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>()), |
||||
); |
||||
} |
||||
} |
@ -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('登录失败', '无网络连接,且无法验证本地凭证'); |
||||
} |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
); |
||||
}), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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>()), |
||||
); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -0,0 +1,4 @@
|
||||
class LoginModel { |
||||
String username = ""; |
||||
String password = ""; |
||||
} |
@ -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}'); |
||||
} |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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>()), |
||||
); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
||||
// 未来可以添加更新用户信息的逻辑 |
||||
} |
@ -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), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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')}'; |
||||
} |
||||
} |
@ -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 payload(删除操作不需要完整payload) |
||||
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(); |
||||
} |
||||
} |
@ -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,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; |
||||
} |
||||
} |
@ -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 : '未选择'), |
||||
// ); |
||||
// } |
||||
// } |
@ -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), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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), |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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 { |
||||
// 其他视图类型(list、grid等):使用 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; |
||||
} |
||||
} |
@ -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 对应 dx,top/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, |
||||
), |
||||
), |
||||
), |
||||
); |
||||
}), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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})'), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
], |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
}, |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
], |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
@ -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); |
||||
} |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue