Browse Source

refactor : 优化查询下拉列表,上传问题页面,数据库查询。

dev
徐振升 5 days ago
parent
commit
48de460804
  1. 15
      lib/data/models/operation.dart
  2. 86
      lib/data/models/problem_model.dart
  3. 14
      lib/data/models/sync_status.dart
  4. 218
      lib/data/providers/sqlite_provider.dart
  5. 8
      lib/data/repositories/problem_repository.dart
  6. 209
      lib/modules/problem/controllers/problem_controller.dart
  7. 5
      lib/modules/problem/controllers/problem_form_controller.dart
  8. 30
      lib/modules/problem/views/problem_list_page.dart
  9. 87
      lib/modules/problem/views/problem_page.dart
  10. 29
      lib/modules/problem/views/problem_upload_page.dart
  11. 122
      lib/modules/problem/views/widgets/compact_filter_bar.dart
  12. 89
      lib/modules/problem/views/widgets/custom_data_range_dropdown.dart
  13. 77
      lib/modules/problem/views/widgets/custom_filter_dropdown.dart
  14. 16
      lib/modules/problem/views/widgets/custom_object_dropdown.dart
  15. 68
      lib/modules/problem/views/widgets/models/date_range_enum.dart
  16. 23
      lib/modules/problem/views/widgets/models/dropdown_option.dart
  17. 78
      lib/modules/problem/views/widgets/problem_card.dart

15
lib/data/models/operation.dart

@ -8,18 +8,3 @@ enum Operation {
/// ///
delete, delete,
} }
/// Operation
extension OperationExtension on Operation {
///
String get displayName {
switch (this) {
case Operation.create:
return '创建';
case Operation.update:
return '修改';
case Operation.delete:
return '删除';
}
}
}

86
lib/data/models/problem_model.dart

@ -1,9 +1,9 @@
// problem_model.dart
import 'dart:convert'; 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/operation.dart';
import 'package:problem_check_system/data/models/sync_status.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 Operation operation;
///
final bool isDeleted;
/// ID /// ID
final String? censorTaskId; final String? censorTaskId;
@ -38,8 +41,10 @@ class Problem {
/// false /// false
final bool isChecked; final bool isChecked;
/// Problem /// uuid
const Problem({ static final Uuid _uuid = Uuid();
Problem({
this.id, this.id,
required this.description, required this.description,
required this.location, required this.location,
@ -47,13 +52,28 @@ class Problem {
required this.creationTime, required this.creationTime,
this.syncStatus = SyncStatus.notSynced, this.syncStatus = SyncStatus.notSynced,
this.operation = Operation.create, this.operation = Operation.create,
this.isDeleted = false,
this.censorTaskId, this.censorTaskId,
this.bindData, this.bindData,
this.isChecked = false, this.isChecked = false,
}); });
/// /// ID
/// factory Problem.create({
required String description,
required String location,
required List<ImageMetadata> imageUrls,
}) {
return Problem(
id: _uuid.v4(),
description: description,
location: location,
imageUrls: imageUrls,
creationTime: DateTime.now(),
);
}
/// copyWith
Problem copyWith({ Problem copyWith({
String? id, String? id,
String? description, String? description,
@ -62,6 +82,7 @@ class Problem {
DateTime? creationTime, DateTime? creationTime,
SyncStatus? syncStatus, SyncStatus? syncStatus,
Operation? operation, Operation? operation,
bool? isDeleted,
String? censorTaskId, String? censorTaskId,
String? bindData, String? bindData,
bool? isChecked, bool? isChecked,
@ -74,50 +95,63 @@ class Problem {
creationTime: creationTime ?? this.creationTime, creationTime: creationTime ?? this.creationTime,
syncStatus: syncStatus ?? this.syncStatus, syncStatus: syncStatus ?? this.syncStatus,
operation: operation ?? this.operation, operation: operation ?? this.operation,
isDeleted: isDeleted ?? this.isDeleted,
censorTaskId: censorTaskId ?? this.censorTaskId, censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData, bindData: bindData ?? this.bindData,
isChecked: isChecked ?? this.isChecked, isChecked: isChecked ?? this.isChecked,
); );
} }
/// Problem Map便 /// MapSQLite存储
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'description': description, 'description': description,
'location': location, 'location': location,
// 使 jsonEncode JSON 'imageUrls': json.encode(imageUrls.map((e) => e.toMap()).toList()),
'imageUrls': jsonEncode(imageUrls.map((meta) => meta.toMap()).toList()),
'creationTime': creationTime.millisecondsSinceEpoch, 'creationTime': creationTime.millisecondsSinceEpoch,
'syncStatus': syncStatus.index, 'syncStatus': syncStatus.index,
'operation': operation.index, // operation 'operation': operation.index,
'isDeleted': isDeleted ? 1 : 0,
'censorTaskId': censorTaskId, 'censorTaskId': censorTaskId,
'bindData': bindData, 'bindData': bindData,
'isChecked': isChecked ? 1 : 0, // isChecked 'isChecked': isChecked ? 1 : 0,
}; };
} }
/// Map Problem /// Map创建对象SQLite读取
factory Problem.fromMap(Map<String, dynamic> map) { factory Problem.fromMap(Map<String, dynamic> map) {
// imageUrls的转换
List<ImageMetadata> imageUrlsList = [];
if (map['imageUrls'] != null) {
try {
final List<dynamic> imageList = json.decode(map['imageUrls']);
imageUrlsList = imageList.map((e) => ImageMetadata.fromMap(e)).toList();
} catch (e) {
//
imageUrlsList = [];
}
}
return Problem( return Problem(
id: map['id'], id: map['id'],
description: map['description'], description: map['description'],
location: map['location'], location: map['location'],
// jsonDecode JSON ImageMetadata imageUrls: imageUrlsList,
imageUrls: (jsonDecode(map['imageUrls']) as List) creationTime: DateTime.fromMillisecondsSinceEpoch(map['creationTime']),
.map((item) => ImageMetadata.fromMap(item as Map<String, dynamic>)) syncStatus: SyncStatus.values[map['syncStatus']],
.toList(), operation: Operation.values[map['operation']],
creationTime: DateTime.fromMillisecondsSinceEpoch( isDeleted: map['isDeleted'] == 1,
map['creationTime'] as int,
),
syncStatus: SyncStatus.values[map['syncStatus'] as int],
operation:
map.containsKey('operation') // operation
? Operation.values[map['operation'] as int]
: Operation.create, //
censorTaskId: map['censorTaskId'], censorTaskId: map['censorTaskId'],
bindData: map['bindData'], 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));
} }

