Browse Source

fate : 悬浮贴靠按钮

dev
徐振升 2 weeks ago
parent
commit
a2b8abd934
  1. 53
      lib/data/models/problem_model.dart
  2. 66
      lib/data/providers/local_database.dart
  3. 2
      lib/main.dart
  4. 181
      lib/modules/problem/controllers/problem_controller.dart
  5. 8
      lib/modules/problem/controllers/problem_form_controller.dart
  6. 160
      lib/modules/problem/views/problem_list_page.dart
  7. 449
      lib/modules/problem/views/problem_page.dart
  8. 84
      lib/modules/problem/views/widgets/custom_data_range_dropdown.dart
  9. 51
      lib/modules/problem/views/widgets/custom_string_dropdown.dart
  10. 76
      lib/modules/problem/views/widgets/date_picker_button.dart
  11. 2
      lib/modules/problem/views/widgets/problem_card.dart

53
lib/data/models/problem_model.dart

@ -4,10 +4,11 @@ class Problem {
String? id; String? id;
String description; String description;
String location; String location;
List<String> imagePaths; List<String> imageUrls;
DateTime createdAt; DateTime createdAt;
bool isUploaded; bool isUploaded;
String? boundInfo; String? censorTaskId;
String? bindData;
// //
final RxBool isChecked = false.obs; final RxBool isChecked = false.obs;
@ -15,67 +16,63 @@ class Problem {
this.id, this.id,
required this.description, required this.description,
required this.location, required this.location,
required this.imagePaths, required this.imageUrls,
required this.createdAt, required this.createdAt,
this.censorTaskId,
this.bindData,
this.isUploaded = false, this.isUploaded = false,
this.boundInfo,
}); });
// copyWith // copyWith
Problem copyWith({ Problem copyWith({
String? id, String? id,
String? description, String? description,
String? location, String? location,
List<String>? imagePaths, List<String>? imageUrls,
DateTime? createdAt, DateTime? createdAt,
bool? isUploaded, bool? isUploaded,
String? boundInfo, String? censorTaskId,
String? bindData,
}) { }) {
return Problem( return Problem(
id: id ?? this.id, id: id ?? this.id,
description: description ?? this.description, description: description ?? this.description,
location: location ?? this.location, location: location ?? this.location,
imagePaths: imagePaths ?? this.imagePaths, imageUrls: imageUrls ?? this.imageUrls,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
isUploaded: isUploaded ?? this.isUploaded, isUploaded: isUploaded ?? this.isUploaded,
boundInfo: boundInfo ?? this.boundInfo, censorTaskId: censorTaskId ?? this.censorTaskId,
bindData: bindData ?? this.bindData,
); );
} }
// toJson // toMap fromMap
Map<String, dynamic> toJson() {
return {
'id': id,
'description': description,
'location': location,
'imagePaths': imagePaths, // List<String>
'createdAt': createdAt.toIso8601String(),
'isUploaded': isUploaded, // bool
'boundInfo': boundInfo,
};
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'description': description, 'description': description,
'location': location, 'location': location,
'imagePaths': imagePaths.join(';;'), 'imageUrls': imageUrls.join(';;'), // 使 'imageUrls'
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.millisecondsSinceEpoch,
'isUploaded': isUploaded ? 1 : 0, 'isUploaded': isUploaded ? 1 : 0,
'boundInfo': boundInfo, 'censorTaskId': censorTaskId,
'bindData': bindData, // 使 'bindData'
}; };
} }
// fromMap toMap
factory Problem.fromMap(Map<String, dynamic> map) { factory Problem.fromMap(Map<String, dynamic> map) {
return Problem( return Problem(
id: map['id'], id: map['id'],
description: map['description'], description: map['description'],
location: map['location'], location: map['location'],
imagePaths: (map['imagePaths'] as String).split(';;'), imageUrls: (map['imageUrls'] as String).split(
createdAt: DateTime.parse(map['createdAt']), ';;',
), // 使 'imageUrls'
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
isUploaded: map['isUploaded'] == 1, isUploaded: map['isUploaded'] == 1,
boundInfo: map['boundInfo'], censorTaskId: map['censorTaskId'],
bindData: map['bindData'], // 使 'bindData'
); );
} }
} }

66
lib/data/providers/local_database.dart

