From a2b8abd934612cdf64bbc4e361d5af9d29cb9d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Tue, 26 Aug 2025 16:32:34 +0800 Subject: [PATCH] =?UTF-8?q?fate=20:=20=E6=82=AC=E6=B5=AE=E8=B4=B4=E9=9D=A0?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/problem_model.dart | 53 +-- lib/data/providers/local_database.dart | 66 ++- lib/main.dart | 2 + .../controllers/problem_controller.dart | 181 +++++-- .../controllers/problem_form_controller.dart | 8 +- .../problem/views/problem_list_page.dart | 160 +++++-- lib/modules/problem/views/problem_page.dart | 449 +++++++++--------- .../widgets/custom_data_range_dropdown.dart | 84 ++++ .../views/widgets/custom_string_dropdown.dart | 51 ++ .../views/widgets/date_picker_button.dart | 76 --- .../problem/views/widgets/problem_card.dart | 2 +- 11 files changed, 721 insertions(+), 411 deletions(-) create mode 100644 lib/modules/problem/views/widgets/custom_data_range_dropdown.dart create mode 100644 lib/modules/problem/views/widgets/custom_string_dropdown.dart delete mode 100644 lib/modules/problem/views/widgets/date_picker_button.dart diff --git a/lib/data/models/problem_model.dart b/lib/data/models/problem_model.dart index 7bbd068..2a3f0b3 100644 --- a/lib/data/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -4,10 +4,11 @@ class Problem { String? id; String description; String location; - List imagePaths; + List imageUrls; DateTime createdAt; bool isUploaded; - String? boundInfo; + String? censorTaskId; + String? bindData; // 添加可观察的选中状态 final RxBool isChecked = false.obs; @@ -15,67 +16,63 @@ class Problem { this.id, required this.description, required this.location, - required this.imagePaths, + required this.imageUrls, required this.createdAt, + this.censorTaskId, + this.bindData, this.isUploaded = false, - this.boundInfo, }); - // copyWith 方法 + // copyWith 方法:根据字段和构造函数进行修正 Problem copyWith({ String? id, String? description, String? location, - List? imagePaths, + List? imageUrls, DateTime? createdAt, bool? isUploaded, - String? boundInfo, + String? censorTaskId, + String? bindData, }) { return Problem( id: id ?? this.id, description: description ?? this.description, location: location ?? this.location, - imagePaths: imagePaths ?? this.imagePaths, + imageUrls: imageUrls ?? this.imageUrls, createdAt: createdAt ?? this.createdAt, isUploaded: isUploaded ?? this.isUploaded, - boundInfo: boundInfo ?? this.boundInfo, + censorTaskId: censorTaskId ?? this.censorTaskId, + bindData: bindData ?? this.bindData, ); } - // 添加 toJson 方法 - Map toJson() { - return { - 'id': id, - 'description': description, - 'location': location, - 'imagePaths': imagePaths, // List 类型可以直接序列化 - 'createdAt': createdAt.toIso8601String(), - 'isUploaded': isUploaded, // bool 类型可以直接序列化 - 'boundInfo': boundInfo, - }; - } - + // toMap 方法:修正键名,确保与 fromMap 保持一致 Map toMap() { return { 'id': id, 'description': description, 'location': location, - 'imagePaths': imagePaths.join(';;'), - 'createdAt': createdAt.toIso8601String(), + 'imageUrls': imageUrls.join(';;'), // 使用正确的键名 'imageUrls' + 'createdAt': createdAt.millisecondsSinceEpoch, 'isUploaded': isUploaded ? 1 : 0, - 'boundInfo': boundInfo, + 'censorTaskId': censorTaskId, + 'bindData': bindData, // 使用正确的键名 'bindData' }; } + // fromMap 方法:修正键名,确保与 toMap 保持一致 factory Problem.fromMap(Map map) { return Problem( id: map['id'], description: map['description'], location: map['location'], - imagePaths: (map['imagePaths'] as String).split(';;'), - createdAt: DateTime.parse(map['createdAt']), + imageUrls: (map['imageUrls'] as String).split( + ';;', + ), // 使用正确的键名 'imageUrls' + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), isUploaded: map['isUploaded'] == 1, - boundInfo: map['boundInfo'], + censorTaskId: map['censorTaskId'], + bindData: map['bindData'], // 使用正确的键名 'bindData' ); } } diff --git a/lib/data/providers/local_database.dart b/lib/data/providers/local_database.dart index 0d43062..cfa407f 100644 --- a/lib/data/providers/local_database.dart +++ b/lib/data/providers/local_database.dart @@ -7,6 +7,7 @@ class LocalDatabase { static final LocalDatabase _instance = LocalDatabase._internal(); factory LocalDatabase() => _instance; static Database? _database; + static const String _tableName = 'problems'; LocalDatabase._internal(); @@ -23,36 +24,29 @@ class LocalDatabase { Future _onCreate(Database db, int version) async { await db.execute(''' - CREATE TABLE problems( + CREATE TABLE $_tableName( id TEXT PRIMARY KEY, description TEXT NOT NULL, location TEXT NOT NULL, - imagePaths TEXT NOT NULL, - createdAt TEXT NOT NULL, + imageUrls TEXT NOT NULL, + createdAt INTEGER NOT NULL, isUploaded INTEGER NOT NULL, - boundInfo TEXT + censorTaskId TEXT, + bindData TEXT ) '''); } Future insertProblem(Problem problem) async { final db = await database; - problem.id = Uuid().v4(); - return await db.insert('problems', problem.toMap()); - } - - Future> getProblems() async { - final db = await database; - final List> maps = await db.query('problems'); - return List.generate(maps.length, (i) { - return Problem.fromMap(maps[i]); - }); + problem.id = const Uuid().v4(); + return await db.insert(_tableName, problem.toMap()); } Future updateProblem(Problem problem) async { final db = await database; return await db.update( - 'problems', + _tableName, problem.toMap(), where: 'id = ?', whereArgs: [problem.id], @@ -61,16 +55,48 @@ class LocalDatabase { Future deleteProblem(String id) async { final db = await database; - return await db.delete('problems', where: 'id = ?', whereArgs: [id]); + return await db.delete(_tableName, where: 'id = ?', whereArgs: [id]); } - Future> getUnuploadedProblems() async { + /// 通用查询方法,根据时间范围、上传状态和绑定状态(censorTaskId)筛选问题。 + Future> getProblems({ + required DateTime startDate, + required DateTime endDate, + required String uploadStatus, // '已上传', '未上传', '全部' + required String bindStatus, // '已绑定', '未绑定', '全部' + }) async { final db = await database; + final int startTimestamp = startDate.millisecondsSinceEpoch; + final int endTimestamp = endDate.millisecondsSinceEpoch; + + final List whereClauses = ['createdAt >= ?', 'createdAt <= ?']; + final List whereArgs = [startTimestamp, endTimestamp]; + + // 根据上传状态添加筛选条件 + if (uploadStatus == '已上传') { + whereClauses.add('isUploaded = ?'); + whereArgs.add(1); + } else if (uploadStatus == '未上传') { + whereClauses.add('isUploaded = ?'); + whereArgs.add(0); + } + + // 根据 censorTaskId 的值判断绑定状态 + if (bindStatus == '已绑定') { + whereClauses.add('censorTaskId IS NOT NULL'); + } else if (bindStatus == '未绑定') { + whereClauses.add('censorTaskId IS NULL'); + } + + final String whereString = whereClauses.join(' AND '); + final List> maps = await db.query( - 'problems', - where: 'isUploaded = ?', - whereArgs: [0], + _tableName, + where: whereString.isEmpty ? null : whereString, + whereArgs: whereArgs.isEmpty ? null : whereArgs, + orderBy: 'createdAt DESC', ); + return List.generate(maps.length, (i) { return Problem.fromMap(maps[i]); }); diff --git a/lib/main.dart b/lib/main.dart index bdce5c4..dfd3e7d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,8 @@ void main() async { ]); // 初始化 GetStorage,这是关键步骤 await GetStorage.init(); + // Add this line + await ScreenUtil.ensureScreenSize(); runApp(const MainApp()); } diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index 7a39792..7514c0b 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -1,19 +1,31 @@ // 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 '../../../data/models/problem_model.dart'; -import '../../../data/providers/local_database.dart'; -import '../../../data/providers/connectivity_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 { +class ProblemController extends GetxController + with GetSingleTickerProviderStateMixin { final LocalDatabase _localDatabase; final RxList problems = [].obs; + final RxList historyProblems = [].obs; + + final Rx 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, @@ -22,44 +34,151 @@ class ProblemController extends GetxController { _dio = dio, _connectivityProvider = connectivityProvider; - // 使用 provider 中的 isOnline RxBool get isOnline => _connectivityProvider.isOnline; List get selectedProblems { - return problems.where((p) => p.isChecked.value).toList(); + return historyProblems.where((p) => p.isChecked.value).toList(); } List 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 + final fabUploadPosition = Offset(301.w, 660.h).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(); } - Future loadProblems() async { + @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 { - problems.value = await _localDatabase.getProblems(); + 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'); - rethrow; } 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 addProblem(Problem problem) async { try { - if (problem.id == null) { - problem = problem.copyWith( - id: DateTime.now().millisecondsSinceEpoch.toString(), - ); - } await _localDatabase.insertProblem(problem); - problems.add(problem); + loadProblems(); } catch (e) { Get.snackbar('错误', '保存问题失败: $e'); rethrow; @@ -69,10 +188,7 @@ class ProblemController extends GetxController { Future updateProblem(Problem problem) async { try { await _localDatabase.updateProblem(problem); - final index = problems.indexWhere((p) => p.id == problem.id); - if (index != -1) { - problems[index] = problem; - } + loadProblems(); } catch (e) { Get.snackbar('错误', '更新问题失败: $e'); rethrow; @@ -83,8 +199,8 @@ class ProblemController extends GetxController { try { if (problem.id != null) { await _localDatabase.deleteProblem(problem.id!); - problems.remove(problem); await _deleteProblemImages(problem); + loadProblems(); } } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); @@ -100,16 +216,20 @@ class ProblemController extends GetxController { } try { for (var problem in problemsToDelete) { - await deleteProblem(problem); + if (problem.id != null) { + await _localDatabase.deleteProblem(problem.id!); + await _deleteProblemImages(problem); + } } Get.snackbar('成功', '已删除${problemsToDelete.length}个问题'); + loadProblems(); } catch (e) { Get.snackbar('错误', '删除问题失败: $e'); } } Future _deleteProblemImages(Problem problem) async { - for (var imagePath in problem.imagePaths) { + for (var imagePath in problem.imageUrls) { try { final file = File(imagePath); if (await file.exists()) { @@ -127,18 +247,18 @@ class ProblemController extends GetxController { 'description': problem.description, 'location': problem.location, 'createdAt': problem.createdAt.toIso8601String(), - 'boundInfo': problem.boundInfo ?? '', + 'boundInfo': problem.bindData ?? '', }); - for (var imagePath in problem.imagePaths) { - final file = File(imagePath); + for (var imageUrl in problem.imageUrls) { + final file = File(imageUrl); if (await file.exists()) { formData.files.add( MapEntry( 'images', await MultipartFile.fromFile( - imagePath, - filename: path.basename(imagePath), + imageUrl, + filename: path.basename(imageUrl), ), ), ); @@ -207,12 +327,13 @@ class ProblemController extends GetxController { if (successCount < unuploaded.length) { Get.snackbar('部分成功', '有${unuploaded.length - successCount}个问题上传失败'); } + loadProblems(); } Future bindInfoToProblem(String id, String info) async { try { - final problem = problems.firstWhere((p) => p.id == id); - final updatedProblem = problem.copyWith(boundInfo: info); + final problem = historyProblems.firstWhere((p) => p.id == id); + final updatedProblem = problem.copyWith(bindData: info); await updateProblem(updatedProblem); Get.snackbar('成功', '信息已绑定'); } catch (e) { @@ -221,7 +342,7 @@ class ProblemController extends GetxController { } void clearSelections() { - for (var problem in problems) { + for (var problem in historyProblems) { problem.isChecked.value = false; } } diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart index a3117c8..a277577 100644 --- a/lib/modules/problem/controllers/problem_form_controller.dart +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -34,7 +34,7 @@ class ProblemFormController extends GetxController { // 清空旧图片,并加载新图片的路径 selectedImages.clear(); - for (var path in problem.imagePaths) { + for (var path in problem.imageUrls) { selectedImages.add(XFile(path)); } } else { @@ -151,7 +151,7 @@ class ProblemFormController extends GetxController { final updatedProblem = _currentProblem!.copyWith( description: descriptionController.text, location: locationController.text, - imagePaths: imagePaths, + imageUrls: imagePaths, createdAt: DateTime.now(), // 更新编辑时间 ); @@ -163,7 +163,7 @@ class ProblemFormController extends GetxController { final problem = Problem( description: descriptionController.text, location: locationController.text, - imagePaths: imagePaths, + imageUrls: imagePaths, createdAt: DateTime.now(), isUploaded: false, ); @@ -250,7 +250,7 @@ class ProblemFormController extends GetxController { // 编辑模式:检查与原始值的差异 return descriptionController.text != _currentProblem!.description || locationController.text != _currentProblem!.location || - selectedImages.length != _currentProblem!.imagePaths.length; + selectedImages.length != _currentProblem!.imageUrls.length; } @override diff --git a/lib/modules/problem/views/problem_list_page.dart b/lib/modules/problem/views/problem_list_page.dart index 4aa4d04..ac97474 100644 --- a/lib/modules/problem/views/problem_list_page.dart +++ b/lib/modules/problem/views/problem_list_page.dart @@ -1,43 +1,129 @@ -// import 'package:flutter/material.dart'; -// import 'package:get/get.dart'; -// import 'package:tdesign_flutter/tdesign_flutter.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; +import 'package:problem_check_system/data/models/problem_model.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; -// class DatePickerController extends GetxController { -// var selectedDateTime = ''.obs; // Reactive variable for selected date-time +class ProblemListPage extends GetView { + // 定义通用组件的参数 + final RxList problemsToShow; + final ProblemCardViewType viewType; -// void updateDateTime(Map selected) { -// selectedDateTime.value = -// '${selected['year'].toString().padLeft(4, '0')}-' -// '${selected['month'].toString().padLeft(2, '0')}-' -// '${selected['day'].toString().padLeft(2, '0')} ' -// '${selected['hour'].toString().padLeft(2, '0')}:' -// '${selected['minute'].toString().padLeft(2, '0')}:' -// '${selected['second'].toString().padLeft(2, '0')}'; -// } -// } + const ProblemListPage({ + super.key, + required this.problemsToShow, + this.viewType = ProblemCardViewType.buttons, + }); -// class DatePickerScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } -// DatePickerScreen({super.key}); + return ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 17.w), + itemCount: problemsToShow.length + 1, + itemBuilder: (context, index) { + if (index == problemsToShow.length) { + return SizedBox(height: 79.5.h); + } + final problem = problemsToShow[index]; + return _buildSwipeableProblemCard(problem); + }, + ); + }); + } -// @override -// Widget build(BuildContext context) { -// return GestureDetector( -// onTap: () {}, -// child: Obx( -// () => buildSelectRow( -// context, -// dateController.selectedDateTime.value, -// '选择时间', -// ), -// ), -// ); -// } + Widget _buildSwipeableProblemCard(Problem problem) { + // 只有在视图类型是 buttons 时,才启用滑动删除 + if (viewType == ProblemCardViewType.buttons) { + return Dismissible( + key: Key(problem.id ?? UniqueKey().toString()), + direction: DismissDirection.endToStart, + background: Container( + color: Colors.red, + alignment: Alignment.centerRight, + padding: EdgeInsets.only(right: 20.w), + child: Icon(Icons.delete, color: Colors.white, size: 30.sp), + ), + confirmDismiss: (direction) async { + return await _showDeleteConfirmationDialog(problem); + }, + onDismissed: (direction) { + controller.deleteProblem(problem); + Get.snackbar('成功', '问题已删除'); + }, + child: ProblemCard(problem, viewType: viewType), + ); + } else { + // 历史问题列表不启用滑动删除 + return ProblemCard(problem, viewType: viewType); + } + } -// Widget buildSelectRow(BuildContext context, String selected, String title) { -// return ListTile( -// title: Text(title), -// subtitle: Text(selected.isNotEmpty ? selected : '未选择'), -// ); -// } -// } + Future _showDeleteConfirmationDialog(Problem problem) async { + return await Get.bottomSheet( + Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 16), + const Text( + '确认删除', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + '确定要删除这个问题吗?此操作不可撤销。', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => Get.back(result: true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + '删除', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () => Get.back(result: false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + '取消', + style: TextStyle(color: Colors.grey[700], fontSize: 16), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ), + ) ?? + false; + } +} diff --git a/lib/modules/problem/views/problem_page.dart b/lib/modules/problem/views/problem_page.dart index 1173dbc..5af14a5 100644 --- a/lib/modules/problem/views/problem_page.dart +++ b/lib/modules/problem/views/problem_page.dart @@ -2,10 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; -import 'package:problem_check_system/data/models/problem_model.dart'; -import 'package:problem_check_system/modules/problem/views/widgets/date_picker_button.dart'; -import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; +import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; // 导入修正后的 ProblemListPage import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/custom_string_dropdown.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; // 导入自定义下拉菜单 class ProblemPage extends GetView { const ProblemPage({super.key}); @@ -16,241 +17,259 @@ class ProblemPage extends GetView { initialIndex: 0, length: 2, child: Scaffold( - body: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 812.h), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 375.w, - height: 83.5.h, - alignment: Alignment.bottomLeft, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], - ), + body: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 375.w, + height: 83.5.h, + alignment: Alignment.bottomLeft, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], ), - child: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - indicator: BoxDecoration( - color: const Color(0xfffff7f7), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8), - topRight: Radius.circular(60), - ), - ), - tabs: const [ - Tab(text: '问题列表'), - Tab(text: '历史问题列表'), - ], - labelStyle: TextStyle( - fontFamily: 'MyFont', - fontWeight: FontWeight.w800, - fontSize: 14.sp, - ), - unselectedLabelStyle: TextStyle( - fontFamily: 'MyFont', - fontWeight: FontWeight.w800, - fontSize: 14.sp, + ), + child: TabBar( + controller: controller.tabController, + indicatorSize: TabBarIndicatorSize.tab, + indicator: const BoxDecoration( + color: Color(0xfffff7f7), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(60), ), - labelColor: Colors.black, - unselectedLabelColor: Colors.white, ), + tabs: const [ + Tab(text: '问题列表'), + Tab(text: '历史问题列表'), + ], + labelStyle: TextStyle( + fontFamily: 'MyFont', + fontWeight: FontWeight.w800, + fontSize: 14.sp, + ), + unselectedLabelStyle: TextStyle( + fontFamily: 'MyFont', + fontWeight: FontWeight.w800, + fontSize: 14.sp, + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.white, ), - Expanded( - child: TabBarView( - children: [ - Column( - children: [ - Container( - margin: EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [DatePickerButton(), DatePickerButton()], - ), + ), + Expanded( + child: TabBarView( + controller: controller.tabController, + children: [ + // 问题列表 Tab + Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 17.w), + child: Row( + children: [ + CustomDateRangeDropdown( + selectedRange: controller.selectedDateRange, + onChanged: (rangeValue) { + controller.updateFiltersAndLoadProblems( + newDateRange: rangeValue, + ); + }, + ), + + CustomStringDropdown( + selectedValue: controller.selectedUploadStatus, + items: const ['全部', '未上传', '已上传'], + onChanged: (uploadValue) { + controller.updateFiltersAndLoadProblems( + newUploadStatus: uploadValue, + ); + }, + ), + + CustomStringDropdown( + selectedValue: controller.selectedBindingStatus, + items: const ['全部', '未绑定', '已绑定'], + onChanged: (bindingValue) { + controller.updateFiltersAndLoadProblems( + newBindingStatus: bindingValue, + ); + }, + ), + ], + ), + ), + + Expanded( + child: // 使用通用列表组件 + ProblemListPage( + problemsToShow: controller.problems, + viewType: ProblemCardViewType.buttons, ), - Expanded( - child: Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } + ), + ], + ), + // 历史问题列表 Tab + Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 17.w), + child: Row( + children: [ + CustomDateRangeDropdown( + selectedRange: controller.selectedDateRange, + onChanged: (rangeValue) { + controller.updateFiltersAndLoadProblems( + newDateRange: rangeValue, + ); + }, + ), - return ListView.builder( - // +1 是为了在列表末尾添加一个额外的“空”项 - itemCount: controller.problems.length + 1, - itemBuilder: (context, index) { - // 如果是最后一个“空”项,返回一个 SizedBox - if (index == controller.problems.length) { - return SizedBox(height: 79.5.h); - } + CustomStringDropdown( + selectedValue: controller.selectedUploadStatus, + items: const ['全部', '未上传', '已上传'], + onChanged: (uploadValue) { + controller.updateFiltersAndLoadProblems( + newUploadStatus: uploadValue, + ); + }, + ), - // 否则,返回正常的列表项 - final problem = controller.problems[index]; - return _buildSwipeableProblemCard( - problem, - controller, + CustomStringDropdown( + selectedValue: controller.selectedBindingStatus, + items: const ['全部', '未绑定', '已绑定'], + onChanged: (bindingValue) { + controller.updateFiltersAndLoadProblems( + newBindingStatus: bindingValue, ); }, - ); - }), + ), + ], ), - ], - ), - Obx(() { - if (controller.isLoading.value) { - return Center(child: CircularProgressIndicator()); - } + ), - return ListView.builder( - itemCount: controller.problems.length, - itemBuilder: (context, index) { - final problem = controller.problems[index]; - return _buildSwipeableProblemCard( - problem, - controller, - viewType: ProblemCardViewType.checkbox, - ); - }, - ); - }), - ], - ), + Expanded( + child: // 使用通用列表组件 + ProblemListPage( + problemsToShow: controller.problems, + viewType: ProblemCardViewType.buttons, + ), + ), + ], + ), + ], ), - ], - ), + ), + ], ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + // floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + // floatingActionButton: Stack( + // children: [ + // Align( + // alignment: Alignment.bottomCenter, + // child: FloatingActionButton( + // heroTag: "btn_add", + // onPressed: () { + // Get.to(() => ProblemFormPage()); + // }, + // shape: const CircleBorder(), + // backgroundColor: Colors.blue[300], + // foregroundColor: Colors.white, + // child: const Icon(Icons.add), + // ), + // ), + // Positioned( + // right: controller.fabPosition.value.dx, //27.w, + // bottom: controller.fabPosition.value.dy, //56.h, + // child: Obx(() { + // final bool isOnline = controller.isOnline.value; + // return GestureDetector( + // onPanUpdate: (details) { + // // 调用控制器中的方法来更新位置 + // controller.updatePosition(details.delta); + // }, + // child: FloatingActionButton( + // heroTag: "btn_upload", + // onPressed: isOnline + // ? () => controller.uploadAllUnuploaded() + // : null, + // foregroundColor: Colors.white, + // backgroundColor: isOnline + // ? Colors.red[300] + // : Colors.grey[400], + // child: Icon( + // isOnline + // ? Icons.file_upload_outlined + // : Icons.cloud_off_outlined, + // ), + // ), + // ); + // }), + // ), + // ], + // ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + // 使用 Stack 统一管理所有浮动按钮 floatingActionButton: Stack( children: [ + // 固定位置的 "添加" 按钮 + // 使用 Align 和 Positioned Align( alignment: Alignment.bottomCenter, - child: FloatingActionButton( - heroTag: "btn_add", - onPressed: () { - Get.to(() => ProblemFormPage()); - }, - shape: CircleBorder(), - backgroundColor: Colors.blue[300], - foregroundColor: Colors.white, - child: const Icon(Icons.add), + child: Padding( + padding: EdgeInsets.only(bottom: 24.h), // 底部间距 + child: FloatingActionButton( + heroTag: "btn_add", + onPressed: () { + Get.to(() => ProblemFormPage()); + }, + shape: const CircleBorder(), + backgroundColor: Colors.blue[300], + foregroundColor: Colors.white, + child: const Icon(Icons.add), + ), ), ), - Positioned( - bottom: 56.h, // 设置距离底部100像素 - right: 27.w, // 设置距离右侧16像素(通常的浮动按钮默认位置) - child: Obx(() { - final bool isOnline = controller.isOnline.value; - return FloatingActionButton( - heroTag: "btn_upload", - onPressed: isOnline - ? () => controller.uploadAllUnuploaded() - : null, - foregroundColor: Colors.white, - backgroundColor: isOnline - ? Colors.red[300] - : Colors.grey[400], - child: Icon( - isOnline - ? Icons.file_upload_outlined - : Icons.cloud_off_outlined, + + // 可拖动的 "上传" 按钮 + Obx(() { + final isOnline = controller.isOnline.value; + return Positioned( + // 使用正确的坐标,left/right 对应 dx,top/bottom 对应 dy + left: controller.fabUploadPosition.value.dx, + top: controller.fabUploadPosition.value.dy, + child: GestureDetector( + onPanUpdate: (details) { + // 调用控制器中的方法来更新位置 + controller.updateFabUploadPosition(details.delta); + }, + onPanEnd: (details) { + // 拖动结束后调用吸附方法 + controller.snapToEdge(); + }, + child: FloatingActionButton( + heroTag: "btn_upload", + onPressed: isOnline + ? () => controller.uploadAllUnuploaded() + : null, + foregroundColor: Colors.white, + backgroundColor: isOnline + ? Colors.red[300] + : Colors.grey[400], + child: Icon( + isOnline + ? Icons.file_upload_outlined + : Icons.cloud_off_outlined, + ), ), - ); - }), - ), + ), + ); + }), ], ), ), ); } - - Widget _buildSwipeableProblemCard( - Problem problem, - ProblemController controller, { - ProblemCardViewType viewType = ProblemCardViewType.buttons, - }) { - return Dismissible( - key: Key(problem.id ?? UniqueKey().toString()), - direction: DismissDirection.endToStart, - background: Container( - color: Colors.red, - alignment: Alignment.centerRight, - padding: EdgeInsets.only(right: 20.w), - child: Icon(Icons.delete, color: Colors.white, size: 30.sp), - ), - confirmDismiss: (direction) async { - return await _showDeleteConfirmationDialog(problem); - }, - onDismissed: (direction) { - controller.deleteProblem(problem); - Get.snackbar('成功', '问题已删除'); - }, - child: ProblemCard(problem, viewType: viewType), - ); - } - - Future _showDeleteConfirmationDialog(Problem problem) async { - return await Get.bottomSheet( - Container( - padding: EdgeInsets.symmetric(horizontal: 16.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SizedBox(height: 16), - Text( - '确认删除', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - '确定要删除这个问题吗?此操作不可撤销。', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - ), - SizedBox(height: 24), - ElevatedButton( - onPressed: () => Get.back(result: true), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - padding: EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: Text( - '删除', - style: TextStyle(color: Colors.white, fontSize: 16), - ), - ), - SizedBox(height: 8), - TextButton( - onPressed: () => Get.back(result: false), - style: TextButton.styleFrom( - padding: EdgeInsets.symmetric(vertical: 16), - ), - child: Text( - '取消', - style: TextStyle(color: Colors.grey[700], fontSize: 16), - ), - ), - SizedBox(height: 16), - ], - ), - ), - ), - isDismissible: false, - ) ?? - false; - } } diff --git a/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart b/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart new file mode 100644 index 0000000..153e55c --- /dev/null +++ b/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart @@ -0,0 +1,84 @@ +// custom_data_range_dropdown.dart +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +// 定义一个枚举来表示不同的日期范围 +enum DateRange { threeDays, oneWeek, oneMonth } + +// 定义一个扩展,让枚举更具可读性 +extension DateRangeExtension on DateRange { + String get name { + switch (this) { + case DateRange.threeDays: + return '近三天'; + case DateRange.oneWeek: + return '近一周'; + case DateRange.oneMonth: + return '近一月'; + } + } + + // 根据枚举值获取对应的开始日期 + DateTime get startDate { + final now = DateTime.now(); + switch (this) { + case DateRange.threeDays: + return now.subtract(const Duration(days: 3)); + case DateRange.oneWeek: + return now.subtract(const Duration(days: 7)); + case DateRange.oneMonth: + return now.subtract(const Duration(days: 30)); // 近一月通常按30天计算 + } + } +} + +class CustomDateRangeDropdown extends StatelessWidget { + final Rx selectedRange; + final Function(DateRange) onChanged; + + const CustomDateRangeDropdown({ + super.key, + required this.selectedRange, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + // 使用Container来创建边框和圆角 + return Obx( + () => Container( + height: 30.h, + margin: EdgeInsets.only(right: 10.w, top: 10.w, bottom: 10.w), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 12.5.w, vertical: 7.5.h), + decoration: BoxDecoration( + color: const Color.fromARGB(90, 158, 158, 158), + borderRadius: BorderRadius.circular(7.5.w), // 圆角 + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedRange.value, + onChanged: (DateRange? newValue) { + if (newValue != null) { + onChanged(newValue); + } + }, + items: DateRange.values.map((DateRange range) { + return DropdownMenuItem( + value: range, + child: Text(range.name), + ); + }).toList(), + icon: const Icon(Icons.arrow_drop_down, size: 15), // 下拉箭头图标 + // 样式调整 + style: TextStyle(fontSize: 10.sp, color: Colors.black), + dropdownColor: Colors.white, + // 移除默认的边距,让DropdownButton紧贴Container + underline: const SizedBox.shrink(), + ), + ), + ), + ); + } +} diff --git a/lib/modules/problem/views/widgets/custom_string_dropdown.dart b/lib/modules/problem/views/widgets/custom_string_dropdown.dart new file mode 100644 index 0000000..9afc1bc --- /dev/null +++ b/lib/modules/problem/views/widgets/custom_string_dropdown.dart @@ -0,0 +1,51 @@ +// custom_string_dropdown.dart + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class CustomStringDropdown extends StatelessWidget { + final RxString selectedValue; + final List items; + final Function(String) onChanged; + + const CustomStringDropdown({ + super.key, + required this.selectedValue, + required this.items, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Obx( + () => Container( + height: 30.h, + margin: EdgeInsets.only(right: 10.w, top: 10.w, bottom: 10.w), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 12.5.w, vertical: 7.5.h), + decoration: BoxDecoration( + color: const Color.fromARGB(90, 158, 158, 158), + borderRadius: BorderRadius.circular(7.5.w), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedValue.value, + onChanged: (String? newValue) { + if (newValue != null) { + onChanged(newValue); + } + }, + items: items.map((String value) { + return DropdownMenuItem(value: value, child: Text(value)); + }).toList(), + icon: const Icon(Icons.arrow_drop_down, size: 15), + style: TextStyle(fontSize: 10.sp, color: Colors.black), + dropdownColor: Colors.white, + underline: const SizedBox.shrink(), + ), + ), + ), + ); + } +} diff --git a/lib/modules/problem/views/widgets/date_picker_button.dart b/lib/modules/problem/views/widgets/date_picker_button.dart deleted file mode 100644 index 8c8b1c5..0000000 --- a/lib/modules/problem/views/widgets/date_picker_button.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tdesign_flutter/tdesign_flutter.dart'; - -class DatePickerButton extends StatelessWidget { - DatePickerButton({super.key}); - final DatePickerController dateController = Get.put(DatePickerController()); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - elevation: 2, - ), - onPressed: () { - TDPicker.showDatePicker( - context, - title: '选择时间', - onConfirm: (selected) { - dateController.updateDateTime(selected); - Get.back(); - }, - useHour: true, - useMinute: true, - useSecond: true, - dateStart: [1999, 01, 01], - dateEnd: [2029, 12, 31], - initialDate: [2025, 1, 1], - ); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Obx( - () => Text( - dateController.selectedDateTime.value.isEmpty - ? "选择日期" - : dateController.selectedDateTime.value, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - ), - SizedBox(width: 8), - Icon(Icons.keyboard_arrow_down, color: Colors.black), - ], - ), - ), - ], - ); - } -} - -class DatePickerController extends GetxController { - var selectedDateTime = ''.obs; - - void updateDateTime(Map selected) { - selectedDateTime.value = - '${selected['year'].toString().padLeft(4, '0')}-' - '${selected['month'].toString().padLeft(2, '0')}-' - '${selected['day'].toString().padLeft(2, '0')} '; - // '${selected['hour'].toString().padLeft(2, '0')}:' - // '${selected['minute'].toString().padLeft(2, '0')}:' - // '${selected['second'].toString().padLeft(2, '0')}'; - } -} diff --git a/lib/modules/problem/views/widgets/problem_card.dart b/lib/modules/problem/views/widgets/problem_card.dart index 313fad3..e259925 100644 --- a/lib/modules/problem/views/widgets/problem_card.dart +++ b/lib/modules/problem/views/widgets/problem_card.dart @@ -79,7 +79,7 @@ class ProblemCard extends StatelessWidget { problem.isUploaded ? TDTag('已上传', isLight: true, theme: TDTagTheme.success) : TDTag('未上传', isLight: true, theme: TDTagTheme.danger), - problem.boundInfo != null && problem.boundInfo!.isNotEmpty + problem.bindData != null && problem.bindData!.isNotEmpty ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) : TDTag('未绑定', isLight: true, theme: TDTagTheme.warning), ],