分区相关

This commit is contained in:
2026-11-06 08:44:05 +08:00
parent 85c83ec143
commit 8eea42b75d
21 changed files with 3245 additions and 439 deletions

View File

@@ -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),