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. 137
      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. 387
      lib/app/features/problem/presentation/pages/problem_list_page.dart
  11. 334
      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:flutter_screenutil/flutter_screenutil.dart';
import 'package:problem_check_system/app/core/extensions/datetime_extension.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:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
// [ 1] // [ 1]
class UnifiedEnterpriseCard extends StatelessWidget { class UnifiedEnterpriseCard extends StatelessWidget {
@ -184,23 +185,23 @@ class UnifiedEnterpriseCard extends StatelessWidget {
/// + /// +
Widget _buildBottomActionRow() { Widget _buildBottomActionRow() {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
// [ 4] // [ 4]
Padding( Padding(
padding: EdgeInsets.only(left: 16.w), padding: EdgeInsets.all(12.r),
child: Row( child: Row(
children: [ children: [
_buildTag( TDTag(
text: '已上传 ${enterpriseListItem.uploadedProblems}', '已上传 ${enterpriseListItem.uploadedProblems}',
textColor: Colors.blue.shade700, textColor: Colors.blue,
backgroundColor: Colors.blue.shade50, backgroundColor: Colors.blue.withAlpha(20),
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
_buildTag( TDTag(
text: '未上传 ${enterpriseListItem.pendingProblems}', '未上传 ${enterpriseListItem.pendingProblems}',
textColor: Colors.red.shade600, textColor: Colors.red,
backgroundColor: Colors.red.shade50, 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 // 使 JOIN
final baseQuery = ''' final baseQuery = '''
SELECT SELECT
p.id as problem_id, p.*,
p.description as problem_description, c.name as enterprise_name,
p.location as problem_location, p.id as problem_id --
p.creationTime as problem_creationTime,
c.name as enterprise_name
FROM problems p FROM problems p
LEFT JOIN enterprises c ON p.enterpriseId = c.id 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/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/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_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_filter_params.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/entities/problem_list_item_entity.dart';
@ -35,18 +38,56 @@ class ProblemRepository implements IProblemRepository {
Future<List<ProblemListItemEntity>> getAllProblemListItem({ Future<List<ProblemListItemEntity>> getAllProblemListItem({
required ProblemFilterParams filter, required ProblemFilterParams filter,
}) async { }) async {
// 1. (List of Maps)
final problemDataMaps = await problemLocalDataSource.getAllProblems(filter); final problemDataMaps = await problemLocalDataSource.getAllProblems(filter);
// 2. Map ProblemListItemEntity
//
return problemDataMaps.map((map) { 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( return ProblemListItemEntity(
id: map['problem_id'], problemEntity: problemEntity,
description: map['problem_description'], enterpriseName: map['enterprise_name'] as String? ?? '未知企业',
location: map['problem_location'],
creationTime: DateTime.parse(map['problem_creationTime']),
enterpriseName: map['enterprise_name'] ?? '未知企业',
); );
}).toList(); }).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 { class ProblemListItemEntity {
final String id; final ProblemEntity problemEntity;
final String description;
final String location;
final DateTime creationTime;
final String enterpriseName; final String enterpriseName;
ProblemListItemEntity({ ProblemListItemEntity({
required this.id, required this.problemEntity,
required this.description,
required this.location,
required this.creationTime,
required this.enterpriseName, 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: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 @override
void dependencies() { void register1Services() {
// TODO: implement dependencies // 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());
} }
} }

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

@ -1,16 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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/models/form_mode.dart';
import 'package:problem_check_system/app/core/routes/app_routes.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/problem/domain/entities/problem_entity.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_list_item_entity.dart';
import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/usecases/get_all_problems_usecase.dart';
class ProblemListController extends GetxController { class ProblemListController extends GetxController {
final GetAllProblemsUsecase getAllProblemsUsecase; final GetAllProblemsUsecase getAllProblemsUsecase;
// final SyncEnterprisesUsecase syncEnterprisesUsecase; // // final SyncEnterprisesUsecase syncEnterprisesUsecase;
// final ResolveConflictUsecase resolveConflictUsecase; // // final ResolveConflictUsecase resolveConflictUsecase;
ProblemListController({ ProblemListController({
required this.getAllProblemsUsecase, required this.getAllProblemsUsecase,
@ -18,16 +17,15 @@ class ProblemListController extends GetxController {
// required this.resolveConflictUsecase, // required this.resolveConflictUsecase,
}); });
// --- --- final problemList = <ProblemListItemEntity>[].obs;
final enterpriseList = <EnterpriseListItem>[].obs;
final isLoading = false.obs; final isLoading = false.obs;
final isSyncing = false.obs; final isSyncing = false.obs;
final nameController = TextEditingController(); final nameController = TextEditingController();
final selectedType = Rx<CompanyType?>(null); // final selectedType = Rx<CompanyType?>(null);
final startDate = Rx<DateTime?>(null); final startDate = Rx<DateTime?>(null);
final endDate = Rx<DateTime?>(null); final endDate = Rx<DateTime?>(null);
final selectedEnterprises = <Enterprise>{}.obs; final selectedProblems = <ProblemEntity>{}.obs;
final ExpansibleController expansibleController = ExpansibleController(); final ExpansibleController expansibleController = ExpansibleController();
@override @override
@ -45,14 +43,12 @@ class ProblemListController extends GetxController {
super.onClose(); super.onClose();
} }
void search() {}
// --- --- // --- ---
// //
// Future<void> loadAndSyncEnterprises() async { Future<void> loadAndSyncEnterprises() async {
// try { try {
// isLoading(true); isLoading(true);
// isSyncing(true); isSyncing(true);
// // 1: // // 1:
// final syncResult = await syncEnterprisesUsecase(); // final syncResult = await syncEnterprisesUsecase();
@ -69,17 +65,17 @@ class ProblemListController extends GetxController {
// } // }
// } // }
// isSyncing(false); isSyncing(false);
// // 3: // 3:
// await loadEnterprises(); await loadProblemItems();
// } catch (e) { } catch (e) {
// Get.snackbar('错误', '操作失败: $e'); Get.snackbar('错误', '操作失败: $e');
// } finally { } finally {
// isLoading(false); isLoading(false);
// isSyncing(false); isSyncing(false);
// } }
// } }
// // [] // // []
// Future<Enterprise?> _showConflictDialog(EnterpriseConflict conflict) { // Future<Enterprise?> _showConflictDialog(EnterpriseConflict conflict) {
@ -150,46 +146,42 @@ class ProblemListController extends GetxController {
// ); // );
// } // }
// Future<void> loadEnterprises() async { void search() {
// expansibleController.collapse(); loadProblemItems();
// 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 clearFilters() { Future<void> loadProblemItems() async {
// nameController.clear(); 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; // selectedType.value = null;
// startDate.value = null; startDate.value = null;
// endDate.value = null; endDate.value = null;
// loadEnterprises(); loadProblemItems();
// } }
/// ///
Future<void> navigateToProblemForm({ Future<void> navigateToEditForm(ProblemEntity problem) async {
Enterprise? enterprise,
FormMode? fromMode,
}) async {
final result = await Get.toNamed( final result = await Get.toNamed(
AppRoutes.problemForm, AppRoutes.problemForm,
arguments: {'data': enterprise, 'mode': fromMode}, arguments: {'data': problem, 'mode': FormMode.edit},
); );
if (result == true) { if (result == true) {
search(); search();
Get.snackbar( Get.snackbar(
'操作成功', '成功',
'问题信息已更新', '企业信息已更新',
backgroundColor: Colors.green[600], backgroundColor: Colors.green[600],
colorText: Colors.white, colorText: Colors.white,
icon: const Icon(Icons.check_circle, color: 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 { void navigateToDetailsView(ProblemEntity problem) {
// await Get.toNamed( Get.toNamed(
// AppRoutes.enterpriseInfo, AppRoutes.problemForm,
// arguments: {'data': enterprise, 'mode': FormMode.view}, 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,
});
}

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

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.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/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/controllers/problem_list_controller.dart';
import 'package:problem_check_system/app/features/problem/presentation/pages/widgets/problem_card.dart';
class ProblemListPage extends GetView<ProblemListController> { class ProblemListPage extends GetView<ProblemListController> {
const ProblemListPage({super.key}); const ProblemListPage({super.key});
@ -12,171 +13,289 @@ class ProblemListPage extends GetView<ProblemListController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: CustomAppBar( appBar: CustomAppBar(
titleName: '企业列表', titleName: '问题列表',
actionsVisible: true, actionsVisible: true,
onAddPressed: () { onAddPressed: () => controller.navigateToAddForm(),
controller.navigateToProblemForm(fromMode: FormMode.add);
},
), ),
body: Text("问题列表"), body: Column(
); children: [
// return Obx(() { //
// if (true) { _buildFilterSection(),
// return const Center(child: CircularProgressIndicator()); const Divider(height: 1, thickness: 1),
// }
// return EasyRefresh( //
// header: ClassicHeader( Expanded(child: _buildEnterpriseList()),
// 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);
// },
// ),
// );
// });
} }
// Widget _buildSwipeableProblemCard(Problem problem) { ///
// // /// `BaseEnterpriseListController`
// final bool isPendingDelete = ///
// problem.syncStatus == ProblemSyncStatus.pendingDelete; 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) { // // 2.
// // buttons // Obx(
// if (!isPendingDelete) { // () => DropdownButtonFormField<CompanyType>(
// // // initialValue: controller.selectedType.value,
// return Dismissible( // decoration: InputDecoration(
// key: ValueKey('${problem.id}-${problem.syncStatus}'), // labelText: '企业类型',
// direction: DismissDirection.endToStart, // prefixIcon: const Icon(Icons.category),
// background: Container( // border: OutlineInputBorder(
// color: Colors.red, // borderRadius: BorderRadius.circular(8.r),
// alignment: Alignment.centerRight,
// padding: EdgeInsets.only(right: 20.w),
// child: Icon(Icons.delete, color: Colors.white, size: 30.sp),
// ), // ),
// confirmDismiss: (direction) async { // isDense: true,
// return await _showDeleteConfirmationDialog(problem);
// },
// onDismissed: (direction) {
// // controller.deleteProblem(problem);
// Get.snackbar('成功', '问题已删除');
// },
// child: ProblemCard(
// key: ValueKey(problem.id),
// problem: problem,
// viewType: viewType,
// isSelected: false,
// ), // ),
// hint: const Text('请选择企业类型'),
// isExpanded: true,
// items: CompanyType.values.map((type) {
// return DropdownMenuItem(
// value: type,
// child: Text(type.displayText),
// ); // );
// } else { // }).toList(),
// // // onChanged: (value) {
// return ProblemCard( // controller.selectedType.value = value;
// 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);
// }, // },
// ); // ),
// }); // ),
// } // SizedBox(height: 12.h),
// }
// 3.
Row(
children: [
Expanded(
child: _buildDatePickerField('开始日期', controller.startDate),
),
SizedBox(width: 12.w),
Expanded(
child: _buildDatePickerField('结束日期', controller.endDate),
),
],
),
SizedBox(height: 16.h),
Future<bool> _showDeleteConfirmationDialog(Problem problem) async { // 4.
// snackbar Row(
if (Get.isSnackbarOpen) { children: [
Get.closeCurrentSnackbar(); Expanded(
child: OutlinedButton.icon(
onPressed: controller.clearFilters,
icon: const Icon(Icons.refresh),
label: const Text('重置'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
),
),
),
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),
),
),
),
],
),
SizedBox(height: 8.h), //
],
),
),
],
);
} }
return await Get.bottomSheet<bool>(
Container( ///
padding: const EdgeInsets.symmetric(horizontal: 16.0), Widget _buildDatePickerField(String label, Rx<DateTime?> date) {
decoration: const BoxDecoration( return InkWell(
color: Colors.white, onTap: () async {
borderRadius: BorderRadius.only( final pickedDate = await showDatePicker(
topLeft: Radius.circular(16), context: Get.context!,
topRight: Radius.circular(16), 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,
), ),
), ),
child: SafeArea( ),
),
);
}
///
Widget _buildEnterpriseList() {
// 使 Obx controller Rx
return Obx(() {
//
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
//
if (controller.problemList.isEmpty) {
return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const SizedBox(height: 16), Icon(
const Text( Icons.folder_off_outlined,
'确认删除', size: 60.sp,
textAlign: TextAlign.center, color: Colors.grey[400],
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), SizedBox(height: 16.h),
Text( Text(
'确定要删除这个问题吗?此操作不可撤销。', '没有找到相关问题',
textAlign: TextAlign.center, style: TextStyle(fontSize: 16.sp, color: Colors.grey[600]),
style: TextStyle(fontSize: 14, color: Colors.grey[600]), ),
],
), ),
const SizedBox(height: 24), );
}
// 使
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( ElevatedButton(
onPressed: () => Get.back(result: true), onPressed: () =>
controller.navigateToEditForm(item.problemEntity),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.red, backgroundColor: const Color(0xFF42A5F5),
padding: const EdgeInsets.symmetric(vertical: 16), foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: 20.w,
vertical: 8.h,
),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
// [!!!]
elevation: 0,
// [!!!]
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.only(
topLeft: Radius.circular(12.r),
bottomRight: Radius.circular(12.r),
), ),
), ),
child: const Text( ),
'删除', child: Text(
style: TextStyle(color: Colors.white, fontSize: 16), "修改",
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),
), ),
), ),
const SizedBox(height: 8),
TextButton(
onPressed: () => Get.back(result: false),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text( child: Text(
'取消', "查看",
style: TextStyle(color: Colors.grey[700], fontSize: 16), style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.bold,
),
), ),
), ),
const SizedBox(height: 16),
], ],
), ),
), ),
);
},
), ),
) ?? );
false; });
} }
} }

