diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 47d09d1a..506c4865 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -6,3 +6,68 @@ # 如果你用 dio + json_serializable / gson -keepattributes Signature -keepattributes *Annotation* + +# ===== 火山引擎 RTC SDK 保留规则 ===== +# 保留所有火山引擎相关类 +-dontwarn com.ss.bytertc.** +-keep class com.ss.bytertc.** { *; } + +# 荣耀音频相关类 +-dontwarn com.hihonor.android.magicx.media.audio.interfaces.** +-keep class com.hihonor.android.magicx.media.audio.interfaces.** { *; } + +# ===== 声网 Agora RTC SDK 保留规则 ===== +# 保留所有声网相关类 +-dontwarn io.agora.** +-keep class io.agora.** { *; } + +# 保留native方法 +-keepclasseswithmembernames class * { + native ; +} + +# ===== FastJSON 保留规则 ===== +# AWT 相关 +-dontwarn java.awt.** +-keep class java.awt.Color { *; } +-keep class java.awt.Font { *; } +-keep class java.awt.Point { *; } +-keep class java.awt.Rectangle { *; } + +# Javax Money +-dontwarn javax.money.** +-keep class javax.money.CurrencyUnit { *; } +-dontwarn org.javamoney.moneta.** +-keep class org.javamoney.moneta.Money { *; } + +# JAX-RS +-dontwarn javax.ws.rs.** +-keep class javax.ws.rs.Consumes { *; } +-keep class javax.ws.rs.Produces { *; } +-keep class javax.ws.rs.core.Response { *; } +-keep class javax.ws.rs.core.StreamingOutput { *; } +-keep class javax.ws.rs.ext.MessageBodyReader { *; } +-keep class javax.ws.rs.ext.MessageBodyWriter { *; } +-keep class javax.ws.rs.ext.Provider { *; } + +# Jersey +-dontwarn org.glassfish.jersey.internal.spi.** +-keep class org.glassfish.jersey.internal.spi.AutoDiscoverable { *; } + +# Joda Time +-dontwarn org.joda.time.** +-keep class org.joda.time.DateTime { *; } +-keep class org.joda.time.DateTimeZone { *; } +-keep class org.joda.time.Duration { *; } +-keep class org.joda.time.Instant { *; } +-keep class org.joda.time.LocalDate { *; } +-keep class org.joda.time.LocalDateTime { *; } +-keep class org.joda.time.LocalTime { *; } +-keep class org.joda.time.Period { *; } +-keep class org.joda.time.ReadablePartial { *; } +-keep class org.joda.time.format.DateTimeFormat { *; } +-keep class org.joda.time.format.DateTimeFormatter { *; } + +# Springfox +-dontwarn springfox.documentation.spring.web.json.** +-keep class springfox.documentation.spring.web.json.Json { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dddd736c..294527d6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,8 @@ + android:icon="@mipmap/launcher_icon" + android:usesCleartextTraffic="true"> GetIt.I(), + child: YourPageContent(), + ); + } +} +``` + +### 2. 获取任务池(选择航线时调用) + +```dart +// 当用户选择航线后,获取该设备的任务 +final deviceId = targetDevice?.deviceId ?? ''; +if (deviceId.isNotEmpty) { + context.read().fetchAndFilterTask(deviceId); +} +``` + +### 3. 监听状态变化(自动显示错误弹窗) + +```dart +BlocConsumer( + listener: (context, state) { + // 🔥 自动显示错误弹窗(2秒后自动消失) + if (state.shouldShowError && state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), + ), + ); + } + + // 操作成功提示 + if (state.operationType == DeviceTaskOperationType.cancel && + !state.isLoading) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('取消任务成功')), + ); + } + }, + builder: (context, state) { + // 显示加载状态 + if (state.isLoading) { + return const CircularProgressIndicator(); + } + + // 显示当前任务ID + final taskId = state.currentTaskId; + return Text('当前任务ID: $taskId'); + }, +) +``` + +**关键点:** +- `shouldShowError` 为 true 时,表示发生了错误,需要显示弹窗 +- 弹窗会自动在 2 秒后消失 +- **不会影响页面展示**,页面继续正常运行 + +### 4. 执行任务操作 + +#### 取消任务 +```dart +final deviceId = targetDevice?.deviceId ?? ''; +context.read().cancelTask(deviceId); +``` + +#### 暂停任务 +```dart +final deviceId = targetDevice?.deviceId ?? ''; +context.read().pauseTask(deviceId); +``` + +#### 恢复任务 +```dart +final deviceId = targetDevice?.deviceId ?? ''; +context.read().recoveryTask(deviceId); +``` + +### 5. 手动更新 taskId(选择新航线时) + +```dart +// 如果需要在选择航线时手动设置 taskId +context.read().updateCurrentTaskId(newTaskId); +``` + +### 6. 清除当前任务 + +```dart +// 退出页面或切换设备时清除 +context.read().clearCurrentTask(); +``` + +## 注意事项 + +1. **自动获取用户信息和场站ID** + - Cubit 内部会自动从 AppUserCubit 和 SiteCubit 获取所需参数 + - 无需手动传递 userId、orgId、siteId + +2. **统一错误处理** + - 所有错误都会通过 ErrorHandler 转换为友好提示 + - 不会显示原始错误信息(如 HTTP 404、连接超时等) + +3. **taskId 全局管理** + - fetchAndFilterTask 会自动过滤并保存当前设备的 taskId + - 所有操作接口都会使用这个全局保存的 taskId + +4. **状态监听** + - operationType 可以区分当前正在进行的操作类型 + - isLoading 表示是否正在执行网络请求 + +## 完整示例 + +```dart +class RoutePlanningPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => GetIt.I(), + child: Scaffold( + appBar: AppBar(title: const Text('路径规划')), + body: BlocConsumer( + listener: (context, state) { + if (state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.errorMessage!)), + ); + } + }, + builder: (context, state) { + return Column( + children: [ + // 显示当前任务ID + Text('当前任务ID: ${state.currentTaskId ?? "无"}'), + + // 操作按钮 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () { + final deviceId = 'YOUR_DEVICE_ID'; + context.read().pauseTask(deviceId); + }, + child: const Text('暂停'), + ), + ElevatedButton( + onPressed: () { + final deviceId = 'YOUR_DEVICE_ID'; + context.read().recoveryTask(deviceId); + }, + child: const Text('恢复'), + ), + ElevatedButton( + onPressed: () { + final deviceId = 'YOUR_DEVICE_ID'; + context.read().cancelTask(deviceId); + }, + child: const Text('取消'), + ), + ], + ), + + // 加载指示器 + if (state.isLoading) + const CircularProgressIndicator(), + ], + ); + }, + ), + ), + ); + } +} +``` + +## 错误处理优化 + +项目中已实现统一的错误处理机制: +- 所有接口错误都会转换为友好的中文提示 +- 不会显示原始的错误信息(如 "HTTP 404"、"Connection timeout" 等) +- 提示会自动消失(2秒后) +- 页面不会崩溃 + +示例错误提示: +- "网络连接超时,请检查网络设置" +- "登录已过期,请重新登录" +- "操作失败,请稍后重试" +- "未知错误,请稍后重试" diff --git a/docs/ERROR_HANDLING_BEST_PRACTICE.md b/docs/ERROR_HANDLING_BEST_PRACTICE.md new file mode 100644 index 00000000..e41abffa --- /dev/null +++ b/docs/ERROR_HANDLING_BEST_PRACTICE.md @@ -0,0 +1,303 @@ +# Flutter 项目统一错误处理规范 + +## 核心原则 + +**所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)** + +## 实现方案 + +### 1. Cubit/Bloc 层处理 + +#### ✅ 正确做法 + +```dart +class MyCubit extends Cubit { + Future fetchData() async { + try { + emit(state.copyWith(isLoading: true)); + + final result = await useCase.call(params); + + result.fold( + (failure) { + // 🔥 设置错误信息 + 标记需要显示弹窗 + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 关键! + )); + }, + (data) { + emit(state.copyWith( + isLoading: false, + data: data, + )); + }, + ); + } catch (e) { + // 🔥 捕获所有异常 + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 关键! + )); + } + } +} +``` + +#### ❌ 错误做法 + +```dart +// ❌ 不要抛出异常到页面层 +throw Exception('网络错误'); + +// ❌ 不要显示原始错误信息 +emit(state.copyWith(errorMessage: e.toString())); + +// ❌ 不要让页面崩溃 +if (error) throw error; +``` + +### 2. State 设计 + +```dart +class MyState extends Equatable { + final bool isLoading; + final String? errorMessage; + final bool shouldShowError; // 🔥 关键字段 + + const MyState({ + this.isLoading = false, + this.errorMessage, + this.shouldShowError = false, // 默认 false + }); + + MyState copyWith({ + bool? isLoading, + String? errorMessage, + bool? shouldShowError, + }) { + return MyState( + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage, + shouldShowError: shouldShowError ?? false, // 🔥 默认重置为 false + ); + } +} +``` + +**关键点:** +- `shouldShowError` 默认为 `false` +- 每次 emit 时如果不传,会自动重置为 `false` +- 这样可以确保错误弹窗只显示一次 + +### 3. 页面层监听 + +```dart +BlocConsumer( + listener: (context, state) { + // 🔥 自动显示错误弹窗 + if (state.shouldShowError && state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + builder: (context, state) { + // 页面正常渲染,不受错误影响 + if (state.isLoading) { + return const CircularProgressIndicator(); + } + + return YourContent(); + }, +) +``` + +### 4. ErrorHandler 工具类 + +```dart +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; + +class ErrorHandler { + /// 获取友好的错误消息 + static String getErrorMessage(Object error) { + if (error is DioException) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return '网络连接超时,请检查网络设置'; + + case DioExceptionType.connectionError: + return '网络连接失败,请检查网络'; + + case DioExceptionType.badResponse: + final statusCode = error.response?.statusCode; + if (statusCode == 401) { + return '登录已过期,请重新登录'; + } else if (statusCode == 403) { + return '没有权限执行此操作'; + } else if (statusCode == 404) { + return '请求的资源不存在'; + } else if (statusCode == 500) { + return '服务器异常,请稍后重试'; + } else { + return '服务器响应异常'; + } + + default: + return '请求失败,请稍后重试'; + } + } else { + return '操作失败,请稍后重试'; + } + } +} +``` + +## 完整示例 + +### DeviceTaskCubit 示例 + +```dart +class DeviceTaskCubit extends Cubit { + Future cancelTask(String deviceId) async { + final taskId = state.currentTaskId; + if (taskId == null) { + emit(state.copyWith(errorMessage: '无可用任务')); + return; + } + + emit(state.copyWith( + isLoading: true, + operationType: DeviceTaskOperationType.cancel, + )); + + try { + final result = await _cancelTaskUseCase.call(params); + + result.fold( + (failure) { + _logger.logWithLevel('❌ 取消任务失败: ${failure.message}'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); + }, + (success) { + _logger.logWithLevel('✅ 取消任务成功'); + emit(state.copyWith( + isLoading: false, + operationType: DeviceTaskOperationType.none, + )); + }, + ); + } catch (e) { + _logger.logWithLevel('❌ 取消任务异常: $e'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); + } + } +} +``` + +### 页面使用示例 + +```dart +class TaskPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => GetIt.I(), + child: Scaffold( + appBar: AppBar(title: const Text('任务管理')), + body: BlocConsumer( + listener: (context, state) { + // 🔥 自动显示错误弹窗 + if (state.shouldShowError && state.errorMessage != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage!), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), + ), + ); + } + }, + builder: (context, state) { + return Column( + children: [ + // 页面内容不受错误影响 + Text('当前任务ID: ${state.currentTaskId ?? "无"}'), + + ElevatedButton( + onPressed: () { + context.read().cancelTask(deviceId); + }, + child: const Text('取消任务'), + ), + + if (state.isLoading) + const CircularProgressIndicator(), + ], + ); + }, + ), + ), + ); + } +} +``` + +## 错误提示文案规范 + +| 错误类型 | 提示文案 | +|---------|---------| +| 网络超时 | "网络连接超时,请检查网络设置" | +| 连接失败 | "网络连接失败,请检查网络" | +| 401 未授权 | "登录已过期,请重新登录" | +| 403 禁止访问 | "没有权限执行此操作" | +| 404 资源不存在 | "请求的资源不存在" | +| 500 服务器错误 | "服务器异常,请稍后重试" | +| 其他业务错误 | "操作失败,请稍后重试" | +| 未知错误 | "未知错误,请稍后重试" | + +**注意:** +- ✅ 使用友好的中文提示 +- ❌ 不要显示 HTTP 状态码 +- ❌ 不要显示技术术语(如 "DioException"、"timeout") +- ❌ 不要显示完整的错误堆栈 + +## 优势 + +1. **页面不崩溃** - 所有异常都被捕获 +2. **用户体验好** - 友好的中文提示 +3. **自动消失** - 2秒后弹窗自动关闭 +4. **不影响操作** - 用户可以继续使用页面 +5. **统一规范** - 全项目统一的错误处理方式 + +## 检查清单 + +在开发新功能时,确保: + +- [ ] Cubit 中所有 try-catch 都使用了 `ErrorHandler.getErrorMessage()` +- [ ] State 中有 `shouldShowError` 字段 +- [ ] 错误时设置 `shouldShowError: true` +- [ ] copyWith 中 `shouldShowError` 默认为 `false` +- [ ] 页面 BlocConsumer listener 中监听 `shouldShowError` +- [ ] 使用 SnackBar 显示错误(2秒自动消失) +- [ ] 不显示原始错误信息 diff --git a/docs/ERROR_HANDLING_FIX.md b/docs/ERROR_HANDLING_FIX.md new file mode 100644 index 00000000..b1e046ba --- /dev/null +++ b/docs/ERROR_HANDLING_FIX.md @@ -0,0 +1,388 @@ +# 统一错误处理修复记录 + +## 问题描述 + +之前项目中存在多个地方直接将原始错误信息(如 `DioException [connection error]...`)显示在页面上,严重影响用户体验。这些错误包括: +- 网络连接失败 +- HTTP 状态码错误 +- 超时错误 +- 其他技术异常 + +**问题表现:** +- 全屏显示错误页面 +- 直接展示技术错误信息(如 `DioException`、`HTTP 500` 等) +- 用户无法继续操作 +- 错误信息不友好,普通用户看不懂 + +## 解决方案 + +采用统一的错误处理机制,确保: +1. ✅ **所有错误都被拦截**,不会直接显示原始错误 +2. ✅ **友好的中文提示**,使用 SnackBar 浮动显示 2 秒后自动消失 +3. ✅ **不影响页面渲染**,错误发生时页面保持不变,用户可以继续操作 +4. ✅ **统一的错误处理模式**,所有 BLoC/Cubit 都遵循相同规范 + +## 修复的文件列表 + +### 1. DeviceStatusBloc (设备状态) +**文件路径:** `lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart` + +**修改内容:** +- ✅ 添加 `ErrorHandler` 导入 +- ✅ 将 `emit(DeviceStatusError(e.toString()))` 改为使用 `ErrorHandler.getErrorMessage(e)` +- ✅ 设置 `shouldShowError: true` 标记 + +**State 更新:** +```dart +class DeviceStatusError extends DeviceStatusState { + final String message; + final bool shouldShowError; // 🔥 新增 + + const DeviceStatusError({ + required this.message, + this.shouldShowError = false, + }); +} +``` + +**页面更新:** +```dart +// device_status_page.dart +BlocConsumer( + listener: (context, state) { + if (state is DeviceStatusError && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, + ), + ); + } + }, + builder: (context, state) { + if (state is DeviceStatusError) { + return const SizedBox.shrink(); // 🔥 不再显示全屏错误 + } + // ... 正常内容 + }, +) +``` + +--- + +### 2. DroneStationBloc (无人机机场) +**文件路径:** `lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart` + +**修改内容:** +- ✅ 添加 `ErrorHandler` 导入 +- ✅ 更新所有错误状态: + - `DroneStationError` + - `UAVDetailError` + - `VideoStreamError` + - `UavVideoStreamError` +- ✅ 所有错误都设置 `shouldShowError: true` + +**State 更新:** +```dart +// lib/features/v2/device_list/presentation/bloc/drone_station_state.dart +class DroneStationError extends DroneStationState { + final String message; + final bool shouldShowError; // 🔥 新增 + + const DroneStationError({ + required this.message, + this.shouldShowError = false, + }); +} +``` + +**页面更新:** +```dart +// device_status_page.dart - _buildDroneStationList +BlocConsumer( + listener: (context, state) { + if (state is DroneStationError && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar(...); + } + }, + builder: (context, state) { + if (state is DroneStationError) { + return const SizedBox.shrink(); // 🔥 保持页面 + } + // ... 正常内容 + }, +) +``` + +--- + +### 3. RobotListBloc (机器人列表) +**文件路径:** `lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart` + +**修改内容:** +- ✅ 添加 `ErrorHandler` 导入 +- ✅ 将 `emit(RobotListError(e.toString()))` 改为使用 `ErrorHandler` +- ✅ 设置 `shouldShowError: true` + +**State 更新:** +```dart +class RobotListError extends RobotListState { + final String message; + final bool shouldShowError; // 🔥 新增 + + const RobotListError({ + required this.message, + this.shouldShowError = false, + }); +} +``` + +**页面更新:** +```dart +// robot_list_page.dart +BlocConsumer( + listener: (context, state) { + if (state is RobotListError && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar(...); + } + }, + builder: (context, state) { + if (state is RobotListError) { + return const SizedBox.shrink(); + } + // ... 正常内容 + }, +) +``` + +--- + +### 4. HomeV2Bloc (首页 V2) +**文件路径:** `lib/features/v2/home/presentation/bloc/home_v2_bloc.dart` + +**修改内容:** +- ✅ 添加 `ErrorHandler` 导入 +- ✅ 将所有 `emit(HomeV2Error(failure.message))` 改为使用 `ErrorHandler` +- ✅ 设置 `shouldShowError: true` + +**State 更新:** +```dart +class HomeV2Error extends HomeV2State { + final String message; + final bool shouldShowError; // 🔥 新增 + + const HomeV2Error({ + required this.message, + this.shouldShowError = false, + }); +} +``` + +**页面更新:** +```dart +// home_v2_page.dart +BlocConsumer( + listener: (context, state) { + if (state is HomeV2Error && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar(...); + } + }, + builder: (context, state) { + if (state is HomeV2Error) { + return const SizedBox.shrink(); + } + // ... 正常内容 + }, +) +``` + +--- + +## 统一错误处理流程 + +### 1. BLoC/Cubit 层 +```dart +try { + // 业务逻辑 + final result = await useCase.call(params); + result.fold( + (failure) => emit(ErrorState( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 关键:标记需要显示弹窗 + )), + (data) => emit(SuccessState(data)), + ); +} catch (e) { + emit(ErrorState( + message: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 关键:标记需要显示弹窗 + )); +} +``` + +### 2. State 层 +```dart +class ErrorState extends SomeState { + final String message; + final bool shouldShowError; // 🔥 关键:用于标记是否显示弹窗 + + const ErrorState({ + required this.message, + this.shouldShowError = false, // 默认不显示 + }); +} +``` + +### 3. Page 层 +```dart +BlocConsumer( + listener: (context, state) { + // 🔥 监听错误并显示友好提示 + if (state is ErrorState && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, + ), + ); + } + }, + builder: (context, state) { + if (state is ErrorState) { + // 🔥 返回空容器,保持当前页面不变 + return const SizedBox.shrink(); + } + // ... 正常内容 + }, +) +``` + +--- + +## ErrorHandler 工具类 + +**文件路径:** `lib/core/network/error_handler.dart` + +**功能:** +- 将所有 Dio 异常转换为友好的中文提示 +- 支持多种错误类型: + - 连接超时 → "网络连接超时,请检查网络设置" + - 连接失败 → "网络连接失败,请检查网络" + - 401 → "登录已过期,请重新登录" + - 403 → "没有权限执行此操作" + - 404 → "请求的资源不存在" + - 500 → "服务器异常,请稍后重试" + - 其他 → "操作失败,请稍后重试" + +**使用方法:** +```dart +// 在 BLoC/Cubit 中 +String friendlyMessage = ErrorHandler.getErrorMessage(error); + +// 或在页面中直接显示 +ErrorHandler.handleError(context, error); +``` + +--- + +## 修复效果对比 + +### 修复前 ❌ +``` +┌─────────────────────────────┐ +│ │ +│ ⚠️ Error Icon │ +│ │ +│ DioException [connection │ +│ error]: The connection │ +│ errored: Failed host │ +│ lookup... │ +│ │ +│ [ 重试 ] │ +│ │ +└─────────────────────────────┘ +``` +- 全屏错误页面 +- 显示原始技术错误 +- 用户无法继续操作 +- 体验极差 + +### 修复后 ✅ +``` +┌─────────────────────────────┐ +│ │ +│ [正常页面内容] │ +│ │ +│ 用户可以看到页面 │ +│ 可以继续操作 │ +│ │ +└─────────────────────────────┘ + ↓ + ┌──────────────┐ + │ 网络连接失败 │ ← SnackBar 浮动显示 + │ 请检查网络 │ 2秒后自动消失 + └──────────────┘ +``` +- 页面保持不变 +- 友好的中文提示 +- SnackBar 浮动显示 +- 2秒自动消失 +- 用户可以继续操作 + +--- + +## 最佳实践 + +### ✅ DO - 应该这样做 +1. 所有 BLoC/Cubit 的错误处理都使用 `ErrorHandler.getErrorMessage()` +2. 所有错误状态都设置 `shouldShowError: true` +3. 页面使用 `BlocConsumer` 监听错误 +4. 错误时使用 `SizedBox.shrink()` 保持页面 +5. SnackBar 设置为 `floating` 和 `orange` 背景 + +### ❌ DON'T - 不要这样做 +1. ❌ 直接使用 `e.toString()` 作为错误消息 +2. ❌ 在页面中显示全屏错误页面 +3. ❌ 直接展示原始技术错误(DioException、HTTP 状态码等) +4. ❌ 让用户点击"重试"按钮才能恢复 +5. ❌ 不同页面使用不同的错误处理方式 + +--- + +## 后续工作 + +### 待迁移的 BLoC/Cubit +以下 BLoC/Cubit 可能还需要迁移到统一的错误处理模式: +- [ ] 其他未检查的 BLoC +- [ ] 第三方库相关的错误处理 +- [ ] WebSocket 连接的错误处理 + +### 优化建议 +1. 考虑为不同类型的错误设置不同的 SnackBar 颜色 +2. 可以添加错误日志上报功能 +3. 可以考虑添加错误重试机制(在后台自动重试) +4. 可以为特定场景定制错误提示文案 + +--- + +## 总结 + +通过本次修复,我们实现了: +✅ 统一的错误处理机制 +✅ 友好的用户提示 +✅ 不影响页面渲染 +✅ 提升用户体验 +✅ 代码规范统一 + +**核心原则:** +> 任何接口异常都不应该影响页面的展示,给个几秒弹窗就行! + +--- + +**修复日期:** 2026-06-15 +**修复人员:** AI Assistant +**影响范围:** 设备管理、首页相关的所有 BLoC/Cubit diff --git a/lib/components/device_status_modal.dart b/lib/components/device_status_modal.dart index e589e2ab..1ceb98a6 100644 --- a/lib/components/device_status_modal.dart +++ b/lib/components/device_status_modal.dart @@ -4,12 +4,16 @@ import 'dart:math' as math; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import '../features/devices/presentation/bloc/device_status_bloc.dart'; import '../features/devices/presentation/bloc/device_status_state.dart'; import '../features/devices/presentation/bloc/device_status_event.dart'; +import '../features/devices/presentation/bloc/devices_cubit.dart'; +import '../features/remote_control/presentation/bloc/remote_control_cubit.dart'; +import '../features/remote_control/presentation/bloc/remote_control_state.dart'; const int DATA_TIMEOUT_SECONDS = 5; @@ -46,6 +50,16 @@ class _DeviceStatusModalState extends State { void initState() { super.initState(); _startDataTimeoutTimer(); + // 🔥 页面初始化时自动调用刷新 + WidgetsBinding.instance.addPostFrameCallback((_) { + _onRefresh(); + }); + } + + /// 🔥 刷新函数 - 与刷新按钮共享逻辑 + void _onRefresh() { + context.read().add(DeviceStatusReset()); + _resetChartData(); } @override @@ -468,14 +482,35 @@ class _DeviceStatusModalState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - AppLocalizations.of( - context, - ).translate('running_status.title'), - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of( + context, + ).translate('running_status.title'), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + // 🔥 显示当前TCP认证的设备号 + BlocBuilder( + builder: (context, state) { + var deviceS = + state.targetDevice?.deviceName ?? '未选择'; + return Text( + '设备: $deviceS', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ); + }, + ), + ], ), ), IconButton( @@ -600,12 +635,7 @@ class _DeviceStatusModalState extends State { ), IconButton( icon: const Icon(Icons.refresh, color: Color(0xFF1677FF)), - onPressed: () { - context.read().add( - DeviceStatusReset(), - ); - _resetChartData(); - }, + onPressed: _onRefresh, ), ], ), diff --git a/lib/core/consts/http_api_consts.dart b/lib/core/consts/http_api_consts.dart index 2b75ad7d..a0491bab 100644 --- a/lib/core/consts/http_api_consts.dart +++ b/lib/core/consts/http_api_consts.dart @@ -52,9 +52,6 @@ class HttpApiConsts { // 更新飞行任务状态 static const String updateFlightTaskStatus = "$baseUrl/iot/UAV/updateFlightTaskStatus"; - // 返航、暂停等命令 - static const String flightTaskCommand = "$baseUrl/iot/UAV/flightTaskCommand"; - // 切换无人机镜头获取视频流 static const String changeUAVLens = "$baseUrl/iot/UAV/changeLens"; } diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart index f470a468..e4e698d9 100644 --- a/lib/core/di/injection.dart +++ b/lib/core/di/injection.dart @@ -17,7 +17,6 @@ import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart'; import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart'; import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.dart'; import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart'; -import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart'; import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart'; import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart'; import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart'; @@ -50,6 +49,8 @@ import '../../features/devices/domain/usecases/generate_path_usecase.dart'; import '../../features/devices/domain/usecases/get_device_location_usecase.dart'; import '../../features/devices/domain/usecases/get_work_record_usecase.dart'; import '../../features/devices/domain/usecases/get_work_records_by_site_id_usecase.dart'; +import '../../features/devices/domain/usecases/create_device_task_usecase.dart'; +import '../../features/devices/domain/usecases/get_work_records_by_site_id_usecase.dart'; import '../../features/devices/domain/usecases/route_planning_usecase.dart'; import '../../features/devices/domain/usecases/save_work_record_usecase.dart'; import '../../features/devices/domain/usecases/select_work_record_usecase.dart'; @@ -110,6 +111,14 @@ import '../../features/v2/message_center/data/repositories/message_center_reposi import '../../features/v2/message_center/domain/repositories/message_center_repository.dart'; import '../../features/v2/message_center/domain/usecases/message_center_usecase.dart'; import '../../features/v2/message_center/presentation/bloc/message_center_cubit.dart'; +import '../../features/devices/data/datasources/device_task_datasource.dart'; +import '../../features/devices/data/repositories/device_task_repository_impl.dart'; +import '../../features/devices/domain/repositories/device_task_repository.dart'; +import '../../features/devices/domain/usecases/get_device_task_pool_usecase.dart'; +import '../../features/devices/domain/usecases/cancel_task_usecase.dart'; +import '../../features/devices/domain/usecases/pause_task_usecase.dart'; +import '../../features/devices/domain/usecases/recovery_task_usecase.dart'; +import '../../features/devices/presentation/bloc/device_task_cubit.dart'; import '../app/app_user_cubit.dart'; import '../localization/locale_cubit.dart'; import '../../features/home/presentation/bloc/permission_request_bloc.dart'; @@ -369,11 +378,6 @@ Future init() async { // Tab 配置 Cubit (单例) sl.registerLazySingleton(() => TabConfigCubit(sl())); - // 🔥 悬浮条设置服务 (单例) - sl.registerLazySingleton( - () => FloatBarSettingService(sl()), - ); - sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl())); sl.registerLazySingleton( () => DevicesCubit( @@ -404,8 +408,8 @@ Future init() async { ), ); - // 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建) - sl.registerFactory( + // 🔥 RemoteControlCubit 注入 DeviceStatusBloc(单例模式,全局共享) + sl.registerLazySingleton( () => RemoteControlCubit( sl(), // RemoteControlRepository sl(), // RequestControlPermissionUseCase @@ -447,6 +451,11 @@ Future init() async { () => GetWorkRecordsBySiteIdUseCase(sl()), ); + /// 创建设备任务(通过接口执行作业) + sl.registerLazySingleton( + () => CreateDeviceTaskUseCase(sl()), + ); + /// 6. 认证 (Auth) // --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 --- sl.registerLazySingleton( @@ -485,4 +494,24 @@ Future init() async { markAllAsReadUseCase: sl(), ), ); + + /// 10. 设备任务管理 (Device Task) + sl.registerLazySingleton( + () => DeviceTaskDatasourceImpl(sl()), + ); + sl.registerLazySingleton( + () => DeviceTaskRepositoryImpl(datasource: sl()), + ); + sl.registerLazySingleton(() => GetDeviceTaskPoolUseCase(sl())); + sl.registerLazySingleton(() => CancelTaskUseCase(sl())); + sl.registerLazySingleton(() => PauseTaskUseCase(sl())); + sl.registerLazySingleton(() => RecoveryTaskUseCase(sl())); + sl.registerFactory( + () => DeviceTaskCubit( + sl(), + sl(), + sl(), + sl(), + ), + ); } diff --git a/lib/core/network/error_handler.dart b/lib/core/network/error_handler.dart new file mode 100644 index 00000000..0cd4dc78 --- /dev/null +++ b/lib/core/network/error_handler.dart @@ -0,0 +1,70 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; + +/// 统一错误处理器 +/// 所有接口错误都应该通过这个类来处理,避免显示原始错误信息 +class ErrorHandler { + /// 处理错误并显示友好提示 + static void handleError(BuildContext context, Object error) { + String message = _getFriendlyErrorMessage(error); + + // 显示友好提示(自动消失) + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.orange, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + ), + ); + } + + /// 获取友好的错误消息 + static String _getFriendlyErrorMessage(Object error) { + if (error is DioException) { + switch (error.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.sendTimeout: + case DioExceptionType.receiveTimeout: + return '网络连接超时,请检查网络设置'; + + case DioExceptionType.connectionError: + return '网络连接失败,请检查网络'; + + case DioExceptionType.badResponse: + final statusCode = error.response?.statusCode; + if (statusCode == 401) { + return '登录已过期,请重新登录'; + } else if (statusCode == 403) { + return '没有权限执行此操作'; + } else if (statusCode == 404) { + return '请求的资源不存在'; + } else if (statusCode == 500) { + return '服务器异常,请稍后重试'; + } else { + return '服务器响应异常'; + } + + case DioExceptionType.cancel: + return '请求已取消'; + + case DioExceptionType.badCertificate: + return '证书验证失败'; + + default: + return '请求失败,请稍后重试'; + } + } else if (error is Exception) { + // 业务逻辑异常 + return '操作失败,请稍后重试'; + } else { + // 未知错误 + return '未知错误,请稍后重试'; + } + } + + /// 在 Cubit/Bloc 中使用的静态方法(不需要 context) + static String getErrorMessage(Object error) { + return _getFriendlyErrorMessage(error); + } +} diff --git a/lib/core/network/net_message_dispatcher.dart b/lib/core/network/net_message_dispatcher.dart index f3644240..ddb78d65 100644 --- a/lib/core/network/net_message_dispatcher.dart +++ b/lib/core/network/net_message_dispatcher.dart @@ -91,7 +91,7 @@ class NetMessageDispatcher { //机器状态页面专用 Stream onStringMessageOfdeviceStatus() { //print("0x02--TCP拦截推送解析开始"); - _logger.log('0x02--TCP拦截推送解析开始'); + _logger.logWithLevel('0x02--TCP拦截推送解析开始' ,shouldLog: true); // 🔥 关键修复:使用asBroadcastStream()确保多个监听者都能收到数据 return onCommand(0x02).map((p) { try { diff --git a/lib/core/network/tcp/tcp_client.dart b/lib/core/network/tcp/tcp_client.dart index 8cd811e1..2e7a4ad6 100644 --- a/lib/core/network/tcp/tcp_client.dart +++ b/lib/core/network/tcp/tcp_client.dart @@ -323,14 +323,14 @@ class TcpClient { // 新增:启动心跳(每 4 秒发送一次 0xFF 指令) void startHeartbeat({Duration interval = const Duration(seconds: 4)}) { if (_heartbeatTimer != null) return; // 防止重复启动 - //debugPrint('收到服务端心跳,自动回复...'); + debugPrint('⏰ [TCP] 启动心跳定时器,间隔: ${interval.inSeconds}秒'); _logger.logWithLevel('✅ [TCP] 收到服务端心跳,自动回复...', shouldLog: true); _heartbeatTimer = Timer.periodic(interval, (_) { - // 发送心跳帧:AB AA FF 00 00 AA AB(与 sendRaw 一致) - //sendRaw(0xFF, []); + // 发送心跳帧:AB AA FF AA AB + debugPrint('💓 [TCP] 发送心跳包 0xFF'); sendHeartbeat(); }); - debugPrint("TCP Connected"); + debugPrint("✅ TCP Connected - 心跳已启动"); } /// 🔥 封装完整的TCP初始化方法:连接 + 认证 + 心跳 @@ -402,11 +402,11 @@ class TcpClient { var packets = _decoder.decode(data); for (var packet in packets) { // 🔥 最根部日志:收到任何推送都打印 - // debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}'); - // _logger.logWithLevel( - // '📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}', - // shouldLog: true, - // ); + debugPrint('📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}'); + _logger.logWithLevel( + '📥 [TCP-ROOT] 收到推送 CMD: 0x${packet.command.toRadixString(16).toUpperCase()}, Payload长度: ${packet.payload.length}', + shouldLog: true, + ); if (!_controller.isClosed) { _controller.add(packet); @@ -463,7 +463,7 @@ class TcpClient { // // } void disconnect() { - //debugPrint('🛑 [TCP] 主动断开连接...'); + debugPrint('🛑 [TCP] 主动断开连接...'); _logger.logWithLevel('🛑 [TCP] 主动断开连接...'); // 1. 停止心跳 @@ -472,19 +472,38 @@ class TcpClient { // 2. 🔥 关键:取消重连定时器!防止断开后自动触发旧逻辑重连回第一个设备 _reconnectTimer?.cancel(); _reconnectTimer = null; + debugPrint('✅ [TCP] 已取消重连定时器'); - // 3. 销毁 Socket + // 3. 清除 Host/Port,防止重连 + _lastHost = null; + _lastPort = null; + debugPrint('✅ [TCP] 已清除 Host/Port'); + + // 4. 重置标志 + isUserSwitch = false; + _isSwitching = false; + + // 5. 销毁 Socket if (_socket != null) { _socket!.destroy(); _socket = null; - //debugPrint('✅ [TCP] Socket 已物理销毁'); + debugPrint('✅ [TCP] Socket 已物理销毁'); _logger.logWithLevel('✅ [TCP] Socket 已物理销毁'); } + // 6. 🔥 通知 TcpStatusCubit 更新状态为断开 + try { + final tcpStatusCubit = GetIt.I(); + tcpStatusCubit.setDisconnected(); + debugPrint('✅ [TCP] 已通知 TcpStatusCubit 更新状态'); + } catch (e) { + debugPrint('⚠️ [TCP] TcpStatusCubit 未注册,跳过状态更新'); + } + // ❌ 绝对不要关闭 _controller!否则数据流断裂,重连后收不到数据 // _controller.close(); - // debugPrint('✅ [TCP] 连接已断开,等待手动重连'); + debugPrint('✅ [TCP] 连接已彻底断开,下次需重新初始化'); _logger.logWithLevel('✅ [TCP] 断开连接成功'); } @@ -566,6 +585,12 @@ class TcpClient { //debugPrint('🔑 [TCP] 已发送认证包 (0x03): $authString'); _logger.logWithLevel('🔑 [TCP] 已发送认证包 (0x03): $authString'); + // 🔥 关键修复:移除登录时自动获取设备列表和切换设备的逻辑 + // 原因: + // 1. 部分账号没有设备权限,会导致403错误 + // 2. 应该在用户手动选择设备时才触发切换 + // 3. 登录阶段只负责建立TCP连接和认证 + /* try { // 1. 获取 Either 结果 final eitherResult = await getUserDeviceUseCase.repository.getUserDevice(username); @@ -615,6 +640,7 @@ class TcpClient { _logger.logWithLevel('❌ [AuthTcp] 设备订阅流程异常:$e'); rethrow; } + */ } void sendRawBytes(Uint8List bytes) { @@ -627,6 +653,14 @@ class TcpClient { required int port, required String deviceName, }) async { + debugPrint('🔌 [TCP-connectBySwitch] ========== 开始连接 =========='); + debugPrint('🔌 [TCP-connectBySwitch] Host: $host, Port: $port, Device: $deviceName'); + + // 🔥 关键:取消之前的重连定时器,防止冲突 + _reconnectTimer?.cancel(); + _reconnectTimer = null; + debugPrint('✅ [TCP-connectBySwitch] 已取消旧的重连定时器'); + isUserSwitch = true; // debugPrint('🔌被动 [TCP] 开始连接:$host:$port'); // ✅ 必须看到这条 _logger.logWithLevel('🔌被动 [TCP] 开始连接:$host:$port'); @@ -649,6 +683,18 @@ class TcpClient { // debugPrint('✅ 被动[TCP] 连接成功!'); // ✅ 必须看到这条 _logger.logWithLevel('✅ 被动[TCP] 连接成功!'); + // 🔥 更新TCP状态为已连接 + try { + GetIt.I().setConnected(); + debugPrint('✅ [TCP] TcpStatusCubit 状态已更新为 Connected'); + } catch (e) { + _logger.logWithLevel('❌ [TCP] TcpStatusCubit 未注册', shouldLog: false); + } + + // 🔥 启动心跳 + startHeartbeat(interval: const Duration(seconds: 4)); + debugPrint('✅ [TCP] 心跳已启动'); + _socket!.listen( (data) { // debugPrint('📥 被动[TCP] 收到原始数据:${data.length} 字节, 内容:$data'); diff --git a/lib/core/update/update_cubit.dart b/lib/core/update/update_cubit.dart index fc18ab01..a3eaabc0 100644 --- a/lib/core/update/update_cubit.dart +++ b/lib/core/update/update_cubit.dart @@ -185,18 +185,19 @@ class UpdateCubit extends Cubit { } } - /// 在浏览器中打开下载链接(手动下载) - Future openInBrowser(String apkUrl) async { + /// 整包更新:浏览器下载 + 引导弹窗 + Future fullApkUpdateWithBrowser(String apkUrl) async { try { - _logger.i('🌐 在浏览器中打开 APK 下载链接'); + _logger.i('🌐 整包更新:打开浏览器下载'); final uri = Uri.parse(apkUrl); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); _logger.i('✅ 已打开浏览器下载'); - // 🔥 整包更新后,清除补丁版本记录 + // 🔥 整包更新后,立即清除补丁版本记录(避免死循环) await _versionService.clearPatchVersionInfo(); + _logger.i('🗑️ 已清除补丁版本记录'); emit(UpdateSuccess()); } else { diff --git a/lib/core/update/update_dialog.dart b/lib/core/update/update_dialog.dart index 13d1943f..70fbb8cb 100644 --- a/lib/core/update/update_dialog.dart +++ b/lib/core/update/update_dialog.dart @@ -126,14 +126,14 @@ class UpdateDialog extends StatelessWidget { label: const Text('去安装'), ); } else if (state is UpdateInstalling) { - return const Column( + return Column( mainAxisSize: MainAxisSize.min, children: [ - Text('📦 正在打开安装页面...', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), - SizedBox(height: 4), - Text('请在系统安装界面确认安装', style: TextStyle(fontSize: 12)), - SizedBox(height: 8), - CircularProgressIndicator(), + const Text('📦 正在打开安装页面...', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + const Text('请在系统安装界面确认安装', style: TextStyle(fontSize: 12)), + const SizedBox(height: 8), + const CircularProgressIndicator(), ], ); } else { @@ -259,23 +259,25 @@ class FullApkUpdateDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.blue.shade200), + border: Border.all(color: Colors.orange.shade200), ), child: const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '📱 整包更新说明', + '📱 整包更新步骤', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), SizedBox(height: 8), Text( - '• 点击"立即下载"将在浏览器中打开下载链接\n' - '• 下载完成后请手动安装 APK\n' - '• 安装完成后重启应用即可', - style: TextStyle(fontSize: 13, height: 1.5), + '1️⃣ 点击"立即下载"打开浏览器\n' + '2️⃣ 等待 APK 下载完成\n' + '3️⃣ 手动安装新版本的 APK\n' + '4️⃣ 安装完成后,卸载旧版本 APP\n' + '5️⃣ 重新打开新版本 APP', + style: TextStyle(fontSize: 13, height: 1.6), ), ], ), @@ -301,15 +303,12 @@ class FullApkUpdateDialog extends StatelessWidget { ElevatedButton( onPressed: () { if (versionInfo.apkUrl != null) { - context.read().downloadAndInstallApk(versionInfo.apkUrl!); + // 🔥 调用新方法:浏览器下载 + 清除补丁记录 + context.read().fullApkUpdateWithBrowser(versionInfo.apkUrl!); Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('正在浏览器中打开下载链接...'), - duration: Duration(seconds: 2), - ), - ); + // 显示后续引导弹窗 + _showManualInstallGuide(context); } }, child: const Text('立即下载'), @@ -317,4 +316,57 @@ class FullApkUpdateDialog extends StatelessWidget { ], ); } + + /// 显示手动安装引导弹窗 + void _showManualInstallGuide(BuildContext context) { + Future.delayed(const Duration(milliseconds: 500), () { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('🔔 重要提示'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '请在浏览器中完成以下操作:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + const Text('✅ 等待 APK 下载完成'), + const SizedBox(height: 8), + const Text('✅ 点击 APK 文件进行安装'), + const SizedBox(height: 8), + const Text('✅ 安装完成后,卸载当前旧版本'), + const SizedBox(height: 8), + const Text('✅ 重新打开新版本 APP'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.red.shade200), + ), + child: const Text( + '⚠️ 注意:必须先卸载旧版本,否则无法正常使用新功能!', + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(ctx).pop(); + }, + child: const Text('我知道了'), + ), + ], + ), + ); + }); + } } + diff --git a/lib/core/update/version_check_service.dart b/lib/core/update/version_check_service.dart index 83f5148b..e0b46332 100644 --- a/lib/core/update/version_check_service.dart +++ b/lib/core/update/version_check_service.dart @@ -83,6 +83,14 @@ class VersionCheckService { _logger.i('🚀 请求后端版本: $requestVersion ($requestVersionCode)'); + // 🔥 打印完整的请求参数 + _logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + _logger.i('📤 App 发送的请求参数:'); + _logger.i(' URL: $apiUrl'); + _logger.i(' version: $requestVersion'); + _logger.i(' versionCode: $requestVersionCode'); + _logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + final response = await _dio.get( apiUrl, queryParameters: { @@ -91,6 +99,13 @@ class VersionCheckService { }, ); + // 🔥 打印完整的后端返回数据 + _logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + _logger.i('📥 后端返回的完整数据:'); + _logger.i(' statusCode: ${response.statusCode}'); + _logger.i(' 原始响应: ${response.data}'); + _logger.i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + if (response.statusCode == 200) { final data = response.data['data']; diff --git a/lib/features/devices/data/datasources/device_task_datasource.dart b/lib/features/devices/data/datasources/device_task_datasource.dart new file mode 100644 index 00000000..fbc0aaab --- /dev/null +++ b/lib/features/devices/data/datasources/device_task_datasource.dart @@ -0,0 +1,188 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import '../../../../core/logging/i_logger_service.dart'; +import '../models/device_task_model.dart'; + +abstract class DeviceTaskDatasource { + Future> getDeviceTaskPool({ + required String userId, + required int siteId, + required int orgId, + int pageNum = 1, + int pageSize = 99999999, + }); + + Future cancelTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); + + Future pauseTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); + + Future> recoveryTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); +} + +class DeviceTaskDatasourceImpl implements DeviceTaskDatasource { + final Dio dio; + final ILoggerService _logger = GetIt.I(); + + DeviceTaskDatasourceImpl(this.dio); + + @override + Future> getDeviceTaskPool({ + required String userId, + required int siteId, + required int orgId, + int pageNum = 1, + int pageSize = 99999999, + }) async { + try { + final url = 'http://1.95.137.212:59015/iot/deviceTask/deviceTaskPool'; + final response = await dio.get( + url, + queryParameters: { + 'userId': userId, + 'siteId': siteId, + 'orgId': orgId, + 'pageNum': pageNum, + 'pageSize': pageSize, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['code'] == 200) { + final rows = data['rows'] as List? ?? []; + return rows + .map((item) => DeviceTaskModel.fromJson(item as Map)) + .toList(); + } else { + throw Exception(data['msg'] ?? '获取任务池失败'); + } + } else { + throw Exception('HTTP ${response.statusCode}'); + } + } catch (e) { + _logger.logWithLevel('❌ 获取任务池失败: $e'); + rethrow; + } + } + + @override + Future cancelTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final url = 'http://1.95.137.212:59015/iot/deviceTask/cancelTask'; + final response = await dio.post( + url, + data: { + 'deviceId': deviceId, + 'taskId': taskId, + 'orgId': orgId, + 'siteId': siteId, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['code'] == 200) { + return data['data'] as bool? ?? false; + } else { + throw Exception(data['msg'] ?? '取消任务失败'); + } + } else { + throw Exception('HTTP ${response.statusCode}'); + } + } catch (e) { + _logger.logWithLevel('❌ 取消任务失败: $e'); + rethrow; + } + } + + @override + Future pauseTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final url = 'http://1.95.137.212:59015/iot/deviceTask/pauseTask'; + final response = await dio.post( + url, + data: { + 'deviceId': deviceId, + 'taskId': taskId, + 'orgId': orgId, + 'siteId': siteId, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['code'] == 200) { + return data['data'] as bool? ?? false; + } else { + throw Exception(data['msg'] ?? '暂停任务失败'); + } + } else { + throw Exception('HTTP ${response.statusCode}'); + } + } catch (e) { + _logger.logWithLevel('❌ 暂停任务失败: $e'); + rethrow; + } + } + + @override + Future> recoveryTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final url = 'http://1.95.137.212:59015/iot/deviceTask/recoveryTask'; + final response = await dio.post( + url, + data: { + 'deviceId': deviceId, + 'taskId': taskId, + 'orgId': orgId, + 'siteId': siteId, + }, + ); + + if (response.statusCode == 200) { + final data = response.data as Map; + if (data['code'] == 200) { + return data['data'] as Map? ?? {}; + } else { + throw Exception(data['msg'] ?? '恢复任务失败'); + } + } else { + throw Exception('HTTP ${response.statusCode}'); + } + } catch (e) { + _logger.logWithLevel('❌ 恢复任务失败: $e'); + rethrow; + } + } +} diff --git a/lib/features/devices/data/models/device_task_model.dart b/lib/features/devices/data/models/device_task_model.dart new file mode 100644 index 00000000..3733aad9 --- /dev/null +++ b/lib/features/devices/data/models/device_task_model.dart @@ -0,0 +1,40 @@ +import '../../domain/entities/device_task_entity.dart'; + +class DeviceTaskModel extends DeviceTaskEntity { + const DeviceTaskModel({ + required super.id, + required super.deviceId, + required super.taskStatus, + required super.taskStatusTranslate, + super.routeId, + super.siteId, + super.orgId, + super.createTime, + }); + + factory DeviceTaskModel.fromJson(Map json) { + return DeviceTaskModel( + id: json['id'] as int, + deviceId: json['deviceId'] as String? ?? '', + taskStatus: json['taskStaus'] as String? ?? '', + taskStatusTranslate: json['taskStausTranslate'] as String? ?? '', + routeId: json['routeId'] as int?, + siteId: json['siteId'] as int?, + orgId: json['orgId'] as int?, + createTime: json['createTime'] as String?, + ); + } + + DeviceTaskEntity toEntity() { + return DeviceTaskEntity( + id: id, + deviceId: deviceId, + taskStatus: taskStatus, + taskStatusTranslate: taskStatusTranslate, + routeId: routeId, + siteId: siteId, + orgId: orgId, + createTime: createTime, + ); + } +} diff --git a/lib/features/devices/data/repositories/device_task_repository_impl.dart b/lib/features/devices/data/repositories/device_task_repository_impl.dart new file mode 100644 index 00000000..e8ae8b62 --- /dev/null +++ b/lib/features/devices/data/repositories/device_task_repository_impl.dart @@ -0,0 +1,93 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/error/failure.dart'; +import '../../domain/entities/device_task_entity.dart'; +import '../../domain/repositories/device_task_repository.dart'; +import '../datasources/device_task_datasource.dart'; + +class DeviceTaskRepositoryImpl implements DeviceTaskRepository { + final DeviceTaskDatasource datasource; + + DeviceTaskRepositoryImpl({required this.datasource}); + + @override + Future>> getDeviceTaskPool({ + required String userId, + required int siteId, + required int orgId, + int pageNum = 1, + int pageSize = 99999999, + }) async { + try { + final models = await datasource.getDeviceTaskPool( + userId: userId, + siteId: siteId, + orgId: orgId, + pageNum: pageNum, + pageSize: pageSize, + ); + return Right(models.map((model) => model.toEntity()).toList()); + } catch (e) { + return Left(Failure(e.toString())); + } + } + + @override + Future> cancelTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final result = await datasource.cancelTask( + deviceId: deviceId, + taskId: taskId, + orgId: orgId, + siteId: siteId, + ); + return Right(result); + } catch (e) { + return Left(Failure(e.toString())); + } + } + + @override + Future> pauseTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final result = await datasource.pauseTask( + deviceId: deviceId, + taskId: taskId, + orgId: orgId, + siteId: siteId, + ); + return Right(result); + } catch (e) { + return Left(Failure(e.toString())); + } + } + + @override + Future>> recoveryTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }) async { + try { + final result = await datasource.recoveryTask( + deviceId: deviceId, + taskId: taskId, + orgId: orgId, + siteId: siteId, + ); + return Right(result); + } catch (e) { + return Left(Failure(e.toString())); + } + } +} diff --git a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart index 82b41e71..b09a4b13 100644 --- a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart +++ b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart @@ -198,24 +198,24 @@ class PathRepositoryImpl implements PathRepository { url, headers: {'Accept': 'application/xml, text/xml, */*'}, ); - print('[XML接口] 响应状态码: ${response.statusCode}'); - print('[XML接口] 响应内容长度: ${response.body.length}'); - print('[XML接口] Content-Type: ${response.headers['content-type']}'); + /// print('[XML接口] 响应状态码: ${response.statusCode}'); + ////print('[XML接口] 响应内容长度: ${response.body.length}'); + ///print('[XML接口] Content-Type: ${response.headers['content-type']}'); if (response.statusCode == 200) { // 打印前200字符确认格式 final preview = response.body.length > 200 ? response.body.substring(0, 200) : response.body; - print('[XML接口] 响应开头: $preview'); + /// print('[XML接口] 响应开头: $preview'); // 判断是JSON还是XML格式 final trimmed = response.body.trim(); if (trimmed.startsWith('{') || trimmed.startsWith('[')) { - print('[XML接口] 检测到JSON格式,使用JSON解析'); + ///print('[XML接口] 检测到JSON格式,使用JSON解析'); return _parseJsonResponse(response.body); } else { - print('[XML接口] 检测到XML格式,使用XML解析'); + ///print('[XML接口] 检测到XML格式,使用XML解析'); return _parseXmlResponse(response.body); } } else { @@ -224,7 +224,7 @@ class PathRepositoryImpl implements PathRepository { ); } } catch (e) { - print('[XML接口] 错误: $e'); + /// print('[XML接口] 错误: $e'); throw Exception('Network error in getWorkRecordsBySiteId: $e'); } } @@ -252,7 +252,7 @@ class PathRepositoryImpl implements PathRepository { records.add(WorkRecordEntity.fromJson(recordsData)); } - print('[XML接口] 最终解析记录数: ${records.length}'); + /// print('[XML接口] 最终解析记录数: ${records.length}'); return records; } @@ -276,7 +276,7 @@ class PathRepositoryImpl implements PathRepository { // 使用非贪婪匹配,确保每个data节点独立提取 final dataRegex = RegExp(r'([\s\S]*?)'); final dataMatches = dataRegex.allMatches(body); - print('[XML接口] 找到data节点数量: ${dataMatches.length}'); + ///print('[XML接口] 找到data节点数量: ${dataMatches.length}'); // 解析所有工作记录 final List records = []; @@ -292,9 +292,9 @@ class PathRepositoryImpl implements PathRepository { final recordElement = document.rootElement; final recordData = _parseXmlRecord(recordElement); - print( - '[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}', - ); + /// print( + /// '[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}', + /// ); records.add(WorkRecordEntity.fromXml(recordData)); } catch (e) { @@ -302,7 +302,7 @@ class PathRepositoryImpl implements PathRepository { } } - print('[XML接口] 最终解析记录数: ${records.length}'); + ///print('[XML接口] 最终解析记录数: ${records.length}'); return records; } @@ -310,15 +310,15 @@ class PathRepositoryImpl implements PathRepository { Map _parseXmlRecord(XmlElement recordElement) { final Map result = {}; - print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}'); + //// print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}'); for (final child in recordElement.childElements) { final tagName = child.name.local; final innerText = child.innerText.trim(); - print( - '[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}', - ); + // print( + // '[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}', + // ); // 特殊处理jsonData节点(包含嵌套结构) if (tagName == 'jsonData') { @@ -329,7 +329,7 @@ class PathRepositoryImpl implements PathRepository { } } - print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}'); + //print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}'); return result; } @@ -392,4 +392,42 @@ class PathRepositoryImpl implements PathRepository { return coordinates; } + + /// 创建设备任务(通过接口执行作业) + /// 接口地址: http://1.95.137.212:59015/iot/deviceTask/createDeviceTask + /// 入参: {"deviceId":"...","routeId":76,"siteId":22} + @override + Future createDeviceTask({ + required String deviceId, + required int routeId, + required int siteId, + }) async { + final url = Uri.parse('http://1.95.137.212:59015/iot/deviceTask/createDeviceTask'); + + final body = jsonEncode({ + 'deviceId': deviceId, + 'routeId': routeId, + 'siteId': siteId, + }); + + print('📤 [创建设备任务] 请求参数: $body'); + + try { + final response = await http.post( + url, + headers: {'Content-Type': 'application/json'}, + body: body, + ); + + print('📥 [创建设备任务] 响应状态码: ${response.statusCode}'); + print('📥 [创建设备任务] 响应内容: ${response.body}'); + + if (response.statusCode != 200) { + throw Exception('创建设备任务失败: HTTP ${response.statusCode}'); + } + } catch (e) { + print('❌ [创建设备任务] 错误: $e'); + throw Exception('创建设备任务失败: $e'); + } + } } diff --git a/lib/features/devices/domain/entities/device_task_entity.dart b/lib/features/devices/domain/entities/device_task_entity.dart new file mode 100644 index 00000000..b18e6f8e --- /dev/null +++ b/lib/features/devices/domain/entities/device_task_entity.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; + +class DeviceTaskEntity extends Equatable { + final int id; + final String deviceId; + final String taskStatus; + final String taskStatusTranslate; + final int? routeId; + final int? siteId; + final int? orgId; + final String? createTime; + + const DeviceTaskEntity({ + required this.id, + required this.deviceId, + required this.taskStatus, + required this.taskStatusTranslate, + this.routeId, + this.siteId, + this.orgId, + this.createTime, + }); + + factory DeviceTaskEntity.fromJson(Map json) { + return DeviceTaskEntity( + id: json['id'] as int, + deviceId: json['deviceId'] as String? ?? '', + taskStatus: json['taskStaus'] as String? ?? '', + taskStatusTranslate: json['taskStausTranslate'] as String? ?? '', + routeId: json['routeId'] as int?, + siteId: json['siteId'] as int?, + orgId: json['orgId'] as int?, + createTime: json['createTime'] as String?, + ); + } + + @override + List get props => [ + id, + deviceId, + taskStatus, + taskStatusTranslate, + routeId, + siteId, + orgId, + createTime, + ]; +} diff --git a/lib/features/devices/domain/repositories/device_task_repository.dart b/lib/features/devices/domain/repositories/device_task_repository.dart new file mode 100644 index 00000000..8056895f --- /dev/null +++ b/lib/features/devices/domain/repositories/device_task_repository.dart @@ -0,0 +1,38 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/error/failure.dart'; +import '../entities/device_task_entity.dart'; + +abstract class DeviceTaskRepository { + /// 获取设备任务池列表 + Future>> getDeviceTaskPool({ + required String userId, + required int siteId, + required int orgId, + int pageNum = 1, + int pageSize = 99999999, + }); + + /// 取消任务 + Future> cancelTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); + + /// 暂停任务 + Future> pauseTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); + + /// 恢复任务 + Future>> recoveryTask({ + required String deviceId, + required int taskId, + required int orgId, + required int siteId, + }); +} diff --git a/lib/features/devices/domain/repositories/path_repository.dart b/lib/features/devices/domain/repositories/path_repository.dart index 69fee637..23bfc2e9 100644 --- a/lib/features/devices/domain/repositories/path_repository.dart +++ b/lib/features/devices/domain/repositories/path_repository.dart @@ -26,4 +26,14 @@ abstract class PathRepository { /// 根据场站ID查询工作记录列表(XML格式) Future> getWorkRecordsBySiteId({required int siteId}); + + /// 创建设备任务(通过接口执行作业) + /// deviceId: 设备ID(targetDevice) + /// routeId: 路线ID(选中的路线任务ID) + /// siteId: 场站ID + Future createDeviceTask({ + required String deviceId, + required int routeId, + required int siteId, + }); } diff --git a/lib/features/devices/domain/usecases/cancel_task_usecase.dart b/lib/features/devices/domain/usecases/cancel_task_usecase.dart new file mode 100644 index 00000000..91b87a22 --- /dev/null +++ b/lib/features/devices/domain/usecases/cancel_task_usecase.dart @@ -0,0 +1,34 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/domain/usecases/base_usecase.dart'; +import '../../../../core/error/failure.dart'; +import '../repositories/device_task_repository.dart'; + +class CancelTaskUseCase implements BaseUseCase { + final DeviceTaskRepository repository; + + CancelTaskUseCase(this.repository); + + @override + Future> call(CancelTaskParams params) async { + return await repository.cancelTask( + deviceId: params.deviceId, + taskId: params.taskId, + orgId: params.orgId, + siteId: params.siteId, + ); + } +} + +class CancelTaskParams { + final String deviceId; + final int taskId; + final int orgId; + final int siteId; + + CancelTaskParams({ + required this.deviceId, + required this.taskId, + required this.orgId, + required this.siteId, + }); +} diff --git a/lib/features/devices/domain/usecases/create_device_task_usecase.dart b/lib/features/devices/domain/usecases/create_device_task_usecase.dart new file mode 100644 index 00000000..5358ea24 --- /dev/null +++ b/lib/features/devices/domain/usecases/create_device_task_usecase.dart @@ -0,0 +1,26 @@ +import 'package:fpdart/fpdart.dart'; +import '../../domain/errors/device_failure.dart'; +import '../../domain/repositories/path_repository.dart'; + +class CreateDeviceTaskUseCase { + final PathRepository repository; + + CreateDeviceTaskUseCase(this.repository); + + Future> execute({ + required String deviceId, + required int routeId, + required int siteId, + }) async { + try { + await repository.createDeviceTask( + deviceId: deviceId, + routeId: routeId, + siteId: siteId, + ); + return const Right(null); + } catch (e) { + return Left(DeviceFailure.networkError(message: e.toString())); + } + } +} diff --git a/lib/features/devices/domain/usecases/get_device_task_pool_usecase.dart b/lib/features/devices/domain/usecases/get_device_task_pool_usecase.dart new file mode 100644 index 00000000..76de409d --- /dev/null +++ b/lib/features/devices/domain/usecases/get_device_task_pool_usecase.dart @@ -0,0 +1,41 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/domain/usecases/base_usecase.dart'; +import '../../../../core/error/failure.dart'; +import '../entities/device_task_entity.dart'; +import '../repositories/device_task_repository.dart'; + +class GetDeviceTaskPoolUseCase + implements BaseUseCase, GetDeviceTaskPoolParams> { + final DeviceTaskRepository repository; + + GetDeviceTaskPoolUseCase(this.repository); + + @override + Future>> call( + GetDeviceTaskPoolParams params, + ) async { + return await repository.getDeviceTaskPool( + userId: params.userId, + siteId: params.siteId, + orgId: params.orgId, + pageNum: params.pageNum, + pageSize: params.pageSize, + ); + } +} + +class GetDeviceTaskPoolParams { + final String userId; + final int siteId; + final int orgId; + final int pageNum; + final int pageSize; + + GetDeviceTaskPoolParams({ + required this.userId, + required this.siteId, + required this.orgId, + this.pageNum = 1, + this.pageSize = 99999999, + }); +} diff --git a/lib/features/devices/domain/usecases/pause_task_usecase.dart b/lib/features/devices/domain/usecases/pause_task_usecase.dart new file mode 100644 index 00000000..8352a64f --- /dev/null +++ b/lib/features/devices/domain/usecases/pause_task_usecase.dart @@ -0,0 +1,34 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/domain/usecases/base_usecase.dart'; +import '../../../../core/error/failure.dart'; +import '../repositories/device_task_repository.dart'; + +class PauseTaskUseCase implements BaseUseCase { + final DeviceTaskRepository repository; + + PauseTaskUseCase(this.repository); + + @override + Future> call(PauseTaskParams params) async { + return await repository.pauseTask( + deviceId: params.deviceId, + taskId: params.taskId, + orgId: params.orgId, + siteId: params.siteId, + ); + } +} + +class PauseTaskParams { + final String deviceId; + final int taskId; + final int orgId; + final int siteId; + + PauseTaskParams({ + required this.deviceId, + required this.taskId, + required this.orgId, + required this.siteId, + }); +} diff --git a/lib/features/devices/domain/usecases/recovery_task_usecase.dart b/lib/features/devices/domain/usecases/recovery_task_usecase.dart new file mode 100644 index 00000000..32be6db3 --- /dev/null +++ b/lib/features/devices/domain/usecases/recovery_task_usecase.dart @@ -0,0 +1,37 @@ +import 'package:fpdart/fpdart.dart'; +import '../../../../core/domain/usecases/base_usecase.dart'; +import '../../../../core/error/failure.dart'; +import '../repositories/device_task_repository.dart'; + +class RecoveryTaskUseCase + implements BaseUseCase, RecoveryTaskParams> { + final DeviceTaskRepository repository; + + RecoveryTaskUseCase(this.repository); + + @override + Future>> call( + RecoveryTaskParams params, + ) async { + return await repository.recoveryTask( + deviceId: params.deviceId, + taskId: params.taskId, + orgId: params.orgId, + siteId: params.siteId, + ); + } +} + +class RecoveryTaskParams { + final String deviceId; + final int taskId; + final int orgId; + final int siteId; + + RecoveryTaskParams({ + required this.deviceId, + required this.taskId, + required this.orgId, + required this.siteId, + }); +} diff --git a/lib/features/devices/presentation/bloc/device_task_cubit.dart b/lib/features/devices/presentation/bloc/device_task_cubit.dart new file mode 100644 index 00000000..8d9a39dc --- /dev/null +++ b/lib/features/devices/presentation/bloc/device_task_cubit.dart @@ -0,0 +1,315 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import '../../../../core/app/app_user_cubit.dart'; +import '../../../../core/logging/i_logger_service.dart'; +import '../../../../core/network/error_handler.dart'; +import '../../../../features/v2/site/presentation/cubit/site_cubit.dart'; +import '../../domain/usecases/cancel_task_usecase.dart'; +import '../../domain/usecases/get_device_task_pool_usecase.dart'; +import '../../domain/usecases/pause_task_usecase.dart'; +import '../../domain/usecases/recovery_task_usecase.dart'; +import 'device_task_state.dart'; + +class DeviceTaskCubit extends Cubit { + final GetDeviceTaskPoolUseCase _getDeviceTaskPoolUseCase; + final CancelTaskUseCase _cancelTaskUseCase; + final PauseTaskUseCase _pauseTaskUseCase; + final RecoveryTaskUseCase _recoveryTaskUseCase; + final ILoggerService _logger = GetIt.I(); + + DeviceTaskCubit( + this._getDeviceTaskPoolUseCase, + this._cancelTaskUseCase, + this._pauseTaskUseCase, + this._recoveryTaskUseCase, + ) : super(const DeviceTaskState()); + + /// 获取任务池并过滤出当前设备的任务 + Future fetchAndFilterTask(String deviceId) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + + try { + // 获取用户信息 + final userCubit = GetIt.I(); + final user = userCubit.state.user; + if (user == null) { + emit(state.copyWith( + isLoading: false, + errorMessage: '用户未登录', + )); + return; + } + + // 获取场站ID + final siteCubit = GetIt.I(); + final siteId = siteCubit.state.selectedSite?.id; + if (siteId == null) { + emit(state.copyWith( + isLoading: false, + errorMessage: '未选择场站', + )); + return; + } + + // 调用接口获取任务池 + final result = await _getDeviceTaskPoolUseCase.call( + GetDeviceTaskPoolParams( + userId: user.userId ?? '', + siteId: siteId, + orgId: user.orgId ?? 0, + ), + ); + + result.fold( + (failure) { + _logger.logWithLevel('❌ 获取任务池失败: ${failure.message}'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + }, + (taskList) { + // 过滤出当前设备的任务 + final deviceTasks = taskList + .where((task) => task.deviceId == deviceId) + .toList(); + + // 取第一个任务(或根据业务逻辑选择) + final currentTask = deviceTasks.isNotEmpty ? deviceTasks.first : null; + + _logger.logWithLevel( + '✅ 找到 ${deviceTasks.length} 个任务,当前任务ID: ${currentTask?.id}', + ); + + emit(state.copyWith( + isLoading: false, + taskPool: taskList, + currentTask: currentTask, + currentTaskId: currentTask?.id, + )); + }, + ); + } catch (e) { + _logger.logWithLevel('❌ 获取任务池异常: $e'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + } + } + + /// 取消任务 + Future cancelTask(String deviceId) async { + final taskId = state.currentTaskId; + if (taskId == null) { + emit(state.copyWith(errorMessage: '无可用任务')); + return; + } + + emit(state.copyWith( + isLoading: true, + operationType: DeviceTaskOperationType.cancel, + )); + + try { + final userCubit = GetIt.I(); + final user = userCubit.state.user; + final siteCubit = GetIt.I(); + final siteId = siteCubit.state.selectedSite?.id; + + if (user == null || siteId == null) { + emit(state.copyWith( + isLoading: false, + errorMessage: '参数不完整', + operationType: DeviceTaskOperationType.none, + )); + return; + } + + final result = await _cancelTaskUseCase.call( + CancelTaskParams( + deviceId: deviceId, + taskId: taskId, + orgId: user.orgId ?? 0, + siteId: siteId, + ), + ); + + result.fold( + (failure) { + _logger.logWithLevel('❌ 取消任务失败: ${failure.message}'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + }, + (success) { + _logger.logWithLevel('✅ 取消任务成功'); + emit(state.copyWith( + isLoading: false, + operationType: DeviceTaskOperationType.none, + )); + }, + ); + } catch (e) { + _logger.logWithLevel('❌ 取消任务异常: $e'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + } + } + + /// 暂停任务 + Future pauseTask(String deviceId) async { + final taskId = state.currentTaskId; + if (taskId == null) { + emit(state.copyWith(errorMessage: '无可用任务')); + return; + } + + emit(state.copyWith( + isLoading: true, + operationType: DeviceTaskOperationType.pause, + )); + + try { + final userCubit = GetIt.I(); + final user = userCubit.state.user; + final siteCubit = GetIt.I(); + final siteId = siteCubit.state.selectedSite?.id; + + if (user == null || siteId == null) { + emit(state.copyWith( + isLoading: false, + errorMessage: '参数不完整', + operationType: DeviceTaskOperationType.none, + )); + return; + } + + final result = await _pauseTaskUseCase.call( + PauseTaskParams( + deviceId: deviceId, + taskId: taskId, + orgId: user.orgId ?? 0, + siteId: siteId, + ), + ); + + result.fold( + (failure) { + _logger.logWithLevel('❌ 暂停任务失败: ${failure.message}'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + }, + (success) { + _logger.logWithLevel('✅ 暂停任务成功'); + emit(state.copyWith( + isLoading: false, + operationType: DeviceTaskOperationType.none, + )); + }, + ); + } catch (e) { + _logger.logWithLevel('❌ 暂停任务异常: $e'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + } + } + + /// 恢复任务 + Future recoveryTask(String deviceId) async { + final taskId = state.currentTaskId; + if (taskId == null) { + emit(state.copyWith(errorMessage: '无可用任务')); + return; + } + + emit(state.copyWith( + isLoading: true, + operationType: DeviceTaskOperationType.recovery, + )); + + try { + final userCubit = GetIt.I(); + final user = userCubit.state.user; + final siteCubit = GetIt.I(); + final siteId = siteCubit.state.selectedSite?.id; + + if (user == null || siteId == null) { + emit(state.copyWith( + isLoading: false, + errorMessage: '参数不完整', + operationType: DeviceTaskOperationType.none, + )); + return; + } + + final result = await _recoveryTaskUseCase.call( + RecoveryTaskParams( + deviceId: deviceId, + taskId: taskId, + orgId: user.orgId ?? 0, + siteId: siteId, + ), + ); + + result.fold( + (failure) { + _logger.logWithLevel('❌ 恢复任务失败: ${failure.message}'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(failure.message), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + }, + (data) { + _logger.logWithLevel('✅ 恢复任务成功: $data'); + emit(state.copyWith( + isLoading: false, + operationType: DeviceTaskOperationType.none, + )); + }, + ); + } catch (e) { + _logger.logWithLevel('❌ 恢复任务异常: $e'); + emit(state.copyWith( + isLoading: false, + errorMessage: ErrorHandler.getErrorMessage(e), + operationType: DeviceTaskOperationType.none, + shouldShowError: true, // 🔥 标记需要显示错误弹窗 + )); + } + } + + /// 更新当前任务ID(当选择新航线时调用) + void updateCurrentTaskId(int taskId) { + emit(state.copyWith(currentTaskId: taskId)); + _logger.logWithLevel('🔄 更新当前任务ID: $taskId'); + } + + /// 清除当前任务 + void clearCurrentTask() { + emit(state.copyWith( + currentTask: null, + currentTaskId: null, + )); + _logger.logWithLevel('🧹 清除当前任务'); + } +} diff --git a/lib/features/devices/presentation/bloc/device_task_state.dart b/lib/features/devices/presentation/bloc/device_task_state.dart new file mode 100644 index 00000000..6ac01a9b --- /dev/null +++ b/lib/features/devices/presentation/bloc/device_task_state.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import '../../domain/entities/device_task_entity.dart'; + +enum DeviceTaskOperationType { + none, + cancel, + pause, + recovery, +} + +class DeviceTaskState extends Equatable { + final List taskPool; + final DeviceTaskEntity? currentTask; + final int? currentTaskId; + final bool isLoading; + final String? errorMessage; + final DeviceTaskOperationType operationType; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 + + const DeviceTaskState({ + this.taskPool = const [], + this.currentTask, + this.currentTaskId, + this.isLoading = false, + this.errorMessage, + this.operationType = DeviceTaskOperationType.none, + this.shouldShowError = false, + }); + + DeviceTaskState copyWith({ + List? taskPool, + DeviceTaskEntity? currentTask, + int? currentTaskId, + bool? isLoading, + String? errorMessage, + DeviceTaskOperationType? operationType, + bool? shouldShowError, + }) { + return DeviceTaskState( + taskPool: taskPool ?? this.taskPool, + currentTask: currentTask ?? this.currentTask, + currentTaskId: currentTaskId ?? this.currentTaskId, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage, + operationType: operationType ?? this.operationType, + shouldShowError: shouldShowError ?? false, // 🔥 默认重置为 false + ); + } + + @override + List get props => [ + taskPool, + currentTask, + currentTaskId, + isLoading, + errorMessage, + operationType, + shouldShowError, + ]; +} diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index 9a21ca06..e8252c97 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -623,4 +623,53 @@ class DevicesCubit extends Cubit { //print('🔄 [DevicesCubit] 已重置到达位置'); _logger.logWithLevel('🔄 [DevicesCubit] 已重置到达位置'); } + + /// 🔥 获取当前选中的设备 + DeviceEntity? getSelectedDevice() { + return state.selectedDevice; + } + + /// 🔥 异步获取当前选中的设备(兼容 RemoteControlCubit 接口) + Future getDevice() async { + return state.selectedDevice; + } + + /// 🔥 判断是否有选中设备 + bool hasSelectedDevice() { + return state.selectedDevice != null; + } + + /// 🔥 获取选中设备名称(安全获取,返回空字符串而非null) + String getSelectedDeviceName() { + return state.selectedDevice?.deviceName ?? ''; + } + + /// 🔥 获取选中设备ID(安全获取,返回空字符串而非null) + String getSelectedDeviceId() { + return state.selectedDevice?.deviceName ?? ''; + } + + /// 🔥 清除选中设备 + void clearSelectedDevice() { + debugPrint('🧹 [DevicesCubit] 清除选中设备'); + _logger.logWithLevel('🧹 [DevicesCubit] 清除选中设备'); + emit(state.copyWith(selectedDevice: null)); + } + + /// 🔥 检查设备是否在列表中 + bool isDeviceInList(String deviceName) { + return state.devices.any((device) => device.deviceName == deviceName); + } + + /// 🔥 根据设备名称查找设备 + DeviceEntity? findDeviceByName(String deviceName) { + try { + return state.devices.firstWhere( + (device) => device.deviceName == deviceName, + orElse: () => throw Exception('Device not found'), + ); + } catch (e) { + return null; + } + } } diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index b0ec4a16..5ac8e702 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -61,24 +61,26 @@ class _HomePageState extends State { // 🔥 自动选中第一个设备(保证是最新的) devicesCubit.selectDevice(firstDevice); + // 🔥 关键修复:首页不再自动连接TCP,改为选择设备时才连接 // 连接 TCP - final tcpClient = GetIt.instance(); - if (!tcpClient.isConnected) { - tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: firstDevice.deviceName).then((_) { - debugPrint('✅ [HomePage] TCP 连接成功'); - tcpClient.startHeartbeat(interval: const Duration(seconds: 4)); - - // 🔥 关键:TCP 连接成功后,主动请求权限以激活服务器的推送机制 - final remoteRepo = GetIt.instance(); - remoteRepo.requestControlPermission(firstDevice.deviceName, "app"); - debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送'); - }); - } else { - // 🔥 TCP 已连接,也要发送权限请求 - final remoteRepo = GetIt.instance(); - remoteRepo.requestControlPermission(firstDevice.deviceName, "app"); - debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求'); - } + // final tcpClient = GetIt.instance(); + // if (!tcpClient.isConnected) { + // tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: firstDevice.deviceName).then((_) { + // debugPrint('✅ [HomePage] TCP 连接成功'); + // tcpClient.startHeartbeat(interval: const Duration(seconds: 4)); + // + // // 🔥 关键:TCP 连接成功后,主动请求权限以激活服务器的推送机制 + // final remoteRepo = GetIt.instance(); + // remoteRepo.requestControlPermission(firstDevice.deviceName, "app"); + // debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送'); + // }); + // } else { + // // 🔥 TCP 已连接,也要发送权限请求 + // final remoteRepo = GetIt.instance(); + // remoteRepo.requestControlPermission(firstDevice.deviceName, "app"); + // debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求'); + // } + debugPrint('ℹ️ [HomePage] TCP 连接已移至选择设备时执行'); } } catch (e) { setState(() => _isDevicesLoading = false); diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index c7f29ce7..4c7a6e47 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -16,6 +16,7 @@ import 'package:latlong2/latlong.dart'; import 'package:maibu_satabot_v2/components/confrim_dialog.dart'; import 'package:maibu_satabot_v2/components/toast.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; +import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart'; import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; @@ -28,6 +29,7 @@ import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_p as work_area_model; import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_work_record_usecase.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/usecases/create_device_task_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_event.dart'; @@ -782,15 +784,10 @@ class _MapPageEnterpriseState extends State { Future _savePlotData(String plotName, String? imgBase64) async { final loc = AppLocalizations.of(context); - // 1. 获取用户ID - final userId = context.read().state.user?.userId ?? ""; - if (userId.isEmpty) { - _showPageToast( - message: loc.translate('route_planning.user_id_empty'), - type: ToastType.error, - ); - - //ToastUtils.showError(context, '用户ID为空,无法保存!'); + // 🔥 V2 适配:使用 SiteCubit 获取站点ID(从 v2 首页场站选择获取,有默认值) + final siteId = sl().state.selectedSite?.id; + if (siteId == null) { + _showPageToast(message: '请先在场站列表中选择一个场站!', type: ToastType.error); return; } for (var i = 0; i < typedPathList.length; i++) { @@ -834,7 +831,7 @@ class _MapPageEnterpriseState extends State { // 3. 构造 workRecord (包裹一层) final Map workRecord = { 'workName': plotName, - 'userId': userId, + 'siteId': siteId, // 修改:使用 siteId 代替 userId 'jsonData': jsonEncode(savePath), // 将 savePath 转为 JSON 字符串 }; final String workRecordJson = jsonEncode(workRecord); @@ -1372,6 +1369,11 @@ class _MapPageEnterpriseState extends State { return GestureDetector( // 核心:点击列表项触发选中逻辑 onTap: () async { + // 🔥 添加日志:打印选中的路线任务详情 + debugPrint('📋 [路径规划] 选中路线任务:'); + debugPrint(' ├─ 地块名称: ${plot.plotName}'); + debugPrint(' └─ 数据ID: ${plot.id}'); + // 【优化1】第一步就UI响应,不卡手 setState(() { _isListBoxOpen = false; @@ -1969,13 +1971,96 @@ class _MapPageEnterpriseState extends State { /// 开始作业 void _startWork() async { + // 🔥 V2 适配:使用接口方式执行作业,不再使用 TCP + + // 1. 校验选中的路线 + if (_selectedPlot == null) { + _showPageToast(message: "请先选择一个路线任务", type: ToastType.info); + return; + } + + // 2. 获取设备ID(从 RemoteControlCubit 的 targetDevice) + final targetDevice = context.read().state.targetDevice; + final deviceId = targetDevice?.deviceName; + if (deviceId == null || deviceId.isEmpty) { + _showPageToast(message: "请先选择一个设备", type: ToastType.info); + return; + } + + // 3. 获取路线ID(从选中的路线任务) + final routeId = int.tryParse(_selectedPlot!.id); + if (routeId == null) { + _showPageToast(message: "路线ID无效", type: ToastType.error); + return; + } + + // 4. 获取场站ID(从 SiteCubit 的 selectedSite) + final siteId = sl().state.selectedSite?.id; + if (siteId == null) { + _showPageToast(message: "请先选择一个场站", type: ToastType.info); + return; + } + + // 5. 打印请求参数日志 + debugPrint('🚀 [开始作业] 请求参数:'); + debugPrint(' ├─ deviceId: $deviceId'); + debugPrint(' ├─ routeId: $routeId'); + debugPrint(' └─ siteId: $siteId'); + + // 6. 更新UI状态 + setState(() { + isStopWork = false; + isStartWork = true; + _workStatus = WorkStatus.working; + _traceManager.reset(); + tracePoint?.clear(); + gctracePoint?.clear(); + }); + + // 7. 更新应用状态 + context.read().updateAppState(AppState.routePlanning); + + try { + // 8. 调用接口创建设备任务 + final result = await sl().execute( + deviceId: deviceId, + routeId: routeId, + siteId: siteId, + ); + + result.fold( + (failure) { + // 失败 + debugPrint('❌ [开始作业] 创建设备任务失败: $failure'); + _showPageToast(message: "作业启动失败", type: ToastType.error); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + }, + (_) { + // 成功 + debugPrint('✅ [开始作业] 创建设备任务成功'); + _showPageToast(message: "作业已开始", type: ToastType.success); + _saveDataToLocal(); + }, + ); + } catch (e) { + debugPrint('❌ [开始作业] 异常: $e'); + _showPageToast(message: "作业启动异常: $e", type: ToastType.error); + setState(() { + isStartWork = false; + _workStatus = WorkStatus.idle; + }); + } + + // ============ 以下是原有的 TCP 方式代码,已注释 ============ + /* if (startWorkList.isEmpty) { _showPageToast(message: "作业列表为空,请重新选择路径", type: ToastType.info); - //ToastUtils.showInfo(context, '作业列表为空,请重新选择路径'); return; } _traceManager.setMode(TPMode.LOCATION); - //_traceManager.reset(); tracePoint = _traceManager.getTracePoint(); gctracePoint = batchWgs84ToGcj02(tracePoint!); _logger.log("[当前轨迹模式][转换后gctracePoint]开始作业: $tracePoint"); @@ -1984,18 +2069,14 @@ class _MapPageEnterpriseState extends State { setState(() { isStopWork = false; - - isStartWork = true; // 🔥 关键:停止作业标志 + isStartWork = true; _workStatus = WorkStatus.working; _traceManager.reset(); tracePoint?.clear(); gctracePoint?.clear(); - //_traceManager.setMode(TPMode.NAVIGATION); }); - // 🔥 核心修复:先更新 AppState 为 routePlanning context.read().updateAppState(AppState.routePlanning); - // 🔥 延迟一下,确保状态已更新 await Future.delayed(const Duration(milliseconds: 100)); debugPrint('⚙️ 开始类型转换...'); @@ -2007,19 +2088,12 @@ class _MapPageEnterpriseState extends State { ); }).toList(); - final Queue pathQueue = Queue.from( - typedList, - ); - - // 步骤 3:调用 Cubit 方法(类型匹配) + final Queue pathQueue = Queue.from(typedList); await context.read().startRoutePlanning(pathQueue); - ///context.read().updateAppState(AppState.routePlanning); - // 可选:显示作业提示 _showPageToast(message: "作业已开始", type: ToastType.success); - //ToastUtils.showSuccess(context, '作业已开始'); _saveDataToLocal(); - //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green)); + */ } /// 暂停作业 @@ -2303,7 +2377,8 @@ class _MapPageEnterpriseState extends State { headingStatus == 0 ? null : () { - _startWork(); + //@开始作业通过后端接口 + // _startWork(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00C853), @@ -2344,6 +2419,7 @@ class _MapPageEnterpriseState extends State { height: 50, child: ElevatedButton( onPressed: () => + //@暂停工作通过接口、恢复(继续)工作通过接口 _workStatus == WorkStatus.working ? _pauseWork() : _resumeWork(), @@ -2487,7 +2563,7 @@ class _MapPageEnterpriseState extends State { }).toList(); } - // ========== 抽象:生成路径的核心函数 ========== + // ========== 抽象:生成路径的核心函数(打点函数) ========== Future _generatePath({bool showTips = true}) async { if (_currentWorkMode == WorkMode.custom) { setState(() { @@ -2742,10 +2818,11 @@ class _MapPageEnterpriseState extends State { final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56 final maxTop = screenHeight - menuHeight; final userState = context.watch().state; + // 🔥 V2 适配:使用 RemoteControlCubit 的 targetDevice 保持一致性 final deviceId = context - .watch() + .watch() .state - .selectedDevice + .targetDevice ?.deviceName; if (deviceId != null && diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index 092fde72..0f6d433e 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -946,6 +946,9 @@ class RemoteControlCubit extends Cubit { debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}'); debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}'); + debugPrint('📦 [RemoteControl] 更新targetDevice状态'); + emit(state.copyWith(targetDevice: device)); + debugPrint('✅ [RemoteControl] targetDevice状态已更新'); // 🔥 关键修改:选择设备时才连接TCP if (!tcpClient.isConnected) { debugPrint('🔌 [RemoteControl] TCP未连接,开始建立连接...'); @@ -981,8 +984,6 @@ class RemoteControlCubit extends Cubit { ); }); - debugPrint('📦 [RemoteControl] 更新targetDevice状态'); - emit(state.copyWith(targetDevice: device)); debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 =========='); } @@ -992,4 +993,12 @@ class RemoteControlCubit extends Cubit { // _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备'); emit(state.copyWith(targetDevice: null)); } + + /// 🔥 获取设备 + + DeviceEntity? getSelectedDevice() { + return state.targetDevice; + } + + } diff --git a/lib/features/v2/device_list/domain/entities/drone_station_entity.dart b/lib/features/v2/device_list/domain/entities/drone_station_entity.dart index 53c24437..c561606d 100644 --- a/lib/features/v2/device_list/domain/entities/drone_station_entity.dart +++ b/lib/features/v2/device_list/domain/entities/drone_station_entity.dart @@ -76,12 +76,12 @@ class PositionState extends Equatable { /// UAV详情实体(用于详情页面API返回的数据) class UAVDetailEntity extends Equatable { - final String deviceSn; // 设备序列号(无人机序列号) - final String gatewaySn; // 网关序列号 - final String callsign; // 机场呼号/名称 - final String droneCallsign; // 无人机呼号 - final int onlineStatus; // 机场在线状态 (1:在线, 0:离线) - final int droneOnlineStatus; // 无人机在线状态 + final String deviceSn; + final String gatewaySn; + final String callsign; + final String droneCallsign; + final int onlineStatus; + final int droneOnlineStatus; final double? latitude; final double? longitude; final double? capacityPercent; @@ -173,7 +173,9 @@ class UAVDetailEntity extends Equatable { : null, droneCameraList: json['drone_camera_list'] != null ? (json['drone_camera_list'] as List) - .map((item) => CameraInfo.fromJson(item as Map)) + .map( + (item) => CameraInfo.fromJson(item as Map), + ) .toList() : null, orgId: json['orgId'], @@ -333,7 +335,9 @@ class DroneStationEntity extends Equatable { : null, droneCameraList: json['drone_camera_list'] != null ? (json['drone_camera_list'] as List) - .map((item) => CameraInfo.fromJson(item as Map)) + .map( + (item) => CameraInfo.fromJson(item as Map), + ) .toList() : null, orgId: json['orgId'] ?? 0, diff --git a/lib/features/v2/device_list/domain/entities/flight_task_detail_entity.dart b/lib/features/v2/device_list/domain/entities/flight_task_detail_entity.dart index ebb7fdc7..1a3d359c 100644 --- a/lib/features/v2/device_list/domain/entities/flight_task_detail_entity.dart +++ b/lib/features/v2/device_list/domain/entities/flight_task_detail_entity.dart @@ -5,7 +5,6 @@ class FlightTaskDetailEntity { final String taskType; final String status; final String sn; - final String droneSn; // 无人机序列号 final String waylineUuid; final String beginAt; final String endAt; @@ -27,7 +26,6 @@ class FlightTaskDetailEntity { required this.taskType, required this.status, required this.sn, - required this.droneSn, required this.waylineUuid, required this.beginAt, required this.endAt, @@ -47,26 +45,18 @@ class FlightTaskDetailEntity { factory FlightTaskDetailEntity.fromJson(Map json) { // 安全处理 folder_info 嵌套结构 final rawFolderInfo = json['folder_info']; - final Map folderInfo = (rawFolderInfo is Map) - ? Map.from(rawFolderInfo) + final Map folderInfo = (rawFolderInfo is Map) + ? Map.from(rawFolderInfo) : {}; - print( - '🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}', - ); + print('🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}'); print('🔍 [FlightTaskDetailEntity] folder_info 值: $rawFolderInfo'); - + return FlightTaskDetailEntity( name: json['name'] ?? '', uuid: json['uuid'] ?? '', taskType: json['task_type'] ?? '', status: json['status'] ?? '', sn: json['sn'] ?? '', - droneSn: - json['drone_sn'] ?? - json['device_sn'] ?? - json['droneSn'] ?? - json['deviceSn'] ?? - '', waylineUuid: json['wayline_uuid'] ?? '', beginAt: json['begin_at'] ?? '', endAt: json['end_at'] ?? '', @@ -99,7 +89,6 @@ class FlightTaskDetailEntity { 'task_type': taskType, 'status': status, 'sn': sn, - 'drone_sn': droneSn, 'wayline_uuid': waylineUuid, 'begin_at': beginAt, 'end_at': endAt, 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 index d4c95c54..b562ce7c 100644 --- a/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/device_status_bloc.dart @@ -1,5 +1,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/usecases/get_device_status_data_usecase.dart'; +import '../../../../../core/network/error_handler.dart'; import 'device_status_event.dart'; import 'device_status_state.dart'; @@ -31,7 +32,10 @@ class DeviceStatusBloc extends Bloc { siteId: event.siteId, )); } catch (e) { - emit(DeviceStatusError(e.toString())); + emit(DeviceStatusError( + message: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); } } @@ -55,7 +59,10 @@ class DeviceStatusBloc extends Bloc { devices: response.devices, )); } catch (e) { - emit(DeviceStatusError(e.toString())); + emit(DeviceStatusError( + message: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); } } } 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 index cdec2c61..a247ee8a 100644 --- a/lib/features/v2/device_list/presentation/bloc/device_status_state.dart +++ b/lib/features/v2/device_list/presentation/bloc/device_status_state.dart @@ -54,11 +54,15 @@ class DeviceStatusLoaded extends DeviceStatusState { class DeviceStatusError extends DeviceStatusState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const DeviceStatusError(this.message); + const DeviceStatusError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } class DeviceStatusEmpty extends DeviceStatusState { diff --git a/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart b/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart index 9957bc51..ad376db3 100644 --- a/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/drone_station_bloc.dart @@ -2,6 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/usecases/get_drone_station_list_usecase.dart'; import '../../domain/usecases/get_video_stream_usecase.dart'; import '../../domain/usecases/get_uav_video_stream_usecase.dart'; +import '../../../../../core/network/error_handler.dart'; import 'drone_station_event.dart'; import 'drone_station_state.dart'; @@ -33,7 +34,10 @@ class DroneStationBloc extends Bloc { final result = await getDroneStationListUseCase(event.siteId); result.fold( - (failure) => emit(DroneStationError(failure.message)), + (failure) => emit(DroneStationError( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (stations) => emit(DroneStationLoaded(stations)), ); } @@ -46,7 +50,10 @@ class DroneStationBloc extends Bloc { final result = await getDroneStationListUseCase(event.siteId); result.fold( - (failure) => emit(DroneStationError(failure.message)), + (failure) => emit(DroneStationError( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (stations) => emit(DroneStationLoaded(stations)), ); } @@ -61,7 +68,10 @@ class DroneStationBloc extends Bloc { final result = await getUAVDetailUseCase(event.gatewaySn, event.deviceSn); result.fold( - (failure) => emit(UAVDetailError(failure.message)), + (failure) => emit(UAVDetailError( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (detail) => emit(UAVDetailLoaded(detail)), ); } @@ -79,7 +89,10 @@ class DroneStationBloc extends Bloc { ); result.fold( - (failure) => emit(VideoStreamError(failure.message)), + (failure) => emit(VideoStreamError( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)), ); } @@ -100,7 +113,10 @@ class DroneStationBloc extends Bloc { ); result.fold( - (failure) => emit(UavVideoStreamError(failure.message)), + (failure) => emit(UavVideoStreamError( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (videoStream) => emit(UavVideoStreamLoaded( videoStream: videoStream, cameraIndex: event.cameraIndex, diff --git a/lib/features/v2/device_list/presentation/bloc/drone_station_state.dart b/lib/features/v2/device_list/presentation/bloc/drone_station_state.dart index 10b6f2f9..540eee7d 100644 --- a/lib/features/v2/device_list/presentation/bloc/drone_station_state.dart +++ b/lib/features/v2/device_list/presentation/bloc/drone_station_state.dart @@ -29,11 +29,15 @@ class DroneStationLoaded extends DroneStationState { class DroneStationError extends DroneStationState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const DroneStationError(this.message); + const DroneStationError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } class UAVDetailLoading extends DroneStationState { @@ -51,11 +55,15 @@ class UAVDetailLoaded extends DroneStationState { class UAVDetailError extends DroneStationState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const UAVDetailError(this.message); + const UAVDetailError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } class VideoStreamLoading extends DroneStationState { @@ -74,11 +82,15 @@ class VideoStreamLoaded extends DroneStationState { class VideoStreamError extends DroneStationState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const VideoStreamError(this.message); + const VideoStreamError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } /// 无人机实时视频流加载状态 @@ -105,9 +117,13 @@ class UavVideoStreamLoaded extends DroneStationState { /// 无人机实时视频流加载失败状态 class UavVideoStreamError extends DroneStationState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const UavVideoStreamError(this.message); + const UavVideoStreamError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart b/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart index 3f254b68..256d0d07 100644 --- a/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../core/consts/http_api_consts.dart'; +import '../../../../../core/network/error_handler.dart'; import '../../data/models/robot_data_model.dart'; import 'robot_list_event.dart'; import 'robot_list_state.dart'; @@ -19,7 +20,10 @@ class RobotListBloc extends Bloc { Emitter emit, ) async { if (event.siteId == null) { - emit(const RobotListError('请先选择场站')); + emit(const RobotListError( + message: '请先选择场站', + shouldShowError: true, + )); return; } @@ -53,7 +57,10 @@ class RobotListBloc extends Bloc { siteId: event.siteId, )); } catch (e) { - emit(RobotListError(e.toString())); + emit(RobotListError( + message: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); } } @@ -65,7 +72,10 @@ class RobotListBloc extends Bloc { final currentState = state as RobotListLoaded; if (currentState.siteId == null) { - emit(const RobotListError('请先选择场站')); + emit(const RobotListError( + message: '请先选择场站', + shouldShowError: true, + )); return; } @@ -94,7 +104,10 @@ class RobotListBloc extends Bloc { emit(currentState.copyWith(robots: robots)); } catch (e) { - emit(RobotListError(e.toString())); + emit(RobotListError( + message: ErrorHandler.getErrorMessage(e), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )); } } } diff --git a/lib/features/v2/device_list/presentation/bloc/robot_list_state.dart b/lib/features/v2/device_list/presentation/bloc/robot_list_state.dart index 93839cd4..1414d9db 100644 --- a/lib/features/v2/device_list/presentation/bloc/robot_list_state.dart +++ b/lib/features/v2/device_list/presentation/bloc/robot_list_state.dart @@ -45,9 +45,13 @@ class RobotListLoaded extends RobotListState { class RobotListError extends RobotListState { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const RobotListError(this.message); + const RobotListError({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } diff --git a/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart b/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart deleted file mode 100644 index 31c94dd4..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/bloc/float_bar_bloc.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import '../model/robot_status_model.dart'; -import '../service/robot_status_service.dart'; -import '../manager/float_bar_manager.dart'; - -/// 悬浮条事件 -abstract class FloatBarEvent {} - -/// 切换折叠/展开状态事件 -class ToggleExpandEvent extends FloatBarEvent {} - -/// 更新状态数据事件 -class UpdateStatusEvent extends FloatBarEvent { - final RobotStatusModel status; - - UpdateStatusEvent(this.status); -} - -/// 悬浮条状态 -abstract class FloatBarState {} - -/// 折叠状态 -class FloatBarCollapsedState extends FloatBarState { - final RobotStatusModel status; - - FloatBarCollapsedState(this.status); -} - -/// 展开状态 -class FloatBarExpandedState extends FloatBarState { - final RobotStatusModel status; - - FloatBarExpandedState(this.status); -} - -/// 悬浮条Bloc -class FloatBarBloc extends Bloc { - final RobotStatusService _statusService; - final FloatBarManager? _floatBarManager; - StreamSubscription? _statusSubscription; - - FloatBarBloc(this._statusService, [this._floatBarManager]) - : super(FloatBarCollapsedState(_statusService.currentStatus)) { - // 监听服务层数据流 - _startListening(); - - on(_handleToggleExpand); - on(_handleUpdateStatus); - } - - /// 开始监听服务层数据 - void _startListening() { - _statusSubscription?.cancel(); - _statusSubscription = _statusService.statusStream.listen((status) { - add(UpdateStatusEvent(status)); - }); - } - - /// 处理切换折叠/展开 - void _handleToggleExpand( - ToggleExpandEvent event, - Emitter emit, - ) { - final currentState = state; - if (currentState is FloatBarCollapsedState) { - emit(FloatBarExpandedState(currentState.status)); - } else if (currentState is FloatBarExpandedState) { - emit(FloatBarCollapsedState(currentState.status)); - } - // 触发UI刷新(使用 ?. 处理空安全) - _floatBarManager?.refresh(); - } - - /// 处理状态数据更新 - void _handleUpdateStatus( - UpdateStatusEvent event, - Emitter emit, - ) { - final currentState = state; - if (currentState is FloatBarCollapsedState) { - emit(FloatBarCollapsedState(event.status)); - } else if (currentState is FloatBarExpandedState) { - emit(FloatBarExpandedState(event.status)); - } - // 触发UI刷新(使用 ?. 处理空安全) - _floatBarManager?.refresh(); - } - - /// 启动状态服务 - void startService() { - _statusService.start(); - } - - /// 停止状态服务 - void stopService() { - _statusService.stop(); - } - - @override - Future close() { - _statusSubscription?.cancel(); - _statusService.stop(); - return super.close(); - } -} diff --git a/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart b/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart deleted file mode 100644 index c9f91841..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -/// 悬浮条设置服务 - 简化版 -/// 使用静态变量存储状态,确保全局同步 -class FloatBarSettingService { - final SharedPreferences _prefs; - static const String _key = 'float_bar_enabled'; - - /// 🔥 静态实例引用 - static FloatBarSettingService? _instance; - - /// 🔥 静态状态变量 - 所有组件共享 - static bool _isEnabled = true; - - /// 🔥 静态 ValueNotifier - 用于通知UI变化 - static final ValueNotifier _settingNotifier = ValueNotifier(true); - - FloatBarSettingService(this._prefs) { - _instance = this; - // 从持久化读取初始状态 - _isEnabled = _prefs.getBool(_key) ?? true; - _settingNotifier.value = _isEnabled; - print('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled'); - } - - /// 获取静态实例 - static FloatBarSettingService? get instance => _instance; - - /// 获取 ValueNotifier - static ValueNotifier get settingNotifier => _settingNotifier; - - /// 获取当前是否启用(直接从静态变量读取) - static bool get isEnabled => _isEnabled; - - /// 设置是否启用 - static Future setEnabled(bool enabled) async { - print('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled'); - - // 1. 更新静态变量 - _isEnabled = enabled; - print('🔍 [FloatBarSettingService] 静态变量已更新: $_isEnabled'); - - // 2. 更新 ValueNotifier(通知所有监听者) - _settingNotifier.value = enabled; - print( - '🔍 [FloatBarSettingService] ValueNotifier 已更新: ${_settingNotifier.value}', - ); - - // 3. 持久化到 SharedPreferences - final instance = _instance; - if (instance != null) { - await instance._prefs.setBool(_key, enabled); - print('🔍 [FloatBarSettingService] 已保存到 SharedPreferences'); - } - } - - /// 切换开关 - static Future toggle() async { - await setEnabled(!_isEnabled); - } -} diff --git a/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart b/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart deleted file mode 100644 index c6e748b1..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/float_bar_controller.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/foundation.dart'; - -/// 悬浮条控制器 - 极简版 -/// 使用静态变量管理全局状态 -class FloatBarController { - /// 🔥 是否显示悬浮条 - static bool isVisible = true; - - /// 🔥 状态变化通知器 - static final ValueNotifier visibilityNotifier = ValueNotifier(true); - - /// 设置显示/隐藏 - static void setVisible(bool visible) { - if (isVisible != visible) { - isVisible = visible; - visibilityNotifier.value = visible; - } - } - - /// 切换显示状态 - static void toggle() { - setVisible(!isVisible); - } -} diff --git a/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart b/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart deleted file mode 100644 index ddb1e579..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -import '../view/float_bar_widget.dart'; - -/// 全局悬浮条管理器 -/// 单例模式,负责管理OverlayEntry的创建、显示、隐藏和刷新 -class FloatBarManager { - static final FloatBarManager _instance = FloatBarManager._internal(); - - factory FloatBarManager() => _instance; - - FloatBarManager._internal(); - - /// OverlayEntry实例 - OverlayEntry? _overlayEntry; - - /// 是否已初始化 - bool _isInitialized = false; - - /// 全局上下文 - BuildContext? _globalContext; - - /// 初始化管理器,保存全局上下文 - void initialize(BuildContext context) { - if (_isInitialized) return; - _globalContext = context; - _isInitialized = true; - } - - /// 显示悬浮条 - void show() { - if (!_isInitialized || _globalContext == null) { - throw Exception('FloatBarManager has not been initialized!'); - } - - if (_overlayEntry != null) { - // 已有浮层,先移除再重新创建 - hide(); - } - - _overlayEntry = OverlayEntry( - builder: (context) => const FloatBarWidget(), - ); - - Overlay.of(_globalContext!)?.insert(_overlayEntry!); - } - - /// 隐藏悬浮条 - void hide() { - if (_overlayEntry != null) { - _overlayEntry!.remove(); - _overlayEntry = null; - } - } - - /// 强制刷新UI - void refresh() { - _overlayEntry?.markNeedsBuild(); - } - - /// 检查浮层是否显示中 - bool get isVisible => _overlayEntry != null; - - /// 释放资源 - void dispose() { - hide(); - _globalContext = null; - _isInitialized = false; - } -} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart b/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart deleted file mode 100644 index 27b06229..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/model/robot_status_model.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:equatable/equatable.dart'; - -/// 机器人状态数据模型 -class RobotStatusModel extends Equatable { - /// 任务名称 - final String taskName; - - /// 电量百分比 - final int battery; - - /// 设备状态:idle/running/charging/error - final String status; - - /// 信号强度 - final int signal; - - /// 当前位置 - final String location; - - /// 速度 - final double speed; - - /// 温度 - final int temperature; - - /// 运行时间 - final String runTime; - - const RobotStatusModel({ - this.taskName = '未知任务', - this.battery = 100, - this.status = 'idle', - this.signal = 100, - this.location = '未知位置', - this.speed = 0.0, - this.temperature = 25, - this.runTime = '00:00:00', - }); - - /// 创建副本 - RobotStatusModel copyWith({ - String? taskName, - int? battery, - String? status, - int? signal, - String? location, - double? speed, - int? temperature, - String? runTime, - }) { - return RobotStatusModel( - taskName: taskName ?? this.taskName, - battery: battery ?? this.battery, - status: status ?? this.status, - signal: signal ?? this.signal, - location: location ?? this.location, - speed: speed ?? this.speed, - temperature: temperature ?? this.temperature, - runTime: runTime ?? this.runTime, - ); - } - - /// 状态描述文本 - String get statusText { - switch (status) { - case 'running': - return '运行中'; - case 'charging': - return '充电中'; - case 'error': - return '故障'; - case 'idle': - default: - return '待机'; - } - } - - /// 状态颜色 - String get statusColor { - switch (status) { - case 'running': - return '#00C853'; - case 'charging': - return '#03DAC6'; - case 'error': - return '#FF5252'; - case 'idle': - default: - return '#9E9E9E'; - } - } - - @override - List get props => [ - taskName, - battery, - status, - signal, - location, - speed, - temperature, - runTime, - ]; -} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart b/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart deleted file mode 100644 index ee2b576d..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/service/robot_status_service.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import '../model/robot_status_model.dart'; - -/// 机器人状态服务 -/// 负责模拟设备状态推送,实际项目中应替换为真实的TCP/接口对接 -class RobotStatusService { - static final RobotStatusService _instance = RobotStatusService._internal(); - - factory RobotStatusService() => _instance; - - RobotStatusService._internal(); - - /// 状态数据流控制器 - final StreamController _statusController = - StreamController.broadcast(); - - /// 当前状态 - RobotStatusModel _currentStatus = const RobotStatusModel(); - - /// 模拟定时器 - Timer? _timer; - - /// 状态数据流 - Stream get statusStream => _statusController.stream; - - /// 获取当前状态 - RobotStatusModel get currentStatus => _currentStatus; - - /// 启动状态推送 - void start() { - if (_timer != null) return; - - // 立即发送初始状态 - _statusController.add(_currentStatus); - - // 模拟每3秒更新一次状态 - _timer = Timer.periodic(const Duration(seconds: 3), (timer) { - _simulateStatusUpdate(); - }); - } - - /// 停止状态推送 - void stop() { - _timer?.cancel(); - _timer = null; - } - - /// 手动更新状态(用于外部触发更新) - void updateStatus(RobotStatusModel status) { - _currentStatus = status; - _statusController.add(status); - } - - /// 模拟状态更新 - void _simulateStatusUpdate() { - final random = Random(); - final statuses = ['idle', 'running', 'charging', 'error']; - - _currentStatus = _currentStatus.copyWith( - battery: max(0, _currentStatus.battery + random.nextInt(3) - 1), - status: random.nextDouble() > 0.95 ? statuses[random.nextInt(statuses.length)] : _currentStatus.status, - signal: min(100, max(0, _currentStatus.signal + random.nextInt(5) - 2)), - speed: _currentStatus.status == 'running' ? random.nextDouble() * 5 : 0, - temperature: min(50, max(20, _currentStatus.temperature + random.nextInt(3) - 1)), - runTime: _updateRunTime(), - ); - - _statusController.add(_currentStatus); - } - - /// 更新运行时间 - String _updateRunTime() { - if (_currentStatus.status != 'running') { - return _currentStatus.runTime; - } - - final parts = _currentStatus.runTime.split(':'); - int hours = int.parse(parts[0]); - int minutes = int.parse(parts[1]); - int seconds = int.parse(parts[2]); - - seconds++; - if (seconds >= 60) { - seconds = 0; - minutes++; - } - if (minutes >= 60) { - minutes = 0; - hours++; - } - - return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - - /// 释放资源 - void dispose() { - stop(); - _statusController.close(); - } -} \ No newline at end of file diff --git a/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart b/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart deleted file mode 100644 index fb388e66..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/simple_float_bar.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:flutter/material.dart'; -import 'float_bar_controller.dart'; - -/// 简单悬浮条组件 -class SimpleFloatBar extends StatefulWidget { - const SimpleFloatBar({super.key}); - - @override - State createState() => _SimpleFloatBarState(); -} - -class _SimpleFloatBarState extends State { - bool _isExpanded = false; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), - child: GestureDetector( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: _isExpanded ? 200 : 56, - width: double.infinity, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFFE8F5E9), Color(0xFFFFFFFF)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.green.withOpacity(0.2), width: 1), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 12, - offset: const Offset(0, 4), - spreadRadius: 2, - ), - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 1, - ), - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: _isExpanded - ? SingleChildScrollView(child: _buildExpanded()) - : _buildCollapsed(), - ), - ), - ), - ); - } - - Widget _buildCollapsed() { - return Row(children: [ - Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))), - const SizedBox(width: 12), - const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87))), - const SizedBox(width: 12), - Row(children: const [Icon(Icons.battery_full, size: 18, color: Colors.grey), SizedBox(width: 4), Text('85%', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500))]), - const SizedBox(width: 8), - const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey), - const SizedBox(width: 8), - _buildCloseButton(), - ]); - } - - Widget _buildExpanded() { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))), - const SizedBox(width: 8), - Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(4)), child: const Text('运行中', style: TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500))), - const SizedBox(width: 12), - const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87))), - const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey), - const SizedBox(width: 8), - _buildCloseButton(), - ]), - const SizedBox(height: 16), - Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - _infoItem(Icons.battery_full, '电量', '85%'), - _infoItem(Icons.signal_cellular_alt, '信号', '100%'), - _infoItem(Icons.speed, '速度', '0.0m/s'), - _infoItem(Icons.thermostat, '温度', '25°C'), - ]), - const SizedBox(height: 12), - Row(children: const [ - Icon(Icons.location_on, size: 14, color: Colors.grey), - SizedBox(width: 4), - Expanded(child: Text('北京市朝阳区', style: TextStyle(fontSize: 12, color: Colors.grey))), - SizedBox(width: 12), - Icon(Icons.timer, size: 14, color: Colors.grey), - SizedBox(width: 4), - Text('02:35:18', style: TextStyle(fontSize: 12, color: Colors.grey)), - ]), - ]); - } - - Widget _buildCloseButton() { - return GestureDetector( - onTap: () => FloatBarController.setVisible(false), - child: Container(padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.close, size: 16, color: Colors.grey)), - ); - } - - Widget _infoItem(IconData icon, String label, String value) { - return Column(mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 20, color: Colors.grey), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), - const SizedBox(height: 2), - Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), - ]); - } -} diff --git a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart b/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart deleted file mode 100644 index 110fee43..00000000 --- a/lib/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart +++ /dev/null @@ -1,350 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../bloc/float_bar_bloc.dart'; -import '../model/robot_status_model.dart'; -import '../service/robot_status_service.dart'; -import '../cubit/float_bar_setting_cubit.dart'; - -/// 悬浮条UI组件 -/// 内部独立管理Bloc,不依赖外部注入 -class FloatBarWidget extends StatefulWidget { - const FloatBarWidget({super.key}); - - @override - State createState() => _FloatBarWidgetState(); -} - -class _FloatBarWidgetState extends State { - late final FloatBarBloc _bloc; - - @override - void initState() { - super.initState(); - debugPrint('🔥 [FloatBarWidget] initState - 开始创建 Bloc'); - // 内部创建Bloc并启动服务(不需要FloatBarManager,因为我们使用Stack方式) - _bloc = FloatBarBloc(RobotStatusService()); - _bloc.startService(); - debugPrint('✅ [FloatBarWidget] Bloc 已创建并启动服务'); - } - - @override - void dispose() { - debugPrint('🔥 [FloatBarWidget] dispose - 关闭 Bloc'); - _bloc.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - debugPrint('🔥 [FloatBarWidget] build - 渲染悬浮条'); - return BlocProvider.value(value: _bloc, child: const _FloatBarContent()); - } -} - -/// 悬浮条内容组件 -class _FloatBarContent extends StatelessWidget { - const _FloatBarContent(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final isExpanded = state is FloatBarExpandedState; - final status = state is FloatBarCollapsedState - ? state.status - : (state as FloatBarExpandedState).status; - - debugPrint( - '🔥 [FloatBarContent] build - isExpanded: $isExpanded, status: ${status.taskName}', - ); - - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), // 底部留出 Tab 栏空间 - child: GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击悬浮条'); - context.read().add(ToggleExpandEvent()); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - height: isExpanded ? 180 : 56, - width: double.infinity, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xFFE8F5E9), // 浅绿色 - Color(0xFFFFFFFF), // 白色 - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.green.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 12, - offset: const Offset(0, 4), - spreadRadius: 2, - ), - BoxShadow( - color: Colors.black.withOpacity(0.08), - blurRadius: 20, - offset: const Offset(0, 8), - spreadRadius: 1, - ), - BoxShadow( - color: Colors.white.withOpacity(0.6), - blurRadius: 8, - offset: const Offset(0, -2), - spreadRadius: -2, - ), - ], - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - child: isExpanded - ? _buildExpandedContent(status) - : _buildCollapsedContent(status), - ), - ), - ), - ); - }, - ); - } - - /// 折叠态内容 - Widget _buildCollapsedContent(RobotStatusModel status) { - return Row( - children: [ - // 状态指示灯 - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: _parseColor(status.statusColor), - borderRadius: BorderRadius.circular(5), - ), - ), - const SizedBox(width: 12), - // 任务名称 - Expanded( - child: Text( - status.taskName, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - // 电量 - Row( - children: [ - Icon( - status.battery > 20 ? Icons.battery_full : Icons.battery_alert, - size: 18, - color: status.battery > 20 ? Colors.grey : Colors.red, - ), - const SizedBox(width: 4), - Text( - '${status.battery}%', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: status.battery > 20 ? Colors.black87 : Colors.red, - ), - ), - ], - ), - const SizedBox(width: 8), - // 展开箭头 - const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey), - const SizedBox(width: 8), - // 关闭按钮 - GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击关闭按钮'); - // 🔥 使用静态方法关闭悬浮条 - FloatBarSettingService.setEnabled(false); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.close, size: 16, color: Colors.grey), - ), - ), - ], - ); - } - - /// 展开态内容 - Widget _buildExpandedContent(RobotStatusModel status) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 顶部:状态+任务名+折叠按钮 - Row( - children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: _parseColor(status.statusColor), - borderRadius: BorderRadius.circular(5), - ), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: _parseColor(status.statusColor).withOpacity(0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - status.statusText, - style: TextStyle( - fontSize: 12, - color: _parseColor(status.statusColor), - fontWeight: FontWeight.w500, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - status.taskName, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: Colors.black87, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey), - const SizedBox(width: 8), - // 关闭按钮 - GestureDetector( - onTap: () { - debugPrint('🔥 [FloatBarContent] 点击关闭按钮(展开态)'); - // 🔥 使用静态方法关闭悬浮条 - FloatBarSettingService.setEnabled(false); - }, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.close, size: 16, color: Colors.grey), - ), - ), - ], - ), - const SizedBox(height: 16), - // 中间:详细信息网格 - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildInfoItem( - status.battery > 20 ? Icons.battery_full : Icons.battery_alert, - '电量', - '${status.battery}%', - status.battery > 20 ? Colors.black87 : Colors.red, - ), - _buildInfoItem( - Icons.signal_cellular_alt, - '信号', - '${status.signal}%', - Colors.black87, - ), - _buildInfoItem( - Icons.speed, - '速度', - '${status.speed.toStringAsFixed(1)}m/s', - Colors.black87, - ), - _buildInfoItem( - Icons.thermostat, - '温度', - '${status.temperature}°C', - Colors.black87, - ), - ], - ), - const SizedBox(height: 12), - // 底部:位置+时间 - Row( - children: [ - const Icon(Icons.location_on, size: 14, color: Colors.grey), - const SizedBox(width: 4), - Expanded( - child: Text( - status.location, - style: const TextStyle(fontSize: 12, color: Colors.grey), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 12), - const Icon(Icons.timer, size: 14, color: Colors.grey), - const SizedBox(width: 4), - Text( - status.runTime, - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - ], - ), - ], - ); - } - - /// 信息项组件 - Widget _buildInfoItem( - IconData icon, - String label, - String value, - Color valueColor, - ) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 20, color: Colors.grey), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), - const SizedBox(height: 2), - Text( - value, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: valueColor, - ), - ), - ], - ); - } - - /// 解析颜色字符串 - Color _parseColor(String colorStr) { - try { - return Color(int.parse(colorStr.replaceFirst('#', '0xFF'))); - } catch (_) { - return Colors.grey; - } - } -} 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 index 14c8ee91..473b1832 100644 --- a/lib/features/v2/device_list/presentation/pages/device_status_page.dart +++ b/lib/features/v2/device_list/presentation/pages/device_status_page.dart @@ -17,6 +17,7 @@ import '../widgets/device_item_widget.dart'; import '../widgets/drone_station_item_card.dart'; import 'robot_list_page.dart'; import 'drone_station_detail_page.dart'; +import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart'; /// 设备状态页面 - 使用 BLoC 模式 class DeviceStatusPage extends StatelessWidget { @@ -50,22 +51,35 @@ class DeviceStatusView extends StatelessWidget { child: Scaffold( backgroundColor: const Color(0xFFF7F7F7), body: SafeArea( - child: - BlocBuilder< - DeviceListBloc.DeviceStatusBloc, - DeviceListState.DeviceStatusState - >( - builder: (context, state) { - return Column( - children: [ - _buildAppBar(context), - _buildSearchBar(context), - _buildTypeFilterBar(context), - Expanded(child: _buildContent(context, state)), - ], - ); - }, - ), + child: BlocConsumer< + DeviceListBloc.DeviceStatusBloc, + DeviceListState.DeviceStatusState + >( + listener: (context, state) { + // 🔥 监听错误状态,显示友好提示 + if (state is DeviceListState.DeviceStatusError && + state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, + ), + ); + } + }, + builder: (context, state) { + return Column( + children: [ + _buildAppBar(context), + _buildSearchBar(context), + _buildTypeFilterBar(context), + Expanded(child: _buildContent(context, state)), + ], + ); + }, + ), ), ), ); @@ -264,33 +278,11 @@ class DeviceStatusView extends StatelessWidget { ); } + // 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar + // 页面保持当前内容,用户可以继续操作 if (state is DeviceListState.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 DeviceListEvent.DeviceStatusLoadData(), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF165DFF), - foregroundColor: Colors.white, - ), - child: const Text('重试'), - ), - ], - ), - ); + // 如果是从 Loaded 状态变成 Error,返回空容器保持页面 + return const SizedBox.shrink(); } if (state is DeviceListState.DeviceStatusLoaded) { @@ -375,7 +367,20 @@ class DeviceStatusView extends StatelessWidget { return BlocProvider( create: (_) => sl()..add(DroneStationLoadData(selectedSite.id)), - child: BlocBuilder( + child: BlocConsumer( + listener: (context, state) { + // 🔥 监听错误状态,显示友好提示 + if (state is DroneStationError && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, + ), + ); + } + }, builder: (context, state) { if (state is DroneStationLoading) { return const Center( @@ -383,40 +388,10 @@ class DeviceStatusView extends StatelessWidget { ); } + // 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar if (state is DroneStationError) { - 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( - DroneStationLoadData(selectedSite.id), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF165DFF), - foregroundColor: Colors.white, - ), - child: const Text('重试'), - ), - ], - ), - ); + // 返回空容器保持页面 + return const SizedBox.shrink(); } if (state is DroneStationLoaded) { @@ -572,8 +547,11 @@ class DeviceStatusView extends StatelessWidget { ), ), builder: (BuildContext context) { - return BlocProvider.value( - value: context.read(), + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value(value: sl()), + ], child: const DeviceStatusModal(), ); }, 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 index bfb41fe5..fc0177a7 100644 --- 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 @@ -11,9 +11,8 @@ import '../../domain/usecases/update_flight_task_status_usecase.dart'; /// 无人机任务与航线控制页面 class DroneMissionControlPage extends StatefulWidget { final List? selectedTasks; - final String? droneSn; // 无人机序列号 - const DroneMissionControlPage({super.key, this.selectedTasks, this.droneSn}); + const DroneMissionControlPage({super.key, this.selectedTasks}); @override State createState() => @@ -23,12 +22,7 @@ class DroneMissionControlPage extends StatefulWidget { class _DroneMissionControlPageState extends State { FlightTaskEntity? _listTask; // 列表数据 FlightTaskDetailEntity? _detailTask; // 详情数据 - String? _droneSn; // 无人机序列号 bool _isLoading = false; - bool _isReturningHome = false; // 是否正在返航 - bool _isPausing = false; // 是否正在暂停 - bool _isReturnHomeLoading = false; // 返航命令是否正在执行 - bool _isPauseLoading = false; // 暂停命令是否正在执行 final Dio _dio = Dio(); @override @@ -38,7 +32,6 @@ class _DroneMissionControlPageState extends State { _listTask = widget.selectedTasks!.first; _loadTaskDetail(); } - _droneSn = widget.droneSn; // 初始化无人机序列号 } Future _loadTaskDetail() async { @@ -78,7 +71,6 @@ class _DroneMissionControlPageState extends State { final detailData = jsonData['data']; print('🔍 [DroneMissionControl] data 字段类型: ${detailData.runtimeType}'); - print('🔍 [DroneMissionControl] data 字段完整内容: $detailData'); // 确保 data 也是 Map final Map detailMap = (detailData is Map) @@ -126,7 +118,7 @@ class _DroneMissionControlPageState extends State { print('❌ [DroneMissionControl] 执行任务失败: ${failure.message}'); ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('下发指令失败'))); + ).showSnackBar(SnackBar(content: Text(failure.message))); }, (data) { print('✅ [DroneMissionControl] 任务执行成功: $data'); @@ -139,105 +131,7 @@ class _DroneMissionControlPageState extends State { print('❌ [DroneMissionControl] 执行任务异常: $e'); ScaffoldMessenger.of( context, - ).showSnackBar(const SnackBar(content: Text('下发指令失败'))); - } - } - - // 返航/取消返航 - Future _toggleReturnHome() async { - if (_detailTask == null || _detailTask!.sn.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('设备SN不存在'))); - return; - } - - setState(() => _isReturnHomeLoading = true); - - try { - final command = _isReturningHome ? 'return_home_cancel' : 'return_home'; - print('🔍 [DroneMissionControl] 发送返航命令: $command, deviceSn: $_droneSn'); - - final response = await _dio.post( - HttpApiConsts.flightTaskCommand, - data: {'command': command, 'deviceSn': _droneSn}, - ); - - print('📦 [DroneMissionControl] 返航响应: ${response.data}'); - - if (response.statusCode == 200) { - final responseData = response.data; - if (responseData['code'] == 0) { - setState(() => _isReturningHome = !_isReturningHome); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_isReturningHome ? '已下发返航命令' : '已取消返航')), - ); - } - } else { - throw Exception(responseData['message'] ?? '操作失败'); - } - } else { - throw Exception('请求失败'); - } - } catch (e) { - print('❌ [DroneMissionControl] 返航操作失败: $e'); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('下发指令失败'))); - } - } finally { - setState(() => _isReturnHomeLoading = false); - } - } - - // 暂停/取消暂停 - Future _togglePause() async { - if (_detailTask == null || _detailTask!.sn.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('设备SN不存在'))); - return; - } - - setState(() => _isPauseLoading = true); - - try { - final command = _isPausing ? 'flighttask_recovery' : 'flighttask_pause'; - print('🔍 [DroneMissionControl] 发送暂停命令: $command, deviceSn: $_droneSn'); - - final response = await _dio.post( - HttpApiConsts.flightTaskCommand, - data: {'command': command, 'deviceSn': _droneSn}, - ); - - print('📦 [DroneMissionControl] 暂停响应: ${response.data}'); - - if (response.statusCode == 200) { - final responseData = response.data; - if (responseData['code'] == 0) { - setState(() => _isPausing = !_isPausing); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(_isPausing ? '已下发暂停命令' : '已取消暂停')), - ); - } - } else { - throw Exception(responseData['message'] ?? '操作失败'); - } - } else { - throw Exception('请求失败'); - } - } catch (e) { - print('❌ [DroneMissionControl] 暂停操作失败: $e'); - if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('下发指令失败'))); - } - } finally { - setState(() => _isPauseLoading = false); + ).showSnackBar(SnackBar(content: Text('执行任务失败: $e'))); } } @@ -362,7 +256,7 @@ class _DroneMissionControlPageState extends State { ], ), const SizedBox(height: 12), - _buildInfoRow('网关序列号', task.sn), + _buildInfoRow('设备序列号', task.sn), const SizedBox(height: 12), _buildInfoRow('任务类型', task.taskType), const SizedBox(height: 12), @@ -628,7 +522,7 @@ class _DroneMissionControlPageState extends State { const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: _isPauseLoading ? null : _togglePause, + onPressed: () {}, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFFF7D00), foregroundColor: Colors.white, @@ -638,28 +532,16 @@ class _DroneMissionControlPageState extends State { ), elevation: 0, ), - child: _isPauseLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : Text( - _isPausing ? '取消暂停' : '暂停任务', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - ), - ), + child: const Text( + '暂停任务', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500), + ), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton( - onPressed: _isReturnHomeLoading ? null : _toggleReturnHome, + onPressed: () {}, style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF4E5969), side: const BorderSide(color: Color(0xFFC9CDD4)), @@ -668,19 +550,10 @@ class _DroneMissionControlPageState extends State { borderRadius: BorderRadius.circular(8), ), ), - child: _isReturnHomeLoading - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : Text( - _isReturningHome ? '取消返航' : '返航降落', - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w500, - ), - ), + child: const Text( + '返航降落', + style: TextStyle(fontSize: 15, 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 index b33aa5a9..7514a954 100644 --- 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 @@ -28,8 +28,10 @@ class DroneStationDetailPage extends StatefulWidget { class _DroneStationDetailPageState extends State { late DroneStationBloc _bloc; - UAVDetailEntity? _detail; // 无人机详情数据 - String? _droneSn; // 无人机序列号 + + // 无人机详情数据 + UAVDetailEntity? _detail; + String? _droneSn; // 悬浮视频监控状态 bool showFloatingMonitor = false; @@ -55,7 +57,7 @@ class _DroneStationDetailPageState extends State { // 加载超时计时器 Timer? _floatingLoadingTimer; static const _floatingLoadingTimeout = Duration(seconds: 15); - + // 无人机状态轮询计时器 Timer? _droneStatusPollingTimer; @@ -69,7 +71,7 @@ class _DroneStationDetailPageState extends State { deviceSn: widget.station.deviceSn, ), ); - + // 启动无人机状态轮询(每5秒刷新一次) _startDroneStatusPolling(); } @@ -81,15 +83,15 @@ class _DroneStationDetailPageState extends State { debugPrint(' - 是否已显示: $showFloatingMonitor'); debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}'); debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}'); - + if (!_isFloatingMonitorEnabled || showFloatingMonitor) { debugPrint('❌ 不满足条件,退出检查'); return; // 开关关闭或已显示,不执行 } - + // 无人机在线且有机场摄像头,自动打开悬浮窗 - if (detail.droneOnlineStatus == 1 && - detail.gatewayCameraList != null && + if (detail.droneOnlineStatus == 1 && + detail.gatewayCameraList != null && detail.gatewayCameraList!.isNotEmpty) { debugPrint('✅ 检测到无人机在线,自动打开悬浮窗'); _loadFloatingVideoStream(); @@ -109,13 +111,30 @@ class _DroneStationDetailPageState extends State { /// 启动无人机状态轮询 void _startDroneStatusPolling() { - // 每5秒刷新一次无人机状态 - _droneStatusPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) { - if (!mounted) { - timer.cancel(); - return; + _scheduleDroneStatusPoll(); + } + + /// 根据无人机状态动态调整轮询周期 + void _scheduleDroneStatusPoll() { + if (!mounted) return; + + // 检查当前无人机状态 + Duration interval = const Duration(seconds: 60); // 默认60秒 + final currentState = _bloc.state; + if (currentState is UAVDetailLoaded) { + if (currentState.detail.droneOnlineStatus == 1) { + // 无人机在线时,每15秒轮询一次 + interval = const Duration(seconds: 15); + } else { + // 无人机离线时,每60秒轮询一次(降低频率) + interval = const Duration(seconds: 60); } - + } + + _droneStatusPollingTimer?.cancel(); + _droneStatusPollingTimer = Timer(interval, () { + if (!mounted) return; + debugPrint('🔄 定时刷新无人机状态...'); _bloc.add( UAVDetailLoad( @@ -123,17 +142,20 @@ class _DroneStationDetailPageState extends State { deviceSn: widget.station.deviceSn, ), ); - + // 如果悬浮窗开启且无人机上线,自动显示悬浮窗 if (_isFloatingMonitorEnabled) { - final currentState = _bloc.state; - if (currentState is UAVDetailLoaded && - currentState.detail.droneOnlineStatus == 1 && + final state = _bloc.state; + if (state is UAVDetailLoaded && + state.detail.droneOnlineStatus == 1 && !showFloatingMonitor) { debugPrint('✅ 无人机已上线,自动打开悬浮窗'); _loadFloatingVideoStream(); } } + + // 重新调度下一次轮询(动态周期) + _scheduleDroneStatusPoll(); }); } @@ -203,10 +225,7 @@ class _DroneStationDetailPageState extends State { _initFloatingRtcEngine(); } else if (state is VideoStreamError) { _floatingLoadingTimer?.cancel(); - setState(() { - _floatingErrorMessage = state.message; - _isFloatingLoading = false; - }); + setState(() {}); } }, builder: (context, state) { @@ -674,7 +693,6 @@ class _DroneStationDetailPageState extends State { ), ), ), - const SizedBox(height: 12), // 悬浮观看功能已禁用 // GestureDetector( // onTap: () { @@ -739,10 +757,8 @@ class _DroneStationDetailPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => DroneMissionControlPage( - selectedTasks: tasks, - droneSn: _droneSn, - ), + builder: (context) => + DroneMissionControlPage(selectedTasks: tasks), ), ); } 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 index d9d61e2e..82ebd8f6 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -46,7 +46,20 @@ class _RobotListViewState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listener: (context, state) { + // 🔥 监听错误状态,显示友好提示 + if (state is RobotListError && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, + ), + ); + } + }, builder: (context, state) { if (state is RobotListLoading) { return const Center( @@ -56,27 +69,10 @@ class _RobotListViewState extends State { ); } + // 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar if (state is RobotListError) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline, - size: 48, - color: Color(0xFFF53F3F), - ), - const SizedBox(height: 16), - Text( - '加载失败: ${state.message}', - style: const TextStyle( - fontSize: 14, - color: Color(0xFF4E5969), - ), - ), - ], - ), - ); + // 返回空容器保持页面 + return const SizedBox.shrink(); } if (state is RobotListLoaded) { @@ -480,9 +476,11 @@ class _RobotListViewState extends State { status: robot.status, battery: robot.battery, task: robot.task, - onTap: () { + onTap: () async { // final logger = sl(); - debugPrint('📱 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}, status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}'); + debugPrint('🔴🔴🔴 [选中机器人] ========== 点击事件触发 =========='); + debugPrint('🔴🔴🔴 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}'); + debugPrint('🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}'); // 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName) final device = DeviceEntity( @@ -495,9 +493,11 @@ class _RobotListViewState extends State { onlineStatus: robot.status == '在线' ? 1 : 0, ); + debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...'); // 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例 final remoteCubit = GetIt.I(); - remoteCubit.setTargetDevice(device); + remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行 + debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)'); // 2. 跳转到机器人控制页面 final robotMap = { @@ -509,6 +509,7 @@ class _RobotListViewState extends State { 'task': robot.task, }; + debugPrint('🚀 [RobotListPage] 准备跳转页面...'); Navigator.push( context, MaterialPageRoute( 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 index dacfca7b..17a73610 100644 --- 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 @@ -3,9 +3,7 @@ import '../pages/drone_mission_control_page.dart'; /// 无人机机场与设备状态组件 class DroneStationStatusWidget extends StatelessWidget { - final String? droneSn; // 无人机序列号 - - const DroneStationStatusWidget({super.key, this.droneSn}); + const DroneStationStatusWidget({super.key}); @override Widget build(BuildContext context) { @@ -222,8 +220,7 @@ class DroneStationStatusWidget extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (context) => - DroneMissionControlPage(droneSn: droneSn), + builder: (context) => const DroneMissionControlPage(), ), ); }, diff --git a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart index c9daf08e..5de16c7b 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_bloc.dart @@ -6,6 +6,7 @@ import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_home_data_ import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_site_list_usecase.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart'; +import '../../../../../core/network/error_handler.dart'; class HomeV2Bloc extends Bloc { final GetHomeDataUseCase getHomeDataUseCase; @@ -27,7 +28,10 @@ class HomeV2Bloc extends Bloc { final user = appUserCubit.state.user; if (user == null) { - emit(const HomeV2Error('用户未登录')); + emit(const HomeV2Error( + message: '用户未登录', + shouldShowError: true, + )); return; } @@ -36,7 +40,10 @@ class HomeV2Bloc extends Bloc { final siteResult = await getSiteListUseCase(user.orgId); // 使用用户的 orgId homeResult.fold( - (failure) => emit(HomeV2Error(failure.message)), + (failure) => emit(HomeV2Error( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (homeData) { List sites = []; SiteEntity? selectedSite; @@ -87,7 +94,10 @@ class HomeV2Bloc extends Bloc { final homeResult = await getHomeDataUseCase(const NoParams()); homeResult.fold( - (failure) => emit(HomeV2Error(failure.message)), + (failure) => emit(HomeV2Error( + message: ErrorHandler.getErrorMessage(failure.message), + shouldShowError: true, // 🔥 标记需要显示弹窗 + )), (homeData) { emit(HomeV2Loaded( homeData: homeData, diff --git a/lib/features/v2/home/presentation/bloc/home_v2_state.dart b/lib/features/v2/home/presentation/bloc/home_v2_state.dart index d37ca36f..5aa42267 100644 --- a/lib/features/v2/home/presentation/bloc/home_v2_state.dart +++ b/lib/features/v2/home/presentation/bloc/home_v2_state.dart @@ -50,9 +50,13 @@ class HomeV2Loaded extends HomeV2State { class HomeV2Error extends HomeV2State { final String message; + final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗 - const HomeV2Error(this.message); + const HomeV2Error({ + required this.message, + this.shouldShowError = false, // 默认 false + }); @override - List get props => [message]; + List get props => [message, shouldShowError]; } 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 3e07d9f4..6df9e9a9 100644 --- a/lib/features/v2/home/presentation/pages/home_v2_page.dart +++ b/lib/features/v2/home/presentation/pages/home_v2_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart'; import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart'; @@ -15,6 +16,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_statu import 'package:maibu_satabot_v2/components/device_status_modal.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart'; +import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart'; + class HomeV2Page extends StatefulWidget { const HomeV2Page({super.key}); @@ -35,29 +38,26 @@ class _HomeV2PageState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F7FA), - body: BlocBuilder( - builder: (context, state) { - if (state is HomeV2Error) { - 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), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => _bloc.add(const HomeV2LoadData()), - child: const Text('重试'), - ), - ], + body: BlocConsumer( + listener: (context, state) { + // 🔥 监听错误状态,显示友好提示 + if (state is HomeV2Error && state.shouldShowError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.orange, ), ); } + }, + builder: (context, state) { + // 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar + if (state is HomeV2Error) { + // 返回空容器保持页面 + return const SizedBox.shrink(); + } if (state is HomeV2Loaded) { return RefreshIndicator( @@ -345,8 +345,15 @@ class _HomeV2PageState extends State { ), ), builder: (BuildContext context) { - return BlocProvider.value( - value: context.read(), + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: GetIt.I(), // 🔥 注入 RemoteControlCubit + ), + ], child: const DeviceStatusModal(), ); }, diff --git a/lib/features/v2/home/presentation/widgets/tcp_status_indicator.dart b/lib/features/v2/home/presentation/widgets/tcp_status_indicator.dart index d1f48264..5da23888 100644 --- a/lib/features/v2/home/presentation/widgets/tcp_status_indicator.dart +++ b/lib/features/v2/home/presentation/widgets/tcp_status_indicator.dart @@ -86,29 +86,33 @@ class _TcpStatusIndicatorState extends State message: tooltip, child: InkWell( onTap: widget.onTap, - borderRadius: BorderRadius.circular(6), - child: AnimatedBuilder( - animation: _animation, - builder: (context, child) { - final opacity = shouldAnimate ? _animation.value : 1.0; - return Container( - width: 12, - height: 12, - decoration: BoxDecoration( - color: color.withOpacity(opacity), - shape: BoxShape.circle, - boxShadow: state.status == TcpConnectionStatus.connected - ? [ - BoxShadow( - color: Colors.green.withOpacity(0.5 * opacity), - blurRadius: 6 * opacity, - spreadRadius: 2 * opacity, - ), - ] - : [], - ), - ); - }, + borderRadius: BorderRadius.circular(12), + // 🔥 增大点击热区 + child: Container( + padding: const EdgeInsets.all(8), + child: AnimatedBuilder( + animation: _animation, + builder: (context, child) { + final opacity = shouldAnimate ? _animation.value : 1.0; + return Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: color.withOpacity(opacity), + shape: BoxShape.circle, + boxShadow: state.status == TcpConnectionStatus.connected + ? [ + BoxShadow( + color: Colors.green.withOpacity(0.5 * opacity), + blurRadius: 6 * opacity, + spreadRadius: 2 * opacity, + ), + ] + : [], + ), + ); + }, + ), ), ), ); diff --git a/lib/features/v2/my/presentation/pages/system_settings_page.dart b/lib/features/v2/my/presentation/pages/system_settings_page.dart index 54f0efdc..95c7faeb 100644 --- a/lib/features/v2/my/presentation/pages/system_settings_page.dart +++ b/lib/features/v2/my/presentation/pages/system_settings_page.dart @@ -8,9 +8,6 @@ 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'; -import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/float_bar_controller.dart'; - -final sl = GetIt.instance; /// 系统设置综合页面 class SystemSettingsPage extends StatefulWidget { @@ -21,17 +18,6 @@ class SystemSettingsPage extends StatefulWidget { } class _SystemSettingsPageState extends State { - /// 悬浮条开关状态 - bool _floatBarEnabled = true; - - @override - void initState() { - super.initState(); - // 初始化时获取当前状态(使用静态属性) - _floatBarEnabled = FloatBarController.isVisible; - print('🔍 [设置页面] initState,初始状态: $_floatBarEnabled'); - } - @override Widget build(BuildContext context) { return Scaffold( @@ -59,9 +45,6 @@ class _SystemSettingsPageState extends State { // Tab 设置 _buildTabSettingsSection(), const SizedBox(height: 12), - // 悬浮条设置 - _buildFloatBarSection(), - const SizedBox(height: 12), // 语言设置 _buildLanguageSection(), const SizedBox(height: 12), @@ -105,9 +88,9 @@ class _SystemSettingsPageState extends State { if (state.runtimeType.toString() != 'TabConfigLoaded') { return const Center(child: CircularProgressIndicator()); } - + final tabs = state.config.items; - + return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -181,18 +164,14 @@ class _SystemSettingsPageState extends State { AppLocalizations.of(context).translate('my.chinese'), 'zh', locale.languageCode == 'zh', - () => context.read().setLocale( - const Locale('zh', 'CN'), - ), + () => 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'), - ), + () => context.read().setLocale(const Locale('en', 'US')), ), ], ); @@ -203,25 +182,16 @@ class _SystemSettingsPageState extends State { ); } - Widget _buildLanguageOption( - String label, - String code, - bool isSelected, - VoidCallback onTap, - ) { + 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, + 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), + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB), width: 1, ), ), @@ -232,9 +202,7 @@ class _SystemSettingsPageState extends State { label, style: TextStyle( fontSize: 14, - color: isSelected - ? const Color(0xFF165DFF) - : const Color(0xFF1D2129), + color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129), fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, ), ), @@ -284,94 +252,6 @@ class _SystemSettingsPageState extends State { ); } - /// 悬浮条设置 - Widget _buildFloatBarSection() { - 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), - _buildFloatBarSwitch(), - ], - ), - ); - } - - /// 悬浮条开关 - Widget _buildFloatBarSwitch() { - print( - '🔍 [设置页面] _buildFloatBarSwitch 被调用,_floatBarEnabled: $_floatBarEnabled', - ); - - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [ - Text( - '显示悬浮条', - style: TextStyle(fontSize: 14, color: Color(0xFF1D2129)), - ), - SizedBox(height: 4), - Text( - '在Tab页面显示设备状态悬浮条', - style: TextStyle(fontSize: 12, color: Color(0xFF8F959E)), - ), - ], - ), - ), - Switch( - value: _floatBarEnabled, - onChanged: (value) { - print('🔍 [设置页面] 开关被点击,新值: $value,当前状态: $_floatBarEnabled'); - - // 🔥 立即更新本地状态 - setState(() { - _floatBarEnabled = value; - }); - print('🔍 [设置页面] 本地状态已更新为: $_floatBarEnabled'); - - // 🔥 使用静态方法设置新值 - FloatBarController.setVisible(value); - print('🔍 [设置页面] setVisible 已调用完成'); - - // 显示提示 - if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(value ? '悬浮条已开启' : '悬浮条已关闭'), - duration: const Duration(seconds: 2), - ), - ); - } - }, - activeColor: const Color(0xFF165DFF), - ), - ], - ); - } - /// 显示退出登录确认对话框 void _showLogoutDialog(BuildContext context) { showDialog( diff --git a/lib/features/v2/site/presentation/cubit/site_cubit.dart b/lib/features/v2/site/presentation/cubit/site_cubit.dart index 22ec608f..5d5c176b 100644 --- a/lib/features/v2/site/presentation/cubit/site_cubit.dart +++ b/lib/features/v2/site/presentation/cubit/site_cubit.dart @@ -1,5 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../../../core/network/tcp/tcp_client.dart'; import '../../../home/domain/entities/site_entity.dart'; class SiteState { @@ -48,8 +51,26 @@ class SiteCubit extends Cubit { /// 选择场站(持久化) void selectSite(SiteEntity site) { + debugPrint('🏭 [SiteCubit] ========== 切换场站 =========='); + debugPrint('🏭 [SiteCubit] 从 ${state.selectedSite?.siteName ?? "无"} 切换到 ${site.siteName}'); + + // 🔥 关键修复:只有切换到不同场站时才断开TCP + if (state.selectedSite?.id != site.id) { + final tcpClient = GetIt.I(); + if (tcpClient.isConnected) { + debugPrint('🛑 [SiteCubit] 检测到TCP已连接,正在断开...'); + tcpClient.disconnect(); + debugPrint('✅ [SiteCubit] TCP已断开,防止场站间设备数据混淆'); + } else { + debugPrint('ℹ️ [SiteCubit] TCP未连接,无需断开'); + } + } else { + debugPrint('✅ [SiteCubit] 相同场站,保持TCP连接状态'); + } + sharedPreferences.setInt(_selectedSiteIdKey, site.id); emit(state.copyWith(selectedSite: site)); + debugPrint('✅ [SiteCubit] 场站切换完成'); } /// 清除选中场站 diff --git a/lib/main.dart b/lib/main.dart index 9f297b1f..3cb6a0f9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,12 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_patcher/flutter_patcher.dart'; import 'package:go_router/go_router.dart'; -import 'package:get_it/get_it.dart'; - -// 悬浮条相关 -import 'features/v2/device_list/presentation/float_bar/float_bar_controller.dart'; -import 'features/v2/device_list/presentation/float_bar/simple_float_bar.dart'; - import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/infrastructure/logging/app_bloc_observer.dart'; import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart'; @@ -23,22 +19,15 @@ import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_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/main_wrapper.dart'; -// 🔥 导入悬浮条管理器和设置服务 -import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart'; -import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart'; -import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart'; import 'core/di/injection.dart'; -import 'core/router/route_paths.dart'; import 'core/localization/app_localizations.dart'; import 'core/localization/locale_cubit.dart'; import 'features/auth/presentation/bloc/auth_state.dart'; import 'features/auth/presentation/bloc/login_cubit.dart'; import 'features/devices/presentation/bloc/device_status_bloc.dart'; import 'features/home/presentation/bloc/permission_request_bloc.dart'; - -// 🔥 全局 GetIt 实例 -final sl = GetIt.instance; +import 'features/remote_control/presentation/bloc/remote_control_cubit.dart'; // 🔥 定义全局 Navigator Key final GlobalKey navigatorKey = GlobalKey(); @@ -50,17 +39,10 @@ void main() async { await FlutterPatcher.init(); await init(); - final logger = sl(); await logger.init(); Bloc.observer = AppBlocObserver(logger); - // 🔥 确保 FloatBarSettingService 被初始化,这样 settingNotifier 才不会是 null - sl(); - debugPrint( - '🔍 [FloatBar] FloatBarSettingService 已初始化,settingNotifier: ${FloatBarSettingService.settingNotifier}', - ); - // ⚠️ 注意:不要在启动时清除补丁版本记录! // 补丁版本记录只在整包更新成功后才清除 // 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新 @@ -104,6 +86,9 @@ class MyApp extends StatelessWidget { BlocProvider( create: (_) => UpdateCubit(VersionCheckService()), ), + BlocProvider( + create: (_) => sl(), + ), ], child: BlocBuilder( bloc: localeCubit, @@ -121,23 +106,7 @@ class MyApp extends StatelessWidget { theme: AppTheme.lightTheme, routerConfig: sl(), builder: (context, child) { - // 🔥 初始化悬浮条管理器并显示悬浮条 - debugPrint( - '======== [FloatBar] MaterialApp.builder START ========', - ); - debugPrint('🔍 [FloatBar] child: $child'); - debugPrint( - '🔍 [FloatBar] settingNotifier: ${FloatBarSettingService.settingNotifier}', - ); - debugPrint( - '🔍 [FloatBar] isEnabled: ${FloatBarSettingService.isEnabled}', - ); - debugPrint( - '======== [FloatBar] MaterialApp.builder END ========', - ); - return _FloatBarInitializer( - child: _LifecycleListener(child: _UpdateChecker(child: child!)), - ); + return _LifecycleListener(child: _UpdateChecker(child: child!)); }, ); }, @@ -146,35 +115,6 @@ class MyApp extends StatelessWidget { } } -/// 悬浮条初始化组件 - 极简版 -class _FloatBarInitializer extends StatelessWidget { - final Widget child; - - const _FloatBarInitializer({required this.child}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: FloatBarController.visibilityNotifier, - builder: (context, isVisible, child) { - return Stack( - children: [ - child!, - if (isVisible) - const Positioned( - left: 0, - right: 0, - bottom: 0, - child: SimpleFloatBar(), - ), - ], - ); - }, - child: child, - ); - } -} - class _LifecycleListener extends StatefulWidget { final Widget? child; const _LifecycleListener({required this.child}); @@ -302,16 +242,15 @@ class _UpdateCheckerState extends State<_UpdateChecker> { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.shade50, + color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( - color: Colors.blue.shade200, + color: Colors.orange.shade200, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 💡 选择下载方式 const Text( '💡 选择下载方式', style: TextStyle( @@ -321,8 +260,8 @@ class _UpdateCheckerState extends State<_UpdateChecker> { ), const SizedBox(height: 8), const Text( - '• 自动安装:App 内下载并调起系统安装\n' - '• 手动下载:在浏览器中下载安装', + '• 浏览器下载(推荐):打开浏览器下载,手动安装并卸载旧版本\n' + '• App 内下载:App 内下载 APK,调起系统安装界面', style: TextStyle( fontSize: 13, height: 1.5, @@ -366,20 +305,24 @@ class _UpdateCheckerState extends State<_UpdateChecker> { onPressed: () { final info = state.versionInfo; if (info.apkUrl != null) { + // 🔥 浏览器下载 + 引导弹窗 context .read() - .downloadAndInstallApk( + .fullApkUpdateWithBrowser( info.apkUrl!, ); + // 显示引导弹窗 + showBrowserDownloadGuide(context); } }, - child: const Text('手动下载'), + child: const Text('浏览器下载'), ), const SizedBox(width: 10), ElevatedButton( onPressed: () { final info = state.versionInfo; if (info.apkUrl != null) { + // App 内下载(保留备用) context .read() .downloadAndInstallApk( @@ -387,7 +330,7 @@ class _UpdateCheckerState extends State<_UpdateChecker> { ); } }, - child: const Text('自动安装'), + child: const Text('App 内下载'), ), ], ], @@ -624,3 +567,55 @@ class _UpdateCheckerState extends State<_UpdateChecker> { ); } } + +/// 显示浏览器下载引导弹窗(全局方法) +void showBrowserDownloadGuide(BuildContext context) { + Future.delayed(const Duration(milliseconds: 500), () { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + title: const Text('🔔 重要提示'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '请在浏览器中完成以下操作:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + const Text('✅ 等待 APK 下载完成'), + const SizedBox(height: 8), + const Text('✅ 点击 APK 文件进行安装'), + const SizedBox(height: 8), + const Text('✅ 安装完成后,卸载当前旧版本'), + const SizedBox(height: 8), + const Text('✅ 重新打开新版本 APP'), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.red.shade200), + ), + child: const Text( + '⚠️ 注意:必须先卸载旧版本,否则无法正常使用新功能!', + style: TextStyle(color: Colors.red, fontSize: 12), + ), + ), + ], + ), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.of(ctx).pop(); + }, + child: const Text('我知道了'), + ), + ], + ), + ); + }); +}