Browse Source

重构代码

dev
徐振升 2 weeks ago
parent
commit
2e5b115a8d
  1. 2
      android/app/src/main/AndroidManifest.xml
  2. 3
      devtools_options.yaml
  3. 62
      lib/app/bindings/initial_binding.dart
  4. 35
      lib/app/routes/app_pages.dart
  5. 7
      lib/app/routes/app_routes.dart
  6. 130
      lib/controllers/add_problem_controller.dart
  7. 73
      lib/controllers/auth_controller.dart
  8. 115
      lib/controllers/problem_controller.dart
  9. 12
      lib/data/models/login_model.dart
  10. 21
      lib/data/models/problem_model.dart
  11. 19
      lib/data/providers/auth_provider.dart
  12. 2
      lib/data/providers/local_database.dart
  13. 49
      lib/main.dart
  14. 19
      lib/modules/auth/bindings/auth_binding.dart
  15. 111
      lib/modules/auth/controllers/auth_controller.dart
  16. 219
      lib/modules/auth/views/login_page.dart
  17. 26
      lib/modules/home/bindings/home_binding.dart
  18. 21
      lib/modules/home/controllers/home_controller.dart
  19. 21
      lib/modules/home/home_controller.dart
  20. 4
      lib/modules/home/views/home_page.dart
  21. 4
      lib/modules/login/models/login_model.dart
  22. 16
      lib/modules/login/view_models/login_controller.dart
  23. 175
      lib/modules/login/views/login_page.dart
  24. 0
      lib/modules/login/views/my_page.dart
  25. 9
      lib/modules/my/bingdings/my_binding.dart
  26. 34
      lib/modules/my/controllers/my_controller.dart
  27. 184
      lib/modules/my/views/my_page.dart
  28. 19
      lib/modules/problem/bindings/problem_binding.dart
  29. 11
      lib/modules/problem/components/problem_card.dart
  30. 235
      lib/modules/problem/controllers/problem_controller.dart
  31. 262
      lib/modules/problem/controllers/problem_form_controller.dart
  32. 64
      lib/modules/problem/controllers/problem_upload_controller.dart
  33. 329
      lib/modules/problem/views/problem_form_page.dart
  34. 0
      lib/modules/problem/views/problem_list_page.dart
  35. 163
      lib/modules/problem/views/problem_page.dart
  36. 84
      lib/modules/problem/views/problem_upload_page.dart
  37. 0
      lib/shared/widgets/custom_button.dart
  38. 0
      lib/shared/widgets/date_picker_button.dart
  39. 8
      pubspec.lock
  40. 1
      pubspec.yaml

2
android/app/src/main/AndroidManifest.xml

@ -13,7 +13,7 @@
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application
android:label="problem_check_system"
android:label="现场问题检查"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity

3
devtools_options.yaml

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

62
lib/app/bindings/initial_binding.dart

@ -0,0 +1,62 @@
import 'package:get/get.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; // debugPrint
import 'package:get_storage/get_storage.dart';
import 'package:problem_check_system/data/providers/local_database.dart';
class InitialBinding implements Bindings {
@override
void dependencies() {
// GetStorage - 使 put
Get.put<GetStorage>(GetStorage(), permanent: true);
// Dio - 使 put lazyPut
Get.put<Dio>(createDioInstance());
//
Get.put<LocalDatabase>(LocalDatabase());
}
// Dio
Dio createDioInstance() {
final dio = Dio(
BaseOptions(
baseUrl: 'https://xhdev.anxincloud.cn', //
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
),
);
//
if (kDebugMode) {
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
logPrint: (o) => debugPrint(o.toString()),
),
);
}
//
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
final token = ""; //todo token
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (DioException e, handler) {
if (e.response?.statusCode == 401) {
// 401 Unauthorized
Get.offAllNamed('/login');
}
return handler.next(e);
},
),
);
return dio;
}
}

35
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>[
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,
// ),
];
}

7
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';
}

130
lib/controllers/add_problem_controller.dart

