集成接口 开始执行任务的领域层和数据层和UI层的开发啊(待测试)

集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上
优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示!
优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。
优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。
调整了获取无人机状态信息的更新频率 为14秒一次
This commit is contained in:
2026-06-16 17:34:00 +08:00
parent 52fe17c0d5
commit 71aea48d9f
61 changed files with 2896 additions and 1648 deletions

View File

@@ -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 <methods>;
}
# ===== 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 { *; }

View File

@@ -9,7 +9,8 @@
<application
android:label="maibu_satabot_v2"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"

6
dist/manifest.json vendored
View File

@@ -1,6 +1,6 @@
{
"version": "1.1.4",
"md5": "eed99d4cdba060904454826051a04f0a",
"targetVersionCode": 2,
"version": "1.0.13",
"md5": "181176ea51ec26ac97f34c2a550661f0",
"targetVersionCode": 1,
"abi": "arm64-v8a"
}

View File

@@ -0,0 +1,213 @@
# DeviceTaskCubit 使用指南
## 功能概述
DeviceTaskCubit 提供了设备任务管理的完整功能:
- 获取任务池并过滤出当前设备的任务
- 取消任务
- 暂停任务
- 恢复任务
- 全局管理 taskId
## 在页面中集成
### 1. 在 BlocProvider 中注册
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../presentation/bloc/device_task_cubit.dart';
class YourPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GetIt.I<DeviceTaskCubit>(),
child: YourPageContent(),
);
}
}
```
### 2. 获取任务池(选择航线时调用)
```dart
// 当用户选择航线后,获取该设备的任务
final deviceId = targetDevice?.deviceId ?? '';
if (deviceId.isNotEmpty) {
context.read<DeviceTaskCubit>().fetchAndFilterTask(deviceId);
}
```
### 3. 监听状态变化(自动显示错误弹窗)
```dart
BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
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<DeviceTaskCubit>().cancelTask(deviceId);
```
#### 暂停任务
```dart
final deviceId = targetDevice?.deviceId ?? '';
context.read<DeviceTaskCubit>().pauseTask(deviceId);
```
#### 恢复任务
```dart
final deviceId = targetDevice?.deviceId ?? '';
context.read<DeviceTaskCubit>().recoveryTask(deviceId);
```
### 5. 手动更新 taskId(选择新航线时)
```dart
// 如果需要在选择航线时手动设置 taskId
context.read<DeviceTaskCubit>().updateCurrentTaskId(newTaskId);
```
### 6. 清除当前任务
```dart
// 退出页面或切换设备时清除
context.read<DeviceTaskCubit>().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<DeviceTaskCubit>(),
child: Scaffold(
appBar: AppBar(title: const Text('路径规划')),
body: BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
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<DeviceTaskCubit>().pauseTask(deviceId);
},
child: const Text('暂停'),
),
ElevatedButton(
onPressed: () {
final deviceId = 'YOUR_DEVICE_ID';
context.read<DeviceTaskCubit>().recoveryTask(deviceId);
},
child: const Text('恢复'),
),
ElevatedButton(
onPressed: () {
final deviceId = 'YOUR_DEVICE_ID';
context.read<DeviceTaskCubit>().cancelTask(deviceId);
},
child: const Text('取消'),
),
],
),
// 加载指示器
if (state.isLoading)
const CircularProgressIndicator(),
],
);
},
),
),
);
}
}
```
## 错误处理优化
项目中已实现统一的错误处理机制:
- 所有接口错误都会转换为友好的中文提示
- 不会显示原始的错误信息(如 "HTTP 404"、"Connection timeout" 等)
- 提示会自动消失(2秒后)
- 页面不会崩溃
示例错误提示:
- "网络连接超时,请检查网络设置"
- "登录已过期,请重新登录"
- "操作失败,请稍后重试"
- "未知错误,请稍后重试"

View File

@@ -0,0 +1,303 @@
# Flutter 项目统一错误处理规范
## 核心原则
**所有接口异常都不应该影响页面展示,只显示友好提示弹窗(2秒后自动消失)**
## 实现方案
### 1. Cubit/Bloc 层处理
#### ✅ 正确做法
```dart
class MyCubit extends Cubit<MyState> {
Future<void> 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<MyCubit, MyState>(
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<DeviceTaskState> {
Future<void> 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<DeviceTaskCubit>(),
child: Scaffold(
appBar: AppBar(title: const Text('任务管理')),
body: BlocConsumer<DeviceTaskCubit, DeviceTaskState>(
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<DeviceTaskCubit>().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秒自动消失)
- [ ] 不显示原始错误信息

388
docs/ERROR_HANDLING_FIX.md Normal file
View File

@@ -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<DeviceStatusBloc, DeviceStatusState>(
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<DroneStationBloc, DroneStationState>(
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<RobotListBloc, RobotListState>(
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<HomeV2Bloc, HomeV2State>(
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<SomeBloc, SomeState>(
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

View File

@@ -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<DeviceStatusModal> {
void initState() {
super.initState();
_startDataTimeoutTimer();
// 🔥 页面初始化时自动调用刷新
WidgetsBinding.instance.addPostFrameCallback((_) {
_onRefresh();
});
}
/// 🔥 刷新函数 - 与刷新按钮共享逻辑
void _onRefresh() {
context.read<DeviceStatusBloc>().add(DeviceStatusReset());
_resetChartData();
}
@override
@@ -468,14 +482,35 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
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<RemoteControlCubit, RemoteControlState>(
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<DeviceStatusModal> {
),
IconButton(
icon: const Icon(Icons.refresh, color: Color(0xFF1677FF)),
onPressed: () {
context.read<DeviceStatusBloc>().add(
DeviceStatusReset(),
);
_resetChartData();
},
onPressed: _onRefresh,
),
],
),

View File

@@ -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";
}

View File

@@ -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<void> init() async {
// Tab 配置 Cubit (单例)
sl.registerLazySingleton(() => TabConfigCubit(sl<SharedPreferences>()));
// 🔥 悬浮条设置服务 (单例)
sl.registerLazySingleton(
() => FloatBarSettingService(sl<SharedPreferences>()),
);
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
sl.registerLazySingleton(
() => DevicesCubit(
@@ -404,8 +408,8 @@ Future<void> init() async {
),
);
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建)
sl.registerFactory(
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(单例模式,全局共享)
sl.registerLazySingleton(
() => RemoteControlCubit(
sl(), // RemoteControlRepository
sl(), // RequestControlPermissionUseCase
@@ -447,6 +451,11 @@ Future<void> init() async {
() => GetWorkRecordsBySiteIdUseCase(sl<PathRepository>()),
);
/// 创建设备任务(通过接口执行作业)
sl.registerLazySingleton(
() => CreateDeviceTaskUseCase(sl<PathRepository>()),
);
/// 6. 认证 (Auth)
// --- 关键修改点 1: AuthCubit 必须在 GoRouter 之前注册,并传入参数 ---
sl.registerLazySingleton(
@@ -485,4 +494,24 @@ Future<void> init() async {
markAllAsReadUseCase: sl(),
),
);
/// 10. 设备任务管理 (Device Task)
sl.registerLazySingleton<DeviceTaskDatasource>(
() => DeviceTaskDatasourceImpl(sl<Dio>()),
);
sl.registerLazySingleton<DeviceTaskRepository>(
() => DeviceTaskRepositoryImpl(datasource: sl<DeviceTaskDatasource>()),
);
sl.registerLazySingleton(() => GetDeviceTaskPoolUseCase(sl()));
sl.registerLazySingleton(() => CancelTaskUseCase(sl()));
sl.registerLazySingleton(() => PauseTaskUseCase(sl()));
sl.registerLazySingleton(() => RecoveryTaskUseCase(sl()));
sl.registerFactory(
() => DeviceTaskCubit(
sl(),
sl(),
sl(),
sl(),
),
);
}

View File

@@ -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);
}
}

View File

@@ -91,7 +91,7 @@ class NetMessageDispatcher {
//机器状态页面专用
Stream<String> onStringMessageOfdeviceStatus() {
//print("0x02--TCP拦截推送解析开始");
_logger.log('0x02--TCP拦截推送解析开始');
_logger.logWithLevel('0x02--TCP拦截推送解析开始' ,shouldLog: true);
// 🔥 关键修复:使用asBroadcastStream()确保多个监听者都能收到数据
return onCommand(0x02).map((p) {
try {

View File

@@ -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>();
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<TcpStatusCubit>().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');

View File

@@ -185,18 +185,19 @@ class UpdateCubit extends Cubit<UpdateState> {
}
}
/// 在浏览器中打开下载链接(手动下载)
Future<void> openInBrowser(String apkUrl) async {
/// 整包更新:浏览器下载 + 引导弹窗
Future<void> 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 {

View File

@@ -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<UpdateCubit>().downloadAndInstallApk(versionInfo.apkUrl!);
// 🔥 调用新方法:浏览器下载 + 清除补丁记录
context.read<UpdateCubit>().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('我知道了'),
),
],
),
);
});
}
}

View File

@@ -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'];

View File

@@ -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<List<DeviceTaskModel>> getDeviceTaskPool({
required String userId,
required int siteId,
required int orgId,
int pageNum = 1,
int pageSize = 99999999,
});
Future<bool> cancelTask({
required String deviceId,
required int taskId,
required int orgId,
required int siteId,
});
Future<bool> pauseTask({
required String deviceId,
required int taskId,
required int orgId,
required int siteId,
});
Future<Map<String, dynamic>> 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<ILoggerService>();
DeviceTaskDatasourceImpl(this.dio);
@override
Future<List<DeviceTaskModel>> 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<String, dynamic>;
if (data['code'] == 200) {
final rows = data['rows'] as List? ?? [];
return rows
.map((item) => DeviceTaskModel.fromJson(item as Map<String, dynamic>))
.toList();
} else {
throw Exception(data['msg'] ?? '获取任务池失败');
}
} else {
throw Exception('HTTP ${response.statusCode}');
}
} catch (e) {
_logger.logWithLevel('❌ 获取任务池失败: $e');
rethrow;
}
}
@override
Future<bool> 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<String, dynamic>;
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<bool> 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<String, dynamic>;
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<Map<String, dynamic>> 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<String, dynamic>;
if (data['code'] == 200) {
return data['data'] as Map<String, dynamic>? ?? {};
} else {
throw Exception(data['msg'] ?? '恢复任务失败');
}
} else {
throw Exception('HTTP ${response.statusCode}');
}
} catch (e) {
_logger.logWithLevel('❌ 恢复任务失败: $e');
rethrow;
}
}
}

