|
|
|
@ -1,22 +1,20 @@
|
|
|
|
|
// modules/problem/controllers/problem_controller.dart |
|
|
|
|
import 'dart:developer'; |
|
|
|
|
|
|
|
|
|
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/data/repositories/problem_repository.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 ProblemRepository problemRepository; |
|
|
|
|
|
|
|
|
|
/// 最近问题列表 |
|
|
|
|
final RxList<Problem> problems = <Problem>[].obs; |
|
|
|
@ -26,9 +24,19 @@ class ProblemController extends GetxController
|
|
|
|
|
|
|
|
|
|
/// 未上传的问题列表 |
|
|
|
|
final RxList<Problem> unUploadedProblems = <Problem>[].obs; |
|
|
|
|
final RxList<Problem> selectedUnUploadedProblems = <Problem>[].obs; |
|
|
|
|
final Rx<bool> allSelected = false.obs; |
|
|
|
|
final RxBool isUploadEnabled = false.obs; |
|
|
|
|
final RxDouble uploadProgress = 0.0.obs; |
|
|
|
|
int get selectedCount { |
|
|
|
|
return unUploadedProblems |
|
|
|
|
.where((problem) => problem.isChecked.value) |
|
|
|
|
.length; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
List<Problem> get selectedUnUploadedProblems { |
|
|
|
|
return unUploadedProblems |
|
|
|
|
.where((problem) => problem.isChecked.value) |
|
|
|
|
.toList(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 筛选条件 |
|
|
|
|
final Rx<DateRange> selectedDateRange = DateRange.oneWeek.obs; |
|
|
|
@ -47,23 +55,15 @@ class ProblemController extends GetxController
|
|
|
|
|
final fabUploadPosition = Offset(337.0, 703.7).obs; |
|
|
|
|
|
|
|
|
|
/// get 选中的 |
|
|
|
|
RxBool get isOnline => connectivityProvider.isOnline; |
|
|
|
|
RxBool get isOnline => problemRepository.isOnline; |
|
|
|
|
|
|
|
|
|
ProblemController({ |
|
|
|
|
required this.localDatabase, |
|
|
|
|
required this.httpProvider, |
|
|
|
|
required this.connectivityProvider, |
|
|
|
|
}); |
|
|
|
|
ProblemController({required this.problemRepository}); |
|
|
|
|
|
|
|
|
|
@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(); |
|
|
|
@ -75,45 +75,87 @@ class ProblemController extends GetxController
|
|
|
|
|
super.onClose(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// #region 问题上传 |
|
|
|
|
/// 当单个问题的选中状态改变时调用 |
|
|
|
|
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 onProblemCheckedChange() {} |
|
|
|
|
|
|
|
|
|
/// 选择全部问题 |
|
|
|
|
void selectAll() { |
|
|
|
|
final bool newState = !allSelected.value; |
|
|
|
|
for (var problem in unUploadedProblems) { |
|
|
|
|
problem.isChecked.value = newState; |
|
|
|
|
} |
|
|
|
|
allSelected.value = newState; |
|
|
|
|
_updateSelectedList(); |
|
|
|
|
// _updateSelectedList(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void uploadProblems() { |
|
|
|
|
if (selectedUnUploadedProblems.isEmpty) return; |
|
|
|
|
// 实际的上传逻辑,例如调用 API |
|
|
|
|
void uploadProblems() async { |
|
|
|
|
// if (selectedUnUploadedProblems.isEmpty) return; |
|
|
|
|
// // 实际的上传逻辑,例如调用 API |
|
|
|
|
print('开始上传 ${selectedUnUploadedProblems.length} 个问题...'); |
|
|
|
|
// 上传完成后,清空列表或更新状态 |
|
|
|
|
selectedUnUploadedProblems.clear(); |
|
|
|
|
// // 上传完成后,清空列表或更新状态 |
|
|
|
|
// selectedUnUploadedProblems.clear(); |
|
|
|
|
for (var problem in selectedUnUploadedProblems) { |
|
|
|
|
await uploadProblem(problem); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<bool> 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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// #endregion |
|
|
|
|
|
|
|
|
|
// #region 悬浮按钮 |
|
|
|
|
/// floatingButton更新位置 |
|
|
|
|
void updateFabUploadPosition(Offset delta) { |
|
|
|
|
final screenWidth = ScreenUtil().screenWidth; |
|
|
|
@ -158,6 +200,9 @@ class ProblemController extends GetxController
|
|
|
|
|
fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// #endregion |
|
|
|
|
|
|
|
|
|
// #region ta按钮 |
|
|
|
|
void _onTabChanged() { |
|
|
|
|
if (!tabController.indexIsChanging) { |
|
|
|
|
selectedDateRange.value = DateRange.oneWeek; |
|
|
|
@ -166,7 +211,9 @@ class ProblemController extends GetxController
|
|
|
|
|
loadProblems(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// #endregion |
|
|
|
|
|
|
|
|
|
/// 加载 |
|
|
|
|
Future<void> loadProblems() async { |
|
|
|
|
isLoading.value = true; |
|
|
|
|
try { |
|
|
|
@ -188,7 +235,7 @@ class ProblemController extends GetxController
|
|
|
|
|
: '全部'; |
|
|
|
|
|
|
|
|
|
// 只执行一次数据库查询 |
|
|
|
|
final loadedProblems = await localDatabase.getProblems( |
|
|
|
|
final loadedProblems = await problemRepository.getProblems( |
|
|
|
|
startDate: startDate, |
|
|
|
|
endDate: endDate, |
|
|
|
|
uploadStatus: uploadStatus, |
|
|
|
@ -233,7 +280,7 @@ class ProblemController extends GetxController
|
|
|
|
|
isLoading.value = true; |
|
|
|
|
try { |
|
|
|
|
// 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 |
|
|
|
|
unUploadedProblems.value = await localDatabase.getProblems( |
|
|
|
|
unUploadedProblems.value = await problemRepository.getProblems( |
|
|
|
|
uploadStatus: '未上传', |
|
|
|
|
); |
|
|
|
|
} catch (e) { |
|
|
|
@ -245,7 +292,7 @@ class ProblemController extends GetxController
|
|
|
|
|
|
|
|
|
|
Future<void> addProblem(Problem problem) async { |
|
|
|
|
try { |
|
|
|
|
await localDatabase.insertProblem(problem); |
|
|
|
|
await problemRepository.insertProblem(problem); |
|
|
|
|
loadProblems(); |
|
|
|
|
} catch (e) { |
|
|
|
|
Get.snackbar('错误', '保存问题失败: $e'); |
|
|
|
@ -255,7 +302,7 @@ class ProblemController extends GetxController
|
|
|
|
|
|
|
|
|
|
Future<void> updateProblem(Problem problem) async { |
|
|
|
|
try { |
|
|
|
|
await localDatabase.updateProblem(problem); |
|
|
|
|
await problemRepository.updateProblem(problem); |
|
|
|
|
loadProblems(); |
|
|
|
|
} catch (e) { |
|
|
|
|
Get.snackbar('错误', '更新问题失败: $e'); |
|
|
|
@ -266,7 +313,7 @@ class ProblemController extends GetxController
|
|
|
|
|
Future<void> deleteProblem(Problem problem) async { |
|
|
|
|
try { |
|
|
|
|
if (problem.id != null) { |
|
|
|
|
await localDatabase.deleteProblem(problem.id!); |
|
|
|
|
await problemRepository.deleteProblem(problem.id!); |
|
|
|
|
await _deleteProblemImages(problem); |
|
|
|
|
loadProblems(); |
|
|
|
|
} |
|
|
|
@ -276,26 +323,6 @@ class ProblemController extends GetxController
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<void> 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<void> _deleteProblemImages(Problem problem) async { |
|
|
|
|
for (var imagePath in problem.imageUrls) { |
|
|
|
|
try { |
|
|
|
@ -309,109 +336,24 @@ class ProblemController extends GetxController
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<bool> 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<void> 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<void> selectDateRange(BuildContext context) async { |
|
|
|
|
final initialDateRange = DateTimeRange( |
|
|
|
|
start: DateTime.now().subtract(const Duration(days: 7)), |
|
|
|
|
end: DateTime.now(), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
Future<void> 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'); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
final DateTimeRange? picked = await showDateRangePicker( |
|
|
|
|
context: context, |
|
|
|
|
firstDate: DateTime(2025, 8, 1), // 可选的最早日期 |
|
|
|
|
lastDate: DateTime(2101), // 可选的最晚日期 |
|
|
|
|
initialDateRange: initialDateRange, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
void clearSelections() { |
|
|
|
|
for (var problem in historyProblems) { |
|
|
|
|
problem.isChecked.value = false; |
|
|
|
|
if (picked != null) { |
|
|
|
|
// 处理用户选择的日期范围 |
|
|
|
|
log('选择的日期范围是: ${picked.start} 到 ${picked.end}'); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|