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.
 
 
 
 
 
 

660 lines
20 KiB

// 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, Response;
import 'package:flutter/material.dart';
import 'package:problem_check_system/app/routes/app_routes.dart';
import 'package:problem_check_system/core/extensions/http_response_extension.dart';
import 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'package:problem_check_system/data/models/image_status.dart';
import 'package:problem_check_system/data/models/problem_sync_status.dart';
import 'package:problem_check_system/data/repositories/file_repository.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 FileRepository fileRepository = Get.find<FileRepository>();
/// 最近问题列表
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 != ProblemSyncStatus.synced)
.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(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
23,
59,
59,
999,
).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 uploadProblems(
_selectedProblems.toList(), // 转换为列表
cancelToken: _cancelToken,
onProgress: (progress) {
uploadProgress.value = progress;
},
);
Get.back();
Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM);
// 上传成功后清空选中状态
clearSelection();
// 重新加载未上传的问题列表
loadUnUploadedProblems();
// 重新加载problems
loadProblems();
} 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('提示', '上传已取消');
}
/// 新增:上传问题列表。
/// 遍历问题列表,并计算总进度。
Future<void> uploadProblems(
List<Problem> problems, {
required CancelToken cancelToken,
required void Function(double progress) onProgress,
}) async {
final int totalProblems = problems.length;
// final List<Problem> updatedProblems = [];
try {
for (int i = 0; i < totalProblems; i++) {
// 如果取消令牌被触发,停止并返回
if (cancelToken.isCancelled) {
break;
}
final problemToUpload = problems[i];
// 传递一个子进度回调,用于计算单个问题的进度
final updatedProblem = await uploadProblem(
problemToUpload,
cancelToken: cancelToken,
onProgress: (progress) {
// 计算总体进度:(已完成的问题数 + 当前问题的进度) / 总问题数
final overallProgress = (i + progress) / totalProblems;
onProgress(overallProgress);
},
);
if (updatedProblem.syncStatus == ProblemSyncStatus.untracked) {
problemRepository.deleteProblem(updatedProblem.id!);
} else {
problemRepository.updateProblem(updatedProblem);
}
}
// return updatedProblems;
} on DioException {
rethrow;
}
}
/// 上传单个问题及其所有关联的图片。
/// 上传单个问题及其所有关联的图片,根据操作类型执行不同逻辑
Future<Problem> uploadProblem(
Problem problem, {
required CancelToken cancelToken,
required void Function(double progress) onProgress,
}) async {
try {
// 检查操作类型有效性
if (problem.syncStatus == ProblemSyncStatus.synced ||
problem.syncStatus == ProblemSyncStatus.untracked) {
throw Exception('问题已同步,无需再次同步');
}
// 1. 上传图片(仅对创建和更新操作)
final List<String> remoteUrls = [];
if (problem.syncStatus != ProblemSyncStatus.pendingDelete) {
final newImages = problem.imageUrls
.where((img) => img.status == ImageStatus.local)
.toList();
final totalFilesToUpload = newImages.length;
int filesUploadedCount = 0;
for (var image in newImages) {
if (cancelToken.isCancelled) {
throw DioException(
requestOptions: RequestOptions(path: ''),
type: DioExceptionType.cancel,
error: '上传已取消',
);
}
final url = await fileRepository.uploadImage(
image.localPath,
cancelToken: cancelToken,
onSendProgress: (sent, total) {
double overallProgress =
(filesUploadedCount + (sent / total)) / totalFilesToUpload;
onProgress(overallProgress);
},
);
remoteUrls.add(url);
filesUploadedCount++;
}
onProgress(1.0);
}
// 2. 构建 API payload(删除操作不需要完整payload)
final apiPayload = problem.syncStatus != ProblemSyncStatus.pendingDelete
? {
'id': problem.id,
'title': problem.description,
'location': problem.location,
'imageUrls': _buildFinalRemoteUrls(problem.imageUrls, remoteUrls),
'creationTime': problem.creationTime.toUtc().toIso8601String(),
}
: null;
// 3. 根据操作类型调用不同的API
late final Response response;
switch (problem.syncStatus) {
case ProblemSyncStatus.untracked:
case ProblemSyncStatus.synced:
throw Exception('无效的操作类型: none');
case ProblemSyncStatus.pendingCreate:
response = await problemRepository.post(apiPayload!, cancelToken);
break;
case ProblemSyncStatus.pendingUpdate:
response = await problemRepository.put(
problem.id!,
apiPayload!,
cancelToken,
);
break;
case ProblemSyncStatus.pendingDelete:
response = await problemRepository.delete(problem.id!, cancelToken);
break;
}
// 4. 处理服务器响应
if (response.isSuccess) {
// 更新图片状态(仅对创建和更新操作)
final updatedImageMetadata =
problem.syncStatus != ProblemSyncStatus.pendingDelete
? _updateImageMetadata(problem.imageUrls, remoteUrls)
: problem.imageUrls;
// 返回同步完成的对象,操作类型重置为none
return problem.copyWith(
syncStatus: problem.syncStatus != ProblemSyncStatus.pendingDelete
? ProblemSyncStatus.synced
: ProblemSyncStatus.untracked, // 同步完成,重置为none
imageUrls: updatedImageMetadata,
);
} else {
throw Exception('操作失败,状态码: ${response.statusCode}');
}
} on DioException {
rethrow;
}
}
/// 辅助方法:构建最终的远程URL列表
List<String> _buildFinalRemoteUrls(
List<ImageMetadata> images,
List<String> newRemoteUrls,
) {
final List<String> finalRemoteUrls = [];
int newImageIndex = 0;
for (var image in images) {
if (image.status == ImageStatus.synced) {
finalRemoteUrls.add(image.remoteUrl!);
} else if (image.status == ImageStatus.local) {
finalRemoteUrls.add(newRemoteUrls[newImageIndex]);
newImageIndex++;
}
}
return finalRemoteUrls;
}
/// 辅助方法:更新图片元数据状态
List<ImageMetadata> _updateImageMetadata(
List<ImageMetadata> images,
List<String> newRemoteUrls,
) {
final List<ImageMetadata> updatedImageMetadata = [];
int uploadedUrlIndex = 0;
for (var image in images) {
if (image.status == ImageStatus.local) {
updatedImageMetadata.add(
ImageMetadata(
localPath: image.localPath,
remoteUrl: newRemoteUrls[uploadedUrlIndex],
status: ImageStatus.synced,
),
);
uploadedUrlIndex++;
} else {
updatedImageMetadata.add(image);
}
}
return updatedImageMetadata;
}
// #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 = DateTime(
picked.end.year,
picked.end.month,
picked.end.day,
23,
59,
59,
999,
);
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);
clearSelection();
loadUnUploadedProblems();
}
// 查询所有未上传的问题
Future<void> loadUnUploadedProblems() async {
isLoading.value = true;
try {
// 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题
unUploadedProblems.value = await problemRepository.getProblems(
syncStatus: '未上传',
);
} catch (e) {
Get.snackbar('错误', '加载未上传问题失败: $e');
} finally {
isLoading.value = false;
}
}
/// 删除问题
/// 控制器中可以添加逻辑
Future<void> deleteProblem(Problem problem) async {
try {
final deleteProblem = ProblemStateManager.markForDeletion(problem);
if (deleteProblem.syncStatus == ProblemSyncStatus.untracked) {
// 直接删除问题和图片
await problemRepository.deleteProblem(problem.id!);
await _deleteProblemImages(problem);
} else {
// 更新状态
await problemRepository.updateProblem(deleteProblem);
}
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({Problem? problem}) async {
await Get.toNamed(AppRoutes.problemForm, arguments: problem);
loadProblems();
}
}