diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 427e780..8bc9705 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,7 @@ (GetStorage(), permanent: true); + + // 全局注册 Dio 实例 - 使用 put 而不是 lazyPut,并设置为永久 + Get.put(createDioInstance()); + // + Get.put(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; + } +} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart new file mode 100644 index 0000000..d2450c7 --- /dev/null +++ b/lib/app/routes/app_pages.dart @@ -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( + 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, + // ), + ]; +} diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart new file mode 100644 index 0000000..300f680 --- /dev/null +++ b/lib/app/routes/app_routes.dart @@ -0,0 +1,7 @@ +abstract class AppRoutes { + // 命名路由,使用 const 常量 + static const home = '/home'; + static const login = '/login'; + static const problem = '/problem'; + static const my = '/my'; +} diff --git a/lib/controllers/add_problem_controller.dart b/lib/controllers/add_problem_controller.dart deleted file mode 100644 index 85cf160..0000000 --- a/lib/controllers/add_problem_controller.dart +++ /dev/null @@ -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 _selectedImages = [].obs; - final TextEditingController descriptionController = TextEditingController(); - final TextEditingController locationController = TextEditingController(); - final RxBool isLoading = false.obs; - - List get selectedImages => _selectedImages; - - // 改进的 pickImage 方法 - Future 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 removeImage(int index) async { - _selectedImages.removeAt(index); - } - - bool get isFormValid { - return descriptionController.text.isNotEmpty && - locationController.text.isNotEmpty; - } - - Future 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(); - } -} diff --git a/lib/controllers/auth_controller.dart b/lib/controllers/auth_controller.dart deleted file mode 100644 index a3c1cba..0000000 --- a/lib/controllers/auth_controller.dart +++ /dev/null @@ -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 checkLoginStatus() async { - final prefs = await SharedPreferences.getInstance(); - final offlineLogin = prefs.getBool('offlineLogin') ?? false; - isLoggedIn.value = offlineLogin; - - if (offlineLogin) { - username.value = prefs.getString('username') ?? '离线用户'; - } - } - - Future 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 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 logout() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove('offlineLogin'); - await prefs.remove('username'); - - isLoggedIn.value = false; - username.value = ''; - } -} diff --git a/lib/controllers/problem_controller.dart b/lib/controllers/problem_controller.dart deleted file mode 100644 index e2ed97e..0000000 --- a/lib/controllers/problem_controller.dart +++ /dev/null @@ -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 problems = [].obs; - final RxBool isLoading = false.obs; - - // 这个方法返回所有已选中的问题对象 - List get selectedProblems { - return problems.where((p) => p.isChecked.value).toList(); - } - - @override - void onInit() { - super.onInit(); - loadProblems(); - } - - Future loadProblems() async { - isLoading.value = true; - problems.value = await _localDatabase.getProblems(); - isLoading.value = false; - } - - Future addProblem( - String description, - String location, - List images, - ) async { - try { - // 保存图片到本地 - final List 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 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 uploadAllUnuploaded() async { - final unuploaded = await _localDatabase.getUnuploadedProblems(); - for (var problem in unuploaded) { - await uploadProblem(problem); - } - } - - Future bindInfoToProblem(int problemId, String info) async { - final problem = problems.firstWhere((p) => p.id == problemId); - problem.boundInfo = info; - await _localDatabase.updateProblem(problem); - problems.refresh(); - } -} diff --git a/lib/data/models/login_model.dart b/lib/data/models/login_model.dart new file mode 100644 index 0000000..e214c80 --- /dev/null +++ b/lib/data/models/login_model.dart @@ -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 toMap() { + return {'username': username, 'password': password}; + } +} diff --git a/lib/models/problem_model.dart b/lib/data/models/problem_model.dart similarity index 73% rename from lib/models/problem_model.dart rename to lib/data/models/problem_model.dart index bdaa7ea..7bbd068 100644 --- a/lib/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -21,6 +21,27 @@ class Problem { this.boundInfo, }); + // copyWith 方法 + Problem copyWith({ + String? id, + String? description, + String? location, + List? imagePaths, + DateTime? createdAt, + bool? isUploaded, + String? boundInfo, + }) { + return Problem( + id: id ?? this.id, + description: description ?? this.description, + location: location ?? this.location, + imagePaths: imagePaths ?? this.imagePaths, + createdAt: createdAt ?? this.createdAt, + isUploaded: isUploaded ?? this.isUploaded, + boundInfo: boundInfo ?? this.boundInfo, + ); + } + // 添加 toJson 方法 Map toJson() { return { diff --git a/lib/data/providers/auth_provider.dart b/lib/data/providers/auth_provider.dart new file mode 100644 index 0000000..d614011 --- /dev/null +++ b/lib/data/providers/auth_provider.dart @@ -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 signIn(LoginModel loginModel) async { + try { + final response = await _dio.post(_signInUrl, data: loginModel.toMap()); + return response; + } on DioException { + rethrow; + } + } +} diff --git a/lib/services/local_database.dart b/lib/data/providers/local_database.dart similarity index 97% rename from lib/services/local_database.dart rename to lib/data/providers/local_database.dart index 8c17107..0d43062 100644 --- a/lib/services/local_database.dart +++ b/lib/data/providers/local_database.dart @@ -59,7 +59,7 @@ class LocalDatabase { ); } - Future deleteProblem(int id) async { + Future deleteProblem(String id) async { final db = await database; return await db.delete('problems', where: 'id = ?', whereArgs: [id]); } diff --git a/lib/main.dart b/lib/main.dart index 560c351..4411e50 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get_navigation/src/root/get_material_app.dart'; -import 'package:get/get_navigation/src/routes/get_route.dart'; -import 'package:get/get_navigation/src/routes/transitions_type.dart'; -import 'package:problem_check_system/modules/home/home_page.dart'; -import 'package:problem_check_system/modules/login/views/login_page.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:problem_check_system/app/routes/app_pages.dart'; +import 'package:problem_check_system/app/routes/app_routes.dart'; // 导入路由常量 +import 'package:problem_check_system/app/bindings/initial_binding.dart'; + +void main() async { + // 确保 Flutter Binding 已初始化 + WidgetsFlutterBinding.ensureInitialized(); + + // 初始化 GetStorage,这是关键步骤 + await GetStorage.init(); -void main() { runApp(const MainApp()); } @@ -15,38 +21,23 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - //填入设计稿中设备的屏幕尺寸,单位dp + // 填入设计稿中设备的屏幕尺寸, 单位dp return ScreenUtilInit( designSize: const Size(375, 812), minTextAdapt: true, splitScreenMode: true, - builder: (context, child) { + builder: (context, _) { + // 使用 _ 忽略 child 参数 return GetMaterialApp( debugShowCheckedModeBanner: false, - title: 'First Method', - // You can use the library anywhere in the app even in theme - theme: ThemeData( - useMaterial3: true, - primarySwatch: Colors.blue, - // textTheme: Typography.englishLike2018.apply(fontSizeFactor: 1.sp), - ), - initialRoute: '/', - getPages: [ - GetPage( - name: '/', - page: () => LoginPage(), - transition: Transition.cupertino, - ), - GetPage( - name: '/home', - page: () => HomePage(), - transition: Transition.cupertino, - ), - ], - home: child, + title: '问题检查系统', // 使用更有意义的应用标题 + theme: ThemeData(useMaterial3: true, primarySwatch: Colors.blue), + // 仅使用 GetX 路由系统 + initialRoute: AppRoutes.login, // 使用路由常量作为初始页面 + initialBinding: InitialBinding(), // 如果有全局绑定,继续保留 + getPages: AppPages.routes, // 绑定所有路由 ); }, - child: LoginPage(), ); } } diff --git a/lib/modules/auth/bindings/auth_binding.dart b/lib/modules/auth/bindings/auth_binding.dart new file mode 100644 index 0000000..581a540 --- /dev/null +++ b/lib/modules/auth/bindings/auth_binding.dart @@ -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(dio: Get.find())); + + // 2. 注入控制器 (AuthController),它依赖于 AuthProvider + // 控制器通过 Get.find() 获取已注入的依赖 + Get.lazyPut( + () => AuthController(authProvider: Get.find()), + ); + } +} diff --git a/lib/modules/auth/controllers/auth_controller.dart b/lib/modules/auth/controllers/auth_controller.dart new file mode 100644 index 0000000..df3c25a --- /dev/null +++ b/lib/modules/auth/controllers/auth_controller.dart @@ -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 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 logout() async { + _box.remove('token'); + _box.remove('refreshToken'); + if (rememberPassword.value == false) { + _box.remove(_usernameKey); + _box.remove(_passwordKey); + } + Get.offAllNamed(AppRoutes.login); + } +} diff --git a/lib/modules/auth/views/login_page.dart b/lib/modules/auth/views/login_page.dart new file mode 100644 index 0000000..d6ea5ca --- /dev/null +++ b/lib/modules/auth/views/login_page.dart @@ -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(); + + // 在 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(Colors.white), + ); + } + return Text( + '登录', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + ), + ), + ); + } +} diff --git a/lib/modules/home/bindings/home_binding.dart b/lib/modules/home/bindings/home_binding.dart new file mode 100644 index 0000000..80ded33 --- /dev/null +++ b/lib/modules/home/bindings/home_binding.dart @@ -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(); + final LocalDatabase database = Get.find(); + // 惰性注入 HomeController,只在第一次被调用时创建 + Get.lazyPut(() => HomeController()); + Get.lazyPut( + () => ProblemController(localDatabase: database, dio: dio), + ); + Get.lazyPut(() => MyController()); + Get.lazyPut(() => AuthProvider(dio: dio)); + Get.lazyPut( + () => AuthController(authProvider: Get.find()), + ); + } +} diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart new file mode 100644 index 0000000..459158c --- /dev/null +++ b/lib/modules/home/controllers/home_controller.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/modules/my/views/my_page.dart'; +import 'package:problem_check_system/modules/problem/views/problem_page.dart'; + +class HomeController extends GetxController { + // 使用 Rx 类型管理响应式状态 + var selectedIndex = 0.obs; + + // 页面列表 + final List pages = [ + const Center(child: Text('首页内容')), + const ProblemPage(), // 使用 const 确保子页面不会频繁重建 + const MyPage(), + ]; + + // 改变选中索引,这个方法将由 NavigationBar 调用 + void changeIndex(int index) { + selectedIndex.value = index; + } +} diff --git a/lib/modules/home/home_controller.dart b/lib/modules/home/home_controller.dart deleted file mode 100644 index 0f81ba6..0000000 --- a/lib/modules/home/home_controller.dart +++ /dev/null @@ -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 pages = [ - Center(child: Text('首页')), - ProblemPage(), - Center(child: Text('我的内容')), - ]; - - // 改变选中索引(更新状态) - void changeIndex(int index) { - selectedIndex.value = index; - } -} diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/views/home_page.dart similarity index 88% rename from lib/modules/home/home_page.dart rename to lib/modules/home/views/home_page.dart index 901f42a..ac8fe4a 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/views/home_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/modules/home/home_controller.dart'; +import 'package:problem_check_system/modules/home/controllers/home_controller.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -8,7 +8,7 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { // 获取控制器 - final HomeController controller = Get.put(HomeController()); + final HomeController controller = Get.find(); return Obx( () => Scaffold( diff --git a/lib/modules/login/models/login_model.dart b/lib/modules/login/models/login_model.dart deleted file mode 100644 index c41cfb0..0000000 --- a/lib/modules/login/models/login_model.dart +++ /dev/null @@ -1,4 +0,0 @@ -class LoginModel { - String username = ""; - String password = ""; -} diff --git a/lib/modules/login/view_models/login_controller.dart b/lib/modules/login/view_models/login_controller.dart deleted file mode 100644 index d8e4a1d..0000000 --- a/lib/modules/login/view_models/login_controller.dart +++ /dev/null @@ -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}'); - } - } -} diff --git a/lib/modules/login/views/login_page.dart b/lib/modules/login/views/login_page.dart deleted file mode 100644 index 0d0c713..0000000 --- a/lib/modules/login/views/login_page.dart +++ /dev/null @@ -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, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/modules/login/views/my_page.dart b/lib/modules/login/views/my_page.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/modules/my/bingdings/my_binding.dart b/lib/modules/my/bingdings/my_binding.dart new file mode 100644 index 0000000..e00442a --- /dev/null +++ b/lib/modules/my/bingdings/my_binding.dart @@ -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()); + } +} diff --git a/lib/modules/my/controllers/my_controller.dart b/lib/modules/my/controllers/my_controller.dart new file mode 100644 index 0000000..e791b85 --- /dev/null +++ b/lib/modules/my/controllers/my_controller.dart @@ -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 来获取用户信息 + } + + // 未来可以添加更新用户信息的逻辑 +} diff --git a/lib/modules/my/views/my_page.dart b/lib/modules/my/views/my_page.dart new file mode 100644 index 0000000..0aa5d4e --- /dev/null +++ b/lib/modules/my/views/my_page.dart @@ -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(); + // 获取 AuthController 实例,用于处理退出登录 + final AuthController authController = Get.find(); + + 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, + ), + ), + ), + ); + } +} diff --git a/lib/modules/problem/bindings/problem_binding.dart b/lib/modules/problem/bindings/problem_binding.dart new file mode 100644 index 0000000..5840803 --- /dev/null +++ b/lib/modules/problem/bindings/problem_binding.dart @@ -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( + localDatabase: Get.find(), + dio: Get.find(), + ), + ); + } +} diff --git a/lib/modules/problem/problem_card.dart b/lib/modules/problem/components/problem_card.dart similarity index 90% rename from lib/modules/problem/problem_card.dart rename to lib/modules/problem/components/problem_card.dart index bee3cdd..fdf7903 100644 --- a/lib/modules/problem/problem_card.dart +++ b/lib/modules/problem/components/problem_card.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; -import 'package:problem_check_system/models/problem_model.dart'; -import 'package:problem_check_system/modules/problem/components/custom_button.dart'; +import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/shared/widgets/custom_button.dart'; +import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; // 定义枚举类型 @@ -22,7 +23,7 @@ class ProblemCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card( - margin: EdgeInsets.symmetric(vertical: 5.h, horizontal: 9.w), + // margin: EdgeInsets.symmetric(vertical: 5.h, horizontal: 9.w), child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -102,14 +103,14 @@ class ProblemCard extends StatelessWidget { CustomButton( text: '修改', onTap: () { - print('点击修改按钮'); + Get.to(ProblemFormPage(problem: problem)); }, ), SizedBox(width: 8.w), CustomButton( text: '查看', onTap: () { - print('点击查看按钮'); + Get.to(ProblemFormPage(problem: problem, isReadOnly: true)); }, ), SizedBox(width: 16.w), diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart new file mode 100644 index 0000000..a645e20 --- /dev/null +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -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 problems = [].obs; + final RxBool isLoading = false.obs; + final Dio _dio; + + // 依赖注入,由 bindings 传入所有依赖 + ProblemController({required LocalDatabase localDatabase, required Dio dio}) + : _localDatabase = localDatabase, + _dio = dio; + + // 获取所有已选中的问题对象 + List get selectedProblems { + return problems.where((p) => p.isChecked.value).toList(); + } + + // 获取未上传的问题 + List get unuploadedProblems { + return problems.where((p) => !p.isUploaded).toList(); + } + + @override + void onInit() { + super.onInit(); + loadProblems(); + } + + Future loadProblems() async { + isLoading.value = true; + try { + problems.value = await _localDatabase.getProblems(); + } catch (e) { + Get.snackbar('错误', '加载问题失败: $e'); + rethrow; + } finally { + isLoading.value = false; + } + } + + /// 在本地数据库和GetX列表中添加一个新问题 + Future 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 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 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 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 _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 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 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 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; + } + } +} diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart new file mode 100644 index 0000000..a3117c8 --- /dev/null +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -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 selectedImages = [].obs; + final RxBool isLoading = false.obs; + + // 当前正在编辑的问题 + Problem? _currentProblem; + + // 使用依赖注入,便于测试 + ProblemFormController({ProblemController? problemController}) + : _problemController = problemController ?? Get.find(); + + // 是否是编辑模式 + 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 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 saveProblem() async { + if (!_validateForm()) { + return; + } + + isLoading.value = true; + + try { + // 保存图片到本地 + final List 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> _saveImagesToLocal() async { + final List 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(); + } +} diff --git a/lib/modules/problem/controllers/problem_upload_controller.dart b/lib/modules/problem/controllers/problem_upload_controller.dart new file mode 100644 index 0000000..2a20e6a --- /dev/null +++ b/lib/modules/problem/controllers/problem_upload_controller.dart @@ -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 problems = [].obs; + + // 用于存储用户选中的问题 + final RxList selectedProblems = [].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(); + } +} diff --git a/lib/views/add_problem_page.dart b/lib/modules/problem/views/problem_form_page.dart similarity index 55% rename from lib/views/add_problem_page.dart rename to lib/modules/problem/views/problem_form_page.dart index d0fddbc..0498614 100644 --- a/lib/views/add_problem_page.dart +++ b/lib/modules/problem/views/problem_form_page.dart @@ -1,68 +1,26 @@ import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:problem_check_system/controllers/add_problem_controller.dart'; +import 'package:problem_check_system/modules/problem/controllers/problem_form_controller.dart'; +import 'package:problem_check_system/data/models/problem_model.dart'; -class AddProblemPage extends StatelessWidget { - AddProblemPage({super.key}); +class ProblemFormPage extends StatelessWidget { + final Problem? problem; + final bool isReadOnly; // 新增的只读标志 + final ProblemFormController controller = Get.put(ProblemFormController()); - final AddProblemController controller = Get.put(AddProblemController()); - - // 显示底部选择器的方法 - void _showImageSourceBottomSheet(BuildContext context) { - showModalBottomSheet( - context: context, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), - ), - builder: (BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 16.h), - Text( - '选择图片来源', - style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), - ), - SizedBox(height: 16.h), - Divider(height: 1.h), - ListTile( - leading: Icon(Icons.camera_alt, color: Colors.blue), - title: Text('拍照'), - onTap: () { - Navigator.pop(context); - controller.pickImage(ImageSource.camera); - }, - ), - Divider(height: 1.h), - ListTile( - leading: Icon(Icons.photo_library, color: Colors.blue), - title: Text('从相册选择'), - onTap: () { - Navigator.pop(context); - controller.pickImage(ImageSource.gallery); - }, - ), - Divider(height: 1.h), - ListTile( - leading: Icon(Icons.cancel, color: Colors.grey), - title: Text('取消', style: TextStyle(color: Colors.grey)), - onTap: () => Navigator.pop(context), - ), - SizedBox(height: 8.h), - ], - ), - ); - }, - ); - } + // 构造函数,接收只读标志 + ProblemFormPage({super.key, this.problem, this.isReadOnly = false}); @override Widget build(BuildContext context) { + // 在页面首次绘制后调用控制器初始化方法 + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.init(problem); + }); + return Scaffold( appBar: AppBar( flexibleSpace: Container( @@ -75,13 +33,19 @@ class AddProblemPage extends StatelessWidget { ), ), leading: IconButton( - icon: const Icon(Icons.arrow_back_ios_new_rounded), - color: Colors.white, + icon: const Icon( + Icons.arrow_back_ios_new_rounded, + color: Colors.white, + ), onPressed: () { Navigator.pop(context); }, ), - title: const Text('新增问题', style: TextStyle(color: Colors.white)), + // 根据只读模式和问题对象动态设置标题 + title: Text( + isReadOnly ? '问题详情' : (problem == null ? '新增问题' : '编辑问题'), + style: const TextStyle(color: Colors.white), + ), centerTitle: true, backgroundColor: Colors.transparent, ), @@ -91,91 +55,89 @@ class AddProblemPage extends StatelessWidget { child: SingleChildScrollView( child: Column( children: [ - Card( - margin: EdgeInsets.all(17.w), - child: Column( - children: [ - ListTile( - title: const Text( - '问题描述', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - subtitle: TextField( - maxLines: null, - controller: controller.descriptionController, - decoration: const InputDecoration( - hintText: '请输入问题描述', - border: InputBorder.none, - ), - ), - ), - ], - ), + _buildInputCard( + title: '问题描述', + controller: controller.descriptionController, + hintText: '请输入问题描述', ), - Card( - margin: EdgeInsets.all(17.w), - child: Column( - children: [ - ListTile( - title: const Text( - '所在位置', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - subtitle: TextField( - maxLines: null, - controller: controller.locationController, - decoration: const InputDecoration( - hintText: '请输入问题所在位置', - border: InputBorder.none, - ), - ), - ), - ], - ), - ), - Card( - margin: EdgeInsets.all(17.w), - child: Column( - children: [ - ListTile( - title: const Text( - '问题图片', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 8.h), - _buildImageGridWithAddButton(context), - ], - ), - ), - ], - ), + _buildInputCard( + title: '所在位置', + controller: controller.locationController, + hintText: '请输入问题所在位置', ), + _buildImageCard(context), ], ), ), ), - _bottomButton(), + // 只有在非只读模式下才显示底部操作按钮 + if (!isReadOnly) _bottomButton(), + ], + ), + ); + } + + /// 构建输入框卡片 + Widget _buildInputCard({ + required String title, + required TextEditingController controller, + required String hintText, + }) { + return Card( + margin: EdgeInsets.all(17.w), + child: Column( + children: [ + ListTile( + title: Text( + title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: TextField( + maxLines: null, + controller: controller, + readOnly: isReadOnly, // 关键:根据只读标志设置可编辑性 + decoration: InputDecoration( + hintText: hintText, + border: InputBorder.none, + ), + ), + ), ], ), ); } - Widget _buildImageGridWithAddButton(BuildContext context) { + /// 构建图片展示卡片 + Widget _buildImageCard(BuildContext context) { + return Card( + margin: EdgeInsets.all(17.w), + child: Column( + children: [ + const ListTile( + title: Text( + '问题图片', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + child: _buildImageGrid(context), + ), + ], + ), + ); + } + + /// 构建图片网格,包含添加按钮和图片项 + Widget _buildImageGrid(BuildContext context) { return Obx(() { - // 计算总项目数(图片数 + 添加按钮) - int totalItems = controller.selectedImages.length + 1; + // 在只读模式下不显示添加按钮 + final bool showAddButton = !isReadOnly; + final int itemCount = + controller.selectedImages.length + (showAddButton ? 1 : 0); return GridView.builder( shrinkWrap: true, @@ -186,20 +148,18 @@ class AddProblemPage extends StatelessWidget { mainAxisSpacing: 8.h, childAspectRatio: 1, ), - itemCount: totalItems, + itemCount: itemCount, itemBuilder: (context, index) { - // 如果是最后一个项目,显示添加按钮 - if (index == controller.selectedImages.length) { + if (showAddButton && index == controller.selectedImages.length) { return _buildAddImageButton(context); } - - // 否则显示图片 return _buildImageItem(index); }, ); }); } + /// 构建添加图片按钮 Widget _buildAddImageButton(BuildContext context) { return InkWell( onTap: () => _showImageSourceBottomSheet(context), @@ -228,6 +188,7 @@ class AddProblemPage extends StatelessWidget { ); } + /// 构建单个图片项 Widget _buildImageItem(int index) { return Container( decoration: BoxDecoration( @@ -245,26 +206,80 @@ class AddProblemPage extends StatelessWidget { fit: BoxFit.cover, ), ), - Positioned( - top: 0, - right: 0, - child: GestureDetector( - onTap: () => controller.removeImage(index), - child: Container( - decoration: const BoxDecoration( - color: Colors.black54, - shape: BoxShape.circle, + // 只有在非只读模式下才显示删除按钮 + if (!isReadOnly) + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => controller.removeImage(index), + child: Container( + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + padding: EdgeInsets.all(4.w), + child: Icon(Icons.close, color: Colors.white, size: 16.sp), ), - padding: EdgeInsets.all(4.w), - child: Icon(Icons.close, color: Colors.white, size: 16.sp), ), ), - ), ], ), ); } + /// 显示底部图片来源选择器 + void _showImageSourceBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), + ), + builder: (BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ... (省略 showModalBottomSheet 内部代码,与原代码相同) + SizedBox(height: 16.h), + Text( + '选择图片来源', + style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), + ), + SizedBox(height: 16.h), + Divider(height: 1.h), + ListTile( + leading: const Icon(Icons.camera_alt, color: Colors.blue), + title: const Text('拍照'), + onTap: () { + Navigator.pop(context); + controller.pickImage(ImageSource.camera); + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.photo_library, color: Colors.blue), + title: const Text('从相册选择'), + onTap: () { + Navigator.pop(context); + controller.pickImage(ImageSource.gallery); + }, + ), + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.cancel, color: Colors.grey), + title: const Text('取消', style: TextStyle(color: Colors.grey)), + onTap: () => Navigator.pop(context), + ), + SizedBox(height: 8.h), + ], + ), + ); + }, + ); + } + + /// 构建底部操作按钮 Widget _bottomButton() { return Container( width: 375.w, @@ -310,26 +325,14 @@ class AddProblemPage extends StatelessWidget { ), onPressed: controller.isLoading.value ? null - : () { - if (controller.descriptionController.text.isEmpty) { - Get.snackbar( - '提示', - '问题描述不能为空', - snackPosition: SnackPosition.TOP, - backgroundColor: Colors.black87, - colorText: Colors.white, - ); - return; - } - controller.saveProblem(); - }, + : controller.saveProblem, child: controller.isLoading.value ? SizedBox( width: 20.w, height: 20.h, - child: CircularProgressIndicator( + child: const CircularProgressIndicator( strokeWidth: 2, - valueColor: const AlwaysStoppedAnimation( + valueColor: AlwaysStoppedAnimation( Colors.white, ), ), diff --git a/lib/modules/problem/problem_list_page.dart b/lib/modules/problem/views/problem_list_page.dart similarity index 100% rename from lib/modules/problem/problem_list_page.dart rename to lib/modules/problem/views/problem_list_page.dart diff --git a/lib/views/problem_page.dart b/lib/modules/problem/views/problem_page.dart similarity index 54% rename from lib/views/problem_page.dart rename to lib/modules/problem/views/problem_page.dart index 1fe83c6..644acea 100644 --- a/lib/views/problem_page.dart +++ b/lib/modules/problem/views/problem_page.dart @@ -1,37 +1,18 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/controllers/auth_controller.dart'; -import 'package:problem_check_system/controllers/problem_controller.dart'; -import 'package:problem_check_system/modules/problem/components/date_picker_button.dart'; -import 'package:problem_check_system/modules/problem/problem_card.dart'; -import 'package:problem_check_system/views/add_problem_page.dart'; +import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; +import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/shared/widgets/date_picker_button.dart'; +import 'package:problem_check_system/modules/problem/components/problem_card.dart'; +import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; class ProblemPage extends StatelessWidget { - ProblemPage({super.key}); - - final AuthController authController = Get.put(AuthController()); - final ProblemController problemController = Get.put(ProblemController()); - - final List> problemData = [ - {"initialBound": false, "initialUploaded": false}, - {"initialBound": true, "initialUploaded": true}, - {"initialBound": true, "initialUploaded": false}, - {"initialBound": false, "initialUploaded": false}, - {"initialBound": true, "initialUploaded": true}, - {"initialBound": true, "initialUploaded": false}, - {"initialBound": false, "initialUploaded": false}, - {"initialBound": true, "initialUploaded": true}, - {"initialBound": true, "initialUploaded": false}, - {"initialBound": false, "initialUploaded": false}, - {"initialBound": true, "initialUploaded": true}, - {"initialBound": true, "initialUploaded": false}, - ]; + const ProblemPage({super.key}); @override Widget build(BuildContext context) { + final ProblemController problemController = Get.find(); return DefaultTabController( initialIndex: 0, length: 2, @@ -58,9 +39,6 @@ class ProblemPage extends StatelessWidget { child: TabBar( indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( - // border: const Border( - // bottom: BorderSide(color: Colors.blue, width: 5.0), - // ), color: const Color(0xfffff7f7), borderRadius: BorderRadius.only( topLeft: Radius.circular(8), @@ -100,21 +78,6 @@ class ProblemPage extends StatelessWidget { Expanded( child: Stack( children: [ - // SingleChildScrollView( - // child: Column( - // children: [ - // ...problemData.map((data) { - // return ProblemCard( - // initialBound: - // data["initialBound"] ?? false, - // initialUploaded: - // data["initialUploaded"] ?? false, - // ); - // }), - // SizedBox(height: 64.h), - // ], - // ), - // ), Obx(() { if (problemController.isLoading.value) { return Center( @@ -127,7 +90,10 @@ class ProblemPage extends StatelessWidget { itemBuilder: (context, index) { final problem = problemController.problems[index]; - return ProblemCard(problem); + return _buildSwipeableProblemCard( + problem, + problemController, + ); }, ); }), @@ -137,7 +103,7 @@ class ProblemPage extends StatelessWidget { child: FloatingActionButton( heroTag: "123", onPressed: () { - Get.to(() => AddProblemPage()); + Get.to(() => ProblemFormPage()); }, shape: CircleBorder(), backgroundColor: Colors.blue[300], @@ -159,8 +125,9 @@ class ProblemPage extends StatelessWidget { itemCount: problemController.problems.length, itemBuilder: (context, index) { final problem = problemController.problems[index]; - return ProblemCard( + return _buildSwipeableProblemCard( problem, + problemController, viewType: ProblemCardViewType.checkbox, ); }, @@ -175,7 +142,8 @@ class ProblemPage extends StatelessWidget { floatingActionButton: FloatingActionButton( heroTag: "abc", onPressed: () { - print('object'); + // 批量上传功能 + problemController.uploadAllUnuploaded(); }, foregroundColor: Colors.white, backgroundColor: Colors.red[300], @@ -184,4 +152,103 @@ class ProblemPage extends StatelessWidget { ), ); } + + // 构建可滑动的ProblemCard组件 + Widget _buildSwipeableProblemCard( + Problem problem, + ProblemController controller, { + ProblemCardViewType viewType = ProblemCardViewType.buttons, + }) { + // 使用Dismissible组件实现左滑删除 + return Dismissible( + key: Key(problem.id ?? UniqueKey().toString()), + direction: DismissDirection.endToStart, // 只允许从右向左滑动 + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 20.w), + child: Icon(Icons.delete, color: Colors.white, size: 30.sp), + ), + confirmDismiss: (direction) async { + // 显示确认对话框 + return await _showDeleteConfirmationDialog(problem); + }, + onDismissed: (direction) { + // 用户确认后执行删除操作 + controller.deleteProblem(problem); + Get.snackbar('成功', '问题已删除'); + }, + child: ProblemCard(problem, viewType: viewType), + ); + } + + // 显示删除确认对话框 + Future _showDeleteConfirmationDialog(Problem problem) async { + // 使用 Get.bottomSheet 实现底部确认表单 + return await Get.bottomSheet( + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 标题 + SizedBox(height: 16), + Text( + '确认删除', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + // 内容 + Text( + '确定要删除这个问题吗?此操作不可撤销。', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + SizedBox(height: 24), + // 删除按钮 + ElevatedButton( + onPressed: () => Get.back(result: true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: Text( + '删除', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + SizedBox(height: 8), + // 取消按钮 + TextButton( + onPressed: () => Get.back(result: false), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + '取消', + style: TextStyle(color: Colors.grey[700], fontSize: 16), + ), + ), + SizedBox(height: 16), + ], + ), + ), + ), + isDismissible: false, // 阻止用户点击外部关闭 + ) ?? + false; + } } diff --git a/lib/modules/problem/views/problem_upload_page.dart b/lib/modules/problem/views/problem_upload_page.dart new file mode 100644 index 0000000..0bf78f7 --- /dev/null +++ b/lib/modules/problem/views/problem_upload_page.dart @@ -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), + ), + ), + ), + ); + } +} diff --git a/lib/modules/problem/components/custom_button.dart b/lib/shared/widgets/custom_button.dart similarity index 100% rename from lib/modules/problem/components/custom_button.dart rename to lib/shared/widgets/custom_button.dart diff --git a/lib/modules/problem/components/date_picker_button.dart b/lib/shared/widgets/date_picker_button.dart similarity index 100% rename from lib/modules/problem/components/date_picker_button.dart rename to lib/shared/widgets/date_picker_button.dart diff --git a/pubspec.lock b/pubspec.lock index 9225354..8f97f79 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -208,6 +208,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.7.2" + get_storage: + dependency: "direct main" + description: + name: get_storage + sha256: "39db1fffe779d0c22b3a744376e86febe4ade43bf65e06eab5af707dc84185a2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 51fcb44..9190bbf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: sdk: flutter flutter_screenutil: ^5.9.3 get: ^4.7.2 + get_storage: ^2.1.1 image_picker: ^1.1.2 intl: ^0.20.2 path: ^1.9.1