View File

@@ -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<String, dynamic> 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,
);
}
}

View File

@@ -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<Either<Failure, List<DeviceTaskEntity>>> 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<Either<Failure, bool>> 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<Either<Failure, bool>> 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<Either<Failure, Map<String, dynamic>>> 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()));
}
}
}

View File

@@ -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'<data>([\s\S]*?)</data>');
final dataMatches = dataRegex.allMatches(body);
print('[XML接口] 找到data节点数量: ${dataMatches.length}');
///print('[XML接口] 找到data节点数量: ${dataMatches.length}');
// 解析所有工作记录
final List<WorkRecordEntity> 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<String, dynamic> _parseXmlRecord(XmlElement recordElement) {
final Map<String, dynamic> 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<void> 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');
}
}
}

View File

@@ -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<String, dynamic> 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<Object?> get props => [
id,
deviceId,
taskStatus,
taskStatusTranslate,
routeId,
siteId,
orgId,
createTime,
];
}

View File

@@ -0,0 +1,38 @@
import 'package:fpdart/fpdart.dart';
import '../../../../core/error/failure.dart';
import '../entities/device_task_entity.dart';
abstract class DeviceTaskRepository {
/// 获取设备任务池列表
Future<Either<Failure, List<DeviceTaskEntity>>> getDeviceTaskPool({
required String userId,
required int siteId,
required int orgId,
int pageNum = 1,
int pageSize = 99999999,
});
/// 取消任务
Future<Either<Failure, bool>> cancelTask({
required String deviceId,
required int taskId,
required int orgId,
required int siteId,
});
/// 暂停任务
Future<Either<Failure, bool>> pauseTask({
required String deviceId,
required int taskId,
required int orgId,
required int siteId,
});
/// 恢复任务
Future<Either<Failure, Map<String, dynamic>>> recoveryTask({
required String deviceId,
required int taskId,
required int orgId,
required int siteId,
});
}