14
lib/data/models/sync_status.dart

@ -5,17 +5,3 @@ enum SyncStatus {
/// ///
notSynced, notSynced,
} }
//
extension SyncStatusExtension on SyncStatus {
String get displayName {
switch (this) {
case SyncStatus.synced:
return '已上传';
case SyncStatus.notSynced:
return '未上传';
}
}
bool get isSynced => this == SyncStatus.synced;
}

218
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:problem_check_system/data/models/problem_model.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
/// `SQLiteProvider` GetxService SQLite /// `SQLiteProvider` GetxService SQLite
/// ///
class SQLiteProvider extends GetxService { class SQLiteProvider extends GetxService {
static const String _dbName = 'problems.db'; static const String _dbName = 'problems.db';
static const String _tableName = 'problems'; static const String _tableName = 'problems';
///
static const int _dbVersion = 1; static const int _dbVersion = 1;
/// 访
late Database _database; late Database _database;
///
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_initDatabase(); _initDatabase();
} }
/// ///
Future<void> _initDatabase() async { Future<void> _initDatabase() async {
try { try {
final databasePath = await getDatabasesPath(); final databasePath = await getDatabasesPath();
@ -36,16 +31,17 @@ class SQLiteProvider extends GetxService {
path, path,
version: _dbVersion, version: _dbVersion,
onCreate: _onCreate, onCreate: _onCreate,
onUpgrade: _onUpgrade, // onUpgrade: _onUpgrade,
); );
Get.log('数据库初始化成功');
} catch (e) { } catch (e) {
// Get.log('数据库初始化失败:$e');
Get.log('数据库初始化失败:$e', isError: true); Get.log('数据库初始化失败:$e', isError: true);
rethrow; rethrow;
} }
} }
/// ///
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
await db.execute(''' await db.execute('''
CREATE TABLE $_tableName( CREATE TABLE $_tableName(
@ -56,126 +52,187 @@ class SQLiteProvider extends GetxService {
creationTime INTEGER NOT NULL, creationTime INTEGER NOT NULL,
syncStatus INTEGER NOT NULL, syncStatus INTEGER NOT NULL,
operation INTEGER NOT NULL, operation INTEGER NOT NULL,
isDeleted INTEGER NOT NULL,
censorTaskId TEXT, censorTaskId TEXT,
bindData TEXT, bindData TEXT,
isChecked INTEGER NOT NULL isChecked INTEGER NOT NULL
) )
'''); ''');
Get.log('数据库表创建成功');
} }
/// ///
///
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async { Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...'); Get.log('正在将数据库从版本 $oldVersion 升级到 $newVersion...');
if (oldVersion < 2) {
// 1 2 'newColumn' //
// await db.execute('ALTER TABLE $_tableName ADD COLUMN newColumn TEXT;'); for (int version = oldVersion + 1; version <= newVersion; version++) {
// Get.log('已添加新列: newColumn'); await _runMigration(db, version);
}
// if (oldVersion < 3) {
// ...
// }
Get.log('数据库升级完成。');
} }
// --- Get.log('数据库升级完成');
}
/// ** (CRUD) ** ///
Future<void> _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<int>`ID 0
Future<int> insertProblem(Problem problem) async { Future<int> insertProblem(Problem problem) async {
try { try {
//
final problemToInsert = problem.copyWith( final problemToInsert = problem.copyWith(
id: problem.id ?? const Uuid().v4(), syncStatus: SyncStatus.notSynced,
); );
return await _database.insert(
final result = await _database.insert(
_tableName, _tableName,
problemToInsert.toMap(), problemToInsert.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
); );
Get.log('问题记录插入成功,ID: ${problem.id}');
return result;
} catch (e) { } catch (e) {
Get.log('插入问题失败:$e', isError: true); Get.log('插入问题失败(ID: ${problem.id}$e', isError: true);
return 0; // 0 return 0;
} }
} }
/// ID ///
///
/// `id` - ID
/// `Future<int>`
Future<int> deleteProblem(String id) async { Future<int> deleteProblem(String id) async {
try { try {
return await _database.delete( final result = await _database.update(
_tableName, _tableName,
{
'isDeleted': 1,
'syncStatus': SyncStatus.notSynced.index, //
},
where: 'id = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [id],
); );
if (result > 0) {
Get.log('问题逻辑删除成功,ID: $id');
}
return result;
} catch (e) { } catch (e) {
Get.log('删除问题(ID: $id)失败:$e', isError: true); Get.log('逻辑删除问题失败(ID: $id):$e', isError: true);
return 0; return 0;
} }
} }
/// ///
///
/// `problem` - `Problem`
/// `Future<int>`
Future<int> updateProblem(Problem problem) async { Future<int> updateProblem(Problem problem) async {
try { try {
return await _database.update( //
final problemToUpdate = problem.copyWith(
syncStatus: SyncStatus.notSynced,
);
final result = await _database.update(
_tableName, _tableName,
problem.toMap(), problemToUpdate.toMap(),
where: 'id = ?', where: 'id = ?',
whereArgs: [problem.id], whereArgs: [problem.id],
); );
if (result > 0) {
Get.log('问题更新成功,ID: ${problem.id}');
}
return result;
} catch (e) { } catch (e) {
Get.log('更新问题(ID: ${problem.id})失败:$e', isError: true); Get.log('更新问题失败(ID: ${problem.id}):$e', isError: true);
return 0; return 0;
} }
} }
/// ID ///
/// Future<List<Problem>> getProblemsForSync() async {
/// `id` - ID try {
/// `Future<Problem?>` `Problem` `null` 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<int> 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: $id):$e', isError: true);
return 0;
}
}
/// ID获取问题记录
Future<Problem?> getProblemById(String id) async { Future<Problem?> getProblemById(String id) async {
try { try {
final List<Map<String, dynamic>> maps = await _database.query( final results = await _database.query(
_tableName, _tableName,
where: 'id = ?', where: 'id = ? AND isDeleted = 0',
whereArgs: [id], whereArgs: [id],
limit: 1, limit: 1,
); );
if (maps.isNotEmpty) {
return Problem.fromMap(maps.first); return results.isNotEmpty ? Problem.fromMap(results.first) : null;
}
return null;
} catch (e) { } catch (e) {
Get.log('获取问题(ID: $id)失败:$e', isError: true); Get.log('获取问题失败(ID: $id):$e', isError: true);
return null; return null;
} }
} }
/// ///
///
///
/// - `startDate`:
/// - `endDate`:
/// - `syncStatus`:
///
/// `Future<List<Problem>>`
Future<List<Problem>> getProblems({ Future<List<Problem>> getProblems({
DateTime? startDate, DateTime? startDate,
DateTime? endDate, DateTime? endDate,
SyncStatus? syncStatus, String? syncStatus,
String? bindStatus,
bool includeDeleted = false,
}) async { }) async {
try { try {
final List<String> whereClauses = []; final whereClauses = <String>[];
final List<dynamic> whereArgs = []; final whereArgs = <dynamic>[];
//
if (!includeDeleted) {
whereClauses.add('isDeleted = 0');
}
//
if (startDate != null) { if (startDate != null) {
whereClauses.add('creationTime >= ?'); whereClauses.add('creationTime >= ?');
whereArgs.add(startDate.millisecondsSinceEpoch); whereArgs.add(startDate.millisecondsSinceEpoch);
@ -186,36 +243,43 @@ class SQLiteProvider extends GetxService {
whereArgs.add(endDate.millisecondsSinceEpoch); whereArgs.add(endDate.millisecondsSinceEpoch);
} }
if (syncStatus != null) { //
if (syncStatus != null && syncStatus != '全部') {
final statusValue = syncStatus == '已上传'
? SyncStatus.synced.index
: SyncStatus.notSynced.index;
whereClauses.add('syncStatus = ?'); whereClauses.add('syncStatus = ?');
whereArgs.add(syncStatus.index); whereArgs.add(statusValue);
} }
final String? whereString = whereClauses.isNotEmpty //
? whereClauses.join(' AND ') if (bindStatus != null && bindStatus != '全部') {
: null; if (bindStatus == '已绑定') {
whereClauses.add('bindData IS NOT NULL AND bindData != ""');
} else {
whereClauses.add('(bindData IS NULL OR bindData = "")');
}
}
final List<Map<String, dynamic>> maps = await _database.query( final results = await _database.query(
_tableName, _tableName,
where: whereString, where: whereClauses.isNotEmpty ? whereClauses.join(' AND ') : null,
whereArgs: whereArgs.isEmpty ? null : whereArgs, whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'creationTime DESC', orderBy: 'creationTime DESC',
); );
return maps.map((json) => Problem.fromMap(json)).toList(); return results.map((json) => Problem.fromMap(json)).toList();
} catch (e) { } catch (e) {
Get.log('获取问题列表失败:$e', isError: true); Get.log('获取问题列表失败:$e', isError: true);
return []; return [];
} }
} }
// ---
/// `GetxService`
///
@override @override
void onClose() { void onClose() {
_database.close(); _database.close();
Get.log('数据库连接已关闭');
super.onClose(); super.onClose();
} }
} }

8
lib/data/repositories/problem_repository.dart

@ -49,8 +49,8 @@ class ProblemRepository extends GetxService {
Future getProblems({ Future getProblems({
DateTime? startDate, DateTime? startDate,
DateTime? endDate, DateTime? endDate,
String uploadStatus = '全部', String? uploadStatus,
String bindStatus = '全部', String? bindStatus,
}) async { }) async {
return await sqliteProvider.getProblems( return await sqliteProvider.getProblems(
startDate: startDate, startDate: startDate,
@ -202,4 +202,8 @@ class ProblemRepository extends GetxService {
rethrow; rethrow;
} }
} }
getProblemsForSync() {
return sqliteProvider.getProblemsForSync();
}
} }

209
lib/modules/problem/controllers/problem_controller.dart

@ -1,5 +1,6 @@
// modules/problem/controllers/problem_controller.dart // modules/problem/controllers/problem_controller.dart
import 'dart:developer'; import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_screenutil/flutter_screenutil.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:flutter/material.dart';
import 'package:problem_check_system/app/routes/app_routes.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/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/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 class ProblemController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
@ -28,15 +30,30 @@ class ProblemController extends GetxController
// Dio // Dio
late CancelToken _cancelToken; late CancelToken _cancelToken;
int get selectedCount { final RxSet<Problem> _selectedProblems = <Problem>{}.obs;
return unUploadedProblems.where((problem) => problem.isChecked).length;
} Set<Problem> get selectedProblems => _selectedProblems;
int get selectedCount => _selectedProblems.length;
List<Problem> get selectedProblems { // ProblemController
return unUploadedProblems.where((problem) => problem.isChecked).toList(); //
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 Rx<DateRange> currentDateRange = DateRange.oneWeek.obs;
final RxString currentUploadFilter = '全部'.obs; final RxString currentUploadFilter = '全部'.obs;
final RxString currentBindFilter = '全部'.obs; final RxString currentBindFilter = '全部'.obs;
@ -66,6 +83,7 @@ class ProblemController extends GetxController
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
tabController = TabController(length: 2, vsync: this); tabController = TabController(length: 2, vsync: this);
tabController.addListener(_onTabChanged); tabController.addListener(_onTabChanged);
loadProblems(); loadProblems();
@ -81,33 +99,36 @@ class ProblemController extends GetxController
// #region // #region
// Controller void updateProblemSelection(Problem problem, bool isChecked) {
void updateProblemCheckedStatus(Rx<Problem> problem, bool isChecked) { if (isChecked) {
// 1. 使 copyWith Problem _selectedProblems.add(problem);
final updatedProblem = problem.value.copyWith(isChecked: isChecked); } else {
_selectedProblems.remove(problem);
// 2. Rx<Problem> UI }
problem.value = updatedProblem; //
allSelected.value = _selectedProblems.length == unUploadedProblems.length;
} }
void selectAll() { void selectAll() {
final bool newState = !allSelected.value; if (allSelected.value) {
//
// 使 .map() _selectedProblems.clear();
final updatedProblems = unUploadedProblems.map((problem) { } else {
return problem.copyWith(isChecked: newState); //
}).toList(); _selectedProblems.addAll(unUploadedProblems);
}
// 使 assignAll allSelected.value = !allSelected.value;
unUploadedProblems.assignAll(updatedProblems); }
// //
allSelected.value = newState; void clearSelection() {
_selectedProblems.clear();
allSelected.value = false;
} }
// // handleUpload clearSelection
Future<void> handleUpload() async { Future<void> handleUpload() async {
if (selectedProblems.isEmpty) { if (_selectedProblems.isEmpty) {
Get.snackbar('提示', '请选择要上传的问题'); Get.snackbar('提示', '请选择要上传的问题');
return; return;
} }
@ -115,13 +136,11 @@ class ProblemController extends GetxController
uploadProgress.value = 0.0; uploadProgress.value = 0.0;
_cancelToken = CancelToken(); _cancelToken = CancelToken();
//
showUploadProgressDialog(); showUploadProgressDialog();
try { try {
// Repository
await problemRepository.uploadProblems( await problemRepository.uploadProblems(
selectedProblems, _selectedProblems.toList(), //
cancelToken: _cancelToken, cancelToken: _cancelToken,
onProgress: (progress) { onProgress: (progress) {
uploadProgress.value = progress; uploadProgress.value = progress;
@ -130,6 +149,11 @@ class ProblemController extends GetxController
Get.back(); Get.back();
Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM); Get.snackbar('成功', '所有问题已成功上传!', snackPosition: SnackPosition.BOTTOM);
//
clearSelection();
//
loadUnUploadedProblems();
} on DioException catch (e) { } on DioException catch (e) {
Get.back(); Get.back();
if (CancelToken.isCancel(e)) { if (CancelToken.isCancel(e)) {
@ -259,7 +283,27 @@ class ProblemController extends GetxController
loadProblems(); loadProblems();
} }
} }
// #endregion // #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<void> loadProblems() async { Future<void> 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<void> loadUnUploadedProblems() async { Future<void> loadUnUploadedProblems() async {
isLoading.value = true; isLoading.value = true;
try { try {
// _localDatabase.getProblems '未上传' // _localDatabase.getProblems '未上传'
unUploadedProblems.value = await problemRepository.getProblems( unUploadedProblems.value = await problemRepository.getProblemsForSync();
uploadStatus: '未上传',
);
} catch (e) { } catch (e) {
Get.snackbar('错误', '加载未上传问题失败: $e'); Get.snackbar('错误', '加载未上传问题失败: $e');
} finally { } finally {
@ -364,25 +362,25 @@ class ProblemController extends GetxController
} }
} }
Future<void> addProblem(Problem problem) async { // Future<void> addProblem(Problem problem) async {
try { // try {
await problemRepository.insertProblem(problem); // await problemRepository.insertProblem(problem);
loadProblems(); // loadProblems();
} catch (e) { // } catch (e) {
Get.snackbar('错误', '保存问题失败: $e'); // Get.snackbar('错误', '保存问题失败: $e');
rethrow; // rethrow;
} // }
} // }
Future<void> updateProblem(Problem problem) async { // Future<void> updateProblem(Problem problem) async {
try { // try {
await problemRepository.updateProblem(problem); // await problemRepository.updateProblem(problem);
loadProblems(); // loadProblems();
} catch (e) { // } catch (e) {
Get.snackbar('错误', '更新问题失败: $e'); // Get.snackbar('错误', '更新问题失败: $e');
rethrow; // rethrow;
} // }
} // }
Future<void> deleteProblem(Problem problem) async { Future<void> deleteProblem(Problem problem) async {
try { try {
@ -397,17 +395,18 @@ class ProblemController extends GetxController
} }
} }
//
Future<void> _deleteProblemImages(Problem problem) async { Future<void> _deleteProblemImages(Problem problem) async {
// for (var imagePath in problem.imageUrls) { for (var imagePath in problem.imageUrls) {
// try { try {
// final file = File(imagePath); final file = File(imagePath.localPath);
// if (await file.exists()) { if (await file.exists()) {
// await file.delete(); await file.delete();
// } }
// } catch (e) { } catch (e) {
// throw Exception(e); throw Exception(e);
// } }
// } }
} }
/// ///

5
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:permission_handler/permission_handler.dart';
import 'package:path_provider/path_provider.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/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 'package:problem_check_system/data/models/image_metadata_model.dart';
import 'dart:io'; import 'dart:io';
import 'package:problem_check_system/data/models/problem_model.dart'; import 'package:problem_check_system/data/models/problem_model.dart';
@ -151,7 +150,6 @@ class ProblemFormController extends GetxController {
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
creationTime: DateTime.now(), //
); );
await problemRepository.updateProblem(updatedProblem); await problemRepository.updateProblem(updatedProblem);
@ -159,11 +157,10 @@ class ProblemFormController extends GetxController {
Get.snackbar('成功', '问题已更新'); Get.snackbar('成功', '问题已更新');
} else { } else {
// //
final problem = Problem( final problem = Problem.create(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imageUrls: imagePaths, imageUrls: imagePaths,
creationTime: DateTime.now(),
); );
await problemRepository.insertProblem(problem); await problemRepository.insertProblem(problem);

30
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'; import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
class ProblemListPage extends GetView<ProblemController> { class ProblemListPage extends GetView<ProblemController> {
//
final RxList<Problem> problemsToShow; final RxList<Problem> problemsToShow;
final ProblemCardViewType viewType; final ProblemCardViewType viewType;
@ -25,11 +24,11 @@ class ProblemListPage extends GetView<ProblemController> {
return ListView.builder( return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 17.w), padding: EdgeInsets.symmetric(horizontal: 17.w),
itemCount: problemsToShow.length + 1, itemCount: problemsToShow.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == problemsToShow.length) { // if (index == problemsToShow.length) {
return SizedBox(height: 79.5.h); // return SizedBox(height: 79.5.h);
} // }
final problem = problemsToShow[index]; final problem = problemsToShow[index];
return _buildSwipeableProblemCard(problem); return _buildSwipeableProblemCard(problem);
}, },
@ -56,10 +55,27 @@ class ProblemListPage extends GetView<ProblemController> {
controller.deleteProblem(problem); controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除'); Get.snackbar('成功', '问题已删除');
}, },
child: ProblemCard(problem: problem.obs, viewType: viewType), child: ProblemCard(
key: ValueKey(problem.id),
problem: problem,
viewType: viewType,
isSelected: false, // false
),
); );
} else { } 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);
},
);
});
} }
} }

87
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/app/routes/app_routes.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.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/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/compact_filter_bar.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_string_dropdown.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'; // import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; //
class ProblemPage extends GetView<ProblemController> { class ProblemPage extends GetView<ProblemController> {
@ -68,39 +68,13 @@ class ProblemPage extends GetView<ProblemController> {
decoration: BoxDecoration(color: Color(0xfff7f7f7)), decoration: BoxDecoration(color: Color(0xfff7f7f7)),
child: Column( child: Column(
children: [ children: [
Container( CompactFilterBar(
padding: EdgeInsets.symmetric(horizontal: 17.w), showDateRangeFilter: true,
child: Row( showUploadFilter: true,
children: [ showBindFilter: true,
CustomDateRangeDropdown( padding: EdgeInsets.symmetric(
selectedRange: controller.currentDateRange, horizontal: 17.w,
onChanged: (rangeValue) { vertical: 0.h,
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,
);
},
),
],
), ),
), ),
@ -119,37 +93,20 @@ class ProblemPage extends GetView<ProblemController> {
decoration: BoxDecoration(color: Color(0xfff7f7f7)), decoration: BoxDecoration(color: Color(0xfff7f7f7)),
child: Column( child: Column(
children: [ children: [
Container( CompactFilterBar(
padding: EdgeInsets.symmetric(horizontal: 17.w), showDateRangeFilter: false,
child: Row( showUploadFilter: true,
children: [ showBindFilter: true,
// showCustomButton: true,
ElevatedButton( customButtonIcon: Icons.date_range,
onPressed: () => customButtonText: "选择日期",
controller.selectDateRange(context), onCustomButtonPressed: () {
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,
);
}, },
), // padding: EdgeInsets.symmetric(
], // horizontal: 0.w,
), // vertical: 0.h,
// ),
), ),
Expanded( Expanded(

29
lib/modules/problem/views/problem_upload_page.dart

@ -17,7 +17,6 @@ class ProblemUploadPage extends GetView<ProblemController> {
); );
} }
// AppBar
PreferredSizeWidget _buildAppBar() { PreferredSizeWidget _buildAppBar() {
return AppBar( return AppBar(
title: Obx(() { title: Obx(() {
@ -27,12 +26,18 @@ class ProblemUploadPage extends GetView<ProblemController> {
centerTitle: true, centerTitle: true,
leading: IconButton(icon: Icon(Icons.close), onPressed: () => Get.back()), leading: IconButton(icon: Icon(Icons.close), onPressed: () => Get.back()),
actions: [ actions: [
TextButton( Obx(
onPressed: controller.selectAll, () => TextButton(
child: Obx( onPressed: controller.unUploadedProblems.isNotEmpty
() => Text( ? controller.selectAll
: null,
child: Text(
controller.allSelected.value ? '取消全选' : '全选', 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<ProblemController> {
); );
} }
//
Widget _buildBody() { Widget _buildBody() {
return Obx(() {
if (controller.unUploadedProblems.isEmpty) {
return Center(
child: Text('暂无未上传的问题', style: TextStyle(fontSize: 16.sp)),
);
}
return ProblemListPage( return ProblemListPage(
problemsToShow: controller.unUploadedProblems, problemsToShow: controller.unUploadedProblems,
viewType: ProblemCardViewType.checkbox, viewType: ProblemCardViewType.checkbox,
); );
});
} }
//
Widget _buildBottomBar() { Widget _buildBottomBar() {
return Container( return Container(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(16.w),
@ -67,8 +77,9 @@ class ProblemUploadPage extends GetView<ProblemController> {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r), borderRadius: BorderRadius.circular(8.r),
), ),
minimumSize: Size(double.infinity, 48.h),
), ),
child: Text('点击上传'), child: Text('点击上传 (${controller.selectedCount})'),
), ),
), ),
); );

122
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<ProblemController> {
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,
),
),
],
),
),
);
}
}

89
lib/modules/problem/views/widgets/custom_data_range_dropdown.dart

@ -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<DateRange> 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<DateRange>(
value: selectedRange.value,
onChanged: (DateRange? newValue) {
if (newValue != null) {
onChanged(newValue);
}
},
items: DateRange.values.map((DateRange range) {
return DropdownMenuItem<DateRange>(
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(),
),
),
),
);
}
}

77
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<DropdownOption> options;
final String selectedValue;
final ValueChanged<String> 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<String>(
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<String>(
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);
}
},
),
),
);
}
}

16
lib/modules/problem/views/widgets/custom_string_dropdown.dart → 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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.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 RxString selectedValue;
final List<String> items; final List<DropdownOption> items;
final Function(String) onChanged; final Function(String) onChanged;
const CustomStringDropdown({ const CustomObjectDropdown({
super.key, super.key,
required this.selectedValue, required this.selectedValue,
required this.items, required this.items,
@ -36,8 +35,11 @@ class CustomStringDropdown extends StatelessWidget {
onChanged(newValue); onChanged(newValue);
} }
}, },
items: items.map((String value) { items: items.map((DropdownOption option) {
return DropdownMenuItem<String>(value: value, child: Text(value)); return DropdownMenuItem<String>(
value: option.value,
child: Text(option.label),
);
}).toList(), }).toList(),
icon: const Icon(Icons.arrow_drop_down, size: 15), icon: const Icon(Icons.arrow_drop_down, size: 15),
style: TextStyle(fontSize: 10.sp, color: Colors.black), style: TextStyle(fontSize: 10.sp, color: Colors.black),

68
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;
}
}

23
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;
}

78
lib/modules/problem/views/widgets/problem_card.dart

@ -4,7 +4,6 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:problem_check_system/data/models/sync_status.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/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/widgets/custom_button.dart';
import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; import 'package:problem_check_system/modules/problem/views/problem_form_page.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
@ -12,20 +11,30 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
// //
enum ProblemCardViewType { buttons, checkbox } enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends GetView<ProblemController> { class ProblemCard extends StatelessWidget {
final Rx<Problem> problem; final Problem problem;
final ProblemCardViewType viewType; final ProblemCardViewType viewType;
final Function(Problem, bool)? onChanged;
final bool isSelected; //
const ProblemCard({ const ProblemCard({
super.key, super.key,
required this.problem, required this.problem,
this.viewType = ProblemCardViewType.buttons, this.viewType = ProblemCardViewType.buttons,
this.onChanged,
required this.isSelected, //
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
// margin: EdgeInsets.symmetric(vertical: 5.h, horizontal: 9.w), child: InkWell(
onTap: viewType == ProblemCardViewType.checkbox
? () {
//
onChanged?.call(problem, !isSelected);
}
: null,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -38,7 +47,7 @@ class ProblemCard extends GetView<ProblemController> {
subtitle: LayoutBuilder( subtitle: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Text( return Text(
problem.value.description, problem.description,
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp), style: TextStyle(fontSize: 14.sp),
@ -50,28 +59,27 @@ class ProblemCard extends GetView<ProblemController> {
Row( Row(
children: [ children: [
SizedBox(width: 16.w), SizedBox(width: 16.w),
Row(
children: [
Icon(Icons.location_on, color: Colors.grey, size: 16.h), Icon(Icons.location_on, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( SizedBox(
problem.value.location, width: 100.w,
child: Text(
problem.location,
style: TextStyle(fontSize: 12.sp), style: TextStyle(fontSize: 12.sp),
overflow: TextOverflow.ellipsis,
maxLines: 1,
), ),
],
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
Row(
children: [
Icon(Icons.access_time, color: Colors.grey, size: 16.h), Icon(Icons.access_time, color: Colors.grey, size: 16.h),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( SizedBox(
DateFormat( width: 100.w,
'yyyy-MM-dd HH:mm', child: Text(
).format(problem.value.creationTime), DateFormat('yyyy-MM-dd HH:mm').format(problem.creationTime),
style: TextStyle(fontSize: 12.sp), style: TextStyle(fontSize: 12.sp),
overflow: TextOverflow.ellipsis,
), ),
],
), ),
], ],
), ),
@ -83,26 +91,29 @@ class ProblemCard extends GetView<ProblemController> {
Wrap( Wrap(
spacing: 8, spacing: 8,
children: [ children: [
problem.value.syncStatus == SyncStatus.synced problem.syncStatus == SyncStatus.synced
? TDTag('已上传', isLight: true, theme: TDTagTheme.success) ? TDTag('已上传', isLight: true, theme: TDTagTheme.success)
: TDTag('未上传', isLight: true, theme: TDTagTheme.danger), : TDTag('未上传', isLight: true, theme: TDTagTheme.danger),
problem.value.bindData != null && problem.bindData != null && problem.bindData!.isNotEmpty
problem.value.bindData!.isNotEmpty
? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary) ? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary)
: TDTag('未绑定', isLight: true, theme: TDTagTheme.warning), : TDTag(
'未绑定',
isLight: true,
theme: TDTagTheme.warning,
),
], ],
), ),
const Spacer(), // 使 Spacer SizedBox const Spacer(),
_buildBottomActions(), _buildBottomActions(), //
], ],
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
], ],
), ),
),
); );
} }
// UI
Widget _buildBottomActions() { Widget _buildBottomActions() {
switch (viewType) { switch (viewType) {
case ProblemCardViewType.buttons: case ProblemCardViewType.buttons:
@ -111,16 +122,14 @@ class ProblemCard extends GetView<ProblemController> {
CustomButton( CustomButton(
text: '修改', text: '修改',
onTap: () { onTap: () {
Get.to(ProblemFormPage(problem: problem.value)); Get.to(ProblemFormPage(problem: problem));
}, },
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
CustomButton( CustomButton(
text: '查看', text: '查看',
onTap: () { onTap: () {
Get.to( Get.to(ProblemFormPage(problem: problem, isReadOnly: true));
ProblemFormPage(problem: problem.value, isReadOnly: true),
);
}, },
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
@ -129,17 +138,14 @@ class ProblemCard extends GetView<ProblemController> {
case ProblemCardViewType.checkbox: case ProblemCardViewType.checkbox:
return Padding( return Padding(
padding: EdgeInsets.only(right: 16.w), padding: EdgeInsets.only(right: 16.w),
child: Obx( child: Checkbox(
() => Checkbox( value: isSelected, // 使
// Checkbox value controller.isChecked.value
value: problem.value.isChecked,
// Checkbox controller
onChanged: (bool? value) { onChanged: (bool? value) {
// Controller if (value != null) {
controller.updateProblemCheckedStatus(problem, value ?? false); onChanged?.call(problem, value);
}
}, },
), ),
),
); );
} }
} }

Loading…
Cancel
Save