// modules/problem/controllers/problem_controller.dart import 'dart:developer'; import 'dart:io'; 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 'package:problem_check_system/app/routes/app_routes.dart'; import 'package:problem_check_system/data/repositories/problem_repository.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/models/date_range_enum.dart'; import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart'; class ProblemController extends GetxController with GetSingleTickerProviderStateMixin { /// 依赖问题数据 final ProblemRepository problemRepository; /// 最近问题列表 final RxList problems = [].obs; /// 历史问题列表 final RxList historyProblems = [].obs; /// 未上传的问题列表 final RxList unUploadedProblems = [].obs; final Rx allSelected = false.obs; final RxDouble uploadProgress = 0.0.obs; // Dio 的取消令牌,用于取消正在进行的请求 late CancelToken _cancelToken; final RxSet _selectedProblems = {}.obs; Set get selectedProblems => _selectedProblems; int get selectedCount => _selectedProblems.length; // 在 ProblemController 中添加 // 添加日期范围选项列表 List get dateRangeOptions { return DateRange.values.map((range) => range.toDropdownOption()).toList(); } final List uploadOptions = const [ DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), DropdownOption(label: '已上传', value: '已上传', icon: Icons.cloud_done), DropdownOption(label: '未上传', value: '未上传', icon: Icons.cloud_off), ]; final List bindOptions = const [ DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link), DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off), ]; final Rx currentDateRange = DateRange.oneWeek.obs; final RxString currentUploadFilter = '全部'.obs; final RxString currentBindFilter = '全部'.obs; // 历史问题列表筛选条件 final Rx historyStartTime = DateTime.now().obs; final Rx historyEndTime = DateTime.now().obs; final RxString historyUploadFilter = '全部'.obs; final RxString historyBindFilter = '全部'.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 => problemRepository.isOnline; ProblemController({required this.problemRepository}); @override void onInit() { super.onInit(); tabController = TabController(length: 2, vsync: this); tabController.addListener(_onTabChanged); loadProblems(); // 查询未上传问题 loadUnUploadedProblems(); } @override void onClose() { tabController.dispose(); super.onClose(); } // #region 问题上传 void updateProblemSelection(Problem problem, bool isChecked) { if (isChecked) { _selectedProblems.add(problem); } else { _selectedProblems.remove(problem); } // 更新全选状态 allSelected.value = _selectedProblems.length == unUploadedProblems.length; } void selectAll() { if (allSelected.value) { // 如果已经是全选,则取消全选 _selectedProblems.clear(); } else { // 如果是取消全选,则选择所有 _selectedProblems.addAll(unUploadedProblems); } allSelected.value = !allSelected.value; } // 上传完成后清空选中状态 void clearSelection() { _selectedProblems.clear(); allSelected.value = false; } // 在 handleUpload 方法中,上传完成后调用 clearSelection Future handleUpload() async { if (_selectedProblems.isEmpty) { Get.snackbar('提示', '请选择要上传的问题'); return; } uploadProgress.value = 0.0; _cancelToken = CancelToken(); showUploadProgressDialog(); try { await problemRepository.uploadProblems( _selectedProblems.toList(), // 转换为列表 cancelToken: _cancelToken, onProgress: (progress) { uploadProgress.value = progress; }, ); Get.back(); Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM); // 上传成功后清空选中状态 clearSelection(); // 重新加载未上传的问题列表 loadUnUploadedProblems(); } on DioException catch (e) { Get.back(); if (CancelToken.isCancel(e)) { Get.snackbar('提示', '上传已取消', snackPosition: SnackPosition.BOTTOM); } else { Get.snackbar( '上传失败', '错误: ${e.message}', snackPosition: SnackPosition.BOTTOM, ); } } catch (e) { Get.back(); Get.snackbar('上传失败', '发生未知错误', snackPosition: SnackPosition.BOTTOM); } } /// 显示上传对话框 void showUploadProgressDialog() { // 显示对话框 Get.defaultDialog( title: '上传问题中...', content: Obx(() { // final progress = (uploadProgress.value * 100).toInt(); return Column( // mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 16.h), LinearProgressIndicator( value: uploadProgress.value, backgroundColor: Colors.grey[300], valueColor: const AlwaysStoppedAnimation(Colors.blue), ), SizedBox(height: 16.h), // Text('已完成: $progress%'), Text('已上传: ${unUploadedProblems.length} / $selectedCount'), ], ); }), barrierDismissible: false, // 防止用户点击外部关闭对话框 // 添加一个 "取消" 按钮 cancel: ElevatedButton( onPressed: () { // 调用 controller 中的取消方法 cancelUpload(); }, child: Text('取消', style: TextStyle(color: Colors.red)), ), ); // 启动上传逻辑 // ... } void cancelUpload() { // 在这里实现你的取消逻辑 // 1. 停止上传任务(例如,取消 HTTP 请求) // 2. 将上传进度重置为0 uploadProgress.value = 0.0; // 3. 关闭对话框 Get.back(); // 4. 可以添加一个提示,例如: // Get.snackbar('提示', '上传已取消'); } void uploadProblems() async { // if (selectedUnUploadedProblems.isEmpty) return; // // 实际的上传逻辑,例如调用 API // // 上传完成后,清空列表或更新状态 // selectedUnUploadedProblems.clear(); // for (var problem in selectedUnUploadedProblems) { // await uploadProblem(problem); // } } // #endregion // #region 悬浮按钮 /// 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); } // #endregion // #region ta按钮 void _onTabChanged() { if (!tabController.indexIsChanging) { loadProblems(); } } // #endregion // 更新日期范围的方法 void updateDateRange(String rangeValue) { final newRange = rangeValue.toDateRange(); if (newRange != null) { currentDateRange.value = newRange; loadProblems(); // 重新加载数据 } } // 添加筛选方法 void updateUploadFilter(String value) { currentUploadFilter.value = value; loadProblems(); // 重新加载数据 } void updateBindFilter(String value) { currentBindFilter.value = value; loadProblems(); // 重新加载数据 } /// 加载 Future loadProblems() async { isLoading.value = true; try { // 根据 Tab 索引设置查询参数的默认值 final bool isProblemListTab = tabController.index == 0; final DateTime startDate = isProblemListTab ? currentDateRange.value.startDate : historyStartTime.value; final DateTime endDate = isProblemListTab ? currentDateRange.value.endDate : historyEndTime.value; final String uploadStatus = isProblemListTab ? currentUploadFilter.value : historyUploadFilter.value; final String bindStatus = isProblemListTab ? currentBindFilter.value : historyBindFilter.value; // 只执行一次数据库查询 final loadedProblems = await problemRepository.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; } } // 新增方法:查询所有未上传的问题 Future loadUnUploadedProblems() async { isLoading.value = true; try { // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 unUploadedProblems.value = await problemRepository.getProblemsForSync(); } catch (e) { Get.snackbar('错误', '加载未上传问题失败: $e'); } finally { isLoading.value = false; } } // Future addProblem(Problem problem) async { // try { // await problemRepository.insertProblem(problem); // loadProblems(); // } catch (e) { // Get.snackbar('错误', '保存问题失败: $e'); // rethrow; // } // } // Future updateProblem(Problem problem) async { // try { // await problemRepository.updateProblem(problem); // loadProblems(); // } catch (e) { // Get.snackbar('错误', '更新问题失败: $e'); // rethrow; // } // } Future deleteProblem(Problem problem) async { try { if (problem.id != null) { await problemRepository.deleteProblem(problem.id!); await _deleteProblemImages(problem); loadProblems(); } } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); rethrow; } } // 删除本地文件 Future _deleteProblemImages(Problem problem) async { for (var imagePath in problem.imageUrls) { try { final file = File(imagePath.localPath); if (await file.exists()) { await file.delete(); } } catch (e) { throw Exception(e); } } } /// 显示日期选择器 /// Future selectDateRange(BuildContext context) async { final initialDateRange = DateTimeRange( start: DateTime.now().subtract(const Duration(days: 7)), end: DateTime.now(), ); final DateTimeRange? picked = await showDateRangePicker( context: context, firstDate: DateTime(2025, 8, 1), // 可选的最早日期 lastDate: DateTime(2101), // 可选的最晚日期 initialDateRange: initialDateRange, ); if (picked != null) { // 处理用户选择的日期范围 log('选择的日期范围是: ${picked.start} 到 ${picked.end}'); } } Future toProblemFormPageAndRefresh() async { await Get.toNamed(AppRoutes.problemForm); loadProblems(); } }