@ -1,130 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import '../controllers/problem_controller.dart';
import 'package:permission_handler/permission_handler.dart';
class AddProblemController extends GetxController {
final ProblemController _problemController = Get.put(ProblemController());
final RxList<XFile> _selectedImages = <XFile>[].obs;
final TextEditingController descriptionController = TextEditingController();
final TextEditingController locationController = TextEditingController();
final RxBool isLoading = false.obs;
List<XFile> get selectedImages => _selectedImages;
// pickImage
Future<void> pickImage(ImageSource source) async {
try {
PermissionStatus status;
String permissionName;
String actionName;
if (source == ImageSource.camera) {
permissionName = "相机";
actionName = "拍照";
status = await Permission.camera.request();
} else {
permissionName = "相册";
actionName = "选择图片";
// Android
if (await Permission.photos.isGranted) {
status = PermissionStatus.granted;
} else if (await Permission.storage.isGranted) {
status = PermissionStatus.granted;
} else {
//
status = await Permission.photos.request();
// photos storage
if (status.isDenied || status.isPermanentlyDenied) {
status = await Permission.storage.request();
}
}
}
if (status.isGranted) {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: source);
if (image != null) {
_selectedImages.add(image);
}
} else if (status.isPermanentlyDenied) {
//
_showPermissionDeniedDialog(permissionName, actionName);
} else {
Get.snackbar('权限被拒绝', '需要$permissionName权限才能$actionName');
}
} catch (e) {
Get.snackbar('错误', '选择图片失败: $e');
}
}
//
void _showPermissionDeniedDialog(String permissionName, String actionName) {
Get.dialog(
AlertDialog(
title: Text('权限被拒绝'),
content: Text('需要$permissionName权限才能$actionName。是否要前往设置开启权限?'),
actions: [
TextButton(onPressed: () => Get.back(), child: Text('取消')),
TextButton(
onPressed: () async {
Get.back();
await openAppSettings();
},
child: Text('去设置'),
),
],
),
);
}
Future<void> removeImage(int index) async {
_selectedImages.removeAt(index);
}
bool get isFormValid {
return descriptionController.text.isNotEmpty &&
locationController.text.isNotEmpty;
}
Future<void> saveProblem() async {
if (descriptionController.text.isEmpty) {
Get.snackbar('错误', '请填写问题描述');
return;
}
isLoading.value = true;
try {
await _problemController.addProblem(
descriptionController.text,
locationController.text,
_selectedImages.toList(),
);
//
descriptionController.clear();
locationController.clear();
_selectedImages.clear();
Get.back();
Get.snackbar('成功', '问题已保存');
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
} finally {
isLoading.value = false;
}
}
@override
void onClose() {
descriptionController.dispose();
locationController.dispose();
super.onClose();
}
}

73
lib/controllers/auth_controller.dart

@ -1,73 +0,0 @@
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:dio/dio.dart';
import '../services/local_database.dart';
class AuthController extends GetxController {
final LocalDatabase _localDatabase = LocalDatabase();
final RxBool isLoggedIn = false.obs;
final RxBool isOnlineMode = false.obs;
final RxString username = ''.obs;
@override
void onInit() {
super.onInit();
checkLoginStatus();
}
Future<void> checkLoginStatus() async {
final prefs = await SharedPreferences.getInstance();
final offlineLogin = prefs.getBool('offlineLogin') ?? false;
isLoggedIn.value = offlineLogin;
if (offlineLogin) {
username.value = prefs.getString('username') ?? '离线用户';
}
}
Future<bool> offlineLogin(String username) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('offlineLogin', true);
await prefs.setString('username', username);
isLoggedIn.value = true;
this.username.value = username;
isOnlineMode.value = false;
return true;
}
Future<bool> onlineLogin(String username, String password) async {
try {
final dio = Dio();
final response = await dio.post(
'https://your-server.com/api/login',
data: {'username': username, 'password': password},
);
if (response.statusCode == 200) {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('offlineLogin', false);
await prefs.setString('username', username);
isLoggedIn.value = true;
this.username.value = username;
isOnlineMode.value = true;
return true;
}
return false;
} catch (e) {
return false;
}
}
Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('offlineLogin');
await prefs.remove('username');
isLoggedIn.value = false;
username.value = '';
}
}

115
lib/controllers/problem_controller.dart

