|
|
|
// 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/models/sync_status.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<Problem> problems = <Problem>[].obs;
|
|
|
|
|
|
|
|
/// 历史问题列表
|
|
|
|
final RxList<Problem> historyProblems = <Problem>[].obs;
|
|
|
|
|
|
|
|
/// 未上传的问题列表
|
|
|
|
final RxList<Problem> unUploadedProblems = <Problem>[].obs;
|
|
|
|
final Rx<bool> allSelected = false.obs;
|
|
|
|
final RxDouble uploadProgress = 0.0.obs;
|
|
|
|
// Dio 的取消令牌,用于取消正在进行的请求
|
|
|
|
late CancelToken _cancelToken;
|
|
|
|
|
|
|
|
final RxSet<Problem> _selectedProblems = <Problem>{}.obs;
|
|
|
|
|
|
|
|
Set<Problem> get selectedProblems => _selectedProblems;
|
|
|
|
|
|
|
|
int get selectedCount => _selectedProblems.length;
|
|
|
|
|
|
|
|
/// 选中未上传的数量
|
|
|
|
int get selectedUnUploadCount => _selectedProblems
|
|
|
|
.where((p) => p.syncStatus == SyncStatus.notSynced)
|
|
|
|
.length;
|
|
|
|
|
|
|
|
// 在 ProblemController 中添加
|
|
|
|
// 添加日期范围选项列表
|
|
|
|
List<DropdownOption> get dateRangeOptions {
|
|
|
|
return DateRange.values.map((range) => range.toDropdownOption()).toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
final List<DropdownOption> 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<DropdownOption> bindOptions = const [
|
|
|
|
DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive),
|
|
|
|
DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link),
|
|
|
|
DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off),
|
|
|
|
];
|
|
|
|
|
|
|
|
final Rx<DateRange> currentDateRange = DateRange.oneWeek.obs;
|
|
|
|
final RxString currentUploadFilter = '全部'.obs;
|
|
|
|
final RxString currentBindFilter = '全部'.obs;
|
|
|
|
|
|
|
|
// 历史问题列表筛选条件
|
|
|
|
final Rx<DateTime> historyStartTime = DateTime.now()
|
|
|
|
.subtract(const Duration(days: 7))
|
|
|
|
.obs;
|
|
|
|
final Rx<DateTime> 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<void> 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<Color>(Colors.blue),
|
|
|
|
),
|
|
|
|
SizedBox(height: 16.h),
|
|
|
|
// Text('已完成: $progress%'),
|
|
|
|
Text('已上传: $selectedUnUploadCount / $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 updateCurrentDateRange(String rangeValue) {
|
|
|
|
final newRange = rangeValue.toDateRange();
|
|
|
|
if (newRange != null) {
|
|
|
|
currentDateRange.value = newRange;
|
|
|
|
loadProblems(); // 重新加载数据
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateCurrentUpload(String value) {
|
|
|
|
currentUploadFilter.value = value;
|
|
|
|
loadProblems(); // 重新加载数据
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateCurrentBind(String value) {
|
|
|
|
currentBindFilter.value = value;
|
|
|
|
loadProblems(); // 重新加载数据
|
|
|
|
}
|
|
|
|
|
|
|
|
// 添加筛选方法
|
|
|
|
/// 显示日期选择器
|
|
|
|
Future<void> selectDateRange(BuildContext context) async {
|
|
|
|
final initialDateRange = DateTimeRange(
|
|
|
|
start: historyStartTime.value,
|
|
|
|
end: historyEndTime.value,
|
|
|
|
);
|
|
|
|
|
|
|
|
final DateTimeRange? picked = await showDateRangePicker(
|
|
|
|
context: context,
|
|
|
|
firstDate: DateTime(2025, 8, 1), // 可选的最早日期
|
|
|
|
lastDate: DateTime(2101), // 可选的最晚日期
|
|
|
|
initialDateRange: initialDateRange,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (picked != null) {
|
|
|
|
// 处理用户选择的日期范围
|
|
|
|
historyStartTime.value = picked.start;
|
|
|
|
historyEndTime.value = picked.end;
|
|
|
|
loadProblems();
|
|
|
|
log('选择的日期范围是: ${picked.start} 到 ${picked.end}');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateHistoryUpload(String value) {
|
|
|
|
historyUploadFilter.value = value;
|
|
|
|
loadProblems(); // 重新加载数据
|
|
|
|
}
|
|
|
|
|
|
|
|
void updateHistoryBind(String value) {
|
|
|
|
historyBindFilter.value = value;
|
|
|
|
loadProblems(); // 重新加载数据
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 加载
|
|
|
|
Future<void> 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,
|
|
|
|
syncStatus: uploadStatus,
|
|
|
|
bindStatus: bindStatus,
|
|
|
|
);
|
|
|
|
|
|
|
|
// 根据 Tab 索引将数据分配给正确的列表
|
|
|
|
if (isProblemListTab) {
|
|
|
|
problems.assignAll(loadedProblems);
|
|
|
|
} else {
|
|
|
|
historyProblems.assignAll(loadedProblems);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
Get.snackbar('错误', '加载问题失败: $e');
|
|
|
|
} finally {
|
|
|
|
isLoading.value = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 显示上传页面
|
|
|
|
void showUploadPage() {
|
|
|
|
Get.toNamed(AppRoutes.problemUpload);
|
|
|
|
loadUnUploadedProblems();
|
|
|
|
}
|
|
|
|
|
|
|
|
// 新增方法:查询所有未上传的问题
|
|
|
|
Future<void> loadUnUploadedProblems() async {
|
|
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
|
|
// 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题
|
|
|
|
unUploadedProblems.value = await problemRepository.getProblemsForSync();
|
|
|
|
} catch (e) {
|
|
|
|
Get.snackbar('错误', '加载未上传问题失败: $e');
|
|
|
|
} finally {
|
|
|
|
isLoading.value = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Future<void> addProblem(Problem problem) async {
|
|
|
|
// try {
|
|
|
|
// await problemRepository.insertProblem(problem);
|
|
|
|
// loadProblems();
|
|
|
|
// } catch (e) {
|
|
|
|
// Get.snackbar('错误', '保存问题失败: $e');
|
|
|
|
// rethrow;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// Future<void> updateProblem(Problem problem) async {
|
|
|
|
// try {
|
|
|
|
// await problemRepository.updateProblem(problem);
|
|
|
|
// loadProblems();
|
|
|
|
// } catch (e) {
|
|
|
|
// Get.snackbar('错误', '更新问题失败: $e');
|
|
|
|
// rethrow;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
Future<void> 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<void> _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<void> toProblemFormPageAndRefresh() async {
|
|
|
|
await Get.toNamed(AppRoutes.problemForm);
|
|
|
|
loadProblems();
|
|
|
|
}
|
|
|
|
}
|