View File

@@ -26,4 +26,14 @@ abstract class PathRepository {
/// 根据场站ID查询工作记录列表(XML格式)
Future<List<WorkRecordEntity>> getWorkRecordsBySiteId({required int siteId});
/// 创建设备任务(通过接口执行作业)
/// deviceId: 设备ID(targetDevice)
/// routeId: 路线ID(选中的路线任务ID)
/// siteId: 场站ID
Future<void> createDeviceTask({
required String deviceId,
required int routeId,
required int siteId,
});
}

View File

@@ -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<bool, CancelTaskParams> {
final DeviceTaskRepository repository;
CancelTaskUseCase(this.repository);
@override
Future<Either<Failure, bool>> 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,
});
}

View File

@@ -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<Either<DeviceFailure, void>> 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()));
}
}
}

View File

@@ -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<List<DeviceTaskEntity>, GetDeviceTaskPoolParams> {
final DeviceTaskRepository repository;
GetDeviceTaskPoolUseCase(this.repository);
@override
Future<Either<Failure, List<DeviceTaskEntity>>> 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,
});
}

View File

@@ -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<bool, PauseTaskParams> {
final DeviceTaskRepository repository;
PauseTaskUseCase(this.repository);
@override
Future<Either<Failure, bool>> 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,
});
}

