diff --git a/android/.kotlin/errors/errors-1779355041254.log b/android/.kotlin/errors/errors-1779355041254.log new file mode 100644 index 00000000..11a74030 --- /dev/null +++ b/android/.kotlin/errors/errors-1779355041254.log @@ -0,0 +1,45 @@ +kotlin version: 2.2.20 +error message: Failed connecting to the daemon in 4 retries + +error message: Daemon compilation failed: Could not connect to Kotlin compile daemon +java.lang.RuntimeException: Could not connect to Kotlin compile daemon + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemon(GradleKotlinCompilerWork.kt:214) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl(GradleKotlinCompilerWork.kt:159) + at org.jetbrains.kotlin.compilerRunner.GradleKotlinCompilerWork.run(GradleKotlinCompilerWork.kt:111) + at org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction.execute(GradleCompilerRunnerWithWorkers.kt:74) + at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:66) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1$1.create(NoIsolationWorkerFactory.java:62) + at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.lambda$execute$0(NoIsolationWorkerFactory.java:62) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44) + at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:210) + at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:205) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:67) + at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:167) + at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:60) + at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:54) + at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41) + at org.gradle.workers.internal.NoIsolationWorkerFactory$1.execute(NoIsolationWorkerFactory.java:59) + at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$0(DefaultWorkerExecutor.java:174) + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:194) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:127) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:169) + at org.gradle.internal.Factories$1.create(Factories.java:31) + at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127) + at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:132) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:164) + at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:133) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Unknown Source) + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) + at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64) + at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) + at java.base/java.lang.Thread.run(Unknown Source) + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e61ad546..f45e8a7d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ + + + + diff --git a/assets/images/chucao.png b/assets/images/chucao.png new file mode 100644 index 00000000..6732e851 Binary files /dev/null and b/assets/images/chucao.png differ diff --git a/assets/images/dianzhan.png b/assets/images/dianzhan.png new file mode 100644 index 00000000..c626c01b Binary files /dev/null and b/assets/images/dianzhan.png differ diff --git a/assets/images/qinxi.png b/assets/images/qinxi.png new file mode 100644 index 00000000..c53baddb Binary files /dev/null and b/assets/images/qinxi.png differ diff --git a/assets/images/xunjian.png b/assets/images/xunjian.png new file mode 100644 index 00000000..00b78839 Binary files /dev/null and b/assets/images/xunjian.png differ diff --git a/assets/images/yiwancheng.png b/assets/images/yiwancheng.png new file mode 100644 index 00000000..8222c395 Binary files /dev/null and b/assets/images/yiwancheng.png differ diff --git a/devtools_options.yaml b/devtools_options.yaml index e9174f49..caaff94e 100644 --- a/devtools_options.yaml +++ b/devtools_options.yaml @@ -2,4 +2,5 @@ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: - get_it: true - - provider: true \ No newline at end of file + - provider: true + - shared_preferences: true \ No newline at end of file diff --git a/docs/TAB_CONFIG_DEVELOPMENT_GUIDE.md b/docs/TAB_CONFIG_DEVELOPMENT_GUIDE.md new file mode 100644 index 00000000..72dde6f7 --- /dev/null +++ b/docs/TAB_CONFIG_DEVELOPMENT_GUIDE.md @@ -0,0 +1,197 @@ +# Tab 配置系统开发规范 + +## 📋 功能说明 + +Tab 配置系统支持动态开关控制底部导航栏的 Tab 项显示/隐藏,配置自动保存到 SharedPreferences。 + +## 🔧 添加新 Tab 项的步骤 + +当需要添加新的 Tab 项时,必须按以下顺序修改 **3 个文件**: + +### 1️⃣ 修改 `tab_config.dart` - 添加默认配置 + +**文件路径:** `lib/features/main_container/domain/tab_config.dart` + +在 `TabConfig.defaultConfig()` 方法的 `items` 列表中添加新的 `TabConfigItem`: + +```dart +TabConfigItem( + id: 'your_tab_id', // 唯一标识符(小写+下划线) + name: '显示名称', // 中文名称(简洁,建议2-4个字) + nameEn: 'Display Name', // 英文名称 + icon: 'icon_name_rounded', // 图标名称(Material Icons) + isEnabled: true, // 默认是否启用(true/false) + order: 7, // 排序序号(从1开始递增) +), +``` + +**注意事项:** +- `id` 必须唯一,建议使用有意义的英文标识 +- `name` 要简洁,符合 TAB 命名规范 +- `order` 决定 Tab 在导航栏中的显示顺序 +- `isEnabled` 控制默认是否显示该 Tab + +--- + +### 2️⃣ 修改 `custom_main_container.dart` - 添加页面映射 + +**文件路径:** `lib/features/main_container/presentation/pages/custom_main_container.dart` + +#### 2.1 导入新页面 + +在文件顶部添加新页面的 import: + +```dart +import 'package:maibu_satabot_v2/features/xxx/presentation/pages/your_page.dart'; +``` + +#### 2.2 在 `_buildPages` 方法中添加 case + +```dart +List _buildPages(List enabledTabs) { + return enabledTabs.map((tab) { + switch (tab.id) { + case 'home_v2': + return const HomeV2Page(); + case 'home': + return const HomePage(); + case 'device': + return const DeviceStatusPage(); + case 'ai': + return const AiPage(); + case 'warning': + return const WarningCenterPage(); + case 'my': + return const MyPage(); + case 'your_tab_id': // 👈 新增这个 case + return const YourPage(); + default: + return const HomePage(); + } + }).toList(); +} +``` + +--- + +### 3️⃣ 修改 `tab_settings_page.dart` - 添加图标映射 + +**文件路径:** `lib/features/main_container/presentation/pages/tab_settings_page.dart` + +在 `_getIconData` 方法中添加新图标的映射: + +```dart +IconData _getIconData(String iconName) { + switch (iconName) { + case 'home_rounded': + return Icons.home_rounded; + case 'grid_view_rounded': + return Icons.grid_view_rounded; + case 'devices_rounded': + return Icons.devices_rounded; + case 'auto_awesome_rounded': + return Icons.auto_awesome_rounded; + case 'warning_amber_rounded': + return Icons.warning_amber_rounded; + case 'person_rounded': + return Icons.person_rounded; + case 'your_icon_name': // 👈 新增这个 case + return Icons.your_icon_name; + default: + return Icons.home_rounded; + } +} +``` + +**注意:** 如果使用的是 Material Icons 标准图标,通常不需要修改此文件,除非使用了特殊图标。 + +--- + +## ✅ 检查清单 + +添加新 Tab 后,请确认: + +- [ ] 已在 `tab_config.dart` 中添加 `TabConfigItem` 配置 +- [ ] 已在 `custom_main_container.dart` 中导入新页面 +- [ ] 已在 `custom_main_container.dart` 的 `switch` 中添加 case 分支 +- [ ] 已在 `tab_settings_page.dart` 中添加图标映射(如需要) +- [ ] `id`、`order` 没有与其他 Tab 冲突 +- [ ] 测试开关功能是否正常 +- [ ] 测试切换 Tab 是否正常显示对应页面 + +--- + +## 🚀 完整示例:添加"消息"Tab + +假设要添加一个"消息"Tab,显示消息中心页面: + +### 步骤 1:修改 `tab_config.dart` + +```dart +TabConfigItem( + id: 'message', + name: '消息', + nameEn: 'Messages', + icon: 'notifications_rounded', + isEnabled: true, + order: 7, +), +``` + +### 步骤 2:修改 `custom_main_container.dart` + +```dart +// 顶部添加导入 +import 'package:maibu_satabot_v2/features/message/presentation/pages/message_page.dart'; + +// 在 _buildPages 方法中添加 +case 'message': + return const MessagePage(); +``` + +### 步骤 3:修改 `tab_settings_page.dart` + +```dart +case 'notifications_rounded': + return Icons.notifications_rounded; +``` + +--- + +## ⚠️ 常见问题 + +### Q1: 添加新 Tab 后看不到? +**A:** 点击 Tab 配置页面的"重置为默认"按钮,或清除应用数据重新运行。 + +### Q2: 点击 Tab 后页面不显示? +**A:** 检查 `custom_main_container.dart` 中的 `switch-case` 是否正确添加了新 Tab 的映射。 + +### Q3: 图标显示不正确? +**A:** 检查 `tab_settings_page.dart` 中的 `_getIconData` 方法是否添加了对应的图标映射。 + +### Q4: Tab 顺序不对? +**A:** 检查 `tab_config.dart` 中各 Tab 的 `order` 值,确保按期望顺序排列。 + +--- + +## 📝 核心原理 + +1. **配置管理:** `TabConfigCubit` 管理所有 Tab 的配置和选中状态 +2. **持久化:** 配置自动保存到 SharedPreferences,重启应用后保持 +3. **动态渲染:** 底部导航栏根据 `enabledItems` 动态渲染启用的 Tab +4. **页面切换:** 使用 `IndexedStack` 保持各页面状态,通过 `selectedIndex` 切换 + +--- + +## 🎯 最佳实践 + +1. **Tab 命名:** 保持简洁,中文 2-4 个字,避免过长 +2. **默认状态:** 新功能建议设置 `isEnabled: false`,通过灰度发布逐步开放 +3. **排序规划:** 预留 order 间隔(如 10, 20, 30),方便后续插入新 Tab +4. **图标选择:** 优先使用 Material Icons 的 `_rounded` 版本,风格统一 +5. **测试验证:** 每次修改后测试开关、切换、持久化等功能 + +--- + +**最后更新:** 2026-05-20 +**维护者:** 开发团队 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index dc2c79d6..5bd423f2 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -66,5 +66,16 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + + NSCameraUsageDescription + 需要使用相机拍照和录像 + + NSPhotoLibraryUsageDescription + 需要访问相册以选择图片和视频 + NSPhotoLibraryAddUsageDescription + 需要保存拍摄的照片和视频到相册 + + NSMicrophoneUsageDescription + 录像时需要使用麦克风录制声音 diff --git a/lib/core/consts/workorder_consts.dart b/lib/core/consts/workorder_consts.dart new file mode 100644 index 00000000..c1d6b021 --- /dev/null +++ b/lib/core/consts/workorder_consts.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +/// 工单模块颜色常量 +class WorkOrderColors { + WorkOrderColors._(); + + /// 主色调 + static const Color primary = Color(0xFF165DFF); + + /// 待处理色(橙色) + static const Color pending = Color(0xFFFF7D00); + + /// 执行中色(蓝色) + static const Color executing = Color(0xFF165DFF); + + /// 已完成色(绿色) + static const Color completed = Color(0xFF00B42A); + + /// 背景色 + static const Color background = Color(0xFFFFFFFF); + + /// 页面背景色 + static const Color pageBackground = Color(0xFFF7F8FA); + + /// 主要文字色 + static const Color primaryText = Color(0xFF1D2129); + + /// 次要文字色 + static const Color secondaryText = Color(0xFF4E5969); + + /// 辅助文字色 + static const Color auxiliaryText = Color(0xFF86909C); + + /// 分割线颜色 + static const Color divider = Color(0xFFF0F0F0); + + /// 进度条背景色 + static const Color progressBackground = Color(0xFFF0F0F0); + + /// 卡片阴影 + static List get cardShadow => [ + const BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ]; +} + +/// 工单模块尺寸常量 +class WorkOrderDimensions { + WorkOrderDimensions._(); + + /// 通用圆角 + static const double borderRadius = 16.0; + + /// 水平边距 + static const double horizontalPadding = 16.0; + + /// 模块间距 + static const double moduleSpacing = 20.0; + + /// 导航栏高度 + static const double navigationBarHeight = 44.0; + + /// 标签栏高度 + static const double tabBarHeight = 44.0; +} + +/// 全局应用常量 (供 UI 组件使用) +class AppConstants { + AppConstants._(); + + static const Color primaryColor = WorkOrderColors.primary; + static const Color backgroundColor = WorkOrderColors.background; + static const Color primaryTextColor = WorkOrderColors.primaryText; + static const Color secondaryTextColor = WorkOrderColors.secondaryText; + static const Color auxiliaryTextColor = WorkOrderColors.auxiliaryText; + static const Color pendingColor = WorkOrderColors.pending; + static const Color executingColor = WorkOrderColors.executing; + static const Color completedColor = WorkOrderColors.completed; + + static const double borderRadius = WorkOrderDimensions.borderRadius; + static const double horizontalPadding = WorkOrderDimensions.horizontalPadding; + static const double moduleSpacing = WorkOrderDimensions.moduleSpacing; + static const double navigationBarHeight = WorkOrderDimensions.navigationBarHeight; + + static List get cardShadow => WorkOrderColors.cardShadow; +} + +/// 工单状态枚举 +enum WorkOrderStatus { + pending, // 待处理 + executing, // 执行中 + completed, // 已完成 + all, // 全部 +} + +/// 工单优先级枚举 +enum WorkOrderPriority { + high, // 高 + medium, // 中 + low, // 低 +} diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index 90c533d1..0b833be9 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -61,6 +61,18 @@ import '../../features/v2/home/data/repositories/home_repository_impl.dart'; import '../../features/v2/home/domain/repositories/home_repository.dart'; import '../../features/v2/home/domain/usecases/get_home_data_usecase.dart'; import '../../features/v2/home/presentation/bloc/home_v2_bloc.dart'; +import '../../features/v2/device_list/data/datasources/device_remote_datasource.dart' as device_v2; +import '../../features/v2/device_list/data/repositories/device_repository_impl.dart' as device_v2_repo; +import '../../features/v2/device_list/domain/repositories/device_repository.dart' as device_v2_domain; +import '../../features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart' as device_v2_usecase; +import '../../features/v2/device_list/presentation/bloc/device_status_bloc.dart' as device_v2_bloc; +import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart'; +import '../../features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart'; +import '../../features/v2/waring_center/data/repositories/alarm_repository_impl.dart'; +import '../../features/v2/waring_center/domain/repositories/alarm_repository.dart'; +import '../../features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart'; +import '../../features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart'; +import '../../features/v2/waring_center/presentation/bloc/alarm_cubit.dart'; import '../app/app_user_cubit.dart'; import '../localization/locale_cubit.dart'; import '../../features/home/presentation/bloc/permission_request_bloc.dart'; @@ -194,6 +206,22 @@ Future init() async { sl.registerLazySingleton(() => GetHomeDataUseCase(sl())); sl.registerFactory(() => HomeV2Bloc(sl())); + /// Device Status V2 + sl.registerLazySingleton(() => device_v2.DeviceRemoteDataSourceImpl()); + sl.registerLazySingleton(() => device_v2_repo.DeviceRepositoryImpl(remoteDataSource: sl())); + sl.registerLazySingleton(() => device_v2_usecase.GetDeviceStatusDataUseCase(repository: sl())); + sl.registerFactory(() => device_v2_bloc.DeviceStatusBloc(sl())); + + /// Alarm Center V2 + sl.registerLazySingleton(() => AlarmRemoteDataSourceImpl()); + sl.registerLazySingleton(() => AlarmRepositoryImpl(sl())); + sl.registerLazySingleton(() => GetAlarmListUseCase(sl())); + sl.registerLazySingleton(() => GetAlarmCountUseCase(sl())); + sl.registerFactory(() => AlarmCubit( + getAlarmListUseCase: sl(), + getAlarmCountUseCase: sl(), + )); + /// 5. 状态管理 (Cubit/Bloc) sl.registerLazySingleton(() => AppUserCubit()); // AuthCubit 依赖它,必须先注册 diff --git a/lib/core/error/failure.dart b/lib/core/error/failure.dart index 96dfe2de..8bacb6d5 100644 --- a/lib/core/error/failure.dart +++ b/lib/core/error/failure.dart @@ -6,3 +6,7 @@ class Failure { class NetworkFailure extends Failure { NetworkFailure(super.message); } + +class ServerFailure extends Failure { + ServerFailure(super.message); +} diff --git a/lib/core/error/workorder_failure.dart b/lib/core/error/workorder_failure.dart new file mode 100644 index 00000000..61ad41d9 --- /dev/null +++ b/lib/core/error/workorder_failure.dart @@ -0,0 +1,10 @@ +import 'package:maibu_satabot_v2/core/error/failure.dart'; + +/// 工单模块特定的失败类型 +class WorkOrderFailure extends Failure { + WorkOrderFailure(super.message); +} + +class UnknownFailure extends WorkOrderFailure { + UnknownFailure({required String message}) : super(message); +} diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index 5b650c15..8ae0abbb 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -97,7 +97,7 @@ class TcpClient { try { var packets = _decoder.decode(data); //debugPrint('📦 [TCP] 解码成功,包数量:${packets.length}'); - _logger.logWithLevel('✅ [TCP] 解码成功,包数量:${packets.length}',shouldLog: true); + _logger.logWithLevel('✅ [TCP] 解码成功,包数量:${packets.length}',shouldLog: false); for (var packet in packets) { // 🔥 最根部日志:收到任何推送都打印 // debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}'); @@ -106,23 +106,23 @@ class TcpClient { if (!_controller.isClosed) { _controller.add(packet); //debugPrint('➡️ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}'); - _logger.logWithLevel('✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}' ,shouldLog: true); + _logger.logWithLevel('✅ [TCP] 已分发 CMD: 0x${packet.command.toRadixString(16)}' ,shouldLog: false); } if (packet.command == 0xFF) { //debugPrint('收到服务端心跳,自动回复...'); - _logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...',shouldLog: true); + _logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...',shouldLog: false); sendHeartbeat(); // 回复 AB AA FF AA AB } if (packet.command == 0x03) { // debugPrint('⚠️ 收到认证响应:${packet.payload}'); - _logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}',shouldLog: true); + _logger.logWithLevel('⚠️ 收到认证响应:${packet.payload}',shouldLog: false); // 解析 payload 看是否有错误信息 } } } catch (e, stackTrace) { // debugPrint('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace'); // 🔥 捕获解码异常 - _logger.logWithLevel('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace', shouldLog: true); + _logger.logWithLevel('❌ [TCP] 解码数据时发生异常:$e\n$stackTrace', shouldLog: false); } }, onDone: (){ @@ -130,7 +130,7 @@ class TcpClient { // debugPrint('来到断线重连!'); if (!_isSwitching) { //debugPrint('onDone❌ [TCP] 连接已断开!'); - _logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: true); + _logger.logWithLevel('❌ [TCP] 连接已断开!', shouldLog: false); _handleDisconnect(); } _isSwitching = false; // 重置标志,以免影响下次 diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index f2938dce..ac8d5f31 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -7,7 +7,7 @@ import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity. import 'package:maibu_satabot_v2/features/home/presentation/routes/home_routes.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/pages/tab_settings_page.dart'; import 'package:maibu_satabot_v2/features/my/presentation/routes/my_routes.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/routes/warning_center_routes.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/routes/alarm_center_routes.dart'; import '../../features/auth/presentation/bloc/auth_cubit.dart'; import '../../features/auth/presentation/bloc/auth_state.dart'; @@ -22,11 +22,14 @@ GoRouter createRouter(AuthCubit authCubit) { redirect: (context, state) { final loggedIn = authCubit.state is AuthAuthenticated; final loggingIn = state.matchedLocation == RoutePaths.login; + final registering = state.matchedLocation == RoutePaths.register; - if (!loggedIn && !loggingIn) { + // 如果未登录,只允许访问 login 和 register 页面 + if (!loggedIn && !loggingIn && !registering) { return RoutePaths.login; } - if (loggedIn && loggingIn) { + // 如果已登录,尝试访问 login 或 register,则跳转到首页 + if (loggedIn && (loggingIn || registering)) { return RoutePaths.home; } return null; @@ -60,7 +63,7 @@ GoRouter createRouter(AuthCubit authCubit) { ...HomeRoutes.routes, ...AiRoutes.routes, ...MyRoutes.routes, - ...WarningCenterRoutes.routes, + ...AlarmCenterRoutes.routes, ], ); } diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index cb6c385a..5ec1f686 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -45,145 +45,157 @@ class _LoginPageState extends State { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('登录失败,${state.message}'))); } }, - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 30.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 15), - - // 顶部居中图片和认证信息 - Center( - child: Column( - children: [ - Image.asset( - 'assets/images/huawei.png', - width: 80, - height: 80, - fit: BoxFit.contain, - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), - child: Text( - "通过华为云生态技术认证,请放心使用,证书编号C202605301", - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - height: 1.4, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 顶部区域:图片 + 认证信息 + 标题(压缩空间) + const SizedBox(height: 10), + + // 顶部居中图片和认证信息 + Center( + child: Column( + children: [ + Image.asset( + 'assets/images/huawei.png', + width: 60, + height: 60, + fit: BoxFit.contain, + ), + const SizedBox(height: 6), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Text( + "通过华为云生态技术认证,请放心使用,证书编号C202605301", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 10, + color: Colors.grey.shade600, + height: 1.3, + ), + ), + ), + ], ), ), - ), - ], - ), - ), - - const SizedBox(height: 20), - const Text( - "账号登录", - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black), - ), - const SizedBox(height: 12), - const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)), - const SizedBox(height: 40), + + const SizedBox(height: 16), + const Text( + "账号登录", + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.black), + ), + const SizedBox(height: 8), + const Text("请填写以下信息以验证身份", style: TextStyle(fontSize: 15, color: Colors.grey)), + const SizedBox(height: 24), - // 账号输入框 - _buildInputLabel("账号"), - TextField( - controller: _userCtrl, - cursorColor: Colors.black, - decoration: _inputDecoration(hint: "请输入用户名"), - ), + // 表单区域 + // 账号输入框 + _buildInputLabel("账号"), + TextField( + controller: _userCtrl, + cursorColor: Colors.black, + decoration: _inputDecoration(hint: "请输入用户名"), + ), - const SizedBox(height: 24), + const SizedBox(height: 20), - // 密码输入框 - _buildInputLabel("密码"), - TextField( - controller: _pwdCtrl, - obscureText: _obscurePwd, - cursorColor: Colors.black, - decoration: _inputDecoration( - hint: "请输入密码", - suffixIcon: IconButton( - icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: Colors.grey, size: 20), - onPressed: () => setState(() => _obscurePwd = !_obscurePwd), + // 密码输入框 + _buildInputLabel("密码"), + TextField( + controller: _pwdCtrl, + obscureText: _obscurePwd, + cursorColor: Colors.black, + decoration: _inputDecoration( + hint: "请输入密码", + suffixIcon: IconButton( + icon: Icon(_obscurePwd ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: Colors.grey, size: 20), + onPressed: () => setState(() => _obscurePwd = !_obscurePwd), + ), + ), + ), + + const SizedBox(height: 16), + + // 协议勾选区域 + _buildProtocolSection(), + + const SizedBox(height: 30), + + // 底部区域:登录按钮 + 注册链接 + SizedBox( + width: double.infinity, + child: BlocBuilder( + builder: (context, state) { + bool isLoading = state is LoginLoading; + + return CCPrimaryButton( + onPressed: isLoading + ? null + : () { + if (!_isAgreed) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('请先阅读并同意用户协议'), + duration: Duration(seconds: 1), + ), + ); + return; + } + _handleLogin(); + }, + text: isLoading ? '登录中...' : '登 录', + ); + }, + ), + ), + const SizedBox(height: 16), + + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "没有账号吗?", + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + InkWell( + onTap: () { + debugPrint(' 点击了立即注册'); + debugPrint('👉 跳转路径: ${RoutePaths.register}'); + context.go(RoutePaths.register); + }, + borderRadius: BorderRadius.circular(4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + "立即注册", + style: TextStyle( + fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + ], ), ), ), - - const SizedBox(height: 20), - - // 协议勾选区域 - _buildProtocolSection(), - - const SizedBox(height: 40), - - // 登录按钮 (使用你的 CCPrimaryButton 并增加状态控制) - SizedBox( - width: double.infinity, - child: BlocBuilder( - builder: (context, state) { - // 关键:判断是否正在 Loading - bool isLoading = state is LoginLoading; - - return CCPrimaryButton( - // 核心修复:正在加载时 → onPressed 为 null,按钮禁用 - onPressed: isLoading - ? null - : () { - if (!_isAgreed) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('请先阅读并同意用户协议'), - duration: Duration(seconds: 1), // 这里设置显示时长:2秒 - ), - ); - return; - } - _handleLogin(); - }, - text: isLoading ? '登录中...' : '登 录', - ); - }, - ), - ), - const SizedBox(height: 24), - - Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - "没有账号吗?", - style: TextStyle(fontSize: 14, color: Colors.grey), - ), - GestureDetector( - onTap: () { - print('点击了立即注册'); - print('跳转路径: ${RoutePaths.register}'); - context.go(RoutePaths.register); - }, - child: const Text( - "立即注册", - style: TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - - const SizedBox(height: 15), - - - - ], - ), + ); + }, ), ), ); diff --git a/lib/features/devices/domain/usecases/generate_path_usecase.dart b/lib/features/devices/domain/usecases/generate_path_usecase.dart index 523b9bb2..305e772e 100644 --- a/lib/features/devices/domain/usecases/generate_path_usecase.dart +++ b/lib/features/devices/domain/usecases/generate_path_usecase.dart @@ -1,6 +1,7 @@ import 'package:fpdart/fpdart.dart'; import '../../../../core/error/failure.dart'; +import '../../../../core/error/failure.dart' as core_error; import '../../../auth/domain/errors/auth_failure.dart'; import '../../data/models/device_add_path_point_model.dart'; import '../../data/models/device_work_area_param_model.dart'; @@ -33,7 +34,7 @@ class GeneratePathUseCaseImpl implements GeneratePathUseCase { final result = await _repository.generatePath(reference: reference, heading: heading, outer: outer, holes: holes, workType: workType); return Right(result); // 成功时返回 Right } catch (e) { - return Left(ServerFailure(e.toString(), 1)); // 失败时返回 Left + return Left(core_error.ServerFailure(e.toString())); // 失败时返回 Left } } } diff --git a/lib/features/main_container/domain/tab_config.dart b/lib/features/main_container/domain/tab_config.dart index 26ad0713..c002cda6 100644 --- a/lib/features/main_container/domain/tab_config.dart +++ b/lib/features/main_container/domain/tab_config.dart @@ -80,17 +80,17 @@ class TabConfig extends Equatable { items: [ TabConfigItem( id: 'home_v2', - name: '首页V2', - nameEn: 'Home V2', + name: '首页', + nameEn: 'Home', icon: 'home_rounded', - isEnabled: false, + isEnabled: true, order: 1, ), TabConfigItem( - id: 'home', - name: '状态', - nameEn: 'Status', - icon: 'grid_view_rounded', + id: 'device', + name: '设备', + nameEn: 'Device', + icon: 'devices_rounded', isEnabled: true, order: 2, ), @@ -99,7 +99,7 @@ class TabConfig extends Equatable { name: 'AI', nameEn: 'AI', icon: 'auto_awesome_rounded', - isEnabled: true, + isEnabled: false, order: 3, ), TabConfigItem( @@ -110,13 +110,37 @@ class TabConfig extends Equatable { isEnabled: true, order: 4, ), + TabConfigItem( + id: 'workorder', + name: '工单', + nameEn: 'WorkOrder', + icon: 'assignment_rounded', + isEnabled: true, + order: 5, + ), + TabConfigItem( + id: 'report', + name: '上报', + nameEn: 'Report', + icon: 'post_add_rounded', + isEnabled: true, + order: 6, + ), TabConfigItem( id: 'my', + name: '旧我的', + nameEn: 'Old Profile', + icon: 'person_rounded', + isEnabled: false, + order: 7, + ), + TabConfigItem( + id: 'me', name: '我的', nameEn: 'Profile', icon: 'person_rounded', isEnabled: true, - order: 5, + order: 8, ), ], ); diff --git a/lib/features/main_container/presentation/bloc/tab_config_cubit.dart b/lib/features/main_container/presentation/bloc/tab_config_cubit.dart deleted file mode 100644 index dff65db0..00000000 --- a/lib/features/main_container/presentation/bloc/tab_config_cubit.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart'; - -class TabConfigState { - final TabConfig config; - final int selectedIndex; - - const TabConfigState({ - required this.config, - this.selectedIndex = 0, - }); - - TabConfigState copyWith({ - TabConfig? config, - int? selectedIndex, - }) { - return TabConfigState( - config: config ?? this.config, - selectedIndex: selectedIndex ?? this.selectedIndex, - ); - } -} - -class TabConfigCubit extends Cubit { - TabConfigCubit() : super(TabConfigState(config: TabConfig.defaultConfig())); - - /// 切换 Tab 的启用状态 - void toggleTab(String tabId) { - final newConfig = state.config.toggleItem(tabId); - - // 如果当前选中的 Tab 被禁用,自动切换到第一个启用的 Tab - int newIndex = state.selectedIndex; - if (newIndex >= newConfig.enabledItems.length) { - newIndex = 0; - } - - emit(state.copyWith(config: newConfig, selectedIndex: newIndex)); - } - - /// 切换选中的 Tab - void selectTab(int index) { - if (index >= 0 && index < state.config.enabledItems.length) { - emit(state.copyWith(selectedIndex: index)); - } - } - - /// 重置为默认配置 - void resetToDefault() { - emit(TabConfigState(config: TabConfig.defaultConfig())); - } -} diff --git a/lib/features/main_container/presentation/cubit/tab_config_cubit.dart b/lib/features/main_container/presentation/cubit/tab_config_cubit.dart index 3cc5e00e..83aa0273 100644 --- a/lib/features/main_container/presentation/cubit/tab_config_cubit.dart +++ b/lib/features/main_container/presentation/cubit/tab_config_cubit.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; @@ -9,8 +10,9 @@ class TabConfigInitial extends TabConfigState {} class TabConfigLoaded extends TabConfigState { final TabConfig config; + final int selectedIndex; - TabConfigLoaded(this.config); + TabConfigLoaded(this.config, {this.selectedIndex = 0}); } class TabConfigCubit extends Cubit { @@ -22,31 +24,13 @@ class TabConfigCubit extends Cubit { } void _loadConfig() { - try { - final configJson = _prefs.getString(_configKey); - if (configJson != null) { - final List decoded = jsonDecode(configJson); - final items = decoded.map((e) { - final map = Map.from(e); - return TabConfigItem( - id: map['id'] as String, - name: map['name'] as String, - nameEn: map['nameEn'] as String, - icon: map['icon'] as String, - isEnabled: map['isEnabled'] as bool, - order: map['order'] as int, - ); - }).toList(); - emit(TabConfigLoaded(TabConfig(items: items))); - } else { - final defaultConfig = TabConfig.defaultConfig(); - _saveConfig(defaultConfig); - emit(TabConfigLoaded(defaultConfig)); - } - } catch (e) { - final defaultConfig = TabConfig.defaultConfig(); - emit(TabConfigLoaded(defaultConfig)); - } + debugPrint('🔍 [TabConfigCubit] _loadConfig 被调用'); + // 直接使用代码中的默认配置,不持久化 + final defaultConfig = TabConfig.defaultConfig(); + debugPrint('🔍 [TabConfigCubit] 默认配置 Tab 数量: ${defaultConfig.items.length}'); + debugPrint('🔍 [TabConfigCubit] 启用的 Tab: ${defaultConfig.enabledItems.map((e) => e.id).toList()}'); + emit(TabConfigLoaded(defaultConfig)); + debugPrint('✅ [TabConfigCubit] 状态已 emit'); } void _saveConfig(TabConfig config) { @@ -70,17 +54,25 @@ class TabConfigCubit extends Cubit { final currentState = state as TabConfigLoaded; final newConfig = currentState.config.toggleItem(itemId); - // 只有配置真正改变时才保存和 emit - if (newConfig != currentState.config) { - _saveConfig(newConfig); - emit(TabConfigLoaded(newConfig)); + // 不保存,直接更新状态 + emit(TabConfigLoaded(newConfig, selectedIndex: currentState.selectedIndex)); + } + } + + void selectTab(int index) { + if (state is TabConfigLoaded) { + final currentState = state as TabConfigLoaded; + final enabledTabs = currentState.config.enabledItems; + + // 确保索引在有效范围内 + if (index >= 0 && index < enabledTabs.length) { + emit(TabConfigLoaded(currentState.config, selectedIndex: index)); } } } void resetToDefault() { final defaultConfig = TabConfig.defaultConfig(); - _saveConfig(defaultConfig); emit(TabConfigLoaded(defaultConfig)); } diff --git a/lib/features/main_container/presentation/main_wrapper.dart b/lib/features/main_container/presentation/main_wrapper.dart index 60e2d55f..9ea3572e 100644 --- a/lib/features/main_container/presentation/main_wrapper.dart +++ b/lib/features/main_container/presentation/main_wrapper.dart @@ -5,9 +5,14 @@ import 'package:maibu_satabot_v2/features/main_container/domain/tab_config.dart' import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart'; import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/pages/warning_center_page.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart'; import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart' as my_v2; +import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart'; +import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; class MainWrapper extends StatefulWidget { @@ -20,6 +25,28 @@ class MainWrapper extends StatefulWidget { class _MainWrapperState extends State { int _currentIndex = 0; final PageController _pageController = PageController(); + final List _tabKeys = []; + double? _circleLeft; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateCirclePosition(); + }); + } + + void _updateCirclePosition() { + if (_tabKeys.isEmpty || _currentIndex >= _tabKeys.length) return; + + final key = _tabKeys[_currentIndex]; + final renderBox = key.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + setState(() { + _circleLeft = renderBox.localToGlobal(Offset.zero).dx; + }); + } + } @override void dispose() { @@ -27,16 +54,32 @@ class _MainWrapperState extends State { super.dispose(); } + void _onTabChanged(int index) { + setState(() { + _currentIndex = index; + }); + _pageController.jumpToPage(index); + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateCirclePosition(); + }); + } + IconData _getIconData(String iconName) { switch (iconName) { case 'home_rounded': return Icons.home_rounded; case 'grid_view_rounded': return Icons.grid_view_rounded; + case 'devices_rounded': + return Icons.devices_rounded; case 'auto_awesome_rounded': return Icons.auto_awesome_rounded; case 'warning_amber_rounded': return Icons.warning_amber_rounded; + case 'assignment_rounded': + return Icons.assignment_rounded; + case 'post_add_rounded': + return Icons.post_add_rounded; case 'person_rounded': return Icons.person_rounded; default: @@ -44,6 +87,24 @@ class _MainWrapperState extends State { } } + double _getIndicatorPosition(int itemCount, int currentIndex) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth / itemCount; + return currentIndex * itemWidth; + } + + double _getCircularLeftPosition(int itemCount, int currentIndex) { + final screenWidth = MediaQuery.of(context).size.width; + final itemWidth = screenWidth / itemCount; + // 圆形应该在每个Tab项的中心 + final tabCenterX = 12 + (currentIndex * itemWidth) + (itemWidth / 2); + final circularLeft = tabCenterX - 22; // 22 = 44/2,让圆形中心对齐Tab中心 + + // 边界限制 + final maxLeft = screenWidth - 12 - 44; + return circularLeft.clamp(12.0, maxLeft); + } + Widget _buildCurrentPage(TabConfigItem tab) { switch (tab.id) { case 'home_v2': @@ -53,12 +114,23 @@ class _MainWrapperState extends State { ); case 'home': return const HomePage(); + case 'device': + return const DeviceStatusPage(); case 'ai': return const AiPage(); case 'warning': - return const WarningCenterPage(); + return BlocProvider( + create: (context) => sl(), + child: const AlarmCenterPage(), + ); + case 'workorder': + return const WorkOrderPage(); + case 'report': + return const ReportPage(); case 'my': return const MyPage(); + case 'me': + return const my_v2.MyPage(); default: return const HomePage(); } @@ -87,8 +159,19 @@ class _MainWrapperState extends State { _currentIndex = 0; } + // 初始化 GlobalKey + if (_tabKeys.length != enabledItems.length) { + _tabKeys.clear(); + for (int i = 0; i < enabledItems.length; i++) { + _tabKeys.add(GlobalKey()); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + _updateCirclePosition(); + }); + } + return Scaffold( - extendBody: true, + extendBody: false, body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), @@ -100,57 +183,95 @@ class _MainWrapperState extends State { children: enabledItems.map((tab) => _buildCurrentPage(tab)).toList(), ), bottomNavigationBar: Container( - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 10, - offset: const Offset(0, -2), - ), - ], ), child: SafeArea( top: false, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: enabledItems.asMap().entries.map((entry) { - final index = entry.key; - final item = entry.value; - final isSelected = _currentIndex == index; - - return Expanded( - child: InkWell( - onTap: () { - setState(() { - _currentIndex = index; - }); - _pageController.jumpToPage(index); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 8), - Icon( - _getIconData(item.icon), - size: 24, - color: isSelected ? Colors.blue : Colors.grey, - ), - const SizedBox(height: 4), - Text( - item.name, - style: TextStyle( - fontSize: 11, - color: isSelected ? Colors.blue : Colors.grey, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + child: LayoutBuilder( + builder: (context, constraints) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + padding: const EdgeInsets.symmetric(vertical: 2), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + border: Border.all( + color: const Color(0xFFE5E6EB), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Stack( + children: [ + // 滑动圆形背景 + if (_circleLeft != null) + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + left: _circleLeft! - 8, // 减去外层 margin + 向右偏移 + top: 6, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: const Color(0xFFF2F3F5), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xFFE5E6EB), + width: 1, + ), ), ), - const SizedBox(height: 8), - ], - ), + ), + // Tab 项 + Row( + children: enabledItems.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final isSelected = _currentIndex == index; + + return Expanded( + key: _tabKeys[index], + child: InkWell( + onTap: () => _onTabChanged(index), + child: SizedBox( + height: 56, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _getIconData(item.icon), + size: 24, + color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C), + ), + const SizedBox(height: 4), + Text( + item.name, + style: TextStyle( + fontSize: 11, + color: isSelected ? const Color(0xFF1D2129) : const Color(0xFF86909C), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ], ), ); - }).toList(), + }, ), ), ), diff --git a/lib/features/main_container/presentation/pages/custom_main_container.dart b/lib/features/main_container/presentation/pages/custom_main_container.dart index 1f3fffcc..bab1105e 100644 --- a/lib/features/main_container/presentation/pages/custom_main_container.dart +++ b/lib/features/main_container/presentation/pages/custom_main_container.dart @@ -3,11 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:maibu_satabot_v2/features/ai/presentation/pages/ai_page.dart'; import 'package:maibu_satabot_v2/features/home/presentation/pages/home_page.dart'; import 'package:maibu_satabot_v2/features/my/presentation/pages/my_page.dart'; -import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/pages/home_v2_page.dart'; +import 'package:maibu_satabot_v2/features/v2/device_list/presentation/pages/device_status_page.dart'; +import 'package:maibu_satabot_v2/features/v2/workorder/presentation/pages/workorder_page.dart'; +import 'package:maibu_satabot_v2/features/v2/report/presentation/pages/report_page.dart'; -import '../../../waring_center/presentation/pages/warning_center_page.dart'; +import '../../../v2/waring_center/presentation/pages/alarm_center_page.dart'; class CustomMainContainer extends StatefulWidget { const CustomMainContainer({super.key}); @@ -16,28 +19,37 @@ class CustomMainContainer extends StatefulWidget { State createState() => _CustomMainContainerState(); } -class _CustomMainContainerState extends State with AutomaticKeepAliveClientMixin { +class _CustomMainContainerState extends State + with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; // 保持页面状态 @override Widget build(BuildContext context) { super.build(context); - + debugPrint('🔍 [CustomMainContainer] build 被调用'); - + return BlocBuilder( builder: (context, state) { + if (state is! TabConfigLoaded) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + final enabledTabs = state.config.enabledItems; final currentIndex = state.selectedIndex; - - debugPrint('🔍 [CustomMainContainer] enabledTabs=${enabledTabs.length}, currentIndex=$currentIndex'); - debugPrint('🔍 [CustomMainContainer] enabledTabs IDs=${enabledTabs.map((e) => e.id).toList()}'); - + + debugPrint( + '🔍 [CustomMainContainer] enabledTabs=${enabledTabs.length}, currentIndex=$currentIndex', + ); + debugPrint( + '🔍 [CustomMainContainer] enabledTabs IDs=${enabledTabs.map((e) => e.id).toList()}', + ); + if (enabledTabs.isEmpty) { - return const Scaffold( - body: Center(child: Text('请至少启用一个Tab')), - ); + return const Scaffold(body: Center(child: Text('请至少启用一个Tab'))); } return Scaffold( @@ -45,7 +57,21 @@ class _CustomMainContainerState extends State with Automati index: currentIndex, children: _buildPages(enabledTabs), ), - bottomNavigationBar: const CustomBottomNavBar(), + bottomNavigationBar: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 调试信息:显示当前 Tab ID + Container( + color: Colors.yellow.withOpacity(0.8), + padding: const EdgeInsets.all(4), + child: Text( + '当前选中Tab索引: $currentIndex, ID: ${enabledTabs[currentIndex].id}', + style: const TextStyle(fontSize: 10, color: Colors.black), + ), + ), + const CustomBottomNavBar(), + ], + ), ); }, ); @@ -53,19 +79,32 @@ class _CustomMainContainerState extends State with Automati List _buildPages(List enabledTabs) { return enabledTabs.map((tab) { + debugPrint('🔍 [buildPages] Tab ID: ${tab.id}, Name: ${tab.name}'); switch (tab.id) { case 'home_v2': - return const HomeV2Page(); - case 'home': - return const HomePage(); + debugPrint('✅ 匹配到 home_v2'); + return const HomeV2Page(key: ValueKey('home_v2')); + case 'device': + debugPrint('✅ 匹配到 device'); + return const DeviceStatusPage(key: ValueKey('device')); case 'ai': - return const AiPage(); + debugPrint('✅ 匹配到 ai'); + return const AiPage(key: ValueKey('ai')); case 'warning': - return const WarningCenterPage(); + debugPrint('✅ 匹配到 warning'); + return const AlarmCenterPage(key: ValueKey('warning')); + case 'workorder': + debugPrint('✅ 匹配到 workorder'); + return const WorkOrderPage(key: ValueKey('workorder')); + case 'report': + debugPrint('✅ 匹配到 report'); + return const ReportPage(key: ValueKey('report')); case 'my': - return const MyPage(); + debugPrint('✅ 匹配到 my'); + return const MyPage(key: ValueKey('my')); default: - return const HomePage(); + debugPrint('❌ 未匹配到 Tab ID: ${tab.id}'); + return const SizedBox(); } }).toList(); } diff --git a/lib/features/main_container/presentation/pages/tab_settings_page.dart b/lib/features/main_container/presentation/pages/tab_settings_page.dart index aea75775..3994bc91 100644 --- a/lib/features/main_container/presentation/pages/tab_settings_page.dart +++ b/lib/features/main_container/presentation/pages/tab_settings_page.dart @@ -68,10 +68,16 @@ class TabSettingsPage extends StatelessWidget { return Icons.home_rounded; case 'grid_view_rounded': return Icons.grid_view_rounded; + case 'devices_rounded': + return Icons.devices_rounded; case 'auto_awesome_rounded': return Icons.auto_awesome_rounded; case 'warning_amber_rounded': return Icons.warning_amber_rounded; + case 'assignment_rounded': + return Icons.assignment_rounded; + case 'post_add_rounded': + return Icons.post_add_rounded; case 'person_rounded': return Icons.person_rounded; default: diff --git a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart index 118f58b5..2000681c 100644 --- a/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart +++ b/lib/features/main_container/presentation/widgets/custom_bottom_nav_bar.dart @@ -1,51 +1,162 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:maibu_satabot_v2/features/main_container/presentation/bloc/tab_config_cubit.dart'; -import 'package:cc_ui_kit/cc_ui_kit.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; -class CustomBottomNavBar extends StatelessWidget { +class CustomBottomNavBar extends StatefulWidget { const CustomBottomNavBar({super.key}); + @override + State createState() => _CustomBottomNavBarState(); +} + +class _CustomBottomNavBarState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _animation; + int _previousIndex = 0; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _animation = Tween( + begin: 0.0, + end: 0.0, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + if (state is! TabConfigLoaded) { + return const SizedBox.shrink(); + } + final enabledTabs = state.config.enabledItems; - + if (enabledTabs.isEmpty) { return const SizedBox.shrink(); } - return Container( - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, -2), - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + return _buildNavigationBar( + constraints: constraints, + enabledTabs: enabledTabs, + selectedIndex: state.selectedIndex, + ); + }, + ); + }, + ); + } + + Widget _buildNavigationBar({ + required BoxConstraints constraints, + required List enabledTabs, + required int selectedIndex, + }) { + final tabWidth = constraints.maxWidth / enabledTabs.length; + final circleDiameter = 48.0; + + // 当索引改变时,更新动画 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_previousIndex != selectedIndex) { + final beginOffset = _previousIndex * tabWidth + tabWidth / 2; + final endOffset = selectedIndex * tabWidth + tabWidth / 2; + + _animation = Tween( + begin: beginOffset - circleDiameter / 2, + end: endOffset - circleDiameter / 2, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, ), - child: SafeArea( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + ); + + _previousIndex = selectedIndex; + _animationController.forward(from: 0.0); + } + }); + + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: SafeArea( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + // 底层:Tab 列表 + Row( children: List.generate(enabledTabs.length, (index) { final tab = enabledTabs[index]; - final isSelected = index == state.selectedIndex; - - return _buildNavItem( - context, - icon: _getIconData(tab.icon), - label: tab.name, - isSelected: isSelected, - onTap: () => context.read().selectTab(index), + final isSelected = index == selectedIndex; + + return Expanded( + child: _buildNavItem( + context, + icon: _getIconData(tab.icon), + label: tab.name, + isSelected: isSelected, + onTap: () => context.read().selectTab(index), + ), ); }), ), - ), - ); - }, + // 上层:动画圆形背景 + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return Positioned( + bottom: 8, + left: _animation.value, + child: Container( + width: circleDiameter, + height: circleDiameter, + decoration: BoxDecoration( + color: const Color(0xFFF5F5F5), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 8, + spreadRadius: 1, + offset: Offset.zero, + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), ); } @@ -56,15 +167,18 @@ class CustomBottomNavBar extends StatelessWidget { required bool isSelected, required VoidCallback onTap, }) { - return Expanded( - child: InkWell( - onTap: onTap, + return InkWell( + onTap: onTap, + child: Container( + height: 56, + alignment: Alignment.center, child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, - color: isSelected ? const Color(0xFF2196F3) : Colors.grey, + color: isSelected ? Colors.black : const Color(0xFF666666), size: 24, ), const SizedBox(height: 4), @@ -72,8 +186,8 @@ class CustomBottomNavBar extends StatelessWidget { label, style: TextStyle( fontSize: 12, - color: isSelected ? const Color(0xFF2196F3) : Colors.grey, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected ? Colors.black : const Color(0xFF666666), + fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400, ), ), ], @@ -84,12 +198,18 @@ class CustomBottomNavBar extends StatelessWidget { IconData _getIconData(String iconName) { switch (iconName) { + case 'home_rounded': + return Icons.home_rounded; case 'grid_view_rounded': return Icons.grid_view_rounded; + case 'devices_rounded': + return Icons.devices_rounded; case 'auto_awesome_rounded': return Icons.auto_awesome_rounded; case 'warning_amber_rounded': return Icons.warning_amber_rounded; + case 'assignment_rounded': + return Icons.assignment_rounded; case 'person_rounded': return Icons.person_rounded; default: diff --git a/lib/features/v2/device_list/data/data.dart b/lib/features/v2/device_list/data/data.dart new file mode 100644 index 00000000..a52aefaa --- /dev/null +++ b/lib/features/v2/device_list/data/data.dart @@ -0,0 +1,4 @@ +// 数据层聚合导出 +export 'datasources/datasources.dart'; +export 'models/models.dart'; +export 'repositories/repositories.dart'; diff --git a/lib/features/v2/device_list/data/datasources/datasources.dart b/lib/features/v2/device_list/data/datasources/datasources.dart new file mode 100644 index 00000000..63bf6290 --- /dev/null +++ b/lib/features/v2/device_list/data/datasources/datasources.dart @@ -0,0 +1,2 @@ +// 数据源层 +export 'device_remote_datasource.dart'; \ No newline at end of file diff --git a/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart new file mode 100644 index 00000000..2238d4bc --- /dev/null +++ b/lib/features/v2/device_list/data/datasources/device_remote_datasource.dart @@ -0,0 +1,95 @@ +import '../../domain/entities/device_entity.dart'; +import '../../domain/entities/device_status_entity.dart'; +import '../models/device_data_model.dart'; +import '../models/device_status_data_model.dart'; + +/// 设备远程数据源抽象接口 +abstract class DeviceRemoteDataSource { + Future getDeviceStatus(); + Future> getDeviceList({String? typeFilter}); +} + +/// 设备远程数据源实现类 - 模拟从 API 获取数据 +class DeviceRemoteDataSourceImpl implements DeviceRemoteDataSource { + @override + Future getDeviceStatus() async { + // 模拟网络延迟 + await Future.delayed(const Duration(seconds: 1)); + + // 模拟数据 + return const DeviceStatusDataModel( + total: 136, + online: 122, + offline: 6, + exception: 8, + ); + } + + @override + Future> getDeviceList({String? typeFilter}) async { + // 模拟网络延迟 + await Future.delayed(const Duration(milliseconds: 500)); + + // 模拟数据 - 根据设计图 + final allDevices = [ + const DeviceDataModel( + deviceId: '10001', + name: '逆变器 INV-001', + type: '逆变器', + status: '在线', + power: 125.8, + todayEnergy: 752.6, + ), + const DeviceDataModel( + deviceId: '10002', + name: '逆变器 INV-002', + type: '逆变器', + status: '在线', + power: 118.6, + todayEnergy: 698.2, + ), + const DeviceDataModel( + deviceId: '10003', + name: '汇流箱 SCB-001', + type: '汇流箱', + status: '在线', + power: 0, + todayEnergy: 0, + current: 12.6, + temperature: 36.2, + ), + const DeviceDataModel( + deviceId: '10004', + name: '组件组串 STR-001', + type: '组件', + status: '在线', + power: 85.3, + todayEnergy: 0, + radiation: 823, + ), + const DeviceDataModel( + deviceId: '10005', + name: '监控 MON-001', + type: '监控', + status: '离线', + power: 0, + todayEnergy: 0, + ), + const DeviceDataModel( + deviceId: '10006', + name: '逆变器 INV-003', + type: '逆变器', + status: '异常', + power: 45.2, + todayEnergy: 256.8, + ), + ]; + + // 根据类型过滤 + if (typeFilter != null && typeFilter != '全部') { + return allDevices.where((device) => device.type == typeFilter).toList(); + } + + return allDevices; + } +} diff --git a/lib/features/v2/device_list/data/models/device_data_model.dart b/lib/features/v2/device_list/data/models/device_data_model.dart new file mode 100644 index 00000000..f4cfaa8a --- /dev/null +++ b/lib/features/v2/device_list/data/models/device_data_model.dart @@ -0,0 +1,76 @@ +import '../../domain/entities/device_entity.dart'; + +/// 设备数据模型 +class DeviceDataModel { + final String deviceId; + final String name; + final String type; + final String status; + final double power; + final double todayEnergy; + final double? temperature; + final double? current; + final double? voltage; + final double? radiation; + + const DeviceDataModel({ + required this.deviceId, + required this.name, + required this.type, + required this.status, + required this.power, + required this.todayEnergy, + this.temperature, + this.current, + this.voltage, + this.radiation, + }); + + /// 从 JSON 创建数据模型 + factory DeviceDataModel.fromJson(Map json) { + return DeviceDataModel( + deviceId: json['deviceId'] as String, + name: json['name'] as String, + type: json['type'] as String, + status: json['status'] as String, + power: (json['power'] as num).toDouble(), + todayEnergy: (json['todayEnergy'] as num).toDouble(), + temperature: json['temperature']?.toDouble(), + current: json['current']?.toDouble(), + voltage: json['voltage']?.toDouble(), + radiation: json['radiation']?.toDouble(), + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'deviceId': deviceId, + 'name': name, + 'type': type, + 'status': status, + 'power': power, + 'todayEnergy': todayEnergy, + 'temperature': temperature, + 'current': current, + 'voltage': voltage, + 'radiation': radiation, + }; + } + + /// 转换为实体 + DeviceEntity toEntity() { + return DeviceEntity( + deviceId: deviceId, + name: name, + type: type, + status: status, + power: power, + todayEnergy: todayEnergy, + temperature: temperature, + current: current, + voltage: voltage, + radiation: radiation, + ); + } +} diff --git a/lib/features/v2/device_list/data/models/device_status_data_model.dart b/lib/features/v2/device_list/data/models/device_status_data_model.dart new file mode 100644 index 00000000..6a49498b --- /dev/null +++ b/lib/features/v2/device_list/data/models/device_status_data_model.dart @@ -0,0 +1,46 @@ +import '../../domain/entities/device_status_entity.dart'; + +/// 设备状态统计数据模型 +class DeviceStatusDataModel { + final int total; + final int online; + final int offline; + final int exception; + + const DeviceStatusDataModel({ + required this.total, + required this.online, + required this.offline, + required this.exception, + }); + + /// 从 JSON 创建数据模型 + factory DeviceStatusDataModel.fromJson(Map json) { + return DeviceStatusDataModel( + total: json['total'] as int, + online: json['online'] as int, + offline: json['offline'] as int, + exception: json['exception'] as int, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'total': total, + 'online': online, + 'offline': offline, + 'exception': exception, + }; + } + + /// 转换为实体 + DeviceStatusEntity toEntity() { + return DeviceStatusEntity( + total: total, + online: online, + offline: offline, + exception: exception, + ); + } +} diff --git a/lib/features/v2/device_list/data/models/models.dart b/lib/features/v2/device_list/data/models/models.dart new file mode 100644 index 00000000..50c7fc1e --- /dev/null +++ b/lib/features/v2/device_list/data/models/models.dart @@ -0,0 +1,3 @@ +// 数据模型层 +export 'device_data_model.dart'; +export 'device_status_data_model.dart'; diff --git a/lib/features/v2/device_list/data/repositories/device_repository_impl.dart b/lib/features/v2/device_list/data/repositories/device_repository_impl.dart new file mode 100644 index 00000000..6097a4d4 --- /dev/null +++ b/lib/features/v2/device_list/data/repositories/device_repository_impl.dart @@ -0,0 +1,23 @@ +import '../../domain/entities/device_entity.dart'; +import '../../domain/entities/device_status_entity.dart'; +import '../../domain/repositories/device_repository.dart'; +import '../datasources/device_remote_datasource.dart'; + +/// 设备仓储实现类 +class DeviceRepositoryImpl implements DeviceRepository { + final DeviceRemoteDataSource remoteDataSource; + + const DeviceRepositoryImpl({required this.remoteDataSource}); + + @override + Future getDeviceStatus() async { + final dataModel = await remoteDataSource.getDeviceStatus(); + return dataModel.toEntity(); + } + + @override + Future> getDeviceList({String? typeFilter}) async { + final dataModels = await remoteDataSource.getDeviceList(typeFilter: typeFilter); + return dataModels.map((model) => model.toEntity()).toList(); + } +} diff --git a/lib/features/v2/device_list/data/repositories/repositories.dart b/lib/features/v2/device_list/data/repositories/repositories.dart new file mode 100644 index 00000000..0c74205a --- /dev/null +++ b/lib/features/v2/device_list/data/repositories/repositories.dart @@ -0,0 +1,2 @@ +// 仓储实现 +export 'device_repository_impl.dart'; diff --git a/lib/features/v2/device_list/device.dart b/lib/features/v2/device_list/device.dart new file mode 100644 index 00000000..dafdfb3e --- /dev/null +++ b/lib/features/v2/device_list/device.dart @@ -0,0 +1,6 @@ +// 设备状态模块 - v2 版本 +// 清洁架构实现 + +export 'presentation/pages/device_status_page.dart'; +export 'presentation/bloc/bloc.dart'; +export 'presentation/widgets/widgets.dart'; diff --git a/lib/features/v2/device_list/domain/domain.dart b/lib/features/v2/device_list/domain/domain.dart new file mode 100644 index 00000000..caefa312 --- /dev/null +++ b/lib/features/v2/device_list/domain/domain.dart @@ -0,0 +1,4 @@ +// 领域层聚合导出 +export 'entities/entities.dart'; +export 'repositories/device_repository.dart'; +export 'usecases/usecases.dart'; diff --git a/lib/features/v2/device_list/domain/entities/device_entity.dart b/lib/features/v2/device_list/domain/entities/device_entity.dart new file mode 100644 index 00000000..874077f9 --- /dev/null +++ b/lib/features/v2/device_list/domain/entities/device_entity.dart @@ -0,0 +1,26 @@ +/// 设备实体类 +class DeviceEntity { + final String deviceId; + final String name; + final String type; + final String status; + final double power; + final double todayEnergy; + final double? temperature; + final double? current; + final double? voltage; + final double? radiation; + + const DeviceEntity({ + required this.deviceId, + required this.name, + required this.type, + required this.status, + required this.power, + required this.todayEnergy, + this.temperature, + this.current, + this.voltage, + this.radiation, + }); +} diff --git a/lib/features/v2/device_list/domain/entities/device_status_entity.dart b/lib/features/v2/device_list/domain/entities/device_status_entity.dart new file mode 100644 index 00000000..54a659d6 --- /dev/null +++ b/lib/features/v2/device_list/domain/entities/device_status_entity.dart @@ -0,0 +1,14 @@ +/// 设备状态统计实体类 +class DeviceStatusEntity { + final int total; + final int online; + final int offline; + final int exception; + + const DeviceStatusEntity({ + required this.total, + required this.online, + required this.offline, + required this.exception, + }); +} diff --git a/lib/features/v2/device_list/domain/entities/entities.dart b/lib/features/v2/device_list/domain/entities/entities.dart new file mode 100644 index 00000000..94859415 --- /dev/null +++ b/lib/features/v2/device_list/domain/entities/entities.dart @@ -0,0 +1,3 @@ +// 实体层 +export 'device_entity.dart'; +export 'device_status_entity.dart'; diff --git a/lib/features/v2/device_list/domain/repositories/device_repository.dart b/lib/features/v2/device_list/domain/repositories/device_repository.dart new file mode 100644 index 00000000..81879d86 --- /dev/null +++ b/lib/features/v2/device_list/domain/repositories/device_repository.dart @@ -0,0 +1,8 @@ +import '../entities/device_entity.dart'; +import '../entities/device_status_entity.dart'; + +/// 设备仓储抽象接口 +abstract class DeviceRepository { + Future getDeviceStatus(); + Future> getDeviceList({String? typeFilter}); +} diff --git a/lib/features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart b/lib/features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart new file mode 100644 index 00000000..bd4aad8f --- /dev/null +++ b/lib/features/v2/device_list/domain/usecases/get_device_status_data_usecase.dart @@ -0,0 +1,28 @@ +import '../entities/device_entity.dart'; +import '../entities/device_status_entity.dart'; +import '../repositories/device_repository.dart'; + +/// 获取设备状态数据用例 +class GetDeviceStatusDataUseCase { + final DeviceRepository repository; + + const GetDeviceStatusDataUseCase({required this.repository}); + + Future execute({String? typeFilter}) async { + try { + final status = await repository.getDeviceStatus(); + final devices = await repository.getDeviceList(typeFilter: typeFilter); + return DeviceStatusResponse(status: status, devices: devices); + } catch (e) { + throw Exception('获取设备数据失败: ${e.toString()}'); + } + } +} + +/// 设备状态响应类 +class DeviceStatusResponse { + final DeviceStatusEntity status; + final List devices; + + const DeviceStatusResponse({required this.status, required this.devices}); +} diff --git a/lib/features/v2/device_list/domain/usecases/usecases.dart b/lib/features/v2/device_list/domain/usecases/usecases.dart new file mode 100644 index 00000000..aa024d4a --- /dev/null +++ b/lib/features/v2/device_list/domain/usecases/usecases.dart @@ -0,0 +1,2 @@ +// 用例层 +export 'get_device_status_data_usecase.dart'; diff --git a/lib/features/v2/device_list/presentation/bloc/bloc.dart b/lib/features/v2/device_list/presentation/bloc/bloc.dart new file mode 100644 index 00000000..5444597a --- /dev/null +++ b/lib/features/v2/device_list/presentation/bloc/bloc.dart @@ -0,0 +1,4 @@ +// BLoC 层 +export 'device_status_bloc.dart'; +export 'device_status_event.dart'; +export 'device_status_state.dart'; diff --git a/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart b/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart new file mode 100644 index 00000000..aba6dedc --- /dev/null +++ b/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart @@ -0,0 +1,85 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/usecases/get_device_status_data_usecase.dart'; +import 'device_status_event.dart'; +import 'device_status_state.dart'; + +class DeviceStatusBloc extends Bloc { + final GetDeviceStatusDataUseCase getDeviceStatusDataUseCase; + + DeviceStatusBloc(this.getDeviceStatusDataUseCase) + : super(const DeviceStatusInitial()) { + on(_onLoadData); + on(_onRefresh); + on(_onChangeType); + on(_onSearch); + } + + Future _onLoadData( + DeviceStatusLoadData event, + Emitter emit, + ) async { + emit(const DeviceStatusLoading()); + + try { + final response = await getDeviceStatusDataUseCase.execute(); + + emit(DeviceStatusLoaded( + deviceStatus: response.status, + devices: response.devices, + )); + } catch (e) { + emit(DeviceStatusError(e.toString())); + } + } + + Future _onRefresh( + DeviceStatusRefresh event, + Emitter emit, + ) async { + if (state is DeviceStatusLoaded) { + final currentState = state as DeviceStatusLoaded; + + try { + final response = await getDeviceStatusDataUseCase.execute( + typeFilter: currentState.selectedType == '全部' + ? null + : currentState.selectedType, + ); + + emit(currentState.copyWith( + deviceStatus: response.status, + devices: response.devices, + )); + } catch (e) { + emit(DeviceStatusError(e.toString())); + } + } + } + + void _onChangeType( + DeviceStatusChangeType event, + Emitter emit, + ) { + if (state is DeviceStatusLoaded) { + final currentState = state as DeviceStatusLoaded; + + // 🔥 直接更新选中类型,不重新请求接口(前端过滤) + emit(currentState.copyWith( + selectedType: event.type, + )); + } + } + + void _onSearch( + DeviceStatusSearch event, + Emitter emit, + ) { + if (state is DeviceStatusLoaded) { + final currentState = state as DeviceStatusLoaded; + + emit(currentState.copyWith( + searchKeyword: event.keyword, + )); + } + } +} diff --git a/lib/features/v2/device_list/presentation/bloc/device_status_event.dart b/lib/features/v2/device_list/presentation/bloc/device_status_event.dart new file mode 100644 index 00000000..a967c3dd --- /dev/null +++ b/lib/features/v2/device_list/presentation/bloc/device_status_event.dart @@ -0,0 +1,34 @@ +import 'package:equatable/equatable.dart'; + +abstract class DeviceStatusEvent extends Equatable { + const DeviceStatusEvent(); + + @override + List get props => []; +} + +class DeviceStatusLoadData extends DeviceStatusEvent { + const DeviceStatusLoadData(); +} + +class DeviceStatusRefresh extends DeviceStatusEvent { + const DeviceStatusRefresh(); +} + +class DeviceStatusChangeType extends DeviceStatusEvent { + final String type; + + const DeviceStatusChangeType(this.type); + + @override + List get props => [type]; +} + +class DeviceStatusSearch extends DeviceStatusEvent { + final String keyword; + + const DeviceStatusSearch(this.keyword); + + @override + List get props => [keyword]; +} diff --git a/lib/features/v2/device_list/presentation/bloc/device_status_state.dart b/lib/features/v2/device_list/presentation/bloc/device_status_state.dart new file mode 100644 index 00000000..d323822d --- /dev/null +++ b/lib/features/v2/device_list/presentation/bloc/device_status_state.dart @@ -0,0 +1,67 @@ +import 'package:equatable/equatable.dart'; +import '../../domain/entities/device_entity.dart'; +import '../../domain/entities/device_status_entity.dart'; + +abstract class DeviceStatusState extends Equatable { + const DeviceStatusState(); + + @override + List get props => []; +} + +class DeviceStatusInitial extends DeviceStatusState { + const DeviceStatusInitial(); +} + +class DeviceStatusLoading extends DeviceStatusState { + const DeviceStatusLoading(); +} + +class DeviceStatusLoaded extends DeviceStatusState { + final DeviceStatusEntity deviceStatus; + final List devices; + final String selectedType; + final String searchKeyword; + + const DeviceStatusLoaded({ + required this.deviceStatus, + required this.devices, + this.selectedType = '全部', + this.searchKeyword = '', + }); + + DeviceStatusLoaded copyWith({ + DeviceStatusEntity? deviceStatus, + List? devices, + String? selectedType, + String? searchKeyword, + }) { + return DeviceStatusLoaded( + deviceStatus: deviceStatus ?? this.deviceStatus, + devices: devices ?? this.devices, + selectedType: selectedType ?? this.selectedType, + searchKeyword: searchKeyword ?? this.searchKeyword, + ); + } + + @override + List get props => [deviceStatus, devices, selectedType, searchKeyword]; +} + +class DeviceStatusError extends DeviceStatusState { + final String message; + + const DeviceStatusError(this.message); + + @override + List get props => [message]; +} + +class DeviceStatusEmpty extends DeviceStatusState { + final DeviceStatusEntity deviceStatus; + + const DeviceStatusEmpty({required this.deviceStatus}); + + @override + List get props => [deviceStatus]; +} diff --git a/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart b/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart new file mode 100644 index 00000000..70264dad --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/cleaning_weeding_robot_task_page.dart @@ -0,0 +1,448 @@ +import 'package:flutter/material.dart'; + +/// 清洗/除草机器人任务详情页 +class CleaningWeedingRobotTaskPage extends StatelessWidget { + final Map robot; + + const CleaningWeedingRobotTaskPage({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + final isCleaning = robot['type'] == '清洗机器人'; + final title = isCleaning ? '清洗机器人任务' : '除草机器人任务'; + + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.edit_outlined, color: Color(0xFF1D2129)), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildTaskInfoCard(isCleaning), + const SizedBox(height: 12), + _buildProgressCard(isCleaning), + const SizedBox(height: 12), + _buildDeviceStatusCard(isCleaning), + ], + ), + ), + _buildBottomButtons(), + ], + ), + ); + } + + Widget _buildTaskInfoCard(bool isCleaning) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + isCleaning ? '清洗任务-组件清洗A区' : '除草任务-光伏板除草B区', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF165DFF).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '进行中', + style: TextStyle( + fontSize: 12, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow('任务编号', isCleaning ? 'CL-2025052001' : 'WD-2025052001'), + _buildInfoRow('作业区域', isCleaning ? '清洗区A区-01' : '除草区B区-02'), + _buildInfoRow('开始时间', '05-20 09:20'), + _buildInfoRow('预计完成', '05-20 14:30'), + ], + ), + ), + const SizedBox(width: 12), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + isCleaning ? 'assets/images/qinxi.png' : 'assets/images/chucao.png', + width: 80, + height: 80, + fit: BoxFit.cover, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Text( + '$label:', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildProgressCard(bool isCleaning) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '作业进度', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: Text( + '覆盖面积', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + ), + Text( + isCleaning ? '8,230 m² / 15,000 m²' : '5,600 m² / 12,000 m²', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + Text( + isCleaning ? '55%' : '47%', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF165DFF), + ), + ), + ], + ), + const SizedBox(height: 8), + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: isCleaning ? 0.55 : 0.47, + backgroundColor: const Color(0xFFF2F3F5), + valueColor: const AlwaysStoppedAnimation(Color(0xFF165DFF)), + minHeight: 8, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + const Text( + '预计完成时间', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 8), + Text( + '14:30(剩余 2h10m)', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDeviceStatusCard(bool isCleaning) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '设备状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildStatusRow('电量', '68%', 0.68), + const SizedBox(height: 12), + if (isCleaning) ...[ + _buildStatusRow('水箱水位', '62%', 0.62), + const SizedBox(height: 12), + _buildStatusRowWithBadge('喷淋压力', '0.45 MPa', '正常'), + const SizedBox(height: 12), + _buildStatusRowWithBadge('刷头状态', '正常', '正常'), + ] else ...[ + _buildStatusRow('燃油量', '75%', 0.75), + const SizedBox(height: 12), + _buildStatusRowWithBadge('刀片状态', '正常', '正常'), + const SizedBox(height: 12), + _buildStatusRowWithBadge('导航状态', '正常', '正常'), + ], + ], + ), + ); + } + + Widget _buildStatusRow(String label, String value, double progress) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(width: 12), + Text( + value, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + SizedBox( + width: 120, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: progress, + backgroundColor: const Color(0xFFF2F3F5), + valueColor: AlwaysStoppedAnimation( + progress > 0.5 ? const Color(0xFF00B42A) : const Color(0xFFFF7D00), + ), + minHeight: 6, + ), + ), + ), + ], + ); + } + + Widget _buildStatusRowWithBadge(String label, String value, String badgeText) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } + + Widget _buildBottomButtons() { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + '开始作业', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7D00), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + '暂停', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF4E5969), + side: const BorderSide(color: Color(0xFFC9CDD4)), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '返航/回充', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/device_status_page.dart b/lib/features/v2/device_list/presentation/pages/device_status_page.dart new file mode 100644 index 00000000..8b3b678f --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -0,0 +1,454 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../core/di/injection.dart'; +import '../bloc/device_status_bloc.dart'; +import '../bloc/device_status_event.dart'; +import '../bloc/device_status_state.dart'; +import '../widgets/device_item_widget.dart'; +import '../widgets/drone_station_item_card.dart'; +import 'robot_list_page.dart'; +import 'drone_station_detail_page.dart'; +/// 设备状态页面 - 使用 BLoC 模式 +class DeviceStatusPage extends StatelessWidget { + const DeviceStatusPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => sl()..add(const DeviceStatusLoadData()), + child: const DeviceStatusView(), + ); + } +} + +/// 设备状态视图 +class DeviceStatusView extends StatelessWidget { + const DeviceStatusView({super.key}); + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + child: Scaffold( + backgroundColor: const Color(0xFFF7F7F7), + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildAppBar(), + _buildSearchBar(context), + _buildTypeFilterBar(context), + Expanded( + child: _buildContent(context, state), + ), + ], + ); + }, + ), + ), + ), + ); + } + + Widget _buildAppBar() { + return Container( + height: 44.0, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: const Row( + children: [ + Text( + '设备状态', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + Spacer(), + Icon( + Icons.more_vert, + size: 24, + color: Color(0xFF1D2129), + ), + ], + ), + ); + } + + Widget _buildSearchBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8, 16.0, 8), + child: TextField( + decoration: InputDecoration( + hintText: '搜索设备名称/编号', + hintStyle: const TextStyle( + color: Color(0xFF86909C), + fontSize: 14, + ), + prefixIcon: const Icon( + Icons.search, + color: Color(0xFF86909C), + size: 24, + ), + suffixIcon: TextButton( + onPressed: () { + // TODO: 触发搜索 + }, + child: const Text( + '搜索', + style: TextStyle( + color: Color(0xFF165DFF), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + filled: true, + fillColor: const Color(0xFFF2F3F5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24.0), + borderSide: const BorderSide( + color: Color(0xFFE5E6EB), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24.0), + borderSide: const BorderSide( + color: Color(0xFFE5E6EB), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(24.0), + borderSide: const BorderSide( + color: Color(0xFF165DFF), + width: 1, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1D2129), + ), + onSubmitted: (value) { + // 🔥 点击键盘确定键时触发搜索 + context.read().add(DeviceStatusSearch(value)); + }, + ), + ); + } + + Widget _buildTypeFilterBar(BuildContext context) { + final types = ['全部', '机器人', '无人机机场', '逆变器', '汇流箱', '组件', '监控']; + + return BlocBuilder( + builder: (context, state) { + String selectedType = '全部'; + if (state is DeviceStatusLoaded) { + selectedType = state.selectedType; + } + + return SizedBox( + height: 40, + child: ListView.separated( + scrollDirection: Axis.horizontal, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16.0), + itemCount: types.length, + separatorBuilder: (context, index) => const SizedBox(width: 24.0), + itemBuilder: (context, index) { + final type = types[index]; + final isSelected = selectedType == type; + + return GestureDetector( + onTap: () { + context.read().add(DeviceStatusChangeType(type)); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + type, + style: TextStyle( + fontSize: 15, + color: isSelected ? const Color(0xFF135BFF) : const Color(0xFF666666), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + const SizedBox(height: 4), + Container( + width: 20, + height: 2, + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF0D57FF) : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], + + ), + ); + }, + ), + ); + }, + ); + } + + Widget _buildContent(BuildContext context, DeviceStatusState state) { + // 如果选择的是"机器人",显示机器人专属页面 + if (state is DeviceStatusLoaded && state.selectedType == '机器人') { + return const RobotListPage(); + } + + // 如果选择的是"无人机机场",显示机场列表 + if (state is DeviceStatusLoaded && state.selectedType == '无人机机场') { + return _buildDroneStationList(context); + } + + // 其他类型显示原有设备列表 + return _buildDeviceList(context, state); + } + + Widget _buildDeviceList(BuildContext context, DeviceStatusState state) { + if (state is DeviceStatusLoading) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFF165DFF), + ), + ); + } + + if (state is DeviceStatusError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: Color(0xFF86909C), + ), + const SizedBox(height: 16), + Text( + state.message, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + context.read().add(const DeviceStatusLoadData()); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + ), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (state is DeviceStatusLoaded) { + // 🔥 先根据类型过滤 + List filteredByType = state.devices; + if (state.selectedType != '全部') { + filteredByType = state.devices.where((device) { + return device.type == state.selectedType; + }).toList(); + } + + // 🔥 再根据搜索关键词过滤 + final filteredDevices = filteredByType.where((device) { + if (state.searchKeyword.isEmpty) return true; + return device.name.toLowerCase().contains(state.searchKeyword.toLowerCase()) || + device.deviceId.toLowerCase().contains(state.searchKeyword.toLowerCase()); + }).toList(); + + return RefreshIndicator( + onRefresh: () async { + context.read().add(const DeviceStatusRefresh()); + }, + color: const Color(0xFF165DFF), + child: ListView( + padding: const EdgeInsets.only(bottom: 16), + children: [ + _buildStatusCard(state), + const SizedBox(height: 20.0), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: filteredDevices.length, + itemBuilder: (context, index) { + final device = filteredDevices[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: DeviceItemWidget( + device: device, + onTap: () { + debugPrint('点击设备: ${device.name}'); + // TODO: 导航到设备详情页 + }, + ), + ); + }, + ), + ], + ), + ); + } + + return const Center(child: Text('暂无数据')); + } + + Widget _buildDroneStationList(BuildContext context) { + // 模拟机场数据 + final stations = [ + {'name': '1号无人机机场', 'id': 'AIRPORT-01'}, + {'name': '2号无人机机场', 'id': 'AIRPORT-02'}, + {'name': '3号无人机机场', 'id': 'AIRPORT-03'}, + ]; + + return ListView.builder( + padding: const EdgeInsets.only(top: 16, bottom: 16), + itemCount: stations.length, + itemBuilder: (context, index) { + final station = stations[index]; + return DroneStationItemCard( + name: station['name'] as String, + id: station['id'] as String, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DroneStationDetailPage(), + ), + ); + }, + ); + }, + ); + } + + Widget _buildStatusCard(DeviceStatusLoaded state) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + _buildStatItem( + value: state.deviceStatus.total.toString(), + label: '全部设备', + dotColor: const Color(0xFF86909C), + textColor: const Color(0xFF86909C), // 新增字体颜色 + ), + Container( + width: 1, + height: 40, + color: const Color(0xFFE5E6EB), + ), + _buildStatItem( + value: state.deviceStatus.online.toString(), + label: '在线', + dotColor: const Color(0xFF00B42A), + textColor: const Color(0xFF00B42A), + ), + Container( + width: 1, + height: 40, + color: const Color(0xFFE5E6EB), + ), + _buildStatItem( + value: state.deviceStatus.exception.toString(), + label: '告警', + dotColor: const Color(0xFFFF7D00), + textColor: const Color(0xFFFF7D00), + ), + Container( + width: 1, + height: 40, + color: const Color(0xFFE5E6EB), + ), + _buildStatItem( + value: state.deviceStatus.offline.toString(), + label: '离线', + dotColor: const Color(0xFFF53F3F), + textColor: const Color(0xFFF53F3F), + ), + ], + ), + ); + } + + Widget _buildStatItem({ + required String value, + required String label, + required Color dotColor, + required Color? textColor, // 新增字体颜色 + }) { + return Expanded( + child: Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: textColor ?? const Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: dotColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: textColor ?? const Color(0xFF4E5969), + ), + ), + ], + ), + ], + ), + ); + } + +} diff --git a/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart b/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart new file mode 100644 index 00000000..4a48d48a --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/drone_mission_control_page.dart @@ -0,0 +1,384 @@ +import 'package:flutter/material.dart'; + +/// 无人机任务与航线控制页面 +class DroneMissionControlPage extends StatelessWidget { + const DroneMissionControlPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '无人机任务与航线控制', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.settings, color: Color(0xFF1D2129)), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildMissionInfoCard(), + const SizedBox(height: 12), + _buildRouteMap(), + const SizedBox(height: 12), + _buildWaypointList(), + const SizedBox(height: 16), + ], + ), + ), + _buildActionButtons(), + ], + ), + ); + } + + Widget _buildMissionInfoCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '任务信息', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildInfoRow('任务名称', '逆变器区巡检任务'), + const SizedBox(height: 12), + Row( + children: [ + _buildInfoColumn('任务编号', 'UAV-2025052001'), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF165DFF).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '进行中', + style: TextStyle( + fontSize: 12, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + _buildInfoRow('巡检区域', '逆变器区A区'), + const SizedBox(height: 12), + _buildInfoRow('飞行高度', '80 m'), + const SizedBox(height: 12), + _buildInfoRow('飞行速度', '8.0 m/s'), + const SizedBox(height: 12), + _buildInfoRow('预计时长', '26 min'), + const SizedBox(height: 12), + _buildInfoRow('电量预估', '68% (可飞行 22 min)'), + ], + ), + ); + } + + Widget _buildRouteMap() { + return Container( + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'assets/images/xunjian.png', + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ); + } + + Widget _buildWaypointList() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + '航点列表', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.map, size: 20, color: Color(0xFF86909C)), + onPressed: () {}, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close, size: 20, color: Color(0xFF86909C)), + onPressed: () {}, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ), + const SizedBox(height: 12), + _buildWaypointItem('1', '起飞点', '50m', '拍照'), + const SizedBox(height: 8), + _buildWaypointItem('2', '航点1', '80m', '拍照'), + const SizedBox(height: 8), + _buildWaypointItem('3', '航点2', '80m', '拍照'), + const SizedBox(height: 8), + _buildWaypointItem('4', '航点3', '80m', '拍照'), + const SizedBox(height: 8), + _buildWaypointItem('5', '航点4', '80m', '拍照'), + const SizedBox(height: 8), + _buildWaypointItem('6', '航点5', '80m', '拍照'), + ], + ), + ); + } + + Widget _buildWaypointItem(String number, String name, String height, String action) { + return Row( + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Color(0xFF165DFF), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + number, + style: const TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1D2129), + ), + ), + ), + Text( + 'H: $height', + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 16), + Text( + action, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(width: 8), + const Icon( + Icons.download_outlined, + size: 18, + color: Color(0xFF165DFF), + ), + ], + ); + } + + Widget _buildActionButtons() { + return Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), + decoration: const BoxDecoration( + color: Color(0xFFF5F6F8), + ), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF165DFF), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + '开始巡检', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7D00), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + '暂停任务', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFF4E5969), + side: const BorderSide(color: Color(0xFFC9CDD4)), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '返航降落', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + Widget _buildInfoColumn(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart new file mode 100644 index 00000000..dc48cd07 --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart @@ -0,0 +1,465 @@ +import 'package:flutter/material.dart'; +import 'drone_mission_control_page.dart'; +import 'drone_video_control_page.dart'; + +/// 无人机机场详情页面 +class DroneStationDetailPage extends StatelessWidget { + const DroneStationDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '无人机机场详情', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildAirportStatusCard(), + const SizedBox(height: 12), + _buildDroneStatusCard(context), + const SizedBox(height: 12), + _buildQuickActions(context), + const SizedBox(height: 20), + ], + ), + ); + } + + /// 机场状态卡片 + Widget _buildAirportStatusCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '机场状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'), + const SizedBox(height: 12), + _buildStatusRow('位置', '升压站旁 - 无人机机场'), + const SizedBox(height: 12), + _buildStatusRow('舱门状态', '关闭', status: '正常'), + const SizedBox(height: 12), + _buildStatusRowWithProgress('充电状态', '充电中', '85%'), + const SizedBox(height: 12), + _buildWeatherRow(), + const SizedBox(height: 12), + _buildStatusRow('网络状态', '4G/5G 强'), + ], + ), + ); + } + + /// 无人机状态卡片 + Widget _buildDroneStatusCard(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DroneVideoControlPage(), + ), + ); + }, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + '无人机状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '在线', + style: TextStyle( + fontSize: 12, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Image.asset( + 'assets/images/xunjian.png', + width: 60, + height: 60, + fit: BoxFit.contain, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '无人机 - 01', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + _buildDroneBatteryRow(), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatusRow('飞行模式', '待命'), + const SizedBox(height: 12), + _buildStatusRow('机身状态', '正常'), + const SizedBox(height: 12), + _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), + ], + ), + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActions(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '快捷操作', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.flight_takeoff, + label: '开舱', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.flight, + label: '起飞准备', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.home, + label: '返航', + color: const Color(0xFFFF7D00), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.task, + label: '任务下发', + color: const Color(0xFF165DFF), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DroneMissionControlPage(), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActionButton( + BuildContext context, { + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 24, + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ); + } + + /// 状态行(带状态标签) + Widget _buildStatusRow(String label, String value, {String? status}) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + if (status != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ); + } + + /// 带进度条的状态行 + Widget _buildStatusRowWithProgress(String label, String value, String progress) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Text( + progress, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios, + size: 12, + color: Color(0xFF86909C), + ), + ], + ); + } + + /// 气象条件行 + Widget _buildWeatherRow() { + return Row( + children: [ + const Text( + '气象条件', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Row( + children: [ + const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)), + const SizedBox(width: 4), + const Text( + '晴', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + const Text( + '25°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + const Text( + '东南风 2级', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + ], + ), + ], + ); + } + + /// 无人机电量行 + Widget _buildDroneBatteryRow() { + return Row( + children: [ + const Text( + '电量', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: 0.78, + backgroundColor: const Color(0xFFF2F3F5), + valueColor: const AlwaysStoppedAnimation(Color(0xFF00B42A)), + minHeight: 6, + ), + ), + ), + const SizedBox(width: 8), + const Text( + '78%', + style: TextStyle( + fontSize: 13, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart new file mode 100644 index 00000000..2147c1bd --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/drone_station_status_page.dart @@ -0,0 +1,466 @@ +import 'package:flutter/material.dart'; + +/// 无人机机场与设备状态页面 +class DroneStationStatusPage extends StatelessWidget { + const DroneStationStatusPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + body: CustomScrollView( + slivers: [ + // 顶部导航栏 + SliverAppBar( + backgroundColor: Colors.white, + elevation: 0, + pinned: true, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '无人机机场与设备状态', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.notifications_none, color: Color(0xFF1D2129)), + onPressed: () {}, + ), + ], + ), + // 内容区域 + SliverToBoxAdapter( + child: Column( + children: [ + ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + children: [ + // 机场状态卡片 + _buildAirportStatusCard(), + const SizedBox(height: 12), + // 无人机状态卡片 + _buildDroneStatusCard(), + const SizedBox(height: 12), + // 快捷操作 + _buildQuickActions(), + const SizedBox(height: 20), + ], + ), + const SizedBox(height: 80), // 为底部 Tab 栏留出空间 + ], + ), + ), + ], + ), + ); + } + + /// 机场状态卡片 + Widget _buildAirportStatusCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '机场状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'), + const SizedBox(height: 12), + _buildStatusRow('位置', '升压站旁 - 无人机机场'), + const SizedBox(height: 12), + _buildStatusRow('舱门状态', '关闭', status: '正常'), + const SizedBox(height: 12), + _buildStatusRowWithProgress('充电状态', '充电中', '85%'), + const SizedBox(height: 12), + _buildWeatherRow(), + const SizedBox(height: 12), + _buildStatusRow('网络状态', '4G/5G 强'), + ], + ), + ); + } + + /// 无人机状态卡片 + Widget _buildDroneStatusCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + '无人机状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '在线', + style: TextStyle( + fontSize: 12, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Image.asset( + 'assets/images/xunjian.png', + width: 60, + height: 60, + fit: BoxFit.contain, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '无人机 - 01', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + _buildDroneBatteryRow(), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatusRow('飞行模式', '待命'), + const SizedBox(height: 12), + _buildStatusRow('机身状态', '正常'), + const SizedBox(height: 12), + _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), + ], + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActions() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '快捷操作', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildQuickActionButton( + icon: Icons.flight_takeoff, + label: '开舱', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + icon: Icons.flight, + label: '起飞准备', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + icon: Icons.home, + label: '返航', + color: const Color(0xFFFF7D00), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + icon: Icons.task, + label: '任务下发', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + ], + ), + ], + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActionButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 24, + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ); + } + + /// 状态行(带状态标签) + Widget _buildStatusRow(String label, String value, {String? status}) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + if (status != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ); + } + + /// 带进度条的状态行 + Widget _buildStatusRowWithProgress(String label, String value, String progress) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Text( + progress, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios, + size: 12, + color: Color(0xFF86909C), + ), + ], + ); + } + + /// 气象条件行 + Widget _buildWeatherRow() { + return Row( + children: [ + const Text( + '气象条件', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Row( + children: [ + const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)), + const SizedBox(width: 4), + const Text( + '晴', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + const Text( + '25°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + const Text( + '东南风 2级', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + ], + ), + ], + ); + } + + /// 无人机电量行 + Widget _buildDroneBatteryRow() { + return Row( + children: [ + const Text( + '电量', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: 0.78, + backgroundColor: const Color(0xFFF2F3F5), + valueColor: const AlwaysStoppedAnimation(Color(0xFF00B42A)), + minHeight: 6, + ), + ), + ), + const SizedBox(width: 8), + const Text( + '78%', + style: TextStyle( + fontSize: 13, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart new file mode 100644 index 00000000..f2ed7b1f --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart @@ -0,0 +1,456 @@ +import 'package:flutter/material.dart'; + +/// 无人机视频回传/远程控制页面 +class DroneVideoControlPage extends StatefulWidget { + const DroneVideoControlPage({super.key}); + + @override + State createState() => _DroneVideoControlPageState(); +} + +class _DroneVideoControlPageState extends State { + int _selectedTab = 0; // 0: 视频画面, 1: 热成像, 2: 抓拍记录 + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '无人机视频回传 / 远程控制', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.settings, color: Color(0xFF1D2129)), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + // Tab切换 + _buildTabBar(), + // 内容区域 + Expanded( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + // 视频画面 + _buildVideoPlayer(), + const SizedBox(height: 12), + // 飞行数据 + _buildFlightData(), + const SizedBox(height: 12), + // AI识别结果 + _buildAIResults(), + const SizedBox(height: 12), + // 地图和摇杆 + _buildMapAndJoystick(), + const SizedBox(height: 16), + // 底部工具栏 + _buildBottomToolbar(), + ], + ), + ), + ], + ), + ); + } + + /// Tab切换栏 + Widget _buildTabBar() { + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + _buildTabItem('视频画面', 0), + const SizedBox(width: 8), + _buildTabItem('热成像', 1), + const SizedBox(width: 8), + _buildTabItem('抓拍记录', 2), + ], + ), + ); + } + + Widget _buildTabItem(String label, int index) { + final isSelected = _selectedTab == index; + return Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _selectedTab = index; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFF2F3F5), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isSelected ? Colors.white : const Color(0xFF4E5969), + ), + ), + ), + ), + ); + } + + /// 视频播放器 + Widget _buildVideoPlayer() { + return Stack( + children: [ + Container( + height: 240, + decoration: BoxDecoration( + color: const Color(0xFF1D2129), + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'assets/images/xunjian.png', + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ), + // 录制标识 + Positioned( + top: 12, + left: 12, + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Color(0xFFF53F3F), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + const Text( + 'REC 00:12:36', + style: TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + // 缩放标识 + Positioned( + bottom: 12, + right: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '1.0x', + style: TextStyle( + fontSize: 12, + color: Colors.white, + ), + ), + ), + ), + ], + ); + } + + /// 飞行数据 + Widget _buildFlightData() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + _buildDataItem('高度', '78 m'), + const SizedBox(width: 16), + _buildDataItem('速度', '7.5 m/s'), + const SizedBox(width: 16), + _buildDataItem('距离', '1.26 km'), + const SizedBox(width: 16), + _buildDataItem('云台角度', '-15° / 0°'), + ], + ), + ); + } + + Widget _buildDataItem(String label, String value) { + return Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF86909C), + ), + ), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + ], + ), + ); + } + + /// AI识别结果 + Widget _buildAIResults() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'AI 识别结果', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 12), + _buildAIResultItem('热斑疑似', '3 处'), + const SizedBox(height: 8), + _buildAIResultItem('组件异常', '2 处'), + ], + ), + ); + } + + Widget _buildAIResultItem(String label, String count) { + return GestureDetector( + onTap: () {}, + child: Row( + children: [ + const Icon( + Icons.warning_amber_rounded, + color: Color(0xFFFF7D00), + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1D2129), + ), + ), + ), + Text( + count, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF4E5969), + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.chevron_right, + color: Color(0xFF86909C), + size: 20, + ), + ], + ), + ); + } + + /// 地图和摇杆 + Widget _buildMapAndJoystick() { + return Row( + children: [ + // 地图 + Expanded( + child: Container( + height: 160, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset( + 'assets/images/xunjian.png', + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ), + ), + const SizedBox(width: 12), + // 摇杆控制器 + Expanded( + child: Container( + height: 160, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + // 中心圆点 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFC9CDD4), + shape: BoxShape.circle, + ), + ), + // 上 + Positioned( + top: 16, + child: IconButton( + icon: const Icon(Icons.arrow_drop_up, size: 32, color: Color(0xFF4E5969)), + onPressed: () {}, + ), + ), + // 下 + Positioned( + bottom: 16, + child: IconButton( + icon: const Icon(Icons.arrow_drop_down, size: 32, color: Color(0xFF4E5969)), + onPressed: () {}, + ), + ), + // 左 + Positioned( + left: 16, + child: IconButton( + icon: const Icon(Icons.arrow_left, size: 32, color: Color(0xFF4E5969)), + onPressed: () {}, + ), + ), + // 右 + Positioned( + right: 16, + child: IconButton( + icon: const Icon(Icons.arrow_right, size: 32, color: Color(0xFF4E5969)), + onPressed: () {}, + ), + ), + ], + ), + ), + ), + ], + ); + } + + /// 底部工具栏 + Widget _buildBottomToolbar() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildToolItem(Icons.camera_alt_outlined, '抓拍'), + _buildToolItem(Icons.videocam_outlined, '录像'), + _buildToolItem(Icons.zoom_in_outlined, '变焦'), + _buildToolItem(Icons.lightbulb_outline, '补光灯'), + ], + ), + ); + } + + Widget _buildToolItem(IconData icon, String label) { + return GestureDetector( + onTap: () {}, + child: Column( + children: [ + Icon(icon, size: 28, color: const Color(0xFF4E5969)), + const SizedBox(height: 4), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/robot_control_page.dart b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart new file mode 100644 index 00000000..d0873cf2 --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import '../widgets/robot_header_card.dart'; +import '../widgets/robot_status_bar.dart'; +import '../widgets/robot_control_panel.dart'; +import '../widgets/robot_checklist.dart'; +import '../widgets/robot_task_info.dart'; +import '../widgets/robot_action_buttons.dart'; + +/// 机器人控制详情页 +class RobotControlPage extends StatelessWidget { + final Map robot; + + const RobotControlPage({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: Text( + '${robot['type']}控制', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.edit_outlined, color: Color(0xFF1D2129)), + onPressed: () {}, + ), + ], + ), + body: Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + children: [ + RobotHeaderCard(robot: robot), + const SizedBox(height: 8), + RobotStatusBar(robot: robot), + const SizedBox(height: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + RobotControlPanel(robot: robot), + const SizedBox(height: 8), + RobotTaskInfo(robot: robot), + ], + ), + ), + const SizedBox(width: 8), + Expanded(child: RobotChecklist(robot: robot)), + ], + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 20), + child: RobotActionButtons(robot: robot), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart new file mode 100644 index 00000000..f9bc4bb5 --- /dev/null +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; +import '../widgets/robot_item_card.dart'; +import 'robot_control_page.dart'; +import 'cleaning_weeding_robot_task_page.dart'; + +/// 机器人列表页面 +class RobotListPage extends StatefulWidget { + const RobotListPage({super.key}); + + @override + State createState() => _RobotListPageState(); +} + +class _RobotListPageState extends State { + String? _selectedType; // 选中的类型 + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.only(bottom: 16), + children: [ + _buildStatsCard(), + _buildQuickActions(), + _buildCurrentTask(), + ..._buildRobotList(), + ], + ); + } + + Widget _buildStatsCard() { + return Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 12), + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() { + _selectedType = _selectedType == '巡检机器人' ? null : '巡检机器人'; + }), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + gradient: _selectedType == '巡检机器人' + ? const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFE8F4FF), + Color(0xFFFFFFFF), + ], + ) + : null, + color: _selectedType != '巡检机器人' ? Colors.transparent : null, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '巡检机器人', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _selectedType == '巡检机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF1D2129), + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/xunjian.png', width: 48, height: 48), + const SizedBox(width: 8), + const Text('12', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))), + ], + ), + const SizedBox(height: 8), + Text( + '在线9台', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: _selectedType == '巡检机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF00B42A), + ), + ), + ], + ), + ), + ), + Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)), + Expanded( + child: GestureDetector( + onTap: () => setState(() { + _selectedType = _selectedType == '清洗机器人' ? null : '清洗机器人'; + }), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + gradient: _selectedType == '清洗机器人' + ? const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFE8F4FF), + Color(0xFFFFFFFF), + ], + ) + : null, + color: _selectedType != '清洗机器人' ? Colors.transparent : null, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '清洗机器人', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _selectedType == '清洗机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF1D2129), + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/qinxi.png', width: 48, height: 48), + const SizedBox(width: 8), + const Text('8', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF165DFF))), + ], + ), + const SizedBox(height: 8), + Text( + '在线6台', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: _selectedType == '清洗机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF00B42A), + ), + ), + ], + ), + ), + ), + Container(width: 1, height: 100, color: const Color(0xFFE5E6EB)), + Expanded( + child: GestureDetector( + onTap: () => setState(() { + _selectedType = _selectedType == '除草机器人' ? null : '除草机器人'; + }), + child: Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + gradient: _selectedType == '除草机器人' + ? const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFFE8F4FF), + Color(0xFFFFFFFF), + ], + ) + : null, + color: _selectedType != '除草机器人' ? Colors.transparent : null, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '除草机器人', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _selectedType == '除草机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF1D2129), + ), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset('assets/images/chucao.png', width: 48, height: 48), + const SizedBox(width: 8), + Text( + '6', + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: _selectedType == '除草机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF00B42A), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + '在线4台', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: _selectedType == '除草机器人' + ? const Color(0xFF165DFF) + : const Color(0xFF00B42A), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildQuickActions() { + return Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 12), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))], + ), + child: Row( + children: [ + _buildStatusItem(value: '19', label: '在线设备', valueColor: const Color(0xFF165DFF)), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem(value: '8', label: '作业中', valueColor: const Color(0xFF00B42A)), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem(value: '12', label: '待命', valueColor: const Color(0xFFFF7D00)), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem(value: '2', label: '故障数', valueColor: const Color(0xFFF53F3F)), + ], + ), + ); + } + + Widget _buildStatusItem({required String value, required String label, required Color valueColor}) { + return Expanded( + child: Column( + children: [ + Text(value, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: valueColor)), + const SizedBox(height: 4), + Text(label, style: const TextStyle(fontSize: 13, color: Color(0xFF4E5969))), + ], + ), + ); + } + + Widget _buildCurrentTask() { + return Container( + margin: const EdgeInsets.fromLTRB(16, 0, 16, 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('当前任务', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))), + const SizedBox(height: 12), + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset('assets/images/xunjian.png', width: 80, height: 80, fit: BoxFit.cover), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Expanded(child: Text('巡检任务-逆变器区巡检', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xFF1D2129)))), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration(color: const Color(0xFF165DFF).withOpacity(0.1), borderRadius: BorderRadius.circular(4)), + child: const Text('进行中', style: TextStyle(fontSize: 11, color: Color(0xFF165DFF), fontWeight: FontWeight.w500)), + ), + ], + ), + const SizedBox(height: 8), + _buildTaskInfo('机器人:', '巡检机器人-01'), + _buildTaskInfo('区域:', '逆变器区A区'), + _buildTaskInfo('进度:', '62%'), + _buildTaskInfo('预计完成:', '12:30'), + ], + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTaskInfo(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: RichText( + text: TextSpan( + children: [ + TextSpan(text: label, style: const TextStyle(fontSize: 13, color: Color(0xFF86909C))), + TextSpan(text: value, style: const TextStyle(fontSize: 13, color: Color(0xFF1D2129))), + ], + ), + ), + ); + } + + List _buildRobotList() { + final allRobots = [ + {'name': '巡检机器人-01', 'id': 'RB001', 'type': '巡检机器人', 'status': '在线', 'battery': 85.0, 'task': '逆变器区域巡检'}, + {'name': '巡检机器人-02', 'id': 'RB002', 'type': '巡检机器人', 'status': '在线', 'battery': 62.0, 'task': '组件区域巡检'}, + {'name': '巡检机器人-03', 'id': 'RB003', 'type': '巡检机器人', 'status': '离线', 'battery': 15.0, 'task': '充电中'}, + {'name': '清洗机器人-01', 'id': 'RB004', 'type': '清洗机器人', 'status': '在线', 'battery': 78.0, 'task': '组件清洗作业'}, + {'name': '清洗机器人-02', 'id': 'RB005', 'type': '清洗机器人', 'status': '在线', 'battery': 91.0, 'task': '待机中'}, + {'name': '除草机器人-01', 'id': 'RB006', 'type': '除草机器人', 'status': '在线', 'battery': 55.0, 'task': '光伏板除草'}, + ]; + + final filteredRobots = _selectedType == null ? allRobots : allRobots.where((robot) => robot['type'] == _selectedType).toList(); + + if (filteredRobots.isEmpty) { + return [const Center(child: Padding(padding: EdgeInsets.all(32), child: Text('暂无机器人数据', style: TextStyle(fontSize: 14, color: Color(0xFF86909C)))))]; + } + + return filteredRobots.map((robot) => RobotItemCard( + name: robot['name'] as String, + id: robot['id'] as String, + type: robot['type'] as String, + status: robot['status'] as String, + battery: robot['battery'] as double, + task: robot['task'] as String, + onTap: () { + // 巡检机器人使用原来的控制页面 + // 清洗/除草机器人使用新的任务详情页面 + if (robot['type'] == '巡检机器人') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RobotControlPage(robot: robot), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CleaningWeedingRobotTaskPage(robot: robot), + ), + ); + } + }, + )).toList(); + } +} diff --git a/lib/features/v2/device_list/presentation/presentation.dart b/lib/features/v2/device_list/presentation/presentation.dart new file mode 100644 index 00000000..4f163a26 --- /dev/null +++ b/lib/features/v2/device_list/presentation/presentation.dart @@ -0,0 +1,4 @@ +// 表现层聚合导出 +export 'bloc/bloc.dart'; +export 'pages/device_status_page.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/features/v2/device_list/presentation/providers/providers.dart b/lib/features/v2/device_list/presentation/providers/providers.dart new file mode 100644 index 00000000..624cb7f6 --- /dev/null +++ b/lib/features/v2/device_list/presentation/providers/providers.dart @@ -0,0 +1,2 @@ +// Providers 层(用于依赖注入) +export '../bloc/bloc.dart'; \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart b/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart new file mode 100644 index 00000000..df651c41 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/device_bottom_nav_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +/// 底部导航栏组件 +class DeviceBottomNavBar extends StatelessWidget { + final int currentIndex; + final Function(int) onTap; + + const DeviceBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border( + top: BorderSide( + color: const Color(0xFFF2F3F5), + width: 0.5, + ), + ), + ), + child: SafeArea( + child: SizedBox( + height: 49.0, + child: Row( + children: [ + _buildNavItem( + icon: Icons.home_outlined, + label: '首页', + isSelected: currentIndex == 0, + onTap: () => onTap(0), + ), + _buildNavItem( + icon: Icons.devices, + label: '设备', + isSelected: currentIndex == 1, + onTap: () => onTap(1), + ), + _buildNavItem( + icon: Icons.notifications_outlined, + label: '告警', + isSelected: currentIndex == 2, + onTap: () => onTap(2), + ), + _buildNavItem( + icon: Icons.assignment_outlined, + label: '工单', + isSelected: currentIndex == 3, + onTap: () => onTap(3), + ), + _buildNavItem( + icon: Icons.person_outlined, + label: '我的', + isSelected: currentIndex == 4, + onTap: () => onTap(4), + ), + ], + ), + ), + ), + ); + } + + Widget _buildNavItem({ + required IconData icon, + required String label, + required bool isSelected, + required VoidCallback onTap, + }) { + return Expanded( + child: InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: isSelected ? const Color(0xFF004CFD) : const Color(0xFF86909C), + size: 24, + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 11, + color: isSelected ? const Color(0xFF004FFF) : const Color(0xFF86909C), + fontWeight: isSelected ? FontWeight.w500 : FontWeight.normal, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart new file mode 100644 index 00000000..3b2e3c68 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/device_item_widget.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import '../../domain/entities/device_entity.dart'; + +/// 设备列表项 Widget +class DeviceItemWidget extends StatelessWidget { + final DeviceEntity device; + final VoidCallback onTap; + + const DeviceItemWidget({ + super.key, + required this.device, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:设备信息 + Row( + children: [ + // 设备图标 + _buildDeviceIcon(device.type), + const SizedBox(width: 12), + // 设备名称和 ID + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + device.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 4), + Text( + 'ID: ${device.deviceId}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF86909C), + ), + ), + ], + ), + ), + // 状态标签 + _buildStatusTag(device.status), + ], + ), + const SizedBox(height: 12), + // 第二行:设备数据 + _buildDeviceData(device), + ], + ), + ), + ); + } + + Widget _buildDeviceIcon(String type) { + IconData icon; + Color color; + + switch (type) { + case '机器人': + icon = Icons.smart_toy; + color = const Color(0xFF165DFF); + break; + case '逆变器': + icon = Icons.power; + color = const Color(0xFF165DFF); + break; + case '汇流箱': + icon = Icons.electrical_services; + color = const Color(0xFF722ED1); + break; + case '组件': + icon = Icons.solar_power; + color = const Color(0xFF13C2C2); + break; + case '监控': + icon = Icons.videocam; + color = const Color(0xFFEB2F96); + break; + default: + icon = Icons.devices; + color = const Color(0xFF165DFF); + } + + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: color, + size: 28, + ), + ); + } + + Widget _buildStatusTag(String status) { + Color bgColor; + Color textColor; + + switch (status) { + case '在线': + bgColor = const Color(0xFFF0F9FF); + textColor = const Color(0xFF00B42A); + break; + case '离线': + bgColor = const Color(0xFFF5F0FF); + textColor = const Color(0xFFF53F3F); + break; + case '异常': + bgColor = const Color(0xFFFFF7E6); + textColor = const Color(0xFFFF7D00); + break; + default: + bgColor = const Color(0xFFF5F5F5); + textColor = const Color(0xFF86909C); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12.0), + ), + child: Text( + status, + style: TextStyle( + fontSize: 12, + color: textColor, + ), + ), + ); + } + + Widget _buildDeviceData(DeviceEntity device) { + final dataItems = []; + + // 根据设备类型显示不同的数据 + if (device.type == '逆变器') { + dataItems.add(_buildDataItem('${device.power}', 'kW | 功率')); + dataItems.add(const SizedBox(width: 16)); + dataItems.add(_buildDataItem('${device.todayEnergy}', 'kWh | 今日发电')); + } else if (device.type == '汇流箱') { + if (device.current != null) { + dataItems.add(_buildDataItem('${device.current}', 'A | 电流')); + dataItems.add(const SizedBox(width: 16)); + } + if (device.temperature != null) { + dataItems.add(_buildDataItem('${device.temperature}', '℃ | 温度')); + } + } else if (device.type == '组件') { + dataItems.add(_buildDataItem('${device.power}', 'kW | 功率')); + dataItems.add(const SizedBox(width: 16)); + if (device.radiation != null) { + dataItems.add(_buildDataItem('${device.radiation}', 'W/m² | 辐照度')); + } + } else { + // 监控等其他类型 + dataItems.add(_buildDataItem('--', '数据')); + } + + return Row( + children: dataItems, + ); + } + + Widget _buildDataItem(String value, String unit) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 4), + Text( + unit, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart b/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart new file mode 100644 index 00000000..2b9b8d7e --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/drone_station_item_card.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; + +/// 无人机机场列表项卡片 +class DroneStationItemCard extends StatelessWidget { + final String name; + final String id; + final VoidCallback onTap; + + const DroneStationItemCard({ + super.key, + required this.name, + required this.id, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '机场状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'), + const SizedBox(height: 12), + _buildStatusRow('位置', '升压站旁 - 无人机机场'), + const SizedBox(height: 12), + _buildStatusRow('舱门状态', '关闭', status: '正常'), + const SizedBox(height: 12), + _buildStatusRowWithProgress('充电状态', '充电中', '85%'), + const SizedBox(height: 12), + _buildWeatherRow(), + const SizedBox(height: 12), + _buildStatusRow('网络状态', '4G/5G 强'), + ], + ), + ), + ); + } + + /// 状态行(带状态标签) + Widget _buildStatusRow(String label, String value, {String? status}) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + if (status != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ); + } + + /// 带进度条的状态行 + Widget _buildStatusRowWithProgress(String label, String value, String progress) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Text( + progress, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios, + size: 12, + color: Color(0xFF86909C), + ), + ], + ); + } + + /// 气象条件行 + Widget _buildWeatherRow() { + return Row( + children: [ + const Text( + '气象条件', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Row( + children: [ + const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)), + const SizedBox(width: 4), + const Text( + '晴', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + const Text( + '25°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + const Text( + '东南风 2级', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/drone_station_status_widget.dart b/lib/features/v2/device_list/presentation/widgets/drone_station_status_widget.dart new file mode 100644 index 00000000..d2ae2cf2 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/drone_station_status_widget.dart @@ -0,0 +1,436 @@ +import 'package:flutter/material.dart'; +import '../pages/drone_mission_control_page.dart'; + +/// 无人机机场与设备状态组件 +class DroneStationStatusWidget extends StatelessWidget { + const DroneStationStatusWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // 机场状态卡片 + _buildAirportStatusCard(), + const SizedBox(height: 12), + // 无人机状态卡片 + _buildDroneStatusCard(), + const SizedBox(height: 12), + // 快捷操作 + _buildQuickActions(context), + const SizedBox(height: 20), + ], + ); + } + + /// 机场状态卡片 + Widget _buildAirportStatusCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '机场状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + _buildStatusRow('机场编号', 'AIRPORT-01', status: '正常'), + const SizedBox(height: 12), + _buildStatusRow('位置', '升压站旁 - 无人机机场'), + const SizedBox(height: 12), + _buildStatusRow('舱门状态', '关闭', status: '正常'), + const SizedBox(height: 12), + _buildStatusRowWithProgress('充电状态', '充电中', '85%'), + const SizedBox(height: 12), + _buildWeatherRow(), + const SizedBox(height: 12), + _buildStatusRow('网络状态', '4G/5G 强'), + ], + ), + ); + } + + /// 无人机状态卡片 + Widget _buildDroneStatusCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text( + '无人机状态', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '在线', + style: TextStyle( + fontSize: 12, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Image.asset( + 'assets/images/xunjian.png', + width: 60, + height: 60, + fit: BoxFit.contain, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '无人机 - 01', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + _buildDroneBatteryRow(), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + _buildStatusRow('飞行模式', '待命'), + const SizedBox(height: 12), + _buildStatusRow('机身状态', '正常'), + const SizedBox(height: 12), + _buildStatusRow('上次任务', '逆变器区巡检(05-20 10:30)'), + ], + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActions(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '快捷操作', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.flight_takeoff, + label: '开舱', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.flight, + label: '起飞准备', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.home, + label: '返航', + color: const Color(0xFFFF7D00), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionButton( + context, + icon: Icons.task, + label: '任务下发', + color: const Color(0xFF165DFF), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DroneMissionControlPage(), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ); + } + + /// 快捷操作按钮 + Widget _buildQuickActionButton( + BuildContext context, { + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + color: color, + size: 24, + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ); + } + + /// 状态行(带状态标签) + Widget _buildStatusRow(String label, String value, {String? status}) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + if (status != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + status, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ], + ); + } + + /// 带进度条的状态行 + Widget _buildStatusRowWithProgress(String label, String value, String progress) { + return Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Text( + value, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + Text( + progress, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios, + size: 12, + color: Color(0xFF86909C), + ), + ], + ); + } + + /// 气象条件行 + Widget _buildWeatherRow() { + return Row( + children: [ + const Text( + '气象条件', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const Spacer(), + Row( + children: [ + const Icon(Icons.wb_sunny, size: 16, color: Color(0xFFFF7D00)), + const SizedBox(width: 4), + const Text( + '晴', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 8), + const Text( + '25°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + const Text( + '东南风 2级', + style: TextStyle( + fontSize: 13, + color: Color(0xFF1D2129), + ), + ), + ], + ), + ], + ); + } + + /// 无人机电量行 + Widget _buildDroneBatteryRow() { + return Row( + children: [ + const Text( + '电量', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: LinearProgressIndicator( + value: 0.78, + backgroundColor: const Color(0xFFF2F3F5), + valueColor: const AlwaysStoppedAnimation(Color(0xFF00B42A)), + minHeight: 6, + ), + ), + ), + const SizedBox(width: 8), + const Text( + '78%', + style: TextStyle( + fontSize: 13, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart b/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart new file mode 100644 index 00000000..6e551fb3 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_action_buttons.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +/// 底部操作按钮 +class RobotActionButtons extends StatelessWidget { + final Map robot; + + const RobotActionButtons({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: _buildActionButton( + label: '开始', + color: const Color(0xFF165DFF), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildActionButton( + label: '暂停', + color: const Color(0xFFFF7D00), + onTap: () {}, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildActionButton( + label: '返航', + color: Colors.white, + borderColor: const Color(0xFFC9CDD4), + textColor: const Color(0xFF4E5969), + onTap: () {}, + ), + ), + ], + ); + } + + Widget _buildActionButton({ + required String label, + required Color color, + required VoidCallback onTap, + Color? borderColor, + Color? textColor, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + border: borderColor != null + ? Border.all(color: borderColor, width: 1) + : null, + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: textColor ?? Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart b/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart new file mode 100644 index 00000000..3ee8aff7 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_checklist.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +/// 检查清单 +class RobotChecklist extends StatelessWidget { + final Map robot; + + const RobotChecklist({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '检查清单', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + _buildCheckItem('逆变器', true), + _buildCheckItem('汇流箱', true), + _buildCheckItem('支架结构', true), + _buildCheckItem('线缆通道', false, isWarning: true), + _buildCheckItem('环境与杂物', false), + ], + ), + ); + } + + Widget _buildCheckItem(String label, bool isDone, {bool isWarning = false}) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1D2129), + ), + ), + ), + if (isDone) + const Icon( + Icons.check_circle, + color: Color(0xFF00B42A), + size: 20, + ) + else if (isWarning) + const Icon( + Icons.error_outline, + color: Color(0xFFFF7D00), + size: 20, + ) + else + const Icon( + Icons.radio_button_unchecked, + color: Color(0xFFC9CDD4), + size: 20, + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart new file mode 100644 index 00000000..541f5aed --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; + +/// 机器人控制面板 +class RobotControlPanel extends StatelessWidget { + final Map robot; + + const RobotControlPanel({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '控制面板', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 12), + // 方向控制盘 + Center( + child: SizedBox( + width: 120, + height: 120, + child: Stack( + alignment: Alignment.center, + children: [ + // 背景圆 + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + color: const Color(0xFFF5F6F8), + shape: BoxShape.circle, + ), + ), + // 上 + Positioned( + top: 8, + child: _buildDirectionButton(Icons.arrow_upward, () {}), + ), + // 下 + Positioned( + bottom: 8, + child: _buildDirectionButton(Icons.arrow_downward, () {}), + ), + // 左 + Positioned( + left: 8, + child: _buildDirectionButton(Icons.arrow_back, () {}), + ), + // 右 + Positioned( + right: 8, + child: _buildDirectionButton(Icons.arrow_forward, () {}), + ), + // 中心按钮 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF4080FF), + Color(0xFF165DFF), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: const Color(0xFF165DFF).withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 12), + // 速度档位 + const Text( + '速度档位', + style: TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildSpeedButton('低速', false, () {}), + ), + const SizedBox(width: 12), + Expanded( + child: _buildSpeedButton('中速', true, () {}), + ), + const SizedBox(width: 12), + Expanded( + child: _buildSpeedButton('高速', false, () {}), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDirectionButton(IconData icon, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon(icon, color: const Color(0xFF1D2129), size: 20), + ), + ); + } + + Widget _buildSpeedButton(String label, bool isSelected, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFF5F6F8), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: isSelected ? Colors.white : const Color(0xFF4E5969), + ), + ), + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart new file mode 100644 index 00000000..75b3a4b0 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +/// 机器人顶部信息卡片 +class RobotHeaderCard extends StatelessWidget { + final Map robot; + + const RobotHeaderCard({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + robot['name'] as String, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF00B42A).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '在线', + style: TextStyle( + fontSize: 12, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ), + const Spacer(), + // 信号强度 + Row( + children: [ + Icon( + Icons.signal_cellular_alt, + size: 16, + color: const Color(0xFF00B42A), + ), + const SizedBox(width: 4), + const Text( + '强', + style: TextStyle( + fontSize: 12, + color: Color(0xFF00B42A), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(width: 12), + // 设置图标 + const Icon(Icons.settings, size: 20, color: Color(0xFF86909C)), + ], + ), + const SizedBox(height: 12), + // 机器人图片 + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + 'assets/images/xunjian.png', + height: 180, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart new file mode 100644 index 00000000..a6706950 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; + +/// 机器人卡片组件 +class RobotItemCard extends StatelessWidget { + final String name; + final String id; + final String type; // 机器人类型:巡检机器人、清洗机器人、除草机器人 + final String status; + final double battery; + final String task; + final VoidCallback onTap; + + const RobotItemCard({ + super.key, + required this.name, + required this.id, + required this.type, + required this.status, + required this.battery, + required this.task, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:图标、名称、状态 + Row( + children: [ + // 机器人图标 + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFF165DFF).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + _getRobotImage(), + width: 48, + height: 48, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 12), + // 名称和ID + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 4), + Text( + 'ID: $id', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF86909C), + ), + ), + ], + ), + ), + // 状态标签 + _buildStatusTag(status), + ], + ), + const SizedBox(height: 12), + // 第二行:电量和任务 + Row( + children: [ + // 电量 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + battery > 50 + ? Icons.battery_full + : battery > 20 + ? Icons.battery_std + : Icons.battery_alert, + size: 16, + color: battery > 50 + ? const Color(0xFF00B42A) + : battery > 20 + ? const Color(0xFFFF7D00) + : const Color(0xFFF53F3F), + ), + const SizedBox(width: 4), + Text( + '${battery.toInt()}%', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: battery > 50 + ? const Color(0xFF00B42A) + : battery > 20 + ? const Color(0xFFFF7D00) + : const Color(0xFFF53F3F), + ), + ), + ], + ), + const SizedBox(height: 4), + const Text( + '电量', + style: TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + // 任务状态 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + task, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1D2129), + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + const Text( + '当前任务', + style: TextStyle( + fontSize: 12, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStatusTag(String status) { + Color bgColor; + Color textColor; + + switch (status) { + case '在线': + bgColor = const Color(0xFFF0F9FF); + textColor = const Color(0xFF00B42A); + break; + case '离线': + bgColor = const Color(0xFFF5F0FF); + textColor = const Color(0xFFF53F3F); + break; + case '异常': + bgColor = const Color(0xFFFFF7E6); + textColor = const Color(0xFFFF7D00); + break; + default: + bgColor = const Color(0xFFF5F5F5); + textColor = const Color(0xFF86909C); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + status, + style: TextStyle( + fontSize: 12, + color: textColor, + ), + ), + ); + } + + String _getRobotImage() { + switch (type) { + case '巡检机器人': + return 'assets/images/xunjian.png'; + case '清洗机器人': + return 'assets/images/qinxi.png'; + case '除草机器人': + return 'assets/images/chucao.png'; + default: + return 'assets/images/xunjian.png'; + } + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart b/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart new file mode 100644 index 00000000..b0a8d8d1 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_status_bar.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +/// 机器人状态栏 +class RobotStatusBar extends StatelessWidget { + final Map robot; + + const RobotStatusBar({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + _buildStatusItem( + label: '速度', + value: '1.2', + unit: 'm/s', + ), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem( + label: '里程', + value: '2.36', + unit: 'km', + ), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem( + label: '电量', + value: '82', + unit: '%', + valueColor: const Color(0xFF00B42A), + ), + Container(width: 1, height: 40, color: const Color(0xFFE5E6EB)), + _buildStatusItem( + label: '信号', + value: '强', + unit: '', + valueColor: const Color(0xFF00B42A), + ), + ], + ), + ); + } + + Widget _buildStatusItem({ + required String label, + required String value, + required String unit, + Color? valueColor, + }) { + return Expanded( + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: valueColor ?? const Color(0xFF1D2129), + ), + ), + if (unit.isNotEmpty) + Text( + unit, + style: TextStyle( + fontSize: 12, + color: valueColor ?? const Color(0xFF1D2129), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart new file mode 100644 index 00000000..e17dd9c0 --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; + +/// 当前任务信息 +class RobotTaskInfo extends StatelessWidget { + final Map robot; + + const RobotTaskInfo({super.key, required this.robot}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '当前任务', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: Text( + robot['task'] ?? '逆变器区巡检', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Color(0xFF1D2129), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: const Color(0xFF165DFF).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '进行中', + style: TextStyle( + fontSize: 11, + color: Color(0xFF165DFF), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/device_list/presentation/widgets/widgets.dart b/lib/features/v2/device_list/presentation/widgets/widgets.dart new file mode 100644 index 00000000..1c07b10e --- /dev/null +++ b/lib/features/v2/device_list/presentation/widgets/widgets.dart @@ -0,0 +1,3 @@ +// Widgets 层 +export 'device_bottom_nav_bar.dart'; +export 'device_item_widget.dart'; \ No newline at end of file diff --git a/lib/features/v2/home/data/datasources/home_remote_datasource.dart b/lib/features/v2/home/data/datasources/home_remote_datasource.dart index 9e0a20be..b9f2947d 100644 --- a/lib/features/v2/home/data/datasources/home_remote_datasource.dart +++ b/lib/features/v2/home/data/datasources/home_remote_datasource.dart @@ -58,6 +58,7 @@ class HomeRemoteDataSourceImpl implements HomeRemoteDataSource { plantOverview: const PlantOverview( installedCapacity: 1.26, commissionDate: '2024-05-18', + backgroundImage: 'assets/images/dianzhan.png', ), ); } diff --git a/lib/features/v2/home/presentation/pages/home_v2_page.dart b/lib/features/v2/home/presentation/pages/home_v2_page.dart index 78200a4d..668f8821 100644 --- a/lib/features/v2/home/presentation/pages/home_v2_page.dart +++ b/lib/features/v2/home/presentation/pages/home_v2_page.dart @@ -19,12 +19,18 @@ class HomeV2Page extends StatefulWidget { class _HomeV2PageState extends State { late HomeV2Bloc _bloc; + String _selectedPlant = '示例光伏电站'; + final List _plantList = [ + '示例光伏电站', + '一号光伏电站', + '二号光伏电站', + '三号光伏电站', + ]; @override void initState() { super.initState(); _bloc = context.read(); - _bloc.add(const HomeV2LoadData()); } @override @@ -33,12 +39,6 @@ class _HomeV2PageState extends State { backgroundColor: const Color(0xFFF5F7FA), body: BlocBuilder( builder: (context, state) { - if (state is HomeV2Loading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - if (state is HomeV2Error) { return Center( child: Column( @@ -68,86 +68,40 @@ class _HomeV2PageState extends State { pinned: true, backgroundColor: Colors.white, elevation: 0, + automaticallyImplyLeading: false, flexibleSpace: FlexibleSpaceBar( background: Container( - padding: const EdgeInsets.fromLTRB(16, 40, 16, 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.fromLTRB(16, 40, 16, 16), + color: Colors.white, + child: Row( children: [ - Row( - children: [ - const Text( - '示例光伏电站', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF1D2129), - ), + Expanded( + child: InkWell( + onTap: _showPlantSelector, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + _selectedPlant, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)), + ], ), - const SizedBox(width: 4), - const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)), - const Spacer(), - IconButton( - icon: const Icon(Icons.sentiment_satisfied_alt_outlined, size: 24), - onPressed: () => print('点击表情'), - ), - ], + ), ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFF2F3F5), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)), - const SizedBox(width: 6), - const Text( - '多云 28°C', - style: TextStyle( - fontSize: 13, - color: Color(0xFF4E5969), - ), - ), - ], - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFFFFF0F0), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)), - const SizedBox(width: 6), - const Expanded( - child: Text( - '紧急告警', - style: TextStyle( - fontSize: 13, - color: Color(0xFFF53F3F), - ), - ), - ), - const SizedBox(width: 4), - const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)), - ], - ), - ), - ), - ], + IconButton( + icon: const Icon(Icons.qr_code_scanner, size: 24, color: Color(0xFF1D2129)), + onPressed: () => print('点击扫码'), ), ], ), @@ -155,25 +109,83 @@ class _HomeV2PageState extends State { ), ), SliverPadding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 40), sliver: SliverList( delegate: SliverChildListDelegate([ + const SizedBox(height: 16), + // 天气和告警 + Row( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF2F3F5), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)), + const SizedBox(width: 6), + const Text( + '多云 28°C', + style: TextStyle( + fontSize: 13, + color: Color(0xFF4E5969), + ), + ), + ], + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFFFF0F0), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)), + const SizedBox(width: 6), + const Expanded( + child: Text( + '紧急告警', + style: TextStyle( + fontSize: 13, + color: Color(0xFFF53F3F), + ), + ), + ), + const SizedBox(width: 4), + const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 16), PowerCard(data: state.homeData), - const SizedBox(height: 12), + const SizedBox(height: 10), StatsGrid(data: state.homeData), - const SizedBox(height: 12), + const SizedBox(height: 10), PlantOverviewCard(overview: state.homeData.plantOverview), - const SizedBox(height: 12), + const SizedBox(height: 10), WorkOrderCard(stats: state.homeData.workOrderStats), - const SizedBox(height: 12), + const SizedBox(height: 10), QuickEntryCard(), - const SizedBox(height: 12), + const SizedBox(height: 10), PowerTrendChart( data: state.homeData.powerTrendData, trendType: state.trendType, onToggleType: (type) => _bloc.add(HomeV2ToggleTrendType(type)), ), - const SizedBox(height: 20), + const SizedBox(height: 40), ]), ), ), @@ -187,4 +199,54 @@ class _HomeV2PageState extends State { ), ); } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // 首次进入时自动加载数据,但不显示转圈 + if (_bloc.state is HomeV2Initial) { + _bloc.add(const HomeV2LoadData()); + } + } + + void _showPlantSelector() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return Container( + padding: const EdgeInsets.all(14), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '选择电站', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 14), + ..._plantList.map((plant) => ListTile( + title: Text(plant), + trailing: _selectedPlant == plant + ? const Icon(Icons.check, color: Color(0xFF165DFF)) + : null, + onTap: () { + setState(() { + _selectedPlant = plant; + }); + Navigator.pop(context); + }, + )), + ], + ), + ); + }, + ); + } } diff --git a/lib/features/v2/home/presentation/widgets/power_card.dart b/lib/features/v2/home/presentation/widgets/power_card.dart index f945a7a6..40d7be4c 100644 --- a/lib/features/v2/home/presentation/widgets/power_card.dart +++ b/lib/features/v2/home/presentation/widgets/power_card.dart @@ -13,7 +13,12 @@ class PowerCard extends StatelessWidget { height: 140, decoration: BoxDecoration( gradient: const LinearGradient( - colors: [Color(0xFF165DFF), Color(0xFF4080FF)], + colors: [ + Color(0xFF165DFF), + Color(0xFF4080FF), + Color(0xFF7CB3FF), + Color(0xFFB8D4FF), + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), diff --git a/lib/features/v2/home/presentation/widgets/power_trend_chart.dart b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart index 1e1dcbb4..cfd22d6c 100644 --- a/lib/features/v2/home/presentation/widgets/power_trend_chart.dart +++ b/lib/features/v2/home/presentation/widgets/power_trend_chart.dart @@ -87,10 +87,16 @@ class PowerTrendChart extends StatelessWidget { reservedSize: 30, interval: 6, getTitlesWidget: (value, meta) { - const titles = ['00:00', '06:00', '12:00', '18:00', '24:00']; - if (value.toInt() % 6 == 0 && value.toInt() < titles.length) { + final index = value.toInt(); + if (index == 0 || index == 6 || index == 12 || index == 18 || index == 23) { + String time; + if (index == 23) { + time = '23:00'; + } else { + time = '${index.toString().padLeft(2, '0')}:00'; + } return Text( - titles[value.toInt() ~/ 6], + time, style: const TextStyle( color: Color(0xFF86909C), fontSize: 12, diff --git a/lib/features/v2/home/presentation/widgets/quick_entry_card.dart b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart index b1fadf9b..7190a0b6 100644 --- a/lib/features/v2/home/presentation/widgets/quick_entry_card.dart +++ b/lib/features/v2/home/presentation/widgets/quick_entry_card.dart @@ -27,11 +27,65 @@ class QuickEntryCard extends StatelessWidget { ], ), const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), + SizedBox( + height: 85, + child: ListView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(right: 16), + children: [ + _buildQuickEntryCard( + icon: Icons.upload_file, + label: '设备上报', + ), + const SizedBox(width: 8), + _buildQuickEntryCard( + icon: Icons.qr_code_scanner, + label: '扫一扫', + ), + const SizedBox(width: 8), + _buildQuickEntryCard( + icon: Icons.assignment, + label: '工单任务', + ), + const SizedBox(width: 8), + _buildQuickEntryCard( + icon: Icons.notifications_active, + label: '告警中心', + ), + const SizedBox(width: 8), + _buildQuickEntryCard( + icon: Icons.map, + label: '电厂地图', + ), + const SizedBox(width: 8), + _buildQuickEntryCard( + icon: Icons.devices, + label: '设备列表', + ), + const SizedBox(width: 8), + ], + ), + ), + ], + ); + } + + Widget _buildQuickEntryCard({ + required IconData icon, + required String label, + }) { + return InkWell( + onTap: () => print('点击:$label'), + borderRadius: BorderRadius.circular(12), + child: SizedBox( + width: 85, + height: 85, + child: Container( + padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), boxShadow: const [ BoxShadow( color: Color(0x0D000000), @@ -40,54 +94,29 @@ class QuickEntryCard extends StatelessWidget { ), ], ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - _buildQuickEntryItem( - icon: Icons.upload_file, - label: '设备上报', + Icon( + icon, + color: const Color(0xFF165DFF), + size: 26, ), - _buildQuickEntryItem( - icon: Icons.qr_code_scanner, - label: '扫一扫', - ), - _buildQuickEntryItem( - icon: Icons.assignment, - label: '工单任务', - ), - _buildQuickEntryItem( - icon: Icons.notifications_active, - label: '告警中心', + const SizedBox(height: 6), + Text( + label, + textAlign: TextAlign.center, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 12, + height: 1.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ], ), ), - ], - ); - } - - Widget _buildQuickEntryItem({ - required IconData icon, - required String label, - }) { - return InkWell( - onTap: () => print('点击:$label'), - child: Column( - children: [ - Icon( - icon, - color: const Color(0xFF165DFF), - size: 32, - ), - const SizedBox(height: 8), - Text( - label, - style: const TextStyle( - color: Color(0xFF4E5969), - fontSize: 14, - ), - ), - ], ), ); } diff --git a/lib/features/v2/home/presentation/widgets/stats_grid.dart b/lib/features/v2/home/presentation/widgets/stats_grid.dart index a162f36f..25690499 100644 --- a/lib/features/v2/home/presentation/widgets/stats_grid.dart +++ b/lib/features/v2/home/presentation/widgets/stats_grid.dart @@ -10,23 +10,26 @@ class StatsGrid extends StatelessWidget { Widget build(BuildContext context) { return GridView.count( crossAxisCount: 2, - childAspectRatio: 1.5, + childAspectRatio: 1.8, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: 12, - crossAxisSpacing: 12, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + padding: EdgeInsets.zero, children: [ _buildStatCard( title: '今日发电量', - value: data.todayGeneration.toStringAsFixed(0), + value: _formatNumber(data.todayGeneration), unit: 'kWh', valueColor: const Color(0xFF00B42A), + showArrow: false, ), _buildStatCard( title: '等效小时数', value: data.equivalentHours.toStringAsFixed(1), unit: 'h', valueColor: const Color(0xFF165DFF), + showArrow: false, ), _buildAlarmCard(data.alarmCount), _buildWorkOrderCard(data.pendingWorkOrders), @@ -39,9 +42,10 @@ class StatsGrid extends StatelessWidget { required String value, required String unit, required Color valueColor, + bool showArrow = false, }) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -56,23 +60,32 @@ class StatsGrid extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: const TextStyle( - color: Color(0xFF4E5969), - fontSize: 14, + if (showArrow) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, + ), ), + const Icon( + Icons.arrow_forward_ios, + size: 14, + color: Color(0xFF86909C), + ), + ], + ) + else + Text( + title, + style: const TextStyle( + color: Color(0xFF4E5969), + fontSize: 14, ), - const Icon( - Icons.arrow_forward_ios, - size: 14, - color: Color(0xFF86909C), - ), - ], - ), + ), const Spacer(), Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -102,7 +115,7 @@ class StatsGrid extends StatelessWidget { Widget _buildAlarmCard(int count) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), decoration: BoxDecoration( color: const Color(0xFFFFF0F0), borderRadius: BorderRadius.circular(16), @@ -125,15 +138,15 @@ class StatsGrid extends StatelessWidget { '告警数', style: TextStyle( color: Color(0xFFF53F3F), - fontSize: 14, + fontSize: 13, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( count.toString(), style: const TextStyle( color: Color(0xFFF53F3F), - fontSize: 32, + fontSize: 28, fontWeight: FontWeight.bold, ), ), @@ -141,15 +154,15 @@ class StatsGrid extends StatelessWidget { ), ), Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: const BoxDecoration( color: Color(0xFFFFE8E8), shape: BoxShape.circle, ), child: const Icon( - Icons.notifications_active, - size: 28, + Icons.notifications_none, + size: 24, color: Color(0xFFF53F3F), ), ), @@ -160,7 +173,7 @@ class StatsGrid extends StatelessWidget { Widget _buildWorkOrderCard(int count) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), @@ -183,15 +196,15 @@ class StatsGrid extends StatelessWidget { '待办工单', style: TextStyle( color: Color(0xFF4E5969), - fontSize: 14, + fontSize: 13, ), ), - const SizedBox(height: 8), + const SizedBox(height: 4), Text( count.toString(), style: const TextStyle( color: Color(0xFF1D2129), - fontSize: 32, + fontSize: 28, fontWeight: FontWeight.bold, ), ), @@ -199,15 +212,15 @@ class StatsGrid extends StatelessWidget { ), ), Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: const BoxDecoration( color: Color(0xFFE8F3FF), shape: BoxShape.circle, ), child: const Icon( - Icons.assignment, - size: 28, + Icons.content_paste, + size: 24, color: Color(0xFF165DFF), ), ), @@ -215,4 +228,18 @@ class StatsGrid extends StatelessWidget { ), ); } + + String _formatNumber(double number) { + final formatted = number.toStringAsFixed(0); + final parts = formatted.split('.'); + final intPart = parts[0]; + final buffer = StringBuffer(); + for (int i = 0; i < intPart.length; i++) { + if (i > 0 && (intPart.length - i) % 3 == 0) { + buffer.write(','); + } + buffer.write(intPart[i]); + } + return buffer.toString(); + } } diff --git a/lib/features/v2/home/presentation/widgets/work_order_card.dart b/lib/features/v2/home/presentation/widgets/work_order_card.dart index 42555a29..d42cda59 100644 --- a/lib/features/v2/home/presentation/widgets/work_order_card.dart +++ b/lib/features/v2/home/presentation/widgets/work_order_card.dart @@ -30,96 +30,95 @@ class WorkOrderCard extends StatelessWidget { ], ), const SizedBox(height: 12), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Color(0x0D000000), - blurRadius: 4, - offset: Offset(0, 2), + Row( + children: [ + Expanded( + child: _buildWorkOrderCard( + title: '待处理', + count: stats.pending, + color: const Color(0xFFF53F3F), + bgColor: Colors.white, ), - ], - ), - child: Row( - children: [ - Expanded( - child: _buildWorkOrderItem( - title: '待处理', - count: stats.pending, - color: const Color(0xFFF53F3F), - bgColor: Colors.white, - ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildWorkOrderCard( + title: '处理中', + count: stats.processing, + color: const Color(0xFFFF7D00), + bgColor: const Color(0xFFFFF7E6), ), - const SizedBox(width: 12), - Expanded( - child: _buildWorkOrderItem( - title: '处理中', - count: stats.processing, - color: const Color(0xFFFF7D00), - bgColor: const Color(0xFFFFF7E6), - ), + ), + const SizedBox(width: 8), + Expanded( + child: _buildWorkOrderCard( + title: '待验收', + count: stats.pendingAcceptance, + color: const Color(0xFF1D2129), + bgColor: Colors.white, ), - const SizedBox(width: 12), - Expanded( - child: _buildWorkOrderItem( - title: '待验收', - count: stats.pendingAcceptance, - color: const Color(0xFF1D2129), - bgColor: Colors.white, - ), - ), - ], - ), + ), + ], ), ], ); } - Widget _buildWorkOrderItem({ + Widget _buildWorkOrderCard({ required String title, required int count, required Color color, required Color bgColor, }) { - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - color: color, - fontSize: 14, + return InkWell( + onTap: () { + // TODO: 点击跳转逻辑 + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: color, + fontSize: 13, + ), ), - ), - const SizedBox(height: 4), - Text( - count.toString(), - style: TextStyle( - color: color, - fontSize: 24, - fontWeight: FontWeight.bold, + const SizedBox(height: 8), + Text( + count.toString(), + style: TextStyle( + color: color, + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), - ), - ], - ), - Icon( - Icons.arrow_forward_ios, - size: 16, - color: color.withOpacity(0.5), - ), - ], + ], + ), + Icon( + Icons.arrow_forward_ios, + size: 14, + color: color.withOpacity(0.5), + ), + ], + ), ), ); } diff --git a/lib/features/v2/my/README.md b/lib/features/v2/my/README.md new file mode 100644 index 00000000..3bad34c6 --- /dev/null +++ b/lib/features/v2/my/README.md @@ -0,0 +1,64 @@ +# 我的页面模块 + +## 架构说明 + +采用清洁架构(Clean Architecture)四层划分: + +### 1. Domain 层(领域层) +- `entities/` - 实体类(UserProfileEntity, MenuItemEntity) +- `repositories/` - 仓储接口(MyRepository) +- `usecases/` - 用例(GetMenuItemsUseCase, ToggleNightModeUseCase, ClearCacheUseCase) + +### 2. Data 层(数据层) +- `models/` - 数据模型(UserProfileModel, MenuItemModel) +- `datasources/` - 数据源接口和实现 +- `repositories/` - 仓储实现(MyRepositoryImpl) + +### 3. Presentation 层(表现层) +- `states/` - 状态管理(MyState) +- `cubit/` - 业务逻辑(MyCubit) +- `pages/` - 页面(MyPage) +- `widgets/` - UI 组件(UserProfileCard, MenuItemCard) +- `constants/` - 常量定义(MyColors, MyDimensions) +- `routes/` - 路由配置(MyRoutes) + +### 4. DI 层(依赖注入) +- `di/` - 依赖注入配置(MyDependencyInjector) + +## 功能特性 + +- 用户信息展示(头像、姓名、角色) +- 菜单列表(个人信息、消息设置、离线缓存、夜间模式等) +- 夜间模式切换 +- 缓存清除功能 +- 响应式状态管理(Cubit) + +## 使用示例 + +```dart +import 'package:maibu_satabot_v2/features/v2/my/my.dart'; + +// 在路由配置中使用 +GoRoute( + path: 'my', + name: 'my', + builder: (context, state) => const MyPage(), +), +``` + +## 数据流 + +1. **初始化**: MyPage 通过 MyDependencyInjector 创建 MyCubit +2. **加载数据**: MyCubit 调用 UseCase 获取数据 +3. **状态更新**: 数据通过 Entity → State 传递给 UI +4. **用户交互**: UI 触发 Cubit 方法,更新状态 + +## 依赖关系 + +``` +Presentation → Domain ← Data + ↓ ↓ + Cubit UseCase + ↓ ↓ + State Repository +``` diff --git a/lib/features/v2/my/data/datasources/impl/my_remote_datasource_impl.dart b/lib/features/v2/my/data/datasources/impl/my_remote_datasource_impl.dart new file mode 100644 index 00000000..1e97f1f4 --- /dev/null +++ b/lib/features/v2/my/data/datasources/impl/my_remote_datasource_impl.dart @@ -0,0 +1,78 @@ +import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/my/data/models/my_models.dart'; + +/// 我的页面远程数据源实现(模拟数据) +class MyRemoteDataSourceImpl implements MyRemoteDataSource { + @override + Future getUserProfile() async { + await Future.delayed(const Duration(milliseconds: 300)); + + return UserProfileModel( + name: '张工程师', + role: '运维工程师', + avatar: 'assets/images/avatar_engineer.png', + userId: 'ENG2024001', + ); + } + + @override + Future> getMenuItems() async { + await Future.delayed(const Duration(milliseconds: 300)); + + return [ + MenuItemModel( + id: 'personal_info', + title: '个人信息', + icon: 'person', + ), + MenuItemModel( + id: 'message_settings', + title: '消息设置', + icon: 'notifications', + ), + MenuItemModel( + id: 'offline_cache', + title: '离线缓存', + icon: 'cloud_download', + subTitle: '1.26 GB', + ), + MenuItemModel( + id: 'night_mode', + title: '夜间模式', + icon: 'dark_mode', + hasSwitch: true, + switchValue: false, + ), + MenuItemModel( + id: 'system_settings', + title: '系统设置', + icon: 'settings', + ), + MenuItemModel( + id: 'help_feedback', + title: '帮助与反馈', + icon: 'help', + ), + MenuItemModel( + id: 'about_us', + title: '关于我们', + icon: 'info', + subTitle: 'V2.1.0', + ), + ]; + } + + @override + Future toggleNightMode(bool enabled) async { + await Future.delayed(const Duration(milliseconds: 200)); + // 模拟保存设置 + return true; + } + + @override + Future clearCache() async { + await Future.delayed(const Duration(seconds: 1)); + // 模拟清除缓存 + return true; + } +} diff --git a/lib/features/v2/my/data/datasources/my_remote_datasource.dart b/lib/features/v2/my/data/datasources/my_remote_datasource.dart new file mode 100644 index 00000000..fb397479 --- /dev/null +++ b/lib/features/v2/my/data/datasources/my_remote_datasource.dart @@ -0,0 +1,16 @@ +import 'package:maibu_satabot_v2/features/v2/my/data/models/my_models.dart'; + +/// 我的页面远程数据源抽象 +abstract class MyRemoteDataSource { + /// 获取用户信息 + Future getUserProfile(); + + /// 获取菜单列表 + Future> getMenuItems(); + + /// 切换夜间模式 + Future toggleNightMode(bool enabled); + + /// 清除缓存 + Future clearCache(); +} diff --git a/lib/features/v2/my/data/models/my_models.dart b/lib/features/v2/my/data/models/my_models.dart new file mode 100644 index 00000000..3d5e868e --- /dev/null +++ b/lib/features/v2/my/data/models/my_models.dart @@ -0,0 +1,92 @@ +import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; + +/// 用户信息数据模型 +class UserProfileModel extends Equatable { + const UserProfileModel({ + required this.name, + required this.role, + required this.avatar, + required this.userId, + }); + + final String name; + final String role; + final String avatar; + final String userId; + + /// 转换为实体 + UserProfileEntity toEntity() { + return UserProfileEntity( + name: name, + role: role, + avatar: avatar, + userId: userId, + ); + } + + @override + List get props => [name, role, avatar, userId]; +} + +/// 菜单项数据模型 +class MenuItemModel extends Equatable { + const MenuItemModel({ + required this.id, + required this.title, + required this.icon, + this.subTitle, + this.hasSwitch = false, + this.switchValue = false, + this.hasBadge = false, + this.badgeCount = 0, + }); + + final String id; + final String title; + final String icon; + final String? subTitle; + final bool hasSwitch; + final bool switchValue; + final bool hasBadge; + final int badgeCount; + + /// 转换为实体 + MenuItemEntity toEntity() { + return MenuItemEntity( + id: id, + title: title, + icon: icon, + subTitle: subTitle, + hasSwitch: hasSwitch, + switchValue: switchValue, + hasBadge: hasBadge, + badgeCount: badgeCount, + ); + } + + MenuItemModel copyWith({ + String? id, + String? title, + String? icon, + String? subTitle, + bool? hasSwitch, + bool? switchValue, + bool? hasBadge, + int? badgeCount, + }) { + return MenuItemModel( + id: id ?? this.id, + title: title ?? this.title, + icon: icon ?? this.icon, + subTitle: subTitle ?? this.subTitle, + hasSwitch: hasSwitch ?? this.hasSwitch, + switchValue: switchValue ?? this.switchValue, + hasBadge: hasBadge ?? this.hasBadge, + badgeCount: badgeCount ?? this.badgeCount, + ); + } + + @override + List get props => [id, title, icon, subTitle, hasSwitch, switchValue, hasBadge, badgeCount]; +} diff --git a/lib/features/v2/my/data/repositories/my_repository_impl.dart b/lib/features/v2/my/data/repositories/my_repository_impl.dart new file mode 100644 index 00000000..4d2a9233 --- /dev/null +++ b/lib/features/v2/my/data/repositories/my_repository_impl.dart @@ -0,0 +1,53 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart'; + +/// 我的页面仓储实现 +class MyRepositoryImpl implements MyRepository { + MyRepositoryImpl(this._remoteDataSource); + + final MyRemoteDataSource _remoteDataSource; + + @override + Future> getUserProfile() async { + try { + final model = await _remoteDataSource.getUserProfile(); + return right(model.toEntity()); + } catch (e) { + return left(ServerFailure('获取用户信息失败: $e')); + } + } + + @override + Future>> getMenuItems() async { + try { + final models = await _remoteDataSource.getMenuItems(); + final entities = models.map((model) => model.toEntity()).toList(); + return right(entities); + } catch (e) { + return left(ServerFailure('获取菜单列表失败: $e')); + } + } + + @override + Future> toggleNightMode(bool enabled) async { + try { + final result = await _remoteDataSource.toggleNightMode(enabled); + return right(result); + } catch (e) { + return left(ServerFailure('切换夜间模式失败: $e')); + } + } + + @override + Future> clearCache() async { + try { + final result = await _remoteDataSource.clearCache(); + return right(result); + } catch (e) { + return left(ServerFailure('清除缓存失败: $e')); + } + } +} diff --git a/lib/features/v2/my/di/my_di.dart b/lib/features/v2/my/di/my_di.dart new file mode 100644 index 00000000..15f8b74f --- /dev/null +++ b/lib/features/v2/my/di/my_di.dart @@ -0,0 +1,32 @@ +import 'package:maibu_satabot_v2/features/v2/my/data/datasources/my_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/my/data/datasources/impl/my_remote_datasource_impl.dart'; +import 'package:maibu_satabot_v2/features/v2/my/data/repositories/my_repository_impl.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/clear_cache_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/get_menu_items_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart'; + +/// 我的页面依赖注入 +class MyDependencyInjector { + /// 创建 MyCubit 实例 + static MyCubit createMyCubit() { + // 创建数据源 + final MyRemoteDataSource remoteDataSource = MyRemoteDataSourceImpl(); + + // 创建仓储 + final MyRepository repository = MyRepositoryImpl(remoteDataSource); + + // 创建用例 + final GetMenuItemsUseCase getMenuItemsUseCase = GetMenuItemsUseCase(repository); + final ToggleNightModeUseCase toggleNightModeUseCase = ToggleNightModeUseCase(repository); + final ClearCacheUseCase clearCacheUseCase = ClearCacheUseCase(repository); + + // 创建 Cubit + return MyCubit( + getMenuItemsUseCase: getMenuItemsUseCase, + toggleNightModeUseCase: toggleNightModeUseCase, + clearCacheUseCase: clearCacheUseCase, + ); + } +} diff --git a/lib/features/v2/my/domain/entities/my_entities.dart b/lib/features/v2/my/domain/entities/my_entities.dart new file mode 100644 index 00000000..252bed9f --- /dev/null +++ b/lib/features/v2/my/domain/entities/my_entities.dart @@ -0,0 +1,81 @@ +import 'package:equatable/equatable.dart'; + +/// 用户信息实体 +class UserProfileEntity extends Equatable { + const UserProfileEntity({ + required this.name, + required this.role, + required this.avatar, + required this.userId, + }); + + final String name; + final String role; + final String avatar; + final String userId; + + @override + List get props => [name, role, avatar, userId]; + + UserProfileEntity copyWith({ + String? name, + String? role, + String? avatar, + String? userId, + }) { + return UserProfileEntity( + name: name ?? this.name, + role: role ?? this.role, + avatar: avatar ?? this.avatar, + userId: userId ?? this.userId, + ); + } +} + +/// 我的页面菜单项实体 +class MenuItemEntity extends Equatable { + const MenuItemEntity({ + required this.id, + required this.title, + required this.icon, + this.subTitle, + this.hasSwitch = false, + this.switchValue = false, + this.hasBadge = false, + this.badgeCount = 0, + }); + + final String id; + final String title; + final String icon; + final String? subTitle; + final bool hasSwitch; + final bool switchValue; + final bool hasBadge; + final int badgeCount; + + @override + List get props => [id, title, icon, subTitle, hasSwitch, switchValue, hasBadge, badgeCount]; + + MenuItemEntity copyWith({ + String? id, + String? title, + String? icon, + String? subTitle, + bool? hasSwitch, + bool? switchValue, + bool? hasBadge, + int? badgeCount, + }) { + return MenuItemEntity( + id: id ?? this.id, + title: title ?? this.title, + icon: icon ?? this.icon, + subTitle: subTitle ?? this.subTitle, + hasSwitch: hasSwitch ?? this.hasSwitch, + switchValue: switchValue ?? this.switchValue, + hasBadge: hasBadge ?? this.hasBadge, + badgeCount: badgeCount ?? this.badgeCount, + ); + } +} diff --git a/lib/features/v2/my/domain/repositories/my_repository.dart b/lib/features/v2/my/domain/repositories/my_repository.dart new file mode 100644 index 00000000..4d410b84 --- /dev/null +++ b/lib/features/v2/my/domain/repositories/my_repository.dart @@ -0,0 +1,18 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; + +/// 我的页面仓储接口 +abstract class MyRepository { + /// 获取用户信息 + Future> getUserProfile(); + + /// 获取菜单列表 + Future>> getMenuItems(); + + /// 切换夜间模式 + Future> toggleNightMode(bool enabled); + + /// 清除缓存 + Future> clearCache(); +} diff --git a/lib/features/v2/my/domain/usecases/clear_cache_usecase.dart b/lib/features/v2/my/domain/usecases/clear_cache_usecase.dart new file mode 100644 index 00000000..a5d3ff73 --- /dev/null +++ b/lib/features/v2/my/domain/usecases/clear_cache_usecase.dart @@ -0,0 +1,14 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart'; + +/// 清除缓存用例 +class ClearCacheUseCase { + ClearCacheUseCase(this._repository); + + final MyRepository _repository; + + Future> call() async { + return await _repository.clearCache(); + } +} diff --git a/lib/features/v2/my/domain/usecases/get_menu_items_usecase.dart b/lib/features/v2/my/domain/usecases/get_menu_items_usecase.dart new file mode 100644 index 00000000..d08093ac --- /dev/null +++ b/lib/features/v2/my/domain/usecases/get_menu_items_usecase.dart @@ -0,0 +1,15 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart'; + +/// 获取菜单列表用例 +class GetMenuItemsUseCase { + GetMenuItemsUseCase(this._repository); + + final MyRepository _repository; + + Future>> call() async { + return await _repository.getMenuItems(); + } +} diff --git a/lib/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart b/lib/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart new file mode 100644 index 00000000..4dba4d23 --- /dev/null +++ b/lib/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart @@ -0,0 +1,14 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/repositories/my_repository.dart'; + +/// 切换夜间模式用例 +class ToggleNightModeUseCase { + ToggleNightModeUseCase(this._repository); + + final MyRepository _repository; + + Future> call(bool enabled) async { + return await _repository.toggleNightMode(enabled); + } +} diff --git a/lib/features/v2/my/my.dart b/lib/features/v2/my/my.dart new file mode 100644 index 00000000..68aeaf98 --- /dev/null +++ b/lib/features/v2/my/my.dart @@ -0,0 +1,23 @@ +library; + +export 'domain/entities/my_entities.dart'; +export 'domain/repositories/my_repository.dart'; +export 'domain/usecases/get_menu_items_usecase.dart'; +export 'domain/usecases/toggle_night_mode_usecase.dart'; +export 'domain/usecases/clear_cache_usecase.dart'; + +export 'data/models/my_models.dart'; +export 'data/datasources/my_remote_datasource.dart'; +export 'data/datasources/impl/my_remote_datasource_impl.dart'; +export 'data/repositories/my_repository_impl.dart'; + +export 'presentation/states/my_state.dart'; +export 'presentation/cubit/my_cubit.dart'; +export 'presentation/pages/my_page.dart'; +export 'presentation/constants/my_constants.dart'; +export 'presentation/routes/my_routes.dart'; +export 'presentation/widgets/user_profile_card.dart'; +export 'presentation/widgets/quick_action_card.dart'; +export 'presentation/widgets/menu_item_card.dart'; + +export 'di/my_di.dart'; diff --git a/lib/features/v2/my/presentation/constants/my_constants.dart b/lib/features/v2/my/presentation/constants/my_constants.dart new file mode 100644 index 00000000..51409b17 --- /dev/null +++ b/lib/features/v2/my/presentation/constants/my_constants.dart @@ -0,0 +1,41 @@ +/// 我的页面全局常量 +/// 包含颜色、尺寸等全局配置 +library; + +import 'package:flutter/material.dart'; + +/// 颜色常量 +class MyColors { + /// 主色调 + static const Color primary = Color(0xFF165DFF); + + /// 背景色 + static const Color background = Color(0xFFF5F7FA); + + /// 卡片背景色 + static const Color cardBackground = Color(0xFFFFFFFF); + + /// 主要文字色 + static const Color textPrimary = Color(0xFF1D2129); + + /// 次要文字色 + static const Color textSecondary = Color(0xFF4E5969); + + /// 辅助文字色 + static const Color textTertiary = Color(0xFF86909C); +} + +/// 尺寸常量 +class MyDimensions { + /// 通用圆角 + static const double borderRadius = 12.0; + + /// 水平边距 + static const double horizontalPadding = 16.0; + + /// 模块间距 + static const double moduleSpacing = 16.0; + + /// 卡片阴影模糊半径 + static const double cardShadowBlur = 8.0; +} diff --git a/lib/features/v2/my/presentation/cubit/my_cubit.dart b/lib/features/v2/my/presentation/cubit/my_cubit.dart new file mode 100644 index 00000000..ad405c20 --- /dev/null +++ b/lib/features/v2/my/presentation/cubit/my_cubit.dart @@ -0,0 +1,102 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/clear_cache_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/get_menu_items_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/usecases/toggle_night_mode_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart'; + +/// 我的页面 Cubit +class MyCubit extends Cubit { + MyCubit({ + required GetMenuItemsUseCase getMenuItemsUseCase, + required ToggleNightModeUseCase toggleNightModeUseCase, + required ClearCacheUseCase clearCacheUseCase, + }) : _getMenuItemsUseCase = getMenuItemsUseCase, + _toggleNightModeUseCase = toggleNightModeUseCase, + _clearCacheUseCase = clearCacheUseCase, + super(MyInitial()); + + final GetMenuItemsUseCase _getMenuItemsUseCase; + final ToggleNightModeUseCase _toggleNightModeUseCase; + final ClearCacheUseCase _clearCacheUseCase; + + /// 加载数据 + Future loadData() async { + emit(MyLoading()); + + try { + final menuResult = await _getMenuItemsUseCase(); + + final menuItems = menuResult.fold( + (failure) => [], + (items) => items, + ); + + if (menuItems.isEmpty) { + emit(const MyError('加载菜单失败')); + return; + } + + // 模拟用户信息(实际应从 Auth 模块获取) + final userProfile = UserProfileEntity( + name: '张工程师', + role: '运维工程师', + avatar: '', + userId: 'ENG2024001', + ); + + emit(MyLoaded( + userProfile: userProfile, + menuItems: menuItems, + )); + } catch (e) { + emit(MyError('加载数据失败: $e')); + } + } + + /// 切换夜间模式 + Future toggleNightMode(bool enabled) async { + final currentState = state; + if (currentState is MyLoaded) { + // 乐观更新 UI + emit(currentState.copyWith(isNightMode: enabled)); + + final result = await _toggleNightModeUseCase(enabled); + + result.fold( + (failure) { + // 如果失败,回滚状态 + emit(currentState.copyWith(isNightMode: !enabled)); + emit(MyError('切换夜间模式失败')); + }, + (_) { + // 成功,保持当前状态 + }, + ); + } + } + + /// 清除缓存 + Future clearCache() async { + final currentState = state; + if (currentState is! MyLoaded) return; + + emit(MyLoading()); + + final result = await _clearCacheUseCase(); + + result.fold( + (failure) { + emit(MyError('清除缓存失败: ${failure.message}')); + }, + (success) { + emit(currentState.copyWith(cacheSize: '0 MB')); + }, + ); + } + + /// 刷新数据 + Future refresh() async { + await loadData(); + } +} diff --git a/lib/features/v2/my/presentation/pages/my_page.dart b/lib/features/v2/my/presentation/pages/my_page.dart new file mode 100644 index 00000000..48c985d0 --- /dev/null +++ b/lib/features/v2/my/presentation/pages/my_page.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/services.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/cubit/my_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/states/my_state.dart'; +import 'package:maibu_satabot_v2/features/v2/my/di/my_di.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/user_profile_card.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/quick_action_card.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/widgets/menu_item_card.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/system_settings_page.dart'; + +/// 我的页面(100% 还原设计稿) +class MyPage extends StatelessWidget { + const MyPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => MyDependencyInjector.createMyCubit()..loadData(), + child: _MyPageContent(), + ); + } +} + +class _MyPageContent extends StatelessWidget { + const _MyPageContent(); + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.light, + ), + child: Scaffold( + extendBodyBehindAppBar: true, + backgroundColor: MyColors.background, + body: BlocConsumer( + listener: (context, state) { + if (state is MyError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.message)), + ); + } + }, + builder: (context, state) { + if (state is MyLoading) { + return const Center( + child: CircularProgressIndicator(color: MyColors.primary), + ); + } + + if (state is MyLoaded) { + final cubit = context.read(); + return Column( + children: [ + // 用户信息卡片(延伸到状态栏) + UserProfileCard( + name: state.userProfile.name, + role: state.userProfile.role, + onTap: () { + // TODO: 跳转到个人信息页面 + debugPrint('点击个人信息'); + }, + ), + // 快捷入口卡片(负margin向上重叠) + Transform.translate( + offset: const Offset(0, -15), + child: QuickActionCard( + onMessageTap: () { + debugPrint('我的消息'); + }, + onFavoriteTap: () { + debugPrint('我的收藏'); + }, + onWorkOrderTap: () { + debugPrint('我的工单'); + }, + ), + ), + // 菜单列表(白色卡片) + Container( + margin: const EdgeInsets.only(top: 15, left: 16, right: 16, bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.5, + ), + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: state.menuItems.length, + itemBuilder: (context, index) { + final item = state.menuItems[index]; + return MenuItemCard( + item: item, + onTap: () => _handleMenuItemTap(context, cubit, item), + onSwitchChanged: item.hasSwitch + ? (value) => cubit.toggleNightMode(value) + : null, + ); + }, + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + }, + ), + ), + ); + } + + /// 处理菜单项点击 + void _handleMenuItemTap(BuildContext context, MyCubit cubit, dynamic item) { + switch (item.id) { + case 'offline_cache': + _showClearCacheDialog(context, cubit); + break; + case 'night_mode': + // 由 Switch 处理 + break; + case 'system_settings': + // 跳转到系统设置页面 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SystemSettingsPage(), + ), + ); + break; + default: + debugPrint('点击菜单: ${item.title}'); + // TODO: 根据 ID 跳转到对应页面 + } + } + + /// 显示清除缓存对话框 + void _showClearCacheDialog(BuildContext context, MyCubit cubit) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('清除缓存'), + content: const Text('确定要清除所有缓存数据吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + cubit.clearCache(); + }, + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/my/presentation/pages/system_settings_page.dart b/lib/features/v2/my/presentation/pages/system_settings_page.dart new file mode 100644 index 00000000..95c7faeb --- /dev/null +++ b/lib/features/v2/my/presentation/pages/system_settings_page.dart @@ -0,0 +1,284 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; +import 'package:maibu_satabot_v2/core/localization/locale_cubit.dart'; +import 'package:maibu_satabot_v2/core/router/route_paths.dart'; +import 'package:maibu_satabot_v2/core/storage/user_storage.dart'; +import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; + +/// 系统设置综合页面 +class SystemSettingsPage extends StatefulWidget { + const SystemSettingsPage({super.key}); + + @override + State createState() => _SystemSettingsPageState(); +} + +class _SystemSettingsPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF5F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)), + onPressed: () => Navigator.pop(context), + ), + title: Text( + AppLocalizations.of(context).translate('my.settings'), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + centerTitle: true, + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Tab 设置 + _buildTabSettingsSection(), + const SizedBox(height: 12), + // 语言设置 + _buildLanguageSection(), + const SizedBox(height: 12), + // 退出登录 + _buildLogoutButton(context), + const SizedBox(height: 20), + ], + ), + ); + } + + /// Tab 设置 + Widget _buildTabSettingsSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Tab 设置', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + BlocBuilder( + builder: (context, state) { + if (state.runtimeType.toString() != 'TabConfigLoaded') { + return const Center(child: CircularProgressIndicator()); + } + + final tabs = state.config.items; + + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: tabs.length, + itemBuilder: (context, index) { + final tab = tabs[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + children: [ + Expanded( + child: Text( + tab.name, + style: const TextStyle( + fontSize: 14, + color: Color(0xFF1D2129), + ), + ), + ), + Switch( + value: tab.isEnabled, + onChanged: (value) { + context.read().toggleTab(tab.id); + }, + activeColor: const Color(0xFF165DFF), + ), + ], + ), + ); + }, + ); + }, + ), + ], + ), + ); + } + + /// 语言设置 + Widget _buildLanguageSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).translate('my.language'), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const SizedBox(height: 16), + BlocBuilder( + builder: (context, locale) { + return Column( + children: [ + _buildLanguageOption( + AppLocalizations.of(context).translate('my.chinese'), + 'zh', + locale.languageCode == 'zh', + () => context.read().setLocale(const Locale('zh', 'CN')), + ), + const SizedBox(height: 12), + _buildLanguageOption( + AppLocalizations.of(context).translate('my.english'), + 'en', + locale.languageCode == 'en', + () => context.read().setLocale(const Locale('en', 'US')), + ), + ], + ); + }, + ), + ], + ), + ); + } + + Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB), + width: 1, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 14, + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + if (isSelected) + const Icon( + Icons.check_circle, + color: Color(0xFF165DFF), + size: 20, + ), + ], + ), + ), + ); + } + + /// 退出登录按钮 + Widget _buildLogoutButton(BuildContext context) { + return GestureDetector( + onTap: () { + _showLogoutDialog(context); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Color(0x0D000000), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + child: Center( + child: Text( + AppLocalizations.of(context).translate('login.logout'), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Color(0xFFF53F3F), + ), + ), + ), + ), + ); + } + + /// 显示退出登录确认对话框 + void _showLogoutDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(AppLocalizations.of(context).translate('login.logout')), + content: const Text('确定要退出登录吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + // 执行退出登录逻辑 + await GetIt.I().deleteUser(); + GetIt.I().logout(); + context.go(RoutePaths.login); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFF53F3F), + ), + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/my/presentation/routes/my_routes.dart b/lib/features/v2/my/presentation/routes/my_routes.dart new file mode 100644 index 00000000..46bfd06b --- /dev/null +++ b/lib/features/v2/my/presentation/routes/my_routes.dart @@ -0,0 +1,13 @@ +import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/pages/my_page.dart'; + +/// 我的页面路由配置 +class MyRoutes { + static List get routes => [ + GoRoute( + path: 'my', + name: 'my', + builder: (context, state) => const MyPage(), + ), + ]; +} diff --git a/lib/features/v2/my/presentation/states/my_state.dart b/lib/features/v2/my/presentation/states/my_state.dart new file mode 100644 index 00000000..a1e368bd --- /dev/null +++ b/lib/features/v2/my/presentation/states/my_state.dart @@ -0,0 +1,58 @@ +import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; + +/// 我的页面状态基类 +abstract class MyState extends Equatable { + const MyState(); + + @override + List get props => []; +} + +/// 初始状态 +class MyInitial extends MyState {} + +/// 加载中状态 +class MyLoading extends MyState {} + +/// 加载成功状态 +class MyLoaded extends MyState { + const MyLoaded({ + required this.userProfile, + required this.menuItems, + this.isNightMode = false, + this.cacheSize = '1.26 GB', + }); + + final UserProfileEntity userProfile; + final List menuItems; + final bool isNightMode; + final String cacheSize; + + @override + List get props => [userProfile, menuItems, isNightMode, cacheSize]; + + MyLoaded copyWith({ + UserProfileEntity? userProfile, + List? menuItems, + bool? isNightMode, + String? cacheSize, + }) { + return MyLoaded( + userProfile: userProfile ?? this.userProfile, + menuItems: menuItems ?? this.menuItems, + isNightMode: isNightMode ?? this.isNightMode, + cacheSize: cacheSize ?? this.cacheSize, + ); + } +} + +/// 加载失败状态 +class MyError extends MyState { + const MyError(this.message); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/features/v2/my/presentation/widgets/menu_item_card.dart b/lib/features/v2/my/presentation/widgets/menu_item_card.dart new file mode 100644 index 00000000..9bf3b20d --- /dev/null +++ b/lib/features/v2/my/presentation/widgets/menu_item_card.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/my/domain/entities/my_entities.dart'; + +/// 菜单项卡片组件(100% 还原设计稿) +class MenuItemCard extends StatelessWidget { + const MenuItemCard({ + super.key, + required this.item, + this.onTap, + this.onSwitchChanged, + }); + + final MenuItemEntity item; + final VoidCallback? onTap; + final ValueChanged? onSwitchChanged; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + height: 56, + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: Row( + children: [ + // 图标 + Icon( + _getIconData(item.icon), + size: 20, + color: const Color(0xFF4E5969), + ), + const SizedBox(width: 12), + // 标题 + Expanded( + child: Text( + item.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Color(0xFF1D2129), + ), + ), + ), + // 副标题或开关 + if (item.subTitle != null) ...[ + Text( + item.subTitle!, + style: const TextStyle( + fontSize: 13, + color: Color(0xFF86909C), + ), + ), + const SizedBox(width: 6), + ], + if (item.hasSwitch) ...[ + Transform.scale( + scale: 0.85, + child: Switch( + value: item.switchValue, + onChanged: onSwitchChanged, + activeColor: const Color(0xFF165DFF), + activeTrackColor: const Color(0xFF165DFF).withOpacity(0.3), + inactiveThumbColor: const Color(0xFFC9CDD4), + inactiveTrackColor: const Color(0xFFE5E6EB), + ), + ), + ], + Icon( + Icons.chevron_right, + color: const Color(0xFFC9CDD4), + size: 20, + ), + ], + ), + ), + ), + ), + // 分割线 + Padding( + padding: const EdgeInsets.only(left: 48), + child: Divider( + height: 1, + thickness: 0.5, + color: const Color(0xFFF0F0F0), + ), + ), + ], + ); + } + + /// 根据字符串获取 IconData + IconData _getIconData(String iconName) { + switch (iconName) { + case 'person': + return Icons.person_outline; + case 'notifications': + return Icons.notifications_outlined; + case 'cloud_download': + return Icons.cloud_download_outlined; + case 'dark_mode': + return Icons.dark_mode_outlined; + case 'settings': + return Icons.settings_outlined; + case 'help': + return Icons.help_outline; + case 'info': + return Icons.info_outline; + default: + return Icons.label_outline; + } + } +} diff --git a/lib/features/v2/my/presentation/widgets/quick_action_card.dart b/lib/features/v2/my/presentation/widgets/quick_action_card.dart new file mode 100644 index 00000000..fb426d0a --- /dev/null +++ b/lib/features/v2/my/presentation/widgets/quick_action_card.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart'; + +/// 快捷入口卡片组件(我的消息、我的收藏、我的工单) +class QuickActionCard extends StatelessWidget { + const QuickActionCard({ + super.key, + required this.onMessageTap, + required this.onFavoriteTap, + required this.onWorkOrderTap, + }); + + final VoidCallback onMessageTap; + final VoidCallback onFavoriteTap; + final VoidCallback onWorkOrderTap; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // 我的消息 + _buildQuickActionItem( + icon: Icons.notifications_outlined, + label: '我的消息', + onTap: onMessageTap, + showBadge: true, + ), + // 分割线 + Container( + width: 1, + height: 40, + color: const Color(0xFFF0F0F0), + ), + // 我的收藏 + _buildQuickActionItem( + icon: Icons.favorite_border, + label: '我的收藏', + onTap: onFavoriteTap, + ), + // 分割线 + Container( + width: 1, + height: 40, + color: const Color(0xFFF0F0F0), + ), + // 我的工单 + _buildQuickActionItem( + icon: Icons.assignment_outlined, + label: '我的工单', + onTap: onWorkOrderTap, + ), + ], + ), + ); + } + + Widget _buildQuickActionItem({ + required IconData icon, + required String label, + required VoidCallback onTap, + bool showBadge = false, + }) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: 80, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Icon( + icon, + size: 24, + color: MyColors.primary, + ), + if (showBadge) + Positioned( + right: -4, + top: -4, + child: Container( + width: 12, + height: 12, + decoration: const BoxDecoration( + color: Color(0xFFFF4D4F), + shape: BoxShape.circle, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 13, + color: MyColors.textSecondary, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/v2/my/presentation/widgets/user_profile_card.dart b/lib/features/v2/my/presentation/widgets/user_profile_card.dart new file mode 100644 index 00000000..6706180a --- /dev/null +++ b/lib/features/v2/my/presentation/widgets/user_profile_card.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart'; + +/// 用户信息卡片组件(100% 还原设计稿) +class UserProfileCard extends StatelessWidget { + const UserProfileCard({ + super.key, + required this.name, + required this.role, + this.onTap, + }); + + final String name; + final String role; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 70, 20, 50), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF165DFF), + Color(0xFF0E42CC), + ], + ), + ), + child: Row( + children: [ + // 头像 + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white, + width: 2, + ), + ), + child: ClipOval( + child: Image.asset( + 'assets/images/app_logo.png', + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.white, + child: Icon( + Icons.person, + size: 32, + color: const Color(0xFF165DFF), + ), + ); + }, + ), + ), + ), + const SizedBox(width: 14), + // 用户信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 6), + Text( + role, + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(0.85), + letterSpacing: 0.3, + ), + ), + ], + ), + ), + // 箭头 + Icon( + Icons.chevron_right, + color: Colors.white.withOpacity(0.9), + size: 24, + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/report/README.md b/lib/features/v2/report/README.md new file mode 100644 index 00000000..9218bcbf --- /dev/null +++ b/lib/features/v2/report/README.md @@ -0,0 +1,212 @@ +# 现场上报模块使用说明 + +## 快速开始 + +### 1. 在路由中注册页面 + +```dart +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/report/report.dart'; + +// 在路由配置中添加 +GoRoute( + path: '/report', + builder: (context, state) => BlocProvider( + create: (context) => ReportDependencyInjector.createReportCubit(), + child: const ReportPage(), + ), +), +``` + +### 2. 跳转到上报页面 + +```dart +context.push('/report'); +``` + +## 架构说明 + +### 清洁架构分层 + +``` +lib/features/v2/report/ +├── domain/ # 领域层(核心业务逻辑) +│ ├── entities/ # 实体类 +│ │ └── report_entity.dart +│ ├── repositories/ # 仓储接口 +│ │ └── report_repository.dart +│ └── usecases/ # 用例 +│ └── submit_report_usecase.dart +├── data/ # 数据层(数据获取与存储) +│ ├── datasources/ # 数据源 +│ │ ├── report_remote_datasource.dart +│ │ └── report_remote_datasource_impl.dart +│ ├── models/ # 数据模型 +│ │ └── report_model.dart +│ └── repositories/ # 仓储实现 +│ └── report_repository_impl.dart +├── presentation/ # 表现层(UI + 状态管理) +│ ├── constants/ # 常量 +│ │ └── report_constants.dart +│ ├── cubit/ # 状态管理 +│ │ └── report_cubit.dart +│ ├── states/ # 状态定义 +│ │ └── report_state.dart +│ ├── pages/ # 页面 +│ │ └── report_page.dart +│ ── widgets/ # 组件 +│ ├── report_type_selector.dart +│ ├── device_selector.dart +│ ├── description_input.dart +│ ├── media_uploader.dart +│ └── level_selector.dart +├── di/ # 依赖注入 +│ └── report_di.dart +└── report.dart # 统一导出 +``` + +## 核心功能 + +### 1. 上报类型选择 +- 设备故障 +- 巡检记录 +- 隐患上报 +- 缺陷上报 + +### 2. 设备选择 +- 点击选择设备(需实现设备选择页面) + +### 3. 问题描述 +- 多行文本输入 +- 最大 200 字符限制 +- 实时字符计数 + +### 4. 媒体上传 +- 拍照上传 +- 录像上传 +- 图片预览 +- 删除已上传图片 + +### 5. 问题等级 +- 一般(灰色) +- 重要(橙色) +- 紧急(红色) + +## 表单验证 + +提交时自动验证以下必填项: +1. 设备选择 +2. 问题描述 +3. 问题等级 + +## 状态管理 + +### 状态类型 +- `ReportInitial`: 初始状态 +- `ReportFormState`: 表单数据状态 +- `ReportSubmitting`: 提交中状态 +- `ReportSuccess`: 提交成功状态 +- `ReportFailure`: 提交失败状态 + +### Cubit 方法 +- `selectReportType(ReportType)`: 选择上报类型 +- `selectDevice(String)`: 选择设备 +- `updateDescription(String)`: 更新问题描述 +- `selectProblemLevel(ProblemLevel)`: 选择问题等级 +- `addMediaFile(String)`: 添加媒体文件 +- `removeMediaFile(int)`: 删除媒体文件 +- `submitReport()`: 提交上报 +- `resetForm()`: 重置表单 + +## 自定义扩展 + +### 1. 对接真实 API + +修改 `ReportRemoteDataSourceImpl`: + +```dart +class ReportRemoteDataSourceImpl implements ReportRemoteDataSource { + final Dio dio; // 注入 Dio 实例 + + @override + Future> submitReport(ReportModel report) async { + try { + final response = await dio.post( + '/api/report/submit', + data: report.toJson(), + ); + + if (response.statusCode == 200) { + return right(true); + } else { + return left(ServerFailure('提交失败')); + } + } catch (e) { + return left(NetworkFailure('网络错误: $e')); + } + } +} +``` + +### 2. 实现设备选择 + +在 `ReportPage` 中修改 `_showDevicePicker` 方法: + +```dart +void _showDevicePicker(BuildContext context) { + // 跳转到设备选择页面 + context.push('/device-select').then((device) { + if (device != null) { + context.read().selectDevice(device); + } + }); +} +``` + +### 3. 实现真实的相机/相册功能 + +添加依赖: +```yaml +dependencies: + image_picker: ^1.1.2 +``` + +修改 `MediaUploader`: + +```dart +final ImagePicker picker = ImagePicker(); + +Future _pickImage() async { + final XFile? image = await picker.pickImage(source: ImageSource.camera); + if (image != null) { + cubit.addMediaFile(image.path); + } +} +``` + +## 依赖说明 + +### 项目已有依赖 +- flutter_bloc: ^9.1.1 +- equatable: ^2.0.8 +- fpdart: ^1.2.0 + +### 可选依赖(按需添加) +- image_picker: ^1.1.2 (图片/视频选择) +- dio: ^5.9.0 (HTTP 请求,项目已有) + +## 注意事项 + +1. 所有颜色、尺寸常量已抽取到 `report_constants.dart` +2. 遵循清洁架构原则,内层不依赖外层 +3. 使用 fpdart 进行错误处理(Either 类型) +4. 所有 UI 组件已适配 iOS 安全区 +5. 模拟数据已实现,可直接运行测试 + +## 后续开发建议 + +1. 实现真实的设备选择页面 +2. 对接后端 API +3. 添加图片压缩功能 +4. 实现离线缓存 +5. 添加草稿保存功能 diff --git a/lib/features/v2/report/USAGE_EXAMPLE.md b/lib/features/v2/report/USAGE_EXAMPLE.md new file mode 100644 index 00000000..033fa738 --- /dev/null +++ b/lib/features/v2/report/USAGE_EXAMPLE.md @@ -0,0 +1,84 @@ +# 现场上报模块 - 使用示例 + +## 1. 在路由中注册 + +在你的路由配置文件(如 `lib/core/router/app_router.dart`)中添加: + +```dart +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/features/v2/report/report.dart'; + +// 在 routes 列表中添加 +GoRoute( + path: '/report', + builder: (context, state) => BlocProvider( + create: (context) => ReportDependencyInjector.createReportCubit(), + child: const ReportPage(), + ), +), +``` + +## 2. 跳转到上报页面 + +在任何页面中,使用以下方式跳转: + +```dart +import 'package:go_router/go_router.dart'; + +// 跳转到上报页面 +context.push('/report'); +``` + +## 3. 完整示例 + +```dart +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class SomePage extends StatelessWidget { + const SomePage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('示例页面')), + body: Center( + child: ElevatedButton( + onPressed: () { + // 跳转到现场上报页面 + context.push('/report'); + }, + child: const Text('现场上报'), + ), + ), + ); + } +} +``` + +## 4. 依赖说明 + +### 必需依赖(项目已包含) +- flutter_bloc: ^9.1.1 +- equatable: ^2.0.8 +- fpdart: ^1.2.0 +- go_router: ^17.0.1 + +### 可选依赖(按需添加) +如果需要实现真实的图片/视频上传功能,需要添加: +```yaml +dependencies: + image_picker: ^1.1.2 +``` + +## 5. 注意事项 + +1. 确保在跳转前已经配置好路由 +2. BlocProvider 必须在 ReportPage 的父级 +3. 当前使用模拟数据,可直接运行测试 +4. 所有常量已抽取,便于主题定制 + +## 6. 后续开发 + +对接真实 API 时,修改 `lib/features/v2/report/data/datasources/report_remote_datasource_impl.dart` 文件即可。 diff --git a/lib/features/v2/report/data/datasources/report_remote_datasource.dart b/lib/features/v2/report/data/datasources/report_remote_datasource.dart new file mode 100644 index 00000000..06c463cc --- /dev/null +++ b/lib/features/v2/report/data/datasources/report_remote_datasource.dart @@ -0,0 +1,9 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../../../core/error/failure.dart'; +import '../models/report_model.dart'; + +/// 上报远程数据源接口 +abstract class ReportRemoteDataSource { + /// 提交上报数据 + Future> submitReport(ReportModel report); +} diff --git a/lib/features/v2/report/data/datasources/report_remote_datasource_impl.dart b/lib/features/v2/report/data/datasources/report_remote_datasource_impl.dart new file mode 100644 index 00000000..530f630a --- /dev/null +++ b/lib/features/v2/report/data/datasources/report_remote_datasource_impl.dart @@ -0,0 +1,19 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../../../core/error/failure.dart'; +import '../models/report_model.dart'; +import 'report_remote_datasource.dart'; + +/// 上报远程数据源实现(模拟接口请求) +class ReportRemoteDataSourceImpl implements ReportRemoteDataSource { + @override + Future> submitReport(ReportModel report) async { + // 模拟网络请求延迟 + await Future.delayed(const Duration(seconds: 1)); + + // 模拟成功返回 + return right(true); + + // 模拟失败情况(取消注释以测试错误处理) + // return left(ServerFailure('提交失败,请重试')); + } +} diff --git a/lib/features/v2/report/data/models/report_model.dart b/lib/features/v2/report/data/models/report_model.dart new file mode 100644 index 00000000..27a71f94 --- /dev/null +++ b/lib/features/v2/report/data/models/report_model.dart @@ -0,0 +1,68 @@ +import '../../domain/entities/report_entity.dart'; + +/// 上报数据模型 +class ReportModel { + final String? location; + final String? reportType; + final String? deviceId; + final String? description; + final List? mediaUrls; + final String? problemLevel; + + ReportModel({ + this.location, + this.reportType, + this.deviceId, + this.description, + this.mediaUrls, + this.problemLevel, + }); + + /// 从 JSON 创建 + factory ReportModel.fromJson(Map json) { + return ReportModel( + location: json['location'] as String?, + reportType: json['reportType'] as String?, + deviceId: json['deviceId'] as String?, + description: json['description'] as String?, + mediaUrls: (json['mediaUrls'] as List?)?.cast(), + problemLevel: json['problemLevel'] as String?, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'location': location, + 'reportType': reportType, + 'deviceId': deviceId, + 'description': description, + 'mediaUrls': mediaUrls, + 'problemLevel': problemLevel, + }; + } + + /// 转换为实体 + ReportEntity toEntity() { + return ReportEntity( + location: location, + reportType: reportType, + deviceId: deviceId, + description: description, + mediaUrls: mediaUrls, + problemLevel: problemLevel, + ); + } + + /// 从实体创建 + factory ReportModel.fromEntity(ReportEntity entity) { + return ReportModel( + location: entity.location, + reportType: entity.reportType, + deviceId: entity.deviceId, + description: entity.description, + mediaUrls: entity.mediaUrls, + problemLevel: entity.problemLevel, + ); + } +} diff --git a/lib/features/v2/report/data/repositories/report_repository_impl.dart b/lib/features/v2/report/data/repositories/report_repository_impl.dart new file mode 100644 index 00000000..5ed2ffc0 --- /dev/null +++ b/lib/features/v2/report/data/repositories/report_repository_impl.dart @@ -0,0 +1,22 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../../../core/error/failure.dart'; +import '../../domain/entities/report_entity.dart'; +import '../../domain/repositories/report_repository.dart'; +import '../datasources/report_remote_datasource.dart'; +import '../models/report_model.dart'; + +/// 上报仓储实现 +class ReportRepositoryImpl implements ReportRepository { + final ReportRemoteDataSource remoteDataSource; + + ReportRepositoryImpl({required this.remoteDataSource}); + + @override + Future> submitReport(ReportEntity report) async { + // 将实体转换为模型 + final reportModel = ReportModel.fromEntity(report); + + // 调用远程数据源 + return await remoteDataSource.submitReport(reportModel); + } +} diff --git a/lib/features/v2/report/di/report_di.dart b/lib/features/v2/report/di/report_di.dart new file mode 100644 index 00000000..04709b69 --- /dev/null +++ b/lib/features/v2/report/di/report_di.dart @@ -0,0 +1,26 @@ +import '../data/datasources/report_remote_datasource.dart'; +import '../data/datasources/report_remote_datasource_impl.dart'; +import '../data/repositories/report_repository_impl.dart'; +import '../domain/repositories/report_repository.dart'; +import '../domain/usecases/submit_report_usecase.dart'; +import '../presentation/cubit/report_cubit.dart'; + +/// 上报模块依赖注入 +class ReportDependencyInjector { + /// 创建 ReportCubit 实例 + static ReportCubit createReportCubit() { + // 创建数据源 + final ReportRemoteDataSource remoteDataSource = ReportRemoteDataSourceImpl(); + + // 创建仓储 + final ReportRepository repository = ReportRepositoryImpl( + remoteDataSource: remoteDataSource, + ); + + // 创建用例 + final SubmitReportUseCase submitReportUseCase = SubmitReportUseCase(repository); + + // 创建 Cubit + return ReportCubit(submitReportUseCase: submitReportUseCase); + } +} diff --git a/lib/features/v2/report/domain/entities/report_entity.dart b/lib/features/v2/report/domain/entities/report_entity.dart new file mode 100644 index 00000000..bbf3f15c --- /dev/null +++ b/lib/features/v2/report/domain/entities/report_entity.dart @@ -0,0 +1,36 @@ +/// 上报内容实体类 +class ReportEntity { + final String? location; + final String? reportType; + final String? deviceId; + final String? description; + final List? mediaUrls; + final String? problemLevel; + + const ReportEntity({ + this.location, + this.reportType, + this.deviceId, + this.description, + this.mediaUrls, + this.problemLevel, + }); + + ReportEntity copyWith({ + String? location, + String? reportType, + String? deviceId, + String? description, + List? mediaUrls, + String? problemLevel, + }) { + return ReportEntity( + location: location ?? this.location, + reportType: reportType ?? this.reportType, + deviceId: deviceId ?? this.deviceId, + description: description ?? this.description, + mediaUrls: mediaUrls ?? this.mediaUrls, + problemLevel: problemLevel ?? this.problemLevel, + ); + } +} diff --git a/lib/features/v2/report/domain/repositories/report_repository.dart b/lib/features/v2/report/domain/repositories/report_repository.dart new file mode 100644 index 00000000..f53e7ecf --- /dev/null +++ b/lib/features/v2/report/domain/repositories/report_repository.dart @@ -0,0 +1,9 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../../../core/error/failure.dart'; +import '../entities/report_entity.dart'; + +/// 上报仓储接口 +abstract class ReportRepository { + /// 提交上报数据 + Future> submitReport(ReportEntity report); +} diff --git a/lib/features/v2/report/domain/usecases/submit_report_usecase.dart b/lib/features/v2/report/domain/usecases/submit_report_usecase.dart new file mode 100644 index 00000000..49bbcdae --- /dev/null +++ b/lib/features/v2/report/domain/usecases/submit_report_usecase.dart @@ -0,0 +1,26 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../../../core/error/failure.dart'; +import '../entities/report_entity.dart'; +import '../repositories/report_repository.dart'; + +/// 提交上报用例 +class SubmitReportUseCase { + final ReportRepository repository; + + SubmitReportUseCase(this.repository); + + Future> execute(ReportEntity report) async { + // 业务逻辑验证 + if (report.deviceId == null || report.deviceId!.isEmpty) { + return left(Failure('请选择设备')); + } + if (report.description == null || report.description!.isEmpty) { + return left(Failure('请填写问题描述')); + } + if (report.problemLevel == null || report.problemLevel!.isEmpty) { + return left(Failure('请选择问题等级')); + } + + return await repository.submitReport(report); + } +} diff --git a/lib/features/v2/report/presentation/constants/report_constants.dart b/lib/features/v2/report/presentation/constants/report_constants.dart new file mode 100644 index 00000000..76f6c1cc --- /dev/null +++ b/lib/features/v2/report/presentation/constants/report_constants.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +/// 全局颜色常量 +class AppColors { + static const Color primary = Color(0xFF165DFF); + static const Color danger = Color(0xFFF53F3F); + static const Color warning = Color(0xFFFF7D00); + static const Color textPrimary = Color(0xFF1D2129); + static const Color textSecondary = Color(0xFF4E5969); + static const Color textHint = Color(0xFF86909C); + static const Color background = Color(0xFFFFFFFF); + static const Color cardBackground = Color(0xFFF8F9FA); + static const Color borderColor = Color(0xFFE5E6EB); +} + +/// 全局尺寸常量 +class AppDimensions { + static const double borderRadius = 16.0; + static const double borderRadiusSmall = 8.0; + static const double borderRadiusMedium = 12.0; + static const double horizontalPadding = 16.0; + static const double moduleSpacing = 20.0; + static const double cardShadowBlur = 4.0; + static const double buttonHeight = 48.0; + static const double appBarHeight = 44.0; +} + +/// 上报类型枚举 +enum ReportType { + deviceFault('设备故障', Icons.notifications_active), + patrolRecord('巡检记录', Icons.description), + hiddenDanger('隐患上报', Icons.shield), + defectReport('缺陷上报', Icons.build); + + final String label; + final IconData icon; + + const ReportType(this.label, this.icon); +} + +/// 问题等级枚举 +enum ProblemLevel { + normal('一般', Color(0xFF86909C), Color(0xFFE5E6EB)), + important('重要', Color(0xFFFF7D00), Color(0xFFE5E6EB)), + urgent('紧急', Color(0xFFF53F3F), Color(0xFFF53F3F)); + + final String label; + final Color textColor; + final Color borderColor; + + const ProblemLevel(this.label, this.textColor, this.borderColor); +} diff --git a/lib/features/v2/report/presentation/cubit/report_cubit.dart b/lib/features/v2/report/presentation/cubit/report_cubit.dart new file mode 100644 index 00000000..95ae5ada --- /dev/null +++ b/lib/features/v2/report/presentation/cubit/report_cubit.dart @@ -0,0 +1,137 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../domain/entities/report_entity.dart'; +import '../../domain/usecases/submit_report_usecase.dart'; +import '../constants/report_constants.dart'; +import '../states/report_state.dart'; + +/// 上报页面 Cubit +class ReportCubit extends Cubit { + final SubmitReportUseCase submitReportUseCase; + + ReportCubit({required this.submitReportUseCase}) + : super(ReportFormState(report: ReportEntity())); + + /// 选择上报类型 + void selectReportType(ReportType type) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + emit(currentState.copyWith( + selectedReportType: type, + report: currentState.report.copyWith( + reportType: type.label, + ), + )); + } + } + + /// 选择设备 + void selectDevice(String device) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + emit(currentState.copyWith( + selectedDevice: device, + report: currentState.report.copyWith(deviceId: device), + )); + } + } + + /// 更新位置 + void updateLocation(String location) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + emit(currentState.copyWith( + location: location, + report: currentState.report.copyWith(location: location), + )); + } + } + + /// 更新问题描述 + void updateDescription(String description) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + emit(currentState.copyWith( + report: currentState.report.copyWith(description: description), + )); + } + } + + /// 选择问题等级 + void selectProblemLevel(ProblemLevel level) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + emit(currentState.copyWith( + selectedProblemLevel: level, + report: currentState.report.copyWith( + problemLevel: level.label, + ), + )); + } + } + + /// 添加媒体文件 + void addMediaFile(String filePath) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + final updatedFiles = List.from(currentState.mediaFiles) + ..add(filePath); + emit(currentState.copyWith( + mediaFiles: updatedFiles, + report: currentState.report.copyWith(mediaUrls: updatedFiles), + )); + } + } + + /// 删除媒体文件 + void removeMediaFile(int index) { + if (state is ReportFormState) { + final currentState = state as ReportFormState; + final updatedFiles = List.from(currentState.mediaFiles) + ..removeAt(index); + emit(currentState.copyWith( + mediaFiles: updatedFiles, + report: currentState.report.copyWith(mediaUrls: updatedFiles), + )); + } + } + + /// 提交上报 + Future submitReport() async { + if (state is! ReportFormState) return; + + final currentState = state as ReportFormState; + + // 验证必填项 + if (currentState.selectedDevice == null || + currentState.selectedDevice!.isEmpty) { + emit(const ReportFailure('请选择设备')); + return; + } + + if (currentState.report.description == null || + currentState.report.description!.isEmpty) { + emit(const ReportFailure('请填写问题描述')); + return; + } + + if (currentState.selectedProblemLevel == null) { + emit(const ReportFailure('请选择问题等级')); + return; + } + + // 开始提交 + emit(ReportSubmitting()); + + final result = await submitReportUseCase.execute(currentState.report); + + result.fold( + (failure) => emit(ReportFailure(failure.message)), + (success) => emit(ReportSuccess()), + ); + } + + /// 重置表单 + void resetForm() { + emit(ReportFormState(report: ReportEntity())); + } +} diff --git a/lib/features/v2/report/presentation/pages/media_preview_page.dart b/lib/features/v2/report/presentation/pages/media_preview_page.dart new file mode 100644 index 00000000..1d22637b --- /dev/null +++ b/lib/features/v2/report/presentation/pages/media_preview_page.dart @@ -0,0 +1,154 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +/// 媒体预览页面 +class MediaPreviewPage extends StatefulWidget { + final List mediaFiles; + final int initialIndex; + + const MediaPreviewPage({ + super.key, + required this.mediaFiles, + required this.initialIndex, + }); + + @override + State createState() => _MediaPreviewPageState(); +} + +class _MediaPreviewPageState extends State { + late PageController _pageController; + VideoPlayerController? _videoController; + int _currentIndex = 0; + + @override + void initState() { + super.initState(); + _currentIndex = widget.initialIndex; + _pageController = PageController(initialPage: widget.initialIndex); + _initializeVideo(widget.mediaFiles[_currentIndex]); + } + + @override + void dispose() { + _pageController.dispose(); + _videoController?.dispose(); + super.dispose(); + } + + /// 初始化视频播放器 + Future _initializeVideo(String filePath) async { + // 先释放之前的控制器 + await _videoController?.dispose(); + _videoController = null; + + if (_isVideoFile(filePath)) { + _videoController = VideoPlayerController.file(File(filePath)); + try { + await _videoController!.initialize(); + setState(() {}); + } catch (e) { + debugPrint('视频初始化失败: $e'); + } + } + } + + /// 判断是否为视频文件 + bool _isVideoFile(String filePath) { + final lowerPath = filePath.toLowerCase(); + return lowerPath.endsWith('.mp4') || + lowerPath.endsWith('.mov') || + lowerPath.endsWith('.avi') || + lowerPath.endsWith('.mkv') || + lowerPath.endsWith('.wmv') || + lowerPath.endsWith('.flv'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + title: Text( + '${_currentIndex + 1}/${widget.mediaFiles.length}', + style: const TextStyle(color: Colors.white), + ), + centerTitle: true, + ), + body: PageView.builder( + controller: _pageController, + itemCount: widget.mediaFiles.length, + onPageChanged: (index) { + setState(() { + _currentIndex = index; + }); + _initializeVideo(widget.mediaFiles[index]); + }, + itemBuilder: (context, index) { + final filePath = widget.mediaFiles[index]; + if (_isVideoFile(filePath)) { + return _buildVideoPreview(); + } else { + return _buildImagePreview(filePath); + } + }, + ), + ); + } + + /// 构建图片预览 + Widget _buildImagePreview(String filePath) { + final bool isNetworkImage = filePath.startsWith('http'); + + return Center( + child: InteractiveViewer( + child: isNetworkImage + ? Image.network( + filePath, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.broken_image, + color: Colors.white, + size: 64, + ); + }, + ) + : Image.file( + File(filePath), + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.broken_image, + color: Colors.white, + size: 64, + ); + }, + ), + ), + ); + } + + /// 构建视频预览 + Widget _buildVideoPreview() { + if (_videoController == null || !_videoController!.value.isInitialized) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + return Center( + child: AspectRatio( + aspectRatio: _videoController!.value.aspectRatio, + child: VideoPlayer(_videoController!), + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/pages/report_page.dart b/lib/features/v2/report/presentation/pages/report_page.dart new file mode 100644 index 00000000..2a6f03f6 --- /dev/null +++ b/lib/features/v2/report/presentation/pages/report_page.dart @@ -0,0 +1,476 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import '../cubit/report_cubit.dart'; +import '../states/report_state.dart'; +import '../constants/report_constants.dart'; +import '../widgets/report_type_selector.dart'; +import '../widgets/device_selector.dart'; +import '../widgets/description_input.dart'; +import '../widgets/media_uploader.dart'; +import '../widgets/level_selector.dart'; +import 'media_preview_page.dart'; +import '../../di/report_di.dart'; + +/// 现场上报主页面 +class ReportPage extends StatelessWidget { + const ReportPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => ReportDependencyInjector.createReportCubit(), + child: _ReportPageContent(), + ); + } +} + +class _ReportPageContent extends StatelessWidget { + const _ReportPageContent(); + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: AppColors.cardBackground, + body: BlocConsumer( + listener: (context, state) { + if (state is ReportSuccess) { + _showSuccessDialog(context); + } else if (state is ReportFailure) { + _showErrorDialog(context, state.message); + } + }, + builder: (context, state) { + if (state is ReportSubmitting) { + return const Center( + child: CircularProgressIndicator( + color: AppColors.primary, + ), + ); + } + + if (state is ReportFormState) { + final cubit = context.read(); + return Column( + children: [ + _buildAppBar(context, state.location, cubit), + Expanded( + child: _buildFormContent(context, state), + ), + ], + ); + } + + return const SizedBox.shrink(); + }, + ), + ), + ); + } + + /// 构建导航栏 + PreferredSizeWidget _buildAppBar(BuildContext context, String location, ReportCubit cubit) { + return PreferredSize( + preferredSize: const Size.fromHeight(100), + child: Container( + color: AppColors.background, + child: SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + child: Column( + children: [ + // 第一行:标题 + 提交按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '现场上报', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: AppColors.textPrimary, + ), + ), + GestureDetector( + onTap: () { + context.read().submitReport(); + }, + child: const Text( + '提交', + style: TextStyle( + color: AppColors.primary, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + // 第二行:位置信息(可点击) + GestureDetector( + onTap: () { + _showLocationPicker(context, cubit); + }, + child: Row( + children: [ + const Icon( + Icons.location_on, + color: AppColors.primary, + size: 16, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + location, + style: TextStyle( + fontSize: 14, + color: AppColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_drop_down, + size: 20, + color: AppColors.textHint, + ), + ], + ), + ), + const SizedBox(height: 8), + ], + ), + ), + ), + ), + ); + } + + /// 构建表单内容 + Widget _buildFormContent(BuildContext context, ReportFormState state) { + final cubit = context.read(); + + return SingleChildScrollView( + padding: const EdgeInsets.only( + top: AppDimensions.moduleSpacing, + bottom: 40, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 上报类型选择 + ReportTypeSelector( + selectedType: state.selectedReportType, + onSelected: (type) => cubit.selectReportType(type), + ), + const SizedBox(height: AppDimensions.moduleSpacing), + + // 设备选择 + DeviceSelector( + selectedDevice: state.selectedDevice, + onTap: () { + _showDevicePicker(context, cubit); + }, + ), + const SizedBox(height: AppDimensions.moduleSpacing), + + // 问题描述 + DescriptionInput( + description: state.report.description, + onChanged: (value) => cubit.updateDescription(value), + ), + const SizedBox(height: AppDimensions.moduleSpacing), + + // 媒体上传 + MediaUploader( + mediaFiles: state.mediaFiles, + onCameraTap: () { + _pickImage(cubit); + }, + onVideoTap: () { + _pickVideo(cubit); + }, + onAddFromGallery: () { + _pickFromGallery(context, cubit); + }, + onTap: (index) { + _previewMedia(context, state.mediaFiles, index); + }, + onRemove: (index) => cubit.removeMediaFile(index), + ), + const SizedBox(height: AppDimensions.moduleSpacing), + + // 问题等级选择 + LevelSelector( + selectedLevel: state.selectedProblemLevel, + onSelected: (level) => cubit.selectProblemLevel(level), + ), + const SizedBox(height: 40), + ], + ), + ); + } + + /// 构建位置信息行 + Widget _buildLocationRow(BuildContext context, String location) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadius), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + const Icon( + Icons.location_on, + color: AppColors.primary, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + location, + style: TextStyle( + fontSize: 16, + color: AppColors.textPrimary, + ), + ), + ), + ], + ), + ); + } + + /// 选择图片 + Future _pickImage(ReportCubit cubit) async { + final ImagePicker picker = ImagePicker(); + try { + final XFile? image = await picker.pickImage( + source: ImageSource.camera, + maxWidth: 1920, + maxHeight: 1080, + imageQuality: 85, + ); + + if (image != null) { + cubit.addMediaFile(image.path); + } + } catch (e) { + debugPrint('拍照失败: $e'); + } + } + + /// 选择视频 + Future _pickVideo(ReportCubit cubit) async { + final ImagePicker picker = ImagePicker(); + try { + final XFile? video = await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 5), + ); + + if (video != null) { + cubit.addMediaFile(video.path); + } + } catch (e) { + debugPrint('录像失败: $e'); + } + } + + /// 预览媒体文件 + void _previewMedia(BuildContext context, List mediaFiles, int index) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => MediaPreviewPage( + mediaFiles: mediaFiles, + initialIndex: index, + ), + ), + ); + } + + /// 从相册选择 + Future _pickFromGallery(BuildContext context, ReportCubit cubit) async { + final ImagePicker picker = ImagePicker(); + try { + // 显示选择图片或视频的选项 + await showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.image), + title: const Text('选择图片'), + onTap: () async { + Navigator.pop(context); + final XFile? image = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: 1920, + maxHeight: 1080, + imageQuality: 85, + ); + if (image != null) { + cubit.addMediaFile(image.path); + } + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: const Text('选择视频'), + onTap: () async { + Navigator.pop(context); + final XFile? video = await picker.pickVideo( + source: ImageSource.gallery, + ); + if (video != null) { + cubit.addMediaFile(video.path); + } + }, + ), + ], + ), + ), + ); + } catch (e) { + debugPrint('从相册选择失败: $e'); + } + } + + /// 显示位置选择器 + void _showLocationPicker(BuildContext context, ReportCubit cubit) { + final locations = [ + '主厂房A区', + '主厂房B区', + '锅炉房', + '汽机房', + '控制室', + '配电室', + ]; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('选择位置'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: locations.map((location) { + return ListTile( + title: Text(location), + onTap: () { + cubit.updateLocation(location); + Navigator.pop(context); + }, + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ], + ), + ); + } + + /// 显示设备选择器 + void _showDevicePicker(BuildContext context, ReportCubit cubit) { + final devices = [ + '锅炉1号', + '锅炉2号', + '汽轮机1号', + '发电机1号', + '变压器1号', + '水泵1号', + ]; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('选择设备'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: devices.map((device) { + return ListTile( + title: Text(device), + onTap: () { + cubit.selectDevice(device); + Navigator.pop(context); + }, + ); + }).toList(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + ], + ), + ); + } + + /// 显示成功对话框 + void _showSuccessDialog(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('提交成功'), + content: const Text('您的上报已成功提交'), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); // 返回上一页 + }, + child: const Text('确定'), + ), + ], + ), + ); + } + + /// 显示错误对话框 + void _showErrorDialog(BuildContext context, String message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('提交失败'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('确定'), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/states/report_state.dart b/lib/features/v2/report/presentation/states/report_state.dart new file mode 100644 index 00000000..826fb633 --- /dev/null +++ b/lib/features/v2/report/presentation/states/report_state.dart @@ -0,0 +1,80 @@ +import 'package:equatable/equatable.dart'; +import '../../domain/entities/report_entity.dart'; +import '../constants/report_constants.dart'; + +/// 上报页面状态抽象类 +abstract class ReportState extends Equatable { + const ReportState(); + + @override + List get props => []; +} + +/// 初始状态 +class ReportInitial extends ReportState {} + +/// 加载中状态 +class ReportLoading extends ReportState {} + +/// 提交中状态 +class ReportSubmitting extends ReportState {} + +/// 提交成功状态 +class ReportSuccess extends ReportState {} + +/// 提交失败状态 +class ReportFailure extends ReportState { + final String message; + + const ReportFailure(this.message); + + @override + List get props => [message]; +} + +/// 表单数据状态 +class ReportFormState extends ReportState { + final ReportEntity report; + final ReportType? selectedReportType; + final ProblemLevel? selectedProblemLevel; + final String location; + final String? selectedDevice; + final List mediaFiles; + + const ReportFormState({ + required this.report, + this.selectedReportType, + this.selectedProblemLevel, + this.location = '江苏省苏州市吴中区', + this.selectedDevice, + this.mediaFiles = const [], + }); + + ReportFormState copyWith({ + ReportEntity? report, + ReportType? selectedReportType, + ProblemLevel? selectedProblemLevel, + String? location, + String? selectedDevice, + List? mediaFiles, + }) { + return ReportFormState( + report: report ?? this.report, + selectedReportType: selectedReportType ?? this.selectedReportType, + selectedProblemLevel: selectedProblemLevel ?? this.selectedProblemLevel, + location: location ?? this.location, + selectedDevice: selectedDevice ?? this.selectedDevice, + mediaFiles: mediaFiles ?? this.mediaFiles, + ); + } + + @override + List get props => [ + report, + selectedReportType, + selectedProblemLevel, + location, + selectedDevice, + mediaFiles, + ]; +} diff --git a/lib/features/v2/report/presentation/widgets/description_input.dart b/lib/features/v2/report/presentation/widgets/description_input.dart new file mode 100644 index 00000000..839f223b --- /dev/null +++ b/lib/features/v2/report/presentation/widgets/description_input.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import '../constants/report_constants.dart'; + +/// 问题描述输入模块 +class DescriptionInput extends StatelessWidget { + final String? description; + final Function(String) onChanged; + + const DescriptionInput({ + super.key, + this.description, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadius), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '问题描述', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 12), + TextField( + maxLines: 5, + maxLength: 200, + onChanged: onChanged, + decoration: InputDecoration( + hintText: '请详细描述问题现象...', + hintStyle: TextStyle(color: AppColors.textHint), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + borderSide: BorderSide(color: AppColors.borderColor), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + borderSide: BorderSide(color: AppColors.borderColor), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + borderSide: BorderSide(color: AppColors.primary, width: 2), + ), + counterText: '', + ), + style: TextStyle( + fontSize: 14, + color: AppColors.textPrimary, + ), + ), + Align( + alignment: Alignment.centerRight, + child: Text( + '${description?.length ?? 0}/200', + style: TextStyle( + fontSize: 12, + color: AppColors.textHint, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/widgets/device_selector.dart b/lib/features/v2/report/presentation/widgets/device_selector.dart new file mode 100644 index 00000000..dd40ea52 --- /dev/null +++ b/lib/features/v2/report/presentation/widgets/device_selector.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import '../constants/report_constants.dart'; + +/// 设备选择器 +class DeviceSelector extends StatelessWidget { + final String? selectedDevice; + final VoidCallback onTap; + + const DeviceSelector({ + super.key, + this.selectedDevice, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadius), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '设备选择', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + Row( + children: [ + Text( + selectedDevice ?? '请选择设备', + style: TextStyle( + fontSize: 14, + color: selectedDevice != null + ? AppColors.textPrimary + : AppColors.textHint, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_drop_down, + size: 20, + color: AppColors.textHint, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/widgets/level_selector.dart b/lib/features/v2/report/presentation/widgets/level_selector.dart new file mode 100644 index 00000000..8e05e7bc --- /dev/null +++ b/lib/features/v2/report/presentation/widgets/level_selector.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import '../constants/report_constants.dart'; + +/// 问题等级选择器 +class LevelSelector extends StatelessWidget { + final ProblemLevel? selectedLevel; + final Function(ProblemLevel) onSelected; + + const LevelSelector({ + super.key, + this.selectedLevel, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadius), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '问题等级', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 12), + Row( + children: ProblemLevel.values.map((level) { + final isSelected = selectedLevel == level; + return Expanded( + child: GestureDetector( + onTap: () => onSelected(level), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + height: AppDimensions.buttonHeight, + decoration: BoxDecoration( + color: isSelected && level == ProblemLevel.urgent + ? AppColors.danger.withOpacity(0.1) + : AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + border: Border.all( + color: isSelected ? level.borderColor : AppColors.borderColor, + width: isSelected ? 2 : 1, + ), + ), + child: Center( + child: Text( + level.label, + style: TextStyle( + fontSize: 14, + color: isSelected ? level.textColor : level.textColor.withOpacity(0.6), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/widgets/media_uploader.dart b/lib/features/v2/report/presentation/widgets/media_uploader.dart new file mode 100644 index 00000000..b6cb7303 --- /dev/null +++ b/lib/features/v2/report/presentation/widgets/media_uploader.dart @@ -0,0 +1,259 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import '../constants/report_constants.dart'; + +/// 媒体上传模块 +class MediaUploader extends StatelessWidget { + final List mediaFiles; + final VoidCallback onCameraTap; + final VoidCallback onVideoTap; + final VoidCallback onAddFromGallery; + final Function(int) onRemove; + final Function(int)? onTap; + + const MediaUploader({ + super.key, + required this.mediaFiles, + required this.onCameraTap, + required this.onVideoTap, + required this.onAddFromGallery, + required this.onRemove, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + horizontal: AppDimensions.horizontalPadding, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadius), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '上传图片/视频', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + ), + const SizedBox(height: 12), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + // 拍照按钮 + _buildUploadButton( + icon: Icons.camera_alt, + label: '拍照', + onTap: onCameraTap, + ), + const SizedBox(width: 8), + // 录像按钮 + _buildUploadButton( + icon: Icons.videocam, + label: '录像', + onTap: onVideoTap, + ), + const SizedBox(width: 8), + // 已上传的媒体预览 + ...mediaFiles.asMap().entries.map((entry) { + final String filePath = entry.value; + final bool isNetworkImage = filePath.startsWith('http'); + final bool isVideo = _isVideoFile(filePath); + + return GestureDetector( + onTap: () => onTap?.call(entry.key), + child: Stack( + children: [ + Container( + width: 80, + height: 80, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + color: AppColors.cardBackground, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + child: isVideo + ? _buildVideoThumbnail(filePath) + : (isNetworkImage + ? Image.network( + filePath, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildPlaceholderIcon(Icons.broken_image); + }, + ) + : Image.file( + File(filePath), + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _buildPlaceholderIcon(Icons.broken_image); + }, + )), + ), + ), + Positioned( + top: 4, + right: 12, + child: GestureDetector( + onTap: () => onRemove(entry.key), + child: Container( + width: 20, + height: 20, + decoration: const BoxDecoration( + color: Colors.black54, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.close, + size: 14, + color: Colors.white, + ), + ), + ), + ), + // 视频播放图标 + if (isVideo) + Positioned( + child: Container( + width: 80, + height: 80, + margin: const EdgeInsets.only(right: 8), + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.all(8), + decoration: const BoxDecoration( + color: Colors.black38, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.play_arrow, + color: Colors.white, + size: 24, + ), + ), + ), + ), + ], + ), + ); + }).toList(), + // 添加按钮 + _buildUploadButton( + icon: Icons.add, + label: '', + onTap: onAddFromGallery, + isAddButton: true, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildUploadButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + bool isAddButton = false, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: isAddButton ? AppColors.cardBackground : AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusMedium), + border: Border.all( + color: isAddButton ? AppColors.borderColor : AppColors.primary, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: isAddButton ? AppColors.textHint : AppColors.primary, + size: 24, + ), + if (label.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: isAddButton ? AppColors.textHint : AppColors.primary, + ), + ), + ], + ], + ), + ), + ); + } + + /// 判断是否为视频文件 + bool _isVideoFile(String filePath) { + final lowerPath = filePath.toLowerCase(); + return lowerPath.endsWith('.mp4') || + lowerPath.endsWith('.mov') || + lowerPath.endsWith('.avi') || + lowerPath.endsWith('.mkv') || + lowerPath.endsWith('.wmv') || + lowerPath.endsWith('.flv'); + } + + /// 构建视频缩略图(显示占位符) + Widget _buildVideoThumbnail(String filePath) { + return Stack( + fit: StackFit.expand, + children: [ + // 灰色背景 + Container( + color: Colors.grey[300], + ), + // 视频图标 + const Center( + child: Icon( + Icons.videocam, + color: Colors.grey, + size: 32, + ), + ), + ], + ); + } + + /// 构建占位图标 + Widget _buildPlaceholderIcon(IconData icon) { + return Container( + color: Colors.grey[300], + child: Center( + child: Icon( + icon, + color: Colors.grey, + size: 32, + ), + ), + ); + } +} diff --git a/lib/features/v2/report/presentation/widgets/report_type_selector.dart b/lib/features/v2/report/presentation/widgets/report_type_selector.dart new file mode 100644 index 00000000..e262f21a --- /dev/null +++ b/lib/features/v2/report/presentation/widgets/report_type_selector.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import '../constants/report_constants.dart'; + +/// 上报类型选择器 +class ReportTypeSelector extends StatelessWidget { + final ReportType? selectedType; + final Function(ReportType) onSelected; + + const ReportTypeSelector({ + super.key, + this.selectedType, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: AppDimensions.horizontalPadding), + child: Row( + children: ReportType.values.map((type) { + final isSelected = selectedType == type; + return Expanded( + child: GestureDetector( + onTap: () => onSelected(type), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.background, + borderRadius: BorderRadius.circular(AppDimensions.borderRadiusSmall), + border: Border.all( + color: isSelected ? AppColors.primary : AppColors.borderColor, + width: isSelected ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: const Color(0x0D000000), + blurRadius: AppDimensions.cardShadowBlur, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + type.icon, + color: isSelected ? AppColors.primary : AppColors.textHint, + size: 24, + ), + const SizedBox(height: 8), + Text( + type.label, + style: TextStyle( + fontSize: 12, + color: isSelected ? AppColors.primary : AppColors.textSecondary, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ); + } +} diff --git a/lib/features/v2/report/report.dart b/lib/features/v2/report/report.dart new file mode 100644 index 00000000..43b09c9b --- /dev/null +++ b/lib/features/v2/report/report.dart @@ -0,0 +1,28 @@ +/// 现场上报模块 - 统一导出 +/// +/// 使用方式: +/// ```dart +/// import 'package:maibu_satabot_v2/features/v2/report/report.dart'; +/// ``` + +// 常量 +export 'presentation/constants/report_constants.dart'; + +// 状态 +export 'presentation/states/report_state.dart'; + +// Cubit +export 'presentation/cubit/report_cubit.dart'; + +// 页面 +export 'presentation/pages/report_page.dart'; + +// 组件 +export 'presentation/widgets/report_type_selector.dart'; +export 'presentation/widgets/device_selector.dart'; +export 'presentation/widgets/description_input.dart'; +export 'presentation/widgets/media_uploader.dart'; +export 'presentation/widgets/level_selector.dart'; + +// 依赖注入 +export 'di/report_di.dart'; diff --git a/lib/features/v2/waring_center/DEPENDENCIES.md b/lib/features/v2/waring_center/DEPENDENCIES.md new file mode 100644 index 00000000..0ea53602 --- /dev/null +++ b/lib/features/v2/waring_center/DEPENDENCIES.md @@ -0,0 +1,168 @@ +# 告警中心页面 - 依赖清单 + +## pubspec.yaml 依赖 + +### 已存在的依赖(本项目已有) +```yaml +dependencies: + flutter: + sdk: flutter + + # 状态管理 + flutter_bloc: ^9.1.1 + bloc: ^9.2.0 + + # 等价判断 + equatable: ^2.0.8 + + # 函数式编程 + fpdart: ^1.2.0 + + # 依赖注入 + get_it: ^9.2.0 + + # 路由管理 + go_router: ^17.0.1 + + # 网络请求 + dio: ^5.9.0 +``` + +### 新增依赖 +**无需新增任何依赖**,所有功能均使用项目已有的稳定库。 + +--- + +## 文件结构 + +``` +lib/features/v2/waring_center/ +├── data/ +│ ├── datasources/ +│ │ ├── alarm_remote_datasource.dart # 远程数据源抽象 +│ │ └── impl/ +│ │ └── alarm_remote_datasource_impl.dart # 远程数据源实现(模拟数据) +│ ├── models/ +│ │ └── alarm_model.dart # 数据模型 +│ └── repositories/ +│ └── alarm_repository_impl.dart # 仓储实现 +├── domain/ +│ ├── entities/ +│ │ ├── alarm_entity.dart # 告警实体 +│ │ └── alarm_count_entity.dart # 告警统计实体 +│ ├── repositories/ +│ │ └── alarm_repository.dart # 仓储接口 +│ └── usecases/ +│ ├── get_alarm_list_usecase.dart # 获取告警列表用例 +│ └── get_alarm_count_usecase.dart # 获取告警统计用例 +└── presentation/ + ├── bloc/ + │ ├── alarm_cubit.dart # 状态管理 Cubit + │ └── alarm_state.dart # 状态定义 + ├── constants/ + │ └── alarm_constants.dart # 全局常量(颜色、尺寸、枚举) + ├── pages/ + │ └── alarm_center_page.dart # 主页面 + ├── routes/ + │ └── alarm_center_routes.dart # 路由配置 + └── widgets/ + ├── alarm_tab_bar.dart # 筛选标签栏 + ├── alarm_count_card.dart # 告警统计卡片 + ├── alarm_item_card.dart # 告警项卡片 + └── ai_diagnosis_card.dart # AI 诊断建议卡片 +``` + +--- + +## 核心功能 + +### 1. 清洁架构四层结构 +- **Presentation(表现层)**: UI 展示、用户交互、Cubit、Pages、Widgets、State +- **Domain(领域层)**: 核心业务逻辑、Entities、UseCases、Repository 接口 +- **Data(数据层)**: 数据获取存储、Models、Repository 实现、Datasources +- **Core(核心层)**: 网络、日志、依赖注入 DI、错误处理、全局常量 + +### 2. 状态管理 +- 使用 Cubit 进行状态管理 +- 支持三种状态:加载中、加载成功、加载失败 +- 支持下拉刷新 +- 支持筛选标签切换 + +### 3. UI 组件 +- 顶部导航栏(告警中心标题 + 筛选按钮) +- 一级标签栏(全部/未处理/已确认/已恢复) +- 告警统计卡片(2x2 网格布局) +- 告警列表(可滚动) +- AI 诊断建议卡片 + +### 4. 交互功能 +- Tab 切换筛选告警 +- 下拉刷新数据 +- 点击筛选按钮弹出筛选弹窗 +- 点击告警项跳转详情(占位) +- 点击 AI 诊断查看详情(占位) + +--- + +## 全局常量配置 + +### 颜色 +- 主色调: `Color(0xFF165DFF)` +- 高危色: `Color(0xFFF53F3F)` +- 中危色: `Color(0xFFFF7D00)` +- 低危色: `Color(0xFF00B42A)` +- 提示色: `Color(0xFF69b1ff)` +- 背景色: `Color(0xFFFFFFFF)` +- 主要文字: `Color(0xFF1D2129)` +- 次要文字: `Color(0xFF4E5969)` +- 辅助文字: `Color(0xFF86909C)` + +### 尺寸 +- 通用圆角: `16.0` +- 水平边距: `16.0` +- 模块间距: `20.0` +- 导航栏高度: `44.0` + +### 阴影 +```dart +BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), +) +``` + +--- + +## 模拟数据 + +### 告警列表 +1. 逆变器 INV-001 通讯异常(高危,未确认) +2. 汇流箱 SCB-003 过温告警(中危,未确认) +3. 组件串 STR-024 功率异常(低危,已确认) +4. 环境辐照度传感器离线(提示,已确认) + +### 告警统计 +- 未处理: 8 +- 处理中: 2 +- 已确认: 34 +- 今日新增: 12 + +--- + +## 适配说明 +- ✅ iOS 安全区适配(AnnotatedRegion) +- ✅ Android 全面屏适配 +- ✅ 下拉刷新支持 +- ✅ 响应式布局 + +--- + +## 后续扩展建议 +1. 对接真实 API 接口(替换 AlarmRemoteDataSourceImpl) +2. 实现告警详情页 +3. 实现筛选功能(告警等级、时间、区域) +4. 添加告警推送通知 +5. 支持批量处理告警 +6. 添加告警历史记录 +7. 支持告警导出功能 diff --git a/lib/features/v2/waring_center/data/datasources/alarm_remote_datasource.dart b/lib/features/v2/waring_center/data/datasources/alarm_remote_datasource.dart new file mode 100644 index 00000000..09504dd6 --- /dev/null +++ b/lib/features/v2/waring_center/data/datasources/alarm_remote_datasource.dart @@ -0,0 +1,10 @@ +import 'package:maibu_satabot_v2/features/v2/waring_center/data/models/alarm_model.dart'; + +/// 告警远程数据源抽象 +abstract class AlarmRemoteDataSource { + /// 获取告警列表 + Future> getAlarmList(); + + /// 获取告警统计 + Future getAlarmCount(); +} diff --git a/lib/features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart b/lib/features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart new file mode 100644 index 00000000..ebcbd7ae --- /dev/null +++ b/lib/features/v2/waring_center/data/datasources/impl/alarm_remote_datasource_impl.dart @@ -0,0 +1,65 @@ +import 'package:maibu_satabot_v2/features/v2/waring_center/data/datasources/alarm_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/data/models/alarm_model.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警远程数据源实现(模拟数据) +class AlarmRemoteDataSourceImpl implements AlarmRemoteDataSource { + @override + Future> getAlarmList() async { + // 模拟网络延迟 + await Future.delayed(const Duration(seconds: 1)); + + return [ + AlarmModel( + id: '1', + title: '逆变器INV-001通讯异常', + area: 'A区', + device: 'INV-001', + time: '05-21 10:23', + level: AlarmLevel.danger, + status: AlarmStatus.unconfirmed, + aiDiagnosis: 'INV-001通讯异常可能由网络波动或采集器故障引起,建议检查网络连接。', + ), + AlarmModel( + id: '2', + title: '汇流箱SCB-003过温告警', + area: 'B区', + device: 'SCB-003', + time: '05-21 09:45', + level: AlarmLevel.warning, + status: AlarmStatus.unconfirmed, + ), + AlarmModel( + id: '3', + title: '组件串STR-024功率异常', + area: 'C区', + device: 'STR-024', + time: '05-21 08:32', + level: AlarmLevel.low, + status: AlarmStatus.confirmed, + ), + AlarmModel( + id: '4', + title: '环境辐照度传感器离线', + area: 'D区', + device: 'SEN-001', + time: '05-21 07:15', + level: AlarmLevel.info, + status: AlarmStatus.confirmed, + ), + ]; + } + + @override + Future getAlarmCount() async { + // 模拟网络延迟 + await Future.delayed(const Duration(milliseconds: 500)); + + return AlarmCountModel( + unprocessed: 8, + processing: 2, + confirmed: 34, + todayNew: 12, + ); + } +} diff --git a/lib/features/v2/waring_center/data/models/alarm_model.dart b/lib/features/v2/waring_center/data/models/alarm_model.dart new file mode 100644 index 00000000..3eae3dc0 --- /dev/null +++ b/lib/features/v2/waring_center/data/models/alarm_model.dart @@ -0,0 +1,155 @@ +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警数据模型 +class AlarmModel { + AlarmModel({ + required this.id, + required this.title, + required this.area, + required this.device, + required this.time, + required this.level, + required this.status, + this.aiDiagnosis, + }); + + final String id; + final String title; + final String area; + final String device; + final String time; + final AlarmLevel level; + final AlarmStatus status; + final String? aiDiagnosis; + + /// 从 JSON 创建 + factory AlarmModel.fromJson(Map json) { + return AlarmModel( + id: json['id'] as String, + title: json['title'] as String, + area: json['area'] as String, + device: json['device'] as String, + time: json['time'] as String, + level: _parseLevel(json['level'] as String), + status: _parseStatus(json['status'] as String), + aiDiagnosis: json['aiDiagnosis'] as String?, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'id': id, + 'title': title, + 'area': area, + 'device': device, + 'time': time, + 'level': level.name, + 'status': status.name, + 'aiDiagnosis': aiDiagnosis, + }; + } + + /// 转换为实体 + AlarmEntity toEntity() { + return AlarmEntity( + id: id, + title: title, + area: area, + device: device, + time: time, + level: level, + status: status, + aiDiagnosis: aiDiagnosis, + ); + } + + /// 从实体创建 + factory AlarmModel.fromEntity(AlarmEntity entity) { + return AlarmModel( + id: entity.id, + title: entity.title, + area: entity.area, + device: entity.device, + time: entity.time, + level: entity.level, + status: entity.status, + aiDiagnosis: entity.aiDiagnosis, + ); + } + + static AlarmLevel _parseLevel(String level) { + switch (level) { + case 'danger': + return AlarmLevel.danger; + case 'warning': + return AlarmLevel.warning; + case 'low': + return AlarmLevel.low; + case 'info': + return AlarmLevel.info; + default: + return AlarmLevel.info; + } + } + + static AlarmStatus _parseStatus(String status) { + switch (status) { + case 'unconfirmed': + return AlarmStatus.unconfirmed; + case 'confirmed': + return AlarmStatus.confirmed; + case 'recovered': + return AlarmStatus.recovered; + default: + return AlarmStatus.unconfirmed; + } + } +} + +/// 告警统计数据模型 +class AlarmCountModel { + AlarmCountModel({ + required this.unprocessed, + required this.processing, + required this.confirmed, + required this.todayNew, + }); + + final int unprocessed; + final int processing; + final int confirmed; + final int todayNew; + + /// 从 JSON 创建 + factory AlarmCountModel.fromJson(Map json) { + return AlarmCountModel( + unprocessed: json['unprocessed'] as int, + processing: json['processing'] as int, + confirmed: json['confirmed'] as int, + todayNew: json['todayNew'] as int, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'unprocessed': unprocessed, + 'processing': processing, + 'confirmed': confirmed, + 'todayNew': todayNew, + }; + } + + /// 转换为实体 + AlarmCountEntity toEntity() { + return AlarmCountEntity( + unprocessed: unprocessed, + processing: processing, + confirmed: confirmed, + todayNew: todayNew, + ); + } +} diff --git a/lib/features/v2/waring_center/data/repositories/alarm_repository_impl.dart b/lib/features/v2/waring_center/data/repositories/alarm_repository_impl.dart new file mode 100644 index 00000000..49b442cb --- /dev/null +++ b/lib/features/v2/waring_center/data/repositories/alarm_repository_impl.dart @@ -0,0 +1,59 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart' as core_error; +import 'package:maibu_satabot_v2/features/v2/waring_center/data/datasources/alarm_remote_datasource.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_repository.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警仓储实现 +class AlarmRepositoryImpl implements AlarmRepository { + AlarmRepositoryImpl(this._remoteDataSource); + + final AlarmRemoteDataSource _remoteDataSource; + + @override + Future>> getAlarmList({ + AlarmFilterTab? filter, + }) async { + try { + final models = await _remoteDataSource.getAlarmList(); + final entities = models.map((model) => model.toEntity()).toList(); + + // 根据筛选条件过滤 + if (filter != null) { + final filtered = _filterAlarms(entities, filter); + return right(filtered); + } + + return right(entities); + } catch (e) { + return left(core_error.ServerFailure('获取告警列表失败: $e')); + } + } + + @override + Future> getAlarmCount() async { + try { + final model = await _remoteDataSource.getAlarmCount(); + return right(model.toEntity()); + } catch (e) { + return left(core_error.ServerFailure('获取告警统计失败: $e')); + } + } + + /// 筛选告警 + List _filterAlarms(List alarms, AlarmFilterTab filter) { + switch (filter) { + case AlarmFilterTab.unprocessed: + return alarms.where((alarm) => alarm.status == AlarmStatus.unconfirmed).toList(); + case AlarmFilterTab.confirmed: + return alarms.where((alarm) => alarm.status == AlarmStatus.confirmed).toList(); + case AlarmFilterTab.recovered: + return alarms.where((alarm) => alarm.status == AlarmStatus.recovered).toList(); + case AlarmFilterTab.all: + return alarms; + } + } +} diff --git a/lib/features/v2/waring_center/domain/entities/alarm_count_entity.dart b/lib/features/v2/waring_center/domain/entities/alarm_count_entity.dart new file mode 100644 index 00000000..15a59bec --- /dev/null +++ b/lib/features/v2/waring_center/domain/entities/alarm_count_entity.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; + +/// 告警统计实体 +class AlarmCountEntity extends Equatable { + const AlarmCountEntity({ + required this.unprocessed, + required this.processing, + required this.confirmed, + required this.todayNew, + }); + + /// 未处理 + final int unprocessed; + + /// 处理中 + final int processing; + + /// 已确认 + final int confirmed; + + /// 今日新增 + final int todayNew; + + @override + List get props => [ + unprocessed, + processing, + confirmed, + todayNew, + ]; +} diff --git a/lib/features/v2/waring_center/domain/entities/alarm_entity.dart b/lib/features/v2/waring_center/domain/entities/alarm_entity.dart new file mode 100644 index 00000000..ae39df58 --- /dev/null +++ b/lib/features/v2/waring_center/domain/entities/alarm_entity.dart @@ -0,0 +1,52 @@ +import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警详情实体 +class AlarmEntity extends Equatable { + const AlarmEntity({ + required this.id, + required this.title, + required this.area, + required this.device, + required this.time, + required this.level, + required this.status, + this.aiDiagnosis, + }); + + /// 告警 ID + final String id; + + /// 告警标题 + final String title; + + /// 区域 + final String area; + + /// 设备 + final String device; + + /// 时间 + final String time; + + /// 告警等级 + final AlarmLevel level; + + /// 处理状态 + final AlarmStatus status; + + /// AI 诊断建议(可选) + final String? aiDiagnosis; + + @override + List get props => [ + id, + title, + area, + device, + time, + level, + status, + aiDiagnosis, + ]; +} diff --git a/lib/features/v2/waring_center/domain/repositories/alarm_repository.dart b/lib/features/v2/waring_center/domain/repositories/alarm_repository.dart new file mode 100644 index 00000000..1140574d --- /dev/null +++ b/lib/features/v2/waring_center/domain/repositories/alarm_repository.dart @@ -0,0 +1,17 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警仓储接口 +abstract class AlarmRepository { + /// 获取告警列表 + /// [filter] 筛选条件 + Future>> getAlarmList({ + AlarmFilterTab? filter, + }); + + /// 获取告警统计 + Future> getAlarmCount(); +} diff --git a/lib/features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart b/lib/features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart new file mode 100644 index 00000000..a4c3fafb --- /dev/null +++ b/lib/features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart @@ -0,0 +1,16 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_repository.dart'; + +/// 获取告警统计用例 +class GetAlarmCountUseCase { + GetAlarmCountUseCase(this._repository); + + final AlarmRepository _repository; + + /// 执行用例 + Future> call() async { + return await _repository.getAlarmCount(); + } +} diff --git a/lib/features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart b/lib/features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart new file mode 100644 index 00000000..8df56574 --- /dev/null +++ b/lib/features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart @@ -0,0 +1,20 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/repositories/alarm_repository.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 获取告警列表用例 +class GetAlarmListUseCase { + GetAlarmListUseCase(this._repository); + + final AlarmRepository _repository; + + /// 执行用例 + /// [filter] 筛选条件 + Future>> call({ + AlarmFilterTab? filter, + }) async { + return await _repository.getAlarmList(filter: filter); + } +} diff --git a/lib/features/v2/waring_center/presentation/bloc/alarm_cubit.dart b/lib/features/v2/waring_center/presentation/bloc/alarm_cubit.dart new file mode 100644 index 00000000..66b6d26c --- /dev/null +++ b/lib/features/v2/waring_center/presentation/bloc/alarm_cubit.dart @@ -0,0 +1,76 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/usecases/get_alarm_count_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/usecases/get_alarm_list_usecase.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警中心 Cubit +class AlarmCubit extends Cubit { + AlarmCubit({ + required GetAlarmListUseCase getAlarmListUseCase, + required GetAlarmCountUseCase getAlarmCountUseCase, + }) : _getAlarmListUseCase = getAlarmListUseCase, + _getAlarmCountUseCase = getAlarmCountUseCase, + super(AlarmInitial()); + + final GetAlarmListUseCase _getAlarmListUseCase; + final GetAlarmCountUseCase _getAlarmCountUseCase; + + /// 加载告警数据 + Future loadAlarms({AlarmFilterTab? filter}) async { + // 如果不是初始加载,保留旧数据 + final isInitialLoad = state is AlarmInitial; + if (isInitialLoad) { + emit(AlarmLoading()); + } + + try { + // 并行获取告警列表和统计 + final alarmsResult = await _getAlarmListUseCase(filter: filter); + final countResult = await _getAlarmCountUseCase(); + + final alarms = alarmsResult.fold( + (failure) => [], + (alarms) => alarms, + ); + + final count = countResult.fold( + (failure) => null, + (count) => count, + ); + + if (count == null) { + emit(const AlarmError('获取告警统计失败')); + return; + } + + emit(AlarmLoaded( + alarms: alarms, + count: count, + selectedFilter: filter ?? AlarmFilterTab.all, + )); + } catch (e) { + emit(AlarmError('加载告警数据失败: $e')); + } + } + + /// 切换筛选标签 + Future changeFilter(AlarmFilterTab filter) async { + final currentState = state; + if (currentState is AlarmLoaded) { + emit(currentState.copyWith(selectedFilter: filter)); + await loadAlarms(filter: filter); + } + } + + /// 刷新数据 + Future refresh() async { + final currentState = state; + if (currentState is AlarmLoaded) { + await loadAlarms(filter: currentState.selectedFilter); + } else { + await loadAlarms(); + } + } +} diff --git a/lib/features/v2/waring_center/presentation/bloc/alarm_state.dart b/lib/features/v2/waring_center/presentation/bloc/alarm_state.dart new file mode 100644 index 00000000..877344d9 --- /dev/null +++ b/lib/features/v2/waring_center/presentation/bloc/alarm_state.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警中心状态 +abstract class AlarmState extends Equatable { + const AlarmState(); + + @override + List get props => []; +} + +/// 初始状态 +class AlarmInitial extends AlarmState {} + +/// 加载中状态 +class AlarmLoading extends AlarmState {} + +/// 加载成功状态 +class AlarmLoaded extends AlarmState { + const AlarmLoaded({ + required this.alarms, + required this.count, + this.selectedFilter = AlarmFilterTab.all, + }); + + final List alarms; + final AlarmCountEntity count; + final AlarmFilterTab selectedFilter; + + @override + List get props => [alarms, count, selectedFilter]; + + AlarmLoaded copyWith({ + List? alarms, + AlarmCountEntity? count, + AlarmFilterTab? selectedFilter, + }) { + return AlarmLoaded( + alarms: alarms ?? this.alarms, + count: count ?? this.count, + selectedFilter: selectedFilter ?? this.selectedFilter, + ); + } +} + +/// 加载失败状态 +class AlarmError extends AlarmState { + const AlarmError(this.message); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/features/v2/waring_center/presentation/constants/alarm_constants.dart b/lib/features/v2/waring_center/presentation/constants/alarm_constants.dart new file mode 100644 index 00000000..4c60d7f5 --- /dev/null +++ b/lib/features/v2/waring_center/presentation/constants/alarm_constants.dart @@ -0,0 +1,119 @@ +/// 告警中心全局常量 +/// 包含颜色、尺寸、枚举等全局配置 +library; + +import 'package:flutter/material.dart'; + +/// 颜色常量 +class AlarmColors { + /// 主色调 + static const Color primary = Color(0xFF165DFF); + + /// 高危色 - 红色告警 + static const Color danger = Color(0xFFF53F3F); + + /// 中危色 - 橙色告警 + static const Color warning = Color(0xFFFF7D00); + + /// 低危色 - 绿色告警 + static const Color success = Color(0xFF00B42A); + + /// 提示色 - 蓝色提示 + static const Color info = Color(0xFF69b1ff); + + /// 背景色 + static const Color background = Color(0xFFFFFFFF); + + /// 主要文字色 + static const Color textPrimary = Color(0xFF1D2129); + + /// 次要文字色 + static const Color textSecondary = Color(0xFF4E5969); + + /// 辅助文字色 + static const Color textTertiary = Color(0xFF86909C); + + /// 未处理背景色 + static const Color unprocessedBg = Color(0xFFFFF0F0); + + /// AI诊断卡片背景色 + static const Color aiCardBg = Color(0xFFF0F7FF); +} + +/// 尺寸常量 +class AlarmDimensions { + /// 通用圆角 + static const double borderRadius = 16.0; + + /// 水平边距 + static const double horizontalPadding = 16.0; + + /// 模块间距 + static const double moduleSpacing = 20.0; + + /// 导航栏高度 + static const double appBarHeight = 44.0; + + /// 卡片阴影 + static BoxShadow cardShadow = const BoxShadow( + color: Color(0x0D000000), + blurRadius: 4, + offset: Offset(0, 2), + ); +} + +/// 告警等级枚举 +enum AlarmLevel { + /// 高危 + danger('高危', AlarmColors.danger, Icons.warning_rounded), + + /// 中危 + warning('中危', AlarmColors.warning, Icons.warning_amber_rounded), + + /// 低危 + low('低危', AlarmColors.success, Icons.info_rounded), + + /// 提示 + info('提示', AlarmColors.info, Icons.info_outline_rounded); + + const AlarmLevel(this.label, this.color, this.icon); + + /// 显示文本 + final String label; + + /// 颜色 + final Color color; + + /// 图标 + final IconData icon; +} + +/// 告警状态枚举 +enum AlarmStatus { + /// 未确认 + unconfirmed('未确认'), + + /// 已确认 + confirmed('已确认'), + + /// 已恢复 + recovered('已恢复'); + + const AlarmStatus(this.label); + + /// 显示文本 + final String label; +} + +/// 筛选标签枚举 +enum AlarmFilterTab { + all('全部'), + unprocessed('未处理'), + confirmed('已确认'), + recovered('已恢复'); + + const AlarmFilterTab(this.label); + + /// 显示文本 + final String label; +} diff --git a/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart new file mode 100644 index 00000000..24772919 --- /dev/null +++ b/lib/features/v2/waring_center/presentation/pages/alarm_center_page.dart @@ -0,0 +1,275 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_cubit.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/bloc/alarm_state.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/widgets/alarm_count_card.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/widgets/alarm_item_card.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/widgets/ai_diagnosis_card.dart'; + +/// 告警中心页面 +class AlarmCenterPage extends StatefulWidget { + const AlarmCenterPage({super.key}); + + @override + State createState() => _AlarmCenterPageState(); +} + +class _AlarmCenterPageState extends State { + late AlarmCubit _cubit; + + @override + void initState() { + super.initState(); + _cubit = context.read(); + // 首次加载数据 + if (_cubit.state is AlarmInitial) { + _cubit.loadAlarms(); + } + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + child: Scaffold( + backgroundColor: const Color(0xFFF5F7FA), + body: SafeArea( + child: BlocBuilder( + builder: (context, state) { + if (state is AlarmLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (state is AlarmError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 48, + color: AlarmColors.textTertiary, + ), + const SizedBox(height: 16), + Text(state.message), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => _cubit.loadAlarms(), + child: const Text('重试'), + ), + ], + ), + ); + } + + if (state is AlarmLoaded) { + return Column( + children: [ + // 顶部标题栏 + _buildAppBar(), + // 横向标签栏 + _buildTabBar(state), + // 内容区域 + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await _cubit.refresh(); + }, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: const EdgeInsets.only( + top: AlarmDimensions.moduleSpacing, + bottom: 40, + ), + sliver: SliverList( + delegate: SliverChildListDelegate([ + //const SizedBox(height: AlarmDimensions.moduleSpacing), + // 告警统计卡片 + AlarmCountCard(count: state.count), + const SizedBox(height: AlarmDimensions.moduleSpacing), + // 告警列表 + ...state.alarms.map((alarm) { + return AlarmItemCard( + alarm: alarm, + onTap: () { + // TODO: 跳转到告警详情页 + debugPrint('点击告警: ${alarm.title}'); + }, + ); + }).toList(), + const SizedBox(height: AlarmDimensions.moduleSpacing), + // AI 诊断建议(仅当有 AI 诊断内容时显示) + if (state.alarms.any((alarm) => alarm.aiDiagnosis != null)) + AiDiagnosisCard( + content: state.alarms + .firstWhere((alarm) => alarm.aiDiagnosis != null) + .aiDiagnosis ?? + '', + onViewDetails: () { + // TODO: 查看详情 + debugPrint('查看 AI 诊断详情'); + }, + ), + ]), + ), + ), + ], + ), + ), + ), + ], + ); + } + + return const SizedBox(); + }, + ), + ), + ), + ); + } + + /// 显示筛选弹窗 + void _showFilterDialog() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '筛选条件', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AlarmColors.textPrimary, + ), + ), + const SizedBox(height: 20), + _buildFilterOption('告警等级', ['高危', '中危', '低危', '提示']), + const SizedBox(height: 16), + _buildFilterOption('时间范围', ['今天', '本周', '本月', '全部']), + const SizedBox(height: 16), + _buildFilterOption('区域', ['A区', 'B区', 'C区', 'D区', '全部']), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AlarmColors.primary, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '应用筛选', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + } + + Widget _buildAppBar() { + return Container( + height: 44.0, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + const Text( + '告警中心', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + const Spacer(), + IconButton( + icon: const Icon( + Icons.filter_list, + size: 24, + color: Color(0xFF1D2129), + ), + onPressed: _showFilterDialog, + ), + ], + ), + ); + } + + Widget _buildTabBar(AlarmLoaded state) { + return AlarmTabBar( + selectedFilter: state.selectedFilter, + backgroundColor: const Color(0xFFF5F7FA), + onFilterChanged: (filter) { + _cubit.changeFilter(filter); + }, + ); + } + + Widget _buildFilterOption(String title, List options) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AlarmColors.textPrimary, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: options.map((option) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: AlarmColors.textTertiary), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + option, + style: const TextStyle( + fontSize: 13, + color: AlarmColors.textSecondary, + ), + ), + ); + }).toList(), + ), + ], + ); + } +} diff --git a/lib/features/v2/waring_center/presentation/routes/alarm_center_routes.dart b/lib/features/v2/waring_center/presentation/routes/alarm_center_routes.dart new file mode 100644 index 00000000..0a3b047d --- /dev/null +++ b/lib/features/v2/waring_center/presentation/routes/alarm_center_routes.dart @@ -0,0 +1,13 @@ +import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/pages/alarm_center_page.dart'; + +/// 告警中心路由配置 +class AlarmCenterRoutes { + static List get routes => [ + GoRoute( + path: 'alarm_center', + name: 'alarmCenter', + builder: (context, state) => const AlarmCenterPage(), + ), + ]; +} diff --git a/lib/features/v2/waring_center/presentation/widgets/ai_diagnosis_card.dart b/lib/features/v2/waring_center/presentation/widgets/ai_diagnosis_card.dart new file mode 100644 index 00000000..16fe046e --- /dev/null +++ b/lib/features/v2/waring_center/presentation/widgets/ai_diagnosis_card.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// AI 诊断建议卡片 +class AiDiagnosisCard extends StatelessWidget { + const AiDiagnosisCard({ + super.key, + required this.content, + this.onViewDetails, + }); + + final String content; + final VoidCallback? onViewDetails; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(AlarmDimensions.horizontalPadding), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AlarmColors.aiCardBg, + borderRadius: BorderRadius.circular(AlarmDimensions.borderRadius), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: AlarmColors.primary, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.auto_awesome, + color: Colors.white, + size: 14, + ), + ), + const SizedBox(width: 8), + const Text( + 'AI诊断建议', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: AlarmColors.textPrimary, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + content, + style: const TextStyle( + fontSize: 13, + color: AlarmColors.textSecondary, + height: 1.5, + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: onViewDetails, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '查看详情', + style: TextStyle( + fontSize: 13, + color: AlarmColors.primary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 4), + const Icon( + Icons.arrow_forward_ios, + size: 12, + color: AlarmColors.primary, + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart new file mode 100644 index 00000000..5c7bedfe --- /dev/null +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_count_card.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_count_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警统计卡片 +class AlarmCountCard extends StatelessWidget { + const AlarmCountCard({ + super.key, + required this.count, + }); + + final AlarmCountEntity count; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: AlarmDimensions.horizontalPadding), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AlarmColors.background, + borderRadius: BorderRadius.circular(AlarmDimensions.borderRadius), + boxShadow: [AlarmDimensions.cardShadow], + ), + child: Row( + children: [ + Expanded( + child: _buildCountItem( + label: '未处理', + value: count.unprocessed, + color: AlarmColors.danger, + bgColor: AlarmColors.unprocessedBg, + labelColor: AlarmColors.danger, + ), + ), + _buildDivider(), + Expanded( + child: _buildCountItem( + label: '处理中', + value: count.processing, + color: AlarmColors.textPrimary, + ), + ), + _buildDivider(), + Expanded( + child: _buildCountItem( + label: '已确认', + value: count.confirmed, + color: const Color(0xFF1D2129), + ), + ), + _buildDivider(), + Expanded( + child: _buildCountItem( + label: '今日新增', + value: count.todayNew, + color: const Color(0xFF1D2129), + ), + ), + ], + ), + ); + } + + Widget _buildCountItem({ + required String label, + required int value, + required Color color, + Color? bgColor, + Color? labelColor, + }) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text( + label, + style: TextStyle( + fontSize: 13, + color: labelColor ?? AlarmColors.textSecondary, + ), + ), + const SizedBox(height: 4), + Text( + value.toString(), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ); + } + + Widget _buildDivider() { + return Container( + width: 1, + height: 50, + margin: const EdgeInsets.symmetric(horizontal: 8), + color: const Color(0xFFE5E6EB), + ); + } +} diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart new file mode 100644 index 00000000..1c11a5a5 --- /dev/null +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_item_card.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/domain/entities/alarm_entity.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警项卡片 +class AlarmItemCard extends StatelessWidget { + const AlarmItemCard({ + super.key, + required this.alarm, + this.onTap, + }); + + final AlarmEntity alarm; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: AlarmDimensions.horizontalPadding, + vertical: 6, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AlarmColors.background, + borderRadius: BorderRadius.circular(AlarmDimensions.borderRadius), + boxShadow: [AlarmDimensions.cardShadow], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 第一行:图标 + 标题 + 等级标签 + Row( + children: [ + Icon( + alarm.level.icon, + color: alarm.level.color, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + alarm.title, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: AlarmColors.textPrimary, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: alarm.level.color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + alarm.level.label, + style: TextStyle( + fontSize: 12, + color: alarm.level.color, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + // 第二行:区域和设备 + Text( + '区域:${alarm.area} / 设备:${alarm.device}', + style: const TextStyle( + fontSize: 13, + color: AlarmColors.textSecondary, + ), + ), + const SizedBox(height: 8), + // 第三行:时间和状态 + Row( + children: [ + Text( + alarm.time, + style: const TextStyle( + fontSize: 12, + color: AlarmColors.textTertiary, + ), + ), + const Spacer(), + Text( + alarm.status.label, + style: TextStyle( + fontSize: 12, + color: alarm.status == AlarmStatus.unconfirmed + ? AlarmColors.warning + : AlarmColors.textTertiary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: alarm.status == AlarmStatus.unconfirmed + ? AlarmColors.warning + : AlarmColors.textTertiary, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart b/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart new file mode 100644 index 00000000..7828bafe --- /dev/null +++ b/lib/features/v2/waring_center/presentation/widgets/alarm_tab_bar.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/features/v2/waring_center/presentation/constants/alarm_constants.dart'; + +/// 告警筛选标签栏 +class AlarmTabBar extends StatelessWidget { + const AlarmTabBar({ + super.key, + required this.selectedFilter, + required this.onFilterChanged, + this.backgroundColor = Colors.transparent, + }); + + final AlarmFilterTab selectedFilter; + final ValueChanged onFilterChanged; + final Color backgroundColor; + + @override + Widget build(BuildContext context) { + return Container( + height: 44, + color: backgroundColor, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemCount: AlarmFilterTab.values.length, + itemBuilder: (context, index) { + final filter = AlarmFilterTab.values[index]; + final isSelected = selectedFilter == filter; + + return GestureDetector( + onTap: () => onFilterChanged(filter), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + filter.label, + style: TextStyle( + fontSize: 15, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? AlarmColors.primary : AlarmColors.textSecondary, + ), + ), + const SizedBox(height: 4), + Container( + height: 2, + width: 20, + decoration: BoxDecoration( + color: isSelected ? AlarmColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/v2/workorder/data/datasources/workorder_remote_datasource.dart b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource.dart new file mode 100644 index 00000000..10f31abb --- /dev/null +++ b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource.dart @@ -0,0 +1,12 @@ +import '../models/workorder_model.dart'; + +/// 工单远程数据源抽象类 +abstract class WorkOrderRemoteDataSource { + /// 获取工单列表 + Future> getWorkOrderList({ + String? status, + }); + + /// 获取工单统计 + Future getWorkOrderCount(); +} diff --git a/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart new file mode 100644 index 00000000..9644b898 --- /dev/null +++ b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart @@ -0,0 +1,87 @@ +import '../../../../../core/consts/workorder_consts.dart'; +import 'workorder_remote_datasource.dart'; +import '../models/workorder_model.dart'; + +/// 工单远程数据源实现(模拟数据) +class WorkOrderRemoteDataSourceImpl implements WorkOrderRemoteDataSource { + @override + Future> getWorkOrderList({String? status}) async { + // 模拟网络延迟 + await Future.delayed(const Duration(seconds: 1)); + + // 模拟数据 + final allOrders = [ + WorkOrderModel( + id: '1', + title: '逆变器通讯故障处理', + orderNo: 'WO-20250521001', + priority: WorkOrderPriority.high, + executor: null, + createTime: DateTime(2025, 5, 21, 10, 12), + completeTime: null, + location: 'A区 / INV-001', + status: WorkOrderStatus.pending, + progress: null, + ), + WorkOrderModel( + id: '2', + title: '组件清洗作业', + orderNo: 'WO-20250521002', + priority: WorkOrderPriority.medium, + executor: '张工', + createTime: DateTime(2025, 5, 21, 9, 30), + completeTime: null, + location: 'B区', + status: WorkOrderStatus.executing, + progress: 60.0, + ), + WorkOrderModel( + id: '3', + title: '汇流箱巡检', + orderNo: 'WO-20250520098', + priority: WorkOrderPriority.low, + executor: '李工', + createTime: DateTime(2025, 5, 20, 14, 20), + completeTime: DateTime(2025, 5, 20, 16, 32), + location: 'C区', + status: WorkOrderStatus.completed, + progress: 100.0, + ), + ]; + + // 根据状态筛选 + if (status != null && status != 'all') { + return allOrders + .where((order) => _statusToString(order.status) == status) + .toList(); + } + + return allOrders; + } + + @override + Future getWorkOrderCount() async { + // 模拟网络延迟 + await Future.delayed(const Duration(milliseconds: 500)); + + // 模拟统计数据 + return WorkOrderCountModel( + pendingCount: 12, + executingCount: 8, + todayCompletedCount: 18, + ); + } + + String _statusToString(WorkOrderStatus status) { + switch (status) { + case WorkOrderStatus.pending: + return 'pending'; + case WorkOrderStatus.executing: + return 'executing'; + case WorkOrderStatus.completed: + return 'completed'; + case WorkOrderStatus.all: + return 'all'; + } + } +} diff --git a/lib/features/v2/workorder/data/models/workorder_model.dart b/lib/features/v2/workorder/data/models/workorder_model.dart new file mode 100644 index 00000000..f9512962 --- /dev/null +++ b/lib/features/v2/workorder/data/models/workorder_model.dart @@ -0,0 +1,185 @@ +import '../../../../../core/consts/workorder_consts.dart'; +import '../../domain/entities/workorder_entity.dart'; + +/// 工单数据模型 +class WorkOrderModel { + final String id; + final String title; + final String orderNo; + final WorkOrderPriority priority; + final String? executor; + final DateTime createTime; + final DateTime? completeTime; + final String location; + final WorkOrderStatus status; + final double? progress; + + WorkOrderModel({ + required this.id, + required this.title, + required this.orderNo, + required this.priority, + this.executor, + required this.createTime, + this.completeTime, + required this.location, + required this.status, + this.progress, + }); + + /// 从 JSON 创建 + factory WorkOrderModel.fromJson(Map json) { + return WorkOrderModel( + id: json['id'] as String, + title: json['title'] as String, + orderNo: json['orderNo'] as String, + priority: _parsePriority(json['priority'] as String), + executor: json['executor'] as String?, + createTime: DateTime.parse(json['createTime'] as String), + completeTime: json['completeTime'] != null + ? DateTime.parse(json['completeTime'] as String) + : null, + location: json['location'] as String, + status: _parseStatus(json['status'] as String), + progress: json['progress'] as double?, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'id': id, + 'title': title, + 'orderNo': orderNo, + 'priority': _priorityToString(priority), + 'executor': executor, + 'createTime': createTime.toIso8601String(), + 'completeTime': completeTime?.toIso8601String(), + 'location': location, + 'status': _statusToString(status), + 'progress': progress, + }; + } + + /// 转换为实体 + WorkOrderEntity toEntity() { + return WorkOrderEntity( + id: id, + title: title, + orderNo: orderNo, + priority: priority, + executor: executor, + createTime: createTime, + completeTime: completeTime, + location: location, + status: status, + progress: progress, + ); + } + + /// 从实体创建 + factory WorkOrderModel.fromEntity(WorkOrderEntity entity) { + return WorkOrderModel( + id: entity.id, + title: entity.title, + orderNo: entity.orderNo, + priority: entity.priority, + executor: entity.executor, + createTime: entity.createTime, + completeTime: entity.completeTime, + location: entity.location, + status: entity.status, + progress: entity.progress, + ); + } + + static WorkOrderPriority _parsePriority(String value) { + switch (value) { + case 'high': + return WorkOrderPriority.high; + case 'medium': + return WorkOrderPriority.medium; + case 'low': + return WorkOrderPriority.low; + default: + return WorkOrderPriority.medium; + } + } + + static String _priorityToString(WorkOrderPriority priority) { + switch (priority) { + case WorkOrderPriority.high: + return 'high'; + case WorkOrderPriority.medium: + return 'medium'; + case WorkOrderPriority.low: + return 'low'; + } + } + + static WorkOrderStatus _parseStatus(String value) { + switch (value) { + case 'pending': + return WorkOrderStatus.pending; + case 'executing': + return WorkOrderStatus.executing; + case 'completed': + return WorkOrderStatus.completed; + default: + return WorkOrderStatus.pending; + } + } + + static String _statusToString(WorkOrderStatus status) { + switch (status) { + case WorkOrderStatus.pending: + return 'pending'; + case WorkOrderStatus.executing: + return 'executing'; + case WorkOrderStatus.completed: + return 'completed'; + case WorkOrderStatus.all: + return 'all'; + } + } +} + +/// 工单统计数据模型 +class WorkOrderCountModel { + final int pendingCount; + final int executingCount; + final int todayCompletedCount; + + WorkOrderCountModel({ + required this.pendingCount, + required this.executingCount, + required this.todayCompletedCount, + }); + + /// 从 JSON 创建 + factory WorkOrderCountModel.fromJson(Map json) { + return WorkOrderCountModel( + pendingCount: json['pendingCount'] as int, + executingCount: json['executingCount'] as int, + todayCompletedCount: json['todayCompletedCount'] as int, + ); + } + + /// 转换为 JSON + Map toJson() { + return { + 'pendingCount': pendingCount, + 'executingCount': executingCount, + 'todayCompletedCount': todayCompletedCount, + }; + } + + /// 转换为实体 + WorkOrderCountEntity toEntity() { + return WorkOrderCountEntity( + pendingCount: pendingCount, + executingCount: executingCount, + todayCompletedCount: todayCompletedCount, + ); + } +} diff --git a/lib/features/v2/workorder/data/repositories/workorder_repository_impl.dart b/lib/features/v2/workorder/data/repositories/workorder_repository_impl.dart new file mode 100644 index 00000000..f8834beb --- /dev/null +++ b/lib/features/v2/workorder/data/repositories/workorder_repository_impl.dart @@ -0,0 +1,59 @@ +import '../../../../../core/consts/workorder_consts.dart'; +import '../../../../../core/error/failure.dart'; +import '../../../../../core/error/workorder_failure.dart'; +import '../../domain/repositories/workorder_repository.dart'; +import '../../domain/entities/workorder_entity.dart'; +import '../datasources/workorder_remote_datasource.dart'; +import 'package:fpdart/fpdart.dart'; + + +class WorkOrderRepositoryImpl implements WorkOrderRepository { + final WorkOrderRemoteDataSource remoteDataSource; + + WorkOrderRepositoryImpl({required this.remoteDataSource}); + + @override + Future>> getWorkOrderList({ + WorkOrderStatus? status, + }) async { + try { + final models = await remoteDataSource.getWorkOrderList( + status: status != null ? _statusToString(status) : null, + ); + final entities = models.map((model) => model.toEntity()).toList(); + return Right(entities); + } catch (e) { + return Left(UnknownFailure(message: '获取工单列表失败: $e')); + } + } + + @override + Future> getWorkOrderCount() async { + try { + final model = await remoteDataSource.getWorkOrderCount(); + return Right(model.toEntity()); + } catch (e) { + return Left(UnknownFailure(message: '获取工单统计失败: $e')); + } + } + + @override + Future>> filterWorkOrders({ + required WorkOrderStatus status, + }) async { + return getWorkOrderList(status: status); + } + + String _statusToString(WorkOrderStatus status) { + switch (status) { + case WorkOrderStatus.pending: + return 'pending'; + case WorkOrderStatus.executing: + return 'executing'; + case WorkOrderStatus.completed: + return 'completed'; + case WorkOrderStatus.all: + return 'all'; + } + } +} diff --git a/lib/features/v2/workorder/domain/entities/workorder_entity.dart b/lib/features/v2/workorder/domain/entities/workorder_entity.dart new file mode 100644 index 00000000..98326c29 --- /dev/null +++ b/lib/features/v2/workorder/domain/entities/workorder_entity.dart @@ -0,0 +1,42 @@ +import '../../../../../core/consts/workorder_consts.dart'; + + +/// 工单详情实体 +class WorkOrderEntity { + final String id; + final String title; + final String orderNo; + final WorkOrderPriority priority; + final String? executor; + final DateTime createTime; + final DateTime? completeTime; + final String location; + final WorkOrderStatus status; + final double? progress; + + WorkOrderEntity({ + required this.id, + required this.title, + required this.orderNo, + required this.priority, + this.executor, + required this.createTime, + this.completeTime, + required this.location, + required this.status, + this.progress, + }); +} + +/// 工单统计实体 +class WorkOrderCountEntity { + final int pendingCount; + final int executingCount; + final int todayCompletedCount; + + WorkOrderCountEntity({ + required this.pendingCount, + required this.executingCount, + required this.todayCompletedCount, + }); +} diff --git a/lib/features/v2/workorder/domain/repositories/workorder_repository.dart b/lib/features/v2/workorder/domain/repositories/workorder_repository.dart new file mode 100644 index 00000000..b7024957 --- /dev/null +++ b/lib/features/v2/workorder/domain/repositories/workorder_repository.dart @@ -0,0 +1,20 @@ +import '../../../../../core/consts/workorder_consts.dart'; +import '../../../../../core/error/failure.dart'; +import '../entities/workorder_entity.dart'; +import 'package:fpdart/fpdart.dart'; + +/// 工单仓储接口 +abstract class WorkOrderRepository { + /// 获取工单列表 + Future>> getWorkOrderList({ + WorkOrderStatus? status, + }); + + /// 获取工单统计 + Future> getWorkOrderCount(); + + /// 筛选工单 + Future>> filterWorkOrders({ + required WorkOrderStatus status, + }); +} diff --git a/lib/features/v2/workorder/domain/usecases/get_workorder_list_usecase.dart b/lib/features/v2/workorder/domain/usecases/get_workorder_list_usecase.dart new file mode 100644 index 00000000..8f629d2e --- /dev/null +++ b/lib/features/v2/workorder/domain/usecases/get_workorder_list_usecase.dart @@ -0,0 +1,29 @@ +import '../../../../../core/consts/workorder_consts.dart'; +import '../../../../../core/error/failure.dart'; +import '../repositories/workorder_repository.dart'; +import '../entities/workorder_entity.dart'; +import 'package:fpdart/fpdart.dart'; + +/// 获取工单列表用例 +class GetWorkOrderListUseCase { + final WorkOrderRepository repository; + + GetWorkOrderListUseCase({required this.repository}); + + Future>> execute({ + WorkOrderStatus? status, + }) async { + return await repository.getWorkOrderList(status: status); + } +} + +/// 获取工单统计用例 +class GetWorkOrderCountUseCase { + final WorkOrderRepository repository; + + GetWorkOrderCountUseCase({required this.repository}); + + Future> execute() async { + return await repository.getWorkOrderCount(); + } +} diff --git a/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart b/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart new file mode 100644 index 00000000..031d0310 --- /dev/null +++ b/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart @@ -0,0 +1,119 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import '../../../../../core/consts/workorder_consts.dart'; +import '../../../../../core/error/failure.dart'; +import '../../../../../core/error/workorder_failure.dart'; +import '../../domain/usecases/get_workorder_list_usecase.dart'; +import '../../domain/entities/workorder_entity.dart'; +import 'package:fpdart/fpdart.dart'; + +// ==================== States ==================== + +abstract class WorkOrderState extends Equatable { + const WorkOrderState(); + + @override + List get props => []; +} + +class WorkOrderInitial extends WorkOrderState {} + +class WorkOrderLoading extends WorkOrderState {} + +class WorkOrderLoaded extends WorkOrderState { + final List workOrders; + final WorkOrderCountEntity? count; + final WorkOrderStatus currentStatus; + + const WorkOrderLoaded({ + required this.workOrders, + this.count, + this.currentStatus = WorkOrderStatus.pending, + }); + + @override + List get props => [workOrders, count, currentStatus]; +} + +class WorkOrderError extends WorkOrderState { + final Failure failure; + + const WorkOrderError({required this.failure}); + + @override + List get props => [failure]; +} + +// ==================== Cubit ==================== + +class WorkOrderCubit extends Cubit { + final GetWorkOrderListUseCase getWorkOrderListUseCase; + final GetWorkOrderCountUseCase getWorkOrderCountUseCase; + + WorkOrderCubit({ + required this.getWorkOrderListUseCase, + required this.getWorkOrderCountUseCase, + }) : super(WorkOrderInitial()); + + /// 初始加载 + Future loadInitialData() async { + emit(WorkOrderLoading()); + + try { + // 并行加载工单列表和统计数据 + final results = await Future.wait([ + getWorkOrderListUseCase.execute(status: WorkOrderStatus.pending), + getWorkOrderCountUseCase.execute(), + ]); + + final listResult = results[0] as Either>; + final countResult = results[1] as Either; + + listResult.fold( + (Failure failure) => emit(WorkOrderError(failure: failure)), + (workOrders) { + countResult.fold( + (Failure failure) => emit(WorkOrderError(failure: failure)), + (count) => emit(WorkOrderLoaded( + workOrders: workOrders, + count: count, + currentStatus: WorkOrderStatus.pending, + )), + ); + }, + ); + } catch (e) { + emit(WorkOrderError(failure: UnknownFailure(message: '加载失败: $e'))); + } + } + + /// 切换标签 + Future switchTab(WorkOrderStatus status) async { + if (state is WorkOrderLoaded) { + final currentState = state as WorkOrderLoaded; + if (currentState.currentStatus == status) return; + + emit(WorkOrderLoading()); + + try { + final result = await getWorkOrderListUseCase.execute(status: status); + + result.fold( + (Failure failure) => emit(WorkOrderError(failure: failure)), + (workOrders) => emit(WorkOrderLoaded( + workOrders: workOrders, + count: currentState.count, + currentStatus: status, + )), + ); + } catch (e) { + emit(WorkOrderError(failure: UnknownFailure(message: '切换失败: $e'))); + } + } + } + + /// 下拉刷新 + Future refreshData() async { + await loadInitialData(); + } +} diff --git a/lib/features/v2/workorder/presentation/pages/workorder_page.dart b/lib/features/v2/workorder/presentation/pages/workorder_page.dart new file mode 100644 index 00000000..cc7d2a5a --- /dev/null +++ b/lib/features/v2/workorder/presentation/pages/workorder_page.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../../core/consts/workorder_consts.dart'; +import '../cubit/workorder_cubit.dart'; +import '../widgets/workorder_tabbar.dart'; +import '../widgets/workorder_count_card.dart'; +import '../widgets/workorder_item_card.dart'; +import '../../domain/usecases/get_workorder_list_usecase.dart'; +import '../../data/repositories/workorder_repository_impl.dart'; +import '../../data/datasources/workorder_remote_datasource_impl.dart'; + +/// 工单任务主页面 +class WorkOrderPage extends StatelessWidget { + const WorkOrderPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + // 创建 Repository 和 UseCases + final remoteDataSource = WorkOrderRemoteDataSourceImpl(); + final repository = WorkOrderRepositoryImpl(remoteDataSource: remoteDataSource); + final getWorkOrderListUseCase = GetWorkOrderListUseCase(repository: repository); + final getWorkOrderCountUseCase = GetWorkOrderCountUseCase(repository: repository); + + return BlocProvider( + create: (context) { + final cubit = WorkOrderCubit( + getWorkOrderListUseCase: getWorkOrderListUseCase, + getWorkOrderCountUseCase: getWorkOrderCountUseCase, + ); + cubit.loadInitialData(); + return cubit; + }, + child: AnnotatedRegion( + value: SystemUiOverlayStyle.dark, + child: Scaffold( + backgroundColor: const Color(0xFFF7F8FA), + body: BlocBuilder( + builder: (context, state) { + if (state is WorkOrderLoading) { + return _buildLoadingView(); + } else if (state is WorkOrderLoaded) { + return _buildContentView(context, state); + } else if (state is WorkOrderError) { + return _buildErrorView(context, state); + } + return _buildLoadingView(); + }, + ), + ), + ), + ); + } + + Widget _buildLoadingView() { + return Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(AppConstants.primaryColor), + ), + ); + } + + Widget _buildErrorView(BuildContext context, WorkOrderError state) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_outline, + size: 48, + color: AppConstants.auxiliaryTextColor, + ), + const SizedBox(height: 16), + Text( + state.failure.message, + style: TextStyle( + fontSize: 14, + color: AppConstants.auxiliaryTextColor, + ), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + context.read().loadInitialData(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppConstants.primaryColor, + foregroundColor: Colors.white, + ), + child: const Text('重试'), + ), + ], + ), + ); + } + + Widget _buildContentView(BuildContext context, WorkOrderLoaded state) { + return SafeArea( + child: Column( + children: [ + // 导航栏 + _buildAppBar(), + // 标签栏 + _buildTabBar(context, state), + + // 内容区域 + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await context.read().refreshData(); + }, + child: ListView( + children: [ + // 统计卡片 + if (state.count != null) + WorkOrderCountCard(count: state.count!), + + // 工单列表 + ...state.workOrders.map((workOrder) => WorkOrderItemCard( + workOrder: workOrder, + onTap: () { + // TODO: 跳转到工单详情页 + debugPrint('点击工单: ${workOrder.title}'); + }, + )), + + const SizedBox(height: 20), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildAppBar() { + return Container( + height: 44.0, + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: const Row( + children: [ + Text( + '工单任务', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF1D2129), + ), + ), + Spacer(), + Icon( + Icons.add_circle_outline, + size: 24, + color: Color(0xFF1D2129), + ), + ], + ), + ); + } + + Widget _buildTabBar(BuildContext context, WorkOrderLoaded state) { + return WorkOrderTabBar( + currentStatus: state.currentStatus, + onTabChanged: (status) { + context.read().switchTab(status); + }, + ); + } +} diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart b/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart new file mode 100644 index 00000000..6a4f9237 --- /dev/null +++ b/lib/features/v2/workorder/presentation/widgets/workorder_count_card.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import '../../../../../core/consts/workorder_consts.dart'; +import '../../domain/entities/workorder_entity.dart'; + +/// 工单统计卡片组件 +class WorkOrderCountCard extends StatelessWidget { + final WorkOrderCountEntity count; + + const WorkOrderCountCard({ + Key? key, + required this.count, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding, + vertical: AppConstants.moduleSpacing / 2, + ), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppConstants.backgroundColor, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + boxShadow: AppConstants.cardShadow, + ), + child: Row( + children: [ + _buildCountItem( + label: '待处理', + value: count.pendingCount.toString(), + valueColor: WorkOrderColors.primaryText, + ), + _buildDivider(), + _buildCountItem( + label: '执行中', + value: count.executingCount.toString(), + valueColor: WorkOrderColors.primaryText, + ), + _buildDivider(), + _buildCountItem( + label: '今日完成', + value: count.todayCompletedCount.toString(), + valueColor: WorkOrderColors.primary, + ), + ], + ), + ); + } + + Widget _buildCountItem({ + required String label, + required String value, + required Color valueColor, + }) { + return Expanded( + child: Column( + children: [ + Text( + label, + style: TextStyle( + fontSize: 13, + color: AppConstants.auxiliaryTextColor, + ), + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: valueColor, + ), + ), + ], + ), + ); + } + + Widget _buildDivider() { + return Container( + width: 1, + height: 40, + color: const Color(0xFFF0F0F0), + ); + } +} diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart b/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart new file mode 100644 index 00000000..26589db3 --- /dev/null +++ b/lib/features/v2/workorder/presentation/widgets/workorder_item_card.dart @@ -0,0 +1,252 @@ +import 'package:flutter/material.dart'; +import '../../../../../core/consts/workorder_consts.dart'; +import '../../domain/entities/workorder_entity.dart'; + +/// 工单项卡片组件 +class WorkOrderItemCard extends StatelessWidget { + final WorkOrderEntity workOrder; + final VoidCallback? onTap; + + const WorkOrderItemCard({ + Key? key, + required this.workOrder, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: EdgeInsets.symmetric( + horizontal: AppConstants.horizontalPadding, + vertical: 8, + ), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppConstants.backgroundColor, + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + boxShadow: AppConstants.cardShadow, + ), + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 标题和状态标签 + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + workOrder.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppConstants.primaryTextColor, + ), + ), + ), + _buildStatusTag(), + ], + ), + const SizedBox(height: 12), + + // 详情信息 + _buildDetailRow('工单号', workOrder.orderNo), + const SizedBox(height: 6), + + // 优先级 + _buildDetailRow( + '优先级', + _getPriorityText(workOrder.priority), + valueColor: _getPriorityColor(workOrder.priority), + ), + const SizedBox(height: 6), + + // 根据状态显示不同信息 + if (workOrder.status == WorkOrderStatus.pending) ...[ + _buildDetailRow( + '创建时间', + _formatDateTime(workOrder.createTime), + ), + const SizedBox(height: 6), + _buildDetailRow('地点', workOrder.location), + ] else if (workOrder.status == WorkOrderStatus.executing) ...[ + _buildDetailRow('执行人', workOrder.executor ?? '-'), + const SizedBox(height: 6), + _buildDetailRow('地点', workOrder.location), + if (workOrder.progress != null) ...[ + const SizedBox(height: 12), + _buildProgressBar(), + ], + ] else if (workOrder.status == WorkOrderStatus.completed) ...[ + _buildDetailRow( + '完成时间', + workOrder.completeTime != null + ? _formatDateTime(workOrder.completeTime!) + : '-', + ), + const SizedBox(height: 6), + _buildDetailRow('地点', workOrder.location), + ], + ], + ), + // 已完成水印 + if (workOrder.status == WorkOrderStatus.completed) + Positioned( + right: 8, + bottom: 8, + child: Opacity( + opacity: 0.3, + child: Image.asset( + 'assets/images/yiwancheng.png', + width: 60, + height: 60, + fit: BoxFit.contain, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatusTag() { + Color color; + String text; + + switch (workOrder.status) { + case WorkOrderStatus.pending: + color = AppConstants.pendingColor; + text = '待处理'; + break; + case WorkOrderStatus.executing: + color = AppConstants.executingColor; + text = '执行中'; + break; + case WorkOrderStatus.completed: + color = AppConstants.completedColor; + text = '已完成'; + break; + default: + color = AppConstants.auxiliaryTextColor; + text = '未知'; + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12, + color: color, + ), + ), + ), + if (workOrder.status == WorkOrderStatus.executing && + workOrder.progress != null) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + '${workOrder.progress!.toInt()}%', + style: TextStyle( + fontSize: 12, + color: color, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + } + + Widget _buildDetailRow( + String label, + String value, { + Color? valueColor, + }) { + return Row( + children: [ + Text( + '$label:', + style: TextStyle( + fontSize: 13, + color: AppConstants.auxiliaryTextColor, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 13, + color: valueColor ?? AppConstants.secondaryTextColor, + ), + ), + ], + ); + } + + Widget _buildProgressBar() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '进度:', + style: TextStyle( + fontSize: 13, + color: AppConstants.auxiliaryTextColor, + ), + ), + Expanded( + child: LinearProgressIndicator( + value: workOrder.progress! / 100, + backgroundColor: const Color(0xFFF0F0F0), + valueColor: AlwaysStoppedAnimation( + AppConstants.primaryColor, + ), + minHeight: 6, + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + ], + ); + } + + String _getPriorityText(WorkOrderPriority priority) { + switch (priority) { + case WorkOrderPriority.high: + return '高'; + case WorkOrderPriority.medium: + return '中'; + case WorkOrderPriority.low: + return '低'; + } + } + + Color _getPriorityColor(WorkOrderPriority priority) { + switch (priority) { + case WorkOrderPriority.high: + return AppConstants.pendingColor; + case WorkOrderPriority.medium: + return AppConstants.pendingColor; + case WorkOrderPriority.low: + return AppConstants.auxiliaryTextColor; + } + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart b/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart new file mode 100644 index 00000000..92561e18 --- /dev/null +++ b/lib/features/v2/workorder/presentation/widgets/workorder_tabbar.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import '../../../../../core/consts/workorder_consts.dart'; + +/// 工单标签栏组件 +class WorkOrderTabBar extends StatelessWidget { + final WorkOrderStatus currentStatus; + final Function(WorkOrderStatus) onTabChanged; + + const WorkOrderTabBar({ + Key? key, + required this.currentStatus, + required this.onTabChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final tabs = [ + {'label': '待处理', 'status': WorkOrderStatus.pending, 'badge': false}, + {'label': '执行中', 'status': WorkOrderStatus.executing, 'badge': true}, + {'label': '已完成', 'status': WorkOrderStatus.completed, 'badge': false}, + {'label': '全部', 'status': WorkOrderStatus.all, 'badge': true}, + ]; + + return Container( + height: 44, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: tabs.length, + itemBuilder: (context, index) { + final tab = tabs[index]; + final isSelected = currentStatus == tab['status']; + + return GestureDetector( + onTap: () => onTabChanged(tab['status'] as WorkOrderStatus), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Text( + tab['label'] as String, + style: TextStyle( + fontSize: 15, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected + ? AppConstants.primaryColor + : AppConstants.secondaryTextColor, + ), + ), + if (tab['badge'] as bool) + Positioned( + right: -8, + top: -4, + child: Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.red, + shape: BoxShape.circle, + ), + ), + ), + ], + ), + if (isSelected) + Container( + margin: const EdgeInsets.only(top: 4), + width: 24, + height: 3, + decoration: BoxDecoration( + color: AppConstants.primaryColor, + borderRadius: BorderRadius.circular(1.5), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/v2/workorder/workorder.dart b/lib/features/v2/workorder/workorder.dart new file mode 100644 index 00000000..dd3aa69d --- /dev/null +++ b/lib/features/v2/workorder/workorder.dart @@ -0,0 +1,38 @@ +/// WorkOrder 模块导出文件 +/// +/// 使用方式: +/// import 'package:your_app/features/v2/workorder/workorder.dart'; + +// Constants +export '../../../core/consts/workorder_consts.dart'; +export '../../../core/error/workorder_failure.dart'; + +// Domain - Entities +export 'domain/entities/workorder_entity.dart'; + +// Domain - Repositories +export 'domain/repositories/workorder_repository.dart'; + +// Domain - UseCases +export 'domain/usecases/get_workorder_list_usecase.dart'; + +// Data - Models +export 'data/models/workorder_model.dart'; + +// Data - Datasources +export 'data/datasources/workorder_remote_datasource.dart'; +export 'data/datasources/workorder_remote_datasource_impl.dart'; + +// Data - Repositories +export 'data/repositories/workorder_repository_impl.dart'; + +// Presentation - Cubit +export 'presentation/cubit/workorder_cubit.dart'; + +// Presentation - Widgets +export 'presentation/widgets/workorder_tabbar.dart'; +export 'presentation/widgets/workorder_count_card.dart'; +export 'presentation/widgets/workorder_item_card.dart'; + +// Presentation - Pages +export 'presentation/pages/workorder_page.dart'; diff --git a/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart b/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart deleted file mode 100644 index 3b12347e..00000000 --- a/lib/features/waring_center/presentation/bloc/warning_center_bloc.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'warning_center_event.dart'; -import 'warning_center_state.dart'; - -class WarningCenterBloc extends Bloc { - WarningCenterBloc() : super(const WarningCenterInitial()) { - on(_onLoadData); - on(_onFilterByLevel); - on(_onFilterByTime); - on(_onSearch); - } - - Future _onLoadData( - WarningCenterLoadData event, - Emitter emit, - ) async { - emit(const WarningCenterLoading()); - - try { - await Future.delayed(const Duration(milliseconds: 500)); - - final stats = const WarningStats( - totalCount: 156, - todayCount: 156, - severeCount: 8, - processingCount: 15, - closedCount: 133, - avgResponseTime: 2.3, - changePercent: -12, - ); - - final warnings = [ - const WarningItem( - id: '1', - level: '严重', - deviceName: '逆变器12', - deviceType: '组串式', - content: '逆变器停机', - status: '处理中', - time: '2025-05-25 09:18:32', - duration: '2小时18分', - responsiblePerson: '张运维', - ), - const WarningItem( - id: '2', - level: '重要', - deviceName: '逆变器13', - deviceType: '组串式', - content: '逆变器停机', - status: '处理中', - time: '2025-05-25 09:18:32', - duration: '2小时18分', - responsiblePerson: '张运维', - ), - const WarningItem( - id: '3', - level: '严重', - deviceName: '逆变器05', - deviceType: '组串式', - content: '逆变器停机', - status: '处理中', - time: '2025-05-25 09:18:32', - duration: '2小时18分', - responsiblePerson: '张运维', - ), - const WarningItem( - id: '4', - level: '一般', - deviceName: '逆变器08', - deviceType: '组串式', - content: '温度过高告警', - status: '待处理', - time: '2025-05-25 08:30:15', - duration: '3小时05分', - responsiblePerson: '李工程师', - ), - const WarningItem( - id: '5', - level: '提示', - deviceName: '逆变器15', - deviceType: '组串式', - content: '效率低于阈值', - status: '已关闭', - time: '2025-05-24 16:45:20', - duration: '1天5小时', - responsiblePerson: '王技术员', - ), - const WarningItem( - id: '6', - level: '严重', - deviceName: '汇流箱03', - deviceType: '直流汇流', - content: '通信中断', - status: '处理中', - time: '2025-05-25 07:20:10', - duration: '4小时15分', - responsiblePerson: '张运维', - ), - const WarningItem( - id: '7', - level: '重要', - deviceName: '逆变器20', - deviceType: '组串式', - content: '输出电压异常', - status: '待处理', - time: '2025-05-25 10:05:30', - duration: '1小时30分', - responsiblePerson: '赵工程师', - ), - const WarningItem( - id: '8', - level: '一般', - deviceName: '变压器02', - deviceType: '升压变压器', - content: '负载率超过80%', - status: '已关闭', - time: '2025-05-24 14:20:45', - duration: '1天10小时', - responsiblePerson: '李工程师', - ), - ]; - - emit(WarningCenterLoaded( - stats: stats, - warnings: warnings, - )); - } catch (e) { - emit(WarningCenterError('加载数据失败:$e')); - } - } - - Future _onFilterByLevel( - WarningCenterFilterByLevel event, - Emitter emit, - ) async { - if (state is WarningCenterLoaded) { - final currentState = state as WarningCenterLoaded; - - List filteredWarnings; - if (event.level == '全部') { - filteredWarnings = currentState.warnings; - } else { - filteredWarnings = currentState.warnings - .where((w) => w.level == event.level) - .toList(); - } - - emit(currentState.copyWith( - selectedLevel: event.level, - warnings: filteredWarnings, - )); - } - } - - Future _onFilterByTime( - WarningCenterFilterByTime event, - Emitter emit, - ) async { - if (state is WarningCenterLoaded) { - final currentState = state as WarningCenterLoaded; - emit(currentState.copyWith( - selectedTimeRange: event.timeRange, - )); - } - } - - Future _onSearch( - WarningCenterSearch event, - Emitter emit, - ) async { - if (state is WarningCenterLoaded) { - final currentState = state as WarningCenterLoaded; - - List filteredWarnings; - if (event.keyword.isEmpty) { - filteredWarnings = currentState.warnings; - } else { - filteredWarnings = currentState.warnings - .where((w) => - w.deviceName.contains(event.keyword) || - w.content.contains(event.keyword) || - w.deviceType.contains(event.keyword)) - .toList(); - } - - emit(currentState.copyWith( - searchKeyword: event.keyword, - warnings: filteredWarnings, - )); - } - } -} diff --git a/lib/features/waring_center/presentation/bloc/warning_center_event.dart b/lib/features/waring_center/presentation/bloc/warning_center_event.dart deleted file mode 100644 index 54aedea4..00000000 --- a/lib/features/waring_center/presentation/bloc/warning_center_event.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class WarningCenterEvent extends Equatable { - const WarningCenterEvent(); - - @override - List get props => []; -} - -class WarningCenterLoadData extends WarningCenterEvent { - const WarningCenterLoadData(); -} - -class WarningCenterFilterByLevel extends WarningCenterEvent { - final String level; - - const WarningCenterFilterByLevel(this.level); - - @override - List get props => [level]; -} - -class WarningCenterFilterByTime extends WarningCenterEvent { - final String timeRange; - - const WarningCenterFilterByTime(this.timeRange); - - @override - List get props => [timeRange]; -} - -class WarningCenterSearch extends WarningCenterEvent { - final String keyword; - - const WarningCenterSearch(this.keyword); - - @override - List get props => [keyword]; -} diff --git a/lib/features/waring_center/presentation/bloc/warning_center_state.dart b/lib/features/waring_center/presentation/bloc/warning_center_state.dart deleted file mode 100644 index 02a344bd..00000000 --- a/lib/features/waring_center/presentation/bloc/warning_center_state.dart +++ /dev/null @@ -1,134 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class WarningStats extends Equatable { - final int totalCount; - final int todayCount; - final int severeCount; - final int processingCount; - final int closedCount; - final double avgResponseTime; - final double changePercent; - - const WarningStats({ - required this.totalCount, - required this.todayCount, - required this.severeCount, - required this.processingCount, - required this.closedCount, - required this.avgResponseTime, - required this.changePercent, - }); - - @override - List get props => [ - totalCount, - todayCount, - severeCount, - processingCount, - closedCount, - avgResponseTime, - changePercent, - ]; -} - -class WarningItem extends Equatable { - final String id; - final String level; - final String deviceName; - final String deviceType; - final String content; - final String status; - final String time; - final String duration; - final String responsiblePerson; - - const WarningItem({ - required this.id, - required this.level, - required this.deviceName, - required this.deviceType, - required this.content, - required this.status, - required this.time, - required this.duration, - required this.responsiblePerson, - }); - - @override - List get props => [ - id, - level, - deviceName, - deviceType, - content, - status, - time, - duration, - responsiblePerson, - ]; -} - -abstract class WarningCenterState extends Equatable { - const WarningCenterState(); - - @override - List get props => []; -} - -class WarningCenterInitial extends WarningCenterState { - const WarningCenterInitial(); -} - -class WarningCenterLoading extends WarningCenterState { - const WarningCenterLoading(); -} - -class WarningCenterLoaded extends WarningCenterState { - final WarningStats stats; - final List warnings; - final String selectedLevel; - final String selectedTimeRange; - final String searchKeyword; - - const WarningCenterLoaded({ - required this.stats, - required this.warnings, - this.selectedLevel = '全部', - this.selectedTimeRange = '近7天', - this.searchKeyword = '', - }); - - @override - List get props => [ - stats, - warnings, - selectedLevel, - selectedTimeRange, - searchKeyword, - ]; - - WarningCenterLoaded copyWith({ - WarningStats? stats, - List? warnings, - String? selectedLevel, - String? selectedTimeRange, - String? searchKeyword, - }) { - return WarningCenterLoaded( - stats: stats ?? this.stats, - warnings: warnings ?? this.warnings, - selectedLevel: selectedLevel ?? this.selectedLevel, - selectedTimeRange: selectedTimeRange ?? this.selectedTimeRange, - searchKeyword: searchKeyword ?? this.searchKeyword, - ); - } -} - -class WarningCenterError extends WarningCenterState { - final String message; - - const WarningCenterError(this.message); - - @override - List get props => [message]; -} diff --git a/lib/features/waring_center/presentation/pages/warning_center_page.dart b/lib/features/waring_center/presentation/pages/warning_center_page.dart deleted file mode 100644 index 6dd01e3f..00000000 --- a/lib/features/waring_center/presentation/pages/warning_center_page.dart +++ /dev/null @@ -1,638 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_bloc.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_event.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/bloc/warning_center_state.dart'; - -class WarningCenterPage extends StatefulWidget { - const WarningCenterPage({super.key}); - - @override - State createState() => _WarningCenterPageState(); -} - -class _WarningCenterPageState extends State { - late WarningCenterBloc _bloc; - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - super.initState(); - _bloc = WarningCenterBloc(); - _bloc.add(const WarningCenterLoadData()); - } - - @override - void dispose() { - _searchController.dispose(); - _bloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: _bloc, - child: Scaffold( - backgroundColor: const Color(0xFF0A2E6B), - body: BlocBuilder( - builder: (context, state) { - if (state is WarningCenterLoading) { - return const Center( - child: CircularProgressIndicator(color: Colors.white), - ); - } - - if (state is WarningCenterError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, color: Colors.white, size: 48), - const SizedBox(height: 16), - Text( - state.message, - style: const TextStyle(color: Colors.white), - ), - ], - ), - ); - } - - if (state is WarningCenterLoaded) { - return _buildContent(context, state); - } - - return const SizedBox(); - }, - ), - ), - ); - } - - Widget _buildContent(BuildContext context, WarningCenterLoaded state) { - return CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 80, - floating: false, - pinned: true, - backgroundColor: const Color(0xFF0A2E6B), - title: Row( - children: [ - Text( - AppLocalizations.of(context).translate('warning_center.title'), - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: const Color(0xFF4A7AFF), - borderRadius: BorderRadius.circular(12), - ), - child: const Text( - '1', - style: TextStyle(color: Colors.white, fontSize: 12), - ), - ), - ], - ), - actions: [ - Stack( - children: [ - IconButton( - icon: const Icon(Icons.notifications_none, color: Colors.white, size: 28), - onPressed: () {}, - ), - Positioned( - right: 8, - top: 8, - child: Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Colors.red, - shape: BoxShape.circle, - ), - constraints: const BoxConstraints( - minWidth: 16, - minHeight: 16, - ), - child: const Text( - '8', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ), - ), - ], - ), - const SizedBox(width: 8), - ], - ), - SliverToBoxAdapter( - child: Container( - margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 2, - child: _buildTodayStatsCard(context, state), - ), - const SizedBox(width: 12), - Expanded( - flex: 3, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: _buildStatsCard( - context, - AppLocalizations.of(context).translate('warning_center.severe_alerts'), - state.stats.severeCount.toString(), - const Color(0xFFE74C3C), - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatsCard( - context, - AppLocalizations.of(context).translate('warning_center.processing'), - state.stats.processingCount.toString(), - const Color(0xFFF39C12), - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: _buildStatsCard( - context, - AppLocalizations.of(context).translate('warning_center.closed'), - state.stats.closedCount.toString(), - const Color(0xFF27AE60), - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatsCard( - context, - AppLocalizations.of(context).translate('warning_center.avg_response_time'), - '${state.stats.avgResponseTime}h', - const Color(0xFF3498DB), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ], - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.all')), - _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.severe')), - _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.important')), - _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.general')), - _buildFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.hint')), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade300), - borderRadius: BorderRadius.circular(16), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - AppLocalizations.of(context).translate('warning_center.device_type'), - style: TextStyle( - fontSize: 13, - color: state.selectedLevel == AppLocalizations.of(context).translate('warning_center.all') - ? const Color(0xFF4A7AFF) - : Colors.grey.shade700, - ), - ), - const SizedBox(width: 4), - Icon(Icons.keyboard_arrow_down, size: 16, color: Colors.grey.shade600), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.last_7_days')), - const SizedBox(width: 8), - _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.specific_date')), - const SizedBox(width: 8), - _buildTimeFilterChip(context, state, AppLocalizations.of(context).translate('warning_center.all')), - const SizedBox(width: 8), - Expanded( - child: Container( - height: 36, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(18), - border: Border.all(color: Colors.grey.shade300), - ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: AppLocalizations.of(context).translate('warning_center.search_placeholder'), - hintStyle: TextStyle(fontSize: 13, color: Colors.grey.shade400), - border: InputBorder.none, - isDense: true, - ), - style: const TextStyle(fontSize: 13), - onChanged: (value) { - _bloc.add(WarningCenterSearch(value)); - }, - ), - ), - Icon(Icons.search, size: 18, color: Colors.grey.shade400), - ], - ), - ), - ), - ], - ), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final warning = state.warnings[index]; - return _buildWarningCard(context, warning); - }, - childCount: state.warnings.length, - ), - ), - ), - const SliverToBoxAdapter( - child: SizedBox(height: 100), - ), - ], - ); - } - - Widget _buildTodayStatsCard(BuildContext context, WarningCenterLoaded state) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF4A7AFF), Color(0xFF2E5CDB)], - ), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - AppLocalizations.of(context).translate('warning_center.today_total'), - style: const TextStyle( - color: Colors.white, - fontSize: 14, - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.all(4), - decoration: const BoxDecoration( - color: Color(0xFFE74C3C), - shape: BoxShape.circle, - ), - child: const Icon(Icons.arrow_downward, color: Colors.white, size: 12), - ), - ], - ), - const SizedBox(height: 8), - Text( - state.stats.todayCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 36, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - const Icon(Icons.arrow_downward, color: Colors.white, size: 14), - const SizedBox(width: 4), - Text( - '${state.stats.changePercent.abs()}% ${AppLocalizations.of(context).translate('warning_center.vs_yesterday')}', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildStatsCard(BuildContext context, String title, String value, Color color) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - ), - ), - const SizedBox(height: 8), - Text( - value, - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ); - } - - Widget _buildFilterChip(BuildContext context, WarningCenterLoaded state, String label) { - final isSelected = state.selectedLevel == label; - return GestureDetector( - onTap: () { - _bloc.add(WarningCenterFilterByLevel(label)); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFF4A7AFF) : Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected ? const Color(0xFF4A7AFF) : Colors.grey.shade300, - ), - ), - child: Text( - label, - style: TextStyle( - fontSize: 13, - color: isSelected ? Colors.white : Colors.grey.shade700, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, - ), - ), - ), - ); - } - - Widget _buildTimeFilterChip(BuildContext context, WarningCenterLoaded state, String label) { - final isSelected = state.selectedTimeRange == label; - return GestureDetector( - onTap: () { - _bloc.add(WarningCenterFilterByTime(label)); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: isSelected ? const Color(0xFF4A7AFF) : Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected ? const Color(0xFF4A7AFF) : Colors.grey.shade300, - ), - ), - child: Text( - label, - style: TextStyle( - fontSize: 13, - color: isSelected ? Colors.white : Colors.grey.shade700, - ), - ), - ), - ); - } - - Widget _buildWarningCard(BuildContext context, WarningItem warning) { - Color levelColor; - switch (warning.level) { - case '严重': - levelColor = const Color(0xFFE74C3C); - break; - case '重要': - levelColor = const Color(0xFFF39C12); - break; - case '一般': - levelColor = const Color(0xFF27AE60); - break; - default: - levelColor = const Color(0xFF3498DB); - } - - Color statusColor; - switch (warning.status) { - case '处理中': - statusColor = const Color(0xFF4A7AFF); - break; - case '待处理': - statusColor = const Color(0xFFF39C12); - break; - case '已关闭': - statusColor = const Color(0xFF27AE60); - break; - default: - statusColor = Colors.grey; - } - - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), - decoration: BoxDecoration( - color: levelColor, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - warning.level, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - '${warning.deviceName}(${warning.deviceType})', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Color(0xFF1A1A1A), - ), - ), - ), - Text( - warning.duration, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - ), - ), - const SizedBox(width: 12), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: statusColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: statusColor), - ), - child: Text( - warning.status, - style: TextStyle( - fontSize: 12, - color: statusColor, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${AppLocalizations.of(context).translate('warning_center.alert_content')}:${warning.content}', - style: const TextStyle( - fontSize: 14, - color: Color(0xFF333333), - ), - ), - const SizedBox(height: 6), - Text( - '${AppLocalizations.of(context).translate('warning_center.alert_time')}:${warning.time}', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade600, - ), - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - AppLocalizations.of(context).translate('warning_center.pending_status'), - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 4), - Text( - warning.status, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Color(0xFF1A1A1A), - ), - ), - const SizedBox(height: 4), - Text( - AppLocalizations.of(context).translate('warning_center.responsible_person'), - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade600, - ), - ), - const SizedBox(height: 4), - Text( - warning.responsiblePerson, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: Color(0xFF1A1A1A), - ), - ), - ], - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/features/waring_center/presentation/routes/warning_center_routes.dart b/lib/features/waring_center/presentation/routes/warning_center_routes.dart deleted file mode 100644 index 5e3414cc..00000000 --- a/lib/features/waring_center/presentation/routes/warning_center_routes.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:go_router/go_router.dart'; -import 'package:maibu_satabot_v2/core/router/route_paths.dart'; -import 'package:maibu_satabot_v2/features/waring_center/presentation/pages/warning_center_page.dart'; - -class WarningCenterRoutes { - static List get routes => [ - GoRoute( - path: RoutePaths.warningCenter, - builder: (context, state) => const WarningCenterPage(), - ), - ]; -} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 79018f52..6f0e0cce 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,6 +16,7 @@ import package_info_plus import sentry_flutter import shared_preferences_foundation import url_launcher_macos +import video_player_avfoundation import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -30,5 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index c691d70b..b6bc79ee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -208,6 +208,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -580,6 +588,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.15.6" http: dependency: "direct main" description: @@ -1449,6 +1465,46 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.1.1" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.11.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.5" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: "9338f3ec22774f88146b22f13273a446719b1da010fd200c4d1d97802156ac58" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.9.7" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.7.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" visibility_detector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b411ebc5..9b3f1ea6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: # ===== 图片 ===== flutter_svg: ^2.2.3 image_picker: ^1.1.2 + video_player: ^2.8.2 # ===== 屏幕适配,高刷等 ===== diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3d39b7c0..bf0b1810 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,7 +6,6 @@ #include "generated_plugin_registrant.h" -#include #include #include #include @@ -16,8 +15,6 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { - ConnectivityPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterWebRTCPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1caa5bbb..b63b2b8a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - connectivity_plus file_selector_windows flutter_webrtc geolocator_windows