From 664cb4ca9f9c956ec5f608494b5b410d8db3c7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Tue, 4 Nov 2025 15:24:09 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/unified_enterprise_card.dart | 45 +- .../problem_local_data_source.dart | 8 +- .../repositories/problem_repository_impl.dart | 57 ++- .../domain/entities/problem_bind_status.dart | 11 + .../entities/problem_list_item_entity.dart | 21 +- .../bindings/problem_upload_binding.dart | 28 +- .../controllers/problem_list_controller.dart | 173 ++++---- .../models/problem_card_state.dart | 97 ---- .../models/problem_form_model.dart | 13 - .../presentation/pages/problem_list_page.dart | 417 +++++++++++------- .../pages/widgets/problem_card.dart | 382 ++++++++-------- 11 files changed, 659 insertions(+), 593 deletions(-) create mode 100644 lib/app/features/problem/domain/entities/problem_bind_status.dart delete mode 100644 lib/app/features/problem/presentation/models/problem_card_state.dart delete mode 100644 lib/app/features/problem/presentation/models/problem_form_model.dart diff --git a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart index 4845c2f..6c26d59 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart +++ b/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, - ), - ), - ); - } } diff --git a/lib/app/features/problem/data/datasources/problem_local_data_source.dart b/lib/app/features/problem/data/datasources/problem_local_data_source.dart index a8ca95e..8f38733 100644 --- a/lib/app/features/problem/data/datasources/problem_local_data_source.dart +++ b/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 '''; diff --git a/lib/app/features/problem/data/repositories/problem_repository_impl.dart b/lib/app/features/problem/data/repositories/problem_repository_impl.dart index 09f7f7b..fdcdb81 100644 --- a/lib/app/features/problem/data/repositories/problem_repository_impl.dart +++ b/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> 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 imageUrls = []; + final imageUrlsRaw = map['imageUrls']; + if (imageUrlsRaw is String && imageUrlsRaw.isNotEmpty) { + try { + imageUrls = List.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(); } diff --git a/lib/app/features/problem/domain/entities/problem_bind_status.dart b/lib/app/features/problem/domain/entities/problem_bind_status.dart new file mode 100644 index 0000000..c7cf5d0 --- /dev/null +++ b/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); +} diff --git a/lib/app/features/problem/domain/entities/problem_list_item_entity.dart b/lib/app/features/problem/domain/entities/problem_list_item_entity.dart index 840b9f8..9b6293a 100644 --- a/lib/app/features/problem/domain/entities/problem_list_item_entity.dart +++ b/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; + } + } } diff --git a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart b/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart index 3c2dc69..655a1e4 100644 --- a/lib/app/features/problem/presentation/bindings/problem_upload_binding.dart +++ b/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()); } } diff --git a/lib/app/features/problem/presentation/controllers/problem_list_controller.dart b/lib/app/features/problem/presentation/controllers/problem_list_controller.dart index 304a587..84ad1c6 100644 --- a/lib/app/features/problem/presentation/controllers/problem_list_controller.dart +++ b/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 = [].obs; + final problemList = [].obs; final isLoading = false.obs; final isSyncing = false.obs; final nameController = TextEditingController(); - final selectedType = Rx(null); + // final selectedType = Rx(null); final startDate = Rx(null); final endDate = Rx(null); - final selectedEnterprises = {}.obs; + final selectedProblems = {}.obs; final ExpansibleController expansibleController = ExpansibleController(); @override @@ -45,41 +43,39 @@ class ProblemListController extends GetxController { super.onClose(); } - void search() {} - // --- 实现基类中定义的方法 --- // 核心流程方法 - // Future 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 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 _showConflictDialog(EnterpriseConflict conflict) { @@ -150,46 +146,42 @@ class ProblemListController extends GetxController { // ); // } - // Future 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 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 navigateToProblemForm({ - Enterprise? enterprise, - FormMode? fromMode, - }) async { + /// 导航到编辑表单页面 + Future 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 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 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), + ); + } + } } diff --git a/lib/app/features/problem/presentation/models/problem_card_state.dart b/lib/app/features/problem/presentation/models/problem_card_state.dart deleted file mode 100644 index f89dc61..0000000 --- a/lib/app/features/problem/presentation/models/problem_card_state.dart +++ /dev/null @@ -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 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}); -} diff --git a/lib/app/features/problem/presentation/models/problem_form_model.dart b/lib/app/features/problem/presentation/models/problem_form_model.dart deleted file mode 100644 index 8f62ed9..0000000 --- a/lib/app/features/problem/presentation/models/problem_form_model.dart +++ /dev/null @@ -1,13 +0,0 @@ -class ProblemFormModel { - final String enterpriseName; - final String description; - final String location; - final List imageUrls; - - ProblemFormModel({ - required this.enterpriseName, - required this.description, - required this.location, - required this.imageUrls, - }); -} diff --git a/lib/app/features/problem/presentation/pages/problem_list_page.dart b/lib/app/features/problem/presentation/pages/problem_list_page.dart index 5c4b652..ee664e0 100644 --- a/lib/app/features/problem/presentation/pages/problem_list_page.dart +++ b/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 { const ProblemListPage({super.key}); @@ -12,171 +13,289 @@ class ProblemListPage extends GetView { 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 { - // // 其他视图类型(list、grid等):使用 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( + // 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 _showDeleteConfirmationDialog(Problem problem) async { - // 确保在返回前关闭可能存在的snackbar - if (Get.isSnackbarOpen) { - Get.closeCurrentSnackbar(); - } - return await Get.bottomSheet( - 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 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, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }); } } diff --git a/lib/app/features/problem/presentation/pages/widgets/problem_card.dart b/lib/app/features/problem/presentation/pages/widgets/problem_card.dart index c061180..85249c8 100644 --- a/lib/app/features/problem/presentation/pages/widgets/problem_card.dart +++ b/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: [ - 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: [ + 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: [ - 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!, + ], + ); } }