40 changed files with 1766 additions and 783 deletions
@ -0,0 +1,3 @@ |
|||||||
|
description: This file stores settings for Dart & Flutter DevTools. |
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states |
||||||
|
extensions: |
@ -0,0 +1,62 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:flutter/foundation.dart'; // 导入 debugPrint |
||||||
|
import 'package:get_storage/get_storage.dart'; |
||||||
|
import 'package:problem_check_system/data/providers/local_database.dart'; |
||||||
|
|
||||||
|
class InitialBinding implements Bindings { |
||||||
|
@override |
||||||
|
void dependencies() { |
||||||
|
// 全局注册 GetStorage 实例 - 也改为使用 put 确保立即创建 |
||||||
|
Get.put<GetStorage>(GetStorage(), permanent: true); |
||||||
|
|
||||||
|
// 全局注册 Dio 实例 - 使用 put 而不是 lazyPut,并设置为永久 |
||||||
|
Get.put<Dio>(createDioInstance()); |
||||||
|
// |
||||||
|
Get.put<LocalDatabase>(LocalDatabase()); |
||||||
|
} |
||||||
|
|
||||||
|
// 提取创建 Dio 实例的逻辑到单独的方法 |
||||||
|
Dio createDioInstance() { |
||||||
|
final dio = Dio( |
||||||
|
BaseOptions( |
||||||
|
baseUrl: 'https://xhdev.anxincloud.cn', // 替换为你的服务器地址 |
||||||
|
connectTimeout: const Duration(seconds: 15), |
||||||
|
receiveTimeout: const Duration(seconds: 15), |
||||||
|
), |
||||||
|
); |
||||||
|
|
||||||
|
// 添加日志拦截器,仅在调试模式下打印日志 |
||||||
|
if (kDebugMode) { |
||||||
|
dio.interceptors.add( |
||||||
|
LogInterceptor( |
||||||
|
requestBody: true, |
||||||
|
responseBody: true, |
||||||
|
logPrint: (o) => debugPrint(o.toString()), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 添加认证拦截器 |
||||||
|
dio.interceptors.add( |
||||||
|
InterceptorsWrapper( |
||||||
|
onRequest: (options, handler) { |
||||||
|
final token = ""; //todo 获取token |
||||||
|
if (token != null && token.isNotEmpty) { |
||||||
|
options.headers['Authorization'] = 'Bearer $token'; |
||||||
|
} |
||||||
|
return handler.next(options); |
||||||
|
}, |
||||||
|
onError: (DioException e, handler) { |
||||||
|
if (e.response?.statusCode == 401) { |
||||||
|
// 如果收到 401 Unauthorized 错误,跳转回登录页 |
||||||
|
Get.offAllNamed('/login'); |
||||||
|
} |
||||||
|
return handler.next(e); |
||||||
|
}, |
||||||
|
), |
||||||
|
); |
||||||
|
|
||||||
|
return dio; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
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/auth_binding.dart'; |
||||||
|
import 'package:problem_check_system/modules/auth/views/login_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: AuthBinding(), |
||||||
|
), |
||||||
|
// GetPage( |
||||||
|
// name: AppRoutes.my, |
||||||
|
// page: () => const MyPage(), |
||||||
|
// binding: null, |
||||||
|
// ), |
||||||
|
// // 添加 Problem 模块的路由和绑定 |
||||||
|
// GetPage( |
||||||
|
// name: AppRoutes.problem, // 确保在 app_routes.dart 中定义了此常量 |
||||||
|
// page: () => const ProblemPage(), |
||||||
|
// binding: null, |
||||||
|
// ), |
||||||
|
]; |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
abstract class AppRoutes { |
||||||
|
// 命名路由,使用 const 常量 |
||||||
|
static const home = '/home'; |
||||||
|
static const login = '/login'; |
||||||
|
static const problem = '/problem'; |
||||||
|
static const my = '/my'; |
||||||
|
} |
@ -1,130 +0,0 @@ |
|||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:image_picker/image_picker.dart'; |
|
||||||
import '../controllers/problem_controller.dart'; |
|
||||||
import 'package:permission_handler/permission_handler.dart'; |
|
||||||
|
|
||||||
class AddProblemController extends GetxController { |
|
||||||
final ProblemController _problemController = Get.put(ProblemController()); |
|
||||||
|
|
||||||
final RxList<XFile> _selectedImages = <XFile>[].obs; |
|
||||||
final TextEditingController descriptionController = TextEditingController(); |
|
||||||
final TextEditingController locationController = TextEditingController(); |
|
||||||
final RxBool isLoading = false.obs; |
|
||||||
|
|
||||||
List<XFile> get selectedImages => _selectedImages; |
|
||||||
|
|
||||||
// 改进的 pickImage 方法 |
|
||||||
Future<void> pickImage(ImageSource source) async { |
|
||||||
try { |
|
||||||
PermissionStatus status; |
|
||||||
String permissionName; |
|
||||||
String actionName; |
|
||||||
|
|
||||||
if (source == ImageSource.camera) { |
|
||||||
permissionName = "相机"; |
|
||||||
actionName = "拍照"; |
|
||||||
status = await Permission.camera.request(); |
|
||||||
} else { |
|
||||||
permissionName = "相册"; |
|
||||||
actionName = "选择图片"; |
|
||||||
|
|
||||||
// 处理不同 Android 版本的相册权限 |
|
||||||
if (await Permission.photos.isGranted) { |
|
||||||
status = PermissionStatus.granted; |
|
||||||
} else if (await Permission.storage.isGranted) { |
|
||||||
status = PermissionStatus.granted; |
|
||||||
} else { |
|
||||||
// 请求适当的权限 |
|
||||||
status = await Permission.photos.request(); |
|
||||||
|
|
||||||
// 如果 photos 权限被拒绝,尝试请求 storage 权限 |
|
||||||
if (status.isDenied || status.isPermanentlyDenied) { |
|
||||||
status = await Permission.storage.request(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (status.isGranted) { |
|
||||||
final ImagePicker picker = ImagePicker(); |
|
||||||
final XFile? image = await picker.pickImage(source: source); |
|
||||||
|
|
||||||
if (image != null) { |
|
||||||
_selectedImages.add(image); |
|
||||||
} |
|
||||||
} else if (status.isPermanentlyDenied) { |
|
||||||
// 权限被永久拒绝,引导用户到设置 |
|
||||||
_showPermissionDeniedDialog(permissionName, actionName); |
|
||||||
} else { |
|
||||||
Get.snackbar('权限被拒绝', '需要$permissionName权限才能$actionName'); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
Get.snackbar('错误', '选择图片失败: $e'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 显示权限被拒绝的对话框 |
|
||||||
void _showPermissionDeniedDialog(String permissionName, String actionName) { |
|
||||||
Get.dialog( |
|
||||||
AlertDialog( |
|
||||||
title: Text('权限被拒绝'), |
|
||||||
content: Text('需要$permissionName权限才能$actionName。是否要前往设置开启权限?'), |
|
||||||
actions: [ |
|
||||||
TextButton(onPressed: () => Get.back(), child: Text('取消')), |
|
||||||
TextButton( |
|
||||||
onPressed: () async { |
|
||||||
Get.back(); |
|
||||||
await openAppSettings(); |
|
||||||
}, |
|
||||||
child: Text('去设置'), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> removeImage(int index) async { |
|
||||||
_selectedImages.removeAt(index); |
|
||||||
} |
|
||||||
|
|
||||||
bool get isFormValid { |
|
||||||
return descriptionController.text.isNotEmpty && |
|
||||||
locationController.text.isNotEmpty; |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> saveProblem() async { |
|
||||||
if (descriptionController.text.isEmpty) { |
|
||||||
Get.snackbar('错误', '请填写问题描述'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
isLoading.value = true; |
|
||||||
|
|
||||||
try { |
|
||||||
await _problemController.addProblem( |
|
||||||
descriptionController.text, |
|
||||||
locationController.text, |
|
||||||
_selectedImages.toList(), |
|
||||||
); |
|
||||||
|
|
||||||
// 清空表单 |
|
||||||
descriptionController.clear(); |
|
||||||
locationController.clear(); |
|
||||||
_selectedImages.clear(); |
|
||||||
|
|
||||||
Get.back(); |
|
||||||
Get.snackbar('成功', '问题已保存'); |
|
||||||
} catch (e) { |
|
||||||
Get.snackbar('错误', '保存问题失败: $e'); |
|
||||||
} finally { |
|
||||||
isLoading.value = false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
void onClose() { |
|
||||||
descriptionController.dispose(); |
|
||||||
locationController.dispose(); |
|
||||||
super.onClose(); |
|
||||||
} |
|
||||||
} |
|
@ -1,73 +0,0 @@ |
|||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:shared_preferences/shared_preferences.dart'; |
|
||||||
import 'package:dio/dio.dart'; |
|
||||||
import '../services/local_database.dart'; |
|
||||||
|
|
||||||
class AuthController extends GetxController { |
|
||||||
final LocalDatabase _localDatabase = LocalDatabase(); |
|
||||||
final RxBool isLoggedIn = false.obs; |
|
||||||
final RxBool isOnlineMode = false.obs; |
|
||||||
final RxString username = ''.obs; |
|
||||||
|
|
||||||
@override |
|
||||||
void onInit() { |
|
||||||
super.onInit(); |
|
||||||
checkLoginStatus(); |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> checkLoginStatus() async { |
|
||||||
final prefs = await SharedPreferences.getInstance(); |
|
||||||
final offlineLogin = prefs.getBool('offlineLogin') ?? false; |
|
||||||
isLoggedIn.value = offlineLogin; |
|
||||||
|
|
||||||
if (offlineLogin) { |
|
||||||
username.value = prefs.getString('username') ?? '离线用户'; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<bool> offlineLogin(String username) async { |
|
||||||
final prefs = await SharedPreferences.getInstance(); |
|
||||||
await prefs.setBool('offlineLogin', true); |
|
||||||
await prefs.setString('username', username); |
|
||||||
|
|
||||||
isLoggedIn.value = true; |
|
||||||
this.username.value = username; |
|
||||||
isOnlineMode.value = false; |
|
||||||
|
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
Future<bool> onlineLogin(String username, String password) async { |
|
||||||
try { |
|
||||||
final dio = Dio(); |
|
||||||
final response = await dio.post( |
|
||||||
'https://your-server.com/api/login', |
|
||||||
data: {'username': username, 'password': password}, |
|
||||||
); |
|
||||||
|
|
||||||
if (response.statusCode == 200) { |
|
||||||
final prefs = await SharedPreferences.getInstance(); |
|
||||||
await prefs.setBool('offlineLogin', false); |
|
||||||
await prefs.setString('username', username); |
|
||||||
|
|
||||||
isLoggedIn.value = true; |
|
||||||
this.username.value = username; |
|
||||||
isOnlineMode.value = true; |
|
||||||
|
|
||||||
return true; |
|
||||||
} |
|
||||||
return false; |
|
||||||
} catch (e) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> logout() async { |
|
||||||
final prefs = await SharedPreferences.getInstance(); |
|
||||||
await prefs.remove('offlineLogin'); |
|
||||||
await prefs.remove('username'); |
|
||||||
|
|
||||||
isLoggedIn.value = false; |
|
||||||
username.value = ''; |
|
||||||
} |
|
||||||
} |
|
@ -1,115 +0,0 @@ |
|||||||
import 'package:dio/dio.dart'; |
|
||||||
import 'package:get/get.dart' hide MultipartFile, FormData; |
|
||||||
import 'package:image_picker/image_picker.dart'; |
|
||||||
import 'package:path_provider/path_provider.dart'; |
|
||||||
import 'dart:io'; |
|
||||||
import '../models/problem_model.dart'; |
|
||||||
import '../services/local_database.dart'; |
|
||||||
|
|
||||||
class ProblemController extends GetxController { |
|
||||||
final LocalDatabase _localDatabase = LocalDatabase(); |
|
||||||
final RxList<Problem> problems = <Problem>[].obs; |
|
||||||
final RxBool isLoading = false.obs; |
|
||||||
|
|
||||||
// 这个方法返回所有已选中的问题对象 |
|
||||||
List<Problem> get selectedProblems { |
|
||||||
return problems.where((p) => p.isChecked.value).toList(); |
|
||||||
} |
|
||||||
|
|
||||||
@override |
|
||||||
void onInit() { |
|
||||||
super.onInit(); |
|
||||||
loadProblems(); |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> loadProblems() async { |
|
||||||
isLoading.value = true; |
|
||||||
problems.value = await _localDatabase.getProblems(); |
|
||||||
isLoading.value = false; |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> addProblem( |
|
||||||
String description, |
|
||||||
String location, |
|
||||||
List<XFile> images, |
|
||||||
) async { |
|
||||||
try { |
|
||||||
// 保存图片到本地 |
|
||||||
final List<String> imagePaths = []; |
|
||||||
final directory = await getApplicationDocumentsDirectory(); |
|
||||||
|
|
||||||
for (var i = 0; i < images.length; i++) { |
|
||||||
final String imagePath = |
|
||||||
'${directory.path}/problem_${DateTime.now().millisecondsSinceEpoch}_$i.jpg'; |
|
||||||
final File imageFile = File(imagePath); |
|
||||||
await imageFile.writeAsBytes(await images[i].readAsBytes()); |
|
||||||
imagePaths.add(imagePath); |
|
||||||
} |
|
||||||
|
|
||||||
final Problem problem = Problem( |
|
||||||
description: description, |
|
||||||
location: location, |
|
||||||
imagePaths: imagePaths, |
|
||||||
createdAt: DateTime.now(), |
|
||||||
isUploaded: false, |
|
||||||
); |
|
||||||
|
|
||||||
await _localDatabase.insertProblem(problem); |
|
||||||
problems.add(problem); |
|
||||||
} catch (e) { |
|
||||||
Get.snackbar('错误', '保存问题失败: $e'); |
|
||||||
rethrow; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> uploadProblem(Problem problem) async { |
|
||||||
try { |
|
||||||
final dio = Dio(); |
|
||||||
|
|
||||||
// 准备表单数据 |
|
||||||
final formData = FormData.fromMap({ |
|
||||||
'description': problem.description, |
|
||||||
'location': problem.location, |
|
||||||
'createdAt': problem.createdAt.toIso8601String(), |
|
||||||
'boundInfo': problem.boundInfo ?? '', |
|
||||||
}); |
|
||||||
|
|
||||||
// 添加图片文件 |
|
||||||
for (var path in problem.imagePaths) { |
|
||||||
formData.files.add( |
|
||||||
MapEntry('images', await MultipartFile.fromFile(path)), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
final response = await dio.post( |
|
||||||
'https://your-server.com/api/problems', |
|
||||||
data: formData, |
|
||||||
); |
|
||||||
|
|
||||||
if (response.statusCode == 200) { |
|
||||||
// 更新上传状态 |
|
||||||
problem.isUploaded = true; |
|
||||||
await _localDatabase.updateProblem(problem); |
|
||||||
problems.refresh(); |
|
||||||
|
|
||||||
Get.snackbar('成功', '问题已上传到服务器'); |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
Get.snackbar('错误', '上传问题失败: $e'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> uploadAllUnuploaded() async { |
|
||||||
final unuploaded = await _localDatabase.getUnuploadedProblems(); |
|
||||||
for (var problem in unuploaded) { |
|
||||||
await uploadProblem(problem); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Future<void> bindInfoToProblem(int problemId, String info) async { |
|
||||||
final problem = problems.firstWhere((p) => p.id == problemId); |
|
||||||
problem.boundInfo = info; |
|
||||||
await _localDatabase.updateProblem(problem); |
|
||||||
problems.refresh(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,12 @@ |
|||||||
|
class LoginModel { |
||||||
|
final String username; |
||||||
|
final String password; |
||||||
|
|
||||||
|
// 使用 const 构造函数,并要求所有字段在创建时必须被提供 |
||||||
|
const LoginModel({required this.username, required this.password}); |
||||||
|
|
||||||
|
// (可选)提供一个 toMap 方法,方便将对象转换为 JSON 或 Map |
||||||
|
Map<String, dynamic> toMap() { |
||||||
|
return {'username': username, 'password': password}; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:problem_check_system/data/models/login_model.dart'; |
||||||
|
|
||||||
|
class AuthProvider { |
||||||
|
final String _signInUrl = '/api/Accounts/SignIn'; |
||||||
|
|
||||||
|
final Dio _dio; |
||||||
|
|
||||||
|
AuthProvider({required Dio dio}) : _dio = dio; |
||||||
|
|
||||||
|
Future<Response> signIn(LoginModel loginModel) async { |
||||||
|
try { |
||||||
|
final response = await _dio.post(_signInUrl, data: loginModel.toMap()); |
||||||
|
return response; |
||||||
|
} on DioException { |
||||||
|
rethrow; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:problem_check_system/modules/auth/controllers/auth_controller.dart'; |
||||||
|
import 'package:problem_check_system/data/providers/auth_provider.dart'; |
||||||
|
|
||||||
|
class AuthBinding implements Bindings { |
||||||
|
@override |
||||||
|
void dependencies() { |
||||||
|
// 1. 注入数据提供者 (AuthProvider),它依赖于 Dio |
||||||
|
// 依赖注入通过构造函数完成,使代码更易于测试 |
||||||
|
Get.lazyPut<AuthProvider>(() => AuthProvider(dio: Get.find<Dio>())); |
||||||
|
|
||||||
|
// 2. 注入控制器 (AuthController),它依赖于 AuthProvider |
||||||
|
// 控制器通过 Get.find() 获取已注入的依赖 |
||||||
|
Get.lazyPut<AuthController>( |
||||||
|
() => AuthController(authProvider: Get.find<AuthProvider>()), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:get_storage/get_storage.dart'; |
||||||
|
|
||||||
|
// 导入数据提供者和数据模型 |
||||||
|
import 'package:problem_check_system/data/providers/auth_provider.dart'; |
||||||
|
import 'package:problem_check_system/data/models/login_model.dart'; |
||||||
|
import 'package:problem_check_system/app/routes/app_routes.dart'; |
||||||
|
|
||||||
|
class AuthController extends GetxController { |
||||||
|
final AuthProvider _authProvider; |
||||||
|
AuthController({required AuthProvider authProvider}) |
||||||
|
: _authProvider = authProvider; |
||||||
|
|
||||||
|
final username = ''.obs; |
||||||
|
final password = ''.obs; |
||||||
|
final isLoading = false.obs; |
||||||
|
final rememberPassword = false.obs; |
||||||
|
|
||||||
|
final _box = GetStorage(); |
||||||
|
static const _usernameKey = 'username'; |
||||||
|
static const _passwordKey = 'password'; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
_loadSavedCredentials(); |
||||||
|
} |
||||||
|
|
||||||
|
void _loadSavedCredentials() { |
||||||
|
final savedUsername = _box.read(_usernameKey); |
||||||
|
final savedPassword = _box.read(_passwordKey); |
||||||
|
|
||||||
|
if (savedUsername != null && savedPassword != null) { |
||||||
|
username.value = savedUsername; |
||||||
|
password.value = savedPassword; |
||||||
|
rememberPassword.value = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
String getToken() { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
void updateUsername(String value) { |
||||||
|
username.value = value; |
||||||
|
} |
||||||
|
|
||||||
|
void updatePassword(String value) { |
||||||
|
password.value = value; |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> login() async { |
||||||
|
if (username.value.isEmpty || password.value.isEmpty) { |
||||||
|
Get.snackbar('输入错误', '用户名和密码不能为空'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isLoading.value = true; |
||||||
|
|
||||||
|
try { |
||||||
|
final loginData = LoginModel( |
||||||
|
username: username.value, |
||||||
|
password: password.value, |
||||||
|
); |
||||||
|
|
||||||
|
final response = await _authProvider.signIn(loginData); |
||||||
|
|
||||||
|
if (response.statusCode == 200 && response.data['token'] != null) { |
||||||
|
final token = response.data['token']; |
||||||
|
final refreshToken = response.data['refreshToken']; |
||||||
|
|
||||||
|
_box.write('token', token); |
||||||
|
_box.write('refreshToken', refreshToken); |
||||||
|
|
||||||
|
if (rememberPassword.value) { |
||||||
|
_box.write(_usernameKey, username.value); |
||||||
|
_box.write(_passwordKey, password.value); |
||||||
|
} else { |
||||||
|
_box.remove(_usernameKey); |
||||||
|
_box.remove(_passwordKey); |
||||||
|
} |
||||||
|
|
||||||
|
Get.offAllNamed(AppRoutes.home); |
||||||
|
} else { |
||||||
|
Get.snackbar('登录失败', '服务器返回了异常数据'); |
||||||
|
} |
||||||
|
} on DioException catch (e) { |
||||||
|
if (e.response?.data != null && e.response?.data['detail'] != null) { |
||||||
|
final serverMessage = e.response!.data['detail']; |
||||||
|
Get.snackbar('登录失败', serverMessage); |
||||||
|
} else { |
||||||
|
Get.snackbar('网络错误', '登录失败,请检查您的网络连接'); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '发生未知错误: ${e.toString()}'); |
||||||
|
} finally { |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> logout() async { |
||||||
|
_box.remove('token'); |
||||||
|
_box.remove('refreshToken'); |
||||||
|
if (rememberPassword.value == false) { |
||||||
|
_box.remove(_usernameKey); |
||||||
|
_box.remove(_passwordKey); |
||||||
|
} |
||||||
|
Get.offAllNamed(AppRoutes.login); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,219 @@ |
|||||||
|
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/auth_controller.dart'; |
||||||
|
|
||||||
|
class LoginPage extends StatelessWidget { |
||||||
|
const LoginPage({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
final AuthController controller = Get.find<AuthController>(); |
||||||
|
|
||||||
|
// 在 build 方法中创建并初始化 TextEditingController |
||||||
|
final TextEditingController usernameController = TextEditingController( |
||||||
|
text: controller.username.value, |
||||||
|
); |
||||||
|
final TextEditingController passwordController = TextEditingController( |
||||||
|
text: controller.password.value, |
||||||
|
); |
||||||
|
|
||||||
|
// 监听 AuthController 的响应式变量,以防它们在其他地方被修改 |
||||||
|
// 并更新到 TextEditingController。 |
||||||
|
// 这里使用 once() 或 ever() 都可以,但为了代码简洁, |
||||||
|
// 我们主要关注初始同步。用户输入后,值会通过 onChanged 更新。 |
||||||
|
usernameController.text = controller.username.value; |
||||||
|
passwordController.text = controller.password.value; |
||||||
|
|
||||||
|
return Scaffold( |
||||||
|
resizeToAvoidBottomInset: false, |
||||||
|
body: Stack( |
||||||
|
children: [ |
||||||
|
_buildBackground(), |
||||||
|
_buildLoginCard(controller, usernameController, passwordController), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildBackground() { |
||||||
|
// ... 此部分代码不变 ... |
||||||
|
return Stack( |
||||||
|
children: [ |
||||||
|
Container( |
||||||
|
decoration: const 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, |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 修改 _buildLoginCard 方法签名,传入控制器 |
||||||
|
Widget _buildLoginCard( |
||||||
|
AuthController controller, |
||||||
|
TextEditingController usernameController, |
||||||
|
TextEditingController passwordController, |
||||||
|
) { |
||||||
|
return Positioned( |
||||||
|
left: 20.5.w, |
||||||
|
top: 220.5.h, |
||||||
|
child: Container( |
||||||
|
width: 334.w, |
||||||
|
height: 574.5.h, |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: const Color(0xFFFFFFFF).withOpacity(0.6), // 使用 withOpacity 更清晰 |
||||||
|
borderRadius: BorderRadius.all(Radius.circular(23.5.r)), |
||||||
|
), |
||||||
|
padding: EdgeInsets.all(24.w), |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
const SizedBox(height: 16), |
||||||
|
// 使用新版 _buildTextFieldSection |
||||||
|
_buildTextFieldSection( |
||||||
|
label: '账号', |
||||||
|
hintText: '请输入您的账号', |
||||||
|
controller: usernameController, |
||||||
|
onChanged: controller.updateUsername, // onChanged 保留,用于实时同步 |
||||||
|
), |
||||||
|
const SizedBox(height: 22), |
||||||
|
_buildTextFieldSection( |
||||||
|
label: '密码', |
||||||
|
hintText: '请输入您的密码', |
||||||
|
obscureText: true, |
||||||
|
controller: passwordController, |
||||||
|
onChanged: controller.updatePassword, |
||||||
|
), |
||||||
|
const SizedBox(height: 9.5), |
||||||
|
_buildRememberPasswordRow(controller), |
||||||
|
const SizedBox(height: 138.5), |
||||||
|
_buildLoginButton(controller), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 修改 _buildTextFieldSection 以接受 TextEditingController |
||||||
|
Widget _buildTextFieldSection({ |
||||||
|
required String label, |
||||||
|
required String hintText, |
||||||
|
required TextEditingController controller, |
||||||
|
bool obscureText = false, |
||||||
|
required Function(String) onChanged, |
||||||
|
}) { |
||||||
|
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, // 使用传入的控制器 |
||||||
|
onChanged: onChanged, |
||||||
|
obscureText: obscureText, |
||||||
|
style: const TextStyle(color: Colors.black), |
||||||
|
decoration: InputDecoration( |
||||||
|
hintText: hintText, |
||||||
|
hintStyle: const TextStyle(color: Colors.grey), |
||||||
|
border: const OutlineInputBorder(), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildRememberPasswordRow(AuthController controller) { |
||||||
|
// ... 此部分代码不变 ... |
||||||
|
return 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), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
Widget _buildLoginButton(AuthController controller) { |
||||||
|
// ... 此部分代码不变 ... |
||||||
|
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, |
||||||
|
), |
||||||
|
); |
||||||
|
}), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:problem_check_system/data/providers/auth_provider.dart'; |
||||||
|
import 'package:problem_check_system/data/providers/local_database.dart'; |
||||||
|
import 'package:problem_check_system/modules/auth/controllers/auth_controller.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() { |
||||||
|
final Dio dio = Get.find<Dio>(); |
||||||
|
final LocalDatabase database = Get.find<LocalDatabase>(); |
||||||
|
// 惰性注入 HomeController,只在第一次被调用时创建 |
||||||
|
Get.lazyPut<HomeController>(() => HomeController()); |
||||||
|
Get.lazyPut<ProblemController>( |
||||||
|
() => ProblemController(localDatabase: database, dio: dio), |
||||||
|
); |
||||||
|
Get.lazyPut<MyController>(() => MyController()); |
||||||
|
Get.lazyPut<AuthProvider>(() => AuthProvider(dio: dio)); |
||||||
|
Get.lazyPut<AuthController>( |
||||||
|
() => AuthController(authProvider: Get.find<AuthProvider>()), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:problem_check_system/modules/my/views/my_page.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/views/problem_page.dart'; |
||||||
|
|
||||||
|
class HomeController extends GetxController { |
||||||
|
// 使用 Rx 类型管理响应式状态 |
||||||
|
var selectedIndex = 0.obs; |
||||||
|
|
||||||
|
// 页面列表 |
||||||
|
final List<Widget> pages = [ |
||||||
|
const Center(child: Text('首页内容')), |
||||||
|
const ProblemPage(), // 使用 const 确保子页面不会频繁重建 |
||||||
|
const MyPage(), |
||||||
|
]; |
||||||
|
|
||||||
|
// 改变选中索引,这个方法将由 NavigationBar 调用 |
||||||
|
void changeIndex(int index) { |
||||||
|
selectedIndex.value = index; |
||||||
|
} |
||||||
|
} |
@ -1,21 +0,0 @@ |
|||||||
// 控制器(Logic 层) |
|
||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:problem_check_system/views/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; |
|
||||||
} |
|
||||||
} |
|
@ -1,4 +0,0 @@ |
|||||||
class LoginModel { |
|
||||||
String username = ""; |
|
||||||
String password = ""; |
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
import 'package:get/get.dart'; |
|
||||||
|
|
||||||
class LoginController extends GetxController { |
|
||||||
var username = ''.obs; |
|
||||||
var password = ''.obs; |
|
||||||
var rememberPassword = false.obs; |
|
||||||
|
|
||||||
void login() { |
|
||||||
if (username.isEmpty || password.isEmpty) { |
|
||||||
Get.snackbar('错误', '请输入完整的信息'); |
|
||||||
} else { |
|
||||||
// 在这里执行登录逻辑,例如 API 调用 |
|
||||||
print('用户名: ${username.value}, 密码: ${password.value}'); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,175 +0,0 @@ |
|||||||
import 'package:flutter/material.dart'; |
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
||||||
import 'package:get/get.dart'; |
|
||||||
import 'package:problem_check_system/modules/login/view_models/login_controller.dart'; |
|
||||||
|
|
||||||
class LoginPage extends StatelessWidget { |
|
||||||
final LoginController controller = Get.put(LoginController()); |
|
||||||
|
|
||||||
LoginPage({super.key}); |
|
||||||
|
|
||||||
@override |
|
||||||
Widget build(BuildContext context) { |
|
||||||
return Scaffold( |
|
||||||
resizeToAvoidBottomInset: false, |
|
||||||
body: Stack( |
|
||||||
children: [ |
|
||||||
// 背景设置 |
|
||||||
Container( |
|
||||||
decoration: BoxDecoration( |
|
||||||
image: DecorationImage( |
|
||||||
image: AssetImage('assets/images/background.png'), |
|
||||||
fit: BoxFit.fitWidth, |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
// 绝对定位的图片 |
|
||||||
Positioned( |
|
||||||
left: 28.5.w, |
|
||||||
top: 89.5.h, |
|
||||||
child: Image.asset( |
|
||||||
'assets/images/label.png', |
|
||||||
width: 171.5.w, |
|
||||||
height: 23.5.h, |
|
||||||
fit: BoxFit.fitWidth, |
|
||||||
), |
|
||||||
), |
|
||||||
Positioned( |
|
||||||
left: 28.5.w, |
|
||||||
top: 128.5.h, |
|
||||||
child: Image.asset( |
|
||||||
'assets/images/label1.png', |
|
||||||
width: 296.5.w, |
|
||||||
height: 35.5.h, |
|
||||||
fit: BoxFit.fitWidth, |
|
||||||
), |
|
||||||
), |
|
||||||
Positioned( |
|
||||||
left: 20.5.w, // 左侧距离20.5dp |
|
||||||
top: 220.5.h, // 顶部距离220.5dp |
|
||||||
child: Container( |
|
||||||
width: 334.w, // 宽度334dp |
|
||||||
height: 574.5.h, // 高度574.5dp |
|
||||||
decoration: BoxDecoration( |
|
||||||
color: const Color( |
|
||||||
0xFFFFFFFF, |
|
||||||
).withValues(alpha: 153), // 白色60%透明度 |
|
||||||
borderRadius: BorderRadius.all( |
|
||||||
Radius.circular(23.5.r), // 四个角都是23.5dp圆角 |
|
||||||
), |
|
||||||
), |
|
||||||
padding: EdgeInsets.all(24.w), |
|
||||||
child: Column( |
|
||||||
crossAxisAlignment: CrossAxisAlignment.start, |
|
||||||
children: [ |
|
||||||
const SizedBox(height: 16), |
|
||||||
Text( |
|
||||||
'账号', |
|
||||||
style: TextStyle(fontSize: 16.5.sp, color: Colors.black), |
|
||||||
), |
|
||||||
const SizedBox(height: 10.5), |
|
||||||
TextField( |
|
||||||
style: TextStyle(color: Colors.black), |
|
||||||
decoration: InputDecoration( |
|
||||||
// labelText: '账号', |
|
||||||
hintText: '请输入您的账号', |
|
||||||
hintStyle: TextStyle(color: Colors.grey), |
|
||||||
border: OutlineInputBorder(), |
|
||||||
), |
|
||||||
), |
|
||||||
const SizedBox(height: 22), |
|
||||||
Text( |
|
||||||
'密码', |
|
||||||
style: TextStyle(fontSize: 16.5.sp, color: Colors.black), |
|
||||||
), |
|
||||||
const SizedBox(height: 10.5), |
|
||||||
TextField( |
|
||||||
obscureText: true, |
|
||||||
style: TextStyle(color: Colors.black), |
|
||||||
decoration: InputDecoration( |
|
||||||
// labelText: '密码', |
|
||||||
hintText: '请输入您的密码', |
|
||||||
hintStyle: TextStyle(color: Colors.grey), |
|
||||||
border: OutlineInputBorder(), |
|
||||||
), |
|
||||||
), |
|
||||||
const SizedBox(height: 9.5), |
|
||||||
Row( |
|
||||||
mainAxisAlignment: MainAxisAlignment.end, |
|
||||||
children: [ |
|
||||||
Obx( |
|
||||||
() => Checkbox( |
|
||||||
value: controller.rememberPassword.value, |
|
||||||
onChanged: (value) => |
|
||||||
controller.rememberPassword.value = value!, |
|
||||||
), |
|
||||||
), |
|
||||||
Text( |
|
||||||
'记住密码', |
|
||||||
style: TextStyle( |
|
||||||
color: const Color(0xFF959595), // 颜色值 |
|
||||||
fontSize: 14.sp, // 响应式字体大小 |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
const SizedBox(height: 138.5), |
|
||||||
SizedBox( |
|
||||||
width: double.infinity, |
|
||||||
child: ElevatedButton( |
|
||||||
onPressed: () { |
|
||||||
// 跳转到 HomePage |
|
||||||
Get.toNamed('/home'); |
|
||||||
// Navigator.push( |
|
||||||
// context, |
|
||||||
// MaterialPageRoute( |
|
||||||
// builder: (context) => const HomePage(), |
|
||||||
// ), |
|
||||||
// ); |
|
||||||
}, |
|
||||||
style: ElevatedButton.styleFrom( |
|
||||||
padding: EdgeInsets.zero, |
|
||||||
backgroundColor: Colors.transparent, |
|
||||||
shadowColor: Colors.transparent, |
|
||||||
shape: RoundedRectangleBorder( |
|
||||||
borderRadius: BorderRadius.circular(8.r), // 响应式圆角 |
|
||||||
), |
|
||||||
minimumSize: Size(double.infinity, 48.h), // 响应式高度 |
|
||||||
), |
|
||||||
child: Ink( |
|
||||||
decoration: BoxDecoration( |
|
||||||
gradient: LinearGradient( |
|
||||||
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], |
|
||||||
begin: Alignment.centerLeft, |
|
||||||
end: Alignment.centerRight, |
|
||||||
), |
|
||||||
borderRadius: BorderRadius.circular(8.r), |
|
||||||
), |
|
||||||
child: Container( |
|
||||||
constraints: BoxConstraints(minHeight: 48.h), |
|
||||||
alignment: Alignment.center, |
|
||||||
padding: EdgeInsets.symmetric( |
|
||||||
vertical: 12.h, |
|
||||||
horizontal: 24.w, |
|
||||||
), |
|
||||||
child: Text( |
|
||||||
'登录', |
|
||||||
style: TextStyle( |
|
||||||
color: Colors.white, |
|
||||||
fontSize: 16.sp, |
|
||||||
fontWeight: FontWeight.w500, |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
), |
|
||||||
), |
|
||||||
], |
|
||||||
), |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,9 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:problem_check_system/modules/my/controllers/my_controller.dart'; |
||||||
|
|
||||||
|
class MyBinding implements Bindings { |
||||||
|
@override |
||||||
|
void dependencies() { |
||||||
|
Get.lazyPut<MyController>(() => MyController()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:get_storage/get_storage.dart'; |
||||||
|
|
||||||
|
class MyController extends GetxController { |
||||||
|
// 响应式变量,用于存储用户信息 |
||||||
|
var userName = '张兰雪'.obs; |
||||||
|
var userPhone = '138****8547'.obs; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
_loadUserInfo(); |
||||||
|
} |
||||||
|
|
||||||
|
// 从本地存储或API加载用户信息 |
||||||
|
void _loadUserInfo() { |
||||||
|
// 假设你从 GetStorage 中读取用户信息 |
||||||
|
final box = GetStorage(); |
||||||
|
final storedUserName = box.read('userName'); |
||||||
|
final storedUserPhone = box.read('userPhone'); |
||||||
|
|
||||||
|
if (storedUserName != null) { |
||||||
|
userName.value = storedUserName; |
||||||
|
} |
||||||
|
if (storedUserPhone != null) { |
||||||
|
// 可以在此处对手机号进行格式化处理 |
||||||
|
userPhone.value = storedUserPhone; |
||||||
|
} |
||||||
|
|
||||||
|
// 或者,你也可以在这里调用 API 来获取用户信息 |
||||||
|
} |
||||||
|
|
||||||
|
// 未来可以添加更新用户信息的逻辑 |
||||||
|
} |
@ -0,0 +1,184 @@ |
|||||||
|
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/modules/auth/controllers/auth_controller.dart'; |
||||||
|
|
||||||
|
class MyPage extends StatelessWidget { |
||||||
|
const MyPage({super.key}); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
// 获取 MyController 实例 |
||||||
|
final MyController controller = Get.find<MyController>(); |
||||||
|
// 获取 AuthController 实例,用于处理退出登录 |
||||||
|
final AuthController authController = Get.find<AuthController>(); |
||||||
|
|
||||||
|
return Scaffold( |
||||||
|
body: Stack( |
||||||
|
children: [ |
||||||
|
// 顶部背景区域 |
||||||
|
_buildBackground(), |
||||||
|
// 内容区域 |
||||||
|
_buildContent(controller, authController), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/// 顶部背景和用户信息部分 |
||||||
|
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(0xFFE4F0FF), |
||||||
|
const Color(0xFFF1F7FF).withOpacity(0.0), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/// 页面核心内容 |
||||||
|
Widget _buildContent(MyController controller, AuthController authController) { |
||||||
|
return Positioned( |
||||||
|
top: 100.h, |
||||||
|
left: 20.w, |
||||||
|
right: 20.w, |
||||||
|
child: Column( |
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
_buildUserInfoCard(controller), |
||||||
|
SizedBox(height: 20.h), |
||||||
|
_buildActionButtons(authController), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/// 用户信息卡片 |
||||||
|
Widget _buildUserInfoCard(MyController controller) { |
||||||
|
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.withOpacity(0.1), |
||||||
|
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.withOpacity(0.4), |
||||||
|
width: 1.w, |
||||||
|
), |
||||||
|
), |
||||||
|
child: 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(AuthController authController) { |
||||||
|
return Column( |
||||||
|
children: [ |
||||||
|
_buildActionButton( |
||||||
|
label: '修改密码', |
||||||
|
onTap: () { |
||||||
|
// TODO: 跳转到修改密码页面 |
||||||
|
// Get.toNamed(AppRoutes.changePassword); |
||||||
|
}, |
||||||
|
), |
||||||
|
SizedBox(height: 15.h), |
||||||
|
_buildActionButton( |
||||||
|
label: '退出登录', |
||||||
|
isLogout: true, |
||||||
|
onTap: () { |
||||||
|
// 调用 AuthController 的退出登录方法 |
||||||
|
authController.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, |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; |
||||||
|
import 'package:problem_check_system/data/providers/local_database.dart'; |
||||||
|
|
||||||
|
class ProblemBinding implements Bindings { |
||||||
|
@override |
||||||
|
void dependencies() { |
||||||
|
// 2. 注入 ProblemController |
||||||
|
// 控制器通过构造函数接收它所需要的依赖 |
||||||
|
// Get.find() 会查找并返回已注入的实例 |
||||||
|
Get.lazyPut<ProblemController>( |
||||||
|
() => ProblemController( |
||||||
|
localDatabase: Get.find<LocalDatabase>(), |
||||||
|
dio: Get.find<Dio>(), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,235 @@ |
|||||||
|
import 'package:dio/dio.dart'; |
||||||
|
import 'package:get/get.dart' hide MultipartFile, FormData; |
||||||
|
import 'dart:io'; |
||||||
|
import 'package:path/path.dart' as path; |
||||||
|
import '../../../data/models/problem_model.dart'; |
||||||
|
import '../../../data/providers/local_database.dart'; |
||||||
|
|
||||||
|
class ProblemController extends GetxController { |
||||||
|
final LocalDatabase _localDatabase; |
||||||
|
final RxList<Problem> problems = <Problem>[].obs; |
||||||
|
final RxBool isLoading = false.obs; |
||||||
|
final Dio _dio; |
||||||
|
|
||||||
|
// 依赖注入,由 bindings 传入所有依赖 |
||||||
|
ProblemController({required LocalDatabase localDatabase, required Dio dio}) |
||||||
|
: _localDatabase = localDatabase, |
||||||
|
_dio = dio; |
||||||
|
|
||||||
|
// 获取所有已选中的问题对象 |
||||||
|
List<Problem> get selectedProblems { |
||||||
|
return problems.where((p) => p.isChecked.value).toList(); |
||||||
|
} |
||||||
|
|
||||||
|
// 获取未上传的问题 |
||||||
|
List<Problem> get unuploadedProblems { |
||||||
|
return problems.where((p) => !p.isUploaded).toList(); |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
loadProblems(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> loadProblems() async { |
||||||
|
isLoading.value = true; |
||||||
|
try { |
||||||
|
problems.value = await _localDatabase.getProblems(); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '加载问题失败: $e'); |
||||||
|
rethrow; |
||||||
|
} finally { |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 在本地数据库和GetX列表中添加一个新问题 |
||||||
|
Future<void> addProblem(Problem problem) async { |
||||||
|
try { |
||||||
|
// 确保问题有ID |
||||||
|
if (problem.id == null) { |
||||||
|
problem = problem.copyWith( |
||||||
|
id: DateTime.now().millisecondsSinceEpoch.toString(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
await _localDatabase.insertProblem(problem); |
||||||
|
problems.add(problem); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '保存问题失败: $e'); |
||||||
|
rethrow; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 在本地数据库和GetX列表中更新一个现有问题 |
||||||
|
Future<void> updateProblem(Problem problem) async { |
||||||
|
try { |
||||||
|
await _localDatabase.updateProblem(problem); |
||||||
|
final index = problems.indexWhere((p) => p.id == problem.id); |
||||||
|
if (index != -1) { |
||||||
|
problems[index] = problem; |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '更新问题失败: $e'); |
||||||
|
rethrow; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 删除单个问题 |
||||||
|
Future<void> deleteProblem(Problem problem) async { |
||||||
|
try { |
||||||
|
if (problem.id != null) { |
||||||
|
await _localDatabase.deleteProblem(problem.id!); |
||||||
|
|
||||||
|
// 从本地列表中移除 |
||||||
|
problems.remove(problem); |
||||||
|
|
||||||
|
// 删除关联的图片文件 |
||||||
|
await _deleteProblemImages(problem); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '删除问题失败: $e'); |
||||||
|
rethrow; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 批量删除选中的问题 |
||||||
|
Future<void> deleteSelectedProblems() async { |
||||||
|
final problemsToDelete = selectedProblems; |
||||||
|
if (problemsToDelete.isEmpty) { |
||||||
|
Get.snackbar('提示', '请至少选择一个问题进行删除'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
for (var problem in problemsToDelete) { |
||||||
|
await deleteProblem(problem); |
||||||
|
} |
||||||
|
Get.snackbar('成功', '已删除${problemsToDelete.length}个问题'); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '删除问题失败: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 删除问题关联的图片文件 |
||||||
|
Future<void> _deleteProblemImages(Problem problem) async { |
||||||
|
for (var imagePath in problem.imagePaths) { |
||||||
|
try { |
||||||
|
final file = File(imagePath); |
||||||
|
if (await file.exists()) { |
||||||
|
await file.delete(); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
print('删除图片失败: $imagePath, 错误: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 上传单个问题到服务器 |
||||||
|
Future<bool> uploadProblem(Problem problem) async { |
||||||
|
try { |
||||||
|
final formData = FormData.fromMap({ |
||||||
|
'description': problem.description, |
||||||
|
'location': problem.location, |
||||||
|
'createdAt': problem.createdAt.toIso8601String(), |
||||||
|
'boundInfo': problem.boundInfo ?? '', |
||||||
|
}); |
||||||
|
|
||||||
|
// 添加图片文件 |
||||||
|
for (var imagePath in problem.imagePaths) { |
||||||
|
final file = File(imagePath); |
||||||
|
if (await file.exists()) { |
||||||
|
formData.files.add( |
||||||
|
MapEntry( |
||||||
|
'images', |
||||||
|
await MultipartFile.fromFile( |
||||||
|
imagePath, |
||||||
|
filename: path.basename(imagePath), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final response = await _dio.post( |
||||||
|
'https://your-server.com/api/problems', |
||||||
|
data: formData, |
||||||
|
options: Options( |
||||||
|
sendTimeout: const Duration(seconds: 30), |
||||||
|
receiveTimeout: const Duration(seconds: 30), |
||||||
|
), |
||||||
|
); |
||||||
|
|
||||||
|
if (response.statusCode == 200) { |
||||||
|
// 标记为已上传 |
||||||
|
final updatedProblem = problem.copyWith(isUploaded: true); |
||||||
|
await updateProblem(updatedProblem); |
||||||
|
return true; |
||||||
|
} else { |
||||||
|
throw Exception('服务器返回错误状态码: ${response.statusCode}'); |
||||||
|
} |
||||||
|
} on DioException catch (e) { |
||||||
|
if (e.type == DioExceptionType.connectionTimeout || |
||||||
|
e.type == DioExceptionType.receiveTimeout) { |
||||||
|
Get.snackbar('网络超时', '请检查网络连接后重试'); |
||||||
|
} else { |
||||||
|
Get.snackbar('网络错误', '上传问题失败: ${e.message}'); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '上传问题失败: $e'); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 批量上传所有未上传的问题 |
||||||
|
Future<void> uploadAllUnuploaded() async { |
||||||
|
final unuploaded = unuploadedProblems; |
||||||
|
if (unuploaded.isEmpty) { |
||||||
|
Get.snackbar('提示', '没有需要上传的问题'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
isLoading.value = true; |
||||||
|
int successCount = 0; |
||||||
|
|
||||||
|
for (var problem in unuploaded) { |
||||||
|
final success = await uploadProblem(problem); |
||||||
|
if (success) { |
||||||
|
successCount++; |
||||||
|
} |
||||||
|
// 添加短暂延迟,避免服务器压力过大 |
||||||
|
await Future.delayed(const Duration(milliseconds: 500)); |
||||||
|
} |
||||||
|
|
||||||
|
isLoading.value = false; |
||||||
|
|
||||||
|
if (successCount > 0) { |
||||||
|
Get.snackbar('成功', '已成功上传$successCount个问题'); |
||||||
|
} |
||||||
|
|
||||||
|
if (successCount < unuploaded.length) { |
||||||
|
Get.snackbar('部分成功', '有${unuploaded.length - successCount}个问题上传失败'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 绑定信息到问题 |
||||||
|
Future<void> bindInfoToProblem(String id, String info) async { |
||||||
|
try { |
||||||
|
final problem = problems.firstWhere((p) => p.id == id); |
||||||
|
final updatedProblem = problem.copyWith(boundInfo: info); |
||||||
|
await updateProblem(updatedProblem); |
||||||
|
Get.snackbar('成功', '信息已绑定'); |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '未找到问题或绑定失败: $e'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// 清除所有选中的状态 |
||||||
|
void clearSelections() { |
||||||
|
for (var problem in problems) { |
||||||
|
problem.isChecked.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,262 @@ |
|||||||
|
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 'dart:io'; |
||||||
|
import '../../../data/models/problem_model.dart'; |
||||||
|
import 'problem_controller.dart'; |
||||||
|
|
||||||
|
class ProblemFormController extends GetxController { |
||||||
|
final ProblemController _problemController; |
||||||
|
final TextEditingController descriptionController = TextEditingController(); |
||||||
|
final TextEditingController locationController = TextEditingController(); |
||||||
|
final RxList<XFile> selectedImages = <XFile>[].obs; |
||||||
|
final RxBool isLoading = false.obs; |
||||||
|
|
||||||
|
// 当前正在编辑的问题 |
||||||
|
Problem? _currentProblem; |
||||||
|
|
||||||
|
// 使用依赖注入,便于测试 |
||||||
|
ProblemFormController({ProblemController? problemController}) |
||||||
|
: _problemController = problemController ?? Get.find<ProblemController>(); |
||||||
|
|
||||||
|
// 是否是编辑模式 |
||||||
|
bool get isEditing => _currentProblem != null; |
||||||
|
|
||||||
|
// 初始化方法,用于加载编辑数据 |
||||||
|
void init(Problem? problem) { |
||||||
|
if (problem != null) { |
||||||
|
_currentProblem = problem; |
||||||
|
descriptionController.text = problem.description; |
||||||
|
locationController.text = problem.location; |
||||||
|
|
||||||
|
// 清空旧图片,并加载新图片的路径 |
||||||
|
selectedImages.clear(); |
||||||
|
for (var path in problem.imagePaths) { |
||||||
|
selectedImages.add(XFile(path)); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// 重置状态,确保是新增模式 |
||||||
|
_currentProblem = null; |
||||||
|
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<String> imagePaths = await _saveImagesToLocal(); |
||||||
|
|
||||||
|
if (isEditing && _currentProblem != null) { |
||||||
|
// 编辑模式:更新现有问题 |
||||||
|
final updatedProblem = _currentProblem!.copyWith( |
||||||
|
description: descriptionController.text, |
||||||
|
location: locationController.text, |
||||||
|
imagePaths: imagePaths, |
||||||
|
createdAt: DateTime.now(), // 更新编辑时间 |
||||||
|
); |
||||||
|
|
||||||
|
await _problemController.updateProblem(updatedProblem); |
||||||
|
Get.back(result: true); // 返回成功结果 |
||||||
|
Get.snackbar('成功', '问题已更新'); |
||||||
|
} else { |
||||||
|
// 新增模式:创建新问题 |
||||||
|
final problem = Problem( |
||||||
|
description: descriptionController.text, |
||||||
|
location: locationController.text, |
||||||
|
imagePaths: imagePaths, |
||||||
|
createdAt: DateTime.now(), |
||||||
|
isUploaded: false, |
||||||
|
); |
||||||
|
|
||||||
|
await _problemController.addProblem(problem); |
||||||
|
Get.back(result: true); // 返回成功结果 |
||||||
|
Get.snackbar('成功', '问题已保存'); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
Get.snackbar('错误', '保存问题失败: $e'); |
||||||
|
} finally { |
||||||
|
isLoading.value = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 保存图片到本地存储 |
||||||
|
Future<List<String>> _saveImagesToLocal() async { |
||||||
|
final List<String> 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 File imageFile = File(imagePath); |
||||||
|
|
||||||
|
// 读取图片字节并写入文件 |
||||||
|
final imageBytes = await image.readAsBytes(); |
||||||
|
await imageFile.writeAsBytes(imageBytes); |
||||||
|
|
||||||
|
imagePaths.add(imagePath); |
||||||
|
} catch (e) { |
||||||
|
print('保存图片失败: ${image.name}, 错误: $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 (_currentProblem == null) { |
||||||
|
// 新增模式:只要有内容就是有更改 |
||||||
|
return descriptionController.text.isNotEmpty || |
||||||
|
locationController.text.isNotEmpty || |
||||||
|
selectedImages.isNotEmpty; |
||||||
|
} |
||||||
|
|
||||||
|
// 编辑模式:检查与原始值的差异 |
||||||
|
return descriptionController.text != _currentProblem!.description || |
||||||
|
locationController.text != _currentProblem!.location || |
||||||
|
selectedImages.length != _currentProblem!.imagePaths.length; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
void onClose() { |
||||||
|
descriptionController.dispose(); |
||||||
|
locationController.dispose(); |
||||||
|
super.onClose(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
// problem_upload_controller.dart |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:problem_check_system/data/models/problem_model.dart'; |
||||||
|
// import 'package:problem_check_system/services/problem_service.dart'; |
||||||
|
|
||||||
|
class ProblemUploadController extends GetxController { |
||||||
|
// 假设从数据库或其他来源获取问题列表 |
||||||
|
final RxList<Problem> problems = <Problem>[].obs; |
||||||
|
|
||||||
|
// 用于存储用户选中的问题 |
||||||
|
final RxList<Problem> selectedProblems = <Problem>[].obs; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
// 监听 problems 列表的变化,并在变化时更新 selectedProblems |
||||||
|
ever(problems, (_) => _updateSelectedList()); |
||||||
|
// 监听 selectedProblems 列表,更新上传按钮状态 |
||||||
|
ever(selectedProblems, (_) => _updateUploadButtonState()); |
||||||
|
|
||||||
|
fetchProblems(); // 假设从数据源获取问题列表 |
||||||
|
} |
||||||
|
|
||||||
|
void fetchProblems() async { |
||||||
|
// 模拟从服务中获取问题列表 |
||||||
|
// final fetched = await ProblemService.getProblemsForUpload(); |
||||||
|
// problems.assignAll(fetched); |
||||||
|
} |
||||||
|
|
||||||
|
// 监听所有问题的 isChecked 状态,更新 selectedProblems 列表 |
||||||
|
void _updateSelectedList() { |
||||||
|
selectedProblems.clear(); |
||||||
|
for (var problem in problems) { |
||||||
|
if (problem.isChecked.value) { |
||||||
|
selectedProblems.add(problem); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 控制上传按钮是否可用 |
||||||
|
RxBool isUploadEnabled = false.obs; |
||||||
|
void _updateUploadButtonState() { |
||||||
|
isUploadEnabled.value = selectedProblems.isNotEmpty; |
||||||
|
} |
||||||
|
|
||||||
|
// 控制全选/取消全选按钮的文案 |
||||||
|
RxBool allSelected = false.obs; |
||||||
|
|
||||||
|
void selectAll() { |
||||||
|
final bool newState = !allSelected.value; |
||||||
|
for (var problem in problems) { |
||||||
|
problem.isChecked.value = newState; |
||||||
|
} |
||||||
|
allSelected.value = newState; |
||||||
|
} |
||||||
|
|
||||||
|
void uploadProblems() { |
||||||
|
if (selectedProblems.isEmpty) return; |
||||||
|
// 实际的上传逻辑,例如调用 API |
||||||
|
print('开始上传 ${selectedProblems.length} 个问题...'); |
||||||
|
// 上传完成后,清空列表或更新状态 |
||||||
|
selectedProblems.clear(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
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_upload_controller.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/components/problem_card.dart'; |
||||||
|
|
||||||
|
class ProblemUploadPage extends StatelessWidget { |
||||||
|
ProblemUploadPage({super.key}); |
||||||
|
|
||||||
|
final ProblemUploadController controller = Get.put(ProblemUploadController()); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Scaffold( |
||||||
|
appBar: _buildAppBar(), |
||||||
|
body: _buildBody(), |
||||||
|
bottomNavigationBar: _buildBottomBar(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 构建顶部 AppBar |
||||||
|
PreferredSizeWidget _buildAppBar() { |
||||||
|
return AppBar( |
||||||
|
title: Obx(() { |
||||||
|
final selectedCount = controller.selectedProblems.length; |
||||||
|
return Text(selectedCount > 0 ? '已选择$selectedCount项' : '问题上传'); |
||||||
|
}), |
||||||
|
centerTitle: true, |
||||||
|
leading: IconButton(icon: Icon(Icons.close), onPressed: () => Get.back()), |
||||||
|
actions: [ |
||||||
|
TextButton( |
||||||
|
onPressed: controller.selectAll, |
||||||
|
child: Obx( |
||||||
|
() => Text( |
||||||
|
controller.allSelected.value ? '取消全选' : '全选', |
||||||
|
style: TextStyle(color: Colors.blue), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 构建页面主体 |
||||||
|
Widget _buildBody() { |
||||||
|
return Obx(() { |
||||||
|
return ListView.builder( |
||||||
|
itemCount: controller.problems.length, |
||||||
|
itemBuilder: (context, index) { |
||||||
|
final problem = controller.problems[index]; |
||||||
|
return ProblemCard( |
||||||
|
problem, |
||||||
|
// 传递视图类型,显示 Checkbox |
||||||
|
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: ElevatedButton( |
||||||
|
onPressed: controller.isUploadEnabled.value |
||||||
|
? controller.uploadProblems |
||||||
|
: null, |
||||||
|
child: Text('点击上传'), |
||||||
|
style: ElevatedButton.styleFrom( |
||||||
|
backgroundColor: Colors.blue, |
||||||
|
foregroundColor: Colors.white, |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(8.r), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue