Browse Source

feat : 显示问题列表

dev
徐振升 1 day ago
parent
commit
664cb4ca9f
  1. 45
      lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart
  2. 8
      lib/app/features/problem/data/datasources/problem_local_data_source.dart
  3. 57
      lib/app/features/problem/data/repositories/problem_repository_impl.dart
  4. 11
      lib/app/features/problem/domain/entities/problem_bind_status.dart
  5. 21
      lib/app/features/problem/domain/entities/problem_list_item_entity.dart
  6. 28
      lib/app/features/problem/presentation/bindings/problem_upload_binding.dart
  7. 173
      lib/app/features/problem/presentation/controllers/problem_list_controller.dart
  8. 97
      lib/app/features/problem/presentation/models/problem_card_state.dart
  9. 13
      lib/app/features/problem/presentation/models/problem_form_model.dart
  10. 417
      lib/app/features/problem/presentation/pages/problem_list_page.dart
  11. 382
      lib/app/features/problem/presentation/pages/widgets/problem_card.dart

45
lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:problem_check_system/app/core/extensions/datetime_extension.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
// [ 1]
class UnifiedEnterpriseCard extends StatelessWidget {
@ -184,23 +185,23 @@ class UnifiedEnterpriseCard extends StatelessWidget {
/// +
Widget _buildBottomActionRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// [ 4]
Padding(
padding: EdgeInsets.only(left: 16.w),
padding: EdgeInsets.all(12.r),
child: Row(
children: [
_buildTag(
text: '已上传 ${enterpriseListItem.uploadedProblems}',
textColor: Colors.blue.shade700,
backgroundColor: Colors.blue.shade50,
TDTag(
'已上传 ${enterpriseListItem.uploadedProblems}',
textColor: Colors.blue,
backgroundColor: Colors.blue.withAlpha(20),
),
SizedBox(width: 8.w),
_buildTag(
text: '未上传 ${enterpriseListItem.pendingProblems}',
textColor: Colors.red.shade600,
backgroundColor: Colors.red.shade50,
TDTag(
'未上传 ${enterpriseListItem.pendingProblems}',
textColor: Colors.red,
backgroundColor: Colors.red.withAlpha(20),
),
],
),
@ -213,28 +214,4 @@ class UnifiedEnterpriseCard extends StatelessWidget {
],
);
}
///
Widget _buildTag({
required String text,
required Color textColor,
required Color backgroundColor,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: backgroundColor,
// borderRadius: BorderRadius.circular(4.r),
// border: Border.all(color: textColor.withAlpha(128), width: 1.w),
),
child: Text(
text,
style: TextStyle(
color: textColor,
fontSize: 10.sp,
fontWeight: FontWeight.w500,
),
),
);
}
}

8
lib/app/features/problem/data/datasources/problem_local_data_source.dart

@ -72,11 +72,9 @@ class ProblemLocalDataSource implements IProblemLocalDataSource {
// 使 JOIN
final baseQuery = '''
SELECT
p.id as problem_id,
p.description as problem_description,
p.location as problem_location,
p.creationTime as problem_creationTime,
c.name as enterprise_name
p.*,
c.name as enterprise_name,
p.id as problem_id --
FROM problems p
LEFT JOIN enterprises c ON p.enterpriseId = c.id
''';

57
lib/app/features/problem/data/repositories/problem_repository_impl.dart

@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/features/problem/data/datasources/problem_local_data_source.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_bind_status.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_filter_params.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart';
@ -35,18 +38,56 @@ class ProblemRepository implements IProblemRepository {
Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter,
}) async {
// 1. (List of Maps)
final problemDataMaps = await problemLocalDataSource.getAllProblems(filter);
// 2. Map ProblemListItemEntity
//
return problemDataMaps.map((map) {
// 1: Map ProblemEntity
// imageUrls
List<String> imageUrls = [];
final imageUrlsRaw = map['imageUrls'];
if (imageUrlsRaw is String && imageUrlsRaw.isNotEmpty) {
try {
imageUrls = List<String>.from(jsonDecode(imageUrlsRaw));
} catch (e) {
print(
'Failed to parse imageUrls for problem ${map['problem_id']}: $e',
);
}
}
// syncStatus
final syncStatusString = map['syncStatus'] as String?;
final syncStatus = SyncStatus.values.firstWhere(
(e) => e.name == syncStatusString,
orElse: () => SyncStatus.pendingCreate, //
);
// Map ProblemEntity
final problemEntity = ProblemEntity(
id: map['problem_id'] as String? ?? '',
description: map['description'] as String? ?? '',
location: map['location'] as String? ?? '',
enterpriseId: map['enterpriseId'] as String? ?? '', // SQL
creatorId: map['creatorId'] as String? ?? '', // SQL
lastModifierId:
map['lastModifierId'] as String? ?? '', // SQL
bindData: map['bindData'] as String?,
imageUrls: imageUrls,
syncStatus: syncStatus,
// DateTime
creationTime:
DateTime.tryParse(map['creationTime'] as String? ?? '') ??
DateTime.now(),
lastModifiedTime:
DateTime.tryParse(map['lastModifiedTime'] as String? ?? '') ??
DateTime.now(),
);
// 2: 使 ProblemEntity enterpriseName ProblemListItemEntity
return ProblemListItemEntity(
id: map['problem_id'],
description: map['problem_description'],
location: map['problem_location'],
creationTime: DateTime.parse(map['problem_creationTime']),
enterpriseName: map['enterprise_name'] ?? '未知企业',
problemEntity: problemEntity,
enterpriseName: map['enterprise_name'] as String? ?? '未知企业',
);
}).toList();
}

11
lib/app/features/problem/domain/entities/problem_bind_status.dart

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
enum ProblemBindStatus {
bound('绑定', Colors.green),
unbound('未绑定', Colors.red);
final String displayName;
final Color displayColor;
const ProblemBindStatus(this.displayName, this.displayColor);
}

21
lib/app/features/problem/domain/entities/problem_list_item_entity.dart

@ -1,15 +1,20 @@
import 'package:problem_check_system/app/features/problem/domain/entities/problem_bind_status.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
class ProblemListItemEntity {
final String id;
final String description;
final String location;
final DateTime creationTime;
final ProblemEntity problemEntity;
final String enterpriseName;
ProblemListItemEntity({
required this.id,
required this.description,
required this.location,
required this.creationTime,
required this.problemEntity,
required this.enterpriseName,
});
ProblemBindStatus get boundStatus {
if (problemEntity.bindData == null || problemEntity.bindData!.isEmpty) {
return ProblemBindStatus.unbound;
} else {
return ProblemBindStatus.bound;
}
}
}

28
lib/app/features/problem/presentation/bindings/problem_upload_binding.dart

@ -1,8 +1,30 @@
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/bindings/base_bindings.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_upload_controller.dart';
class ProblemUploadBinding extends Bindings {
class ProblemUploadBinding extends BaseBindings {
@override
void dependencies() {
// TODO: implement dependencies
void register1Services() {
// TODO: implement register1Services
}
@override
void register2DataSource() {
// TODO: implement register2DataSource
}
@override
void register3Repositories() {
// TODO: implement register3Repositories
}
@override
void register4Usecases() {
// TODO: implement register4Usecases
}
@override
void register5Controllers() {
Get.lazyPut(() => ProblemUploadController());
}
}

173
lib/app/features/problem/presentation/controllers/problem_list_controller.dart

@ -1,16 +1,15 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/models/company_enum.dart';
import 'package:problem_check_system/app/core/models/form_mode.dart';
import 'package:problem_check_system/app/core/routes/app_routes.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart';
class ProblemListController extends GetxController {
final GetAllProblemsUsecase getAllProblemsUsecase;
// final SyncEnterprisesUsecase syncEnterprisesUsecase; //
// final ResolveConflictUsecase resolveConflictUsecase; //
// final SyncEnterprisesUsecase syncEnterprisesUsecase;
// final ResolveConflictUsecase resolveConflictUsecase;
ProblemListController({
required this.getAllProblemsUsecase,
@ -18,16 +17,15 @@ class ProblemListController extends GetxController {
// required this.resolveConflictUsecase,
});
// --- ---
final enterpriseList = <EnterpriseListItem>[].obs;
final problemList = <ProblemListItemEntity>[].obs;
final isLoading = false.obs;
final isSyncing = false.obs;
final nameController = TextEditingController();
final selectedType = Rx<CompanyType?>(null);
// final selectedType = Rx<CompanyType?>(null);
final startDate = Rx<DateTime?>(null);
final endDate = Rx<DateTime?>(null);
final selectedEnterprises = <Enterprise>{}.obs;
final selectedProblems = <ProblemEntity>{}.obs;
final ExpansibleController expansibleController = ExpansibleController();
@override
@ -45,41 +43,39 @@ class ProblemListController extends GetxController {
super.onClose();
}
void search() {}
// --- ---
//
// Future<void> loadAndSyncEnterprises() async {
// try {
// isLoading(true);
// isSyncing(true);
// // 1:
// final syncResult = await syncEnterprisesUsecase();
// // 2:
// if (syncResult.hasConflicts) {
// //
// for (final conflict in syncResult.conflicts) {
// final chosenVersion = await _showConflictDialog(conflict);
// if (chosenVersion != null) {
// //
// await resolveConflictUsecase(chosenVersion);
// }
// }
// }
// isSyncing(false);
// // 3:
// await loadEnterprises();
// } catch (e) {
// Get.snackbar('错误', '操作失败: $e');
// } finally {
// isLoading(false);
// isSyncing(false);
// }
// }
Future<void> loadAndSyncEnterprises() async {
try {
isLoading(true);
isSyncing(true);
// // 1:
// final syncResult = await syncEnterprisesUsecase();
// // 2:
// if (syncResult.hasConflicts) {
// //
// for (final conflict in syncResult.conflicts) {
// final chosenVersion = await _showConflictDialog(conflict);
// if (chosenVersion != null) {
// //
// await resolveConflictUsecase(chosenVersion);
// }
// }
// }
isSyncing(false);
// 3:
await loadProblemItems();
} catch (e) {
Get.snackbar('错误', '操作失败: $e');
} finally {
isLoading(false);
isSyncing(false);
}
}
// // []
// Future<Enterprise?> _showConflictDialog(EnterpriseConflict conflict) {
@ -150,46 +146,42 @@ class ProblemListController extends GetxController {
// );
// }
// Future<void> loadEnterprises() async {
// expansibleController.collapse();
// isLoading.value = true;
// try {
// final result = await getEnterpriseListUsecase.call(
// name: nameController.text,
// type: selectedType.value?.displayText,
// startDate: startDate.value,
// endDate: endDate.value,
// );
// enterpriseList.assignAll(result);
// } catch (e) {
// Get.snackbar('错误', '加载企业列表失败: $e');
// } finally {
// isLoading.value = false;
// }
// }
void search() {
loadProblemItems();
}
// void clearFilters() {
// nameController.clear();
// selectedType.value = null;
// startDate.value = null;
// endDate.value = null;
// loadEnterprises();
// }
Future<void> loadProblemItems() async {
expansibleController.collapse();
isLoading.value = true;
try {
final result = await getAllProblemsUsecase.call();
problemList.assignAll(result);
} catch (e) {
Get.snackbar('错误', '加载问题列表失败: $e');
} finally {
isLoading.value = false;
}
}
void clearFilters() {
nameController.clear();
// selectedType.value = null;
startDate.value = null;
endDate.value = null;
loadProblemItems();
}
///
Future<void> navigateToProblemForm({
Enterprise? enterprise,
FormMode? fromMode,
}) async {
///
Future<void> navigateToEditForm(ProblemEntity problem) async {
final result = await Get.toNamed(
AppRoutes.problemForm,
arguments: {'data': enterprise, 'mode': fromMode},
arguments: {'data': problem, 'mode': FormMode.edit},
);
if (result == true) {
search();
Get.snackbar(
'操作成功',
'问题信息已更新',
'成功',
'企业信息已更新',
backgroundColor: Colors.green[600],
colorText: Colors.white,
icon: const Icon(Icons.check_circle, color: Colors.white),
@ -198,11 +190,30 @@ class ProblemListController extends GetxController {
}
}
///
// Future<void> navigateToEnterpriseInfoPage(Enterprise enterprise) async {
// await Get.toNamed(
// AppRoutes.enterpriseInfo,
// arguments: {'data': enterprise, 'mode': FormMode.view},
// );
// }
///
void navigateToDetailsView(ProblemEntity problem) {
Get.toNamed(
AppRoutes.problemForm,
arguments: {'data': problem, 'mode': FormMode.view},
);
}
///
Future<void> navigateToAddForm() async {
final result = await Get.toNamed(
AppRoutes.problemForm,
arguments: {'mode': FormMode.add},
);
if (result == true) {
search();
Get.snackbar(
'成功',
'企业信息已创建',
backgroundColor: Colors.green[600],
colorText: Colors.white,
icon: const Icon(Icons.check_circle, color: Colors.white),
duration: const Duration(seconds: 3),
);
}
}
}

97
lib/app/features/problem/presentation/models/problem_card_state.dart

@ -1,97 +0,0 @@
// presentation/states/problem_card_state.dart
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart';
class ProblemCardState extends Equatable {
final String problemId; //
final String? previewImageUrl; // []
final String description; // []
final String enterpriseName; // []
final String location; // []
final String creationTime; // []
final StatusTagState uploadStatus; // []
final StatusTagState bindingStatus; // []
const ProblemCardState({
required this.problemId,
this.previewImageUrl,
required this.description,
required this.enterpriseName,
required this.location,
required this.creationTime,
required this.uploadStatus,
required this.bindingStatus,
});
// []
factory ProblemCardState.fromEntities({
required ProblemEntity problem,
required Enterprise enterprise,
}) {
// 1. null
final String? previewImageUrl = problem.imageUrls.isNotEmpty
? problem.imageUrls.first
: null;
// 2. DateTime
final String formattedTime =
'${problem.creationTime.year}-${problem.creationTime.month.toString().padLeft(2, '0')}-${problem.creationTime.day.toString().padLeft(2, '0')}';
// 3. SyncStatus UI的 StatusTagState
StatusTagState uploadStatusState;
switch (problem.syncStatus) {
case SyncStatus.synced:
uploadStatusState = StatusTagState(text: '已上传', color: Colors.green);
break;
case SyncStatus.pendingCreate:
case SyncStatus.pendingUpdate:
case SyncStatus.pendingDelete:
uploadStatusState = StatusTagState(text: '待上传', color: Colors.orange);
break;
case SyncStatus.untracked:
default:
uploadStatusState = StatusTagState(text: '未上传', color: Colors.grey);
break;
}
// 4. bindData
final StatusTagState bindingStatusState =
(problem.bindData != null && problem.bindData!.isNotEmpty)
? StatusTagState(text: '已绑定', color: Colors.blue)
: StatusTagState(text: '未绑定', color: Colors.red);
return ProblemCardState(
problemId: problem.id,
previewImageUrl: previewImageUrl,
description: problem.description,
enterpriseName: enterprise.name,
location: problem.location,
creationTime: formattedTime,
uploadStatus: uploadStatusState,
bindingStatus: bindingStatusState,
);
}
@override
List<Object?> get props => [
problemId,
previewImageUrl,
description,
enterpriseName,
location,
creationTime,
uploadStatus,
bindingStatus,
];
}
//
class StatusTagState {
final String text;
final Color color;
StatusTagState({required this.text, required this.color});
}

13
lib/app/features/problem/presentation/models/problem_form_model.dart

@ -1,13 +0,0 @@
class ProblemFormModel {
final String enterpriseName;
final String description;
final String location;
final List<String> imageUrls;
ProblemFormModel({
required this.enterpriseName,
required this.description,
required this.location,
required this.imageUrls,
});
}

417
lib/app/features/problem/presentation/pages/problem_list_page.dart

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:problem_check_system/app/core/models/form_mode.dart';
import 'package:problem_check_system/app/core/extensions/datetime_extension.dart';
import 'package:problem_check_system/app/core/pages/widgets/custom_app_bar.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart';
import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_list_controller.dart';
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/problem_card.dart';
class ProblemListPage extends GetView<ProblemListController> {
const ProblemListPage({super.key});
@ -12,171 +13,289 @@ class ProblemListPage extends GetView<ProblemListController> {
Widget build(BuildContext context) {
return Scaffold(
appBar: CustomAppBar(
titleName: '企业列表',
titleName: '问题列表',
actionsVisible: true,
onAddPressed: () {
controller.navigateToProblemForm(fromMode: FormMode.add);
},
onAddPressed: () => controller.navigateToAddForm(),
),
body: Text("问题列表"),
);
// return Obx(() {
// if (true) {
// return const Center(child: CircularProgressIndicator());
// }
body: Column(
children: [
//
_buildFilterSection(),
const Divider(height: 1, thickness: 1),
// return EasyRefresh(
// header: ClassicHeader(
// dragText: '下拉刷新'.tr,
// armedText: '释放开始'.tr,
// readyText: '刷新中...'.tr,
// processingText: '刷新中...'.tr,
// processedText: '成功了'.tr,
// noMoreText: 'No more'.tr,
// failedText: '失败'.tr,
// messageText: '最后更新于 %T'.tr,
// ),
// onRefresh: () async {
// //
// await controller.pullDataFromServer();
// },
// child: ListView.builder(
// padding: EdgeInsets.symmetric(horizontal: 17.w),
// itemCount: problemsToShow.length,
// itemBuilder: (context, index) {
// // if (index == problemsToShow.length) {
// // return SizedBox(height: 79.5.h);
// // }
// final problem = problemsToShow[index];
// return _buildSwipeableProblemCard(problem);
// },
// ),
// );
// });
//
Expanded(child: _buildEnterpriseList()),
],
),
);
}
// Widget _buildSwipeableProblemCard(Problem problem) {
// //
// final bool isPendingDelete =
// problem.syncStatus == ProblemSyncStatus.pendingDelete;
///
/// `BaseEnterpriseListController`
///
Widget _buildFilterSection() {
return ExpansionTile(
controller: controller.expansibleController,
title: const Text('筛选查询'),
leading: const Icon(Icons.filter_alt_outlined),
tilePadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 0.0.h),
childrenPadding: EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0.h,
).copyWith(top: 0),
dense: true,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
child: Column(
children: [
// 1.
TextFormField(
controller: controller.nameController,
decoration: InputDecoration(
labelText: '企业名称',
hintText: '请输入关键词',
prefixIcon: const Icon(Icons.business_center),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.r),
),
isDense: true,
),
),
SizedBox(height: 12.h),
// if (viewType == ProblemCardViewType.buttons) {
// // buttons
// if (!isPendingDelete) {
// //
// return Dismissible(
// key: ValueKey('${problem.id}-${problem.syncStatus}'),
// 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(
// key: ValueKey(problem.id),
// problem: problem,
// viewType: viewType,
// isSelected: false,
// ),
// );
// } else {
// //
// return ProblemCard(
// key: ValueKey(problem.id),
// problem: problem,
// viewType: viewType,
// isSelected: false,
// );
// }
// } else {
// // listgrid等使 Obx
// return 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);
// },
// );
// });
// }
// }
// // 2.
// Obx(
// () => DropdownButtonFormField<CompanyType>(
// initialValue: controller.selectedType.value,
// decoration: InputDecoration(
// labelText: '企业类型',
// prefixIcon: const Icon(Icons.category),
// border: OutlineInputBorder(
// borderRadius: BorderRadius.circular(8.r),
// ),
// isDense: true,
// ),
// hint: const Text('请选择企业类型'),
// isExpanded: true,
// items: CompanyType.values.map((type) {
// return DropdownMenuItem(
// value: type,
// child: Text(type.displayText),
// );
// }).toList(),
// onChanged: (value) {
// controller.selectedType.value = value;
// },
// ),
// ),
// SizedBox(height: 12.h),
Future<bool> _showDeleteConfirmationDialog(Problem problem) async {
// snackbar
if (Get.isSnackbarOpen) {
Get.closeCurrentSnackbar();
}
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,
// 3.
Row(
children: [
const SizedBox(height: 16),
const Text(
'确认删除',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
Expanded(
child: _buildDatePickerField('开始日期', controller.startDate),
),
const SizedBox(height: 8),
Text(
'确定要删除这个问题吗?此操作不可撤销。',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
SizedBox(width: 12.w),
Expanded(
child: _buildDatePickerField('结束日期', controller.endDate),
),
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),
],
),
SizedBox(height: 16.h),
// 4.
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: controller.clearFilters,
icon: const Icon(Icons.refresh),
label: const Text('重置'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
),
),
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),
SizedBox(width: 16.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.search,
icon: const Icon(Icons.search),
label: const Text('查询'),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
),
),
),
const SizedBox(height: 16),
],
),
SizedBox(height: 8.h), //
],
),
),
],
);
}
///
Widget _buildDatePickerField(String label, Rx<DateTime?> date) {
return InkWell(
onTap: () async {
final pickedDate = await showDatePicker(
context: Get.context!,
initialDate: date.value ?? DateTime.now(),
firstDate: DateTime(2020), //
lastDate: DateTime(2125),
);
if (pickedDate != null) {
date.value = pickedDate;
}
},
child: InputDecorator(
decoration: InputDecoration(
labelText: label,
prefixIcon: const Icon(Icons.calendar_today),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
isDense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 14.h,
), //
),
child: Obx(
() => Text(
date.value == null ? '请选择日期' : date.value!.toDateString(),
style: TextStyle(
fontSize: 14.sp,
color: date.value == null ? Colors.grey[700] : Colors.black87,
),
),
) ??
false;
),
),
);
}
///
Widget _buildEnterpriseList() {
// 使 Obx controller Rx
return Obx(() {
//
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
//
if (controller.problemList.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_off_outlined,
size: 60.sp,
color: Colors.grey[400],
),
SizedBox(height: 16.h),
Text(
'没有找到相关问题',
style: TextStyle(fontSize: 16.sp, color: Colors.grey[600]),
),
],
),
);
}
// 使
return RefreshIndicator(
onRefresh: () async => controller.loadAndSyncEnterprises(),
child: ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
itemCount: controller.problemList.length,
itemBuilder: (context, index) {
final item = controller.problemList[index];
final enterprise = item.problemEntity;
// : base controller
final isSelected = controller.selectedProblems.contains(enterprise);
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: ProblemCard(
problemListItem: item,
isSelected: isSelected,
onTap: () =>
controller.navigateToDetailsView(item.problemEntity),
actions: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
//
ElevatedButton(
onPressed: () =>
controller.navigateToEditForm(item.problemEntity),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF42A5F5),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 8.h,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// [!!!]
elevation: 0,
// [!!!]
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.r),
bottomRight: Radius.circular(12.r),
),
),
),
child: Text(
"修改",
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 8.w),
// ()
ElevatedButton(
onPressed: () =>
controller.navigateToDetailsView(item.problemEntity),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF42A5F5),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 8.h,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// [!!!]
elevation: 0,
// [!!!]
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.r),
bottomRight: Radius.circular(12.r),
),
),
),
child: Text(
"查看",
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
},
),
);
});
}
}

