21 changed files with 701 additions and 607 deletions
@ -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>(() => EnterpriseUploadController()); |
||||
Get.lazyPut<BaseEnterpriseListController>( |
||||
() => Get.find<EnterpriseUploadController>(), |
||||
); |
||||
} |
||||
} |
||||
|
||||
@ -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<EnterpriseListItem> get enterpriseList; |
||||
|
||||
/// 是否正在加载数据。 |
||||
/// View 用它来显示或隐藏加载指示器 (如 `CircularProgressIndicator`)。 |
||||
RxBool get isLoading; |
||||
|
||||
/// [筛选功能] 企业名称的文本编辑器控制器。 |
||||
TextEditingController get nameController; |
||||
|
||||
/// [筛选功能] 已选择的企业类型。 |
||||
Rx<CompanyType?> get selectedType; |
||||
|
||||
/// [筛选功能] 已选择的开始日期。 |
||||
Rx<DateTime?> get startDate; |
||||
|
||||
/// [筛选功能] 已选择的结束日期。 |
||||
Rx<DateTime?> get endDate; |
||||
|
||||
/// [选择功能] 被选中的企业集合。 |
||||
/// 使用 `Set` 可以高效地进行增、删、查操作,并保证元素的唯一性。 |
||||
RxSet<Enterprise> get selectedEnterprises; |
||||
|
||||
// --- 操作方法 (View 需要调用的行为) --- |
||||
|
||||
/// 执行搜索/筛选操作。 |
||||
/// 由 View 中的“查询”按钮调用。 |
||||
void search(); |
||||
|
||||
/// 清空所有筛选条件并重新加载数据。 |
||||
/// 由 View 中的“重置”按钮调用。 |
||||
void clearFilters(); |
||||
|
||||
/// 处理整个卡片的点击事件。 |
||||
/// 在不同模式下(管理/选择),此方法的具体行为由子类实现。 |
||||
void onItemTap(EnterpriseListItem item); |
||||
|
||||
/// 处理卡片上复选框状态的变化。 |
||||
/// 当用户在选择模式下勾选或取消勾选时被调用。 |
||||
void onSelectionChanged(Enterprise enterprise); |
||||
} |
||||
@ -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>`,它依赖于我们定义的 |
||||
/// 抽象基类 (`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<CompanyType>( |
||||
initialValue: controller.selectedType.value, |
||||
decoration: InputDecoration( |
||||
labelText: '企业类型', |
||||
prefixIcon: const Icon(Icons.category), |
||||
border: OutlineInputBorder( |
||||
borderRadius: BorderRadius.circular(8.r), |
||||
), |
||||
isDense: true, |
||||
), |
||||
hint: const Text('请选择企业类型'), |
||||
isExpanded: true, |
||||
items: CompanyType.values.map((type) { |
||||
return DropdownMenuItem( |
||||
value: type, |
||||
child: Text(type.displayText), |
||||
); |
||||
}).toList(), |
||||
onChanged: (value) { |
||||
controller.selectedType.value = value; |
||||
}, |
||||
), |
||||
), |
||||
SizedBox(height: 12.h), |
||||
|
||||
// 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<DateTime?> date) { |
||||
return InkWell( |
||||
onTap: () async { |
||||
final pickedDate = await showDatePicker( |
||||
context: Get.context!, |
||||
initialDate: date.value ?? DateTime.now(), |
||||
firstDate: DateTime(2020), // 调整一个合理的开始年份 |
||||
lastDate: DateTime(2125), |
||||
); |
||||
if (pickedDate != null) { |
||||
date.value = pickedDate; |
||||
} |
||||
}, |
||||
child: InputDecorator( |
||||
decoration: InputDecoration( |
||||
labelText: label, |
||||
prefixIcon: const Icon(Icons.calendar_today), |
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)), |
||||
isDense: true, |
||||
contentPadding: EdgeInsets.symmetric( |
||||
horizontal: 12.w, |
||||
vertical: 14.h, |
||||
), // 调整内边距 |
||||
), |
||||
child: Obx( |
||||
() => Text( |
||||
date.value == null ? '请选择日期' : date.value!.toDateString(), |
||||
style: TextStyle( |
||||
fontSize: 14.sp, |
||||
color: date.value == null ? Colors.grey[700] : Colors.black87, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
/// 构建企业列表 |
||||
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); |
||||
} |
||||
}, |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
}); |
||||
} |
||||
} |
||||
@ -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, |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue