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