334
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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:problem_check_system/app/core/domain/entities/sync_status.dart';
import 'package:intl/intl.dart'; import 'package:problem_check_system/app/core/extensions/datetime_extension.dart';
import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_list_item_entity.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:tdesign_flutter/tdesign_flutter.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'dart:io'; //
//
enum ProblemCardViewType { buttons, checkbox }
class ProblemCard extends StatelessWidget { class ProblemCard extends StatelessWidget {
final Problem problem; final ProblemListItemEntity problemListItem;
final ProblemCardViewType viewType; final bool isSelected;
final Function(Problem, bool)? onChanged; final VoidCallback? onTap;
final Function()? onTap; final Widget? actions;
final bool isSelected; //
const ProblemCard({ const ProblemCard({
super.key, super.key,
required this.problem, required this.problemListItem,
this.viewType = ProblemCardViewType.buttons, this.isSelected = false,
this.onChanged,
this.onTap, this.onTap,
required this.isSelected, // this.actions,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//
final bool isDeleted = final bool isDeleted =
problem.syncStatus == ProblemSyncStatus.pendingDelete; problemListItem.problemEntity.syncStatus == SyncStatus.pendingDelete;
final Color cardColor = isDeleted final Color cardColor = isDeleted
? Colors.grey[300]! ? Colors.grey[200]! // 使
: Theme.of(context).cardColor; : Theme.of(context).cardColor;
final Color contentColor = isDeleted final Color contentColor = isDeleted
? Colors.grey[600]! ? Colors.grey[600]!
: Theme.of(context).textTheme.bodyMedium!.color!; : Theme.of(context).textTheme.bodyMedium!.color!;
return Card( 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, color: cardColor,
child: InkWell( child: InkWell(
onTap: viewType == ProblemCardViewType.checkbox onTap: onTap,
? () { // FIX 5:
onChanged?.call(problem, !isSelected); child: Column(
} children: [
: null, Padding(
padding: EdgeInsets.all(12.r),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ 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( Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(width: 16.w), _buildImageWidget(isDeleted),
Icon(Icons.location_on, color: contentColor, size: 16.h), SizedBox(width: 12.w), // FIX 7:
SizedBox(width: 8.w), // 使 Expanded
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( Expanded(
child: Text( child: _buildDescriptionAndCompany(contentColor),
DateFormat(
'yyyy-MM-dd HH:mm:ss',
).format(problem.creationTime),
style: TextStyle(fontSize: 12.sp, color: contentColor),
), ),
],
), ),
SizedBox(height: 12.h),
// const Divider(height: 1, color: Color(0xFFEEEEEE)),
// SizedBox(height: 10.h),
_buildLocationAndTime(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,
), ),
// actions线
if (actions != null) ...[
// const Divider(height: 1, color: Color(0xFFEEEEEE)),
// SizedBox(height: 10.h),
_buildBottomActionRow(),
], ],
], ],
), ),
const Spacer(),
_buildBottomActions(isDeleted),
],
), ),
SizedBox(height: 8.h), );
], }
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) { Widget _buildImageWidget(bool isDeleted) {
return AspectRatio( return SizedBox(
aspectRatio: 1, // width: 64.w,
height: 64.w,
child: ClipRRect(
borderRadius: BorderRadius.circular(8.r),
child: _buildImageContent(isDeleted), child: _buildImageContent(isDeleted),
),
); );
} }
Widget _buildImageContent(bool isDeleted) { Widget _buildImageContent(bool isDeleted) {
// // FIX 1: 访
if (problem.imageUrls.isEmpty || problem.imageUrls[0].localPath.isEmpty) { final String? imagePath = problemListItem.problemEntity.imageUrls.isNotEmpty
// ? problemListItem.problemEntity.imageUrls.first
return Image.asset( : null;
'assets/images/problem_preview.png',
fit: BoxFit.cover, // 使 cover
color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null,
);
}
final String imagePath = problem.imageUrls[0].localPath; final placeholder = Image.asset(
//
final File imageFile = File(imagePath);
if (!imageFile.existsSync()) {
//
return Image.asset(
'assets/images/problem_preview.png', 'assets/images/problem_preview.png',
fit: BoxFit.cover, fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null, color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null, colorBlendMode: isDeleted ? BlendMode.saturation : null,
); );
if (imagePath == null || imagePath.isEmpty) {
return placeholder;
} }
// 使 Image.file // BEST PRACTICE 1: existsSync()
return Image.file( return Image.file(
imageFile, File(imagePath),
fit: BoxFit.cover, // 使 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, fit: BoxFit.cover,
color: isDeleted ? Colors.grey[500] : null, color: isDeleted ? Colors.grey[500] : null,
colorBlendMode: isDeleted ? BlendMode.saturation : null, colorBlendMode: isDeleted ? BlendMode.saturation : null,
); errorBuilder: (context, error, stackTrace) {
//
return placeholder;
}, },
); );
} }
Widget _buildBottomActions(bool isDeleted) { Widget _buildIconText(IconData icon, String text, Color color) {
switch (viewType) {
case ProblemCardViewType.buttons:
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, // Row
children: [ children: [
if (!isDeleted) Icon(icon, color: color, size: 16.r),
CustomButton( SizedBox(width: 4.w),
text: '修改', Text(
onTap: () { text,
onTap; style: TextStyle(fontSize: 12.sp, color: color),
// controller.toProblemFormPageAndRefresh(problem: problem); overflow: TextOverflow.ellipsis,
}, maxLines: 1,
), ),
if (!isDeleted) SizedBox(width: 8.w), ],
CustomButton(
text: '查看',
onTap: () {
Get.toNamed(
AppRoutes.problemForm,
arguments: problem,
parameters: {'isReadOnly': 'true'},
); );
}, }
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,
), ),
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);
} }
},
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),
), ),
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