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

66
lib/data/providers/local_database.dart

@ -7,6 +7,7 @@ class LocalDatabase {
static final LocalDatabase _instance = LocalDatabase._internal();
factory LocalDatabase() => _instance;
static Database? _database;
static const String _tableName = 'problems';
LocalDatabase._internal();
@ -23,36 +24,29 @@ class LocalDatabase {
Future<void> _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE problems(
CREATE TABLE $_tableName(
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
location TEXT NOT NULL,
imagePaths TEXT NOT NULL,
createdAt TEXT NOT NULL,
imageUrls TEXT NOT NULL,
createdAt INTEGER NOT NULL,
isUploaded INTEGER NOT NULL,
boundInfo TEXT
censorTaskId TEXT,
bindData TEXT
)
''');
}
Future<int> insertProblem(Problem problem) async {
final db = await database;
problem.id = Uuid().v4();
return await db.insert('problems', 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]);
});
problem.id = const Uuid().v4();
return await db.insert(_tableName, problem.toMap());
}
Future<int> updateProblem(Problem problem) async {
final db = await database;
return await db.update(
'problems',
_tableName,
problem.toMap(),
where: 'id = ?',
whereArgs: [problem.id],
@ -61,16 +55,48 @@ class LocalDatabase {
Future<int> deleteProblem(String id) async {
final db = await database;
return await db.delete('problems', where: 'id = ?', whereArgs: [id]);
return await db.delete(_tableName, where: 'id = ?', whereArgs: [id]);
}
Future<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 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(
'problems',
where: 'isUploaded = ?',
whereArgs: [0],
_tableName,
where: whereString.isEmpty ? null : whereString,
whereArgs: whereArgs.isEmpty ? null : whereArgs,
orderBy: 'createdAt DESC',
);
return List.generate(maps.length, (i) {
return Problem.fromMap(maps[i]);
});

2
lib/main.dart

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

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

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

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

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

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

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

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

Loading…
Cancel
Save