@ -7,6 +7,7 @@ class LocalDatabase {
static final LocalDatabase _instance = LocalDatabase._internal(); static final LocalDatabase _instance = LocalDatabase._internal();
factory LocalDatabase() => _instance; factory LocalDatabase() => _instance;
static Database? _database; static Database? _database;
static const String _tableName = 'problems';
LocalDatabase._internal(); LocalDatabase._internal();
@ -23,36 +24,29 @@ class LocalDatabase {
Future<void> _onCreate(Database db, int version) async { Future<void> _onCreate(Database db, int version) async {
await db.execute(''' await db.execute('''
CREATE TABLE problems( CREATE TABLE $_tableName(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
description TEXT NOT NULL, description TEXT NOT NULL,
location TEXT NOT NULL, location TEXT NOT NULL,
imagePaths TEXT NOT NULL, imageUrls TEXT NOT NULL,
createdAt TEXT NOT NULL, createdAt INTEGER NOT NULL,
isUploaded INTEGER NOT NULL, isUploaded INTEGER NOT NULL,
boundInfo TEXT censorTaskId TEXT,
bindData TEXT
) )
'''); ''');
} }
Future<int> insertProblem(Problem problem) async { Future<int> insertProblem(Problem problem) async {
final db = await database; final db = await database;
problem.id = Uuid().v4(); problem.id = const Uuid().v4();
return await db.insert('problems', problem.toMap()); return await db.insert(_tableName, problem.toMap());
}
Future<List<Problem>> getProblems() async {
final db = await database;
final List<Map<String, dynamic>> maps = await db.query('problems');
return List.generate(maps.length, (i) {
return Problem.fromMap(maps[i]);
});
} }
Future<int> updateProblem(Problem problem) async { Future<int> updateProblem(Problem problem) async {
final db = await database; final db = await database;
return await db.update( return await db.update(
'problems', _tableName,
problem.toMap(), problem.toMap(),
where: 'id = ?', where: 'id = ?',
whereArgs: [problem.id], whereArgs: [problem.id],
@ -61,16 +55,48 @@ class LocalDatabase {
Future<int> deleteProblem(String id) async { Future<int> deleteProblem(String id) async {
final db = await database; final db = await database;
return await db.delete('problems', where: 'id = ?', whereArgs: [id]); return await db.delete(_tableName, where: 'id = ?', whereArgs: [id]);
} }
Future<List<Problem>> getUnuploadedProblems() async { /// censorTaskId
Future<List<Problem>> getProblems({
required DateTime startDate,
required DateTime endDate,
required String uploadStatus, // '已上传', '未上传', '全部'
required String bindStatus, // '已绑定', '未绑定', '全部'
}) async {
final db = await database; final db = await database;
final int startTimestamp = startDate.millisecondsSinceEpoch;
final int endTimestamp = endDate.millisecondsSinceEpoch;
final List<String> whereClauses = ['createdAt >= ?', 'createdAt <= ?'];
final List<dynamic> whereArgs = [startTimestamp, endTimestamp];
//
if (uploadStatus == '已上传') {
whereClauses.add('isUploaded = ?');
whereArgs.add(1);
} else if (uploadStatus == '未上传') {
whereClauses.add('isUploaded = ?');
whereArgs.add(0);
}
// censorTaskId
if (bindStatus == '已绑定') {
whereClauses.add('censorTaskId IS NOT NULL');
} else if (bindStatus == '未绑定') {
whereClauses.add('censorTaskId IS NULL');
}
final String whereString = whereClauses.join(' AND ');
final List<Map<String, dynamic>> maps = await db.query( final List<Map<String, dynamic>> maps = await db.query(
'problems', _tableName,
where: 'isUploaded = ?', where: whereString.isEmpty ? null : whereString,
whereArgs: [0], whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'createdAt DESC',
); );
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
return Problem.fromMap(maps[i]); return Problem.fromMap(maps[i]);
}); });

2
lib/main.dart

@ -17,6 +17,8 @@ void main() async {
]); ]);
// GetStorage // GetStorage
await GetStorage.init(); await GetStorage.init();
// Add this line
await ScreenUtil.ensureScreenSize();
runApp(const MainApp()); runApp(const MainApp());
} }

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