View File

@@ -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<Map<String, dynamic>, RecoveryTaskParams> {
final DeviceTaskRepository repository;
RecoveryTaskUseCase(this.repository);
@override
Future<Either<Failure, Map<String, dynamic>>> 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,
});
}

View File

@@ -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<DeviceTaskState> {
final GetDeviceTaskPoolUseCase _getDeviceTaskPoolUseCase;
final CancelTaskUseCase _cancelTaskUseCase;
final PauseTaskUseCase _pauseTaskUseCase;
final RecoveryTaskUseCase _recoveryTaskUseCase;
final ILoggerService _logger = GetIt.I<ILoggerService>();
DeviceTaskCubit(
this._getDeviceTaskPoolUseCase,
this._cancelTaskUseCase,
this._pauseTaskUseCase,
this._recoveryTaskUseCase,
) : super(const DeviceTaskState());
/// 获取任务池并过滤出当前设备的任务
Future<void> fetchAndFilterTask(String deviceId) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
try {
// 获取用户信息
final userCubit = GetIt.I<AppUserCubit>();
final user = userCubit.state.user;
if (user == null) {
emit(state.copyWith(
isLoading: false,
errorMessage: '用户未登录',
));
return;
}
// 获取场站ID
final siteCubit = GetIt.I<SiteCubit>();
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<void> 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<AppUserCubit>();
final user = userCubit.state.user;
final siteCubit = GetIt.I<SiteCubit>();
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<void> 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<AppUserCubit>();
final user = userCubit.state.user;
final siteCubit = GetIt.I<SiteCubit>();
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<void> 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<AppUserCubit>();
final user = userCubit.state.user;
final siteCubit = GetIt.I<SiteCubit>();
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('🧹 清除当前任务');
}
}

View File

@@ -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<DeviceTaskEntity> 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<DeviceTaskEntity>? 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<Object?> get props => [
taskPool,
currentTask,
currentTaskId,
isLoading,
errorMessage,
operationType,
shouldShowError,
];
}

View File

@@ -623,4 +623,53 @@ class DevicesCubit extends Cubit<DevicesState> {
//print('🔄 [DevicesCubit] 已重置到达位置');
_logger.logWithLevel('🔄 [DevicesCubit] 已重置到达位置');
}
/// 🔥 获取当前选中的设备
DeviceEntity? getSelectedDevice() {
return state.selectedDevice;
}
/// 🔥 异步获取当前选中的设备(兼容 RemoteControlCubit 接口)
Future<DeviceEntity?> 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;
}
}
}

View File

