集成接口 开始执行任务的领域层和数据层和UI层的开发啊(待测试)
集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上 优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示! 优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。 优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。 调整了获取无人机状态信息的更新频率 为14秒一次
This commit is contained in:
213
docs/DEVICE_TASK_CUBIT_USAGE.md
Normal file
213
docs/DEVICE_TASK_CUBIT_USAGE.md
Normal 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秒后)
|
||||
- 页面不会崩溃
|
||||
|
||||
示例错误提示:
|
||||
- "网络连接超时,请检查网络设置"
|
||||
- "登录已过期,请重新登录"
|
||||
- "操作失败,请稍后重试"
|
||||
- "未知错误,请稍后重试"
|
||||
303
docs/ERROR_HANDLING_BEST_PRACTICE.md
Normal file
303
docs/ERROR_HANDLING_BEST_PRACTICE.md
Normal 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
388
docs/ERROR_HANDLING_FIX.md
Normal 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
|
||||
Reference in New Issue
Block a user