|
|
|
// 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<Problem> problems = <Problem>[].obs;
|
|
|
|
|
|
|
|
/// 历史问题列表
|
|
|
|
final RxList<Problem> historyProblems = <Problem>[].obs;
|
|
|
|
|
|
|
|
/// 未上传的问题列表
|
|
|
|
final RxList<Problem> unUploadedProblems = <Problem>[].obs;
|
|
|
|
final RxList<Problem> selectedUnUploadedProblems = <Problem>[].obs;
|
|
|
|
final Rx<bool> allSelected = false.obs;
|
|
|
|
final RxBool isUploadEnabled = false.obs;
|
|
|
|
|
|
|
|
/// 筛选条件
|
|
|
|
final Rx<DateRange> 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<void> 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<void> loadUnUploadedProblems() async {
|
|
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
|
|
// 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题
|
|
|
|
unUploadedProblems.value = await localDatabase.getProblems(
|
|
|
|
uploadStatus: '未上传',
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
Get.snackbar('错误', '加载未上传问题失败: $e');
|
|
|
|
} finally {
|
|
|
|
isLoading.value = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 = 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 {
|
|
|
|
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 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> 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|