@ -1,115 +0,0 @@
import 'package:dio/dio.dart';
import 'package:get/get.dart' hide MultipartFile, FormData;
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import '../models/problem_model.dart';
import '../services/local_database.dart';
class ProblemController extends GetxController {
final LocalDatabase _localDatabase = LocalDatabase();
final RxList<Problem> problems = <Problem>[].obs;
final RxBool isLoading = false.obs;
//
List<Problem> get selectedProblems {
return problems.where((p) => p.isChecked.value).toList();
}
@override
void onInit() {
super.onInit();
loadProblems();
}
Future<void> loadProblems() async {
isLoading.value = true;
problems.value = await _localDatabase.getProblems();
isLoading.value = false;
}
Future<void> addProblem(
String description,
String location,
List<XFile> images,
) async {
try {
//
final List<String> imagePaths = [];
final directory = await getApplicationDocumentsDirectory();
for (var i = 0; i < images.length; i++) {
final String imagePath =
'${directory.path}/problem_${DateTime.now().millisecondsSinceEpoch}_$i.jpg';
final File imageFile = File(imagePath);
await imageFile.writeAsBytes(await images[i].readAsBytes());
imagePaths.add(imagePath);
}
final Problem problem = Problem(
description: description,
location: location,
imagePaths: imagePaths,
createdAt: DateTime.now(),
isUploaded: false,
);
await _localDatabase.insertProblem(problem);
problems.add(problem);
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
rethrow;
}
}
Future<void> uploadProblem(Problem problem) async {
try {
final dio = Dio();
//
final formData = FormData.fromMap({
'description': problem.description,
'location': problem.location,
'createdAt': problem.createdAt.toIso8601String(),
'boundInfo': problem.boundInfo ?? '',
});
//
for (var path in problem.imagePaths) {
formData.files.add(
MapEntry('images', await MultipartFile.fromFile(path)),
);
}
final response = await dio.post(
'https://your-server.com/api/problems',
data: formData,
);
if (response.statusCode == 200) {
//
problem.isUploaded = true;
await _localDatabase.updateProblem(problem);
problems.refresh();
Get.snackbar('成功', '问题已上传到服务器');
}
} catch (e) {
Get.snackbar('错误', '上传问题失败: $e');
}
}
Future<void> uploadAllUnuploaded() async {
final unuploaded = await _localDatabase.getUnuploadedProblems();
for (var problem in unuploaded) {
await uploadProblem(problem);
}
}
Future<void> bindInfoToProblem(int problemId, String info) async {
final problem = problems.firstWhere((p) => p.id == problemId);
problem.boundInfo = info;
await _localDatabase.updateProblem(problem);
problems.refresh();
}
}

12
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<String, dynamic> toMap() {
return {'username': username, 'password': password};
}
}

21
lib/models/problem_model.dart → lib/data/models/problem_model.dart

@ -21,6 +21,27 @@ class Problem {
this.boundInfo,
});
// copyWith
Problem copyWith({
String? id,
String? description,
String? location,
List<String>? 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<String, dynamic> toJson() {
return {

19
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<Response> signIn(LoginModel loginModel) async {
try {
final response = await _dio.post(_signInUrl, data: loginModel.toMap());
return response;
} on DioException {
rethrow;
}
}
}

2
lib/services/local_database.dart → lib/data/providers/local_database.dart

@ -59,7 +59,7 @@ class LocalDatabase {
);
}
Future<int> deleteProblem(int id) async {
Future<int> deleteProblem(String id) async {
final db = await database;
return await db.delete('problems', where: 'id = ?', whereArgs: [id]);
}

49
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(),
);
}
}

19
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>(() => AuthProvider(dio: Get.find<Dio>()));
// 2. (AuthController) AuthProvider
// Get.find()
Get.lazyPut<AuthController>(
() => AuthController(authProvider: Get.find<AuthProvider>()),
);
}
}

111
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<void> login() async {
if (username.value.isEmpty || password.value.isEmpty) {
Get.snackbar('输入错误', '用户名和密码不能为空');
return;
}
isLoading.value = true;
try {
final loginData = LoginModel(
username: username.value,
password: password.value,
);
final response = await _authProvider.signIn(loginData);
if (response.statusCode == 200 && response.data['token'] != null) {
final token = response.data['token'];
final refreshToken = response.data['refreshToken'];
_box.write('token', token);
_box.write('refreshToken', refreshToken);
if (rememberPassword.value) {
_box.write(_usernameKey, username.value);
_box.write(_passwordKey, password.value);
} else {
_box.remove(_usernameKey);
_box.remove(_passwordKey);
}
Get.offAllNamed(AppRoutes.home);
} else {
Get.snackbar('登录失败', '服务器返回了异常数据');
}
} on DioException catch (e) {
if (e.response?.data != null && e.response?.data['detail'] != null) {
final serverMessage = e.response!.data['detail'];
Get.snackbar('登录失败', serverMessage);
} else {
Get.snackbar('网络错误', '登录失败,请检查您的网络连接');
}
} catch (e) {
Get.snackbar('错误', '发生未知错误: ${e.toString()}');
} finally {
isLoading.value = false;
}
}
Future<void> logout() async {
_box.remove('token');
_box.remove('refreshToken');
if (rememberPassword.value == false) {
_box.remove(_usernameKey);
_box.remove(_passwordKey);
}
Get.offAllNamed(AppRoutes.login);
}
}

