// modules/problem/controllers/problem_controller.dart import 'package:dio/dio.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart' hide MultipartFile, FormData; import 'package:flutter/material.dart'; import 'dart:io'; import 'package:path/path.dart' as path; import 'package:problem_check_system/data/providers/http_provider.dart'; import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/data/providers/local_database.dart'; import 'package:problem_check_system/data/providers/connectivity_provider.dart'; class ProblemController extends GetxController with GetSingleTickerProviderStateMixin { /// 依赖注入适配器 final LocalDatabase localDatabase; final HttpProvider httpProvider; final ConnectivityProvider connectivityProvider; /// 最近问题列表 final RxList problems = [].obs; /// 历史问题列表 final RxList historyProblems = [].obs; /// 未上传的问题列表 final RxList unUploadedProblems = [].obs; final RxList selectedUnUploadedProblems = [].obs; final Rx allSelected = false.obs; final RxBool isUploadEnabled = false.obs; /// 筛选条件 final Rx selectedDateRange = DateRange.oneWeek.obs; final RxString selectedUploadStatus = '全部'.obs; final RxString selectedBindingStatus = '全部'.obs; /// 是否加载中 final RxBool isLoading = false.obs; late TabController tabController; /// floatingButton 拖动 final double _fabSize = 56.0; final double _edgePaddingX = 27.0.w; final double _edgePaddingY = 111.0.h; final fabUploadPosition = Offset(337.0, 703.7).obs; /// get 选中的 RxBool get isOnline => connectivityProvider.isOnline; ProblemController({ required this.localDatabase, required this.httpProvider, required this.connectivityProvider, }); @override void onInit() { super.onInit(); tabController = TabController(length: 2, vsync: this); tabController.addListener(_onTabChanged); // 监听 unUploadedProblems 列表的变化,并在变化时更新 selectedProblems ever(unUploadedProblems, (_) => _updateSelectedList()); // 监听 selectedProblems 列表,更新上传按钮状态 ever(selectedUnUploadedProblems, (_) => _updateUploadButtonState()); loadProblems(); // 查询未上传问题 loadUnUploadedProblems(); } @override void onClose() { tabController.dispose(); super.onClose(); } /// 当单个问题的选中状态改变时调用 void onProblemCheckedChange() { // 重新计算 selectedUnUploadedProblems 列表 _updateSelectedList(); // 因为 _updateSelectedList 已经会触发 selectedUnUploadedProblems 的变化, // 所以 _updateUploadButtonState() 会被 ever 监听器自动调用。 } /// 上传问题逻辑 void _updateSelectedList() { selectedUnUploadedProblems.clear(); for (var problem in unUploadedProblems) { if (problem.isChecked.value) { selectedUnUploadedProblems.add(problem); } } } void _updateUploadButtonState() { isUploadEnabled.value = selectedUnUploadedProblems.isNotEmpty; } void selectAll() { final bool newState = !allSelected.value; for (var problem in unUploadedProblems) { problem.isChecked.value = newState; } allSelected.value = newState; _updateSelectedList(); } void uploadProblems() { if (selectedUnUploadedProblems.isEmpty) return; // 实际的上传逻辑,例如调用 API print('开始上传 ${selectedUnUploadedProblems.length} 个问题...'); // 上传完成后,清空列表或更新状态 selectedUnUploadedProblems.clear(); } /// floatingButton更新位置 void updateFabUploadPosition(Offset delta) { final screenWidth = ScreenUtil().screenWidth; final screenHeight = ScreenUtil().screenHeight; Offset newPosition = fabUploadPosition.value + delta; // 限制水平范围:按钮左边缘与屏幕左边缘的距离 double clampedDx = newPosition.dx.clamp( _edgePaddingX, screenWidth - _fabSize - _edgePaddingX, ); // 限制垂直范围:按钮上边缘与屏幕上边缘的距离 double clampedDy = newPosition.dy.clamp( _edgePaddingY, screenHeight - _fabSize - _edgePaddingY, ); fabUploadPosition.value = Offset(clampedDx, clampedDy); } /// floatingButton 贴靠 void snapToEdge() { final screenWidth = ScreenUtil().screenWidth; // 获取当前按钮的水平中心点 final buttonCenterDx = fabUploadPosition.value.dx + _fabSize / 2; double newDx; // 判断按钮中心点位于屏幕的左半部分还是右半部分 if (buttonCenterDx < screenWidth / 2) { // 贴靠到左侧,按钮左边缘与屏幕左边缘距离为 _edgePaddingX newDx = _edgePaddingX; } else { // 贴靠到右侧,按钮右边缘与屏幕右边缘距离为 _edgePaddingX newDx = screenWidth - _fabSize - _edgePaddingX; } // 关键:只更新水平位置,垂直位置保持不变 fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy); } void _onTabChanged() { if (!tabController.indexIsChanging) { selectedDateRange.value = DateRange.oneWeek; selectedUploadStatus.value = '全部'; selectedBindingStatus.value = '全部'; loadProblems(); } } Future loadProblems() async { isLoading.value = true; try { // 根据 Tab 索引设置查询参数的默认值 final bool isProblemListTab = tabController.index == 0; final DateTime startDate = isProblemListTab ? selectedDateRange.value.startDate : DateTime(2000); // 历史列表从很早的日期开始 final DateTime endDate = DateTime.now(); final String uploadStatus = isProblemListTab ? selectedUploadStatus.value : '全部'; final String bindStatus = isProblemListTab ? selectedBindingStatus.value : '全部'; // 只执行一次数据库查询 final loadedProblems = await localDatabase.getProblems( startDate: startDate, endDate: endDate, uploadStatus: uploadStatus, bindStatus: bindStatus, ); // 根据 Tab 索引将数据分配给正确的列表 if (isProblemListTab) { problems.assignAll(loadedProblems); } else { historyProblems.assignAll(loadedProblems); } } catch (e) { Get.snackbar('错误', '加载问题失败: $e'); } finally { isLoading.value = false; } } /// 筛选问题 void updateFiltersAndLoadProblems({ DateRange? newDateRange, String? newUploadStatus, String? newBindingStatus, }) { if (newDateRange != null) { selectedDateRange.value = newDateRange; } if (newUploadStatus != null) { selectedUploadStatus.value = newUploadStatus; } if (newBindingStatus != null) { selectedBindingStatus.value = newBindingStatus; } // 只要调用此方法,就重新加载数据 loadProblems(); } // 新增方法:查询所有未上传的问题 Future loadUnUploadedProblems() async { isLoading.value = true; try { // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 unUploadedProblems.value = await localDatabase.getProblems( uploadStatus: '未上传', ); } catch (e) { Get.snackbar('错误', '加载未上传问题失败: $e'); } finally { isLoading.value = false; } } Future addProblem(Problem problem) async { try { await localDatabase.insertProblem(problem); loadProblems(); } catch (e) { Get.snackbar('错误', '保存问题失败: $e'); rethrow; } } Future updateProblem(Problem problem) async { try { await localDatabase.updateProblem(problem); loadProblems(); } catch (e) { Get.snackbar('错误', '更新问题失败: $e'); rethrow; } } Future deleteProblem(Problem problem) async { try { if (problem.id != null) { await localDatabase.deleteProblem(problem.id!); await _deleteProblemImages(problem); loadProblems(); } } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); rethrow; } } Future deleteSelectedProblems() async { final problemsToDelete = selectedUnUploadedProblems; if (problemsToDelete.isEmpty) { Get.snackbar('提示', '请至少选择一个问题进行删除'); return; } try { for (var problem in problemsToDelete) { if (problem.id != null) { await localDatabase.deleteProblem(problem.id!); await _deleteProblemImages(problem); } } Get.snackbar('成功', '已删除${problemsToDelete.length}个问题'); loadProblems(); } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); } } Future _deleteProblemImages(Problem problem) async { for (var imagePath in problem.imageUrls) { 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.bindData ?? '', }); for (var imageUrl in problem.imageUrls) { final file = File(imageUrl); if (await file.exists()) { formData.files.add( MapEntry( 'images', await MultipartFile.fromFile( imageUrl, filename: path.basename(imageUrl), ), ), ); } } final response = await httpProvider.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 { if (!isOnline.value) { Get.snackbar('提示', '当前无网络,无法上传'); return; } 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}个问题上传失败'); } loadProblems(); } Future bindInfoToProblem(String id, String info) async { try { final problem = historyProblems.firstWhere((p) => p.id == id); final updatedProblem = problem.copyWith(bindData: info); await updateProblem(updatedProblem); Get.snackbar('成功', '信息已绑定'); } catch (e) { Get.snackbar('错误', '未找到问题或绑定失败: $e'); } } void clearSelections() { for (var problem in historyProblems) { problem.isChecked.value = false; } } }