From 4ec2e41e512d20de3173fdedfebeafb74994ba59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=8C=AF=E5=8D=87?= <359059686@qq.com> Date: Mon, 3 Nov 2025 09:31:51 +0800 Subject: [PATCH] =?UTF-8?q?feat=20:=20=E6=96=B0=E5=A2=9E=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E4=BC=81=E4=B8=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/app.dart | 85 ++++++++ lib/app/core/models/use_case/use_case.dart | 40 ++++ .../enterprise_repository_impl.dart | 7 + .../repositories/enterprise_repository.dart | 2 + .../usecases/get_enterprises_usecase.dart | 12 ++ .../bindings/problem_form_binding.dart | 8 +- .../controllers/problem_form_controller.dart | 40 +++- .../presentation/pages/problem_form_page.dart | 201 ++++++++++-------- lib/main.dart | 84 +------- 9 files changed, 291 insertions(+), 188 deletions(-) create mode 100644 lib/app/app.dart create mode 100644 lib/app/core/models/use_case/use_case.dart create mode 100644 lib/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100644 index 0000000..cc6dc09 --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get_navigation/src/root/get_material_app.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/core/bindings/initial_binding.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + // 填入设计稿中设备的屏幕尺寸, 单位dp + return ScreenUtilInit( + designSize: const Size(375, 812), + minTextAdapt: true, + splitScreenMode: true, + builder: (context, _) { + // 使用 _ 忽略 child 参数 + return GetMaterialApp( + debugShowCheckedModeBanner: false, + // --- 关键配置 --- + localizationsDelegates: const [ + // 必需:用于 Material 组件库的本地化 + GlobalMaterialLocalizations.delegate, + // 必需:用于 Widgets 库的本地化 + GlobalWidgetsLocalizations.delegate, + // 必需:用于 iOS 风格组件的本地化 + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('zh', 'CN'), // 支持中文 + ], + title: '问题检查系统', // 使用更有意义的应用标题 + theme: ThemeData( + useMaterial3: true, + + // 【推荐】使用 colorSchemeSeed 从一个种子颜色生成完整的 M3 调色板 + colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue), + primaryColor: Colors.lightBlue, + // (可选) 如果想进一步定制 NavigationBar 的主题 + navigationBarTheme: NavigationBarThemeData( + // 1. 在主题中设置你想要的高度 + height: 48, // 比如我们想让它更高,设置为 70 + // 2. 【关键】增大图标尺寸,让内容撑满新高度 + // 使用 MaterialStateProperty.resolveWith 来响应不同状态(如下拉、聚焦等) + iconTheme: WidgetStateProperty.resolveWith((states) { + // 你可以根据状态返回不同的 IconThemeData + return IconThemeData( + size: 24, // 将图标大小从默认的 24 增大到 28 + ); + }), + // 标签行为:控制标签是否总是显示 + labelBehavior: + NavigationDestinationLabelBehavior.onlyShowSelected, + // backgroundColor: Colors.white, + // 指示器的颜色 + indicatorColor: Colors.deepOrange.shade200, + // 【核心】使用 indicatorShape 属性并传入我们的自定义类 + // indicatorShape: const CustomIndicatorShape( + // // 在这里调整你想要的宽度 + // // horizontalPadding: 24.0, // 让指示器变得更窄 + // // horizontalPadding: 8.0, // 让指示器变得更宽 + // horizontalPadding: 0, // 让指示器宽度接近内容宽度 + // ), + // 标签文本样式 + labelTextStyle: WidgetStateProperty.all( + TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: Colors.deepOrange, + ), + ), + ), + ), + // 仅使用 GetX 路由系统 + initialRoute: AppRoutes.login, // 使用路由常量作为初始页面 + initialBinding: InitialBinding(), // 如果有全局绑定,继续保留 + getPages: AppPages.routes, // 绑定所有路由 + ); + }, + ); + } +} diff --git a/lib/app/core/models/use_case/use_case.dart b/lib/app/core/models/use_case/use_case.dart new file mode 100644 index 0000000..170e768 --- /dev/null +++ b/lib/app/core/models/use_case/use_case.dart @@ -0,0 +1,40 @@ +// // lib/core/usecases/usecase.dart + +// // 定义一个通用的 UseCase 抽象类 +// // Type: 代表成功时返回的数据类型 (例如: List) +// // Params: 代表执行这个 UseCase所需要的参数 (例如: 一个包含 ID 的类) +// import 'package:equatable/equatable.dart'; + +// abstract class UseCase { +// /// 所有 Use Case 子类都必须实现这个 call 方法 +// /// 它被设计为异步操作,因此返回一个 Future +// /// Either 是一种函数式编程的错误处理方式 +// /// Left 代表失败 (Failure),Right 代表成功 (Type) +// Future> call(Params params); +// } +// // lib/core/usecases/usecase.dart (可以放在同一个文件中) + +// /// 当 UseCase 不需要任何参数时,使用这个类 +// class NoParams extends Equatable { +// @override +// List get props => []; +// } + +// // 定义一个通用的失败类,所有具体的 Failure 都应该继承它 +// abstract class Failure extends Equatable { +// const Failure([List properties = const []]); + +// @override +// List get props => []; +// } + +// // 定义一些具体的失败类型 + +// /// 当调用服务器 API 失败时使用 +// class ServerFailure extends Failure {} + +// /// 当操作本地缓存失败时使用 +// class CacheFailure extends Failure {} + +// /// 当没有网络连接时使用 +// class NetworkFailure extends Failure {} diff --git a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart index 8d81e8e..f4e10a4 100644 --- a/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart +++ b/lib/app/features/enterprise/data/repositories_impl/enterprise_repository_impl.dart @@ -185,4 +185,11 @@ class EnterpriseRepositoryImpl implements EnterpriseRepository { final enterpriseModel = EnterpriseModel.fromEntity(chosenEnterprise); return localDataSource.upsertEnterprise(enterpriseModel); } + + @override + Future> getAllEnterprises() async { + final models = await localDataSource.getAllEnterprises(); + final enterprises = models.map((model) => model.toEntity()).toList(); + return enterprises; + } } diff --git a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart index 3305146..3a8dd3a 100644 --- a/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart +++ b/lib/app/features/enterprise/domain/repositories/enterprise_repository.dart @@ -29,4 +29,6 @@ abstract class EnterpriseRepository implements SyncableRepository { Future syncWithServer(); // [新增] 用户选择后,用一个版本覆盖另一个版本 Future resolveConflictAndUpdate(Enterprise chosenEnterprise); + // 获取所有企业信息 + Future> getAllEnterprises(); } diff --git a/lib/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart b/lib/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart new file mode 100644 index 0000000..ead8f12 --- /dev/null +++ b/lib/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart @@ -0,0 +1,12 @@ +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/repositories/enterprise_repository.dart'; + +class GetEnterprisesUsecase { + final EnterpriseRepository repository; + + GetEnterprisesUsecase({required this.repository}); + + Future> call() { + return repository.getAllEnterprises(); + } +} diff --git a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart index 525f1d4..3223f22 100644 --- a/lib/app/features/problem/presentation/bindings/problem_form_binding.dart +++ b/lib/app/features/problem/presentation/bindings/problem_form_binding.dart @@ -6,7 +6,7 @@ import 'package:problem_check_system/app/features/enterprise/data/datasources/en 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/domain/usecases/get_enterprises_usecase.dart'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_form_controller.dart'; @@ -51,15 +51,15 @@ class ProblemFormBinding extends Bindings { ), ); - Get.put( - GetEnterpriseListUsecase(repository: Get.find()), + Get.put( + GetEnterprisesUsecase(repository: Get.find()), ); Get.lazyPut( () => ProblemFormController( problem: problem, formMode: formMode, - getEnterpriseListUsecase: Get.find(), + getEnterprisesUsecase: Get.find(), ), ); } diff --git a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart index 63a36b1..d88fe4f 100644 --- a/lib/app/features/problem/presentation/controllers/problem_form_controller.dart +++ b/lib/app/features/problem/presentation/controllers/problem_form_controller.dart @@ -9,14 +9,19 @@ import 'package:path_provider/path_provider.dart'; import 'package:problem_check_system/app/core/models/form_mode.dart'; import 'package:problem_check_system/app/core/models/image_status.dart'; import 'package:problem_check_system/app/core/models/image_metadata_model.dart'; -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/domain/entities/enterprise.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/usecases/get_enterprises_usecase.dart'; import 'dart:io'; import 'package:problem_check_system/app/features/problem/domain/entities/problem_entity.dart'; import 'package:uuid/uuid.dart'; class ProblemFormController extends GetxController { - final RxBool isLoading = false.obs; + // 2. 状态变量: 使用 .obs 使其变为响应式 + final isLoading = true.obs; // 用于控制加载指示器 + final error = ''.obs; // 用于显示错误信息 + final enterpriseList = [].obs; // 存储从数据库获取的企业列表 + final selectedEnterprise = Rxn(); + final ProblemEntity? problem; late Map bindInfo; @@ -35,13 +40,13 @@ class ProblemFormController extends GetxController { final TextEditingController descriptionController = TextEditingController(); final TextEditingController locationController = TextEditingController(); final RxList selectedImages = [].obs; - final GetEnterpriseListUsecase getEnterpriseListUsecase; + final GetEnterprisesUsecase getEnterprisesUsecase; // 使用依赖注入,便于测试 ProblemFormController({ this.problem, this.formMode = FormMode.view, - required this.getEnterpriseListUsecase, + required this.getEnterprisesUsecase, }) { if (problem != null) { if (problem!.bindData != null) { @@ -62,6 +67,7 @@ class ProblemFormController extends GetxController { void onInit() { super.onInit(); updatePageTitle(); + _fetchEnterprises(); } @override @@ -85,9 +91,27 @@ class ProblemFormController extends GetxController { } } - // 获取企业列表数据 - Future> getAllEnterprises() async { - return await getEnterpriseListUsecase(); + // 4. 私有方法: 获取数据的核心逻辑 + Future _fetchEnterprises() async { + try { + isLoading(true); + error(''); // 重置错误信息 + final result = await getEnterprisesUsecase(); // 调用UseCase + enterpriseList.assignAll(result); // 更新列表 + } catch (e) { + error.value = '加载企业列表失败: $e'; // 捕获并设置错误信息 + } finally { + isLoading(false); // 无论成功或失败,最后都停止加载 + } + } + + // 5. 公有方法: 供UI调用以更新状态 + void selectEnterprise(Enterprise? enterprise) { + selectedEnterprise.value = enterprise; + // 你可以在这里添加额外的逻辑,比如当企业被选中时执行某些操作 + if (enterprise != null) { + print('Selected: ${enterprise.name}'); + } } // 改进的 pickImage 方法 diff --git a/lib/app/features/problem/presentation/pages/problem_form_page.dart b/lib/app/features/problem/presentation/pages/problem_form_page.dart index b55e9f2..f97a97b 100644 --- a/lib/app/features/problem/presentation/pages/problem_form_page.dart +++ b/lib/app/features/problem/presentation/pages/problem_form_page.dart @@ -4,6 +4,7 @@ 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/pages/widgets/custom_app_bar.dart'; +import 'package:problem_check_system/app/features/enterprise/domain/entities/enterprise.dart'; import 'package:problem_check_system/app/features/problem/presentation/controllers/problem_form_controller.dart'; class ProblemFormPage extends GetView { @@ -18,73 +19,94 @@ class ProblemFormPage extends GetView { leadingVisible: true, ), body: SafeArea( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (controller.isShowBindData) - Container( - color: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - '项目名称', - controller.bindInfo['projectName'], - ), - _buildDivider(), - _buildSection( - '企业名称', - controller.bindInfo['companyName'], - ), - _buildDivider(), - _buildSection( - '要素名称', - controller.bindInfo['censorElementName'], - ), - _buildDivider(), - _buildSection( - '评估内容', - controller.bindInfo['pgContent'], - ), - _buildDivider(), - _buildSection( - '建议项', - controller.bindInfo['suggestion'], - ), - _buildDivider(), - _buildSection('问题类型', ''), - _buildDivider(), - _buildSection('工艺设备名称', ''), - ], + child: Obx(() { + // 1. 加载中状态 + if (controller.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + // 2. 错误状态 + if (controller.error.isNotEmpty) { + return Center( + child: Text( + controller.error.value, + style: const TextStyle(color: Colors.red), + ), + ); + } + + // 3. 列表为空状态 + if (controller.enterpriseList.isEmpty) { + return const Center(child: Text('没有可用的企业')); + } + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + if (controller.isShowBindData) + Container( + color: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSection( + '项目名称', + controller.bindInfo['projectName'], + ), + _buildDivider(), + _buildSection( + '企业名称', + controller.bindInfo['companyName'], + ), + _buildDivider(), + _buildSection( + '要素名称', + controller.bindInfo['censorElementName'], + ), + _buildDivider(), + _buildSection( + '评估内容', + controller.bindInfo['pgContent'], + ), + _buildDivider(), + _buildSection( + '建议项', + controller.bindInfo['suggestion'], + ), + _buildDivider(), + _buildSection('问题类型', ''), + _buildDivider(), + _buildSection('工艺设备名称', ''), + ], + ), ), + _buildDropDownCard(title: '企业名称'), + _buildInputCard( + title: '问题描述', + textController: controller.descriptionController, + hintText: '请输入问题描述', ), - _buildDropDownCard(title: '企业名称'), - _buildInputCard( - title: '问题描述', - textController: controller.descriptionController, - hintText: '请输入问题描述', - ), - _buildInputCard( - title: '所在位置', - textController: controller.locationController, - hintText: '请输入问题所在位置', - ), - _buildImageCard(context), - ], + _buildInputCard( + title: '所在位置', + textController: controller.locationController, + hintText: '请输入问题所在位置', + ), + _buildImageCard(context), + ], + ), ), ), - ), - // 只有在非只读模式下才显示底部操作按钮 - if (!controller.isReadOnly) _bottomButton(), - ], - ), + // 只有在非只读模式下才显示底部操作按钮 + if (!controller.isReadOnly) _bottomButton(), + ], + ); + }), ), ); } @@ -436,7 +458,29 @@ class ProblemFormPage extends GetView { const SizedBox(height: 20.0), // --- 2. 底部区域:下拉选择器显示 --- - _buildDropdownSelector(), + Padding( + padding: EdgeInsetsGeometry.only(left: 16.w, right: 16.w), + child: DropdownButtonHideUnderline( + child: DropdownButton( + // 提示文本 + hint: const Text('请选择一个企业'), + isExpanded: true, + // 当前选中的值,直接从控制器获取 + value: controller.selectedEnterprise.value, + // 列表项,通过map从控制器列表动态生成 + items: controller.enterpriseList.map((enterprise) { + return DropdownMenuItem( + value: enterprise, // 每个选项的值是EnterpriseEntity对象本身 + child: Text(enterprise.name), // 显示的文本是企业名称 + ); + }).toList(), + // 当用户选择一个新项目时,调用控制器的方法来更新状态 + onChanged: (newValue) { + controller.selectEnterprise(newValue); + }, + ), + ), + ), ], ), ); @@ -487,33 +531,4 @@ class ProblemFormPage extends GetView { ], ); } - - /// 构建底部类似下拉菜单的显示区域 - Widget _buildDropdownSelector() { - // 使用 InkWell 包裹,使整个区域都可以响应点击事件 - return InkWell( - onTap: () { - // 在这里处理点击事件,例如弹出一个选择企业的对话框或页面 - print('下拉选择区域被点击'); - }, - // 自定义圆角,使其与外部容器的点击效果一致 - borderRadius: BorderRadius.circular(8.0), - child: Padding( - padding: EdgeInsets.only(left: 16.w, top: 0, right: 16.w, bottom: 16.h), - child: Row( - // 同样使用 spaceBetween 将文本和图标推向两端 - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // 左侧的企业名称文本 - const Text( - '山东汇丰石化集团有限公司', - style: TextStyle(fontSize: 16, color: Colors.black54), - ), - // 右侧的下拉箭头图标 - Icon(Icons.arrow_drop_down, color: Colors.grey[700]), - ], - ), - ), - ); - } } diff --git a/lib/main.dart b/lib/main.dart index 107003e..f122b97 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get_navigation/src/root/get_material_app.dart'; import 'package:get_storage/get_storage.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/core/bindings/initial_binding.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:problem_check_system/app/app.dart'; void main() async { // 确保 Flutter Binding 已初始化 @@ -23,81 +19,3 @@ void main() async { runApp(const MainApp()); } - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - // 填入设计稿中设备的屏幕尺寸, 单位dp - return ScreenUtilInit( - designSize: const Size(375, 812), - minTextAdapt: true, - splitScreenMode: true, - builder: (context, _) { - // 使用 _ 忽略 child 参数 - return GetMaterialApp( - debugShowCheckedModeBanner: false, - // --- 关键配置 --- - localizationsDelegates: const [ - // 必需:用于 Material 组件库的本地化 - GlobalMaterialLocalizations.delegate, - // 必需:用于 Widgets 库的本地化 - GlobalWidgetsLocalizations.delegate, - // 必需:用于 iOS 风格组件的本地化 - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('zh', 'CN'), // 支持中文 - ], - title: '问题检查系统', // 使用更有意义的应用标题 - theme: ThemeData( - useMaterial3: true, - - // 【推荐】使用 colorSchemeSeed 从一个种子颜色生成完整的 M3 调色板 - colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightBlue), - primaryColor: Colors.lightBlue, - // (可选) 如果想进一步定制 NavigationBar 的主题 - navigationBarTheme: NavigationBarThemeData( - // 1. 在主题中设置你想要的高度 - height: 48, // 比如我们想让它更高,设置为 70 - // 2. 【关键】增大图标尺寸,让内容撑满新高度 - // 使用 MaterialStateProperty.resolveWith 来响应不同状态(如下拉、聚焦等) - iconTheme: WidgetStateProperty.resolveWith((states) { - // 你可以根据状态返回不同的 IconThemeData - return IconThemeData( - size: 24, // 将图标大小从默认的 24 增大到 28 - ); - }), - // 标签行为:控制标签是否总是显示 - labelBehavior: - NavigationDestinationLabelBehavior.onlyShowSelected, - // backgroundColor: Colors.white, - // 指示器的颜色 - indicatorColor: Colors.deepOrange.shade200, - // 【核心】使用 indicatorShape 属性并传入我们的自定义类 - // indicatorShape: const CustomIndicatorShape( - // // 在这里调整你想要的宽度 - // // horizontalPadding: 24.0, // 让指示器变得更窄 - // // horizontalPadding: 8.0, // 让指示器变得更宽 - // horizontalPadding: 0, // 让指示器宽度接近内容宽度 - // ), - // 标签文本样式 - labelTextStyle: WidgetStateProperty.all( - TextStyle( - fontSize: 12.sp, - fontWeight: FontWeight.w500, - color: Colors.deepOrange, - ), - ), - ), - ), - // 仅使用 GetX 路由系统 - initialRoute: AppRoutes.login, // 使用路由常量作为初始页面 - initialBinding: InitialBinding(), // 如果有全局绑定,继续保留 - getPages: AppPages.routes, // 绑定所有路由 - ); - }, - ); - } -}