219
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<AuthController>();
// build TextEditingController
final TextEditingController usernameController = TextEditingController(
text: controller.username.value,
);
final TextEditingController passwordController = TextEditingController(
text: controller.password.value,
);
// AuthController
// TextEditingController
// 使 once() ever()
// onChanged
usernameController.text = controller.username.value;
passwordController.text = controller.password.value;
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
_buildBackground(),
_buildLoginCard(controller, usernameController, passwordController),
],
),
);
}
Widget _buildBackground() {
// ... ...
return Stack(
children: [
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.png'),
fit: BoxFit.fitWidth,
),
),
),
Positioned(
left: 28.5.w,
top: 89.5.h,
child: Image.asset(
'assets/images/label.png',
width: 171.5.w,
height: 23.5.h,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 28.5.w,
top: 128.5.h,
child: Image.asset(
'assets/images/label1.png',
width: 296.5.w,
height: 35.5.h,
fit: BoxFit.fitWidth,
),
),
],
);
}
// _buildLoginCard
Widget _buildLoginCard(
AuthController controller,
TextEditingController usernameController,
TextEditingController passwordController,
) {
return Positioned(
left: 20.5.w,
top: 220.5.h,
child: Container(
width: 334.w,
height: 574.5.h,
decoration: BoxDecoration(
color: const Color(0xFFFFFFFF).withOpacity(0.6), // 使 withOpacity
borderRadius: BorderRadius.all(Radius.circular(23.5.r)),
),
padding: EdgeInsets.all(24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
// 使 _buildTextFieldSection
_buildTextFieldSection(
label: '账号',
hintText: '请输入您的账号',
controller: usernameController,
onChanged: controller.updateUsername, // onChanged
),
const SizedBox(height: 22),
_buildTextFieldSection(
label: '密码',
hintText: '请输入您的密码',
obscureText: true,
controller: passwordController,
onChanged: controller.updatePassword,
),
const SizedBox(height: 9.5),
_buildRememberPasswordRow(controller),
const SizedBox(height: 138.5),
_buildLoginButton(controller),
],
),
),
);
}
// _buildTextFieldSection TextEditingController
Widget _buildTextFieldSection({
required String label,
required String hintText,
required TextEditingController controller,
bool obscureText = false,
required Function(String) onChanged,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
controller: controller, // 使
onChanged: onChanged,
obscureText: obscureText,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(),
),
),
],
);
}
Widget _buildRememberPasswordRow(AuthController controller) {
// ... ...
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(
() => Checkbox(
value: controller.rememberPassword.value,
onChanged: (value) => controller.rememberPassword.value = value!,
),
),
Text(
'记住密码',
style: TextStyle(color: const Color(0xFF959595), fontSize: 14.sp),
),
],
);
}
Widget _buildLoginButton(AuthController controller) {
// ... ...
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.login,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
minimumSize: Size(double.infinity, 48.h),
),
child: Ink(
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Container(
constraints: BoxConstraints(minHeight: 48.h),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(vertical: 12.h, horizontal: 24.w),
child: Obx(() {
if (controller.isLoading.value) {
return const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
);
}
return Text(
'登录',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
);
}),
),
),
),
);
}
}

26
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<Dio>();
final LocalDatabase database = Get.find<LocalDatabase>();
// HomeController
Get.lazyPut<HomeController>(() => HomeController());
Get.lazyPut<ProblemController>(
() => ProblemController(localDatabase: database, dio: dio),
);
Get.lazyPut<MyController>(() => MyController());
Get.lazyPut<AuthProvider>(() => AuthProvider(dio: dio));
Get.lazyPut<AuthController>(
() => AuthController(authProvider: Get.find<AuthProvider>()),
);
}
}

21
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<Widget> pages = [
const Center(child: Text('首页内容')),
const ProblemPage(), // 使 const
const MyPage(),
];
// NavigationBar
void changeIndex(int index) {
selectedIndex.value = index;
}
}

21
lib/modules/home/home_controller.dart

