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,
}
/// 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 '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<ImageMetadata> 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便
/// MapSQLite存储
Map<String, dynamic> 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<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(
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<String, dynamic>))
.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));
}

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

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: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<void> _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<void> _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<void> _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');
}
// if (oldVersion < 3) {
// ...
// }
Get.log('数据库升级完成。');
//
for (int version = oldVersion + 1; version <= newVersion; version++) {
await _runMigration(db, version);
}
// ---
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 {
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<int>`
///
Future<int> 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<int>`
///
Future<int> 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);
Get.log('更新问题失败(ID: ${problem.id}):$e', isError: true);
return 0;
}
}
/// ID
///
/// `id` - ID
/// `Future<Problem?>` `Problem` `null`
///
Future<List<Problem>> 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<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 {
try {
final List<Map<String, dynamic>> 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<List<Problem>>`
///
Future<List<Problem>> getProblems({
DateTime? startDate,
DateTime? endDate,
SyncStatus? syncStatus,
String? syncStatus,
String? bindStatus,
bool includeDeleted = false,
}) async {
try {
final List<String> whereClauses = [];
final List<dynamic> whereArgs = [];
final whereClauses = <String>[];
final whereArgs = <dynamic>[];
//
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<Map<String, dynamic>> 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();
}
}

8
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();
}
}

209
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<Problem> _selectedProblems = <Problem>{}.obs;
Set<Problem> get selectedProblems => _selectedProblems;
int get selectedCount => _selectedProblems.length;
List<Problem> get selectedProblems {
return unUploadedProblems.where((problem) => problem.isChecked).toList();
// ProblemController
//
List<DropdownOption> get dateRangeOptions {
return DateRange.values.map((range) => range.toDropdownOption()).toList();
}
///
final List<DropdownOption> uploadOptions = const [
DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive),
DropdownOption(label: '已上传', value: '已上传', icon: Icons.cloud_done),
DropdownOption(label: '未上传', value: '未上传', icon: Icons.cloud_off),
];
final List<DropdownOption> bindOptions = const [
DropdownOption(label: '全部', value: '全部', icon: Icons.all_inclusive),
DropdownOption(label: '已绑定', value: '已绑定', icon: Icons.link),
DropdownOption(label: '未绑定', value: '未绑定', icon: Icons.link_off),
];
final Rx<DateRange> currentDateRange = DateRange.oneWeek.obs;
final RxString currentUploadFilter = '全部'.obs;
final RxString currentBindFilter = '全部'.obs;
@ -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> problem, bool isChecked) {
// 1. 使 copyWith Problem
final updatedProblem = problem.value.copyWith(isChecked: isChecked);
// 2. Rx<Problem> 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<void> 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<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 {
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<void> addProblem(Problem problem) async {
try {
await problemRepository.insertProblem(problem);
loadProblems();
} catch (e) {
Get.snackbar('错误', '保存问题失败: $e');
rethrow;
}
}
// Future<void> addProblem(Problem problem) async {
// try {
// await problemRepository.insertProblem(problem);
// loadProblems();
// } catch (e) {
// Get.snackbar('错误', '保存问题失败: $e');
// rethrow;
// }
// }
Future<void> updateProblem(Problem problem) async {
try {
await problemRepository.updateProblem(problem);
loadProblems();
} catch (e) {
Get.snackbar('错误', '更新问题失败: $e');
rethrow;
}
}
// Future<void> updateProblem(Problem problem) async {
// try {
// await problemRepository.updateProblem(problem);
// loadProblems();
// } catch (e) {
// Get.snackbar('错误', '更新问题失败: $e');
// rethrow;
// }
// }
Future<void> deleteProblem(Problem problem) async {
try {
@ -397,17 +395,18 @@ class ProblemController extends GetxController
}
}
//
Future<void> _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);
}
}
}
///

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: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);

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';
class ProblemListPage extends GetView<ProblemController> {
//
final RxList<Problem> problemsToShow;
final ProblemCardViewType viewType;
@ -25,11 +24,11 @@ class ProblemListPage extends GetView<ProblemController> {
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<ProblemController> {
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);
},
);
});
}
}

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/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<ProblemController> {
@ -68,39 +68,13 @@ class ProblemPage extends GetView<ProblemController> {
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<ProblemController> {
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(

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

@ -17,7 +17,6 @@ class ProblemUploadPage extends GetView<ProblemController> {
);
}
// AppBar
PreferredSizeWidget _buildAppBar() {
return AppBar(
title: Obx(() {
@ -27,12 +26,18 @@ class ProblemUploadPage extends GetView<ProblemController> {
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<ProblemController> {
);
}
//
Widget _buildBody() {
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<ProblemController> {
shape: RoundedRectangleBorder(
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_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<String> items;
final List<DropdownOption> 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<String>(value: value, child: Text(value));
items: items.map((DropdownOption option) {
return DropdownMenuItem<String>(
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),

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: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,20 +11,30 @@ import 'package:tdesign_flutter/tdesign_flutter.dart';
//
enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends GetView<ProblemController> {
final Rx<Problem> 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: InkWell(
onTap: viewType == ProblemCardViewType.checkbox
? () {
//
onChanged?.call(problem, !isSelected);
}
: null,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -38,7 +47,7 @@ class ProblemCard extends GetView<ProblemController> {
subtitle: LayoutBuilder(
builder: (context, constraints) {
return Text(
problem.value.description,
problem.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp),
@ -50,28 +59,27 @@ class ProblemCard extends GetView<ProblemController> {
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(
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: 100.w,
child: Text(
DateFormat('yyyy-MM-dd HH:mm').format(problem.creationTime),
style: TextStyle(fontSize: 12.sp),
overflow: TextOverflow.ellipsis,
),
],
),
],
),
@ -83,26 +91,29 @@ class ProblemCard extends GetView<ProblemController> {
Wrap(
spacing: 8,
children: [
problem.value.syncStatus == SyncStatus.synced
problem.syncStatus == SyncStatus.synced
? TDTag('已上传', isLight: true, theme: TDTagTheme.success)
: TDTag('未上传', isLight: true, theme: TDTagTheme.danger),
problem.value.bindData != null &&
problem.value.bindData!.isNotEmpty
problem.bindData != null && problem.bindData!.isNotEmpty
? TDTag('已绑定', isLight: true, theme: TDTagTheme.primary)
: TDTag('未绑定', isLight: true, theme: TDTagTheme.warning),
: TDTag(
'未绑定',
isLight: true,
theme: TDTagTheme.warning,
),
],
),
const Spacer(), // 使 Spacer SizedBox
_buildBottomActions(),
const Spacer(),
_buildBottomActions(), //
],
),
SizedBox(height: 8.h),
],
),
),
);
}
// UI
Widget _buildBottomActions() {
switch (viewType) {
case ProblemCardViewType.buttons:
@ -111,16 +122,14 @@ class ProblemCard extends GetView<ProblemController> {
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,17 +138,14 @@ class ProblemCard extends GetView<ProblemController> {
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
child: Checkbox(
value: isSelected, // 使
onChanged: (bool? value) {
// Controller
controller.updateProblemCheckedStatus(problem, value ?? false);
if (value != null) {
onChanged?.call(problem, value);
}
},
),
),
);
}
}

Loading…
Cancel
Save