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
|