@ -1,21 +0,0 @@
// Logic
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/views/problem_page.dart';
class HomeController extends GetxController {
//
var selectedIndex = 0.obs;
// View
final List<Widget> pages = [
Center(child: Text('首页')),
ProblemPage(),
Center(child: Text('我的内容')),
];
//
void changeIndex(int index) {
selectedIndex.value = index;
}
}

4
lib/modules/home/home_page.dart → 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<HomeController>();
return Obx(
() => Scaffold(

4
lib/modules/login/models/login_model.dart

@ -1,4 +0,0 @@
class LoginModel {
String username = "";
String password = "";
}

16
lib/modules/login/view_models/login_controller.dart

@ -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}');
}
}
}

175
lib/modules/login/views/login_page.dart

@ -1,175 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/modules/login/view_models/login_controller.dart';
class LoginPage extends StatelessWidget {
final LoginController controller = Get.put(LoginController());
LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
//
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/background.png'),
fit: BoxFit.fitWidth,
),
),
),
//
Positioned(
left: 28.5.w,
top: 89.5.h,
child: Image.asset(
'assets/images/label.png',
width: 171.5.w,
height: 23.5.h,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 28.5.w,
top: 128.5.h,
child: Image.asset(
'assets/images/label1.png',
width: 296.5.w,
height: 35.5.h,
fit: BoxFit.fitWidth,
),
),
Positioned(
left: 20.5.w, // 20.5dp
top: 220.5.h, // 220.5dp
child: Container(
width: 334.w, // 334dp
height: 574.5.h, // 574.5dp
decoration: BoxDecoration(
color: const Color(
0xFFFFFFFF,
).withValues(alpha: 153), // 60%
borderRadius: BorderRadius.all(
Radius.circular(23.5.r), // 23.5dp圆角
),
),
padding: EdgeInsets.all(24.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Text(
'账号',
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
// labelText: '账号',
hintText: '请输入您的账号',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 22),
Text(
'密码',
style: TextStyle(fontSize: 16.5.sp, color: Colors.black),
),
const SizedBox(height: 10.5),
TextField(
obscureText: true,
style: TextStyle(color: Colors.black),
decoration: InputDecoration(
// labelText: '密码',
hintText: '请输入您的密码',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 9.5),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Obx(
() => Checkbox(
value: controller.rememberPassword.value,
onChanged: (value) =>
controller.rememberPassword.value = value!,
),
),
Text(
'记住密码',
style: TextStyle(
color: const Color(0xFF959595), //
fontSize: 14.sp, //
),
),
],
),
const SizedBox(height: 138.5),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// HomePage
Get.toNamed('/home');
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) => const HomePage(),
// ),
// );
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), //
),
minimumSize: Size(double.infinity, 48.h), //
),
child: Ink(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(8.r),
),
child: Container(
constraints: BoxConstraints(minHeight: 48.h),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
vertical: 12.h,
horizontal: 24.w,
),
child: Text(
'登录',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
],
),
),
),
],
),
);
}
}

0
lib/modules/login/views/my_page.dart

9
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>(() => MyController());
}
}

34
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
}
//
}

184
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<MyController>();
// AuthController 退
final AuthController authController = Get.find<AuthController>();
return Scaffold(
body: Stack(
children: [
//
_buildBackground(),
//
_buildContent(controller, authController),
],
),
);
}
///
Widget _buildBackground() {
return Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
height: 250.h,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xFFE4F0FF),
const Color(0xFFF1F7FF).withOpacity(0.0),
],
),
),
),
);
}
///
Widget _buildContent(MyController controller, AuthController authController) {
return Positioned(
top: 100.h,
left: 20.w,
right: 20.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserInfoCard(controller),
SizedBox(height: 20.h),
_buildActionButtons(authController),
],
),
);
}
///
Widget _buildUserInfoCard(MyController controller) {
return Obx(
() => Container(
width: 335.w,
height: 106.h,
padding: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
//
Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.grey[200],
shape: BoxShape.circle,
border: Border.all(
color: Colors.grey.withOpacity(0.4),
width: 1.w,
),
),
child: const Icon(
Icons.person,
size: 40,
color: Color(0xFFC8E0FF),
),
),
SizedBox(width: 15.w),
//
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
controller.userName.value,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
Text(
controller.userPhone.value,
style: TextStyle(fontSize: 14.sp, color: Colors.grey),
),
],
),
],
),
),
);
}
///
Widget _buildActionButtons(AuthController authController) {
return Column(
children: [
_buildActionButton(
label: '修改密码',
onTap: () {
// TODO:
// Get.toNamed(AppRoutes.changePassword);
},
),
SizedBox(height: 15.h),
_buildActionButton(
label: '退出登录',
isLogout: true,
onTap: () {
// AuthController 退
authController.logout();
},
),
],
);
}
///
Widget _buildActionButton({
required String label,
required VoidCallback onTap,
bool isLogout = false,
}) {
return InkWell(
onTap: onTap,
child: Container(
width: 335.w,
height: 50.h,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.r),
border: Border.all(color: const Color(0xFFEEEEEE)),
),
child: Text(
label,
style: TextStyle(
fontSize: 16.sp,
color: isLogout ? const Color(0xFFE50000) : Colors.black,
fontWeight: isLogout ? FontWeight.bold : FontWeight.normal,
),
),
),
);
}
}