382
lib/app/features/problem/presentation/pages/widgets/problem_card.dart

@ -1,248 +1,240 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:problem_check_system/app/core/routes/app_routes.dart';
import 'package:problem_check_system/app/features/problem/data/model/problem_model.dart';
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/custom_button.dart';
import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:problem_check_system/app/core/extensions/datetime_extension.dart';
import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'dart:io'; //
//
enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends StatelessWidget {
final Problem problem;
final ProblemCardViewType viewType;
final Function(Problem, bool)? onChanged;
final Function()? onTap;
final bool isSelected; //
final ProblemListItemEntity problemListItem;
final bool isSelected;
final VoidCallback? onTap;
final Widget? actions;
const ProblemCard({
super.key,
required this.problem,
this.viewType = ProblemCardViewType.buttons,
this.onChanged,
required this.problemListItem,
this.isSelected = false,
this.onTap,
required this.isSelected, //
this.actions,
});
@override
Widget build(BuildContext context) {
//
final bool isDeleted =
problem.syncStatus == ProblemSyncStatus.pendingDelete;
problemListItem.problemEntity.syncStatus == SyncStatus.pendingDelete;
final Color cardColor = isDeleted
? Colors.grey[300]!
? Colors.grey[200]! // 使
: Theme.of(context).cardColor;
final Color contentColor = isDeleted
? Colors.grey[600]!
: Theme.of(context).textTheme.bodyMedium!.color!;
return Card(
elevation: isSelected ? 4.0 : 0.2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
side: isSelected
? BorderSide(color: Theme.of(context).primaryColor, width: 1.5.w)
: BorderSide(color: Colors.grey.shade300, width: 1.w),
),
clipBehavior: Clip.antiAlias,
margin: EdgeInsets.zero,
color: cardColor,
child: InkWell(
onTap: viewType == ProblemCardViewType.checkbox
? () {
onChanged?.call(problem, !isSelected);
}
: null,
onTap: onTap,
// FIX 5:
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: _buildImageWidget(isDeleted), // 使
title: Text(
'问题描述',
style: TextStyle(fontSize: 16.sp, color: contentColor),
),
subtitle: LayoutBuilder(
builder: (context, constraints) {
return Text(
problem.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp, color: contentColor),
);
},
),
),
SizedBox(height: 8.h),
Row(
children: [
SizedBox(width: 16.w),
Icon(Icons.location_on, color: contentColor, size: 16.h),
SizedBox(width: 8.w),
SizedBox(
width: 100.w,
child: Text(
problem.location,
style: TextStyle(fontSize: 12.sp, color: contentColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
SizedBox(width: 16.w),
Icon(Icons.access_time, color: contentColor, size: 16.h),
SizedBox(width: 8.w),
Expanded(
child: Text(
DateFormat(
'yyyy-MM-dd HH:mm:ss',
).format(problem.creationTime),
style: TextStyle(fontSize: 12.sp, color: contentColor),
children: [
Padding(
padding: EdgeInsets.all(12.r),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImageWidget(isDeleted),
SizedBox(width: 12.w), // FIX 7:
// 使 Expanded
Expanded(
child: _buildDescriptionAndCompany(contentColor),
),
],
),
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
SizedBox(width: 16.w),
Wrap(
spacing: 8,
children: [
...problem.syncStatus == ProblemSyncStatus.pendingDelete
? [
TDTag(
'服务器未删除',
isLight: true,
theme: TDTagTheme.defaultTheme,
textColor: isDeleted ? Colors.grey[700] : null,
// backgroundColor: isDeleted
// ? Colors.grey[400]
// : null,
),
]
: [
problem.syncStatus == ProblemSyncStatus.synced
? TDTag(
'已上传',
isLight: true,
theme: TDTagTheme.success,
)
: TDTag(
'未上传',
isLight: true,
theme: TDTagTheme.danger,
),
problem.bindData != null &&
problem.bindData!.isNotEmpty
? TDTag(
'已绑定',
isLight: true,
theme: TDTagTheme.primary,
)
: TDTag(
'未绑定',
isLight: true,
theme: TDTagTheme.warning,
),
],
],
),
const Spacer(),
_buildBottomActions(isDeleted),
],
SizedBox(height: 12.h),
// const Divider(height: 1, color: Color(0xFFEEEEEE)),
// SizedBox(height: 10.h),
_buildLocationAndTime(contentColor),
],
),
),
SizedBox(height: 8.h),
// actions线
if (actions != null) ...[
// const Divider(height: 1, color: Color(0xFFEEEEEE)),
// SizedBox(height: 10.h),
_buildBottomActionRow(),
],
],
),
),
);
}
Widget _buildDescriptionAndCompany(Color contentColor) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'问题描述',
style: TextStyle(color: Colors.grey, fontSize: 9.sp),
),
SizedBox(height: 2.h),
// FIX 3: 使
Text(
problemListItem.problemEntity.description,
style: TextStyle(
fontSize: 12.5.sp,
color: contentColor,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'企业名称',
style: TextStyle(color: Colors.grey, fontSize: 9.sp),
),
SizedBox(height: 2.h),
// FIX 3: 使
Text(
problemListItem.enterpriseName,
style: TextStyle(
fontSize: 12.5.sp,
color: contentColor,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
);
}
Widget _buildImageWidget(bool isDeleted) {
return AspectRatio(
aspectRatio: 1, //
child: _buildImageContent(isDeleted),
return SizedBox(
width: 64.w,
height: 64.w,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: _buildImageContent(isDeleted),
),
);
}
Widget _buildImageContent(bool isDeleted) {
//
if (problem.imageUrls.isEmpty || problem.imageUrls[0].localPath.isEmpty) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover, // 使 cover
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
}
// FIX 1: 访
final String? imagePath = problemListItem.problemEntity.imageUrls.isNotEmpty
? problemListItem.problemEntity.imageUrls.first
: null;
final String imagePath = problem.imageUrls[0].localPath;
final placeholder = Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
//
final File imageFile = File(imagePath);
if (!imageFile.existsSync()) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
if (imagePath == null || imagePath.isEmpty) {
return placeholder;
}
// 使 Image.file
// BEST PRACTICE 1: existsSync()
return Image.file(
imageFile,
fit: BoxFit.cover, // 使 cover
File(imagePath),
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
errorBuilder: (context, error, stackTrace) {
//
return Image.asset(
'assets/images/problem_preview.png',
fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
//
return placeholder;
},
);
}
Widget _buildBottomActions(bool isDeleted) {
switch (viewType) {
case ProblemCardViewType.buttons:
return Row(
children: [
if (!isDeleted)
CustomButton(
text: '修改',
onTap: () {
onTap;
// controller.toProblemFormPageAndRefresh(problem: problem);
},
Widget _buildIconText(IconData icon, String text, Color color) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, // Row
children: [
Icon(icon, color: color, size: 16.r),
SizedBox(width: 4.w),
Text(
text,
style: TextStyle(fontSize: 12.sp, color: color),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
);
}
Widget _buildLocationAndTime(Color contentColor) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: _buildIconText(
Icons.location_on_outlined,
problemListItem.problemEntity.location,
contentColor,
),
),
SizedBox(width: 12.w),
_buildIconText(
Icons.access_time_outlined,
problemListItem.problemEntity.creationTime.toDateTimeString(),
contentColor,
),
],
);
}
Widget _buildBottomActionRow() {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Padding(
padding: EdgeInsets.only(left: 12.r, bottom: 12.r),
child: Row(
children: [
TDTag(
problemListItem.problemEntity.syncStatus.displayName,
textColor:
problemListItem.problemEntity.syncStatus.displayColor,
backgroundColor: problemListItem
.problemEntity
.syncStatus
.displayColor
.withAlpha(20),
),
if (!isDeleted) SizedBox(width: 8.w),
CustomButton(
text: '查看',
onTap: () {
Get.toNamed(
AppRoutes.problemForm,
arguments: problem,
parameters: {'isReadOnly': 'true'},
);
},
),
SizedBox(width: 16.w),
],
);
case ProblemCardViewType.checkbox:
return Padding(
padding: EdgeInsets.only(right: 16.w),
child: Checkbox(
value: isSelected,
onChanged: (bool? value) {
if (value != null) {
onChanged?.call(problem, value);
}
},
SizedBox(width: 8.w),
TDTag(
problemListItem.boundStatus.displayName,
textColor: problemListItem.boundStatus.displayColor,
backgroundColor: problemListItem.boundStatus.displayColor
.withAlpha(20),
),
],
),
);
}
),
const Spacer(),
// FIX 2: actions
if (actions != null) actions!,
],
);
}
}

Loading…
Cancel
Save