@ -1,19 +1,31 @@
// modules/problem/controllers/problem_controller.dart // modules/problem/controllers/problem_controller.dart
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart' hide MultipartFile, FormData; import 'package:get/get.dart' hide MultipartFile, FormData;
import 'package:flutter/material.dart';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../../../data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.dart';
import '../../../data/providers/local_database.dart'; import 'package:problem_check_system/data/models/problem_model.dart';
import '../../../data/providers/connectivity_provider.dart'; // import 'package:problem_check_system/data/providers/local_database.dart';
import 'package:problem_check_system/data/providers/connectivity_provider.dart';
class ProblemController extends GetxController { class ProblemController extends GetxController
with GetSingleTickerProviderStateMixin {
final LocalDatabase _localDatabase; final LocalDatabase _localDatabase;
final RxList<Problem> problems = <Problem>[].obs; final RxList<Problem> problems = <Problem>[].obs;
final RxList<Problem> historyProblems = <Problem>[].obs;
final Rx<DateRange> selectedDateRange = DateRange.oneWeek.obs;
final RxString selectedUploadStatus = '全部'.obs;
final RxString selectedBindingStatus = '全部'.obs;
final RxBool isLoading = false.obs; final RxBool isLoading = false.obs;
final Dio _dio; final Dio _dio;
final ConnectivityProvider _connectivityProvider; final ConnectivityProvider _connectivityProvider;
late TabController tabController;
ProblemController({ ProblemController({
required LocalDatabase localDatabase, required LocalDatabase localDatabase,
required Dio dio, required Dio dio,
@ -22,44 +34,151 @@ class ProblemController extends GetxController {
_dio = dio, _dio = dio,
_connectivityProvider = connectivityProvider; _connectivityProvider = connectivityProvider;
// 使 provider isOnline
RxBool get isOnline => _connectivityProvider.isOnline; RxBool get isOnline => _connectivityProvider.isOnline;
List<Problem> get selectedProblems { List<Problem> get selectedProblems {
return problems.where((p) => p.isChecked.value).toList(); return historyProblems.where((p) => p.isChecked.value).toList();
} }
List<Problem> get unuploadedProblems { List<Problem> get unuploadedProblems {
return problems.where((p) => !p.isUploaded).toList(); return problems.where((p) => !p.isUploaded).toList();
} }
// FAB的尺寸和贴靠间距
final double _fabSize = 56.0; // FloatingActionButton的默认尺寸
final double _edgePaddingX = 27.0.w; //
final double _edgePaddingY = 111.0.h; //
// 使 Rx<Offset>
final fabUploadPosition = Offset(301.w, 660.h).obs;
void updateFabUploadPosition(Offset delta) {
final screenWidth = ScreenUtil().screenWidth;
final screenHeight = ScreenUtil().screenHeight;
Offset newPosition = fabUploadPosition.value + delta;
//
double clampedDx = newPosition.dx.clamp(
_edgePaddingX,
screenWidth - _fabSize - _edgePaddingX,
);
//
double clampedDy = newPosition.dy.clamp(
_edgePaddingY,
screenHeight - _fabSize - _edgePaddingY,
);
fabUploadPosition.value = Offset(clampedDx, clampedDy);
}
void snapToEdge() {
final screenWidth = ScreenUtil().screenWidth;
//
final buttonCenterDx = fabUploadPosition.value.dx + _fabSize / 2;
double newDx;
//
if (buttonCenterDx < screenWidth / 2) {
// _edgePaddingX
newDx = _edgePaddingX;
} else {
// _edgePaddingX
newDx = screenWidth - _fabSize - _edgePaddingX;
}
//
fabUploadPosition.value = Offset(newDx, fabUploadPosition.value.dy);
print(fabUploadPosition.value);
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
tabController = TabController(length: 2, vsync: this);
tabController.addListener(_onTabChanged);
loadProblems(); loadProblems();
} }
Future<void> loadProblems() async { @override
void onClose() {
tabController.dispose();
super.onClose();
}
void _onTabChanged() {
if (!tabController.indexIsChanging) {
selectedDateRange.value = DateRange.oneWeek;
selectedUploadStatus.value = '全部';
selectedBindingStatus.value = '全部';
loadProblems();
}
}
void loadProblems() async {
isLoading.value = true; isLoading.value = true;
try { try {
problems.value = await _localDatabase.getProblems(); if (tabController.index == 0) {
// "问题列表" Tab: 使
final startDate = selectedDateRange.value.startDate;
final endDate = DateTime.now();
final problems = await _localDatabase.getProblems(
startDate: startDate,
endDate: endDate,
uploadStatus: selectedUploadStatus.value,
bindStatus: selectedBindingStatus.value,
);
this.problems.assignAll(problems);
} else {
// "历史问题列表" Tab:
final allProblems = await _localDatabase.getProblems(
startDate: DateTime(2000),
endDate: DateTime.now(),
uploadStatus: '全部',
bindStatus: '全部',
);
this.historyProblems.assignAll(allProblems);
}
} catch (e) { } catch (e) {
Get.snackbar('错误', '加载问题失败: $e'); Get.snackbar('错误', '加载问题失败: $e');
rethrow;
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
///
void updateFiltersAndLoadProblems({
DateRange? newDateRange,
String? newUploadStatus,
String? newBindingStatus,
}) {
if (newDateRange != null) {
selectedDateRange.value = newDateRange;
}
if (newUploadStatus != null) {
selectedUploadStatus.value = newUploadStatus;
}
if (newBindingStatus != null) {
selectedBindingStatus.value = newBindingStatus;
}
// loadProblems
if (newDateRange != null ||
newUploadStatus != null ||
newBindingStatus != null) {
loadProblems();
}
}
Future<void> addProblem(Problem problem) async { Future<void> addProblem(Problem problem) async {
try { try {
if (problem.id == null) {
problem = problem.copyWith(
id: DateTime.now().millisecondsSinceEpoch.toString(),
);
}
await _localDatabase.insertProblem(problem); await _localDatabase.insertProblem(problem);
problems.add(problem); loadProblems();
} catch (e) { } catch (e) {
Get.snackbar('错误', '保存问题失败: $e'); Get.snackbar('错误', '保存问题失败: $e');
rethrow; rethrow;
@ -69,10 +188,7 @@ class ProblemController extends GetxController {
Future<void> updateProblem(Problem problem) async { Future<void> updateProblem(Problem problem) async {
try { try {
await _localDatabase.updateProblem(problem); await _localDatabase.updateProblem(problem);
final index = problems.indexWhere((p) => p.id == problem.id); loadProblems();
if (index != -1) {
problems[index] = problem;
}
} catch (e) { } catch (e) {
Get.snackbar('错误', '更新问题失败: $e'); Get.snackbar('错误', '更新问题失败: $e');
rethrow; rethrow;
@ -83,8 +199,8 @@ class ProblemController extends GetxController {
try { try {
if (problem.id != null) { if (problem.id != null) {
await _localDatabase.deleteProblem(problem.id!); await _localDatabase.deleteProblem(problem.id!);
problems.remove(problem);
await _deleteProblemImages(problem); await _deleteProblemImages(problem);
loadProblems();
} }
} catch (e) { } catch (e) {
Get.snackbar('错误', '删除问题失败: $e'); Get.snackbar('错误', '删除问题失败: $e');
@ -100,16 +216,20 @@ class ProblemController extends GetxController {
} }
try { try {
for (var problem in problemsToDelete) { for (var problem in problemsToDelete) {
await deleteProblem(problem); if (problem.id != null) {
await _localDatabase.deleteProblem(problem.id!);
await _deleteProblemImages(problem);
}
} }
Get.snackbar('成功', '已删除${problemsToDelete.length}个问题'); Get.snackbar('成功', '已删除${problemsToDelete.length}个问题');
loadProblems();
} catch (e) { } catch (e) {
Get.snackbar('错误', '删除问题失败: $e'); Get.snackbar('错误', '删除问题失败: $e');
} }
} }
Future<void> _deleteProblemImages(Problem problem) async { Future<void> _deleteProblemImages(Problem problem) async {
for (var imagePath in problem.imagePaths) { for (var imagePath in problem.imageUrls) {
try { try {
final file = File(imagePath); final file = File(imagePath);
if (await file.exists()) { if (await file.exists()) {
@ -127,18 +247,18 @@ class ProblemController extends GetxController {
'description': problem.description, 'description': problem.description,
'location': problem.location, 'location': problem.location,
'createdAt': problem.createdAt.toIso8601String(), 'createdAt': problem.createdAt.toIso8601String(),
'boundInfo': problem.boundInfo ?? '', 'boundInfo': problem.bindData ?? '',
}); });
for (var imagePath in problem.imagePaths) { for (var imageUrl in problem.imageUrls) {
final file = File(imagePath); final file = File(imageUrl);
if (await file.exists()) { if (await file.exists()) {
formData.files.add( formData.files.add(
MapEntry( MapEntry(
'images', 'images',
await MultipartFile.fromFile( await MultipartFile.fromFile(
imagePath, imageUrl,
filename: path.basename(imagePath), filename: path.basename(imageUrl),
), ),
), ),
); );
@ -207,12 +327,13 @@ class ProblemController extends GetxController {
if (successCount < unuploaded.length) { if (successCount < unuploaded.length) {
Get.snackbar('部分成功', '${unuploaded.length - successCount}个问题上传失败'); Get.snackbar('部分成功', '${unuploaded.length - successCount}个问题上传失败');
} }
loadProblems();
} }
Future<void> bindInfoToProblem(String id, String info) async { Future<void> bindInfoToProblem(String id, String info) async {
try { try {
final problem = problems.firstWhere((p) => p.id == id); final problem = historyProblems.firstWhere((p) => p.id == id);
final updatedProblem = problem.copyWith(boundInfo: info); final updatedProblem = problem.copyWith(bindData: info);
await updateProblem(updatedProblem); await updateProblem(updatedProblem);
Get.snackbar('成功', '信息已绑定'); Get.snackbar('成功', '信息已绑定');
} catch (e) { } catch (e) {
@ -221,7 +342,7 @@ class ProblemController extends GetxController {
} }
void clearSelections() { void clearSelections() {
for (var problem in problems) { for (var problem in historyProblems) {
problem.isChecked.value = false; problem.isChecked.value = false;
} }
} }

8
lib/modules/problem/controllers/problem_form_controller.dart

@ -34,7 +34,7 @@ class ProblemFormController extends GetxController {
// //
selectedImages.clear(); selectedImages.clear();
for (var path in problem.imagePaths) { for (var path in problem.imageUrls) {
selectedImages.add(XFile(path)); selectedImages.add(XFile(path));
} }
} else { } else {
@ -151,7 +151,7 @@ class ProblemFormController extends GetxController {
final updatedProblem = _currentProblem!.copyWith( final updatedProblem = _currentProblem!.copyWith(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imagePaths: imagePaths, imageUrls: imagePaths,
createdAt: DateTime.now(), // createdAt: DateTime.now(), //
); );
@ -163,7 +163,7 @@ class ProblemFormController extends GetxController {
final problem = Problem( final problem = Problem(
description: descriptionController.text, description: descriptionController.text,
location: locationController.text, location: locationController.text,
imagePaths: imagePaths, imageUrls: imagePaths,
createdAt: DateTime.now(), createdAt: DateTime.now(),
isUploaded: false, isUploaded: false,
); );
@ -250,7 +250,7 @@ class ProblemFormController extends GetxController {
// //
return descriptionController.text != _currentProblem!.description || return descriptionController.text != _currentProblem!.description ||
locationController.text != _currentProblem!.location || locationController.text != _currentProblem!.location ||
selectedImages.length != _currentProblem!.imagePaths.length; selectedImages.length != _currentProblem!.imageUrls.length;
} }
@override @override

160
lib/modules/problem/views/problem_list_page.dart

@ -1,43 +1,129 @@
// import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
// import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:get/get.dart';
import 'package:problem_check_system/modules/problem/controllers/problem_controller.dart';
import 'package:problem_check_system/data/models/problem_model.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
// class DatePickerController extends GetxController { class ProblemListPage extends GetView<ProblemController> {
// var selectedDateTime = ''.obs; // Reactive variable for selected date-time //
final RxList<Problem> problemsToShow;
final ProblemCardViewType viewType;
// void updateDateTime(Map<String, int> selected) { const ProblemListPage({
// selectedDateTime.value = super.key,
// '${selected['year'].toString().padLeft(4, '0')}-' required this.problemsToShow,
// '${selected['month'].toString().padLeft(2, '0')}-' this.viewType = ProblemCardViewType.buttons,
// '${selected['day'].toString().padLeft(2, '0')} ' });
// '${selected['hour'].toString().padLeft(2, '0')}:'
// '${selected['minute'].toString().padLeft(2, '0')}:'
// '${selected['second'].toString().padLeft(2, '0')}';
// }
// }
// class DatePickerScreen extends StatelessWidget { @override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// DatePickerScreen({super.key}); return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 17.w),
itemCount: problemsToShow.length + 1,
itemBuilder: (context, index) {
if (index == problemsToShow.length) {
return SizedBox(height: 79.5.h);
}
final problem = problemsToShow[index];
return _buildSwipeableProblemCard(problem);
},
);
});
}
// @override Widget _buildSwipeableProblemCard(Problem problem) {
// Widget build(BuildContext context) { // buttons
// return GestureDetector( if (viewType == ProblemCardViewType.buttons) {
// onTap: () {}, return Dismissible(
// child: Obx( key: Key(problem.id ?? UniqueKey().toString()),
// () => buildSelectRow( direction: DismissDirection.endToStart,
// context, background: Container(
// dateController.selectedDateTime.value, color: Colors.red,
// '选择时间', alignment: Alignment.centerRight,
// ), padding: EdgeInsets.only(right: 20.w),
// ), child: Icon(Icons.delete, color: Colors.white, size: 30.sp),
// ); ),
// } confirmDismiss: (direction) async {
return await _showDeleteConfirmationDialog(problem);
},
onDismissed: (direction) {
controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除');
},
child: ProblemCard(problem, viewType: viewType),
);
} else {
//
return ProblemCard(problem, viewType: viewType);
}
}
// Widget buildSelectRow(BuildContext context, String selected, String title) { Future<bool> _showDeleteConfirmationDialog(Problem problem) async {
// return ListTile( return await Get.bottomSheet<bool>(
// title: Text(title), Container(
// subtitle: Text(selected.isNotEmpty ? selected : '未选择'), padding: const EdgeInsets.symmetric(horizontal: 16.0),
// ); decoration: const BoxDecoration(
// } color: Colors.white,
// } borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 16),
const Text(
'确认删除',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'确定要删除这个问题吗?此操作不可撤销。',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'删除',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => Get.back(result: false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'取消',
style: TextStyle(color: Colors.grey[700], fontSize: 16),
),
),
const SizedBox(height: 16),
],
),
),
),
) ??
false;
}
}

449
lib/modules/problem/views/problem_page.dart

@ -2,10 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.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/data/models/problem_model.dart'; import 'package:problem_check_system/modules/problem/views/problem_list_page.dart'; // ProblemListPage
import 'package:problem_check_system/modules/problem/views/widgets/date_picker_button.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart';
import 'package:problem_check_system/modules/problem/views/problem_form_page.dart'; import 'package:problem_check_system/modules/problem/views/problem_form_page.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_data_range_dropdown.dart';
import 'package:problem_check_system/modules/problem/views/widgets/custom_string_dropdown.dart';
import 'package:problem_check_system/modules/problem/views/widgets/problem_card.dart'; //
class ProblemPage extends GetView<ProblemController> { class ProblemPage extends GetView<ProblemController> {
const ProblemPage({super.key}); const ProblemPage({super.key});
@ -16,241 +17,259 @@ class ProblemPage extends GetView<ProblemController> {
initialIndex: 0, initialIndex: 0,
length: 2, length: 2,
child: Scaffold( child: Scaffold(
body: ConstrainedBox( body: Column(
constraints: BoxConstraints(maxHeight: 812.h), mainAxisSize: MainAxisSize.min,
child: Column( children: [
mainAxisSize: MainAxisSize.min, Container(
children: [ width: 375.w,
Container( height: 83.5.h,
width: 375.w, alignment: Alignment.bottomLeft,
height: 83.5.h, decoration: const BoxDecoration(
alignment: Alignment.bottomLeft, gradient: LinearGradient(
decoration: const BoxDecoration( begin: Alignment.centerLeft,
gradient: LinearGradient( end: Alignment.centerRight,
begin: Alignment.centerLeft, colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
end: Alignment.centerRight,
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)],
),
), ),
child: TabBar( ),
indicatorSize: TabBarIndicatorSize.tab, child: TabBar(
indicator: BoxDecoration( controller: controller.tabController,
color: const Color(0xfffff7f7), indicatorSize: TabBarIndicatorSize.tab,
borderRadius: BorderRadius.only( indicator: const BoxDecoration(
topLeft: Radius.circular(8), color: Color(0xfffff7f7),
topRight: Radius.circular(60), borderRadius: BorderRadius.only(
), topLeft: Radius.circular(8),
), topRight: Radius.circular(60),
tabs: const [
Tab(text: '问题列表'),
Tab(text: '历史问题列表'),
],
labelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
unselectedLabelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
), ),
labelColor: Colors.black,
unselectedLabelColor: Colors.white,
), ),
tabs: const [
Tab(text: '问题列表'),
Tab(text: '历史问题列表'),
],
labelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
unselectedLabelStyle: TextStyle(
fontFamily: 'MyFont',
fontWeight: FontWeight.w800,
fontSize: 14.sp,
),
labelColor: Colors.black,
unselectedLabelColor: Colors.white,
), ),
Expanded( ),
child: TabBarView( Expanded(
children: [ child: TabBarView(
Column( controller: controller.tabController,
children: [ children: [
Container( // Tab
margin: EdgeInsets.all(16), Column(
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Container(
children: [DatePickerButton(), DatePickerButton()], padding: EdgeInsets.symmetric(horizontal: 17.w),
), child: Row(
children: [
CustomDateRangeDropdown(
selectedRange: controller.selectedDateRange,
onChanged: (rangeValue) {
controller.updateFiltersAndLoadProblems(
newDateRange: rangeValue,
);
},
),
CustomStringDropdown(
selectedValue: controller.selectedUploadStatus,
items: const ['全部', '未上传', '已上传'],
onChanged: (uploadValue) {
controller.updateFiltersAndLoadProblems(
newUploadStatus: uploadValue,
);
},
),
CustomStringDropdown(
selectedValue: controller.selectedBindingStatus,
items: const ['全部', '未绑定', '已绑定'],
onChanged: (bindingValue) {
controller.updateFiltersAndLoadProblems(
newBindingStatus: bindingValue,
);
},
),
],
),
),
Expanded(
child: // 使
ProblemListPage(
problemsToShow: controller.problems,
viewType: ProblemCardViewType.buttons,
), ),
Expanded( ),
child: Obx(() { ],
if (controller.isLoading.value) { ),
return Center(child: CircularProgressIndicator()); // Tab
} Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 17.w),
child: Row(
children: [
CustomDateRangeDropdown(
selectedRange: controller.selectedDateRange,
onChanged: (rangeValue) {
controller.updateFiltersAndLoadProblems(
newDateRange: rangeValue,
);
},
),
return ListView.builder( CustomStringDropdown(
// +1 selectedValue: controller.selectedUploadStatus,
itemCount: controller.problems.length + 1, items: const ['全部', '未上传', '已上传'],
itemBuilder: (context, index) { onChanged: (uploadValue) {
// SizedBox controller.updateFiltersAndLoadProblems(
if (index == controller.problems.length) { newUploadStatus: uploadValue,
return SizedBox(height: 79.5.h); );
} },
),
// CustomStringDropdown(
final problem = controller.problems[index]; selectedValue: controller.selectedBindingStatus,
return _buildSwipeableProblemCard( items: const ['全部', '未绑定', '已绑定'],
problem, onChanged: (bindingValue) {
controller, controller.updateFiltersAndLoadProblems(
newBindingStatus: bindingValue,
); );
}, },
); ),
}), ],
), ),
], ),
),
Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder( Expanded(
itemCount: controller.problems.length, child: // 使
itemBuilder: (context, index) { ProblemListPage(
final problem = controller.problems[index]; problemsToShow: controller.problems,
return _buildSwipeableProblemCard( viewType: ProblemCardViewType.buttons,
problem, ),
controller, ),
viewType: ProblemCardViewType.checkbox, ],
); ),
}, ],
);
}),
],
),
), ),
], ),
), ],
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, // floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
// floatingActionButton: Stack(
// children: [
// Align(
// alignment: Alignment.bottomCenter,
// child: FloatingActionButton(
// heroTag: "btn_add",
// onPressed: () {
// Get.to(() => ProblemFormPage());
// },
// shape: const CircleBorder(),
// backgroundColor: Colors.blue[300],
// foregroundColor: Colors.white,
// child: const Icon(Icons.add),
// ),
// ),
// Positioned(
// right: controller.fabPosition.value.dx, //27.w,
// bottom: controller.fabPosition.value.dy, //56.h,
// child: Obx(() {
// final bool isOnline = controller.isOnline.value;
// return GestureDetector(
// onPanUpdate: (details) {
// //
// controller.updatePosition(details.delta);
// },
// child: FloatingActionButton(
// heroTag: "btn_upload",
// onPressed: isOnline
// ? () => controller.uploadAllUnuploaded()
// : null,
// foregroundColor: Colors.white,
// backgroundColor: isOnline
// ? Colors.red[300]
// : Colors.grey[400],
// child: Icon(
// isOnline
// ? Icons.file_upload_outlined
// : Icons.cloud_off_outlined,
// ),
// ),
// );
// }),
// ),
// ],
// ),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
// 使 Stack
floatingActionButton: Stack( floatingActionButton: Stack(
children: [ children: [
// "添加"
// 使 Align Positioned
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: FloatingActionButton( child: Padding(
heroTag: "btn_add", padding: EdgeInsets.only(bottom: 24.h), //
onPressed: () { child: FloatingActionButton(
Get.to(() => ProblemFormPage()); heroTag: "btn_add",
}, onPressed: () {
shape: CircleBorder(), Get.to(() => ProblemFormPage());
backgroundColor: Colors.blue[300], },
foregroundColor: Colors.white, shape: const CircleBorder(),
child: const Icon(Icons.add), backgroundColor: Colors.blue[300],
foregroundColor: Colors.white,
child: const Icon(Icons.add),
),
), ),
), ),
Positioned(
bottom: 56.h, // 100 // "上传"
right: 27.w, // 16 Obx(() {
child: Obx(() { final isOnline = controller.isOnline.value;
final bool isOnline = controller.isOnline.value; return Positioned(
return FloatingActionButton( // 使left/right dxtop/bottom dy
heroTag: "btn_upload", left: controller.fabUploadPosition.value.dx,
onPressed: isOnline top: controller.fabUploadPosition.value.dy,
? () => controller.uploadAllUnuploaded() child: GestureDetector(
: null, onPanUpdate: (details) {
foregroundColor: Colors.white, //
backgroundColor: isOnline controller.updateFabUploadPosition(details.delta);
? Colors.red[300] },
: Colors.grey[400], onPanEnd: (details) {
child: Icon( //
isOnline controller.snapToEdge();
? Icons.file_upload_outlined },
: Icons.cloud_off_outlined, child: FloatingActionButton(
heroTag: "btn_upload",
onPressed: isOnline
? () => controller.uploadAllUnuploaded()
: null,
foregroundColor: Colors.white,
backgroundColor: isOnline
? Colors.red[300]
: Colors.grey[400],
child: Icon(
isOnline
? Icons.file_upload_outlined
: Icons.cloud_off_outlined,
),
), ),
); ),
}), );
), }),
], ],
), ),
), ),
); );
} }
Widget _buildSwipeableProblemCard(
Problem problem,
ProblemController controller, {
ProblemCardViewType viewType = ProblemCardViewType.buttons,
}) {
return Dismissible(
key: Key(problem.id ?? UniqueKey().toString()),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 20.w),
child: Icon(Icons.delete, color: Colors.white, size: 30.sp),
),
confirmDismiss: (direction) async {
return await _showDeleteConfirmationDialog(problem);
},
onDismissed: (direction) {
controller.deleteProblem(problem);
Get.snackbar('成功', '问题已删除');
},
child: ProblemCard(problem, viewType: viewType),
);
}
Future<bool> _showDeleteConfirmationDialog(Problem problem) async {
return await Get.bottomSheet<bool>(
Container(
padding: EdgeInsets.symmetric(horizontal: 16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
Text(
'确认删除',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text(
'确定要删除这个问题吗?此操作不可撤销。',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
SizedBox(height: 24),
ElevatedButton(
onPressed: () => Get.back(result: true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
padding: EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'删除',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
SizedBox(height: 8),
TextButton(
onPressed: () => Get.back(result: false),
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
child: Text(
'取消',
style: TextStyle(color: Colors.grey[700], fontSize: 16),
),
),
SizedBox(height: 16),
],
),
),
),
isDismissible: false,
) ??
false;
}
} }

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