19
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>(
() => ProblemController(
localDatabase: Get.find<LocalDatabase>(),
dio: Get.find<Dio>(),
),
);
}
}

11
lib/modules/problem/problem_card.dart → 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: <Widget>[
@ -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),

235
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<Problem> problems = <Problem>[].obs;
final RxBool isLoading = false.obs;
final Dio _dio;
// bindings
ProblemController({required LocalDatabase localDatabase, required Dio dio})
: _localDatabase = localDatabase,
_dio = dio;
//
List<Problem> get selectedProblems {
return problems.where((p) => p.isChecked.value).toList();
}
//
List<Problem> get unuploadedProblems {
return problems.where((p) => !p.isUploaded).toList();
}
@override
void onInit() {
super.onInit();
loadProblems();
}
Future<void> loadProblems() async {
isLoading.value = true;
try {
problems.value = await _localDatabase.getProblems();
} catch (e) {
Get.snackbar('错误', '加载问题失败: $e');
rethrow;
} finally {
isLoading.value = false;
}
}
/// GetX列表中添加一个新问题
Future<void> addProblem(Problem problem) async {
try {
// ID
if (problem.id == null) {
problem = problem.copyWith(
id: DateTime.now().millisecondsSinceEpoch.toString(),
);
}
await _localDatabase.insertProblem(problem);
problems.add(problem);
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
rethrow;
}
}
/// GetX列表中更新一个现有问题
Future<void> updateProblem(Problem problem) async {
try {
await _localDatabase.updateProblem(problem);
final index = problems.indexWhere((p) => p.id == problem.id);
if (index != -1) {
problems[index] = problem;
}
} catch (e) {
Get.snackbar('错误', '更新问题失败: $e');
rethrow;
}
}
///
Future<void> deleteProblem(Problem problem) async {
try {
if (problem.id != null) {
await _localDatabase.deleteProblem(problem.id!);
//
problems.remove(problem);
//
await _deleteProblemImages(problem);
}
} catch (e) {
Get.snackbar('错误', '删除问题失败: $e');
rethrow;
}
}
///
Future<void> deleteSelectedProblems() async {
final problemsToDelete = selectedProblems;
if (problemsToDelete.isEmpty) {
Get.snackbar('提示', '请至少选择一个问题进行删除');
return;
}
try {
for (var problem in problemsToDelete) {
await deleteProblem(problem);
}
Get.snackbar('成功', '已删除${problemsToDelete.length}个问题');
} catch (e) {
Get.snackbar('错误', '删除问题失败: $e');
}
}
///
Future<void> _deleteProblemImages(Problem problem) async {
for (var imagePath in problem.imagePaths) {
try {
final file = File(imagePath);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
print('删除图片失败: $imagePath, 错误: $e');
}
}
}
///
Future<bool> uploadProblem(Problem problem) async {
try {
final formData = FormData.fromMap({
'description': problem.description,
'location': problem.location,
'createdAt': problem.createdAt.toIso8601String(),
'boundInfo': problem.boundInfo ?? '',
});
//
for (var imagePath in problem.imagePaths) {
final file = File(imagePath);
if (await file.exists()) {
formData.files.add(
MapEntry(
'images',
await MultipartFile.fromFile(
imagePath,
filename: path.basename(imagePath),
),
),
);
}
}
final response = await _dio.post(
'https://your-server.com/api/problems',
data: formData,
options: Options(
sendTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
),
);
if (response.statusCode == 200) {
//
final updatedProblem = problem.copyWith(isUploaded: true);
await updateProblem(updatedProblem);
return true;
} else {
throw Exception('服务器返回错误状态码: ${response.statusCode}');
}
} on DioException catch (e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
Get.snackbar('网络超时', '请检查网络连接后重试');
} else {
Get.snackbar('网络错误', '上传问题失败: ${e.message}');
}
return false;
} catch (e) {
Get.snackbar('错误', '上传问题失败: $e');
return false;
}
}
///
Future<void> uploadAllUnuploaded() async {
final unuploaded = unuploadedProblems;
if (unuploaded.isEmpty) {
Get.snackbar('提示', '没有需要上传的问题');
return;
}
isLoading.value = true;
int successCount = 0;
for (var problem in unuploaded) {
final success = await uploadProblem(problem);
if (success) {
successCount++;
}
//
await Future.delayed(const Duration(milliseconds: 500));
}
isLoading.value = false;
if (successCount > 0) {
Get.snackbar('成功', '已成功上传$successCount个问题');
}
if (successCount < unuploaded.length) {
Get.snackbar('部分成功', '${unuploaded.length - successCount}个问题上传失败');
}
}
///
Future<void> bindInfoToProblem(String id, String info) async {
try {
final problem = problems.firstWhere((p) => p.id == id);
final updatedProblem = problem.copyWith(boundInfo: info);
await updateProblem(updatedProblem);
Get.snackbar('成功', '信息已绑定');
} catch (e) {
Get.snackbar('错误', '未找到问题或绑定失败: $e');
}
}
///
void clearSelections() {
for (var problem in problems) {
problem.isChecked.value = false;
}
}
}

