集成接口 开始执行任务的领域层和数据层和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

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