You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
10 KiB
349 lines
10 KiB
// 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/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 RxList<Problem> problems = <Problem>[].obs; |
|
final RxList<Problem> historyProblems = <Problem>[].obs; |
|
|
|
final Rx<DateRange> selectedDateRange = DateRange.oneWeek.obs; |
|
final RxString selectedUploadStatus = '全部'.obs; |
|
final RxString selectedBindingStatus = '全部'.obs; |
|
|
|
final RxBool isLoading = false.obs; |
|
final Dio _dio; |
|
final ConnectivityProvider _connectivityProvider; |
|
|
|
late TabController tabController; |
|
|
|
ProblemController({ |
|
required LocalDatabase localDatabase, |
|
required Dio dio, |
|
required ConnectivityProvider connectivityProvider, |
|
}) : _localDatabase = localDatabase, |
|
_dio = dio, |
|
_connectivityProvider = connectivityProvider; |
|
|
|
RxBool get isOnline => _connectivityProvider.isOnline; |
|
|
|
List<Problem> get selectedProblems { |
|
return historyProblems.where((p) => p.isChecked.value).toList(); |
|
} |
|
|
|
List<Problem> get unuploadedProblems { |
|
return problems.where((p) => !p.isUploaded).toList(); |
|
} |
|
|
|
// 常量:FAB的尺寸和贴靠间距 |
|
final double _fabSize = 56.0; // FloatingActionButton的默认尺寸 |
|
final double _edgePaddingX = 27.0.w; // 边缘间距 |
|
final double _edgePaddingY = 111.0.h; // 边缘间距 |
|
// 可拖动按钮的位置,使用 Rx<Offset> |
|
final fabUploadPosition = Offset(337.0, 703.7).obs; |
|
|
|
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); |
|
} |
|
|
|
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); |
|
|
|
print(fabUploadPosition.value); |
|
} |
|
|
|
@override |
|
void onInit() { |
|
super.onInit(); |
|
tabController = TabController(length: 2, vsync: this); |
|
tabController.addListener(_onTabChanged); |
|
|
|
loadProblems(); |
|
} |
|
|
|
@override |
|
void onClose() { |
|
tabController.dispose(); |
|
super.onClose(); |
|
} |
|
|
|
void _onTabChanged() { |
|
if (!tabController.indexIsChanging) { |
|
selectedDateRange.value = DateRange.oneWeek; |
|
selectedUploadStatus.value = '全部'; |
|
selectedBindingStatus.value = '全部'; |
|
loadProblems(); |
|
} |
|
} |
|
|
|
void loadProblems() async { |
|
isLoading.value = true; |
|
try { |
|
if (tabController.index == 0) { |
|
// "问题列表" Tab: 使用日期范围和筛选条件 |
|
final startDate = selectedDateRange.value.startDate; |
|
final endDate = DateTime.now(); |
|
|
|
final problems = await _localDatabase.getProblems( |
|
startDate: startDate, |
|
endDate: endDate, |
|
uploadStatus: selectedUploadStatus.value, |
|
bindStatus: selectedBindingStatus.value, |
|
); |
|
this.problems.assignAll(problems); |
|
} else { |
|
// "历史问题列表" Tab: 查询所有问题 |
|
final allProblems = await _localDatabase.getProblems( |
|
startDate: DateTime(2000), |
|
endDate: DateTime.now(), |
|
uploadStatus: '全部', |
|
bindStatus: '全部', |
|
); |
|
this.historyProblems.assignAll(allProblems); |
|
} |
|
} 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 |
|
if (newDateRange != null || |
|
newUploadStatus != null || |
|
newBindingStatus != null) { |
|
loadProblems(); |
|
} |
|
} |
|
|
|
Future<void> addProblem(Problem problem) async { |
|
try { |
|
await _localDatabase.insertProblem(problem); |
|
loadProblems(); |
|
} catch (e) { |
|
Get.snackbar('错误', '保存问题失败: $e'); |
|
rethrow; |
|
} |
|
} |
|
|
|
Future<void> updateProblem(Problem problem) async { |
|
try { |
|
await _localDatabase.updateProblem(problem); |
|
loadProblems(); |
|
} catch (e) { |
|
Get.snackbar('错误', '更新问题失败: $e'); |
|
rethrow; |
|
} |
|
} |
|
|
|
Future<void> 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<void> deleteSelectedProblems() async { |
|
final problemsToDelete = selectedProblems; |
|
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 { |
|
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.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 _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 { |
|
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> 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; |
|
} |
|
} |
|
}
|
|
|