集成双 MQTT 客户端实现设备实时数据监控(未测试)
1.集成两个独立的 MQTT 客户端,支持 WebSocket 和 TCP 协议 2.实现无人机/机场 OSD 实时监控(ws://1.95.137.212:8083/mqtt) 3.实现任务状态消息订阅(tcp://1.95.137.212:59020) 4.采用清洁架构设计,分层清晰(Domain → Data → Presentation)
This commit is contained in:
@@ -36,6 +36,9 @@ import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_statu
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_task_entity.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_task_cubit.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_task_state.dart'; // 🔥 新增
|
||||
import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
@@ -1978,6 +1981,102 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 🔥 显示任务选择弹窗
|
||||
Future<DeviceTaskEntity?> _showTaskSelectionDialog(
|
||||
List<DeviceTaskEntity> tasks,
|
||||
) async {
|
||||
return showDialog<DeviceTaskEntity>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('选择任务'),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: tasks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final task = tasks[index];
|
||||
// 状态标签颜色
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
switch (task.taskStatus) {
|
||||
case 'NEW':
|
||||
statusColor = Colors.blue;
|
||||
statusText = '新建';
|
||||
break;
|
||||
case 'EXECUTING':
|
||||
statusColor = Colors.green;
|
||||
statusText = '执行中';
|
||||
break;
|
||||
case 'PAUSE':
|
||||
statusColor = Colors.orange;
|
||||
statusText = '暂停中';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = task.taskStatusTranslate;
|
||||
}
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text('任务 #${task.id}'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('设备号: ${task.deviceId}'),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (task.createTime != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
task.createTime!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(dialogContext, task);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, null),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 开始作业
|
||||
void _startWork() async {
|
||||
// 🔥 V2 适配:使用接口方式执行作业,不再使用 TCP
|
||||
@@ -2010,13 +2109,53 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 打印请求参数日志
|
||||
// 🔥 5. 先过滤出活跃任务,让用户选择
|
||||
debugPrint('🔍 [开始作业] 正在查询活跃任务...');
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
await taskCubit.fetchAndFilterTask(deviceId);
|
||||
|
||||
// 等待一下让状态更新
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final activeTasks = taskCubit.state.activeTasks;
|
||||
debugPrint('✅ [开始作业] 找到 ${activeTasks.length} 个活跃任务');
|
||||
|
||||
if (activeTasks.isEmpty) {
|
||||
_showPageToast(
|
||||
message: "当前设备没有活跃任务(新建/执行中/暂停中)",
|
||||
type: ToastType.warn,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔥 6. 如果有多条任务,显示选择弹窗
|
||||
DeviceTaskEntity? selectedTask;
|
||||
if (activeTasks.length > 1) {
|
||||
debugPrint('⚠️ [开始作业] 有多个活跃任务,显示选择弹窗');
|
||||
selectedTask = await _showTaskSelectionDialog(activeTasks);
|
||||
if (selectedTask == null) {
|
||||
// 用户取消选择
|
||||
debugPrint('❌ [开始作业] 用户取消选择任务');
|
||||
return;
|
||||
}
|
||||
// 用户选择了任务,更新 cubit
|
||||
taskCubit.selectTask(selectedTask);
|
||||
} else {
|
||||
// 只有1条任务,直接使用
|
||||
selectedTask = activeTasks.first;
|
||||
debugPrint('✅ [开始作业] 自动选择唯一任务 #${selectedTask.id}');
|
||||
}
|
||||
|
||||
debugPrint('📋 [开始作业] 最终选择的任务ID: ${selectedTask.id}, 状态: ${selectedTask.taskStatus}');
|
||||
|
||||
// 7. 打印请求参数日志
|
||||
debugPrint('🚀 [开始作业] 请求参数:');
|
||||
debugPrint(' ├─ deviceId: $deviceId');
|
||||
debugPrint(' ├─ routeId: $routeId');
|
||||
debugPrint(' ├─ taskId: ${selectedTask.id}');
|
||||
debugPrint(' └─ siteId: $siteId');
|
||||
|
||||
// 6. 更新UI状态
|
||||
// 8. 更新UI状态
|
||||
setState(() {
|
||||
isStopWork = false;
|
||||
isStartWork = true;
|
||||
@@ -2026,11 +2165,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
gctracePoint?.clear();
|
||||
});
|
||||
|
||||
// 7. 更新应用状态
|
||||
// 9. 更新应用状态
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
|
||||
try {
|
||||
// 8. 调用接口创建设备任务
|
||||
// 10. 调用接口创建设备任务
|
||||
final result = await sl<CreateDeviceTaskUseCase>().execute(
|
||||
deviceId: deviceId,
|
||||
routeId: routeId,
|
||||
@@ -2107,18 +2246,63 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
/// 暂停作业
|
||||
void _pauseWork() async {
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('⏸️ [暂停作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
setState(() {
|
||||
isStartWork = false; // 🔥 关键:停止作业标志
|
||||
_workStatus = WorkStatus.paused;
|
||||
});
|
||||
_saveDataToLocal(); // 🔥 保存暂停状态
|
||||
|
||||
// 🔥 调用接口暂停任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.pauseTask(deviceId);
|
||||
_showPageToast(message: "作业已暂停", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [暂停作业] 异常: $e');
|
||||
_showPageToast(message: "暂停失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//context.read<DevicesCubit>().updateAppState(AppState.none);
|
||||
await context.read<DevicesCubit>().pauseRoutePlanning();
|
||||
//await context.read<DevicesCubit>().pauseRoutePlanning();
|
||||
}
|
||||
|
||||
void _stopWork() async {
|
||||
_logger.log('按下停止按钮');
|
||||
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('⏹️ [停止作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isStartWork = false;
|
||||
@@ -2133,7 +2317,18 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
});
|
||||
}
|
||||
|
||||
await context.read<DevicesCubit>().stopRoutePlanning();
|
||||
// 🔥 调用接口取消任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.cancelTask(deviceId);
|
||||
_showPageToast(message: "作业已停止", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [停止作业] 异常: $e');
|
||||
_showPageToast(message: "停止失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//await context.read<DevicesCubit>().stopRoutePlanning();
|
||||
|
||||
_saveDataToLocal();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
if (mounted) {
|
||||
@@ -2152,12 +2347,40 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
/// 继续作业
|
||||
void _resumeWork() async {
|
||||
// 🔥 获取设备ID和taskId
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
final taskCubit = sl<DeviceTaskCubit>();
|
||||
final taskId = taskCubit.state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
_showPageToast(message: "无可用任务", type: ToastType.warn);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('▶️ [继续作业] deviceId: $deviceId, taskId: $taskId');
|
||||
|
||||
setState(() {
|
||||
_workStatus = WorkStatus.working;
|
||||
});
|
||||
_saveDataToLocal(); // 🔥 保存暂停状态
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
await context.read<DevicesCubit>().resumeRoutePlanning();
|
||||
|
||||
// 🔥 调用接口恢复任务(注释掉原有的 TCP 方式)
|
||||
try {
|
||||
await taskCubit.recoveryTask(deviceId);
|
||||
_showPageToast(message: "作业已恢复", type: ToastType.success);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [继续作业] 异常: $e');
|
||||
_showPageToast(message: "恢复失败: $e", type: ToastType.error);
|
||||
}
|
||||
|
||||
// 原有的 TCP 方式(已注释)
|
||||
//context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
//await context.read<DevicesCubit>().resumeRoutePlanning();
|
||||
}
|
||||
|
||||
void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) {
|
||||
@@ -2284,7 +2507,147 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 2. 错误提示(如果加载失败)
|
||||
// 🔥 3. 当前任务信息显示(可点击重新选择)
|
||||
BlocBuilder<DeviceTaskCubit, DeviceTaskState>(
|
||||
builder: (context, taskState) {
|
||||
final currentTask = taskState.currentTask;
|
||||
if (currentTask == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// 状态标签颜色
|
||||
Color statusColor;
|
||||
String statusText;
|
||||
switch (currentTask.taskStatus) {
|
||||
case 'NEW':
|
||||
statusColor = Colors.blue;
|
||||
statusText = '新建';
|
||||
break;
|
||||
case 'EXECUTING':
|
||||
statusColor = Colors.green;
|
||||
statusText = '执行中';
|
||||
break;
|
||||
case 'PAUSE':
|
||||
statusColor = Colors.orange;
|
||||
statusText = '暂停中';
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.grey;
|
||||
statusText = currentTask.taskStatusTranslate;
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
// 🔥 点击后显示所有活跃任务供用户选择
|
||||
final activeTasks = taskState.activeTasks;
|
||||
if (activeTasks.isEmpty) {
|
||||
_showPageToast(
|
||||
message: "没有可用的任务",
|
||||
type: ToastType.warn,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final selectedTask = await _showTaskSelectionDialog(
|
||||
activeTasks,
|
||||
);
|
||||
if (selectedTask != null) {
|
||||
// 用户选择了新任务,更新 cubit
|
||||
sl<DeviceTaskCubit>().selectTask(selectedTask);
|
||||
_showPageToast(
|
||||
message: "已切换到任务 #${selectedTask.id}",
|
||||
type: ToastType.success,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F7FA),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: const Color(0xFF165DFF).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.assignment_outlined,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'当前任务: #${currentTask.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'设备: ${currentTask.deviceId}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: statusColor,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.swap_horiz,
|
||||
color: const Color(0xFF165DFF),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
'切换',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF165DFF),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 4. 错误提示(如果加载失败)
|
||||
if (startWorkList.isEmpty || headingStatus == 0)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
Reference in New Issue
Block a user