262
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<XFile> selectedImages = <XFile>[].obs;
final RxBool isLoading = false.obs;
//
Problem? _currentProblem;
// 使便
ProblemFormController({ProblemController? problemController})
: _problemController = problemController ?? Get.find<ProblemController>();
//
bool get isEditing => _currentProblem != null;
//
void init(Problem? problem) {
if (problem != null) {
_currentProblem = problem;
descriptionController.text = problem.description;
locationController.text = problem.location;
//
selectedImages.clear();
for (var path in problem.imagePaths) {
selectedImages.add(XFile(path));
}
} else {
//
_currentProblem = null;
descriptionController.clear();
locationController.clear();
selectedImages.clear();
}
}
// pickImage
Future<void> pickImage(ImageSource source) async {
try {
PermissionStatus status;
if (source == ImageSource.camera) {
status = await Permission.camera.request();
} else {
//
status = await Permission.photos.request();
if (!status.isGranted) {
// Android
status = await Permission.storage.request();
}
}
if (status.isGranted) {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(
source: source,
imageQuality: 85, //
maxWidth: 1920, //
);
if (image != null) {
//
if (selectedImages.length >= 10) {
Get.snackbar('提示', '最多只能选择10张图片');
return;
}
selectedImages.add(image);
}
} else if (status.isPermanentlyDenied) {
_showPermissionPermanentlyDeniedDialog();
} else {
Get.snackbar('权限被拒绝', '需要相册权限才能选择图片');
}
} catch (e) {
Get.snackbar('错误', '选择图片失败: $e');
}
}
//
void _showPermissionPermanentlyDeniedDialog() {
Get.dialog(
AlertDialog(
title: const Text('权限被永久拒绝'),
content: const Text('需要相册权限来选择图片。请前往设置开启。'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () async {
Get.back();
await openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
}
//
void removeImage(int index) {
selectedImages.removeAt(index);
}
//
bool _validateForm() {
if (descriptionController.text.isEmpty) {
Get.snackbar('提示', '请填写问题描述', snackPosition: SnackPosition.TOP);
return false;
}
if (locationController.text.isEmpty) {
Get.snackbar('提示', '请填写问题位置', snackPosition: SnackPosition.TOP);
return false;
}
if (selectedImages.isEmpty) {
Get.snackbar('提示', '请至少上传一张图片', snackPosition: SnackPosition.TOP);
return false;
}
return true;
}
//
Future<void> saveProblem() async {
if (!_validateForm()) {
return;
}
isLoading.value = true;
try {
//
final List<String> imagePaths = await _saveImagesToLocal();
if (isEditing && _currentProblem != null) {
//
final updatedProblem = _currentProblem!.copyWith(
description: descriptionController.text,
location: locationController.text,
imagePaths: imagePaths,
createdAt: DateTime.now(), //
);
await _problemController.updateProblem(updatedProblem);
Get.back(result: true); //
Get.snackbar('成功', '问题已更新');
} else {
//
final problem = Problem(
description: descriptionController.text,
location: locationController.text,
imagePaths: imagePaths,
createdAt: DateTime.now(),
isUploaded: false,
);
await _problemController.addProblem(problem);
Get.back(result: true); //
Get.snackbar('成功', '问题已保存');
}
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
} finally {
isLoading.value = false;
}
}
//
Future<List<String>> _saveImagesToLocal() async {
final List<String> imagePaths = [];
final directory = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${directory.path}/problem_images');
//
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
for (var image in selectedImages) {
try {
final String fileName =
'problem_${DateTime.now().millisecondsSinceEpoch}_${path.basename(image.name)}';
final String imagePath = '${imagesDir.path}/$fileName';
final File imageFile = File(imagePath);
//
final imageBytes = await image.readAsBytes();
await imageFile.writeAsBytes(imageBytes);
imagePaths.add(imagePath);
} catch (e) {
print('保存图片失败: ${image.name}, 错误: $e');
//
}
}
return imagePaths;
}
// /
void cancel() {
//
final hasChanges = _hasFormChanges();
if (hasChanges) {
Get.dialog(
AlertDialog(
title: const Text('放弃编辑'),
content: const Text('您有未保存的更改,确定要放弃吗?'),
actions: [
TextButton(onPressed: () => Get.back(), child: const Text('取消')),
TextButton(
onPressed: () {
Get.back();
Get.back(result: false); //
},
child: const Text('确定'),
),
],
),
);
} else {
Get.back(result: false);
}
}
//
bool _hasFormChanges() {
if (_currentProblem == null) {
//
return descriptionController.text.isNotEmpty ||
locationController.text.isNotEmpty ||
selectedImages.isNotEmpty;
}
//
return descriptionController.text != _currentProblem!.description ||
locationController.text != _currentProblem!.location ||
selectedImages.length != _currentProblem!.imagePaths.length;
}
@override
void onClose() {
descriptionController.dispose();
locationController.dispose();
super.onClose();
}
}

64
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<Problem> problems = <Problem>[].obs;
//
final RxList<Problem> selectedProblems = <Problem>[].obs;
@override
void onInit() {
super.onInit();
// problems selectedProblems
ever(problems, (_) => _updateSelectedList());
// selectedProblems
ever(selectedProblems, (_) => _updateUploadButtonState());
fetchProblems(); //
}
void fetchProblems() async {
//
// final fetched = await ProblemService.getProblemsForUpload();
// problems.assignAll(fetched);
}
// isChecked selectedProblems
void _updateSelectedList() {
selectedProblems.clear();
for (var problem in problems) {
if (problem.isChecked.value) {
selectedProblems.add(problem);
}
}
}
//
RxBool isUploadEnabled = false.obs;
void _updateUploadButtonState() {
isUploadEnabled.value = selectedProblems.isNotEmpty;
}
// /
RxBool allSelected = false.obs;
void selectAll() {
final bool newState = !allSelected.value;
for (var problem in problems) {
problem.isChecked.value = newState;
}
allSelected.value = newState;
}
void uploadProblems() {
if (selectedProblems.isEmpty) return;
// API
print('开始上传 ${selectedProblems.length} 个问题...');
//
selectedProblems.clear();
}
}

329
lib/views/add_problem_page.dart → 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<Color>(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),

0
lib/modules/problem/problem_list_page.dart → lib/modules/problem/views/problem_list_page.dart

163
lib/views/problem_page.dart → 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<Map<String, bool>> problemData = [
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
{"initialBound": false, "initialUploaded": false},
{"initialBound": true, "initialUploaded": true},
{"initialBound": true, "initialUploaded": false},
];
const ProblemPage({super.key});
@override
Widget build(BuildContext context) {
final ProblemController problemController = Get.find<ProblemController>();
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<bool> _showDeleteConfirmationDialog(Problem problem) async {
// 使 Get.bottomSheet
return await Get.bottomSheet<bool>(
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;
}
}

84
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),
),
),
),
);
}
}

0
lib/modules/problem/components/custom_button.dart → lib/shared/widgets/custom_button.dart

0
lib/modules/problem/components/date_picker_button.dart → lib/shared/widgets/date_picker_button.dart

8
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:

1
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

Loading…
Cancel
Save