@@ -61,24 +61,26 @@ class _HomePageState extends State<HomePage> {
// 🔥 自动选中第一个设备(保证是最新的)
devicesCubit.selectDevice(firstDevice);
// 🔥 关键修复:首页不再自动连接TCP,改为选择设备时才连接
// 连接 TCP
final tcpClient = GetIt.instance<TcpClient>();
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<RemoteControlRepository>();
remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送');
});
} else {
// 🔥 TCP 已连接,也要发送权限请求
final remoteRepo = GetIt.instance<RemoteControlRepository>();
remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求');
}
// final tcpClient = GetIt.instance<TcpClient>();
// 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<RemoteControlRepository>();
// remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
// debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送');
// });
// } else {
// // 🔥 TCP 已连接,也要发送权限请求
// final remoteRepo = GetIt.instance<RemoteControlRepository>();
// remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
// debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求');
// }
debugPrint('ℹ️ [HomePage] TCP 连接已移至选择设备时执行');
}
} catch (e) {
setState(() => _isDevicesLoading = false);

View File

@@ -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<MapPageEnterprise> {
Future<void> _savePlotData(String plotName, String? imgBase64) async {
final loc = AppLocalizations.of(context);
// 1. 获取用户ID
final userId = context.read<AppUserCubit>().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<SiteCubit>().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<MapPageEnterprise> {
// 3. 构造 workRecord (包裹一层)
final Map<String, dynamic> 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<MapPageEnterprise> {
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<MapPageEnterprise> {
/// 开始作业
void _startWork() async {
// 🔥 V2 适配:使用接口方式执行作业,不再使用 TCP
// 1. 校验选中的路线
if (_selectedPlot == null) {
_showPageToast(message: "请先选择一个路线任务", type: ToastType.info);
return;
}
// 2. 获取设备ID(从 RemoteControlCubit 的 targetDevice)
final targetDevice = context.read<RemoteControlCubit>().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<SiteCubit>().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<DevicesCubit>().updateAppState(AppState.routePlanning);
try {
// 8. 调用接口创建设备任务
final result = await sl<CreateDeviceTaskUseCase>().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<MapPageEnterprise> {
setState(() {
isStopWork = false;
isStartWork = true; // 🔥 关键:停止作业标志
isStartWork = true;
_workStatus = WorkStatus.working;
_traceManager.reset();
tracePoint?.clear();
gctracePoint?.clear();
//_traceManager.setMode(TPMode.NAVIGATION);
});
// 🔥 核心修复:先更新 AppState 为 routePlanning
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
// 🔥 延迟一下,确保状态已更新
await Future.delayed(const Duration(milliseconds: 100));
debugPrint('⚙️ 开始类型转换...');
@@ -2007,19 +2088,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
);
}).toList();
final Queue<work_area_model.DeviceAddPathPointModel> pathQueue = Queue.from(
typedList,
);
// 步骤 3:调用 Cubit 方法(类型匹配)
final Queue<work_area_model.DeviceAddPathPointModel> pathQueue = Queue.from(typedList);
await context.read<DevicesCubit>().startRoutePlanning(pathQueue);
///context.read<DevicesCubit>().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<MapPageEnterprise> {
headingStatus == 0
? null
: () {
_startWork();
//@开始作业通过后端接口
// _startWork();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00C853),
@@ -2344,6 +2419,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
height: 50,
child: ElevatedButton(
onPressed: () =>
//@暂停工作通过接口、恢复(继续)工作通过接口
_workStatus == WorkStatus.working
? _pauseWork()
: _resumeWork(),
@@ -2487,7 +2563,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
}).toList();
}
// ========== 抽象:生成路径的核心函数 ==========
// ========== 抽象:生成路径的核心函数(打点函数) ==========
Future<void> _generatePath({bool showTips = true}) async {
if (_currentWorkMode == WorkMode.custom) {
setState(() {
@@ -2742,10 +2818,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
final maxTop = screenHeight - menuHeight;
final userState = context.watch<AppUserCubit>().state;
// 🔥 V2 适配:使用 RemoteControlCubit 的 targetDevice 保持一致性
final deviceId = context
.watch<DevicesCubit>()
.watch<RemoteControlCubit>()
.state
.selectedDevice
.targetDevice
?.deviceName;
if (deviceId != null &&

View File

@@ -946,6 +946,9 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
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<RemoteControlState> {
);
});
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
emit(state.copyWith(targetDevice: device));
debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 ==========');
}
@@ -992,4 +993,12 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
// _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备');
emit(state.copyWith(targetDevice: null));
}
/// 🔥 获取设备
DeviceEntity? getSelectedDevice() {
return state.targetDevice;
}
}

View File

@@ -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<String, dynamic>))
.map(
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
)
.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<String, dynamic>))
.map(
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
)
.toList()
: null,
orgId: json['orgId'] ?? 0,

View File

@@ -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<String, dynamic> json) {
// 安全处理 folder_info 嵌套结构
final rawFolderInfo = json['folder_info'];
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
? Map<String, dynamic>.from(rawFolderInfo)
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
? Map<String, dynamic>.from(rawFolderInfo)
: <String, dynamic>{};
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,

View File

@@ -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<DeviceStatusEvent, DeviceStatusState> {
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<DeviceStatusEvent, DeviceStatusState> {
devices: response.devices,
));
} catch (e) {
emit(DeviceStatusError(e.toString()));
emit(DeviceStatusError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
}

View File

@@ -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<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}
class DeviceStatusEmpty extends DeviceStatusState {

View File

@@ -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<DroneStationEvent, DroneStationState> {
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<DroneStationEvent, DroneStationState> {
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<DroneStationEvent, DroneStationState> {
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<DroneStationEvent, DroneStationState> {
);
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<DroneStationEvent, DroneStationState> {
);
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,

View File

@@ -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<Object?> get props => [message];
List<Object?> 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<Object?> get props => [message];
List<Object?> 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<Object?> get props => [message];
List<Object?> 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<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -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<RobotListEvent, RobotListState> {
Emitter<RobotListState> emit,
) async {
if (event.siteId == null) {
emit(const RobotListError('请先选择场站'));
emit(const RobotListError(
message: '请先选择场站',
shouldShowError: true,
));
return;
}
@@ -53,7 +57,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
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<RobotListEvent, RobotListState> {
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<RobotListEvent, RobotListState> {
emit(currentState.copyWith(robots: robots));
} catch (e) {
emit(RobotListError(e.toString()));
emit(RobotListError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
}

View File

@@ -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<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -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<FloatBarEvent, FloatBarState> {
final RobotStatusService _statusService;
final FloatBarManager? _floatBarManager;
StreamSubscription? _statusSubscription;
FloatBarBloc(this._statusService, [this._floatBarManager])
: super(FloatBarCollapsedState(_statusService.currentStatus)) {
// 监听服务层数据流
_startListening();
on<ToggleExpandEvent>(_handleToggleExpand);
on<UpdateStatusEvent>(_handleUpdateStatus);
}
/// 开始监听服务层数据
void _startListening() {
_statusSubscription?.cancel();
_statusSubscription = _statusService.statusStream.listen((status) {
add(UpdateStatusEvent(status));
});
}
/// 处理切换折叠/展开
void _handleToggleExpand(
ToggleExpandEvent event,
Emitter<FloatBarState> 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<FloatBarState> 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<void> close() {
_statusSubscription?.cancel();
_statusService.stop();
return super.close();
}
}

View File

@@ -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<bool> _settingNotifier = ValueNotifier<bool>(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<bool> get settingNotifier => _settingNotifier;
/// 获取当前是否启用(直接从静态变量读取)
static bool get isEnabled => _isEnabled;
/// 设置是否启用
static Future<void> 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<void> toggle() async {
await setEnabled(!_isEnabled);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/foundation.dart';
/// 悬浮条控制器 - 极简版
/// 使用静态变量管理全局状态
class FloatBarController {
/// 🔥 是否显示悬浮条
static bool isVisible = true;
/// 🔥 状态变化通知器
static final ValueNotifier<bool> visibilityNotifier = ValueNotifier<bool>(true);
/// 设置显示/隐藏
static void setVisible(bool visible) {
if (isVisible != visible) {
isVisible = visible;
visibilityNotifier.value = visible;
}
}
/// 切换显示状态
static void toggle() {
setVisible(!isVisible);
}
}

View File

@@ -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;
}
}

View File

@@ -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<Object?> get props => [
taskName,
battery,
status,
signal,
location,
speed,
temperature,
runTime,
];
}

View File

@@ -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<RobotStatusModel> _statusController =
StreamController.broadcast();
/// 当前状态
RobotStatusModel _currentStatus = const RobotStatusModel();
/// 模拟定时器
Timer? _timer;
/// 状态数据流
Stream<RobotStatusModel> 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();
}
}

View File

@@ -1,123 +0,0 @@
import 'package:flutter/material.dart';
import 'float_bar_controller.dart';
/// 简单悬浮条组件
class SimpleFloatBar extends StatefulWidget {
const SimpleFloatBar({super.key});
@override
State<SimpleFloatBar> createState() => _SimpleFloatBarState();
}
class _SimpleFloatBarState extends State<SimpleFloatBar> {
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)),
]);
}
}

