diff --git a/lib/app/core/routes/app_pages.dart b/lib/app/core/routes/app_pages.dart index 49e15c6..78f7f2e 100644 --- a/lib/app/core/routes/app_pages.dart +++ b/lib/app/core/routes/app_pages.dart @@ -1,9 +1,13 @@ import 'package:get/get.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/bindings/enterprise_form_binding.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/bindings/enterprise_info_binding.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_form_page.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_list_page.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_upload_page.dart'; +import 'package:problem_check_system/app/features/my/bindings/profile_binding.dart'; +import 'package:problem_check_system/app/features/my/views/my_page.dart'; import 'package:problem_check_system/app/features/navigation/presentation/bindings/navigation_binding.dart'; import 'package:problem_check_system/app/features/navigation/presentation/pages/navigation_page.dart'; import 'package:problem_check_system/app/features/home/bindings/home_binding.dart'; @@ -12,8 +16,10 @@ import 'package:problem_check_system/app/features/auth/bindings/login_binding.da import 'package:problem_check_system/app/features/auth/views/login_page.dart'; import 'package:problem_check_system/app/features/my/bindings/change_password_binding.dart'; import 'package:problem_check_system/app/features/my/views/change_password.dart'; +import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_binding.dart'; import 'package:problem_check_system/app/features/problem/presentation/bindings/problem_form_binding.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/problem_form_page.dart'; +import 'package:problem_check_system/app/features/problem/presentation/views/problem_page.dart'; import 'package:problem_check_system/app/features/problem/presentation/views/problem_upload_page.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/enterprise_info_page.dart'; @@ -43,15 +49,31 @@ abstract class AppPages { page: () => const ChangePasswordPage(), binding: ChangePasswordBinding(), ), + GetPage( + name: AppRoutes.problem, + page: () => const ProblemPage(), + binding: ProblemBinding(), + ), + GetPage( + name: AppRoutes.my, + page: () => const MyPage(), + binding: ProfileBinding(), + ), GetPage( name: AppRoutes.problemUpload, page: () => const ProblemUploadPage(), + binding: ProblemBinding(), ), GetPage( name: AppRoutes.problemForm, page: () => const ProblemFormPage(), binding: ProblemFormBinding(), ), + GetPage( + name: AppRoutes.enterpriseList, + page: () => const EnterpriseListPage(), + binding: EnterpriseListBinding(), + ), GetPage( name: AppRoutes.enterpriseInfo, page: () => const EnterpriseInfoPage(), diff --git a/lib/app/core/widgets/app_bar_add.dart b/lib/app/core/widgets/app_bar_add.dart deleted file mode 100644 index 5abc4d9..0000000 --- a/lib/app/core/widgets/app_bar_add.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; - -class AppBarAdd extends StatelessWidget implements PreferredSizeWidget { - const AppBarAdd({super.key, required this.titleName, this.onAddPressed}); - final String titleName; - final VoidCallback? onAddPressed; - - @override - Widget build(BuildContext context) { - return AppBar( - title: Text( - titleName, - style: TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'MyFont', - fontSize: 18.sp, - color: Colors.white, - ), - ), - backgroundColor: Colors.transparent, - flexibleSpace: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - ), - elevation: 0, - centerTitle: true, - actions: [ - IconButton( - icon: Icon(Icons.add, color: Colors.white), // 使用 .sp - onPressed: onAddPressed, - ), - ], - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/lib/app/core/widgets/custom_app_bar.dart b/lib/app/core/widgets/custom_app_bar.dart new file mode 100644 index 0000000..607dfae --- /dev/null +++ b/lib/app/core/widgets/custom_app_bar.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({ + super.key, + required this.titleName, + this.leadingVisible = false, + this.actionsVisible = false, + this.onAddPressed, + }); + final String titleName; + final bool leadingVisible; + final bool actionsVisible; + final VoidCallback? onAddPressed; + + @override + Widget build(BuildContext context) { + return AppBar( + title: Text( + titleName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'MyFont', + fontSize: 17.5.sp, + color: Colors.white, + ), + ), + automaticallyImplyLeading: leadingVisible, + leading: leadingVisible + ? IconButton( + icon: Icon( + Icons.arrow_back_ios_new_rounded, + size: 17.5.sp, + color: Colors.white, + ), + onPressed: () => Get.back(), + ) + : null, + backgroundColor: Colors.transparent, + flexibleSpace: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ), + ), + elevation: 0, + centerTitle: true, + actions: actionsVisible + ? [ + IconButton( + icon: Icon(Icons.add, color: Colors.white), // 使用 .sp + onPressed: onAddPressed, + ), + ] + : null, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart new file mode 100644 index 0000000..2d9a240 --- /dev/null +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_list_binding.dart @@ -0,0 +1,38 @@ +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/services/database_service.dart'; +import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; +import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; +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 { + @override + void dependencies() { + Get.put( + EnterpriseLocalDataSourceImpl( + databaseService: Get.find(), + ), + ); + Get.put(EnterpriseRemoteDataSourceImpl()); + Get.put( + EnterpriseRepositoryImpl( + localDataSource: Get.find(), + remoteDataSource: Get.find(), + ), + ); + Get.put( + GetEnterpriseListUsecase(repository: Get.find()), + ); + Get.lazyPut( + () => EnterpriseListController( + 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 0064f36..91c43ed 100644 --- a/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart +++ b/lib/app/features/enterprise/presentation/bindings/enterprise_upload_binding.dart @@ -1,8 +1,13 @@ 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() { - // TODO: implement 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 new file mode 100644 index 0000000..42cae86 --- /dev/null +++ b/lib/app/features/enterprise/presentation/controllers/base_enterprise_list_controller.dart @@ -0,0 +1,66 @@ +// 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 d57d598..9381022 100644 --- a/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart @@ -1,4 +1,4 @@ -// lib/app/modules/enterprise_list/enterprise_list_controller.dart +// lib/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -8,32 +8,81 @@ 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'; -class EnterpriseListController extends GetxController { - // --- 状态管理 --- - var isLoading = false.obs; - var enterpriseList = [].obs; - - // --- 筛选条件的状态 --- - final nameController = TextEditingController(); - final Rx selectedType = Rx(null); - final Rx startDate = Rx(null); - final Rx endDate = Rx(null); - +/// ----------------------------------------------------------------------------- +/// [具体实现] 企业管理列表控制器 +/// ----------------------------------------------------------------------------- +/// +/// 继承自 `BaseEnterpriseListController`,专门为“企业管理”页面服务。 +/// +/// **职责:** +/// - 实现完整的筛选、搜索和重置功能。 +/// - 从数据源加载完整的企业列表。 +/// - 处理卡片点击事件,导航到企业详情页或编辑页。 +/// - 提供导航到新增企业页面的方法。 +/// +class EnterpriseListController extends BaseEnterpriseListController { 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 void onInit() { super.onInit(); fetchEnterprises(); // 页面初始化时加载数据 } + // --- 实现基类中定义的方法 --- + + @override + void search() { + fetchEnterprises(); + } + + @override + void clearFilters() { + nameController.clear(); + selectedType.value = null; + startDate.value = null; + endDate.value = null; + fetchEnterprises(); + } + + @override + void onItemTap(EnterpriseListItem item) { + // 在管理模式下,点击卡片是导航到企业详情(问题列表) + navigateToEnterpriseInfoPage(item.enterprise); + } + + @override + void onSelectionChanged(Enterprise enterprise) { + // 管理模式下没有选择功能,所以此方法体为空。 + // 这是符合接口隔离原则的正常现象。 + } + + // --- EnterpriseListController 特有的方法 --- + /// 核心方法:获取企业列表 Future fetchEnterprises() async { + isLoading.value = true; try { - isLoading.value = true; final result = await getEnterpriseListUsecase.call( name: nameController.text, type: selectedType.value?.displayText, @@ -48,38 +97,14 @@ class EnterpriseListController extends GetxController { } } - /// 供 UI 调用的搜索方法 - void search() { - fetchEnterprises(); - } - - void refreshList() { - fetchEnterprises(); - } - - /// 清空筛选条件并重新加载 - void clearFilters() { - nameController.clear(); - selectedType.value = null; - startDate.value = null; - endDate.value = null; - fetchEnterprises(); - } - - @override - void onClose() { - nameController.dispose(); - super.onClose(); - } - /// 导航到编辑表单页面 Future navigateToEditForm(Enterprise enterprise) async { final result = await Get.toNamed( AppRoutes.enterpriseForm, - arguments: {'data': enterprise, 'mode': FormMode.edit}, // 将要编辑的数据作为参数传递 + arguments: {'data': enterprise, 'mode': FormMode.edit}, ); if (result == true) { - refreshList(); // 调用控制器的方法刷新 + search(); // 如果表单页返回 true,则刷新列表 } } @@ -90,26 +115,21 @@ class EnterpriseListController extends GetxController { arguments: {'mode': FormMode.add}, ); if (result == true) { - refreshList(); // 调用控制器的方法刷新 + search(); } } - /// 导航到上传页面 - Future navigateToUploadPage() async { - // final result = await Get.toNamed(AppRoutes.test); - // if (result == true) { - // refreshList(); // 调用控制器的方法刷新 - // } - } - /// 导航到企业问题列表 Future navigateToEnterpriseInfoPage(Enterprise enterprise) async { - final result = await Get.toNamed( + await Get.toNamed( AppRoutes.enterpriseInfo, arguments: {'data': enterprise, 'mode': FormMode.view}, ); - if (result == true) { - refreshList(); // 调用控制器的方法刷新 - } + } + + @override + void onClose() { + nameController.dispose(); + super.onClose(); } } diff --git a/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart new file mode 100644 index 0000000..034e6a6 --- /dev/null +++ b/lib/app/features/enterprise/presentation/controllers/enterprise_upload_controller.dart @@ -0,0 +1,116 @@ +// 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'; + +/// ----------------------------------------------------------------------------- +/// [具体实现] 企业上传选择控制器 +/// ----------------------------------------------------------------------------- +/// +/// 继承自 `BaseEnterpriseListController`,专门为“选择上传企业”页面服务。 +/// +/// **职责:** +/// - 从数据源加载**待上传**的企业列表。 +/// - 管理用户的多选状态 (`selectedEnterprises`)。 +/// - 处理卡片点击和复选框事件,更新选择集。 +/// - 提供确认上传的方法,并将结果返回给上一个页面。 +/// +class EnterpriseUploadController extends BaseEnterpriseListController { + // 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)) { + selectedEnterprises.remove(enterprise); + } else { + selectedEnterprises.add(enterprise); + } + } + + // --- 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(); + // enterpriseList.assignAll(mockData); + } catch (e) { + Get.snackbar('错误', '加载待上传列表失败: $e'); + } finally { + isLoading.value = false; + } + } + + /// 确认上传,并返回结果 + void confirmUpload() { + if (selectedEnterprises.isEmpty) { + Get.snackbar('提示', '请至少选择一个企业进行上传'); + return; + } + // 在这里执行实际的上传业务逻辑... + print('正在上传 ${selectedEnterprises.length} 个企业...'); + + // 操作成功后,返回 true 通知上一个页面刷新 + Get.back(result: true); + } +} 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 95a18bd..afc4398 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_form_page.dart @@ -1,6 +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/features/enterprise/presentation/controllers/enterprise_form_controller.dart'; import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_form_view.dart'; @@ -13,7 +14,10 @@ class EnterpriseFormPage extends GetView { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), - appBar: _buildAppBar(), + appBar: CustomAppBar( + titleName: controller.pageTitle.value, + leadingVisible: true, + ), body: SafeArea( child: Column( children: [ 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 58107ba..77d5c15 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_list_page.dart @@ -1,10 +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/extensions/datetime_extension.dart'; -import 'package:problem_check_system/app/core/models/company_enum.dart'; -import 'package:problem_check_system/app/core/widgets/app_bar_add.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_card.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 '../controllers/enterprise_list_controller.dart'; class EnterpriseListPage extends GetView { @@ -13,185 +10,12 @@ class EnterpriseListPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBarAdd( + appBar: CustomAppBar( titleName: '企业列表', + actionsVisible: true, onAddPressed: () => controller.navigateToAddForm(), ), - body: Stack( - children: [ - Column( - children: [ - _buildFilterSection(), - const Divider(height: 1, thickness: .1), - Expanded(child: _buildEnterpriseList()), - ], - ), - ], - ), - ); - } - - /// [新增] 构建可展开的筛选查询区域 - 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), - ), - ), - ), - ], - ), - ], - ), - ), - ], + body: EnterpriseListView(controller: controller), ); } - - /// [新增] 构建日期选择器的辅助方法 - Widget _buildDatePickerField(String label, Rx date) { - return InkWell( - onTap: () async { - final pickedDate = await showDatePicker( - context: Get.context!, - initialDate: date.value ?? DateTime.now(), - firstDate: DateTime(2025), - 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, - ), - child: Obx( - () => Text( - date.value == null ? '请选择日期' : date.value!.toDateString(), - style: TextStyle( - color: date.value == null ? Colors.grey[600] : Colors.black87, - ), - ), - ), - ), - ); - } - - Widget _buildEnterpriseList() { - return Obx(() { - if (controller.isLoading.value) { - return const Center(child: CircularProgressIndicator()); - } - return ListView.builder( - padding: EdgeInsets.symmetric( - horizontal: 16.w, // 使用 .w - vertical: 8.h, // 使用 .h - ), - itemCount: controller.enterpriseList.length, - itemBuilder: (context, index) { - final item = controller.enterpriseList[index]; - return Padding( - padding: EdgeInsets.only(bottom: 12.h), // 使用 .h - // child: _EnterpriseCard(enterprise: enterprise), - child: EnterpriseCard( - enterpriseListItem: item, - onEdit: () { - controller.navigateToEditForm(item.enterprise); - }, - onViewProblems: () { - controller.navigateToEnterpriseInfoPage(item.enterprise); - }, - ), - ); - }, - ); - }); - } } 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 28ce425..67b70f8 100644 --- a/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart +++ b/lib/app/features/enterprise/presentation/pages/enterprise_upload_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.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/features/enterprise/presentation/controllers/enterprise_upload_controller.dart'; +import 'package:problem_check_system/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart'; -class EnterpriseUploadPage extends StatelessWidget { +class EnterpriseUploadPage extends GetView { const EnterpriseUploadPage({super.key}); @override @@ -15,6 +18,11 @@ class EnterpriseUploadPage extends StatelessWidget { print("object"); }, ), + body: EnterpriseListView( + controller: controller, + filterSectionVisible: false, + itemMode: EnterpriseListPageMode.select, + ), ); } } 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 b35e8a8..df847ff 100644 --- a/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart +++ b/lib/app/features/enterprise/presentation/pages/widgets/enterprise_card.dart @@ -3,24 +3,33 @@ 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; const EnterpriseCard({ super.key, required this.enterpriseListItem, + required this.isSelected, required this.onEdit, required this.onViewProblems, + required this.mode, + this.onTap, + this.onSelectionChanged, }); @override Widget build(BuildContext context) { - return Material( - color: Colors.transparent, + return InkWell( + onTap: onTap, child: Container( // 【核心修改 1】移除 Container 的 padding // padding: EdgeInsets.only(...), // <--- 移除这一段 @@ -57,60 +66,59 @@ class EnterpriseCard extends StatelessWidget { ), ), - // --- 按钮层 --- - // 【核心修改 4】修改 Positioned 的定位 - 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, + 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), ), - 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), + // --- “查看问题” 按钮 --- + 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, + ), + // elevation: 0, // 移除阴影,因为它已经被剪裁了 + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: const Size(0, 0), ), - 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, + child: Text( + "查看问题", + style: TextStyle( + fontSize: 13.sp, + fontWeight: FontWeight.w500, + ), ), ), - ), - ], + ], + ), ), - ), ], ), ), @@ -237,22 +245,23 @@ class EnterpriseCard extends StatelessWidget { backgroundColor: Colors.red.shade50, ), const Spacer(), - - Theme( - data: Theme.of(context).copyWith( - checkboxTheme: CheckboxThemeData( - // 将点击区域收缩,移除额外的 padding + 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, ), ), - child: Checkbox( - value: true, - onChanged: (bool? value) {}, - // 其他属性... - ), - ), ], ); } 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 new file mode 100644 index 0000000..a218961 --- /dev/null +++ b/lib/app/features/enterprise/presentation/pages/widgets/enterprise_list_view.dart @@ -0,0 +1,280 @@ +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/home/views/home_page.dart b/lib/app/features/home/views/home_page.dart index 2ca2da1..44cf2de 100644 --- a/lib/app/features/home/views/home_page.dart +++ b/lib/app/features/home/views/home_page.dart @@ -2,6 +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/features/home/controllers/home_controller.dart'; class HomePage extends GetView { @@ -9,6 +10,9 @@ class HomePage extends GetView { @override Widget build(BuildContext context) { - return Scaffold(body: Center(child: Text("首页"))); + return Scaffold( + appBar: CustomAppBar(titleName: "首页"), + body: Center(child: Text("首页")), + ); } } diff --git a/lib/app/features/my/bindings/profile_binding.dart b/lib/app/features/my/bindings/profile_binding.dart new file mode 100644 index 0000000..8e628f3 --- /dev/null +++ b/lib/app/features/my/bindings/profile_binding.dart @@ -0,0 +1,13 @@ +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; +import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; + +class ProfileBinding implements Bindings { + @override + void dependencies() { + /// 注册我的控制器 + Get.lazyPut( + () => MyController(authRepository: Get.find()), + ); + } +} diff --git a/lib/app/features/navigation/presentation/bindings/navigation_binding.dart b/lib/app/features/navigation/presentation/bindings/navigation_binding.dart index ca73e5d..a108e6f 100644 --- a/lib/app/features/navigation/presentation/bindings/navigation_binding.dart +++ b/lib/app/features/navigation/presentation/bindings/navigation_binding.dart @@ -1,18 +1,6 @@ import 'package:get/get.dart'; -import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; -import 'package:problem_check_system/app/core/repositories/auth_repository.dart'; -import 'package:problem_check_system/app/core/services/database_service.dart'; import 'package:problem_check_system/app/core/services/network_status_service.dart'; -import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_local_data_source.dart'; -import 'package:problem_check_system/app/features/enterprise/data/datasources/enterprise_remote_data_source.dart'; -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/problem/data/repositories/problem_repository.dart'; -import 'package:problem_check_system/app/features/enterprise/presentation/controllers/enterprise_list_controller.dart'; import 'package:problem_check_system/app/features/navigation/presentation/controllers/navigation_controller.dart'; -import 'package:problem_check_system/app/features/my/controllers/my_controller.dart'; -import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; class NavigationBinding extends Bindings { @override @@ -23,41 +11,5 @@ class NavigationBinding extends Bindings { networkStatusService: Get.find(), ), ); - Get.put( - EnterpriseLocalDataSourceImpl( - databaseService: Get.find(), - ), - ); - Get.put(EnterpriseRemoteDataSourceImpl()); - Get.put( - EnterpriseRepositoryImpl( - localDataSource: Get.find(), - remoteDataSource: Get.find(), - ), - ); - Get.put( - GetEnterpriseListUsecase(repository: Get.find()), - ); - Get.lazyPut( - () => EnterpriseListController( - getEnterpriseListUsecase: Get.find(), - ), - ); - - Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); - - /// 注册问题控制器 - Get.lazyPut( - () => ProblemController( - problemRepository: Get.find(), - problemStateManager: Get.find(), - ), - fenix: true, - ); - - /// 注册我的控制器 - Get.lazyPut( - () => MyController(authRepository: Get.find()), - ); } } diff --git a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart index d036bb1..ebd046e 100644 --- a/lib/app/features/navigation/presentation/controllers/navigation_controller.dart +++ b/lib/app/features/navigation/presentation/controllers/navigation_controller.dart @@ -1,16 +1,23 @@ +import 'package:curved_navigation_bar/curved_navigation_bar.dart'; import 'package:flutter/material.dart'; 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; @@ -21,15 +28,15 @@ class NavigationController extends GetxController { /// get 选中的 RxBool get isOnline => networkStatusService.isOnline; - // 页面列表 - final List pages = [ - const HomePage(), - const EnterpriseListPage(), - const ProblemPage(), - const MyPage(), - ]; + // // 页面列表 + // final List pages = [ + // const HomePage(), + // const EnterpriseListPage(), + // const ProblemPage(), + // const MyPage(), + // ]; - get currentPage => pages[selectedIndex.value]; + // get currentPage => pages[selectedIndex.value]; @override void onInit() { @@ -38,7 +45,24 @@ 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 3227b94..c6c65d9 100644 --- a/lib/app/features/navigation/presentation/pages/navigation_page.dart +++ b/lib/app/features/navigation/presentation/pages/navigation_page.dart @@ -1,22 +1,39 @@ -// lib/app/features/navigation/presentation/pages/navigation_page.dart - 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}); @override Widget build(BuildContext context) { - // 1. 将整个页面包裹在一个 Stack 中,以创建一个全局的坐标系 return Stack( children: [ - // 2. Scaffold 作为 Stack 的底层,负责主要的页面结构 + // 1. Scaffold 作为 Stack 的底层,负责主要的页面结构 Scaffold( - // 3. 移除 Scaffold 的 floatingActionButton 属性 + // 使用 controller 中的 GlobalKey 关联 CurvedNavigationBar, + // 这在某些需要从外部代码控制导航栏状态的场景下很有用。 bottomNavigationBar: CurvedNavigationBar( + key: controller.bottomNavigationKey, backgroundColor: Colors.transparent, color: Colors.blueAccent, items: const [ @@ -29,10 +46,23 @@ class NavigationPage extends GetView { controller.changePageIndex(index); }, ), - body: Obx(() => controller.currentPage), + // [核心优化] 使用 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), + ], + ), + ), ), - // 4. 将悬浮按钮作为 Stack 的顶层,使用 Positioned 进行精确定位 + // 2. 将悬浮按钮作为 Stack 的顶层,使用 Positioned 进行精确定位 Obx(() { final isOnline = controller.isOnline.value; return Positioned( @@ -65,4 +95,40 @@ 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/bindings/problem_binding.dart b/lib/app/features/problem/presentation/bindings/problem_binding.dart new file mode 100644 index 0000000..55d1f8d --- /dev/null +++ b/lib/app/features/problem/presentation/bindings/problem_binding.dart @@ -0,0 +1,20 @@ +import 'package:get/get.dart'; +import 'package:problem_check_system/app/core/models/problem_sync_status.dart'; +import 'package:problem_check_system/app/features/problem/data/repositories/problem_repository.dart'; +import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_controller.dart'; + +class ProblemBinding extends Bindings { + @override + void dependencies() { + Get.put(ProblemStateManager(uuid: Get.find(), authRepository: Get.find())); + + /// 注册问题控制器 + Get.lazyPut( + () => ProblemController( + problemRepository: Get.find(), + problemStateManager: Get.find(), + ), + fenix: true, + ); + } +} 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 fbc8ba5..455ad3d 100644 --- a/lib/app/features/problem/presentation/views/problem_form_page.dart +++ b/lib/app/features/problem/presentation/views/problem_form_page.dart @@ -3,6 +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/features/problem/presentation/controllers/problem_form_controller.dart'; class ProblemFormPage extends GetView { @@ -12,34 +13,11 @@ class ProblemFormPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - flexibleSpace: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], - begin: Alignment.centerLeft, - end: Alignment.centerRight, - ), - ), - ), - leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios_new_rounded, - color: Colors.white, - ), - onPressed: () { - Navigator.pop(context); - }, - ), - // 根据只读模式和问题对象动态设置标题 - title: Text( - controller.isReadOnly - ? '问题详情' - : (controller.problem == null ? '新增问题' : '编辑问题'), - style: const TextStyle(color: Colors.white), - ), - centerTitle: true, - backgroundColor: Colors.transparent, + appBar: CustomAppBar( + titleName: controller.isReadOnly + ? '问题详情' + : (controller.problem == null ? '新增问题' : '编辑问题'), + leadingVisible: true, ), body: Column( children: [ diff --git a/lib/app/features/problem/presentation/views/problem_page.dart b/lib/app/features/problem/presentation/views/problem_page.dart index c2a43f5..c664bdf 100644 --- a/lib/app/features/problem/presentation/views/problem_page.dart +++ b/lib/app/features/problem/presentation/views/problem_page.dart @@ -1,7 +1,6 @@ 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/app_bar_add.dart'; +import 'package:problem_check_system/app/core/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'; @@ -13,8 +12,9 @@ class ProblemPage extends GetView { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBarAdd( + appBar: CustomAppBar( titleName: '问题列表', + actionsVisible: true, onAddPressed: () => controller.toProblemFormPageAndRefresh(), ), body: Column(