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