View File

@@ -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<FloatBarWidget> createState() => _FloatBarWidgetState();
}
class _FloatBarWidgetState extends State<FloatBarWidget> {
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<FloatBarBloc, FloatBarState>(
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<FloatBarBloc>().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;
}
}
}

View File

@@ -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<DeviceListBloc.DeviceStatusBloc>().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<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
child: BlocBuilder<DroneStationBloc, DroneStationState>(
child: BlocConsumer<DroneStationBloc, DroneStationState>(
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<DroneStationBloc>().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<DeviceStatusBloc>(),
return MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<DeviceStatusBloc>()),
BlocProvider.value(value: sl<RemoteControlCubit>()),
],
child: const DeviceStatusModal(),
);
},

View File

@@ -11,9 +11,8 @@ import '../../domain/usecases/update_flight_task_status_usecase.dart';
/// 无人机任务与航线控制页面
class DroneMissionControlPage extends StatefulWidget {
final List<FlightTaskEntity>? selectedTasks;
final String? droneSn; // 无人机序列号
const DroneMissionControlPage({super.key, this.selectedTasks, this.droneSn});
const DroneMissionControlPage({super.key, this.selectedTasks});
@override
State<DroneMissionControlPage> createState() =>
@@ -23,12 +22,7 @@ class DroneMissionControlPage extends StatefulWidget {
class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
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<DroneMissionControlPage> {
_listTask = widget.selectedTasks!.first;
_loadTaskDetail();
}
_droneSn = widget.droneSn; // 初始化无人机序列号
}
Future<void> _loadTaskDetail() async {
@@ -78,7 +71,6 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
final detailData = jsonData['data'];
print('🔍 [DroneMissionControl] data 字段类型: ${detailData.runtimeType}');
print('🔍 [DroneMissionControl] data 字段完整内容: $detailData');
// 确保 data 也是 Map
final Map<String, dynamic> detailMap = (detailData is Map)
@@ -126,7 +118,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
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<DroneMissionControlPage> {
print('❌ [DroneMissionControl] 执行任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
}
}
// 返航/取消返航
Future<void> _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<void> _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<DroneMissionControlPage> {
],
),
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<DroneMissionControlPage> {
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<DroneMissionControlPage> {
),
elevation: 0,
),
child: _isPauseLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(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<DroneMissionControlPage> {
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),
),
),
),
],

