分区相关
This commit is contained in:
@@ -1,54 +1,68 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/consts/http_api_consts.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../devices/domain/entities/device_task_entity.dart';
|
||||
import '../../../devices/data/models/work_record_entity.dart';
|
||||
import '../../../devices/domain/repositories/device_task_repository.dart';
|
||||
import '../../../devices/domain/usecases/get_work_records_by_site_id_usecase.dart';
|
||||
import '../../../devices/domain/usecases/select_work_record_usecase.dart';
|
||||
import '../v2_work_task_tracker.dart';
|
||||
import '../v2_work_store.dart';
|
||||
import '../../../devices/domain/usecases/create_device_task_usecase.dart';
|
||||
import '../../../devices/presentation/bloc/device_task_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../domain/entities/work_session_models.dart';
|
||||
import '../../domain/entities/return_plan_models.dart';
|
||||
import '../../domain/repositories/autonomous_work_gateway.dart';
|
||||
|
||||
/// A 环境实现:完全复用「路径规划」那条链路
|
||||
/// 作业记录 selectBySiteId → 任务池 getDeviceTaskPool → createDeviceTask →
|
||||
/// pause/recovery/cancel + MQTT arrive 监听。
|
||||
///
|
||||
/// 这里刻意复用全局的 [DevicesCubit] / [DeviceTaskCubit](均为 lazySingleton),
|
||||
/// 保证与路径规划页共享同一份任务状态与同一条推送流,不会出现"两边状态不一致"。
|
||||
class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
PathPlanningWorkGateway({
|
||||
DevicesCubit? devices,
|
||||
DeviceTaskCubit? tasks,
|
||||
CreateDeviceTaskUseCase? createTask,
|
||||
}) : _devices = devices ?? sl<DevicesCubit>(),
|
||||
_tasks = tasks ?? sl<DeviceTaskCubit>(),
|
||||
_createTask = createTask ?? sl<CreateDeviceTaskUseCase>();
|
||||
|
||||
final DevicesCubit _devices;
|
||||
final DeviceTaskCubit _tasks;
|
||||
final CreateDeviceTaskUseCase _createTask;
|
||||
/// 新版作业门面:沿用旧版协议和 HTTP 接口,独立维护任务状态与 MQTT 订阅。
|
||||
/// 不写旧版 Cubit 或缓存。
|
||||
class PathPlanningWorkGateway
|
||||
implements
|
||||
AutonomousWorkGateway,
|
||||
WorkTaskHistorySource,
|
||||
WorkTaskQueueSource {
|
||||
PathPlanningWorkGateway({V2WorkTaskTracker? tracker})
|
||||
: _injectedTracker = tracker;
|
||||
final V2WorkTaskTracker? _injectedTracker;
|
||||
V2WorkTaskTracker? _ownedTracker;
|
||||
String? _trackerOwner;
|
||||
V2WorkTaskTracker get _tracker {
|
||||
if (_injectedTracker != null) return _injectedTracker;
|
||||
final owner = sl<AppUserCubit>().state.user?.userId ?? '';
|
||||
if (_ownedTracker == null || _trackerOwner != owner) {
|
||||
_trackerOwner = owner;
|
||||
_ownedTracker = V2WorkTaskTracker(
|
||||
repository: sl<DeviceTaskRepository>(),
|
||||
createTask: sl<CreateDeviceTaskUseCase>(),
|
||||
resolveScope: (deviceId) => resolveScope(deviceId: deviceId),
|
||||
userId: () => sl<AppUserCubit>().state.user?.userId ?? '',
|
||||
store: V2WorkStore(owner),
|
||||
);
|
||||
}
|
||||
return _ownedTracker!;
|
||||
}
|
||||
|
||||
@override
|
||||
String get gatewayId => 'path_planning';
|
||||
|
||||
@override
|
||||
WorkSceneCapabilities get capabilities => const WorkSceneCapabilities(
|
||||
routeLibrary: true,
|
||||
taskQueue: true,
|
||||
pause: true,
|
||||
resume: true,
|
||||
stop: true,
|
||||
cancelQueued: true,
|
||||
schedule: false, // 定时计划后端未提供
|
||||
routeEditing: true, // 走 RouteEditorPageV2
|
||||
progress: false, // 任务池不返回进度,页面不臆造百分比
|
||||
multiActiveTaskResolve: true,
|
||||
);
|
||||
routeLibrary: true,
|
||||
taskQueue: true,
|
||||
pause: true,
|
||||
resume: true,
|
||||
stop: true,
|
||||
cancelQueued: true,
|
||||
schedule: false, // 定时计划走作业计划创建页(SaveDeviceSchedulePlanUseCase),不属本页能力
|
||||
routeEditing: true, // 走 RouteEditorPageV2
|
||||
progress: false, // 任务池不返回进度,页面不臆造百分比
|
||||
multiActiveTaskResolve: true,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<WorkScope> resolveScope({required String deviceId}) async {
|
||||
@@ -60,23 +74,39 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
Future<WorkResult<List<WorkRouteOption>>> loadRoutes({
|
||||
required int siteId,
|
||||
}) async {
|
||||
debugPrint('[路线库] A 环境 → /iot/workRecord/selectBySiteId?siteId=$siteId');
|
||||
try {
|
||||
await _devices.loadWorkRecordsBySiteId(siteId);
|
||||
final records =
|
||||
_devices.state.workRecords ?? const <Map<String, dynamic>>[];
|
||||
final routes = <WorkRouteOption>[];
|
||||
for (final record in records) {
|
||||
final option = _routeFromRecord(record);
|
||||
if (option != null) routes.add(option);
|
||||
}
|
||||
if (routes.isEmpty) {
|
||||
final error = _devices.state.errorMessage;
|
||||
return WorkResult.fail(
|
||||
(error != null && error.isNotEmpty) ? error : '该场站下暂无作业路线',
|
||||
);
|
||||
}
|
||||
return WorkResult.ok(routes);
|
||||
final result = await sl<GetWorkRecordsBySiteIdUseCase>()(siteId);
|
||||
return result.fold((failure) {
|
||||
debugPrint('[路线库] ← 接口失败: ${failure.message}');
|
||||
return WorkResult.fail(failure.message);
|
||||
}, (
|
||||
records,
|
||||
) {
|
||||
debugPrint('[路线库] ← 作业记录条数=${records.length}');
|
||||
final routes = <WorkRouteOption>[];
|
||||
for (final record in records) {
|
||||
final json = record.jsonData;
|
||||
final option = _routeFromRecord({
|
||||
'id': record.id,
|
||||
'workName': record.workName,
|
||||
'imgUrl': record.imgUrl,
|
||||
'jsonData': json is WorkRecordJsonData
|
||||
? {
|
||||
'path': json.path,
|
||||
'outer': json.outer,
|
||||
'planModel': json.planModel,
|
||||
'img': json.img,
|
||||
}
|
||||
: json,
|
||||
});
|
||||
if (option != null) routes.add(option);
|
||||
}
|
||||
debugPrint('[路线库] 解析出可绘制路线=${routes.length}');
|
||||
return WorkResult.ok(routes);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('[路线库] ❌ 加载异常: $e');
|
||||
return WorkResult.fail('路线库加载失败:$e');
|
||||
}
|
||||
}
|
||||
@@ -91,8 +121,13 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
try {
|
||||
// 与路径规划完全一致:按作业名回查记录详情,
|
||||
// selectBySiteId 的 jsonData 可能是空标签,真实 path/outer/planModel 在记录顶层
|
||||
await _devices.loadSelectedPath(route.name);
|
||||
final records = _devices.state.pathData;
|
||||
final result = await sl<SelectWorkRecordUseCase>()(route.name);
|
||||
String? error;
|
||||
final records = result.fold<List<Map<String, dynamic>>?>((failure) {
|
||||
error = failure.message;
|
||||
return null;
|
||||
}, (data) => data);
|
||||
if (error != null) return WorkResult.fail(error!);
|
||||
if (records == null || records.isEmpty) {
|
||||
return WorkResult.fail('「${route.name}」没有可用的路径数据');
|
||||
}
|
||||
@@ -118,21 +153,11 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
@override
|
||||
Future<WorkResult<List<WorkTaskSnapshot>>> loadTasks({
|
||||
required String deviceId,
|
||||
}) async {
|
||||
try {
|
||||
await _tasks.fetchAndFilterTask(deviceId);
|
||||
final error = _tasks.state.errorMessage;
|
||||
final tasks = _tasks.state.activeTasks
|
||||
.map((task) => _snapshotFromEntity(task, deviceId))
|
||||
.toList();
|
||||
if (tasks.isEmpty && error != null && error.isNotEmpty) {
|
||||
return WorkResult.fail(error);
|
||||
}
|
||||
return WorkResult.ok(tasks);
|
||||
} catch (e) {
|
||||
return WorkResult.fail('任务队列加载失败:$e');
|
||||
}
|
||||
}
|
||||
}) => _tracker.load(deviceId);
|
||||
@override
|
||||
Future<WorkResult<List<WorkTaskSnapshot>>> loadTaskHistory({
|
||||
required String deviceId,
|
||||
}) async => WorkResult.ok(await _tracker.history(deviceId));
|
||||
|
||||
/// 1:1 复刻路径规划「保存地块」:multipart 提交 /iot/workRecord/add。
|
||||
///
|
||||
@@ -146,6 +171,7 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
required List<LatLng> path,
|
||||
required List<LatLng> outer,
|
||||
required int planModel,
|
||||
required String routeType,
|
||||
String? imageBase64,
|
||||
}) async {
|
||||
// 场站与 orgId 必须同源(selectBySiteId 的查询条件),不要取登录用户的 orgId
|
||||
@@ -156,7 +182,8 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
}
|
||||
final orgId = site?.orgId;
|
||||
|
||||
List<Map<String, double>> toWgs(List<LatLng> points) => <Map<String, double>>[
|
||||
List<Map<String, double>> toWgs(List<LatLng> points) =>
|
||||
<Map<String, double>>[
|
||||
for (final point in points)
|
||||
{'lng': point.longitude, 'lat': point.latitude},
|
||||
];
|
||||
@@ -174,8 +201,12 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
// jsonData 必须是对象,不能是字符串(后端 PlanPath 类型要求)
|
||||
'jsonData': savePath,
|
||||
'mode': coverage ? 'coverage' : 'single_point',
|
||||
// 后端必填的规划类型(Java 枚举 RouteType.name()),与 mode 平级
|
||||
'routeType': routeType,
|
||||
};
|
||||
if (orgId != null) workRecord['orgId'] = orgId;
|
||||
// 与路径规划页一致:打印后端实际接收的完整报文,便于排查字段缺失
|
||||
debugPrint('[区域保存] workRecord 完整JSON: ${jsonEncode(workRecord)}');
|
||||
|
||||
final http.MultipartRequest request = http.MultipartRequest(
|
||||
'POST',
|
||||
@@ -233,236 +264,279 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
}
|
||||
final code = resp['code'];
|
||||
if (code != null && code.toString() != '200') {
|
||||
return WorkResult.fail(
|
||||
'${resp['msg'] ?? resp['message'] ?? '保存失败'}',
|
||||
);
|
||||
return WorkResult.fail('${resp['msg'] ?? resp['message'] ?? '保存失败'}');
|
||||
}
|
||||
// 与路径规划一致:保存成功后刷新该场站的作业记录
|
||||
await _devices.loadWorkRecordsBySiteId(siteId);
|
||||
return const WorkResult<bool>.ok(true);
|
||||
} catch (e) {
|
||||
return WorkResult.fail('保存失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 一键返航方案落库:POST /system/siteMapInfo/save(JSON)。
|
||||
/// 严格对齐后端字段:restPoints / tempObstaclePoints / siteId / orgId / id。
|
||||
@override
|
||||
Stream<WorkTaskSnapshot?> watchTask({required String deviceId}) {
|
||||
return _tasks.stream.map((state) {
|
||||
final task = state.currentTask;
|
||||
if (task == null) return null;
|
||||
if (task.deviceId.isNotEmpty && task.deviceId != deviceId) return null;
|
||||
return _snapshotFromEntity(task, deviceId);
|
||||
}).distinct((a, b) {
|
||||
if (a == null || b == null) return a == b;
|
||||
return a.taskId == b.taskId &&
|
||||
a.rawStatus == b.rawStatus &&
|
||||
a.state == b.state;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> startWork(
|
||||
WorkStartRequest request,
|
||||
) async {
|
||||
final scope = await resolveScope(deviceId: request.deviceId);
|
||||
final siteId = request.siteId ?? scope.siteId;
|
||||
final orgId = request.orgId ?? scope.orgId ?? 0;
|
||||
if (siteId == null) {
|
||||
return WorkResult.fail('未选择场站,无法下发作业');
|
||||
Future<WorkResult<int?>> saveReturnPlan(ReturnPlanDraft draft) async {
|
||||
if (draft.isEmpty) {
|
||||
return const WorkResult.fail('没有可保存的返航点或障碍');
|
||||
}
|
||||
|
||||
// ① 复用设备上已有的活跃任务
|
||||
final reuseId = request.reuseTaskId;
|
||||
if (reuseId != null) {
|
||||
final entity = _findEntity(reuseId);
|
||||
if (entity != null) {
|
||||
_tasks.selectTask(entity);
|
||||
await _devices.startListeningMqttArrive(
|
||||
deviceId: request.deviceId,
|
||||
taskId: entity.id,
|
||||
Map<String, double> wgs(LatLng p) =>
|
||||
<String, double>{'lat': p.latitude, 'lng': p.longitude};
|
||||
|
||||
// 尺寸归一化:米级宽度从经纬度几何反算会带 1e-9 量级的浮点残渣
|
||||
// (如 3.0000000009815784),后端字段是普通 double,这里抹到微米精度(1e-6)
|
||||
// 即可去掉残渣,又不丢失用户真实录入的尺寸。
|
||||
double normM(double v) => (v * 1e6).roundToDouble() / 1e6;
|
||||
|
||||
final Map<String, dynamic> payload = <String, dynamic>{
|
||||
'restPoints': <Map<String, dynamic>>[
|
||||
for (final p in draft.restPoints)
|
||||
<String, dynamic>{
|
||||
'position': wgs(p.position),
|
||||
'bindDeviceId': p.bindDeviceId,
|
||||
'alias': p.alias,
|
||||
},
|
||||
],
|
||||
'tempObstaclePoints': <Map<String, dynamic>>[
|
||||
for (final o in draft.obstacles)
|
||||
<String, dynamic>{
|
||||
'center': wgs(o.center),
|
||||
// 后端要求 position 与 center 一致
|
||||
'position': wgs(o.center),
|
||||
'shapeType': o.shapeType,
|
||||
'alias': o.alias,
|
||||
'source': o.source,
|
||||
'size_x': normM(o.sizeX),
|
||||
'size_y': normM(o.sizeY),
|
||||
},
|
||||
],
|
||||
'siteId': draft.siteId,
|
||||
'orgId': draft.orgId,
|
||||
// 新增不传 id、修改才传:只有拿到真实主键(上次保存回写)才带上走更新
|
||||
if (draft.id != null) 'id': draft.id,
|
||||
};
|
||||
|
||||
final String url = '${HttpApiConsts.baseUrl}/system/siteMapInfo/save';
|
||||
// 提交前先打印真实发出的报文,便于排查后端字段/坐标问题
|
||||
debugPrint('[一键返航] → POST $url');
|
||||
debugPrint('[一键返航] 提交数据: ${jsonEncode(payload)}');
|
||||
|
||||
final http.Request request = http.Request('POST', Uri.parse(url))
|
||||
..headers['Content-Type'] = 'application/json;charset=utf-8';
|
||||
final userToken = sl<AppUserCubit>().state.user?.token;
|
||||
if (userToken != null) {
|
||||
request.headers['Authorization'] = 'Bearer $userToken';
|
||||
}
|
||||
request.body = jsonEncode(payload);
|
||||
|
||||
try {
|
||||
final http.StreamedResponse response = await request.send();
|
||||
final String body = await response.stream.bytesToString();
|
||||
debugPrint(
|
||||
'[一键返航] ← HTTP ${response.statusCode} 响应: $body',
|
||||
);
|
||||
if (response.statusCode != 200) {
|
||||
return WorkResult.fail(
|
||||
'保存失败(HTTP ${response.statusCode}):$body',
|
||||
);
|
||||
return WorkResult.ok(_snapshotFromEntity(entity, request.deviceId));
|
||||
}
|
||||
Map<String, dynamic>? resp;
|
||||
try {
|
||||
resp = jsonDecode(body) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
return WorkResult.fail('保存失败:响应体非 JSON,$body');
|
||||
}
|
||||
final code = resp['code'];
|
||||
if (code != null && code.toString() != '200') {
|
||||
return WorkResult.fail('${resp['msg'] ?? resp['message'] ?? '保存失败'}');
|
||||
}
|
||||
// 若后端回写主键则回填,用于下一次「更新」;否则 null(每次新建)
|
||||
final data = resp['data'];
|
||||
final int? savedId = data is num
|
||||
? data.toInt()
|
||||
: int.tryParse('${data ?? ''}');
|
||||
debugPrint('[一键返航] ✅ 保存成功,siteMapInfo id=$savedId');
|
||||
return WorkResult<int?>.ok(savedId);
|
||||
} catch (e) {
|
||||
debugPrint('[一键返航] ❌ 保存异常: $e');
|
||||
return WorkResult.fail('保存失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询某场站已有的一键返航方案:GET /system/siteMapInfo/selectBySiteId?siteId=X。
|
||||
///
|
||||
/// 一个场站业务上只允许一条,data 为带完整几何的单体(也可能为 null=无方案)。
|
||||
/// 返回结构已确认为 {code,msg,data:{id,siteId,orgId,restPoints[],tempObstaclePoints[]}},
|
||||
/// 解析仍保留少量字段别名做容错,坐标/尺寸缺失则跳过该条、不臆造。
|
||||
@override
|
||||
Future<WorkResult<ReturnPlanDraft?>> fetchReturnPlanBySiteId({
|
||||
required int siteId,
|
||||
}) async {
|
||||
final String url =
|
||||
'${HttpApiConsts.baseUrl}/system/siteMapInfo/selectBySiteId?siteId=$siteId';
|
||||
debugPrint('[一键返航] → GET $url');
|
||||
try {
|
||||
final http.Response res = await http.get(
|
||||
Uri.parse(url),
|
||||
headers: _returnAuthHeaders(),
|
||||
);
|
||||
debugPrint(
|
||||
'[一键返航] ← HTTP ${res.statusCode} selectBySiteId 响应: ${res.body}',
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
return WorkResult.fail('返航方案加载失败(HTTP ${res.statusCode})');
|
||||
}
|
||||
dynamic decoded;
|
||||
try {
|
||||
decoded = jsonDecode(res.body);
|
||||
} catch (_) {
|
||||
return WorkResult.fail('返航方案加载失败:响应体非 JSON');
|
||||
}
|
||||
if (decoded is! Map) {
|
||||
return WorkResult.fail('返航方案响应格式异常');
|
||||
}
|
||||
final code = decoded['code'];
|
||||
if (code != null && code.toString() != '200') {
|
||||
return WorkResult.fail(
|
||||
'${decoded['msg'] ?? decoded['message'] ?? '请求失败'}',
|
||||
);
|
||||
}
|
||||
final data = decoded['data'];
|
||||
if (data is! Map || data.isEmpty) {
|
||||
// 该场站暂无一键返航方案 → 新建态
|
||||
return WorkResult<ReturnPlanDraft?>.ok(null);
|
||||
}
|
||||
return WorkResult<ReturnPlanDraft?>.ok(
|
||||
_returnDraftFromJson(Map<String, dynamic>.from(data)),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[一键返航] ❌ 查询异常: $e');
|
||||
return WorkResult.fail('返航方案加载失败:$e');
|
||||
}
|
||||
}
|
||||
|
||||
/// GET 头:Content-Type + Bearer token(与保存接口同源)
|
||||
Map<String, String> _returnAuthHeaders() {
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
};
|
||||
final token = sl<AppUserCubit>().state.user?.token;
|
||||
if (token != null) headers['Authorization'] = 'Bearer $token';
|
||||
return headers;
|
||||
}
|
||||
|
||||
/// selectBySiteId 的 data 对象 → [ReturnPlanDraft]。
|
||||
/// 字段名对齐后端:restPoints.position{lat,lng}/alias/bindDeviceId,
|
||||
/// tempObstaclePoints.center{lat,lng}/size_x/size_y/source。
|
||||
ReturnPlanDraft _returnDraftFromJson(Map<String, dynamic> m) {
|
||||
LatLng? parseLatLng(dynamic node) {
|
||||
if (node is! Map) return null;
|
||||
final lat = node['lat'] ?? node['latitude'];
|
||||
final lng = node['lng'] ?? node['lon'] ?? node['longitude'];
|
||||
if (lat is! num || lng is! num) return null;
|
||||
return LatLng(lat.toDouble(), lng.toDouble());
|
||||
}
|
||||
|
||||
// ② 新建任务
|
||||
final routeId = int.tryParse(request.route.id);
|
||||
if (routeId == null) {
|
||||
return WorkResult.fail('路线 ID 非法:${request.route.id}');
|
||||
double asSize(dynamic v) {
|
||||
if (v is num) return v.toDouble();
|
||||
return double.tryParse('${v ?? ''}') ?? 0;
|
||||
}
|
||||
|
||||
final created = await _createTask.execute(
|
||||
deviceId: request.deviceId,
|
||||
routeId: routeId,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
);
|
||||
|
||||
String? failure;
|
||||
final taskId = created.fold<int?>((f) {
|
||||
failure = f.message;
|
||||
return null;
|
||||
}, (id) => id);
|
||||
if (taskId == null) {
|
||||
return WorkResult.fail(failure ?? '任务下发失败');
|
||||
int asId(dynamic v) {
|
||||
if (v is num) return v.toInt();
|
||||
return int.tryParse('${v ?? ''}') ?? 0;
|
||||
}
|
||||
|
||||
await _devices.startListeningMqttArrive(
|
||||
deviceId: request.deviceId,
|
||||
taskId: taskId,
|
||||
);
|
||||
|
||||
// 刷新任务池拿真实状态;拿不到就返回本地快照,交给 watchTask 后续校正
|
||||
await _tasks.fetchAndFilterTask(request.deviceId);
|
||||
final entity = _findEntity('$taskId');
|
||||
if (entity != null) {
|
||||
_tasks.selectTask(entity);
|
||||
return WorkResult.ok(_snapshotFromEntity(entity, request.deviceId));
|
||||
final List<dynamic> restRaw =
|
||||
m['restPoints'] is List ? m['restPoints'] as List : const <dynamic>[];
|
||||
final restPoints = <ReturnRestPoint>[];
|
||||
for (final e in restRaw) {
|
||||
if (e is! Map) continue;
|
||||
final pos =
|
||||
parseLatLng(e['position']) ??
|
||||
parseLatLng(e['center']) ??
|
||||
parseLatLng(e);
|
||||
if (pos == null) continue;
|
||||
final alias = (e['alias'] ?? e['name'] ?? '').toString();
|
||||
final bind =
|
||||
(e['bindDeviceId'] ?? e['deviceSn'] ?? e['deviceId'])?.toString();
|
||||
restPoints.add(
|
||||
ReturnRestPoint(
|
||||
position: pos,
|
||||
alias: alias,
|
||||
bindDeviceId: (bind != null && bind.isNotEmpty) ? bind : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
_tasks.updateCurrentTaskId(taskId);
|
||||
return WorkResult.ok(
|
||||
WorkTaskSnapshot(
|
||||
taskId: '$taskId',
|
||||
deviceId: request.deviceId,
|
||||
state: WorkRunState.queued,
|
||||
routeId: request.route.id,
|
||||
routeName: request.route.name,
|
||||
rawStatus: 'NEW',
|
||||
),
|
||||
|
||||
final List<dynamic> obsRaw = m['tempObstaclePoints'] is List
|
||||
? m['tempObstaclePoints'] as List
|
||||
: const <dynamic>[];
|
||||
final obstacles = <ReturnObstaclePoint>[];
|
||||
for (final e in obsRaw) {
|
||||
if (e is! Map) continue;
|
||||
final center =
|
||||
parseLatLng(e['center']) ??
|
||||
parseLatLng(e['position']) ??
|
||||
parseLatLng(e);
|
||||
if (center == null) continue;
|
||||
obstacles.add(
|
||||
ReturnObstaclePoint(
|
||||
center: center,
|
||||
alias: (e['alias'] ?? e['name'] ?? '').toString(),
|
||||
sizeX: asSize(e['size_x'] ?? e['sizeX'] ?? e['width']),
|
||||
sizeY: asSize(e['size_y'] ?? e['sizeY'] ?? e['height']),
|
||||
source: (e['source'] ?? 'platform').toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ReturnPlanDraft(
|
||||
siteId: asId(m['siteId']),
|
||||
orgId: asId(m['orgId']),
|
||||
id: m['id'] == null ? null : asId(m['id']),
|
||||
restPoints: restPoints,
|
||||
obstacles: obstacles,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<WorkTaskSnapshot?> watchTask({required String deviceId}) =>
|
||||
_tracker.watch(deviceId);
|
||||
@override
|
||||
Stream<WorkTaskSnapshot?> watchTaskChanges({required String deviceId}) =>
|
||||
_tracker.watch(deviceId, allTasks: true);
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> startWork(WorkStartRequest request) =>
|
||||
_tracker.start(request);
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> pauseWork({
|
||||
required String deviceId,
|
||||
required WorkTaskSnapshot task,
|
||||
}) async {
|
||||
_bindTask(task);
|
||||
await _tasks.pauseTask(deviceId);
|
||||
final error = _tasks.state.errorMessage;
|
||||
if (error != null && error.isNotEmpty) return WorkResult.fail(error);
|
||||
return WorkResult.ok(
|
||||
task.copyWith(state: WorkRunState.paused, rawStatus: 'PAUSE'),
|
||||
);
|
||||
}
|
||||
|
||||
}) => _tracker.command(deviceId, task, 'pause');
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> resumeWork({
|
||||
required String deviceId,
|
||||
required WorkTaskSnapshot task,
|
||||
}) async {
|
||||
_bindTask(task);
|
||||
await _tasks.recoveryTask(deviceId);
|
||||
final error = _tasks.state.errorMessage;
|
||||
if (error != null && error.isNotEmpty) return WorkResult.fail(error);
|
||||
return WorkResult.ok(
|
||||
task.copyWith(state: WorkRunState.running, rawStatus: 'EXECUTING'),
|
||||
);
|
||||
}
|
||||
|
||||
}) => _tracker.command(deviceId, task, 'resume');
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> stopWork({
|
||||
required String deviceId,
|
||||
required WorkTaskSnapshot task,
|
||||
}) async {
|
||||
_bindTask(task);
|
||||
await _tasks.cancelTask(deviceId, forceCancel: true);
|
||||
final error = _tasks.state.errorMessage;
|
||||
if (error != null && error.isNotEmpty) return WorkResult.fail(error);
|
||||
await _devices.stopListeningMqttArrive();
|
||||
return WorkResult.ok(
|
||||
task.copyWith(state: WorkRunState.finished, rawStatus: 'CANCELED'),
|
||||
);
|
||||
}
|
||||
|
||||
}) => _tracker.command(deviceId, task, 'cancel');
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> cancelQueued({
|
||||
required String deviceId,
|
||||
required WorkTaskSnapshot task,
|
||||
}) async {
|
||||
_bindTask(task);
|
||||
await _tasks.cancelTask(deviceId, forceCancel: false);
|
||||
final error = _tasks.state.errorMessage;
|
||||
if (error != null && error.isNotEmpty) return WorkResult.fail(error);
|
||||
_tasks.clearCurrentTask();
|
||||
return WorkResult.ok(
|
||||
task.copyWith(state: WorkRunState.finished, rawStatus: 'CANCELED'),
|
||||
);
|
||||
}
|
||||
|
||||
}) => _tracker.command(deviceId, task, 'cancel', forceCancel: false);
|
||||
@override
|
||||
Future<WorkResult<WorkTaskSnapshot>> activateTask({
|
||||
required String deviceId,
|
||||
required WorkTaskSnapshot task,
|
||||
}) async {
|
||||
final entity = _findEntity(task.taskId);
|
||||
if (entity == null) {
|
||||
return WorkResult.fail('该任务已不在任务池中,请刷新后重试');
|
||||
}
|
||||
_tasks.selectTask(entity);
|
||||
await _devices.startListeningMqttArrive(
|
||||
deviceId: deviceId,
|
||||
taskId: entity.id,
|
||||
);
|
||||
return WorkResult.ok(_snapshotFromEntity(entity, deviceId));
|
||||
}
|
||||
|
||||
// ===== 内部:状态映射 / 记录解析 =====
|
||||
|
||||
/// 让 DeviceTaskCubit 的操作作用在页面选中的那条任务上
|
||||
void _bindTask(WorkTaskSnapshot task) {
|
||||
final entity = _findEntity(task.taskId);
|
||||
if (entity != null) {
|
||||
_tasks.selectTask(entity);
|
||||
return;
|
||||
}
|
||||
final id = int.tryParse(task.taskId);
|
||||
if (id != null) _tasks.updateCurrentTaskId(id);
|
||||
}
|
||||
|
||||
DeviceTaskEntity? _findEntity(String taskId) {
|
||||
for (final entity in _tasks.state.activeTasks) {
|
||||
if ('${entity.id}' == taskId) return entity;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
WorkTaskSnapshot _snapshotFromEntity(
|
||||
DeviceTaskEntity task,
|
||||
String deviceId,
|
||||
) {
|
||||
return WorkTaskSnapshot(
|
||||
taskId: '${task.id}',
|
||||
deviceId: task.deviceId.isEmpty ? deviceId : task.deviceId,
|
||||
state: _stateFromStatus(task.taskStatus),
|
||||
routeId: task.routeId?.toString(),
|
||||
rawStatus: task.taskStatus,
|
||||
createdAt: task.createTime,
|
||||
extras: <String, dynamic>{
|
||||
'routeId': task.routeId,
|
||||
'siteId': task.siteId,
|
||||
'orgId': task.orgId,
|
||||
'statusText': task.taskStatusTranslate,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 后端状态码 → 统一运行态。新增状态码只改这里。
|
||||
WorkRunState _stateFromStatus(String status) {
|
||||
switch (status) {
|
||||
case 'NEW':
|
||||
return WorkRunState.queued;
|
||||
case 'EXECUTING':
|
||||
return WorkRunState.running;
|
||||
case 'PAUSE':
|
||||
return WorkRunState.paused;
|
||||
case 'FAILED':
|
||||
return WorkRunState.failed;
|
||||
case 'CANCELING':
|
||||
return WorkRunState.finished;
|
||||
default:
|
||||
return WorkRunState.queued;
|
||||
}
|
||||
if (task.deviceId != deviceId) return const WorkResult.fail('设备不匹配');
|
||||
return _tracker.select(task);
|
||||
}
|
||||
|
||||
/// 作业记录 → 路线选项:字段解析集中在这里,
|
||||
@@ -494,8 +568,9 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
// 真实 path/outer/planModel 也可能直接挂在记录顶层,两处都读。
|
||||
final path = _points(data['path'] ?? record['path']);
|
||||
var outer = _points(data['outer'] ?? record['outer']);
|
||||
final planModel =
|
||||
int.tryParse('${data['planModel'] ?? record['planModel'] ?? ''}');
|
||||
final planModel = int.tryParse(
|
||||
'${data['planModel'] ?? record['planModel'] ?? ''}',
|
||||
);
|
||||
final imgUrl = record['imgUrl']?.toString() ?? '';
|
||||
|
||||
// 后端可能返回 outer 经度全为 0 的脏数据:覆盖模式下用 path 兜底,
|
||||
@@ -517,8 +592,9 @@ class PathPlanningWorkGateway implements AutonomousWorkGateway {
|
||||
pathPoints: path,
|
||||
outerPoints: outer,
|
||||
coverageMode: coverageMode,
|
||||
planModeLabel:
|
||||
planModel == null ? null : (coverageMode ? '覆盖模式' : '自定义模式'),
|
||||
planModeLabel: planModel == null
|
||||
? null
|
||||
: (coverageMode ? '覆盖模式' : '自定义模式'),
|
||||
imageUrl: imgUrl.isNotEmpty
|
||||
? imgUrl
|
||||
: (data['img'] is String ? data['img'] as String : null),
|
||||
|
||||
Reference in New Issue
Block a user