@ -0,0 +1,84 @@
// custom_data_range_dropdown.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
//
enum DateRange { threeDays, oneWeek, oneMonth }
//
extension DateRangeExtension on DateRange {
String get name {
switch (this) {
case DateRange.threeDays:
return '近三天';
case DateRange.oneWeek:
return '近一周';
case DateRange.oneMonth:
return '近一月';
}
}
//
DateTime get startDate {
final now = DateTime.now();
switch (this) {
case DateRange.threeDays:
return now.subtract(const Duration(days: 3));
case DateRange.oneWeek:
return now.subtract(const Duration(days: 7));
case DateRange.oneMonth:
return now.subtract(const Duration(days: 30)); // 30
}
}
}
class CustomDateRangeDropdown extends StatelessWidget {
final Rx<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(),
),
),
),
);
}
}

51
lib/modules/problem/views/widgets/custom_string_dropdown.dart

@ -0,0 +1,51 @@
// custom_string_dropdown.dart
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class CustomStringDropdown extends StatelessWidget {
final RxString selectedValue;
final List<String> items;
final Function(String) onChanged;
const CustomStringDropdown({
super.key,
required this.selectedValue,
required this.items,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Obx(
() => Container(
height: 30.h,
margin: EdgeInsets.only(right: 10.w, top: 10.w, bottom: 10.w),
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 12.5.w, vertical: 7.5.h),
decoration: BoxDecoration(
color: const Color.fromARGB(90, 158, 158, 158),
borderRadius: BorderRadius.circular(7.5.w),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedValue.value,
onChanged: (String? newValue) {
if (newValue != null) {
onChanged(newValue);
}
},
items: items.map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
icon: const Icon(Icons.arrow_drop_down, size: 15),
style: TextStyle(fontSize: 10.sp, color: Colors.black),
dropdownColor: Colors.white,
underline: const SizedBox.shrink(),
),
),
),
);
}
}