View File

@@ -28,8 +28,10 @@ class DroneStationDetailPage extends StatefulWidget {
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
late DroneStationBloc _bloc;
UAVDetailEntity? _detail; // 无人机详情数据
String? _droneSn; // 无人机序列号
// 无人机详情数据
UAVDetailEntity? _detail;
String? _droneSn;
// 悬浮视频监控状态
bool showFloatingMonitor = false;
@@ -55,7 +57,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// 加载超时计时器
Timer? _floatingLoadingTimer;
static const _floatingLoadingTimeout = Duration(seconds: 15);
// 无人机状态轮询计时器
Timer? _droneStatusPollingTimer;
@@ -69,7 +71,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 启动无人机状态轮询(每5秒刷新一次)
_startDroneStatusPolling();
}
@@ -81,15 +83,15 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
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<DroneStationDetailPage> {
/// 启动无人机状态轮询
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<DroneStationDetailPage> {
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<DroneStationDetailPage> {
_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<DroneStationDetailPage> {
),
),
),
const SizedBox(height: 12),
// 悬浮观看功能已禁用
// GestureDetector(
// onTap: () {
@@ -739,10 +757,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DroneMissionControlPage(
selectedTasks: tasks,
droneSn: _droneSn,
),
builder: (context) =>
DroneMissionControlPage(selectedTasks: tasks),
),
);
}

View File

