diff --git a/lib/data/models/operation.dart b/lib/data/models/operation.dart index cbe081d..7658b1a 100644 --- a/lib/data/models/operation.dart +++ b/lib/data/models/operation.dart @@ -8,18 +8,3 @@ enum Operation { /// 删除 delete, } - -/// Operation 枚举的扩展方法 -extension OperationExtension on Operation { - /// 获取操作的中文名 - String get displayName { - switch (this) { - case Operation.create: - return '创建'; - case Operation.update: - return '修改'; - case Operation.delete: - return '删除'; - } - } -} diff --git a/lib/data/models/problem_model.dart b/lib/data/models/problem_model.dart index 4d6b79d..eb6c8d0 100644 --- a/lib/data/models/problem_model.dart +++ b/lib/data/models/problem_model.dart @@ -1,9 +1,9 @@ -// problem_model.dart - import 'dart:convert'; + +import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'package:problem_check_system/data/models/operation.dart'; import 'package:problem_check_system/data/models/sync_status.dart'; -import 'package:problem_check_system/data/models/image_metadata_model.dart'; +import 'package:uuid/uuid.dart'; /// 问题的数据模型。 /// 用于表示系统中的一个具体问题,包含了问题的描述、位置、图片等信息。 @@ -29,6 +29,9 @@ class Problem { /// 对问题的操作类型,默认为创建。 final Operation operation; + /// 是否逻辑删除 + final bool isDeleted; + /// 相关的审查任务ID,可空。 final String? censorTaskId; @@ -38,8 +41,10 @@ class Problem { /// 问题是否已被检查,默认为false。 final bool isChecked; - /// Problem 类的构造函数。 - const Problem({ + /// 静态对象uuid + static final Uuid _uuid = Uuid(); + + Problem({ this.id, required this.description, required this.location, @@ -47,13 +52,28 @@ class Problem { required this.creationTime, this.syncStatus = SyncStatus.notSynced, this.operation = Operation.create, + this.isDeleted = false, this.censorTaskId, this.bindData, this.isChecked = false, }); - /// 创建一个新实例,并可以选择性地更新某些字段。 - /// 采用不可变设计模式,返回一个新对象而不是修改现有对象。 + /// 工厂方法:创建新问题(自动生成ID) + factory Problem.create({ + required String description, + required String location, + required List imageUrls, + }) { + return Problem( + id: _uuid.v4(), + description: description, + location: location, + imageUrls: imageUrls, + creationTime: DateTime.now(), + ); + } + + /// copyWith 方法,用于创建对象的副本并修改指定字段 Problem copyWith({ String? id, String? description, @@ -62,6 +82,7 @@ class Problem { DateTime? creationTime, SyncStatus? syncStatus, Operation? operation, + bool? isDeleted, String? censorTaskId, String? bindData, bool? isChecked, @@ -74,50 +95,63 @@ class Problem { creationTime: creationTime ?? this.creationTime, syncStatus: syncStatus ?? this.syncStatus, operation: operation ?? this.operation, + isDeleted: isDeleted ?? this.isDeleted, censorTaskId: censorTaskId ?? this.censorTaskId, bindData: bindData ?? this.bindData, isChecked: isChecked ?? this.isChecked, ); } - /// 将 Problem 实例序列化为 Map,便于存储到数据库或发送到服务器。 + /// 将对象转换为Map,用于SQLite存储 Map toMap() { return { 'id': id, 'description': description, 'location': location, - // 使用 jsonEncode 将图片元数据列表转换为 JSON 字符串 - 'imageUrls': jsonEncode(imageUrls.map((meta) => meta.toMap()).toList()), + 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()), 'creationTime': creationTime.millisecondsSinceEpoch, 'syncStatus': syncStatus.index, - 'operation': operation.index, // 增加对 operation 字段的序列化 + 'operation': operation.index, + 'isDeleted': isDeleted ? 1 : 0, 'censorTaskId': censorTaskId, 'bindData': bindData, - 'isChecked': isChecked ? 1 : 0, // 增加对 isChecked 字段的序列化 + 'isChecked': isChecked ? 1 : 0, }; } - /// 从 Map 中反序列化创建一个 Problem 实例。 + /// 从Map创建对象,用于从SQLite读取 factory Problem.fromMap(Map map) { + // 处理imageUrls的转换 + List imageUrlsList = []; + if (map['imageUrls'] != null) { + try { + final List imageList = json.decode(map['imageUrls']); + imageUrlsList = imageList.map((e) => ImageMetadata.fromMap(e)).toList(); + } catch (e) { + // 如果解析失败,保持空列表 + imageUrlsList = []; + } + } + return Problem( id: map['id'], description: map['description'], location: map['location'], - // jsonDecode 将 JSON 字符串转换为列表,然后映射为 ImageMetadata 对象 - imageUrls: (jsonDecode(map['imageUrls']) as List) - .map((item) => ImageMetadata.fromMap(item as Map)) - .toList(), - creationTime: DateTime.fromMillisecondsSinceEpoch( - map['creationTime'] as int, - ), - syncStatus: SyncStatus.values[map['syncStatus'] as int], - operation: - map.containsKey('operation') // 检查 operation 字段是否存在 - ? Operation.values[map['operation'] as int] - : Operation.create, // 兼容旧数据 + imageUrls: imageUrlsList, + creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']), + syncStatus: SyncStatus.values[map['syncStatus']], + operation: Operation.values[map['operation']], + isDeleted: map['isDeleted'] == 1, censorTaskId: map['censorTaskId'], bindData: map['bindData'], - isChecked: (map['isChecked'] as int) == 1, // 增加对 isChecked 字段的反序列化 + isChecked: map['isChecked'] == 1, ); } + + /// 转换为JSON字符串 + String toJson() => json.encode(toMap()); + + /// 从JSON字符串创建对象 + factory Problem.fromJson(String source) => + Problem.fromMap(json.decode(source)); } diff --git a/lib/data/models/sync_status.dart b/lib/data/models/sync_status.dart index 9aa9451..4e6ed2e 100644 --- a/lib/data/models/sync_status.dart +++ b/lib/data/models/sync_status.dart @@ -5,17 +5,3 @@ enum SyncStatus { /// 未同步到服务器 notSynced, } - -// 添加更多扩展方法 -extension SyncStatusExtension on SyncStatus { - String get displayName { - switch (this) { - case SyncStatus.synced: - return '已上传'; - case SyncStatus.notSynced: - return '未上传'; - } - } - - bool get isSynced => this == SyncStatus.synced; -} diff --git a/lib/data/providers/sqlite_provider.dart b/lib/data/providers/sqlite_provider.dart index 5af096c..6eb2a95 100644 --- a/lib/data/providers/sqlite_provider.dart +++ b/lib/data/providers/sqlite_provider.dart @@ -5,28 +5,23 @@ import 'package:problem_check_system/data/models/sync_status.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; -import 'package:uuid/uuid.dart'; /// `SQLiteProvider` 是一个 GetxService,负责管理本地 SQLite 数据库。 /// 作为一个单例服务,它在整个应用生命周期中只会被创建一次。 class SQLiteProvider extends GetxService { static const String _dbName = 'problems.db'; static const String _tableName = 'problems'; - - /// 数据库版本号,用于处理表结构升级。 static const int _dbVersion = 1; - /// 私有数据库实例,仅在服务内部访问。 late Database _database; - /// 在服务首次初始化时调用,用于执行异步设置。 @override void onInit() { super.onInit(); _initDatabase(); } - /// 异步初始化数据库连接。如果数据库不存在,则会创建它;如果版本过旧,则会升级。 + /// 异步初始化数据库连接 Future _initDatabase() async { try { final databasePath = await getDatabasesPath(); @@ -36,16 +31,17 @@ class SQLiteProvider extends GetxService { path, version: _dbVersion, onCreate: _onCreate, - onUpgrade: _onUpgrade, // 新增:处理数据库版本升级 + onUpgrade: _onUpgrade, ); + + Get.log('数据库初始化成功'); } catch (e) { - // 在这里添加日志记录,例如 Get.log('数据库初始化失败:$e'); Get.log('数据库初始化失败:$e', isError: true); rethrow; } } - /// 数据库创建时的回调函数,用于定义表结构。 + /// 数据库创建时的回调函数 Future _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE $_tableName( @@ -56,126 +52,187 @@ class SQLiteProvider extends GetxService { creationTime INTEGER NOT NULL, syncStatus INTEGER NOT NULL, operation INTEGER NOT NULL, + isDeleted INTEGER NOT NULL, censorTaskId TEXT, bindData TEXT, isChecked INTEGER NOT NULL ) '''); + + Get.log('数据库表创建成功'); } - /// 数据库版本升级时的回调函数。 - /// 用于在应用更新后,向现有数据库中添加新的列,而不会丢失数据。 + /// 数据库版本升级处理 Future _onUpgrade(Database db, int oldVersion, int newVersion) async { Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...'); - if (oldVersion < 2) { - // 示例:从版本 1 升级到版本 2,添加一个新的列 'newColumn' - // await db.execute('ALTER TABLE $_tableName ADD COLUMN newColumn TEXT;'); - // Get.log('已添加新列: newColumn'); + + // 版本升级迁移逻辑 + for (int version = oldVersion + 1; version <= newVersion; version++) { + await _runMigration(db, version); } - // if (oldVersion < 3) { - // ... - // } - Get.log('数据库升级完成。'); - } - // --- + Get.log('数据库升级完成'); + } - /// **数据操作 (CRUD) 方法** + /// 执行特定版本的数据库迁移 + Future _runMigration(Database db, int version) async { + switch (version) { + case 2: + // 版本2迁移逻辑 + // await db.execute('ALTER TABLE $_tableName ADD COLUMN newColumn TEXT;'); + break; + // 添加更多版本迁移逻辑 + default: + Get.log('没有找到版本 $version 的迁移脚本'); + } + } - /// 向数据库中插入一个新问题。 - /// 如果 `problem` 没有 `id`,会自动生成一个唯一的 UUID。 - /// - /// 返回:`Future`,表示插入的行ID。如果失败,会返回 0。 + /// 插入问题记录,并设置同步状态为未同步 Future insertProblem(Problem problem) async { try { + // 确保插入的问题同步状态为未同步 final problemToInsert = problem.copyWith( - id: problem.id ?? const Uuid().v4(), + syncStatus: SyncStatus.notSynced, ); - return await _database.insert( + + final result = await _database.insert( _tableName, problemToInsert.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); + + Get.log('问题记录插入成功,ID: ${problem.id}'); + return result; } catch (e) { - Get.log('插入问题失败:$e', isError: true); - return 0; // 返回 0 表示插入失败 + Get.log('插入问题失败(ID: ${problem.id}):$e', isError: true); + return 0; } } - /// 根据 ID 从数据库中删除一个问题。 - /// - /// 参数:`id` - 要删除的问题ID。 - /// 返回:`Future`,表示被删除的行数。 + /// 逻辑删除问题记录,并设置同步状态为未同步 Future deleteProblem(String id) async { try { - return await _database.delete( + final result = await _database.update( _tableName, + { + 'isDeleted': 1, + 'syncStatus': SyncStatus.notSynced.index, // 设置为未同步 + }, where: 'id = ?', whereArgs: [id], ); + + if (result > 0) { + Get.log('问题逻辑删除成功,ID: $id'); + } + + return result; } catch (e) { - Get.log('删除问题(ID: $id)失败:$e', isError: true); + Get.log('逻辑删除问题失败(ID: $id):$e', isError: true); return 0; } } - /// 更新数据库中已存在的问题。 - /// - /// 参数:`problem` - 包含更新数据的 `Problem` 对象。 - /// 返回:`Future`,表示被更新的行数。 + /// 更新问题记录,并设置同步状态为未同步 Future updateProblem(Problem problem) async { try { - return await _database.update( + // 确保更新后同步状态为未同步 + final problemToUpdate = problem.copyWith( + syncStatus: SyncStatus.notSynced, + ); + + final result = await _database.update( _tableName, - problem.toMap(), + problemToUpdate.toMap(), where: 'id = ?', whereArgs: [problem.id], ); + + if (result > 0) { + Get.log('问题更新成功,ID: ${problem.id}'); + } + + return result; + } catch (e) { + Get.log('更新问题失败(ID: ${problem.id}):$e', isError: true); + return 0; + } + } + + /// 获取需要同步的问题记录(所有同步状态为未同步的记录) + Future> getProblemsForSync() async { + try { + final results = await _database.query( + _tableName, + where: 'syncStatus = ?', + whereArgs: [SyncStatus.notSynced.index], + orderBy: 'creationTime ASC', + ); + + Get.log('找到 ${results.length} 条需要同步的记录'); + return results.map((json) => Problem.fromMap(json)).toList(); + } catch (e) { + Get.log('获取待同步问题失败:$e', isError: true); + return []; + } + } + + /// 标记问题为已同步(在同步成功后调用) + Future markAsSynced(String id) async { + try { + final result = await _database.update( + _tableName, + {'syncStatus': SyncStatus.synced.index}, + where: 'id = ? AND syncStatus = ?', + whereArgs: [id, SyncStatus.notSynced.index], + ); + + if (result > 0) { + Get.log('问题标记为已同步,ID: $id'); + } + + return result; } catch (e) { - Get.log('更新问题(ID: ${problem.id})失败:$e', isError: true); + Get.log('标记同步状态失败(ID: $id):$e', isError: true); return 0; } } - /// 根据 ID 获取单个问题。 - /// - /// 参数:`id` - 要获取的问题ID。 - /// 返回:`Future`,如果找到则返回 `Problem` 对象,否则返回 `null`。 + /// 根据ID获取问题记录 Future getProblemById(String id) async { try { - final List> maps = await _database.query( + final results = await _database.query( _tableName, - where: 'id = ?', + where: 'id = ? AND isDeleted = 0', whereArgs: [id], limit: 1, ); - if (maps.isNotEmpty) { - return Problem.fromMap(maps.first); - } - return null; + + return results.isNotEmpty ? Problem.fromMap(results.first) : null; } catch (e) { - Get.log('获取问题(ID: $id)失败:$e', isError: true); + Get.log('获取问题失败(ID: $id):$e', isError: true); return null; } } - /// 获取所有问题,支持按筛选条件查询。 - /// - /// 可选参数: - /// - `startDate`: 创建时间范围的开始时间。 - /// - `endDate`: 创建时间范围的结束时间。 - /// - `syncStatus`: 同步状态。 - /// - /// 返回:`Future>`,返回符合条件的问题列表。如果查询失败,返回空列表。 + /// 获取问题列表(支持多种筛选条件) Future> getProblems({ DateTime? startDate, DateTime? endDate, - SyncStatus? syncStatus, + String? syncStatus, + String? bindStatus, + bool includeDeleted = false, }) async { try { - final List whereClauses = []; - final List whereArgs = []; + final whereClauses = []; + final whereArgs = []; + + // 删除状态筛选 + if (!includeDeleted) { + whereClauses.add('isDeleted = 0'); + } + // 时间范围筛选 if (startDate != null) { whereClauses.add('creationTime >= ?'); whereArgs.add(startDate.millisecondsSinceEpoch); @@ -186,36 +243,43 @@ class SQLiteProvider extends GetxService { whereArgs.add(endDate.millisecondsSinceEpoch); } - if (syncStatus != null) { + // 同步状态筛选 + if (syncStatus != null && syncStatus != '全部') { + final statusValue = syncStatus == '已上传' + ? SyncStatus.synced.index + : SyncStatus.notSynced.index; + whereClauses.add('syncStatus = ?'); - whereArgs.add(syncStatus.index); + whereArgs.add(statusValue); } - final String? whereString = whereClauses.isNotEmpty - ? whereClauses.join(' AND ') - : null; + // 绑定状态筛选 + if (bindStatus != null && bindStatus != '全部') { + if (bindStatus == '已绑定') { + whereClauses.add('bindData IS NOT NULL AND bindData != ""'); + } else { + whereClauses.add('(bindData IS NULL OR bindData = "")'); + } + } - final List> maps = await _database.query( + final results = await _database.query( _tableName, - where: whereString, + where: whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null, whereArgs: whereArgs.isEmpty ? null : whereArgs, orderBy: 'creationTime DESC', ); - return maps.map((json) => Problem.fromMap(json)).toList(); + return results.map((json) => Problem.fromMap(json)).toList(); } catch (e) { Get.log('获取问题列表失败:$e', isError: true); return []; } } - // --- - - /// `GetxService` 生命周期方法,在服务被销毁前调用, - /// 用于关闭数据库连接,防止资源泄漏。 @override void onClose() { _database.close(); + Get.log('数据库连接已关闭'); super.onClose(); } } diff --git a/lib/data/repositories/problem_repository.dart b/lib/data/repositories/problem_repository.dart index 83e956c..a28a430 100644 --- a/lib/data/repositories/problem_repository.dart +++ b/lib/data/repositories/problem_repository.dart @@ -49,8 +49,8 @@ class ProblemRepository extends GetxService { Future getProblems({ DateTime? startDate, DateTime? endDate, - String uploadStatus = '全部', - String bindStatus = '全部', + String? uploadStatus, + String? bindStatus, }) async { return await sqliteProvider.getProblems( startDate: startDate, @@ -202,4 +202,8 @@ class ProblemRepository extends GetxService { rethrow; } } + + getProblemsForSync() { + return sqliteProvider.getProblemsForSync(); + } } diff --git a/lib/modules/problem/controllers/problem_controller.dart b/lib/modules/problem/controllers/problem_controller.dart index 20cc9bb..066b94a 100644 --- a/lib/modules/problem/controllers/problem_controller.dart +++ b/lib/modules/problem/controllers/problem_controller.dart @@ -1,5 +1,6 @@ // modules/problem/controllers/problem_controller.dart import 'dart:developer'; +import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -7,8 +8,9 @@ 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/repositories/problem_repository.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/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 { @@ -28,15 +30,30 @@ class ProblemController extends GetxController // Dio 的取消令牌,用于取消正在进行的请求 late CancelToken _cancelToken; - int get selectedCount { - return unUploadedProblems.where((problem) => problem.isChecked).length; - } + final RxSet _selectedProblems = {}.obs; + + Set get selectedProblems => _selectedProblems; - List get selectedProblems { - return unUploadedProblems.where((problem) => problem.isChecked).toList(); + int get selectedCount => _selectedProblems.length; + + // 在 ProblemController 中添加 + // 添加日期范围选项列表 + List get dateRangeOptions { + return DateRange.values.map((range) => range.toDropdownOption()).toList(); } - /// 问题列表筛选条件 + final List 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 bindOptions = const [ + DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive), + DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link), + DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off), + ]; + final Rx currentDateRange = DateRange.oneWeek.obs; final RxString currentUploadFilter = '全部'.obs; final RxString currentBindFilter = '全部'.obs; @@ -66,6 +83,7 @@ class ProblemController extends GetxController @override void onInit() { super.onInit(); + tabController = TabController(length: 2, vsync: this); tabController.addListener(_onTabChanged); loadProblems(); @@ -81,33 +99,36 @@ class ProblemController extends GetxController // #region 问题上传 - // 在你的 Controller 类中添加一个新方法 - void updateProblemCheckedStatus(Rx problem, bool isChecked) { - // 1. 使用 copyWith 创建一个包含新状态的新 Problem 对象 - final updatedProblem = problem.value.copyWith(isChecked: isChecked); - - // 2. 更新 Rx 的值,这会触发 UI 更新 - problem.value = updatedProblem; + void updateProblemSelection(Problem problem, bool isChecked) { + if (isChecked) { + _selectedProblems.add(problem); + } else { + _selectedProblems.remove(problem); + } + // 更新全选状态 + allSelected.value = _selectedProblems.length == unUploadedProblems.length; } void selectAll() { - final bool newState = !allSelected.value; - - // 使用 .map() 创建一个包含新副本的列表 - final updatedProblems = unUploadedProblems.map((problem) { - return problem.copyWith(isChecked: newState); - }).toList(); - - // 使用 assignAll 替换整个列表,并触发一次更新 - unUploadedProblems.assignAll(updatedProblems); + if (allSelected.value) { + // 如果已经是全选,则取消全选 + _selectedProblems.clear(); + } else { + // 如果是取消全选,则选择所有 + _selectedProblems.addAll(unUploadedProblems); + } + allSelected.value = !allSelected.value; + } - // 更新全选状态 - allSelected.value = newState; + // 上传完成后清空选中状态 + void clearSelection() { + _selectedProblems.clear(); + allSelected.value = false; } - // 启动上传流程 + // 在 handleUpload 方法中,上传完成后调用 clearSelection Future handleUpload() async { - if (selectedProblems.isEmpty) { + if (_selectedProblems.isEmpty) { Get.snackbar('提示', '请选择要上传的问题'); return; } @@ -115,13 +136,11 @@ class ProblemController extends GetxController uploadProgress.value = 0.0; _cancelToken = CancelToken(); - // 显示上传对话框 showUploadProgressDialog(); try { - // 直接将问题列表传递给 Repository await problemRepository.uploadProblems( - selectedProblems, + _selectedProblems.toList(), // 转换为列表 cancelToken: _cancelToken, onProgress: (progress) { uploadProgress.value = progress; @@ -130,6 +149,11 @@ class ProblemController extends GetxController Get.back(); Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM); + + // 上传成功后清空选中状态 + clearSelection(); + // 重新加载未上传的问题列表 + loadUnUploadedProblems(); } on DioException catch (e) { Get.back(); if (CancelToken.isCancel(e)) { @@ -259,7 +283,27 @@ class ProblemController extends GetxController loadProblems(); } } + // #endregion + // 更新日期范围的方法 + void updateDateRange(String rangeValue) { + final newRange = rangeValue.toDateRange(); + if (newRange != null) { + currentDateRange.value = newRange; + loadProblems(); // 重新加载数据 + } + } + + // 添加筛选方法 + void updateUploadFilter(String value) { + currentUploadFilter.value = value; + loadProblems(); // 重新加载数据 + } + + void updateBindFilter(String value) { + currentBindFilter.value = value; + loadProblems(); // 重新加载数据 + } /// 加载 Future loadProblems() async { @@ -305,58 +349,12 @@ class ProblemController extends GetxController } } - /// - void updateCurrentFilters({ - DateRange? newDateRange, - String? newUploadStatus, - String? newBindingStatus, - }) { - if (newDateRange != null && currentDateRange.value != newDateRange) { - currentDateRange.value = newDateRange; - } - if (newUploadStatus != null && - currentUploadFilter.value != newUploadStatus) { - currentUploadFilter.value = newUploadStatus; - } - if (newBindingStatus != null && - currentBindFilter.value != newBindingStatus) { - currentBindFilter.value = newBindingStatus; - } - - // 只要调用此方法,就重新加载数据 - loadProblems(); - } - - void updateHistoryFilters({ - DateTime? newStartTime, - DateTime? newEndTime, - String? newUploadStatus, - String? newBindingStatus, - }) { - if (newStartTime != null && historyStartTime.value != newStartTime) { - historyStartTime.value = newStartTime; - } - if (newUploadStatus != null && - historyUploadFilter.value != newUploadStatus) { - historyUploadFilter.value = newUploadStatus; - } - if (newBindingStatus != null && - historyBindFilter.value != newBindingStatus) { - historyBindFilter.value = newBindingStatus; - } - - // 只要调用此方法,就重新加载数据 - loadProblems(); - } - // 新增方法:查询所有未上传的问题 Future loadUnUploadedProblems() async { isLoading.value = true; try { // 调用 _localDatabase.getProblems 并只筛选 '未上传' 的问题 - unUploadedProblems.value = await problemRepository.getProblems( - uploadStatus: '未上传', - ); + unUploadedProblems.value = await problemRepository.getProblemsForSync(); } catch (e) { Get.snackbar('错误', '加载未上传问题失败: $e'); } finally { @@ -364,25 +362,25 @@ class ProblemController extends GetxController } } - Future addProblem(Problem problem) async { - try { - await problemRepository.insertProblem(problem); - loadProblems(); - } catch (e) { - Get.snackbar('错误', '保存问题失败: $e'); - rethrow; - } - } - - Future updateProblem(Problem problem) async { - try { - await problemRepository.updateProblem(problem); - loadProblems(); - } catch (e) { - Get.snackbar('错误', '更新问题失败: $e'); - rethrow; - } - } + // Future addProblem(Problem problem) async { + // try { + // await problemRepository.insertProblem(problem); + // loadProblems(); + // } catch (e) { + // Get.snackbar('错误', '保存问题失败: $e'); + // rethrow; + // } + // } + + // Future updateProblem(Problem problem) async { + // try { + // await problemRepository.updateProblem(problem); + // loadProblems(); + // } catch (e) { + // Get.snackbar('错误', '更新问题失败: $e'); + // rethrow; + // } + // } Future deleteProblem(Problem problem) async { try { @@ -397,17 +395,18 @@ class ProblemController extends GetxController } } + // 删除本地文件 Future _deleteProblemImages(Problem problem) async { - // for (var imagePath in problem.imageUrls) { - // try { - // final file = File(imagePath); - // if (await file.exists()) { - // await file.delete(); - // } - // } catch (e) { - // throw Exception(e); - // } - // } + for (var imagePath in problem.imageUrls) { + try { + final file = File(imagePath.localPath); + if (await file.exists()) { + await file.delete(); + } + } catch (e) { + throw Exception(e); + } + } } /// 显示日期选择器 diff --git a/lib/modules/problem/controllers/problem_form_controller.dart b/lib/modules/problem/controllers/problem_form_controller.dart index a6a61ed..b4ce3e9 100644 --- a/lib/modules/problem/controllers/problem_form_controller.dart +++ b/lib/modules/problem/controllers/problem_form_controller.dart @@ -5,7 +5,6 @@ import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; import 'package:path_provider/path_provider.dart'; import 'package:problem_check_system/data/models/image_status.dart'; -import 'package:problem_check_system/data/models/sync_status.dart'; import 'package:problem_check_system/data/models/image_metadata_model.dart'; import 'dart:io'; import 'package:problem_check_system/data/models/problem_model.dart'; @@ -151,7 +150,6 @@ class ProblemFormController extends GetxController { description: descriptionController.text, location: locationController.text, imageUrls: imagePaths, - creationTime: DateTime.now(), // 更新编辑时间 ); await problemRepository.updateProblem(updatedProblem); @@ -159,11 +157,10 @@ class ProblemFormController extends GetxController { Get.snackbar('成功', '问题已更新'); } else { // 新增模式:创建新问题 - final problem = Problem( + final problem = Problem.create( description: descriptionController.text, location: locationController.text, imageUrls: imagePaths, - creationTime: DateTime.now(), ); await problemRepository.insertProblem(problem); diff --git a/lib/modules/problem/views/problem_list_page.dart b/lib/modules/problem/views/problem_list_page.dart index fe96749..8654e00 100644 --- a/lib/modules/problem/views/problem_list_page.dart +++ b/lib/modules/problem/views/problem_list_page.dart @@ -6,7 +6,6 @@ import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; class ProblemListPage extends GetView { - // 定义通用组件的参数 final RxList problemsToShow; final ProblemCardViewType viewType; @@ -25,11 +24,11 @@ class ProblemListPage extends GetView { return ListView.builder( padding: EdgeInsets.symmetric(horizontal: 17.w), - itemCount: problemsToShow.length + 1, + itemCount: problemsToShow.length, itemBuilder: (context, index) { - if (index == problemsToShow.length) { - return SizedBox(height: 79.5.h); - } + // if (index == problemsToShow.length) { + // return SizedBox(height: 79.5.h); + // } final problem = problemsToShow[index]; return _buildSwipeableProblemCard(problem); }, @@ -56,10 +55,27 @@ class ProblemListPage extends GetView { controller.deleteProblem(problem); Get.snackbar('成功', '问题已删除'); }, - child: ProblemCard(problem: problem.obs, viewType: viewType), + child: ProblemCard( + key: ValueKey(problem.id), + problem: problem, + viewType: viewType, + isSelected: false, // 非选择模式,默认false + ), ); } else { - return ProblemCard(problem: problem.obs, viewType: viewType); + return Obx(() { + // 使用 Obx 来监听选中状态的变化 + final isSelected = controller.selectedProblems.contains(problem); + return ProblemCard( + key: ValueKey(problem.id), + problem: problem, + viewType: viewType, + isSelected: isSelected, // 传递选中状态 + onChanged: (problem, isChecked) { + controller.updateProblemSelection(problem, isChecked); + }, + ); + }); } } diff --git a/lib/modules/problem/views/problem_page.dart b/lib/modules/problem/views/problem_page.dart index 9aaad69..2de02aa 100644 --- a/lib/modules/problem/views/problem_page.dart +++ b/lib/modules/problem/views/problem_page.dart @@ -4,8 +4,8 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/routes/app_routes.dart'; import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; // 导入修正后的 ProblemListPage -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/compact_filter_bar.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/custom_object_dropdown.dart'; import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; // 导入自定义下拉菜单 class ProblemPage extends GetView { @@ -68,39 +68,13 @@ class ProblemPage extends GetView { decoration: BoxDecoration(color: Color(0xfff7f7f7)), child: Column( children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 17.w), - child: Row( - children: [ - CustomDateRangeDropdown( - selectedRange: controller.currentDateRange, - onChanged: (rangeValue) { - controller.updateCurrentFilters( - newDateRange: rangeValue, - ); - }, - ), - - CustomStringDropdown( - selectedValue: controller.currentUploadFilter, - items: const ['全部', '未上传', '已上传'], - onChanged: (uploadValue) { - controller.updateCurrentFilters( - newUploadStatus: uploadValue, - ); - }, - ), - - CustomStringDropdown( - selectedValue: controller.currentBindFilter, - items: const ['全部', '未绑定', '已绑定'], - onChanged: (bindingValue) { - controller.updateCurrentFilters( - newBindingStatus: bindingValue, - ); - }, - ), - ], + CompactFilterBar( + showDateRangeFilter: true, + showUploadFilter: true, + showBindFilter: true, + padding: EdgeInsets.symmetric( + horizontal: 17.w, + vertical: 0.h, ), ), @@ -119,37 +93,20 @@ class ProblemPage extends GetView { decoration: BoxDecoration(color: Color(0xfff7f7f7)), child: Column( children: [ - Container( - padding: EdgeInsets.symmetric(horizontal: 17.w), - child: Row( - children: [ - // 在你的代码中这样调用 - ElevatedButton( - onPressed: () => - controller.selectDateRange(context), - child: const Text('选择日期范围'), - ), - CustomStringDropdown( - selectedValue: controller.historyUploadFilter, - items: const ['全部', '未上传', '已上传'], - onChanged: (uploadValue) { - controller.updateHistoryFilters( - newUploadStatus: uploadValue, - ); - }, - ), - - CustomStringDropdown( - selectedValue: controller.historyBindFilter, - items: const ['全部', '未绑定', '已绑定'], - onChanged: (bindingValue) { - controller.updateHistoryFilters( - newBindingStatus: bindingValue, - ); - }, - ), - ], - ), + CompactFilterBar( + showDateRangeFilter: false, + showUploadFilter: true, + showBindFilter: true, + showCustomButton: true, + customButtonIcon: Icons.date_range, + customButtonText: "选择日期", + onCustomButtonPressed: () { + // 设置逻辑 + }, + // padding: EdgeInsets.symmetric( + // horizontal: 0.w, + // vertical: 0.h, + // ), ), Expanded( diff --git a/lib/modules/problem/views/problem_upload_page.dart b/lib/modules/problem/views/problem_upload_page.dart index 0f6b021..f53c55e 100644 --- a/lib/modules/problem/views/problem_upload_page.dart +++ b/lib/modules/problem/views/problem_upload_page.dart @@ -17,7 +17,6 @@ class ProblemUploadPage extends GetView { ); } - // 构建顶部 AppBar PreferredSizeWidget _buildAppBar() { return AppBar( title: Obx(() { @@ -27,12 +26,18 @@ class ProblemUploadPage extends GetView { centerTitle: true, leading: IconButton(icon: Icon(Icons.close), onPressed: () => Get.back()), actions: [ - TextButton( - onPressed: controller.selectAll, - child: Obx( - () => Text( + Obx( + () => TextButton( + onPressed: controller.unUploadedProblems.isNotEmpty + ? controller.selectAll + : null, + child: Text( controller.allSelected.value ? '取消全选' : '全选', - style: TextStyle(color: Colors.blue), + style: TextStyle( + color: controller.unUploadedProblems.isNotEmpty + ? Colors.blue + : Colors.grey, + ), ), ), ), @@ -40,15 +45,20 @@ class ProblemUploadPage extends GetView { ); } - // 构建页面主体 Widget _buildBody() { - return ProblemListPage( - problemsToShow: controller.unUploadedProblems, - viewType: ProblemCardViewType.checkbox, - ); + return Obx(() { + if (controller.unUploadedProblems.isEmpty) { + return Center( + child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)), + ); + } + return ProblemListPage( + problemsToShow: controller.unUploadedProblems, + viewType: ProblemCardViewType.checkbox, + ); + }); } - // 构建底部操作栏 Widget _buildBottomBar() { return Container( padding: EdgeInsets.all(16.w), @@ -67,8 +77,9 @@ class ProblemUploadPage extends GetView { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), + minimumSize: Size(double.infinity, 48.h), ), - child: Text('点击上传'), + child: Text('点击上传 (${controller.selectedCount})'), ), ), ); diff --git a/lib/modules/problem/views/widgets/compact_filter_bar.dart b/lib/modules/problem/views/widgets/compact_filter_bar.dart new file mode 100644 index 0000000..669d5f3 --- /dev/null +++ b/lib/modules/problem/views/widgets/compact_filter_bar.dart @@ -0,0 +1,122 @@ +// widgets/compact_filter_bar.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 'custom_filter_dropdown.dart'; + +class CompactFilterBar extends GetView { + final bool showDateRangeFilter; + final bool showUploadFilter; + final bool showBindFilter; + final bool showCustomButton; // 新增:是否显示自定义按钮 + final String? customButtonText; // 新增:自定义按钮文本 + final IconData? customButtonIcon; // 新增:自定义按钮图标 + final VoidCallback? onCustomButtonPressed; // 新增:自定义按钮点击回调 + final EdgeInsetsGeometry? padding; + + const CompactFilterBar({ + super.key, + this.showDateRangeFilter = true, + this.showUploadFilter = true, + this.showBindFilter = true, + this.showCustomButton = false, // 默认不显示 + this.customButtonText, + this.customButtonIcon, + this.onCustomButtonPressed, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: padding ?? EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + color: Colors.grey[50], + child: Row( + children: [ + // 自定义按钮 + if (showCustomButton) ...[_buildCustomButton()], + // 日期范围筛选 + if (showDateRangeFilter) ...[ + Obx( + () => CustomFilterDropdown( + title: '时间范围', + options: controller.dateRangeOptions, + selectedValue: controller.currentDateRange.value.name, + onChanged: controller.updateDateRange, + width: 110.w, + showBorder: false, + ), + ), + ], + + // 上传状态筛选 + if (showUploadFilter) ...[ + Obx( + () => CustomFilterDropdown( + title: '上传状态', + options: controller.uploadOptions, + selectedValue: controller.currentUploadFilter.value, + onChanged: controller.updateUploadFilter, + width: 110.w, + showBorder: false, + ), + ), + ], + + // 绑定状态筛选 + if (showBindFilter) ...[ + Obx( + () => CustomFilterDropdown( + title: '绑定状态', + options: controller.bindOptions, + selectedValue: controller.currentBindFilter.value, + onChanged: controller.updateBindFilter, + width: 110.w, + showBorder: false, + ), + ), + ], + ], + ), + ); + } + + // 构建自定义按钮 + Widget _buildCustomButton() { + return SizedBox( + width: 110.w, + // decoration: BoxDecoration( + // border: Border.all(color: Colors.grey.shade300), + // borderRadius: BorderRadius.circular(8.r), + // ), + child: TextButton( + onPressed: onCustomButtonPressed, + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (customButtonIcon != null) ...[ + Icon(customButtonIcon, size: 16.sp, color: Colors.grey[700]), + SizedBox(width: 4.w), + ], + Text( + customButtonText ?? '自定义', + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart b/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart deleted file mode 100644 index fec470f..0000000 --- a/lib/modules/problem/views/widgets/custom_data_range_dropdown.dart +++ /dev/null @@ -1,89 +0,0 @@ -// 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天计算 - } - } - - // - DateTime get endDate { - return DateTime.now(); - } -} - -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_filter_dropdown.dart b/lib/modules/problem/views/widgets/custom_filter_dropdown.dart new file mode 100644 index 0000000..e1e17f9 --- /dev/null +++ b/lib/modules/problem/views/widgets/custom_filter_dropdown.dart @@ -0,0 +1,77 @@ +// widgets/custom_filter_dropdown.dart +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart'; + +class CustomFilterDropdown extends StatelessWidget { + final String title; + final List options; + final String selectedValue; + final ValueChanged onChanged; + final double? width; + final bool showBorder; + + const CustomFilterDropdown({ + super.key, + required this.title, + required this.options, + required this.selectedValue, + required this.onChanged, + this.width, + this.showBorder = true, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width ?? 120.w, + decoration: showBorder + ? BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8.r), + ) + : null, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 0.h), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: selectedValue, + isExpanded: true, + icon: Icon(Icons.arrow_drop_down, size: 16.sp, color: Colors.grey), + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + fontWeight: FontWeight.normal, + ), + dropdownColor: Colors.white, + borderRadius: BorderRadius.circular(8.r), + items: options.map((DropdownOption option) { + return DropdownMenuItem( + value: option.value, + child: Row( + children: [ + if (option.icon != null) + Padding( + padding: EdgeInsets.only(right: 4.w), + child: Icon(option.icon, size: 16.sp, color: Colors.grey), + ), + Expanded( + child: Text( + option.label, + style: TextStyle(fontSize: 14.sp), + // overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + onChanged(newValue); + } + }, + ), + ), + ); + } +} diff --git a/lib/modules/problem/views/widgets/custom_string_dropdown.dart b/lib/modules/problem/views/widgets/custom_object_dropdown.dart similarity index 76% rename from lib/modules/problem/views/widgets/custom_string_dropdown.dart rename to lib/modules/problem/views/widgets/custom_object_dropdown.dart index 9afc1bc..9ad92d8 100644 --- a/lib/modules/problem/views/widgets/custom_string_dropdown.dart +++ b/lib/modules/problem/views/widgets/custom_object_dropdown.dart @@ -1,15 +1,14 @@ -// custom_string_dropdown.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/views/widgets/models/dropdown_option.dart'; -class CustomStringDropdown extends StatelessWidget { +class CustomObjectDropdown extends StatelessWidget { final RxString selectedValue; - final List items; + final List items; final Function(String) onChanged; - const CustomStringDropdown({ + const CustomObjectDropdown({ super.key, required this.selectedValue, required this.items, @@ -36,8 +35,11 @@ class CustomStringDropdown extends StatelessWidget { onChanged(newValue); } }, - items: items.map((String value) { - return DropdownMenuItem(value: value, child: Text(value)); + items: items.map((DropdownOption option) { + return DropdownMenuItem( + value: option.value, + child: Text(option.label), + ); }).toList(), icon: const Icon(Icons.arrow_drop_down, size: 15), style: TextStyle(fontSize: 10.sp, color: Colors.black), diff --git a/lib/modules/problem/views/widgets/models/date_range_enum.dart b/lib/modules/problem/views/widgets/models/date_range_enum.dart new file mode 100644 index 0000000..45b4f94 --- /dev/null +++ b/lib/modules/problem/views/widgets/models/date_range_enum.dart @@ -0,0 +1,68 @@ +// models/date_range_enum.dart +import 'package:flutter/material.dart'; +import 'package:problem_check_system/modules/problem/views/widgets/models/dropdown_option.dart'; + +enum DateRange { threeDays, oneWeek, oneMonth } + +extension DateRangeExtension on DateRange { + String get displayName { + 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)); + } + } + + DateTime get endDate { + return DateTime.now(); + } +} + +// 为 DateRange 枚举创建 DropdownOption 转换扩展 +extension DateRangeDropdown on DateRange { + DropdownOption toDropdownOption() { + return DropdownOption( + label: displayName, + value: name, // 使用枚举的名称作为值 + icon: _getIcon(), + ); + } + + IconData _getIcon() { + switch (this) { + case DateRange.threeDays: + return Icons.calendar_today; + case DateRange.oneWeek: + return Icons.date_range; + case DateRange.oneMonth: + return Icons.calendar_month; + } + } +} + +// 添加反向转换方法 +extension StringToDateRange on String { + DateRange? toDateRange() { + for (var range in DateRange.values) { + if (range.name == this) { + return range; + } + } + return null; + } +} diff --git a/lib/modules/problem/views/widgets/models/dropdown_option.dart b/lib/modules/problem/views/widgets/models/dropdown_option.dart new file mode 100644 index 0000000..a0b2af9 --- /dev/null +++ b/lib/modules/problem/views/widgets/models/dropdown_option.dart @@ -0,0 +1,23 @@ +// models/dropdown_option.dart +import 'package:flutter/material.dart'; + +class DropdownOption { + final String label; + final String value; + final IconData? icon; + + const DropdownOption({required this.label, required this.value, this.icon}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DropdownOption && + runtimeType == other.runtimeType && + value == other.value; + + @override + int get hashCode => value.hashCode; + + @override + String toString() => label; +} diff --git a/lib/modules/problem/views/widgets/problem_card.dart b/lib/modules/problem/views/widgets/problem_card.dart index 3d800c7..947e127 100644 --- a/lib/modules/problem/views/widgets/problem_card.dart +++ b/lib/modules/problem/views/widgets/problem_card.dart @@ -4,7 +4,6 @@ import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:problem_check_system/data/models/sync_status.dart'; import 'package:problem_check_system/data/models/problem_model.dart'; -import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart'; import 'package:problem_check_system/modules/problem/views/widgets/custom_button.dart'; import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; @@ -12,97 +11,109 @@ import 'package:tdesign_flutter/tdesign_flutter.dart'; // 定义枚举类型 enum ProblemCardViewType { buttons, checkbox } -class ProblemCard extends GetView { - final Rx problem; +class ProblemCard extends StatelessWidget { + final Problem problem; final ProblemCardViewType viewType; + final Function(Problem, bool)? onChanged; + final bool isSelected; // 改为必需参数 const ProblemCard({ super.key, required this.problem, this.viewType = ProblemCardViewType.buttons, + this.onChanged, + required this.isSelected, // 改为必需参数 }); @override Widget build(BuildContext context) { return Card( - // margin: EdgeInsets.symmetric(vertical: 5.h, horizontal: 9.w), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Image.asset( - 'assets/images/problem_preview.png', - fit: BoxFit.contain, - ), - title: Text('问题描述', style: TextStyle(fontSize: 16.sp)), - subtitle: LayoutBuilder( - builder: (context, constraints) { - return Text( - problem.value.description, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 14.sp), - ); - }, + child: InkWell( + onTap: viewType == ProblemCardViewType.checkbox + ? () { + // 点击卡片时切换选中状态 + onChanged?.call(problem, !isSelected); + } + : null, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Image.asset( + 'assets/images/problem_preview.png', + fit: BoxFit.contain, + ), + title: Text('问题描述', style: TextStyle(fontSize: 16.sp)), + subtitle: LayoutBuilder( + builder: (context, constraints) { + return Text( + problem.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 14.sp), + ); + }, + ), ), - ), - SizedBox(height: 8.h), - Row( - children: [ - SizedBox(width: 16.w), - Row( - children: [ - Icon(Icons.location_on, color: Colors.grey, size: 16.h), - SizedBox(width: 8.w), - Text( - problem.value.location, + SizedBox(height: 8.h), + Row( + children: [ + SizedBox(width: 16.w), + Icon(Icons.location_on, color: Colors.grey, size: 16.h), + SizedBox(width: 8.w), + SizedBox( + width: 100.w, + child: Text( + problem.location, style: TextStyle(fontSize: 12.sp), + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - ], - ), - SizedBox(width: 16.w), - Row( - children: [ - Icon(Icons.access_time, color: Colors.grey, size: 16.h), - SizedBox(width: 8.w), - Text( - DateFormat( - 'yyyy-MM-dd HH:mm', - ).format(problem.value.creationTime), + ), + SizedBox(width: 16.w), + Icon(Icons.access_time, color: Colors.grey, size: 16.h), + SizedBox(width: 8.w), + SizedBox( + width: 100.w, + child: Text( + DateFormat('yyyy-MM-dd HH:mm').format(problem.creationTime), style: TextStyle(fontSize: 12.sp), + overflow: TextOverflow.ellipsis, ), - ], - ), - ], - ), - SizedBox(height: 8.h), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox(width: 16.w), - Wrap( - spacing: 8, - children: [ - problem.value.syncStatus == SyncStatus.synced - ? TDTag('已上传', isLight: true, theme: TDTagTheme.success) - : TDTag('未上传', isLight: true, theme: TDTagTheme.danger), - problem.value.bindData != null && - problem.value.bindData!.isNotEmpty - ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) - : TDTag('未绑定', isLight: true, theme: TDTagTheme.warning), - ], - ), - const Spacer(), // 使用 Spacer 替代固定的 SizedBox - _buildBottomActions(), - ], - ), - SizedBox(height: 8.h), - ], + ), + ], + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox(width: 16.w), + Wrap( + spacing: 8, + children: [ + problem.syncStatus == SyncStatus.synced + ? TDTag('已上传', isLight: true, theme: TDTagTheme.success) + : TDTag('未上传', isLight: true, theme: TDTagTheme.danger), + problem.bindData != null && problem.bindData!.isNotEmpty + ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) + : TDTag( + '未绑定', + isLight: true, + theme: TDTagTheme.warning, + ), + ], + ), + const Spacer(), + _buildBottomActions(), // 移除参数 + ], + ), + SizedBox(height: 8.h), + ], + ), ), ); } - // 渲染底部 UI 的私有方法 Widget _buildBottomActions() { switch (viewType) { case ProblemCardViewType.buttons: @@ -111,16 +122,14 @@ class ProblemCard extends GetView { CustomButton( text: '修改', onTap: () { - Get.to(ProblemFormPage(problem: problem.value)); + Get.to(ProblemFormPage(problem: problem)); }, ), SizedBox(width: 8.w), CustomButton( text: '查看', onTap: () { - Get.to( - ProblemFormPage(problem: problem.value, isReadOnly: true), - ); + Get.to(ProblemFormPage(problem: problem, isReadOnly: true)); }, ), SizedBox(width: 16.w), @@ -129,16 +138,13 @@ class ProblemCard extends GetView { case ProblemCardViewType.checkbox: return Padding( padding: EdgeInsets.only(right: 16.w), - child: Obx( - () => Checkbox( - // 将 Checkbox 的 value 绑定到 controller.isChecked.value - value: problem.value.isChecked, - // 当 Checkbox 状态改变时,调用 controller 中的方法来更新状态 - onChanged: (bool? value) { - // 调用 Controller 中的方法来处理状态更新 - controller.updateProblemCheckedStatus(problem, value ?? false); - }, - ), + child: Checkbox( + value: isSelected, // 使用组件的选中状态 + onChanged: (bool? value) { + if (value != null) { + onChanged?.call(problem, value); + } + }, ), ); }