76
lib/modules/problem/views/widgets/date_picker_button.dart

@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
class DatePickerButton extends StatelessWidget {
DatePickerButton({super.key});
final DatePickerController dateController = Get.put(DatePickerController());
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
),
onPressed: () {
TDPicker.showDatePicker(
context,
title: '选择时间',
onConfirm: (selected) {
dateController.updateDateTime(selected);
Get.back();
},
useHour: true,
useMinute: true,
useSecond: true,
dateStart: [1999, 01, 01],
dateEnd: [2029, 12, 31],
initialDate: [2025, 1, 1],
);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Obx(
() => Text(
dateController.selectedDateTime.value.isEmpty
? "选择日期"
: dateController.selectedDateTime.value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
SizedBox(width: 8),
Icon(Icons.keyboard_arrow_down, color: Colors.black),
],
),
),
],
);
}
}
class DatePickerController extends GetxController {
var selectedDateTime = ''.obs;
void updateDateTime(Map<String, int> selected) {
selectedDateTime.value =
'${selected['year'].toString().padLeft(4, '0')}-'
'${selected['month'].toString().padLeft(2, '0')}-'
'${selected['day'].toString().padLeft(2, '0')} ';
// '${selected['hour'].toString().padLeft(2, '0')}:'
// '${selected['minute'].toString().padLeft(2, '0')}:'
// '${selected['second'].toString().padLeft(2, '0')}';
}
}

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

@ -79,7 +79,7 @@ class ProblemCard extends StatelessWidget {
problem.isUploaded problem.isUploaded
? 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.boundInfo != null && problem.boundInfo!.isNotEmpty problem.bindData != null && problem.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),
], ],

Loading…
Cancel
Save