diff --git a/lib/app/core/pages/upload_page.dart b/lib/app/core/pages/upload_page.dart new file mode 100644 index 0000000..5db60e4 --- /dev/null +++ b/lib/app/core/pages/upload_page.dart @@ -0,0 +1 @@ +class UploadPageT {} diff --git a/lib/app/core/widgets/custom_app_bar.dart b/lib/app/core/pages/widgets/custom_app_bar.dart similarity index 100% rename from lib/app/core/widgets/custom_app_bar.dart rename to lib/app/core/pages/widgets/custom_app_bar.dart diff --git a/lib/app/core/widgets/upload_app_bar.dart b/lib/app/core/pages/widgets/upload_app_bar.dart similarity index 100% rename from lib/app/core/widgets/upload_app_bar.dart rename to lib/app/core/pages/widgets/upload_app_bar.dart diff --git a/lib/app/core/routes/app_pages.dart b/lib/app/core/routes/app_pages.dart index 78f7f2e..7641a52 100644 --- a/lib/app/core/routes/app_pages.dart +++ b/lib/app/core/routes/app_pages.dart @@ -31,7 +31,12 @@ abstract class AppPages { GetPage( name: AppRoutes.navigation, page: () => const NavigationPage(), - binding: NavigationBinding(), + binding: BindingsBuilder(() { + NavigationBinding().dependencies(); + EnterpriseListBinding().dependencies(); + ProblemBinding().dependencies(); + ProfileBinding().dependencies(); + }), ), GetPage( name: AppRoutes.home, diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart index 2d9a240..15144f2 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart @@ -5,7 +5,6 @@ import 'package:problem_check_system/app/features/enterprise/data/datasources/en import 'package:problem_check_system/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart'; import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; class EnterpriseListBinding extends Bindings { @@ -31,8 +30,5 @@ class EnterpriseListBinding extends Bindings { getEnterpriseListUsecase: Get.find(), ), ); - Get.lazyPut( - () => Get.find(), - ); } } diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart index 91c43ed..1114630 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart @@ -1,13 +1,9 @@ import 'package:get/get.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; class EnterpriseUploadBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => EnterpriseUploadController()); - Get.lazyPut( - () => Get.find(), - ); } } diff --git a/lib/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart b/lib/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart deleted file mode 100644 index 42cae86..0000000 --- a/lib/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart +++ /dev/null @@ -1,66 +0,0 @@ -// lib/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart - -import 'package:flutter/material.dart'; -import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; -import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/models/company_enum.dart'; -import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; - -/// ----------------------------------------------------------------------------- -/// [抽象基类] 企业列表控制器接口 -/// ----------------------------------------------------------------------------- -/// -/// 定义了所有希望驱动 `EnterpriseListView` 的控制器必须实现的属性和方法。 -/// -/// **设计思想:** -/// - **依赖倒置原则**: `EnterpriseListView` (高层模块) 不直接依赖于 -/// `EnterpriseListController` 或 `EnterpriseUploadController` (低层模块), -/// 而是两者都依赖于这个抽象 (`BaseEnterpriseListController`)。 -/// - **接口隔离原则**: 这个基类只定义了 `EnterpriseListView` 真正需要交互的成员, -/// 不多也不少,确保了 View 和 Controller 之间的最小化耦合。 -/// -abstract class BaseEnterpriseListController extends GetxController { - // --- 状态属性 (View 需要监听的数据) --- - - /// 可观察的企业列表数据源。 - /// View 将监听此列表的变化来刷新UI。 - RxList get enterpriseList; - - /// 是否正在加载数据。 - /// View 用它来显示或隐藏加载指示器 (如 `CircularProgressIndicator`)。 - RxBool get isLoading; - - /// [筛选功能] 企业名称的文本编辑器控制器。 - TextEditingController get nameController; - - /// [筛选功能] 已选择的企业类型。 - Rx get selectedType; - - /// [筛选功能] 已选择的开始日期。 - Rx get startDate; - - /// [筛选功能] 已选择的结束日期。 - Rx get endDate; - - /// [选择功能] 被选中的企业集合。 - /// 使用 `Set` 可以高效地进行增、删、查操作,并保证元素的唯一性。 - RxSet get selectedEnterprises; - - // --- 操作方法 (View 需要调用的行为) --- - - /// 执行搜索/筛选操作。 - /// 由 View 中的“查询”按钮调用。 - void search(); - - /// 清空所有筛选条件并重新加载数据。 - /// 由 View 中的“重置”按钮调用。 - void clearFilters(); - - /// 处理整个卡片的点击事件。 - /// 在不同模式下(管理/选择),此方法的具体行为由子类实现。 - void onItemTap(EnterpriseListItem item); - - /// 处理卡片上复选框状态的变化。 - /// 当用户在选择模式下勾选或取消勾选时被调用。 - void onSelectionChanged(Enterprise enterprise); -} diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart index 9381022..750ef7c 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart @@ -8,7 +8,6 @@ import 'package:problem_check_system/app/features/enterprise/domain/entities/ent import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprise_list_usecase.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_form_controller.dart'; -import 'base_enterprise_list_controller.dart'; /// ----------------------------------------------------------------------------- /// [具体实现] 企业管理列表控制器 @@ -22,25 +21,18 @@ import 'base_enterprise_list_controller.dart'; /// - 处理卡片点击事件,导航到企业详情页或编辑页。 /// - 提供导航到新增企业页面的方法。 /// -class EnterpriseListController extends BaseEnterpriseListController { +class EnterpriseListController extends GetxController { final GetEnterpriseListUsecase getEnterpriseListUsecase; EnterpriseListController({required this.getEnterpriseListUsecase}); // --- 实现基类中定义的属性 --- - @override final enterpriseList = [].obs; - @override final isLoading = false.obs; - @override final nameController = TextEditingController(); - @override final selectedType = Rx(null); - @override final startDate = Rx(null); - @override final endDate = Rx(null); - @override final selectedEnterprises = {}.obs; // 在此页面,此 Set 通常为空 @override @@ -51,12 +43,10 @@ class EnterpriseListController extends BaseEnterpriseListController { // --- 实现基类中定义的方法 --- - @override void search() { fetchEnterprises(); } - @override void clearFilters() { nameController.clear(); selectedType.value = null; @@ -65,13 +55,11 @@ class EnterpriseListController extends BaseEnterpriseListController { fetchEnterprises(); } - @override void onItemTap(EnterpriseListItem item) { // 在管理模式下,点击卡片是导航到企业详情(问题列表) navigateToEnterpriseInfoPage(item.enterprise); } - @override void onSelectionChanged(Enterprise enterprise) { // 管理模式下没有选择功能,所以此方法体为空。 // 这是符合接口隔离原则的正常现象。 diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart index 034e6a6..76d9c29 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart @@ -1,11 +1,8 @@ // lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart -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/features/enterprise/domain/entities/enterprise.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; -import 'base_enterprise_list_controller.dart'; // 假设你有一个专门获取待上传列表的 Usecase // import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_pending_uploads_usecase.dart'; @@ -21,56 +18,26 @@ import 'base_enterprise_list_controller.dart'; /// - 处理卡片点击和复选框事件,更新选择集。 /// - 提供确认上传的方法,并将结果返回给上一个页面。 /// -class EnterpriseUploadController extends BaseEnterpriseListController { +class EnterpriseUploadController extends GetxController { // final GetPendingUploadsUsecase getPendingUploadsUsecase; // EnterpriseUploadController({required this.getPendingUploadsUsecase}); // --- 实现基类中定义的属性 --- - @override final enterpriseList = [].obs; - @override final isLoading = false.obs; - @override final selectedEnterprises = {}.obs; // 此控制器会重度使用此 Set - // 在上传页,复杂的筛选功能通常是不需要的。 - // 我们仍然需要提供这些属性的实例以满足接口要求,但它们可能不会在UI中被使用。 - @override - final nameController = TextEditingController(); - @override - final selectedType = Rx(null); - @override - final startDate = Rx(null); - @override - final endDate = Rx(null); - @override void onInit() { super.onInit(); fetchPendingUploads(); } - // --- 实现基类中定义的方法 --- - @override - void search() { - // 上传页的“搜索”可能只是重新加载列表 - fetchPendingUploads(); - } - - @override - void clearFilters() { - // 上传页的“重置”可能只是清空一个简单的搜索框 - nameController.clear(); - fetchPendingUploads(); - } - - @override void onItemTap(EnterpriseListItem item) { // 在选择模式下,点击整个卡片的效果等同于操作复选框 onSelectionChanged(item.enterprise); } - @override void onSelectionChanged(Enterprise enterprise) { // 核心逻辑:切换单个企业的选中状态 if (selectedEnterprises.contains(enterprise)) { @@ -80,16 +47,10 @@ class EnterpriseUploadController extends BaseEnterpriseListController { } } - // --- EnterpriseUploadController 特有的方法 --- - /// 从数据源获取待上传的企业列表 Future fetchPendingUploads() async { isLoading.value = true; try { - // 在这里调用您的 Usecase 来获取待上传列表 - // final result = await getPendingUploadsUsecase.call(); - // enterpriseList.assignAll(result); - // --- 使用模拟数据进行演示 --- await Future.delayed(const Duration(seconds: 1)); // final mockData = createMockEnterpriseListItems(); diff --git a/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart b/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart index afc4398..9178729 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:problem_check_system/app/core/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/enterprise/presentation/controllers/enterprise_form_controller.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_form_view.dart'; diff --git a/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart b/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart index 77d5c15..ff45b92 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart @@ -1,7 +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/widgets/custom_app_bar.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart'; +import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; +import 'package:problem_check_system/app/core/models/company_enum.dart'; +import 'package:problem_check_system/app/core/pages/widgets/custom_app_bar.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart'; import '../controllers/enterprise_list_controller.dart'; class EnterpriseListPage extends GetView { @@ -15,7 +18,280 @@ class EnterpriseListPage extends GetView { actionsVisible: true, onAddPressed: () => controller.navigateToAddForm(), ), - body: EnterpriseListView(controller: controller), + body: Column( + children: [ + // 根据参数决定是否构建筛选区域 + _buildFilterSection(), + const Divider(height: 1, thickness: .1), + + // 列表区域总是显示 + Expanded(child: _buildEnterpriseList()), + ], + ), ); } + + /// 构建可展开的筛选查询区域。 + /// 此方法的所有交互都绑定到 `BaseEnterpriseListController` 的接口, + /// 因此它无需关心具体的控制器是哪个。 + Widget _buildFilterSection() { + return ExpansionTile( + title: const Text('筛选查询'), + leading: const Icon(Icons.filter_alt_outlined), + initiallyExpanded: false, // 默认收起 + 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), + + // 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), + + // 3. 日期范围选择器 + Row( + children: [ + Expanded( + child: _buildDatePickerField('开始日期', controller.startDate), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildDatePickerField('结束日期', controller.endDate), + ), + ], + ), + 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), + ), + ), + ), + 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), // 增加底部间距 + ], + ), + ), + ], + ); + } + + /// 构建日期选择器的辅助方法 + 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, + ), + ), + ), + ), + ); + } + + /// 构建企业列表 + Widget _buildEnterpriseList() { + // 使用 Obx 包裹以监听 controller 中所有 Rx 变量的变化 + return Obx(() { + // 在列表为空且仍在加载时显示加载指示器 + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + // 在加载完成但列表为空时显示提示信息 + if (controller.enterpriseList.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.search(), + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemCount: controller.enterpriseList.length, + itemBuilder: (context, index) { + final item = controller.enterpriseList[index]; + final enterprise = item.enterprise; + // 核心: 从 base controller 获取选中状态 + final isSelected = controller.selectedEnterprises.contains( + enterprise, + ); + + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: UnifiedEnterpriseCard( + enterpriseListItem: item, + isSelected: isSelected, + // 根据外部传入的 itemMode 决定卡片内部的 mode + // --- [核心] 将卡片的所有交互事件转发给 controller 的抽象方法 --- + onTap: () => controller.onItemTap(item), + // [重点] 构建并传入符合贴边样式的 actions + actions: Row( + mainAxisSize: MainAxisSize.min, + // 让按钮在垂直方向上对齐 + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // “修改信息” 按钮 + TextButton.icon( + onPressed: () { + /* 编辑逻辑 */ + }, + icon: Icon( + Icons.edit_outlined, + size: 16.sp, + color: Colors.grey.shade700, + ), + label: Text( + '修改信息', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey.shade700, + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 12.h, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + + // “查看问题” 按钮 (关键样式在这里) + ElevatedButton( + onPressed: () { + /* 查看问题逻辑 */ + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A5F5), + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric( + horizontal: 20.w, + vertical: 12.h, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // [!!!] 移除按钮自身的阴影,因为它现在在卡片内部 + elevation: 0, + // [!!!] 自定义形状,只保留左上角圆角,以完美贴合卡片边缘 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.r), + ), + ), + ), + child: Text( + "查看问题", + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + }); + } } diff --git a/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart b/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart index 67b70f8..5cb996a 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:get/get_state_manager/src/simple/get_view.dart'; -import 'package:problem_check_system/app/core/widgets/upload_app_bar.dart'; +import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart'; class EnterpriseUploadPage extends GetView { const EnterpriseUploadPage({super.key}); @@ -18,11 +20,93 @@ class EnterpriseUploadPage extends GetView { print("object"); }, ), - body: EnterpriseListView( - controller: controller, - filterSectionVisible: false, - itemMode: EnterpriseListPageMode.select, + body: _buildEnterpriseList(), + bottomSheet: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + ), + child: ElevatedButton( + onPressed: null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.r), + ), + minimumSize: Size(double.infinity, 48.h), + ), + child: Text('点击上传 (1)'), + ), ), ); } + + Widget _buildEnterpriseList() { + // 使用 Obx 包裹以监听 controller 中所有 Rx 变量的变化 + return Obx(() { + // 在列表为空且仍在加载时显示加载指示器 + if (controller.isLoading.value && controller.enterpriseList.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + // 在加载完成但列表为空时显示提示信息 + if (controller.enterpriseList.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.fetchPendingUploads(), + child: ListView.builder( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), + itemCount: controller.enterpriseList.length, + itemBuilder: (context, index) { + final item = controller.enterpriseList[index]; + final enterprise = item.enterprise; + // 核心: 从 base controller 获取选中状态 + final isSelected = controller.selectedEnterprises.contains( + enterprise, + ); + + return Padding( + padding: EdgeInsets.only(bottom: 12.h), + child: UnifiedEnterpriseCard( + enterpriseListItem: item, + isSelected: isSelected, + // 根据外部传入的 itemMode 决定卡片内部的 mode + // --- [核心] 将卡片的所有交互事件转发给 controller 的抽象方法 --- + onTap: () => controller.onItemTap(item), + actions: Padding( + // 给 Checkbox 自身的点击区域和视觉留出空间 + padding: EdgeInsets.only(right: 8.w), + child: Checkbox( + value: isSelected, + onChanged: (value) {}, + // controller.toggleSelection(enterprise.id), + ), + ), + ), + ); + }, + ), + ); + }); + } } diff --git a/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart b/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart index df847ff..a9b0cb5 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart +++ b/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart @@ -3,27 +3,22 @@ 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/models/sync_status.dart'; import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart'; // 主卡片组件 class EnterpriseCard extends StatelessWidget { final EnterpriseListItem enterpriseListItem; final bool isSelected; - final VoidCallback onEdit; - final VoidCallback onViewProblems; final VoidCallback? onTap; - final ValueChanged? onSelectionChanged; - final EnterpriseListPageMode mode; + final Widget? trailing; + final Widget? bottomAction; const EnterpriseCard({ super.key, required this.enterpriseListItem, - required this.isSelected, - required this.onEdit, - required this.onViewProblems, - required this.mode, + this.isSelected = false, this.onTap, - this.onSelectionChanged, + this.trailing, + this.bottomAction, }); @override @@ -66,59 +61,58 @@ class EnterpriseCard extends StatelessWidget { ), ), - if (mode == EnterpriseListPageMode.normal) - Positioned( - bottom: 0, // 相对于卡片底部 - right: 0, // 相对于卡片右侧 - child: Row( - children: [ - // --- “修改信息” 按钮 --- - TextButton.icon( - onPressed: onEdit, - icon: Icon(Icons.edit_outlined, size: 16.sp), - label: Text('修改信息', style: TextStyle(fontSize: 9.5.sp)), - style: TextButton.styleFrom( - foregroundColor: Colors.grey.shade600, - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 8.h, - ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(0, 0), + Positioned( + bottom: 0, // 相对于卡片底部 + right: 0, // 相对于卡片右侧 + child: Row( + children: [ + // --- “修改信息” 按钮 --- + TextButton.icon( + onPressed: () {}, + icon: Icon(Icons.edit_outlined, size: 16.sp), + label: Text('修改信息', style: TextStyle(fontSize: 9.5.sp)), + style: TextButton.styleFrom( + foregroundColor: Colors.grey.shade600, + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(0, 0), ), - // --- “查看问题” 按钮 --- - ElevatedButton( - onPressed: onViewProblems, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF42A5F5), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12.r), - // 注意:右下角因为贴边,不再需要圆角,否则会有缝隙 - // bottomRight: Radius.circular(12.r), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 8.h, + ), + // --- “查看问题” 按钮 --- + ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A5F5), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.r), + // 注意:右下角因为贴边,不再需要圆角,否则会有缝隙 + // bottomRight: Radius.circular(12.r), ), - // elevation: 0, // 移除阴影,因为它已经被剪裁了 - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - minimumSize: const Size(0, 0), ), - child: Text( - "查看问题", - style: TextStyle( - fontSize: 13.sp, - fontWeight: FontWeight.w500, - ), + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 8.h, + ), + // elevation: 0, // 移除阴影,因为它已经被剪裁了 + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(0, 0), + ), + child: Text( + "查看问题", + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, ), ), - ], - ), + ), + ], ), + ), ], ), ), @@ -245,23 +239,22 @@ class EnterpriseCard extends StatelessWidget { backgroundColor: Colors.red.shade50, ), const Spacer(), - if (mode == EnterpriseListPageMode.select) - Theme( - data: Theme.of(context).copyWith( - checkboxTheme: CheckboxThemeData( - // 将点击区域收缩,移除额外的 padding - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - // (可选) 进一步压缩视觉密度 - visualDensity: VisualDensity.compact, - ), - ), - child: Checkbox( - value: isSelected, - onChanged: onSelectionChanged, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - ), - ), + // Theme( + // data: Theme.of(context).copyWith( + // checkboxTheme: CheckboxThemeData( + // // 将点击区域收缩,移除额外的 padding + // materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + // // (可选) 进一步压缩视觉密度 + // visualDensity: VisualDensity.compact, + // ), + // ), + // child: Checkbox( + // value: isSelected, + // onChanged: onSelectionChanged, + // materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + // visualDensity: VisualDensity.compact, + // ), + // ), ], ); } diff --git a/lib/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart b/lib/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart deleted file mode 100644 index a218961..0000000 --- a/lib/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart +++ /dev/null @@ -1,280 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/extensions/datetime_extension.dart'; -import 'package:problem_check_system/app/core/models/company_enum.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; -import 'enterprise_card.dart'; - -/// 定义列表项的显示模式 -enum EnterpriseListPageMode { - /// 普通模式,显示操作按钮 - normal, - - /// 选择模式,显示复选框 - select, -} - -/// ----------------------------------------------------------------------------- -/// [共享UI组件] 企业列表视图 -/// ----------------------------------------------------------------------------- -/// -/// 一个可复用的、“哑”的(Dumb)UI组件,负责渲染企业列表和筛选器。 -/// -/// **设计思想:** -/// - **高内聚,低耦合**: 此 View 只负责 UI 的展示和用户事件的转发。它不包含任何业务逻辑。 -/// - **依赖于抽象**: 通过 `GetView`,它依赖于我们定义的 -/// 抽象基类 (`BaseEnterpriseListController`),而不是任何具体的控制器实现。 -/// 这使得它可以被任何实现了该接口的控制器驱动。 -/// -class EnterpriseListView extends StatelessWidget { - const EnterpriseListView({ - super.key, - required this.controller, - this.filterSectionVisible = true, - this.itemMode = EnterpriseListPageMode.normal, - }); - final BaseEnterpriseListController controller; - - /// 是否显示顶部的筛选区域 - final bool filterSectionVisible; - - /// 列表项的模式(按钮模式或选择模式) - final EnterpriseListPageMode itemMode; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // 根据参数决定是否构建筛选区域 - if (filterSectionVisible) ...[ - _buildFilterSection(), - const Divider(height: 1, thickness: .1), - ], - // 列表区域总是显示 - Expanded(child: _buildEnterpriseList()), - ], - ); - } - - /// 构建可展开的筛选查询区域。 - /// 此方法的所有交互都绑定到 `BaseEnterpriseListController` 的接口, - /// 因此它无需关心具体的控制器是哪个。 - Widget _buildFilterSection() { - return ExpansionTile( - title: const Text('筛选查询'), - leading: const Icon(Icons.filter_alt_outlined), - initiallyExpanded: false, // 默认收起 - 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), - - // 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), - - // 3. 日期范围选择器 - Row( - children: [ - Expanded( - child: _buildDatePickerField('开始日期', controller.startDate), - ), - SizedBox(width: 12.w), - Expanded( - child: _buildDatePickerField('结束日期', controller.endDate), - ), - ], - ), - 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), - ), - ), - ), - 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), // 增加底部间距 - ], - ), - ), - ], - ); - } - - /// 构建日期选择器的辅助方法 - 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, - ), - ), - ), - ), - ); - } - - /// 构建企业列表 - Widget _buildEnterpriseList() { - // 使用 Obx 包裹以监听 controller 中所有 Rx 变量的变化 - return Obx(() { - // 在列表为空且仍在加载时显示加载指示器 - if (controller.isLoading.value && controller.enterpriseList.isEmpty) { - return const Center(child: CircularProgressIndicator()); - } - // 在加载完成但列表为空时显示提示信息 - if (controller.enterpriseList.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.search(), - child: ListView.builder( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), - itemCount: controller.enterpriseList.length, - itemBuilder: (context, index) { - final item = controller.enterpriseList[index]; - final enterprise = item.enterprise; - // 核心: 从 base controller 获取选中状态 - final isSelected = controller.selectedEnterprises.contains( - enterprise, - ); - - return Padding( - padding: EdgeInsets.only(bottom: 12.h), - child: EnterpriseCard( - enterpriseListItem: item, - isSelected: isSelected, - // 根据外部传入的 itemMode 决定卡片内部的 mode - mode: itemMode, - // --- [核心] 将卡片的所有交互事件转发给 controller 的抽象方法 --- - onTap: () => controller.onItemTap(item), - onSelectionChanged: (value) => - controller.onSelectionChanged(enterprise), - - // 对于只存在于特定 Controller 的方法,需要进行类型检查。 - // 这样可以确保即使在上传页面(其控制器没有这些方法),代码也不会崩溃。 - onEdit: () { - if (controller is EnterpriseListController) { - // 只有当控制器是 EnterpriseListController 类型时,才调用其特有方法 - (controller as EnterpriseListController).navigateToEditForm( - enterprise, - ); - } - }, - onViewProblems: () { - if (controller is EnterpriseListController) { - (controller as EnterpriseListController) - .navigateToEnterpriseInfoPage(enterprise); - } - }, - ), - ); - }, - ), - ); - }); - } -} 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 new file mode 100644 index 0000000..f5e9328 --- /dev/null +++ b/lib/app/features/enterprise/presentation/pages/widgets/unified_enterprise_card.dart @@ -0,0 +1,230 @@ +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/core/models/sync_status.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise_list_item.dart'; + +// [重构 1] 新的统一卡片组件 +class UnifiedEnterpriseCard extends StatelessWidget { + final EnterpriseListItem enterpriseListItem; + final bool isSelected; + final VoidCallback? onTap; + + final Widget? actions; + + const UnifiedEnterpriseCard({ + super.key, + required this.enterpriseListItem, + this.isSelected = false, // 默认未选中 + this.onTap, + this.actions, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: isSelected ? 4.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), + ), + // [关键 1] 必须剪裁,才能让按钮的形状与卡片边缘完美融合 + clipBehavior: Clip.antiAlias, + margin: EdgeInsets.zero, + child: InkWell( + onTap: onTap, + // [关键 2] 使用 Column 作为主布局 + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- 区域 1: 顶部和中部的内容 --- + // 这部分内容有完整的左右内边距 + Padding( + padding: EdgeInsets.all(16.0.w), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTopSection(), + SizedBox(height: 12.h), + _buildMiddleSection(), + ], + ), + ), + + // 可选的分割线,让布局更清晰 + // if (actions != null) const Divider(height: 1), + + // --- 区域 2: 底部的操作栏 --- + // [关键 3] 这一行是整个布局的核心 + if (actions != null) _buildBottomActionRow(), + ], + ), + ), + ); + } + + /// 构建顶部区域:企业名称、类型和状态 + Widget _buildTopSection() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '企业名称', + style: TextStyle( + fontSize: 9.sp, + color: Colors.grey.shade500, + ), // .sp 适配字体 + ), + SizedBox(height: 4.h), // .h 适配垂直间距 + Text( + enterpriseListItem.enterprise.name, + style: TextStyle( + fontSize: 12.5.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '企业类型', + style: TextStyle(fontSize: 9.sp, color: Colors.grey.shade500), + ), + SizedBox(height: 4.h), + Text( + enterpriseListItem.enterprise.type, + style: TextStyle( + fontSize: 12.5.sp, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + // SizedBox(width: 8.w), + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 3.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.red.shade400, width: 1.w), + ), + child: Text( + enterpriseListItem.enterprise.syncStatus == SyncStatus.synced + ? '信息已上传' + : '信息未上传', + style: TextStyle(fontSize: 7.sp, color: Colors.red.shade400), + ), + ), + ], + ); + } + + /// 构建中间区域:问题总数和创建时间 + Widget _buildMiddleSection() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.description_outlined, color: Colors.grey, size: 16.sp), + SizedBox(width: 4.w), + Text( + '问题总数: ', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + Text( + enterpriseListItem.totalProblems.toString(), + style: TextStyle( + fontSize: 12.5.sp, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(Icons.access_time, color: Colors.grey, size: 16.sp), + Text( + '创建时间: ${enterpriseListItem.enterprise.creationTime.toDateTimeString()}', + style: TextStyle(fontSize: 12.sp, color: Colors.grey), + ), + ], + ), + ], + ); + } + + /// 构建底部的“标签+操作”行 + Widget _buildBottomActionRow() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // [关键 4] 只给左侧的标签组添加左边距 + Padding( + padding: EdgeInsets.only(left: 16.w), + child: Row( + children: [ + _buildTag( + text: '已上传 ${enterpriseListItem.uploadedProblems}', + textColor: Colors.blue.shade700, + backgroundColor: Colors.blue.shade50, + ), + SizedBox(width: 8.w), + _buildTag( + text: '未上传 ${enterpriseListItem.pendingProblems}', + textColor: Colors.red.shade600, + backgroundColor: Colors.red.shade50, + ), + ], + ), + ), + + const Spacer(), + + // [关键 5] actions 插槽在这里,它自身没有任何外层 Padding,所以可以贴边 + actions!, + ], + ); + } + + /// 用于创建“已上传”和“未上传”标签的辅助方法 + 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/home/views/home_page.dart b/lib/app/features/home/views/home_page.dart index 44cf2de..b1111ea 100644 --- a/lib/app/features/home/views/home_page.dart +++ b/lib/app/features/home/views/home_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/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/home/controllers/home_controller.dart'; class HomePage extends GetView { diff --git a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart index ebd046e..34738aa 100644 --- a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart +++ b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart @@ -4,39 +4,39 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/core/services/network_status_service.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; +import 'package:problem_check_system/app/features/home/views/home_page.dart'; +import 'package:problem_check_system/app/features/my/views/my_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; class NavigationController extends GetxController { - static const int navigationKeyId = 1; var selectedIndex = 0.obs; - final GlobalKey bottomNavigationKey = GlobalKey(); - - final List pageRoutes = [ - AppRoutes.home, - AppRoutes.enterpriseList, - AppRoutes.problem, - AppRoutes.my, - ]; - /// floatingButton 拖动 final double _fabSize = 56.0; final double _edgePaddingX = 27.0.w; final double _edgePaddingY = 111.0.h; - final fabUploadPosition = Offset(337.0, 703.7).obs; + final fabUploadPosition = Offset(0, 0).obs; final NetworkStatusService networkStatusService; NavigationController({required this.networkStatusService}); /// get 选中的 RxBool get isOnline => networkStatusService.isOnline; - // // 页面列表 - // final List pages = [ - // const HomePage(), - // const EnterpriseListPage(), - // const ProblemPage(), - // const MyPage(), - // ]; + // 页面列表 + final List pages = const [ + HomePage(), + EnterpriseListPage(), + ProblemPage(), + MyPage(), + ]; + final navigationItems = const [ + Icon(Icons.home, color: Colors.white, size: 30), + Icon(Icons.business, color: Colors.white, size: 30), + Icon(Icons.list, color: Colors.white, size: 30), + Icon(Icons.person, color: Colors.white, size: 30), + ]; - // get currentPage => pages[selectedIndex.value]; + get currentPage => pages[selectedIndex.value]; @override void onInit() { @@ -45,22 +45,6 @@ class NavigationController extends GetxController { snapToEdge(); } - // void changePageIndex(int index) { - // selectedIndex.value = index; - // } - - // void changePageIndex(int index) { - // if (selectedIndex.value == index) return; // 避免重复点击同一项 - // selectedIndex.value = index; - - // // offAllNamed 会清除当前 nested Navigator 的堆栈并推入新页面, - // // 这完美模拟了底部导航栏的切换行为。 - // Get.offAllNamed( - // pageRoutes[index], - // id: navigationKeyId, // 指定要操作的是哪个 Navigator - // ); - // } - void changePageIndex(int index) { if (selectedIndex.value == index) return; selectedIndex.value = index; diff --git a/lib/app/features/navigation/presentation/pages/navigation_page.dart b/lib/app/features/navigation/presentation/pages/navigation_page.dart index c6c65d9..992ef4f 100644 --- a/lib/app/features/navigation/presentation/pages/navigation_page.dart +++ b/lib/app/features/navigation/presentation/pages/navigation_page.dart @@ -2,25 +2,8 @@ import 'package:curved_navigation_bar/curved_navigation_bar.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/routes/app_pages.dart'; -import 'package:problem_check_system/app/core/routes/app_routes.dart'; import 'package:problem_check_system/app/features/navigation/presentation/controllers/navigation_controller.dart'; -/// ----------------------------------------------------------------------------- -/// [主导航页面] NavigationPage -/// ----------------------------------------------------------------------------- -/// -/// 应用的主导航框架,包含底部导航栏和可拖动的悬浮按钮。 -/// -/// **核心设计:** -/// - **Stack布局**: 使用 `Stack` 将 `Scaffold` (页面主体) 和 `FloatingActionButton` -/// (悬浮按钮) 分层,使得悬浮按钮可以覆盖在所有主页面之上并自由拖动。 -/// - **IndexedStack**: 这是实现流畅切换的关键。它会一次性构建所有主页面,并将它们 -/// 保留在内存中,仅显示当前索引对应的页面。这避免了每次切换Tab时的页面重建, -/// 从而消除了“白屏闪烁”现象,并能完美保留每个页面的状态(如滚动位置)。 -/// - **GetX驱动**: 页面完全由 `NavigationController` 驱动。`Obx` 用于监听 -/// `selectedIndex` 的变化来更新 `IndexedStack` 的可见页面,以及监听悬浮按钮的位置和状态。 -/// class NavigationPage extends GetView { const NavigationPage({super.key}); @@ -28,41 +11,20 @@ class NavigationPage extends GetView { Widget build(BuildContext context) { return Stack( children: [ - // 1. Scaffold 作为 Stack 的底层,负责主要的页面结构 Scaffold( - // 使用 controller 中的 GlobalKey 关联 CurvedNavigationBar, - // 这在某些需要从外部代码控制导航栏状态的场景下很有用。 bottomNavigationBar: CurvedNavigationBar( - key: controller.bottomNavigationKey, backgroundColor: Colors.transparent, color: Colors.blueAccent, - items: const [ - Icon(Icons.home, color: Colors.white, size: 30), - Icon(Icons.business, color: Colors.white, size: 30), - Icon(Icons.list, color: Colors.white, size: 30), - Icon(Icons.person, color: Colors.white, size: 30), - ], - onTap: (index) { - controller.changePageIndex(index); - }, + items: controller.navigationItems, + onTap: controller.changePageIndex, ), - // [核心优化] 使用 Obx 包裹 IndexedStack body: Obx( () => IndexedStack( - // IndexedStack 的可见子项由 controller 的 selectedIndex 驱动 index: controller.selectedIndex.value, - // [关键] 一次性构建所有导航页面,并让它们在内存中保持活动状态 - children: [ - _getPageForRoute(AppRoutes.home), - _getPageForRoute(AppRoutes.enterpriseList), - _getPageForRoute(AppRoutes.problem), - _getPageForRoute(AppRoutes.my), - ], + children: controller.pages, ), ), ), - - // 2. 将悬浮按钮作为 Stack 的顶层,使用 Positioned 进行精确定位 Obx(() { final isOnline = controller.isOnline.value; return Positioned( @@ -95,40 +57,4 @@ class NavigationPage extends GetView { ], ); } - - /// --------------------------------------------------------------------------- - /// [辅助方法] _getPageForRoute - /// --------------------------------------------------------------------------- - /// - /// 根据提供的路由名称,从 `AppPages.routes` 列表中查找对应的 `GetPage` 配置, - /// 然后执行其 `binding` 并返回其实例化的页面 `Widget`。 - /// - /// 这是连接 GetX 路由系统和 `IndexedStack` 的桥梁。 - /// - /// @param route 路由名称 (e.g., AppRoutes.home) - /// @return 对应的页面 Widget,如果找不到则返回一个错误提示 Widget。 - /// - Widget _getPageForRoute(String route) { - // 使用 .firstWhereOrNull 查找路由配置,避免在找不到时抛出异常 - final GetPage? page = AppPages.routes.firstWhereOrNull( - (p) => p.name == route, - ); - - // 如果成功找到了页面配置 - if (page != null) { - // [关键] 手动执行此页面的依赖注入。 - // GetX 的智能管理系统 (配合 fenix: true) 会确保每个 Binding 只被执行一次。 - page.binding?.dependencies(); - // 调用 page() 函数来创建 Widget 实例 - return page.page(); - } - - // 如果在 AppPages 中没有找到对应的路由配置,则返回一个错误提示页面 - return Center( - child: Text( - '错误: 路由 "$route" 未在 AppPages 中定义', - textAlign: TextAlign.center, - ), - ); - } } diff --git a/lib/app/features/problem/presentation/views/problem_form_page.dart b/lib/app/features/problem/presentation/views/problem_form_page.dart index 455ad3d..7c3aee9 100644 --- a/lib/app/features/problem/presentation/views/problem_form_page.dart +++ b/lib/app/features/problem/presentation/views/problem_form_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:problem_check_system/app/core/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/presentation/controllers/problem_form_controller.dart'; class ProblemFormPage extends GetView { diff --git a/lib/app/features/problem/presentation/views/problem_page.dart b/lib/app/features/problem/presentation/views/problem_page.dart index c664bdf..e8c2987 100644 --- a/lib/app/features/problem/presentation/views/problem_page.dart +++ b/lib/app/features/problem/presentation/views/problem_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/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/presentation/controllers/problem_controller.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_list_page.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/widgets/current_filter_bar.dart'; diff --git a/lib/app/features/problem/presentation/views/problem_upload_page.dart b/lib/app/features/problem/presentation/views/problem_upload_page.dart index 6c75991..1cb0ae9 100644 --- a/lib/app/features/problem/presentation/views/problem_upload_page.dart +++ b/lib/app/features/problem/presentation/views/problem_upload_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/widgets/upload_app_bar.dart'; +import 'package:problem_check_system/app/core/pages/widgets/upload_app_bar.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_list_page.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/widgets/problem_card.dart';