@@ -46,7 +46,20 @@ class _RobotListViewState extends State<RobotListView> {
@override
Widget build(BuildContext context) {
return BlocBuilder<RobotListBloc, RobotListState>(
return BlocConsumer<RobotListBloc, RobotListState>(
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<RobotListView> {
);
}
// 🔥 错误状态不再显示全屏错误页面,而是通过 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<RobotListView> {
status: robot.status,
battery: robot.battery,
task: robot.task,
onTap: () {
onTap: () async {
// final logger = sl<ILoggerService>();
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<RobotListView> {
onlineStatus: robot.status == '在线' ? 1 : 0,
);
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
final remoteCubit = GetIt.I<RemoteControlCubit>();
remoteCubit.setTargetDevice(device);
remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行
debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)');
// 2. 跳转到机器人控制页面
final robotMap = {
@@ -509,6 +509,7 @@ class _RobotListViewState extends State<RobotListView> {
'task': robot.task,
};
debugPrint('🚀 [RobotListPage] 准备跳转页面...');
Navigator.push(
context,
MaterialPageRoute(

View File

@@ -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(),
),
);
},

View File

@@ -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<HomeV2Event, HomeV2State> {
final GetHomeDataUseCase getHomeDataUseCase;
@@ -27,7 +28,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
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<HomeV2Event, HomeV2State> {
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<SiteEntity> sites = [];
SiteEntity? selectedSite;
@@ -87,7 +94,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
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,

View File

@@ -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<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -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<HomeV2Page> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
body: BlocBuilder<HomeV2Bloc, HomeV2State>(
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<HomeV2Bloc, HomeV2State>(
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<HomeV2Page> {
),
),
builder: (BuildContext context) {
return BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
return MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
),
BlocProvider.value(
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
),
],
child: const DeviceStatusModal(),
);
},

View File

@@ -86,29 +86,33 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
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,
),
]
: [],
),
);
},
),
),
),
);

View File

@@ -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<SystemSettingsPage> {
/// 悬浮条开关状态
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<SystemSettingsPage> {
// Tab 设置
_buildTabSettingsSection(),
const SizedBox(height: 12),
// 悬浮条设置
_buildFloatBarSection(),
const SizedBox(height: 12),
// 语言设置
_buildLanguageSection(),
const SizedBox(height: 12),
@@ -105,9 +88,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
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<SystemSettingsPage> {
AppLocalizations.of(context).translate('my.chinese'),
'zh',
locale.languageCode == 'zh',
() => context.read<LocaleCubit>().setLocale(
const Locale('zh', 'CN'),
),
() => context.read<LocaleCubit>().setLocale(const Locale('zh', 'CN')),
),
const SizedBox(height: 12),
_buildLanguageOption(
AppLocalizations.of(context).translate('my.english'),
'en',
locale.languageCode == 'en',
() => context.read<LocaleCubit>().setLocale(
const Locale('en', 'US'),
),
() => context.read<LocaleCubit>().setLocale(const Locale('en', 'US')),
),
],
);
@@ -203,25 +182,16 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
);
}
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<SystemSettingsPage> {
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<SystemSettingsPage> {
);
}
/// 悬浮条设置
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(

View File

@@ -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<SiteState> {
/// 选择场站(持久化)
void selectSite(SiteEntity site) {
debugPrint('🏭 [SiteCubit] ========== 切换场站 ==========');
debugPrint('🏭 [SiteCubit] 从 ${state.selectedSite?.siteName ?? "无"} 切换到 ${site.siteName}');
// 🔥 关键修复:只有切换到不同场站时才断开TCP
if (state.selectedSite?.id != site.id) {
final tcpClient = GetIt.I<TcpClient>();
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] 场站切换完成');
}
/// 清除选中场站

View File

@@ -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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
@@ -50,17 +39,10 @@ void main() async {
await FlutterPatcher.init();
await init();
final logger = sl<ILoggerService>();
await logger.init();
Bloc.observer = AppBlocObserver(logger);
// 🔥 确保 FloatBarSettingService 被初始化,这样 settingNotifier 才不会是 null
sl<FloatBarSettingService>();
debugPrint(
'🔍 [FloatBar] FloatBarSettingService 已初始化,settingNotifier: ${FloatBarSettingService.settingNotifier}',
);
// ⚠️ 注意:不要在启动时清除补丁版本记录!
// 补丁版本记录只在整包更新成功后才清除
// 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新
@@ -104,6 +86,9 @@ class MyApp extends StatelessWidget {
BlocProvider<UpdateCubit>(
create: (_) => UpdateCubit(VersionCheckService()),
),
BlocProvider<RemoteControlCubit>(
create: (_) => sl<RemoteControlCubit>(),
),
],
child: BlocBuilder<LocaleCubit, Locale>(
bloc: localeCubit,
@@ -121,23 +106,7 @@ class MyApp extends StatelessWidget {
theme: AppTheme.lightTheme,
routerConfig: sl<GoRouter>(),
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<bool>(
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<UpdateCubit>()
.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<UpdateCubit>()
.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('我知道了'),
),
],
),
);
});
}