集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上 优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示! 优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。 优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。 调整了获取无人机状态信息的更新频率 为14秒一次
389 lines
10 KiB
Markdown
389 lines
10 KiB
Markdown
# 统一错误处理修复记录
|
||
|
||
## 问题描述
|
||
|
||
之前项目中存在多个地方直接将原始错误信息(如 `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
|