import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:math'; import 'dart:math' as math; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:geolocator/geolocator.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:latlong2/latlong.dart'; import 'package:maibu_satabot_v2/components/confrim_dialog.dart'; import 'package:maibu_satabot_v2/components/toast.dart'; import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart'; import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart'; import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart'; import 'package:maibu_satabot_v2/core/router/route_paths.dart'; import 'package:maibu_satabot_v2/features/devices/data/models/device_add_path_point_model.dart' as work_area_model; import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_param_model.dart' as work_area_model; import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_work_record_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/create_device_task_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_event.dart'; 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'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart'; import 'package:maibu_satabot_v2/components/input_confirm_dialog.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/dji_tile_layer.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/obsToast.dart'; import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart'; import 'package:image/image.dart' as _image; // 注意命名空间冲突,使用 as img import 'package:maibu_satabot_v2/features/home/presentation/widgets/video.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:ui' as ui; import '../path_list_pages.dart'; import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart'; // 定义轨迹点类型(经纬度) typedef PlotPoint = LatLng; // 本地存储Key常量 const String kSavedPlotData = 'saved_plot_data'; const String kSavedGcjPathPoints = 'saved_gcj_path_points'; const String kSavedGcjOuterPoints = 'saved_gcj_outer_points'; const String kSavedMarkedPoints = 'saved_marked_points'; const String kSavedObstacleHoles = 'saved_obstacle_holes'; const String kSavedIsWorkAreaCompleted = 'saved_is_work_area_completed'; const String kSavedRobotModeWgsPoints = "saved_robot_mode_wgs_points"; const String kSavedIsWorkPanelOpen = "saved_is_work_panel_open"; const String kSavedSelectedPlot = 'saved_selected_plot'; const String kSavedStartWorkList = 'saved_start_work_list'; const String kSavedWorkMode = 'saved_work_mode'; const String kSavedWorkStatus = 'saved_work_status'; // 作业状态(idle/working/paused) const String kSavedIsStartWork = 'saved_is_start_work'; // 是否开始作业 const String kSavedIsStopWork = 'saved_is_stop_work'; // 是否点击过停止 const String kSavedTPMode = "saved_tp_mode"; const String kSavedCurrentRobotMode = "kSavedCurrentRobotMode"; const String kSavedIsPanelOpen = "kSavedIsPanelOpen"; const String kSavedCurrentWorkMode = "kSavedCurrentWorkMode"; // 保持 PlotData 类不变 class PlotData { final String id; final String plotName; final String imageUrl; // 图片URL(本地/assets/网络都可) final String? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用) PlotData({ required this.id, required this.plotName, required this.imageUrl, this.jsonData, }); } class PlotDataPath { final dynamic? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用) PlotDataPath({this.jsonData}); } class _ToastItem { final String message; final ToastType type; _ToastItem({required this.message, required this.type}); } class MapPageEnterprise extends StatefulWidget { const MapPageEnterprise({Key? key}) : super(key: key); @override State createState() => _MapPageEnterpriseState(); } class _MapPageEnterpriseState extends State { final ILoggerService _logger = GetIt.I(); final _dispatcher = sl(); bool _isRefreshing = false; // 新增:页面刷新状态标志 bool _isVideoDialogOpen = false; // 控制视频弹窗显示 String _videoStreamUrl = ""; bool isStopWork = false; //是否点击过停止 bool isreceiveFirstCompletePoint = false; Offset _videoPopupPos = const Offset(0, 100); // 弹窗初始位置 final GlobalKey _mapRepaintKey = GlobalKey(); // 用于存储当前页面的 Toast OverlayEntry List _toastEntries = []; // Toast 队列:存储待显示的消息 final _toastQueue = <_ToastItem>[]; // 是否正在显示 Toast bool _isToastShowing = false; final MapController _mapController = MapController(); // 初始化轨迹管理器(泛型指定为LatLng) late final TracePoint _traceManager; late String workMode = "弓字模式"; // 作业模式:默认值为"弓字模式" WorkStatus _workStatus = WorkStatus.idle; bool isStartWork = false; //是否开始作业 LatLng? _currentLatLng; //gc格式 LatLng? _currentWgsLatLng; bool _isPanelOpen = false; // 控制面板显示/隐藏 bool _directionBoxOpen = false; //航线面板显示/隐藏 bool _saveBoxOpen = false; //保存按钮显示/隐藏 bool _isListBoxOpen = false; //列表面板显示/隐藏 bool _showHeadingWarn = false; // 航向角未初始化提示 bool _showControlModeWarn = false; // 远程模式提示 // 新增:存储选中的地块(null表示未选中) PlotData? _selectedPlot; // 新增:控制底部作业面板显示 bool _isWorkPanelOpen = false; bool _isWorkAreaCompleted = false; RobotMode _currentRobotMode = RobotMode.robot; //打点模式 AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域 WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式 // 新增:存储转换后的path和outer坐标 List gcjPathPoints = []; List gcjOuterPoints = []; List startWorkList = []; List? tracePoint = []; //从tracepoint中获取的经纬度坐标 List? gctracePoint = []; // ========== 核心新增状态 ========== List _markedPoints = []; // 存储所有s十字准星打点坐标 List _robotModeWgsPoints = []; // Robot模式下从设备获取的wgs打点 List _robotModeObsWgsPoints = []; // Robot模式下从设备获取的wgs打点 障碍物 LatLng _mapCenter = const LatLng(39.9042, 116.4074); double _headingAngle = 0.0; // 当前机器航向角(单位:度) List typedPathList = []; // 存储生成路径的坐标列表(已转换为LatLng) // ========== 障碍物模式核心状态 ========== List> _obstacleHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物) List> _obstacleWgsHoles = []; // 存储多组障碍物打点(嵌套数组:每组是一个障碍物) List _currentObstaclePoints = []; // 当前正在绘制的障碍物打点 bool _isObstacleEditing = false; // 是否处于障碍物编辑状态 //打点 double _workDistance = 0.0; // 作业行距(单位:米) double _angle = 0.0; // 航线方向角(单位:度) // 大疆地图 bool isDjMapShow = false; late DJIStation _currentStation; // 所有可用的场站列表(根据你的实际瓦片目录配置) final List _stationList = [ const DJIStation(name: 'liaoning', displayName: 'station_1'), const DJIStation(name: 'station_b', displayName: 'station_2'), const DJIStation(name: 'station_c', displayName: 'station_3'), // 新增场站只需在这里添加,无需修改其他逻辑 ]; // 其他 StreamSubscription? _pathPlanningSub; @override void initState() { super.initState(); debugPrint('🚀 [MapPage] initState 开始'); _currentStation = _stationList.first; _traceManager = TracePoint(); // 🔥 修复:异步加载数据,避免阻塞 _loadSavedData().catchError((e) { debugPrint('❌ [MapPage] 加载本地数据失败: $e'); }); // 示例:切换到导航模式 _traceManager.setMode(TPMode.LOCATION); tracePoint = []; gctracePoint = []; // 🔥 核心修复:使用 addPostFrameCallback 延迟获取地图中心(渲染完成后执行) WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; debugPrint('🗺️ [MapPage] addPostFrameCallback 执行'); if (_mapController.camera != null) { final originalCenter = _mapController.camera!.center; final highPrecisionCenter = LatLng( originalCenter.latitude, originalCenter.longitude, ); setState(() { _mapCenter = highPrecisionCenter; }); } else { // 兜底:使用默认坐标初始化地图中心 final defaultCenter = _getDefaultValidCenter(); setState(() { _mapCenter = defaultCenter; }); } }); // 🔥 关键修复:开始监听路径规划指令应答 _setupPathPlanningListener(); // 监听地图移动事件,实时更新连线 _mapController.mapEventStream.listen((event) { if (event is MapEventMove || event is MapEventMoveEnd) { if (mounted && _mapController.camera != null) { final originalCenter = _mapController.camera!.center; final highPrecisionCenter = LatLng( originalCenter.latitude, originalCenter.longitude, ); setState(() { _mapCenter = highPrecisionCenter; }); } } }); debugPrint('✅ [MapPage] initState 完成'); } // 🔥 核心修复:监听路径规划应答流 void _setupPathPlanningListener() { debugPrint('🔗 [MapPage] 开始监听路径规划指令应答 (CMD: 0x01)'); _pathPlanningSub = _dispatcher.onPathPlanningResponse().listen( (packet) { debugPrint('✅ [MapPage] 收到路径规划应答,准备发送下一个点'); // 这里不需要做任何事情,因为 NetMessageDispatcher 内部已经处理了 // 它会自动从队列中移除已发送的点并触发下一个点 }, onDone: () => debugPrint('⚠️ [MapPage] 路径规划流已结束'), onError: (e) => debugPrint('❌ [MapPage] 路径规划流错误:$e'), ); } // 新增:手动清除本地存储数据 Future _clearLocalData() async { try { await LocationUtils.clearCache(LocationUtils.kSavedGcjPathPoints); await LocationUtils.clearCache(LocationUtils.kSavedGcjOuterPoints); await LocationUtils.clearCache(LocationUtils.kSavedMarkedPoints); await LocationUtils.clearCache(LocationUtils.kSavedObstacleHoles); await LocationUtils.clearCache(LocationUtils.kSavedIsWorkAreaCompleted); final prefs = await SharedPreferences.getInstance(); await prefs.remove(kSavedWorkStatus); await prefs.remove(kSavedTPMode); await prefs.remove(kSavedCurrentRobotMode); // 机器人模式 await prefs.remove("kSavedTypedPathList"); // 生成的路径 await prefs.remove(kSavedIsStartWork); await prefs.remove(kSavedIsStopWork); await prefs.remove(kSavedGcjPathPoints); // 路径点 await prefs.remove(kSavedGcjOuterPoints); // 外边框 await prefs.remove(kSavedMarkedPoints); // 打点 await prefs.remove(kSavedObstacleHoles); // 障碍物 await prefs.remove(kSavedIsWorkAreaCompleted); await prefs.remove(kSavedIsWorkPanelOpen); await prefs.remove(kSavedWorkMode); await prefs.remove(kSavedStartWorkList); await prefs.remove(kSavedSelectedPlot); await prefs.remove(kSavedIsPanelOpen); await prefs.remove(kSavedCurrentWorkMode); } catch (e) { debugPrint('清空本地数据失败:$e'); } } // 新增:加载本地存储数据方法 Future _loadSavedData() async { try { _traceManager.setMode(TPMode.LOCATION); final prefs = await SharedPreferences.getInstance(); final workStatusStr = prefs.getString(kSavedWorkStatus); if (workStatusStr != null) { _workStatus = WorkStatus.values.firstWhere( (e) => e.toString() == workStatusStr, orElse: () => WorkStatus.idle, ); } final modeStr = prefs.getString(kSavedTPMode); if (modeStr != null) { _traceManager.setMode( TPMode.values.firstWhere((e) => e.toString() == modeStr), ); } final robotModeStr = prefs.getString(kSavedCurrentRobotMode); if (robotModeStr != null) { _currentRobotMode = RobotMode.values.firstWhere( (e) => e.toString() == robotModeStr, orElse: () => RobotMode.point, ); } // 读取 生成完成的路径 typedPathList final typedPathString = prefs.getString("kSavedTypedPathList"); if (typedPathString != null) { final List typedPathJson = jsonDecode(typedPathString); setState(() { typedPathList = typedPathJson.map((p) { return work_area_model.DeviceAddPathPointModel( latitude: p['lat'], longitude: p['lng'], ); }).toList(); }); } // 2. 恢复是否开始作业 isStartWork = prefs.getBool(kSavedIsStartWork) ?? false; // 3. 恢复是否点击过停止 isStopWork = prefs.getBool(kSavedIsStopWork) ?? false; _isPanelOpen = prefs.getBool(kSavedIsPanelOpen) ?? false; // 加载路径点 final pathJson = prefs.getString(kSavedGcjPathPoints); if (pathJson != null) { final List pathList = jsonDecode(pathJson); gcjPathPoints = pathList .map( (item) => LatLng( double.parse(item['lat'].toString()), double.parse(item['lng'].toString()), ), ) .toList(); } // 加载外边界点 final outerJson = prefs.getString(kSavedGcjOuterPoints); if (outerJson != null) { final List outerList = jsonDecode(outerJson); gcjOuterPoints = outerList .map( (item) => LatLng( double.parse(item['lat'].toString()), double.parse(item['lng'].toString()), ), ) .toList(); } // 加载打点数据 final markedJson = prefs.getString(kSavedMarkedPoints); if (markedJson != null) { final List markedList = jsonDecode(markedJson); _markedPoints = markedList .map( (item) => LatLng( double.parse(item['lat'].toString()), double.parse(item['lng'].toString()), ), ) .toList(); } // 加载机器人模式WGS打点 final robotWgsJson = prefs.getString(kSavedRobotModeWgsPoints); if (robotWgsJson != null) { final List robotWgsList = jsonDecode(robotWgsJson); _robotModeWgsPoints = robotWgsList .map( (item) => LatLng( double.parse(item['lat'].toString()), double.parse(item['lng'].toString()), ), ) .toList(); } // 加载障碍物数据 final obstacleJson = prefs.getString(kSavedObstacleHoles); if (obstacleJson != null) { final List obstacleList = jsonDecode(obstacleJson); _obstacleHoles = obstacleList.map((hole) { return (hole as List) .map( (item) => LatLng( double.parse(item['lat'].toString()), double.parse(item['lng'].toString()), ), ) .toList(); }).toList(); } // 加载作业区域完成状态 _isWorkAreaCompleted = prefs.getBool(kSavedIsWorkAreaCompleted) ?? false; _saveBoxOpen = _isWorkAreaCompleted; // 1. 恢复作业面板状态 _isWorkPanelOpen = prefs.getBool(kSavedIsWorkPanelOpen) ?? false; // 2. 恢复作业模式 workMode = prefs.getString(kSavedWorkMode) ?? "弓字模式"; // 读取 _currentWorkMode = prefs.getString(kSavedCurrentWorkMode) != null ? WorkMode.values.firstWhere( (e) => e.toString() == prefs.getString(kSavedCurrentWorkMode), orElse: () => WorkMode.bow, ) : WorkMode.bow; // 3. 恢复作业列表 final startWorkJson = prefs.getString(kSavedStartWorkList); if (startWorkJson != null) { startWorkList = jsonDecode(startWorkJson); } // 4. 恢复选中地块 final selectedPlotJson = prefs.getString(kSavedSelectedPlot); if (selectedPlotJson != null && selectedPlotJson.isNotEmpty) { final Map plotMap = jsonDecode(selectedPlotJson); // 兼容 jsonData 为 Map 或 String String? cachedJsonData; final rawCached = plotMap['jsonData']; if (rawCached is String) { cachedJsonData = rawCached; } else if (rawCached is Map) { cachedJsonData = jsonEncode(rawCached); } _selectedPlot = PlotData( id: plotMap['id'] ?? '', plotName: plotMap['plotName'] ?? '', imageUrl: plotMap['imageUrl'] ?? '', jsonData: cachedJsonData, ); } // ==================== ✅ 核心修复 ==================== // 1. 从缓存恢复后,强制让 _markedPoints 同步给外边框(解决不显示) if (_markedPoints.isNotEmpty && gcjOuterPoints.isEmpty) { gcjOuterPoints = List.from(_markedPoints); } // 2. 关键:同步机器人模式坐标(保存时边框不丢失) if (_currentRobotMode == RobotMode.point && _robotModeWgsPoints.isEmpty) { _robotModeWgsPoints = _markedPoints.map((p) { return gcj02ToWgs84(p.latitude, p.longitude); }).toList(); } // 兜底:Robot模式下_robotModeWgsPoints缓存未写入时,从_markedPoints恢复 if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isEmpty && _markedPoints.isNotEmpty) { _robotModeWgsPoints = _markedPoints.map((p) { return gcj02ToWgs84(p.latitude, p.longitude); }).toList(); } if (mounted) { setState(() {}); // 移动地图到绘制内容中心 List allPoints = []; allPoints.addAll(gcjPathPoints); allPoints.addAll(gcjOuterPoints); if (allPoints.isNotEmpty) { moveMapToPointsCenter(allPoints); } } } catch (e) { debugPrint('加载本地数据失败:$e'); } } // 新增:保存绘制数据到本地 Future _saveDataToLocal() async { try { final prefs = await SharedPreferences.getInstance(); // 保存路径点 final pathList = gcjPathPoints .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); prefs.setString(kSavedGcjPathPoints, jsonEncode(pathList)); // 保存外边界点 final outerList = gcjOuterPoints .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); prefs.setString(kSavedGcjOuterPoints, jsonEncode(outerList)); // 保存打点数据 final markedList = _markedPoints .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); prefs.setString(kSavedMarkedPoints, jsonEncode(markedList)); // 保存机器人模式WGS打点 final robotWgsList = _robotModeWgsPoints .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); prefs.setString(kSavedRobotModeWgsPoints, jsonEncode(robotWgsList)); // 保存障碍物数据 final obstacleList = _obstacleHoles.map((hole) { return hole .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); }).toList(); prefs.setString(kSavedObstacleHoles, jsonEncode(obstacleList)); final typedPath = typedPathList .map((point) => {'lat': point.latitude, 'lng': point.longitude}) .toList(); prefs.setString("kSavedTypedPathList", jsonEncode(typedPath)); // 保存作业区域完成状态 prefs.setBool(kSavedIsWorkAreaCompleted, _isWorkAreaCompleted); prefs.setBool(kSavedIsPanelOpen, _isPanelOpen); // 1. 保存作业面板显示状态 prefs.setBool(kSavedIsWorkPanelOpen, _isWorkPanelOpen); // 2. 保存作业模式 prefs.setString(kSavedWorkMode, workMode); // 3. 保存作业列表 prefs.setString(kSavedStartWorkList, jsonEncode(startWorkList)); prefs.setString(kSavedWorkStatus, _workStatus.toString()); // 2. 保存是否开始作业 prefs.setBool(kSavedIsStartWork, isStartWork); // 3. 保存是否点击过停止 prefs.setBool(kSavedIsStopWork, isStopWork); prefs.setString(kSavedCurrentWorkMode, _currentWorkMode.toString()); // 保存当前模式到本地缓存 prefs.setString(kSavedTPMode, _traceManager.getMode().toString()); prefs.setString(kSavedCurrentRobotMode, _currentRobotMode.toString()); // 4. 保存选中地块 if (_selectedPlot != null) { final plotMap = { 'id': _selectedPlot!.id, 'plotName': _selectedPlot!.plotName, 'imageUrl': _selectedPlot!.imageUrl, 'jsonData': _selectedPlot!.jsonData, }; prefs.setString(kSavedSelectedPlot, jsonEncode(plotMap)); } else { prefs.remove(kSavedSelectedPlot); } } catch (e) { debugPrint('保存本地数据失败:$e'); } } // ========== 新增:计算坐标列表的边界范围 ========== LatLngBounds? calculateBounds(List points) { if (points.isEmpty) return null; // 使用静态方法 fromPoints 创建边界(所有latlong2版本都支持) return LatLngBounds.fromPoints(points); } void moveMapToPointsCenter(List points) { if (points.isEmpty || !mounted) return; // 计算边界 final bounds = calculateBounds(points); if (bounds == null) return; final currentZoom = _mapController.camera?.zoom ?? 18.0; _mapController.move( bounds.center, // 边界中心点 currentZoom, // 保留当前缩放比例 ); print('地图已移动到绘制内容中心,边界范围:$bounds'); } // ========== 新增:安全转换任意类型为double ========== double? safeToDouble(dynamic value) { if (value == null) return null; if (value is double) return value; if (value is int) return value.toDouble(); if (value is String) { return double.tryParse(value); // 字符串转数字(失败返回null) } return null; } bool __isValidLatLng(LatLng? latLng) { if (latLng == null) return false; // 排除赤道0,0坐标,同时校验经纬度范围(避免非法值) return latLng.latitude != 0.0 && latLng.longitude != 0.0 && latLng.latitude >= -90 && latLng.latitude <= 90 && latLng.longitude >= -180 && latLng.longitude <= 180; } bool _isValidLatLng(double lat, double lng) { return lat != 0 && lng != 0 && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180; } void _onDeviceData( double lng, double lat, bool obfFlag, int headingStatus, dynamic controlMode, ) { try { // 1. 校验坐标是否有效 bool isValid = _isValidLatLng(lat, lng); // ============================================== // 🔥 关键:无效坐标只打印,不 return,不断流 // ============================================== if (!isValid) { debugPrint('[定位] 坐标无效:lat=$lat, lng=$lng → 跳过本次定位更新'); // 不写 return!!! } // ============================================== // 2. 航向角提示(不受坐标影响,必须执行) // ============================================== if (isStartWork && !isStopWork) { if (headingStatus == 0) { if (!_showHeadingWarn) { if (mounted) { setState(() { _showHeadingWarn = true; }); } } } else { if (_showHeadingWarn && mounted) { setState(() { _showHeadingWarn = false; }); } } // ============================================== // 3. 远程模式提示(不受坐标影响,必须执行) // ============================================== String mode = controlMode?.toString() ?? ""; if (mode != "3") { if (!_showControlModeWarn && mounted) { setState(() { _showControlModeWarn = true; }); } } else { if (_showControlModeWarn && mounted) { setState(() { _showControlModeWarn = false; }); } } } else { // 未作业 → 自动隐藏所有警告 if ((_showHeadingWarn || _showControlModeWarn) && mounted) { setState(() { _showHeadingWarn = false; _showControlModeWarn = false; }); } } // ============================================== // 4. 障碍物提示(可选,你自己开关) // ============================================== // if (isStartWork && obfFlag) { // ObsToastWidget.show(context: context, message: "小迈提醒您前方有障碍物哦!"); // } else { // ObsToastWidget.dismiss(); // } } catch (e) { debugPrint('[定位] 处理设备数据异常:$e'); } } void _showSavePlotDialog() { final loc = AppLocalizations.of(context); showInputConfirmDialog( context: context, title: loc.translate('route_planning.save_plot'), // 自定义标题 hintText: loc.translate('route_planning.plot_name_hint'), // 自定义输入提示 labelText: loc.translate('route_planning.plot_name_label'), // 自定义输入框标签 confirmText: loc.translate('route_planning.save'), // 确认按钮文字 cancelText: loc.translate('route_planning.cancel'), // 取消按钮文字 // 输入校验器(可选) inputValidator: (inputText) { if (inputText.isEmpty) { return loc.translate('route_planning.plot_name_empty'); } if (inputText.length > 20) { return loc.translate('route_planning.plot_name_too_long'); } return null; // 校验通过 }, onConfirm: (plotName) async { // 1. 关闭输入弹窗,显示截图加载中 Navigator.pop(context); showDialog( context: context, barrierDismissible: false, // 禁止点击外部关闭 builder: (ctx) => const Center( child: SizedBox( width: 60, height: 60, child: CircularProgressIndicator( strokeWidth: 3, color: Colors.white, ), ), ), ); // 2. 执行截图(核心调用) String? imgBase64 = await _captureMapToJpgBase64(quality: 80); // 4. 处理截图结果并保存 if (mounted) { if (imgBase64 != null) { _savePlotData(plotName, imgBase64); // 传入 Base64 } else { // 截图失败时,允许空图片保存(可根据业务调整为强制失败) _savePlotData(plotName, null); _showPageToast( message: loc.translate('route_planning.save_success') + "," + "但地图截图生成失败!", type: ToastType.warn, ); //ToastUtils.showWarn(context, '地块保存成功,但地图截图生成失败!'); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber)); } } }, ); } void _debugPrintMultipartRequest(http.MultipartRequest request) { debugPrint('===================================='); debugPrint('📡 请求 URL: ${request.url}'); debugPrint('📡 请求方法: ${request.method}'); // 1. 打印请求头(包含自动生成的 boundary) debugPrint('📡 请求头: ${request.headers}'); // 2. 打印普通字段 (虽然我们现在没有普通字段,但保留以防万一) if (request.fields.isNotEmpty) { debugPrint('📝 普通字段:'); request.fields.forEach((key, value) { debugPrint(' - $key: $value'); }); } else { debugPrint('📝 普通字段: 无'); } // 3. 打印文件/Blob 字段 (核心:这里包含 file 和 workRecord) debugPrint('📁 上传文件/Blob 数量: ${request.files.length}'); for (var file in request.files) { // 特殊处理:如果是 workRecord,打印其内容 if (file.field == 'workRecord' && file is http.MultipartFile) { // 将 ByteStream 转为 Uint8List 以查看内容 file.finalize().first.then((bytes) { String content = utf8.decode(bytes); // 格式化 JSON 打印,更易读 try { dynamic jsonObj = jsonDecode(content); debugPrint( ' 内容 (content): ${const JsonEncoder.withIndent(' ').convert(jsonObj)}', ); } catch (e) { debugPrint(' 内容 (content): $content'); } }); } else { // 对于图片文件,打印大小即可,避免打印海量 Base64 file.finalize().first.then((bytes) { debugPrint( ' 文件大小: ${bytes.length} 字节 (约 ${(bytes.length / 1024).toStringAsFixed(1)} KB)', ); }); } } } Future _savePlotData(String plotName, String? imgBase64) async { debugPrint('🔍 [保存地块] ====== _savePlotData 被调用, plotName=$plotName ======'); final loc = AppLocalizations.of(context); // 🔥 V2 适配:使用 SiteCubit 获取站点ID(从 v2 首页场站选择获取,有默认值) final siteId = sl().state.selectedSite?.id; if (siteId == null) { debugPrint('❌ [保存地块] siteId为空,提前返回!'); _showPageToast(message: '请先在场站列表中选择一个场站!', type: ToastType.error); return; } for (var i = 0; i < typedPathList.length; i++) { debugPrint(typedPathList[i].toString()); } // 2. 构造 SavePath 数据 (对齐 Web 端逻辑) // 🔥 Web 端参考: // 弓字模式: {path: API返回路径, outer: 打点坐标, planModel: 0} // 自定义模式: {path: 打点坐标, outer: [], planModel: 2} // 辅助方法:将打点转为 WGS84 格式 {lng, lat} List> _markedPointsToWgs() { if (_currentRobotMode == RobotMode.robot) { return _robotModeWgsPoints .map((p) => {'lng': p.longitude, 'lat': p.latitude}) .toList(); } return _markedPoints.map((point) { final wgs84 = gcj02ToWgs84(point.latitude, point.longitude); return {'lng': wgs84.longitude, 'lat': wgs84.latitude}; }).toList(); } final Map savePath = { //'img': imgBase64 ?? '', // 截图的 Base64 字符串 // 'name': plotName, // 旧逻辑:Web 端不包含 name // 🔥 对齐 Web 端:弓字模式 outer=打点,自定义模式 outer=[] // 旧逻辑:outer 始终取 gcjOuterPoints 'outer': _currentWorkMode == WorkMode.custom ? [] // 自定义模式 outer 为空 : _markedPointsToWgs(), // 弓字模式 outer 为打点坐标 // 🔥 对齐 Web 端:弓字模式 path=API返回,自定义模式 path=打点 // 旧逻辑:自定义模式 path 为空数组 'path': _currentWorkMode == WorkMode.custom ? _markedPointsToWgs() // 自定义模式 path 为打点坐标 : typedPathList // 弓字模式 path 为 API 返回的路径 .map((point) => {'lng': point.longitude, 'lat': point.latitude}) .toList(), 'planModel': _currentWorkMode == WorkMode.bow ? 0 : 2, // 作业模式值 }; // 3. 构造 workRecord (包裹一层) final Map workRecord = { 'workName': plotName, 'siteId': siteId, // 修改:使用 siteId 代替 userId // 🔥 jsonData 必须是对象,不能是字符串(后端 PlanPath 类型要求) 'jsonData': savePath, }; final String workRecordJson = jsonEncode(workRecord); final http.MultipartRequest request = http.MultipartRequest( 'POST', Uri.parse('http://1.95.137.212:8081/iot/workRecord/add'), ); // 🔥 添加 Authorization 认证头 final userToken = sl().state.user?.token; if (userToken != null) { request.headers['Authorization'] = 'Bearer $userToken'; debugPrint('📤 [保存地块] Authorization 已设置'); } else { debugPrint('⚠️ [保存地块] Token为空,请求可能被拒绝'); } // 5. 处理文件:将 Base64 转为 MultipartFile (对应 JS 的 dataURLtoFile) // 🔥 后端要求 file 字段必须存在,即使没有截图也要传 if (imgBase64 != null && imgBase64.isNotEmpty) { // 移除 Base64 头部 (如果有的话) String base64String = imgBase64; if (base64String.startsWith('data:image/jpeg;base64,')) { base64String = base64String.split(',').last; } Uint8List bytes = base64Decode(base64String); // 添加文件 (对应 formData.append("file", file)) request.files.add( http.MultipartFile.fromBytes( 'file', bytes, filename: 'image.jpg', contentType: http.MediaType('image', 'jpeg'), ), ); } else { // 🔥 没有截图时,也要传一个空的 image/jpeg 文件,满足后端要求 final emptyImageBytes = []; // 空字节数组 request.files.add( http.MultipartFile.fromBytes( 'file', emptyImageBytes, filename: 'empty.jpg', contentType: http.MediaType('image', 'jpeg'), ), ); } // 6. 添加 workRecord (对应 formData.append("workRecord", Blob)) // 🔥 必须用 fromBytes 包装成文件,匹配前端 new Blob([JSON.stringify(workRecord)], {type: "application/json"}) final workRecordBytes = utf8.encode(workRecordJson); request.files.add( http.MultipartFile.fromBytes( 'workRecord', workRecordBytes, filename: '', // 关键:和浏览器 Blob 保持一致,文件名填空字符串 contentType: http.MediaType('application', 'json'), ), ); debugPrint('🔍 [保存地块] 构造请求完成,即将进入try发送...'); // 7. 发送请求 try { // debugPrint('📤 [保存地块] ========== 请求参数 BEGIN =========='); // debugPrint('📤 [保存地块] plotName: $plotName'); // debugPrint('📤 [保存地块] siteId: $siteId'); // debugPrint('📤 [保存地块] workMode: $_currentWorkMode'); // debugPrint('📤 [保存地块] planModel: ${savePath['planModel']}'); // debugPrint('📤 [保存地块] savePath.name: ${savePath['name']}'); // debugPrint('📤 [保存地块] path长度: ${(savePath['path'] as List?)?.length ?? 0}'); // debugPrint('📤 [保存地块] outer长度: ${(savePath['outer'] as List?)?.length ?? 0}'); // debugPrint('📤 [保存地块] 是否有图片文件: ${imgBase64 != null && imgBase64.isNotEmpty}'); // debugPrint('📤 [保存地块] 请求URL: http://1.95.137.212:8081/iot/workRecord/add'); // debugPrint('📤 [保存地块] workRecord(完整JSON): $workRecordJson'); // debugPrint('📤 [保存地块] savePath(原始对象): $savePath'); // debugPrint('📤 [保存地块] ========== 请求参数 END =========='); _showPageToast( message: loc.translate('route_planning.saving') + "「$plotName」...", type: ToastType.loading, ); //ToastUtils.showLoading(context, '正在保存地块「$plotName」...'); //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('正在保存地块...'), backgroundColor: Colors.blue)); final http.StreamedResponse response = await request.send(); final String responseBody = await response.stream.bytesToString(); debugPrint('📥 [保存地块] 响应状态码: ${response.statusCode}'); debugPrint('📥 [保存地块] 响应体(完整): $responseBody'); if (response.statusCode == 200) { // 🔥 解析响应体确认后端是否真的成功 Map? respJson; try { respJson = jsonDecode(responseBody) as Map; } catch (_) { debugPrint('❌ [保存地块] 响应体非JSON: $responseBody'); throw Exception('响应体非JSON: $responseBody'); } final apiCode = respJson['code']; if (apiCode != null && apiCode.toString() != '200') { final apiMsg = respJson['msg'] ?? respJson['message'] ?? '未知错误'; debugPrint('❌ [保存地块] 后端返回失败, code=$apiCode, msg=$apiMsg'); throw Exception('$apiMsg'); } debugPrint('✅ [保存地块] 保存成功, plotName=$plotName, 响应: $respJson'); // 🔥 刷新地块列表 debugPrint('🔄 [保存地块] 刷新地块列表, siteId=$siteId'); context.read().loadWorkRecordsBySiteId(siteId); _showPageToast( message: loc.translate('route_planning.save_success') + "「$plotName」", type: ToastType.success, ); //ToastUtils.showSuccess(context, '地块「$plotName」保存成功!'); _clearLocalData(); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green)); // 保存成功 setState(() { _markedPoints.clear(); // 清空打点 gcjPathPoints.clear(); // 清空路径 gcjOuterPoints.clear(); // 清空外边界 _robotModeWgsPoints.clear(); _obstacleHoles.clear(); _isPanelOpen = false; // 关闭操作面板 _directionBoxOpen = false; _isWorkAreaCompleted = false; // 重置作业区域完成状态 }); } else { throw Exception('服务器错误: ${response.statusCode}, $responseBody'); } } catch (e) { debugPrint('保存失败: $e'); _showPageToast( message: loc.translate('route_planning.save_failed') + ": ${e.toString()}", type: ToastType.error, ); //ToastUtils.showError(context, '保存失败: ${e.toString()}'); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red)); } } List _getPolygonPoints() { if (_markedPoints.isEmpty || _mapController.camera == null) return []; List polygonPoints = List.from(_markedPoints); polygonPoints.add(_mapCenter); return polygonPoints; } // ========== 统一撤回方法(适配完成后状态) ========== void _undoAction() { debugPrint( '🔄 [撤回] 当前区域模式: $_currentAreaMode, 作业点数: ${_markedPoints.length}, 障碍物点数: ${_currentObstaclePoints.length}, 已完成空洞: ${_obstacleHoles.length}', ); setState(() { // 1. 作业区域模式 if (_currentAreaMode == AreaMode.work) { if (_markedPoints.isNotEmpty) { // 单独判断,单独删除 if (_robotModeWgsPoints.isNotEmpty) { _robotModeWgsPoints.removeLast(); } if (_markedPoints.isNotEmpty) { _markedPoints.removeLast(); } // 作业点撤回后,若路径已生成,重新生成路径(保持同步) if (_isWorkAreaCompleted && _markedPoints.length >= 3) { _generatePath(showTips: false); } _showPageToast( message: "已撤回作业区域最后一个点,剩余${_markedPoints.length}个点", type: ToastType.info, ); } else { final loc = AppLocalizations.of(context); _showPageToast( message: loc.translate('route_planning.no_work_points'), type: ToastType.info, ); } return; } // 2. 空洞模式(完成后优先撤回已完成的空洞) if (_currentAreaMode == AreaMode.obstacle) { if (_currentObstaclePoints.isNotEmpty) { // 单独判断,单独删除 if (_currentObstaclePoints.isNotEmpty) { _currentObstaclePoints.removeLast(); } if (_robotModeObsWgsPoints.isNotEmpty) { _robotModeObsWgsPoints.removeLast(); } if (_currentObstaclePoints.isEmpty) { _isObstacleEditing = false; } _showPageToast( message: "已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点", type: ToastType.info, ); return; } // 第二步:当前空洞无点可撤 → 再撤回「上一个完整空洞」 if (_obstacleHoles.isNotEmpty) { _obstacleHoles.removeLast(); // 撤完空洞后重新生成路径(排除该空洞) if (_isWorkAreaCompleted) { _generatePath(showTips: false); } _showPageToast( message: "已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞", type: ToastType.info, ); return; } // 第三步:无任何可撤内容 _showPageToast(message: "暂无空洞可撤回", type: ToastType.info); } }); _saveDataToLocal(); } // ========== 统一删除方法(适配完成后状态) ========== void _deleteAllAction() { debugPrint( '🗑️ [删除全部] 当前区域模式: $_currentAreaMode, 作业点数: ${_markedPoints.length}, 障碍物点数: ${_currentObstaclePoints.length}, 已完成空洞: ${_obstacleHoles.length}', ); setState(() { // 1. 作业区域模式:清空作业点 + 重置路径 if (_currentAreaMode == AreaMode.work) { if (_markedPoints.isEmpty) { _showPageToast(message: "暂无作业区域点可删除", type: ToastType.info); //ToastUtils.showInfo(context, '暂无作业区域点可删除'); //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除'))); return; } _markedPoints.clear(); gcjPathPoints.clear(); gcjOuterPoints.clear(); _robotModeWgsPoints.clear(); _obstacleHoles.clear(); _isWorkAreaCompleted = false; _saveBoxOpen = false; typedPathList.clear(); final loc = AppLocalizations.of(context); _showPageToast( message: loc.translate('route_planning.clear_work_area'), type: ToastType.success, ); //ToastUtils.showSuccess(context, '已清空所有作业区域点和路径'); //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已清空所有作业区域点和路径'))); } // 2. 空洞模式:清空所有空洞 + 恢复UI + 重新生成路径 else if (_currentAreaMode == AreaMode.obstacle) { if (_obstacleHoles.isEmpty && _currentObstaclePoints.isEmpty) { final loc = AppLocalizations.of(context); _showPageToast( message: loc.translate('route_planning.no_obstacles'), type: ToastType.info, ); //ToastUtils.showInfo(context, '暂无障碍物可删除'); return; } _obstacleHoles.clear(); _currentObstaclePoints.clear(); _robotModeObsWgsPoints.clear(); _isObstacleEditing = false; // 清空后重新生成路径(无空洞) if (_isWorkAreaCompleted) { _generatePath(showTips: false); } final loc = AppLocalizations.of(context); _showPageToast( message: loc.translate('route_planning.clear_obstacles'), type: ToastType.success, ); //ToastUtils.showSuccess(context, '已清空所有障碍物'); } }); _saveDataToLocal(); } void _handleGotoLocation() { debugPrint('📍 [定位] 跳转到设备当前位置: $_currentLatLng'); _mapController.move(_currentLatLng!, 18); } /// =============================== /// 企业级定位初始化(已修复坐标系) Future _initLocationEnterprise() async { // 1. 设置轨迹模式为 LOCATION(核心:确保模式正确) _traceManager.setMode(TPMode.LOCATION); } // ========== 新增:页面刷新初始化方法 ========== Future _handleRefresh() async { debugPrint('🔄 [刷新] 开始重置页面状态...'); if (_workStatus != WorkStatus.idle) { _stopWork(); } _cancelAllToast(); if (_isRefreshing) return; setState(() { _isRefreshing = true; }); try { if (_currentLatLng != null && _isValidLatLng(_currentLatLng!.latitude, _currentLatLng!.longitude)) { _mapController.move(_currentLatLng!, 18); } // ========================================== // 🔥 1. 清空 所有本地缓存 // ========================================== await _clearLocalData(); // ========================================== // 🔥 2. 清空 所有内存状态(彻底重置) // ========================================== setState(() { // 轨迹彻底清空(关键!) tracePoint?.clear(); gctracePoint?.clear(); tracePoint = []; gctracePoint = []; // 所有绘制数据清空 _markedPoints.clear(); gcjPathPoints.clear(); gcjOuterPoints.clear(); _robotModeWgsPoints.clear(); _robotModeObsWgsPoints.clear(); _obstacleHoles.clear(); _currentObstaclePoints.clear(); // 路径清空 typedPathList.clear(); startWorkList.clear(); // 状态重置 _isWorkAreaCompleted = false; _saveBoxOpen = false; _isPanelOpen = false; _directionBoxOpen = false; _isListBoxOpen = false; _isWorkPanelOpen = false; _isVideoDialogOpen = false; _isObstacleEditing = false; _showControlModeWarn = false; // 模式重置 _currentRobotMode = RobotMode.robot; _currentAreaMode = AreaMode.work; _currentWorkMode = WorkMode.bow; // 作业状态重置 _workStatus = WorkStatus.idle; isStartWork = false; isStopWork = false; isreceiveFirstCompletePoint = false; _showHeadingWarn = false; _isPanelOpen = false; _directionBoxOpen = false; _isListBoxOpen = false; _isWorkPanelOpen = false; _isVideoDialogOpen = false; _isObstacleEditing = false; _selectedPlot = null; // 重置模式 _currentRobotMode = RobotMode.robot; _currentAreaMode = AreaMode.work; _currentWorkMode = WorkMode.bow; // 重置参数 _workDistance = 0.0; _angle = 0.0; // 定位重置 _currentLatLng = null; _currentWgsLatLng = null; _mapCenter = _getDefaultValidCenter(); isDjMapShow = false; _workStatus = WorkStatus.idle; isStartWork = false; isStopWork = false; // ========================================== // 🔥 🔥 🔥 核心修复:强制重置轨迹管理器 + 强制改为 LOCATION 模式 // ========================================== _traceManager.reset(); _traceManager.setMode(TPMode.LOCATION); }); // ========================================== // 🔥 3. 保存空状态 // ========================================== await _saveDataToLocal(); if (mounted) { _showPageToast( message: AppLocalizations.of( context, ).translate('route_planning.clear_cache_success'), type: ToastType.success, ); } } catch (e) { if (mounted) { _showPageToast( message: "${AppLocalizations.of(context).translate('route_planning.refresh_failed')}:${e.toString().substring(0, 50)}", type: ToastType.error, ); } } finally { if (mounted) { setState(() { _isRefreshing = false; }); } } } /// 手动回到当前位置 void _moveToCurrentLocation() { if (_currentLatLng == null) return; debugPrint('📍 [定位] 移动到当前位置: $_currentLatLng'); _mapController.move(_currentLatLng!, 17); } // ========== 障碍物模式:完成当前障碍物绘制 ========== void _completeObstacle() { debugPrint( '✅ [完成障碍物] 当前障碍物点数: ${_currentObstaclePoints.length}, 已有障碍物区域: ${_obstacleHoles.length}', ); if (_currentObstaclePoints.length < 3) { _showPageToast(message: "障碍物区域至少需要3个打点!", type: ToastType.error); //ToastUtils.showError(context, '障碍物区域至少需要3个打点!'); return; } setState(() { // 保存当前障碍物组(Robot模式下直接用WGS84坐标) if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) { final gcjPoints = _robotModeObsWgsPoints .map((wgs) => wgs84ToGcj02(wgs.latitude, wgs.longitude)) .toList(); _obstacleHoles.add(gcjPoints); _obstacleWgsHoles.add(List.from(_robotModeObsWgsPoints)); } else { _obstacleHoles.add(List.from(_currentObstaclePoints)); } _isObstacleEditing = false; // 退出编辑状态 }); _saveDataToLocal(); _showPageToast( message: "已添加第${_obstacleHoles.length}个障碍物区域", type: ToastType.success, ); //ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域'); } // ========== 核心方法:打点逻辑 ========== void _addMarkedPoint() { debugPrint( '📍 [打点] 机器人模式: $_currentRobotMode, 区域模式: $_currentAreaMode, 作业已完成: $_isWorkAreaCompleted, 当前作业点数: ${_markedPoints.length}', ); _traceManager.reset(); if (_currentAreaMode == AreaMode.work && _isWorkAreaCompleted) { _showPageToast(message: "作业区域已完成,无法继续添加打点!", type: ToastType.error); //ToastUtils.showError(context, '作业区域已完成,无法继续添加打点!'); return; } setState(() { if (_currentRobotMode == RobotMode.point) { if (_currentAreaMode == AreaMode.work) { _markedPoints.add(_mapCenter); debugPrint( '新增作业区域打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}', ); } else { if (_markedPoints.isEmpty) { _showPageToast(message: "请先完成作业区域打点!", type: ToastType.info); return; } // 障碍物模式:添加到当前障碍物打点 _currentObstaclePoints.add(_mapCenter); _isObstacleEditing = true; // 标记进入障碍物编辑状态 debugPrint( '新增障碍物打点:第${_currentObstaclePoints.length}个点,经纬度:${_mapCenter.latitude}, ${_mapCenter.longitude}', ); } } else { if (_currentLatLng == null || !_isValidLatLng( _currentLatLng!.latitude, _currentLatLng!.longitude, )) { _showPageToast(message: "设备坐标无效,无法添加打点!", type: ToastType.error); //ToastUtils.showError(context, '设备坐标无效,无法添加打点!'); return; } // 2. Robot模式-作业区域打点 if (_currentAreaMode == AreaMode.work) { _markedPoints.add(_currentLatLng!); // 从设备实时坐标取最后一个值 _robotModeWgsPoints.add(_currentWgsLatLng!); debugPrint( 'Robot模式新增作业打点:第${_markedPoints.length}个点,经纬度:${_currentLatLng!.latitude}, ${_currentLatLng!.longitude}', ); } else { if (_markedPoints.isEmpty || _robotModeWgsPoints.isEmpty) { _showPageToast(message: "请先完成作业区域打点!", type: ToastType.info); return; } // 3. Robot模式-障碍物打点 _currentObstaclePoints.add(_currentLatLng!); _robotModeObsWgsPoints.add(_currentWgsLatLng!); _isObstacleEditing = true; debugPrint( 'Robot模式新增障碍物打点:第${_currentObstaclePoints.length}个点,经纬度:${_currentLatLng!.latitude}, ${_currentLatLng!.longitude}', ); } } }); _saveDataToLocal(); debugPrint( '$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}', ); } // ========== 新增:返回上一级页面的方法 ========== void _navigateBack() { debugPrint('🔙 [返回] 关闭页面,返回上一级'); Navigator.of(context).pop(); } @override void dispose() { _traceManager.reset(); _mapController.dispose(); _pathPlanningSub?.cancel(); // 🔥 取消订阅 _saveDataToLocal(); // 🔥 页面销毁时保存最终状态 _cancelAllToast(); // 退出页面 → 关闭所有本页面的Toast super.dispose(); } void _handleVideo() { debugPrint('🎬 [视频] 打开视频弹窗'); setState(() { _isVideoDialogOpen = true; // 打开视频弹窗 }); } Widget _buildVideoPopup() { return Positioned( left: _videoPopupPos.dx, top: _videoPopupPos.dy, child: GestureDetector( // 拖拽区域(整个弹窗可拖拽) onPanUpdate: (details) { setState(() { _videoPopupPos = Offset( _videoPopupPos.dx + details.delta.dx, _videoPopupPos.dy + details.delta.dy, ); }); }, child: WebRTCMapPlayer( streamUrl: _videoStreamUrl, width: 320, height: 180, showLeftPip: false, showRightPip: false, onClose: _closeVideoDialog, // 关闭回调 ), ), ); } void _closeVideoDialog() { setState(() { _isVideoDialogOpen = false; }); } Widget _buildPlotListItem(PlotData plot, Function(PlotData) onDelete) { return GestureDetector( // 核心:点击列表项触发选中逻辑 onTap: () async { // 🔥 添加日志:打印选中的路线任务详情 debugPrint('📋 [路径规划] 选中路线任务:'); debugPrint(' ├─ 地块名称: ${plot.plotName}'); debugPrint(' └─ 数据ID: ${plot.id}'); // 【优化1】第一步就UI响应,不卡手 setState(() { _isListBoxOpen = false; _isPanelOpen = false; _directionBoxOpen = false; gcjPathPoints = []; gcjOuterPoints = []; _selectedPlot = plot; }); try { _traceManager.reset(); // 🔥 核心修复:优先使用列表中已加载的 plot.jsonData,避免重复查询导致所有任务显示同一条路线 Map? parsedJson; if (plot.jsonData != null && plot.jsonData!.isNotEmpty) { debugPrint('✅ [选中地块] 尝试使用本地 jsonData,长度: ${plot.jsonData!.length}'); try { final decoded = jsonDecode(plot.jsonData!); if (decoded is Map) { // 🔥 关键检查:jsonData 必须包含 path 或 outer 才算有效数据 // selectBySiteId 接口返回的 是空标签,解析后是 {},没有路径数据 final hasPath = decoded.containsKey('path') && decoded['path'] != null; final hasOuter = decoded.containsKey('outer') && decoded['outer'] != null; if (hasPath || hasOuter) { parsedJson = decoded; debugPrint( '✅ [选中地块] 本地 jsonData 有效,path=$hasPath, outer=$hasOuter', ); } else { debugPrint( '⚠️ [选中地块] 本地 jsonData 无路径数据 (path=$hasPath, outer=$hasOuter),将回退调接口', ); } } else { debugPrint( '❌ [选中地块] plot.jsonData jsonDecode 结果不是 Map: ${decoded.runtimeType}', ); } } catch (e) { debugPrint('❌ [选中地块] plot.jsonData jsonDecode 失败: $e'); } } // 回退:本地 jsonData 为空时,才调接口查询 if (parsedJson == null) { debugPrint('⚠️ [选中地块] 本地 jsonData 为空,回退调用接口查询'); await context.read().loadSelectedPath(plot.plotName); final cubitState = context.read().state; final _loadedPlot = cubitState.pathData; debugPrint( '🔍 [选中地块] pathData 类型: ${_loadedPlot.runtimeType}, 是否为空: ${_loadedPlot?.isEmpty ?? true}', ); if (_loadedPlot is List && _loadedPlot!.isNotEmpty) { final pathRecords = _loadedPlot .where((item) => item is Map) .cast>() .toList(); if (pathRecords.isNotEmpty) { final record = pathRecords.first; // 🔥 核心修复:直接从接口记录的顶层字段取 path/outer,而不是从 jsonData 中取 final rawPath = record['path']; final rawOuter = record['outer']; final planModel = record['planModel']; debugPrint( '🔍 [选中地块] 接口顶层 path 类型: ${rawPath.runtimeType}, outer 类型: ${rawOuter.runtimeType}', ); // 构造 parsedJson,保持 path/outer 为顶层字段 parsedJson = { 'path': rawPath, 'outer': rawOuter, if (planModel != null) 'planModel': planModel, }; // 如果顶层 path/outer 都为空,尝试从 jsonData 兜底 if (rawPath == null && rawOuter == null && record['jsonData'] != null) { debugPrint('⚠️ [选中地块] 顶层 path/outer 为空,尝试从 jsonData 兜底'); final nestedJsonRaw = record['jsonData']; if (nestedJsonRaw is String) { try { final decoded = jsonDecode(nestedJsonRaw); if (decoded is Map) { parsedJson = decoded; } } catch (_) {} } else if (nestedJsonRaw is Map) { parsedJson = nestedJsonRaw; } } } } } if (parsedJson == null) { debugPrint('❌ [选中地块] 无法获取路径数据,面板显示但 startWorkList 为空'); setState(() { _isWorkPanelOpen = true; _isPanelOpen = false; _directionBoxOpen = false; }); return; } debugPrint( '🔍 [选中地块] parsedJson keys: ${parsedJson.keys}, planModel: ${parsedJson['planModel']}', ); // 🔥 安全解析 planModel:支持 int 和 String 类型 final int planModelValue = int.tryParse(parsedJson['planModel']?.toString() ?? '0') ?? 0; debugPrint('🔍 [选中地块] planModelValue: $planModelValue'); workMode = planModelValue == WorkMode.bow.value ? "弓字模式" : "自定义模式"; _currentWorkMode = planModelValue == WorkMode.bow.value ? WorkMode.bow : WorkMode.custom; debugPrint('🔍 [选中地块] workMode: $workMode'); // 【优化4】统一 path/outer 解析逻辑,不重复代码 List pathList = []; final rawPath = parsedJson['path']; debugPrint('🔍 [选中地块] rawPath 类型: ${rawPath.runtimeType}'); if (rawPath is String) { try { pathList = jsonDecode(rawPath) as List; } catch (_) {} } else if (rawPath is List) { pathList = rawPath; } debugPrint('🔍 [选中地块] pathList 长度: ${pathList.length}'); if (pathList.isNotEmpty) { debugPrint('🔍 [选中地块] pathList[0]: ${pathList.first}'); } List outerList = []; final rawOuter = parsedJson['outer']; debugPrint('🔍 [选中地块] rawOuter 类型: ${rawOuter.runtimeType}'); if (rawOuter is String) { try { outerList = jsonDecode(rawOuter) as List; } catch (_) {} } else if (rawOuter is List) { outerList = rawOuter; } debugPrint('🔍 [选中地块] outerList 长度: ${outerList.length}'); if (outerList.isNotEmpty) { debugPrint('🔍 [选中地块] outerList[0]: ${outerList.first}'); } // 弓字模式:path是路线轨迹,outer是边界框 // 自定义模式:只有path if (workMode == "弓字模式") { startWorkList = pathList.isNotEmpty ? pathList : outerList; } else { startWorkList = pathList.isNotEmpty ? pathList : outerList; } debugPrint('✅ [选中地块] startWorkList 长度: ${startWorkList.length}'); // 【优化5】一次性生成坐标,不反复操作数组 List newPathPoints = []; List newOuterPoints = []; for (var item in pathList) { double? lat = safeToDouble(item['lat']); double? lon = safeToDouble(item['lng'] ?? item['lon']); if (lat != null && lon != null) { newPathPoints.add(convertWGS84ToGCJ02(lat, lon)); } } for (var item in outerList) { double? lng = safeToDouble(item['lng'] ?? item['lon']); double? lat = safeToDouble(item['lat']); if (lng != null && lat != null) { newOuterPoints.add(convertWGS84ToGCJ02(lat, lng)); } } // 【优化6】整个方法只调用一次 setState,性能暴涨 // 🔥 弓字模式(0):显示 outer灰色透明框 + path路线轨迹 // 🔥 自定义模式(2):只显示 path setState(() { gcjPathPoints = newPathPoints; // 两种模式都显示 path gcjOuterPoints = _currentWorkMode == WorkMode.bow ? newOuterPoints // 弓字模式:显示 outer 边框 : []; // 自定义模式:不显示 outer _isWorkPanelOpen = true; _isPanelOpen = false; _directionBoxOpen = false; }); debugPrint( '✅ [选中地块] 最终渲染: gcjPathPoints=${gcjPathPoints.length}, gcjOuterPoints=${gcjOuterPoints.length}, workMode=$_currentWorkMode', ); // 最后移动地图 List allPoints = []; allPoints.addAll(gcjPathPoints); allPoints.addAll(gcjOuterPoints); if (allPoints.isNotEmpty) { moveMapToPointsCenter(allPoints); } } catch (e) { debugPrint('解析失败:$e'); setState(() { gcjPathPoints = []; gcjOuterPoints = []; _isWorkPanelOpen = true; _isPanelOpen = false; _directionBoxOpen = false; }); } finally { _saveDataToLocal(); } }, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), border: Border.all( color: _selectedPlot?.id == plot.id ? Colors.blue : Colors.grey[100]!, width: _selectedPlot?.id == plot.id ? 2 : 1, // 选中项高亮边框 ), ), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( plot.imageUrl, width: 30, height: 30, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 30, height: 30, color: Colors.grey[200], child: const Icon(Icons.image_outlined, color: Colors.grey), ); }, ), ), const SizedBox(width: 10), Expanded( child: Text( plot.plotName, style: const TextStyle(fontSize: 16, color: Colors.black87), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( onPressed: () => _showDeleteConfirmDialog(plot, onDelete), icon: const Icon( Icons.delete_outline, color: Colors.redAccent, size: 20, ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ); } LatLng _getDefaultValidCenter() { return const LatLng(30.279651, 120.154871); } Widget _buildMap( double lat, double lng, bool obfFlag, int headingStatus, String controlMode, ) { _onDeviceData(lng, lat, obfFlag, headingStatus, controlMode); final gcjPoint = wgs84ToGcj02(lat, lng); //final initCenter = bd09ToGcj02( // '120.81992468426961', // 经度 // '32.04532826114155', //); // 🔥 修复1:初始化坐标改为直接使用有效GCJ02坐标,避免BD09转换风险 final initCenter = _getDefaultValidCenter(); _currentWgsLatLng = LatLng(lat, lng); _traceManager.upsert(_currentWgsLatLng as PlotPoint, TPAction.UPDATE); tracePoint = _traceManager.getTracePoint(); gctracePoint = batchWgs84ToGcj02(tracePoint!); _logger.log("[当前轨迹模式][转换后gctracePoint]: ${gctracePoint!.length}"); _logger.log("[当前轨迹模式]:${_traceManager.getMode()}"); // 这里必须加 () //_currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新 if (_isValidLatLng(gcjPoint.latitude, gcjPoint.longitude)) { _currentLatLng = gcjPoint; } else { _currentLatLng = null; } return FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: __isValidLatLng(_currentLatLng) ? _currentLatLng! : initCenter, initialZoom: 18, maxZoom: 22, minZoom: 3, enableScrollWheel: true, // 🔥 视觉放大:通过缩放系数实现"伪20级"显示(无空白) onTap: (_, __) {}, // 空实现,禁用地图点击响应 ), children: [ /// 高德瓦片(GCJ-02) TileLayer( //urlTemplate: // "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=2&style=7&key=bbb1f0f20eed6bf679eddf2625630aba", urlTemplate: 'https://webrd02.is.autonavi.com/appmaptile' '?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1' '&key=bbb1f0f20eed6bf679eddf2625630aba', // 🔥 修复:适配高DPI(替代pixelRatio) // tileSize: 512, // 高清瓦片尺寸(默认256,改为512适配scale=2) tileProvider: NetworkTileProvider( // 自定义瓦片请求头(可选,提升兼容性) headers: { 'Referer': 'https://www.amap.com', // 高德要求的Referer }, ), maxZoom: 18, ), //if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController), // 绘制path折线(Line模式) if (gcjPathPoints.isNotEmpty) PolylineLayer( polylines: [ Polyline( points: gcjPathPoints, // 转换后的GCJ02坐标 color: const Color.fromARGB(255, 255, 204, 0), // 金黄色(更醒目) strokeWidth: 1.0, // 折线宽度 isDotted: false, // 非虚线(Line模式) borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 borderStrokeWidth: 1, ), ], ), // 绘制outer边框(Polygon模式)—— 弓字模式:灰色透明框 + 内部路线轨迹 if (gcjOuterPoints.isNotEmpty && _currentWorkMode == WorkMode.bow) PolygonLayer( polygons: [ Polygon( points: gcjOuterPoints, // 转换后的GCJ02坐标 color: Colors.grey.withOpacity(0.15), // 灰色半透明填充 borderColor: Colors.grey.withOpacity(0.6), // 灰色半透明边框 borderStrokeWidth: 1.5, // 边框宽度 isFilled: true, // 开启填充 ), ], ), if (gcjOuterPoints.isNotEmpty && _currentWorkMode == WorkMode.custom) PolylineLayer( polylines: [ Polyline( points: gcjOuterPoints, // 转换后的GCJ02坐标 color: Colors.green, // 内部填充色(透明) strokeWidth: 3.0, // 折线宽度 isDotted: false, // 非虚线(Line模式) borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 borderStrokeWidth: 0.5, ), ], ), /// 中心标与历史打点的虚线连线 if (!_isWorkAreaCompleted) PolylineLayer( polylines: [ for (int i = 0; i < _markedPoints.length - 1; i++) Polyline( points: [_markedPoints[i], _markedPoints[i + 1]], color: Colors.orange.withOpacity(0.5), strokeWidth: 1.5, ), ], ), if (_markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point) Stack( // 核心:替换Column为Stack children: [ // 1. 蓝色连线 PolylineLayer( polylines: [ Polyline( points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5, ), ], ), // 2. 距离文本Marker MarkerLayer( markers: [ Marker( point: _getMiddleLatLng(_mapCenter, _markedPoints.last), width: 100, height: 30, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), child: Text( _calculateLatLngDistance( _mapCenter, _markedPoints.last, ), style: const TextStyle( color: Colors.red, fontSize: 12, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ), ], ), if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted && _currentRobotMode == RobotMode.point) PolygonLayer( polygons: [ Polygon( points: _getPolygonPoints(), color: Colors.green.withOpacity(0.2), borderColor: Colors.green.withOpacity(0.5), borderStrokeWidth: 1, isFilled: true, ), ], ), if (_obstacleHoles.isNotEmpty && _isObstacleEditing) PolygonLayer( polygons: _obstacleHoles.map((holePoints) { return Polygon( points: holePoints, color: Colors.red.withOpacity(0.2), // 红色半透明填充 borderColor: Colors.red, // 红色边框 borderStrokeWidth: 1.5, isFilled: true, ); }).toList(), ), //开始作业之后 // 2. 正在绘制的障碍物打点连线(红色虚线) if (_isObstacleEditing && _currentObstaclePoints.isNotEmpty) PolylineLayer( polylines: [ // 已绘制的障碍物点连线 for (int i = 0; i < _currentObstaclePoints.length - 1; i++) Polyline( points: [ _currentObstaclePoints[i], _currentObstaclePoints[i + 1], ], color: Colors.red.withOpacity(0.8), strokeWidth: 1.5, isDotted: true, // 虚线区分作业区域 ), if (_currentRobotMode == RobotMode.point) // 最后一个点到地图中心的连线 Polyline( points: [_mapCenter, _currentObstaclePoints.last], color: Colors.red.withOpacity(0.5), strokeWidth: 1.5, isDotted: true, ), ], ), // 3. 障碍物打点标记(红色) if (_currentAreaMode == AreaMode.obstacle && _isObstacleEditing) MarkerLayer( markers: [ // 已完成的障碍物打点 for ( var holeIndex = 0; holeIndex < _obstacleHoles.length; holeIndex++ ) for ( var pointIndex = 0; pointIndex < _obstacleHoles[holeIndex].length; pointIndex++ ) Marker( point: _obstacleHoles[holeIndex][pointIndex], width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.red, // 红色标记区分作业区域 shape: BoxShape.circle, boxShadow: [ BoxShadow(color: Colors.black12, blurRadius: 2), ], ), child: Center( child: Text( '${holeIndex + 1}-${pointIndex + 1}', // 格式:组号-点号 style: const TextStyle( color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), // 正在绘制的障碍物打点 for ( var pointIndex = 0; pointIndex < _currentObstaclePoints.length; pointIndex++ ) Marker( point: _currentObstaclePoints[pointIndex], width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.orange, // 橙色标记区分正在绘制 shape: BoxShape.circle, boxShadow: [ BoxShadow(color: Colors.black12, blurRadius: 2), ], ), child: Center( child: Text( '${pointIndex + 1}', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ], ), // 只在 ≥2 个点时才显示图层,否则直接消失! if (gctracePoint != null && gctracePoint!.length >= 2) PolylineLayer( key: const ValueKey('trace_polyline'), polylines: [ Polyline( points: gctracePoint!, color: const Color.fromARGB(255, 6, 187, 232), strokeWidth: 3, ), ], ), /// 当前定位 Marker if (_currentLatLng != null && __isValidLatLng(_currentLatLng)) MarkerLayer( markers: [ Marker( point: _currentLatLng!, width: 40, height: 40, child: CustomPaint( size: const Size(40, 40), painter: HeadingMarkerPainter(headingAngle: _headingAngle), ), ), ], ), if (!_isWorkAreaCompleted) /// 历史打点的绿色标记 MarkerLayer( markers: _markedPoints.asMap().entries.map((entry) { int index = entry.key + 1; // 打点序号(从1开始) LatLng point = entry.value; return Marker( point: point, width: 80, height: 40, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), // 向下偏移8px(抵消默认的顶部对齐) // 绿色打点标记 Container( width: 16, height: 16, decoration: const BoxDecoration( color: Color.fromARGB(255, 10, 218, 55), // 绿色主题色 shape: BoxShape.circle, boxShadow: [ BoxShadow(color: Colors.black12, blurRadius: 2), ], ), child: Center( child: Text( '$index', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ), ], ), ); }).toList(), ), ], ); } /// 🔥 显示任务选择弹窗 Future _showTaskSelectionDialog( List tasks, ) async { return showDialog( 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 // 1. 校验选中的路线 if (_selectedPlot == null) { _showPageToast(message: "请先选择一个路线任务", type: ToastType.info); return; } // 2. 获取设备ID(从 RemoteControlCubit 的 targetDevice) final targetDevice = context.read().state.targetDevice; final deviceId = targetDevice?.deviceName; if (deviceId == null || deviceId.isEmpty) { _showPageToast(message: "请先选择一个设备", type: ToastType.info); return; } // 3. 获取路线ID(从选中的路线任务) final routeId = int.tryParse(_selectedPlot!.id); if (routeId == null) { _showPageToast(message: "路线ID无效", type: ToastType.error); return; } // 4. 获取场站ID(从 SiteCubit 的 selectedSite) final siteId = sl().state.selectedSite?.id; if (siteId == null) { _showPageToast(message: "请先选择一个场站", type: ToastType.info); return; } // 5. 获取组织ID(从 AppUserCubit 的 user) final userCubit = sl(); final orgId = userCubit.state.user?.orgId; if (orgId == null) { _showPageToast(message: "用户信息异常,请重新登录", type: ToastType.error); return; } // 🔥 6. 先过滤出活跃任务,让用户选择 debugPrint('🔍 [开始作业] 正在查询活跃任务...'); final taskCubit = sl(); await taskCubit.fetchAndFilterTask(deviceId); // 等待一下让状态更新 await Future.delayed(const Duration(milliseconds: 100)); final activeTasks = taskCubit.state.activeTasks; debugPrint('✅ [开始作业] 找到 ${activeTasks.length} 个活跃任务'); // 🔥 6. 任务选择(仅当有活跃任务时才需要选择,否则直接创建新任务) DeviceTaskEntity? selectedTask; if (activeTasks.length > 1) { debugPrint('⚠️ [开始作业] 有多个活跃任务,显示选择弹窗'); selectedTask = await _showTaskSelectionDialog(activeTasks); if (selectedTask == null) { debugPrint('❌ [开始作业] 用户取消选择任务'); return; } taskCubit.selectTask(selectedTask); } else if (activeTasks.length == 1) { selectedTask = activeTasks.first; debugPrint('✅ [开始作业] 自动选择唯一任务 #${selectedTask.id}'); } else { debugPrint('ℹ️ [开始作业] 无活跃任务,将直接创建新任务'); } debugPrint( '📋 [开始作业] 选中的任务ID: ${selectedTask?.id ?? "(新建)"}, 状态: ${selectedTask?.taskStatus ?? "N/A"}', ); // 7. 打印请求参数日志 debugPrint('🚀 [开始作业] 请求参数:'); debugPrint(' ├─ deviceId: $deviceId'); debugPrint(' ├─ routeId: $routeId'); debugPrint(' ├─ siteId: $siteId'); debugPrint(' ├─ orgId: $orgId'); debugPrint(' └─ taskId: ${selectedTask?.id ?? "(无,将新建)"}'); // 8. 更新UI状态 setState(() { isStopWork = false; isStartWork = true; _workStatus = WorkStatus.working; _traceManager.reset(); tracePoint?.clear(); gctracePoint?.clear(); }); // 9. 更新应用状态 context.read().updateAppState(AppState.routePlanning); // 10. 如果池子里已有活跃任务,直接使用;否则创建新任务 if (selectedTask != null) { // 🔥 池子里已有任务,直接使用,不需要再创建 debugPrint('✅ [开始作业] 使用已有任务 #${selectedTask.id},跳过创建'); // taskId 已由 selectTask 存入 cubit } else { // 🔥 池子里没有,创建新任务 debugPrint('🆕 [开始作业] 池子为空,创建新任务...'); try { final result = await sl().execute( deviceId: deviceId, routeId: routeId, siteId: siteId, orgId: orgId, ); var needRequery = false; var failed = false; result.fold( (failure) { final failMsg = failure.toString(); if (failMsg.contains('存在任务') || failMsg.contains('already exists')) { needRequery = true; return; } failed = true; debugPrint('[开始作业] 创建失败: $failMsg'); _showPageToast(message: '作业启动失败', type: ToastType.error); taskCubit.clearCurrentTask(); return; }, (taskId) { taskCubit.updateCurrentTaskId(taskId); }, ); if (failed) return; if (needRequery) { await taskCubit.fetchAndFilterTask(deviceId); await Future.delayed(const Duration(milliseconds: 300)); final existingTasks = taskCubit.state.activeTasks; if (existingTasks.isNotEmpty) { taskCubit.selectTask(existingTasks.first); } else { debugPrint('[开始作业] needRequery 后仍无活跃任务'); _showPageToast(message: '未找到活跃任务', type: ToastType.warn); } return; } } catch (e) { debugPrint('❌ [开始作业] 异常: $e'); _showPageToast(message: "作业启动异常: $e", type: ToastType.error); taskCubit.clearCurrentTask(); // 🔥 不重置 _workStatus,保持按钮可见 return; } } // 🔥 到这里说明任务已就绪(无论是已有还是新建),刷新一次任务池显示最新状态 taskCubit.fetchAndFilterTask(deviceId); _showPageToast(message: "作业已开始", type: ToastType.success); _saveDataToLocal(); // ============ 以下是原有的 TCP 方式代码,已注释 ============ /* if (startWorkList.isEmpty) { _showPageToast(message: "作业列表为空,请重新选择路径", type: ToastType.info); return; } _traceManager.setMode(TPMode.LOCATION); tracePoint = _traceManager.getTracePoint(); gctracePoint = batchWgs84ToGcj02(tracePoint!); _logger.log("[当前轨迹模式][转换后gctracePoint]开始作业: $tracePoint"); _logger.log("[当前轨迹模式][转换后gctracePoint]开始作业: $gctracePoint"); _logger.log("[当前轨迹模式][转换后gctracePoint]开始作业= ${gctracePoint!.length}"); setState(() { isStopWork = false; isStartWork = true; _workStatus = WorkStatus.working; _traceManager.reset(); tracePoint?.clear(); gctracePoint?.clear(); }); context.read().updateAppState(AppState.routePlanning); await Future.delayed(const Duration(milliseconds: 100)); debugPrint('⚙️ 开始类型转换...'); final List typedList = startWorkList.whereType>().map((item) { return work_area_model.DeviceAddPathPointModel( latitude: (item['lat'] as num).toDouble(), longitude: (item['lng'] ?? item["lon"] as num).toDouble(), ); }).toList(); final Queue pathQueue = Queue.from(typedList); await context.read().startRoutePlanning(pathQueue); _showPageToast(message: "作业已开始", type: ToastType.success); _saveDataToLocal(); */ } /// 暂停作业 void _pauseWork() async { // 🔥 获取设备ID和taskId final targetDevice = context.read().state.targetDevice; final deviceId = targetDevice?.deviceName; if (deviceId == null || deviceId.isEmpty) { _showPageToast(message: "请先选择一个设备", type: ToastType.info); return; } final taskCubit = sl(); 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().updateAppState(AppState.none); //await context.read().pauseRoutePlanning(); } void _stopWork() async { _logger.log('[停止] 按下停止按钮'); debugPrint('══════════ [停止作业] 开始 ══════════'); // 🔥 获取设备ID和taskId final targetDevice = context.read().state.targetDevice; final deviceId = targetDevice?.deviceName; debugPrint('[停止作业] deviceId: $deviceId, targetDevice: ${targetDevice?.deviceName}'); if (deviceId == null || deviceId.isEmpty) { debugPrint('[停止作业] ❌ deviceId 为空,退出'); _showPageToast(message: "请先选择一个设备", type: ToastType.info); return; } final taskCubit = sl(); final taskId = taskCubit.state.currentTaskId; debugPrint('[停止作业] taskId: $taskId'); if (taskId == null) { debugPrint('[停止作业] ❌ taskId 为 null,退出'); _showPageToast(message: "无可用任务", type: ToastType.warn); return; } debugPrint('[停止作业] 准备调用 cancelTask API, deviceId: $deviceId, taskId: $taskId'); if (mounted) { setState(() { isStartWork = false; _workStatus = WorkStatus.idle; _showHeadingWarn = false; _showControlModeWarn = false; isStopWork = true; isreceiveFirstCompletePoint = false; // 1. 立刻切换模式(阻止新的点再进入 NAVIGATION 逻辑) _traceManager.setMode(TPMode.LOCATION); }); debugPrint('[停止作业] setState 完成,workStatus = idle'); } // 🔥 调用接口取消任务 try { debugPrint('[停止作业] 正在调用 taskCubit.cancelTask...'); await taskCubit.cancelTask(deviceId); debugPrint('[停止作业] cancelTask 返回成功'); taskCubit.clearCurrentTask(); // 🔥 停止后清除 taskId,释放任务 debugPrint('[停止作业] clearCurrentTask 完成'); _showPageToast(message: "作业已停止", type: ToastType.success); debugPrint('[停止作业] HTTP 取消成功,准备发送 TCP 停止指令'); // 🔥 还需要通过 TCP 向机器发送停止指令 try { debugPrint('[停止作业] 调用 TCP stopRoutePlanning...'); await context.read().stopRoutePlanning(); debugPrint('[停止作业] TCP stopRoutePlanning 完成'); } catch (tcpError) { debugPrint('[停止作业] ⚠️ TCP 停止异常: $tcpError'); } } catch (e) { debugPrint('[停止作业] ❌ cancelTask 异常: $e'); _showPageToast(message: "停止失败: $e", type: ToastType.error); } _saveDataToLocal(); debugPrint('[停止作业] _saveDataToLocal 完成'); await Future.delayed(const Duration(milliseconds: 300)); if (mounted) { setState(() { // 2. 延迟后重置 + 清空(彻底绝杀残留点) _traceManager.reset(); tracePoint?.clear(); gctracePoint?.clear(); tracePoint = []; gctracePoint = []; }); debugPrint('[停止作业] 最终 setState 完成,轨迹已清空'); } debugPrint('══════════ [停止作业] 结束 ══════════'); _showPageToast(message: "作业已停止", type: ToastType.error); } /// 继续作业 void _resumeWork() async { // 🔥 获取设备ID和taskId final targetDevice = context.read().state.targetDevice; final deviceId = targetDevice?.deviceName; if (deviceId == null || deviceId.isEmpty) { _showPageToast(message: "请先选择一个设备", type: ToastType.info); return; } final taskCubit = sl(); final taskId = taskCubit.state.currentTaskId; if (taskId == null) { _showPageToast(message: "无可用任务", type: ToastType.warn); return; } debugPrint('▶️ [继续作业] deviceId: $deviceId, taskId: $taskId'); setState(() { _workStatus = WorkStatus.working; }); _saveDataToLocal(); // 🔥 保存暂停状态 // 🔥 调用接口恢复任务(注释掉原有的 TCP 方式) try { await taskCubit.recoveryTask(deviceId); _showPageToast(message: "作业已恢复", type: ToastType.success); } catch (e) { debugPrint('❌ [继续作业] 异常: $e'); _showPageToast(message: "恢复失败: $e", type: ToastType.error); } // 原有的 TCP 方式(已注释) //context.read().updateAppState(AppState.routePlanning); //await context.read().resumeRoutePlanning(); } void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) { ConfirmDialog.show( context: context, title: '确认删除', content: '是否删除地块「${plot.plotName}」?删除后不可恢复。', confirmText: '删除', confirmTextColor: Colors.red, onConfirm: () { onDelete(plot); }, ); } /// 🔥 从 pathData 中实时解析 startWorkList,避免时序问题: /// BlocBuilder 在 pathData 更新后立即渲染面板,但 startWorkList 在 onTap async 中 /// 稍后才赋值,导致面板先显示"路径区域为空"。此方法直接从 state.pathData 计算, /// 确保面板渲染时数据已就绪。 List _parseStartWorkListFromPathData( List>? pathData, ) { debugPrint( '🔍 [_parseSWL] pathData 是否为null: ${pathData == null}, 长度: ${pathData?.length}', ); if (pathData == null || pathData.isEmpty) { debugPrint('❌ [_parseSWL] pathData 为空,返回 []'); return []; } final firstRecord = pathData.first; debugPrint('🔍 [_parseSWL] firstRecord keys: ${firstRecord.keys}'); final nestedJsonRaw = firstRecord['jsonData']; debugPrint('🔍 [_parseSWL] nestedJsonRaw 类型: ${nestedJsonRaw.runtimeType}'); // 🔥 兼容两种格式:String(需 jsonDecode)和 Map(已解析) Map parsedJson; if (nestedJsonRaw is String) { try { final decoded = jsonDecode(nestedJsonRaw); if (decoded is! Map) { debugPrint('❌ [_parseSWL] jsonDecode 结果不是 Map,返回 []'); return []; } parsedJson = decoded; } catch (_) { debugPrint('❌ [_parseSWL] jsonDecode 失败,返回 []'); return []; } } else if (nestedJsonRaw is Map) { parsedJson = nestedJsonRaw; } else { debugPrint( '❌ [_parseSWL] nestedJsonRaw 类型不支持: ${nestedJsonRaw.runtimeType},返回 []', ); return []; } debugPrint('🔍 [_parseSWL] parsedJson keys: ${parsedJson.keys}'); final planModel = parsedJson['planModel']; // 🔥 安全解析:支持 int 和 String 类型 final int planModelValue = int.tryParse(planModel?.toString() ?? '0') ?? 0; debugPrint( '🔍 [_parseSWL] planModel: $planModel, planModelValue: $planModelValue, WorkMode.bow.value: ${WorkMode.bow.value}', ); final isBow = planModelValue == WorkMode.bow.value; List pathList = []; final rawPath = parsedJson['path']; if (rawPath is String) { try { pathList = jsonDecode(rawPath); } catch (_) {} } else if (rawPath is List) { pathList = rawPath; } List outerList = []; final rawOuter = parsedJson['outer']; if (rawOuter is String) { try { outerList = jsonDecode(rawOuter); } catch (_) {} } else if (rawOuter is List) { outerList = rawOuter; } if (isBow) { final result = pathList.isNotEmpty ? pathList : outerList; debugPrint( '✅ [_parseSWL] 弓字模式,pathList: ${pathList.length}, outerList: ${outerList.length}, 最终返回: ${result.length}', ); return result; } else { final result = outerList.isNotEmpty ? outerList : pathList; debugPrint( '✅ [_parseSWL] 自定义模式,outerList: ${outerList.length}, pathList: ${pathList.length}, 最终返回: ${result.length}', ); return result; } } Widget _buildWorkPanel(headingStatus) { return BlocBuilder( builder: (context, state) { if (_selectedPlot == null || state.pathData == null) { return const SizedBox.shrink(); } // 🔥 核心修复:从 state.pathData 实时计算 startWorkList // 确保 BlocBuilder 触发时数据已就绪,不再依赖 onTap 的异步赋值时序 debugPrint( '🔍 [_buildWorkPanel] BlocBuilder 触发, state.pathData 是否为空: ${state.pathData?.isEmpty ?? true}, startWorkList 当前长度: ${startWorkList.length}', ); final parsedList = _parseStartWorkListFromPathData(state.pathData); if (parsedList.isNotEmpty) { startWorkList = parsedList; // 同步到类字段,供 _saveDataToLocal 等方法使用 debugPrint( '✅ [_buildWorkPanel] startWorkList 已更新,新长度: ${startWorkList.length}', ); } else { debugPrint( '⚠️ [_buildWorkPanel] parsedList 为空,startWorkList 保持: ${startWorkList.length}', ); } return Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 10, offset: Offset(0, -2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 1. 地块基础信息 Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( _selectedPlot!.imageUrl, width: 80, height: 80, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: 80, height: 80, color: Colors.grey[200], child: const Icon( Icons.image_outlined, color: Colors.grey, size: 32, ), ); }, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _selectedPlot!.plotName, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( '${AppLocalizations.of(context).translate('route_planning.work_mode')}:$workMode', style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ], ), ), IconButton( onPressed: () { debugPrint('❌ [工作面板] 关闭面板,重置状态'); setState(() { _isWorkPanelOpen = false; _selectedPlot = null; gcjPathPoints = []; gcjOuterPoints = []; _workStatus = WorkStatus.idle; _showHeadingWarn = false; _showControlModeWarn = false; isStartWork = false; }); _saveDataToLocal(); _stopWork(); //if (isStopWork || !isStartWork) return; }, icon: const Icon( Icons.close, color: Colors.grey, size: 20, ), padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 24, minHeight: 24, ), ), ], ), const SizedBox(height: 16), // 🔥 3. 当前任务信息显示(可点击重新选择) BlocBuilder( 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().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, children: [ // 路径为空提示 50% if (startWorkList.isEmpty) Expanded( flex: 1, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), margin: const EdgeInsets.only(right: 6, bottom: 12), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.red[200]!), ), child: const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.error_outline, color: Colors.redAccent, size: 16, ), SizedBox(width: 8), Expanded( child: Text( "路径区域为空", style: TextStyle( fontSize: 14, color: Colors.redAccent, ), softWrap: true, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), // 航向异常提示 50% if (headingStatus == 0) Expanded( flex: 1, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), margin: const EdgeInsets.only(left: 6, bottom: 12), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.red[200]!), ), child: const Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.error_outline, color: Colors.redAccent, size: 16, ), SizedBox(width: 8), Expanded( child: Text( "航向信息异常", style: TextStyle( fontSize: 14, color: Colors.redAccent, ), softWrap: true, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ], ), // 3. 开始作业按钮(加载中禁用) // 放在 Column/Row 的 children: [ ... ] 内部 _workStatus == WorkStatus.idle ? SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: state.isLoading || startWorkList.isEmpty || headingStatus == 0 ? null : () { _startWork(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00C853), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), elevation: 2, disabledBackgroundColor: Colors.grey[300], disabledForegroundColor: Colors.grey[600], ), child: state.isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : Text( AppLocalizations.of( context, ).translate('route_planning.start_work'), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), ), ) : Row( children: [ // 暂停/继续图标按钮 Expanded( child: SizedBox( width: 50, height: 50, child: ElevatedButton( onPressed: () => //@暂停工作通过接口、恢复(继续)工作通过接口 _workStatus == WorkStatus.working ? _pauseWork() : _resumeWork(), style: ElevatedButton.styleFrom( backgroundColor: _workStatus == WorkStatus.working ? Colors.amber : const Color(0xFF00C853), foregroundColor: Colors.white, shape: const CircleBorder(), elevation: 2, // 调整内边距,让图标居中更协调 padding: const EdgeInsets.symmetric( horizontal: 8, ), ), child: Icon( // 作业中显示暂停图标,暂停中显示播放(继续)图标 _workStatus == WorkStatus.working ? Icons.pause : Icons.play_arrow, size: 24, // 图标尺寸 ), ), ), ), const SizedBox(width: 12), // 停止图标按钮 Expanded( child: SizedBox( width: 50, height: 50, child: ElevatedButton( onPressed: () => _stopWork(), style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, foregroundColor: Colors.white, shape: const CircleBorder(), elevation: 2, padding: const EdgeInsets.symmetric( horizontal: 8, ), ), child: const Icon( Icons.stop, // 停止图标(也可以用 Icons.close) size: 24, ), ), ), ), ], ), //SizedBox( // width: double.infinity, // height: 50, // child: ElevatedButton( // onPressed: state.isLoading || gcjPathPoints.isEmpty // ? null // : () { // // 作业逻辑:使用加载的pathData // debugPrint('开始作业:${_selectedPlot!.plotName},路径数据:$gcjPathPoints'); // }, // style: ElevatedButton.styleFrom( // backgroundColor: const Color(0xFF00C853), // foregroundColor: Colors.white, // shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), // elevation: 2, // disabledBackgroundColor: Colors.grey[300], // disabledForegroundColor: Colors.grey[600], // ), // child: state.isLoading // ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) // : const Text('开始作业', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), // ), //), ], ), ), ); }, ); } Future _captureMapToJpgBase64({int quality = 80}) async { try { // 1. 第一步:截取 PNG (Flutter 唯一支持的格式) RenderRepaintBoundary? boundary = _mapRepaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { debugPrint('截图失败:渲染对象未找到'); return null; } // 🔥 注意:这里是 dart:ui 的 Image,需显式指定 ui.Image ui.Image image = await boundary.toImage(pixelRatio: 2.0); ByteData? byteData = await image.toByteData( format: ui.ImageByteFormat.png, ); if (byteData == null) { debugPrint('截图失败:字节数据为空'); return null; } Uint8List pngBytes = byteData.buffer.asUint8List(); // 2. 第二步:使用 image 库解码 // 🚨 核心修复:变量类型必须显式写为 _image.Image? _image.Image? decodedImage = _image.decodeImage(pngBytes); if (decodedImage == null) { debugPrint('转换失败:无法解码 PNG'); return null; } // 3. 第三步:编码为 JPG List jpgBytes = _image.encodeJpg(decodedImage, quality: quality); Uint8List finalBytes = Uint8List.fromList(jpgBytes); // 4. 第四步:转换为 Base64 String base64Str = base64Encode(finalBytes); debugPrint('JPG Base64 长度:${base64Str.length} (体积更小)'); return base64Str; } catch (e) { debugPrint('生成 JPG Base64 异常:$e'); return null; } } // 🔥 改动4:新增方法 - 将Bloc的workRecords转换为PlotData列表 List _convertWorkRecordsToPlotData( List> records, ) { // debugPrint('📊 [_convertWorkRecordsToPlotData] 输入记录数: ${records.length}'); return records.map((record) { final jd = record['jsonData']; // debugPrint(' ├─ workName: ${record['workName']}, jsonData类型: ${jd?.runtimeType}, jsonData长度: ${jd is String ? jd.length : "N/A"}'); // if (jd is String && jd.isNotEmpty) { // try { // final decoded = jsonDecode(jd); // if (decoded is Map) { // debugPrint(' │ path类型: ${decoded['path']?.runtimeType}, outer类型: ${decoded['outer']?.runtimeType}, planModel: ${decoded['planModel']}'); // } // } catch (_) {} // } // 兼容 jsonData 为 Map 或 String 的情况:统一转为 JSON 字符串 String? jsonDataStr; final rawJsonData = record['jsonData']; if (rawJsonData is String) { jsonDataStr = rawJsonData; } else if (rawJsonData is Map) { jsonDataStr = jsonEncode(rawJsonData); } return PlotData( id: record['id']?.toString() ?? DateTime.now().microsecondsSinceEpoch.toString(), // 唯一ID plotName: record['workName'] ?? '未命名地块', // 地块名称(从接口字段取) imageUrl: record['imgUrl'] ?? '', // 图片URL(从接口字段取,无则为空) jsonData: jsonDataStr, // 原始数据的JSON字符串(可选,便于调试或后续使用) ); }).toList(); } // ========== 抽象:生成路径的核心函数(打点函数) ========== Future _generatePath({bool showTips = true}) async { debugPrint( '🛤️ [生成路径] 作业模式: $_currentWorkMode, 机器人模式: $_currentRobotMode, 打点数: ${_markedPoints.length}, 行距: $_workDistance, 航向角: $_angle', ); if (_currentWorkMode == WorkMode.custom) { // 🔥 对齐 Web 端:自定义模式 path=打点, outer=[] debugPrint( '📌 [生成路径] 自定义模式: path点数=${_markedPoints.length}, outer=[], planModel=2', ); setState(() { _saveBoxOpen = true; _isWorkAreaCompleted = true; gcjPathPoints = List.from(_markedPoints); // gcjOuterPoints = List.from(_markedPoints); // 旧逻辑:outer也存打点 gcjOuterPoints = []; // 对齐 Web 端:自定义模式 outer 为空 typedPathList = []; }); return; } if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) { if (showTips) { _showPageToast(message: '请先添加作业区域打点!', type: ToastType.error); } return; } if (_currentAreaMode == AreaMode.work && _markedPoints.length < 3) { if (showTips) { _showPageToast(message: '作业区域至少需要3个打点才能生成路径!', type: ToastType.error); } return; } if (_currentWorkMode == null) { if (showTips) { _showPageToast(message: '请先选择作业模式!', type: ToastType.error); } return; } // 弓字模式行距默认为0.0,与Web端保持一致 try { LatLng firstPointWgs; if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) { firstPointWgs = _robotModeWgsPoints.first; } else if (_currentRobotMode == RobotMode.point && _markedPoints.isNotEmpty) { LatLng firstPointGcj = _markedPoints.first; firstPointWgs = gcj02ToWgs84( firstPointGcj.latitude, firstPointGcj.longitude, ); } else if (_markedPoints.isNotEmpty) { // 兜底:_robotModeWgsPoints 为空但_markedPoints有数据(新缓存key未写入等场景) LatLng firstPointGcj = _markedPoints.first; firstPointWgs = gcj02ToWgs84( firstPointGcj.latitude, firstPointGcj.longitude, ); } else { if (showTips) { _showPageToast(message: '参考点坐标为空,请先添加作业区域打点!', type: ToastType.error); } return; } final referencePoint = work_area_model.ReferencePoint( lat: firstPointWgs.latitude, lon: firstPointWgs.longitude, ); final heading = _angle == -1 ? -1 : _angle.toInt(); List outerPositions = []; if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) { outerPositions = _robotModeWgsPoints.map((latLng) { return work_area_model.Position( lat: latLng.latitude, lon: latLng.longitude, ); }).toList(); } else if (_markedPoints.isNotEmpty) { // robot模式但_robotModeWgsPoints为空时,兜底用_markedPoints转换 outerPositions = _markedPoints.map((latLng) { final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude); return work_area_model.Position( lat: wgs84Point.latitude, lon: wgs84Point.longitude, ); }).toList(); } gcjOuterPoints = outerPositions.map((point) { double wgs84Lat = point.lat; double wgs84Lon = point.lon; final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon); return LatLng(gcjPoint.latitude, gcjPoint.longitude); }).toList(); final outerBoundary = work_area_model.OuterBoundary( position: outerPositions, sideWidth: _workDistance, ); work_area_model.HoleBoundary? holeBoundary; if (_obstacleHoles.isNotEmpty) { List> positionList = []; if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) { for (var holePoints in _obstacleWgsHoles) { List innerPositionList = []; innerPositionList = holePoints.map((latLng) { return work_area_model.Position( lat: latLng.latitude, lon: latLng.longitude, ); }).toList(); positionList.add(innerPositionList); } } else if (_currentRobotMode == RobotMode.point && _obstacleHoles.isNotEmpty) { for (var holePoints in _obstacleHoles) { List innerPositionList = []; innerPositionList = holePoints.map((latLng) { final wgs84Point = gcj02ToWgs84( latLng.latitude, latLng.longitude, ); return work_area_model.Position( lat: wgs84Point.latitude, lon: wgs84Point.longitude, ); }).toList(); positionList.add(innerPositionList); } } holeBoundary = work_area_model.HoleBoundary( position: positionList, sideWidth: 0.0, ); } Map holes = {}; if (holeBoundary != null) { holes = {'tempKey': holeBoundary}; } final workType = _currentWorkMode == WorkMode.bow ? 0 : 2; debugPrint( '🔍 [生成路径-发送] referencePoint=(${referencePoint.lat}, ${referencePoint.lon}), heading=$heading, workType=$workType', ); debugPrint( '🔍 [生成路径-发送] outerPositions点数=${outerPositions.length}, sideWidth=$_workDistance', ); for (var i = 0; i < outerPositions.length; i++) { debugPrint( '🔍 [生成路径-发送] outer[$i]=(${outerPositions[i].lat}, ${outerPositions[i].lon})', ); } await context.read().generatePath( reference: referencePoint, heading: heading, outer: outerBoundary, holes: holes.isNotEmpty ? holes : {}, workType: workType, ); final cubitState = context.read().state; if (cubitState.errorMessage != null) { debugPrint('❌ [生成路径] 失败: ${cubitState.errorMessage}'); if (showTips) { _showPageToast( message: '路径生成失败:${cubitState.errorMessage}', type: ToastType.error, ); } } else if (cubitState.generatedPath != null) { if (showTips) { _showPageToast(message: '路径生成成功!', type: ToastType.success); } debugPrint( '✅ [生成路径] 成功,路径点数: ${(cubitState.generatedPath as List).length}', ); if (cubitState.generatedPath is List) { setState(() { _isWorkAreaCompleted = true; _saveBoxOpen = true; _currentObstaclePoints.clear(); _robotModeObsWgsPoints.clear(); final pathList = cubitState.generatedPath as List; typedPathList = pathList .whereType() .toList(); gcjPathPoints = typedPathList.map((point) { double wgs84Lat = point.latitude; double wgs84Lon = point.longitude; final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon); return LatLng(gcjPoint.latitude, gcjPoint.longitude); }).toList(); }); _saveDataToLocal(); moveMapToPointsCenter(gcjPathPoints); } } } catch (e) { debugPrint('路径生成异常:$e'); if (showTips) { _showPageToast( message: '路径生成异常:${e.toString().substring(0, 50)}', type: ToastType.error, ); } } } void _showPageToast({required String message, required ToastType type}) { _toastQueue.add(_ToastItem(message: message, type: type)); if (!_isToastShowing) { _processNextToast(); } } void _processNextToast() { if (_toastQueue.isEmpty) { _isToastShowing = false; return; } _isToastShowing = true; final item = _toastQueue.removeAt(0); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; OverlayEntry entry = OverlayEntry( builder: (context) => SafeArea( child: Align( alignment: Alignment.topCenter, child: Container( margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: _getToastColor(item.type), borderRadius: BorderRadius.circular(8), ), child: Text( item.message, style: const TextStyle(color: Colors.white, fontSize: 14), ), ), ), ), ); _toastEntries.add(entry); Overlay.of(context)?.insert(entry); Future.delayed(const Duration(seconds: 2), () { if (entry.mounted) entry.remove(); _toastEntries.remove(entry); _processNextToast(); }); }); } void _cancelAllToast() { for (final entry in _toastEntries) { if (entry.mounted) { entry.remove(); } } _toastEntries.clear(); _toastQueue.clear(); _isToastShowing = false; } Color _getToastColor(ToastType type) { switch (type) { case ToastType.success: return Colors.green; case ToastType.error: return Colors.red; case ToastType.warn: return Colors.orange; case ToastType.info: return Colors.blue; case ToastType.loading: return Colors.black87; } } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56 final maxTop = screenHeight - menuHeight; final userState = context.watch().state; // 🔥 V2 适配:使用 RemoteControlCubit 的 targetDevice 保持一致性 final deviceId = context .watch() .state .targetDevice ?.deviceName; if (deviceId != null && userState.user != null && userState.user!.token != null) { _videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}"; } else { _videoStreamUrl = ''; } return BlocBuilder( builder: (context, state) { // 🔥 修复:使用 read 而非 watch,避免不必要的重建和接口调用 final devicesState = context.read().state; bool isFinishWork = devicesState.isFinshWork ?? false; double? arriLatitude = devicesState.arriLatitude; double? arriLongitude = devicesState.arriLongitude; LatLng? targetLatLng; if (arriLatitude != null && arriLongitude != null) { targetLatLng = LatLng(arriLatitude, arriLongitude); _logger.log( "当前轨迹模式: ${_traceManager.getMode()},收到完成点坐标: $targetLatLng", ); if (__isValidLatLng(targetLatLng)) { //debugPrint("${targetLatLng}收到完成点arriLatitude"); //if (!isreceiveFirstCompletePoint && isStartWork && _workStatus == WorkStatus.working) { debugPrint("收到完成点arriLatitude"); if (_traceManager.getMode() != TPMode.NAVIGATION) { _traceManager.setMode(TPMode.NAVIGATION); debugPrint("切换到导航模式"); } isreceiveFirstCompletePoint = true; //} if (isStartWork && _workStatus == WorkStatus.working) { _traceManager.upsert(targetLatLng, TPAction.ADD); } } } double currentLat = 0.0; double currentLng = 0.0; bool obfFlag = false; //障碍物标志位 int headingStatus = 0; String controlMode = "0"; DeviceStatusUpdated? updatedState; //有停止信号 if (isFinishWork) { // 🔥 关键:用微任务延迟执行状态更新,避开构建阶段 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _workStatus = WorkStatus.idle; isStopWork = true; isreceiveFirstCompletePoint = false; tracePoint = []; gctracePoint = []; tracePoint?.clear(); gctracePoint?.clear(); }); _showPageToast(message: "作业已停止", type: ToastType.error); // 延迟重置轨迹管理器 Future.delayed(const Duration(seconds: 1), () { if (mounted) { _traceManager.setMode(TPMode.LOCATION); _traceManager.reset(); } }); } }); } if (state is DeviceStatusUpdated) { // 强转获取具体的状态数据 updatedState = state as DeviceStatusUpdated; obfFlag = updatedState.status.obstacleFlag.toString() == 1; headingStatus = updatedState.status.headingStatus; controlMode = updatedState.status.controlMode; _headingAngle = updatedState.status.yaw; //debugPrint("路径规划四十数据 - 经纬度:"); //debugPrint(" RunningStatus纬度:${updatedState.status.latitude}"); //debugPrint(" RunningStatus经度:${updatedState.status.longitude}"); // debugPrint("障碍物${updatedState.status.obstacleFlag}"); // debugPrint("初始化${updatedState.status.headingStatus}"); // 提取经纬度并更新本地变量 currentLat = updatedState.status.latitude; currentLng = updatedState.status.longitude; } else {} return Scaffold( body: SafeArea( child: Stack( children: [ RepaintBoundary( key: _mapRepaintKey, child: _buildMap( currentLat, currentLng, obfFlag, headingStatus, controlMode, ), ), // 地图核心组件 if (_isRefreshing) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.3), child: const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ], ), ), ), ), // 悬浮返回按钮(核心修改) Positioned( top: 10, left: 10, child: GestureDetector( // 自定义点击事件 onTap: _navigateBack, // 自定义点击反馈(替代 IconButton 的高亮/水波纹) child: Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 3, offset: const Offset(0, 2), ), ], ), // 核心:用 Stack 实现点击高亮层 + 图标层 child: Stack( alignment: Alignment.center, // 所有子组件居中 children: [ // 1. 点击高亮层(默认隐藏,点击时显示) Positioned.fill( child: Container( decoration: BoxDecoration( color: Colors.transparent, // 默认透明 borderRadius: BorderRadius.circular(20), ), ), ), // 2. 图标层(绝对居中) const Icon( Icons.arrow_back_ios, color: Colors.black87, size: 20, ), ], ), ), ), ), /// 核心:地图正中心固定定位标 if (_currentRobotMode == RobotMode.point) Positioned( left: 0, right: 0, top: 0, bottom: 0, child: Center( // 自定义十字标(中心精准对齐地图中心) child: SizedBox( width: 16, // 十字整体宽度 height: 16, // 十字整体高度 child: CustomPaint( painter: CrosshairPainter(), // 自定义十字画笔 ), ), ), ), //保存按钮 if (_saveBoxOpen) Positioned( right: 80, top: maxTop > 10 ? 11 : maxTop, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: const Color(0xFF00C853), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: IconButton( onPressed: () { _showSavePlotDialog(); }, icon: const Icon( Icons.save, color: Colors.white, size: 18, ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ), ), // 右侧悬浮菜单 Positioned( height: 336, right: 16, top: maxTop > 10 ? 11 : maxTop, child: FloatingActionButton( onPressed: _moveToCurrentLocation, child: VerticalFloatMenu( selectedWorkModeIn: _currentWorkMode ?? WorkMode.bow, onEditTap: (bool isOpen) { setState(() { _isPanelOpen = isOpen; }); }, onListBox: (bool isOpen) { // 🔥 使用新接口:根据场站ID加载作业记录 final selectedSite = sl().state.selectedSite; if (selectedSite == null) { _showPageToast( message: '请先选择场站', type: ToastType.error, ); return; } print( '加载作业记录,当前场站ID:${selectedSite.id},场站名称:${selectedSite.siteName}', ); context.read().loadWorkRecordsBySiteId( selectedSite.id, ); setState(() { _isListBoxOpen = isOpen; _isWorkPanelOpen = false; }); _saveDataToLocal(); }, onDjShow: (bool isOpen) { if (isOpen) context.push(RoutePaths.djMap); setState(() { isDjMapShow = isOpen; }); }, onWorkModeSelected: (mode) { debugPrint('外部收到作业模式选择:$mode'); setState(() { _currentWorkMode = mode; _saveDataToLocal(); }); debugPrint('外部收到作业模式:$mode'); }, handleGotoLocation: _handleGotoLocation, onRefreshTap: _handleRefresh, // 推荐用局部刷新 onVideoTap: _handleVideo, ), ), ), // 底部操作面板 if (_isPanelOpen) Positioned( left: 0, right: 0, bottom: 0, child: BottomOperationPanel( initialRobotMode: _currentRobotMode, initialAreaMode: _currentAreaMode, initialWorkMode: _currentWorkMode!, isShowButton: _currentWorkMode == WorkMode.bow ? true : false, canUndo: _isWorkAreaCompleted && _currentAreaMode == AreaMode.work ? false : true, onComplete: () async { debugPrint( '操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle', ); if (_currentAreaMode == AreaMode.obstacle) { // 障碍物模式:完成当前障碍物绘制 _completeObstacle(); await _generatePath(showTips: true); } else { // 作业区域模式:生成路径 await _generatePath(showTips: true); } // 5. 关闭操作面板 setState(() { //_isPanelOpen = false; // 关闭操作面板 }); }, onUndoTap: () { _undoAction(); }, onDeleteTap: () { _deleteAllAction(); }, onRobotModeChanged: (mode) { setState(() => _currentRobotMode = mode); debugPrint('外部收到机器人模式:$mode'); }, onAreaModeChanged: (mode) { setState(() => _currentAreaMode = mode); debugPrint('外部收到区域模式:$mode'); }, onAddTap: () { _addMarkedPoint(); debugPrint('外部处理加号按钮点击,已添加打点'); }, onDistanceTap: (distance) { _workDistance = distance; debugPrint( '外部处理作业行距距离设置,当前距离:${distance.toStringAsFixed(1)}米', ); if (_markedPoints.length >= 3 && _currentWorkMode != null) { _generatePath(showTips: false); } }, onSettingTap: () { setState(() { _directionBoxOpen = true; }); }, onLandTap: () { debugPrint('外部处理地块标签点击'); }, onRouteTap: () { debugPrint('外部处理航线标签点击'); }, ), ), // 航线方向面板 if (_directionBoxOpen) Positioned( left: 0, right: 0, bottom: 0, child: RouteDirectionPanel( initialOptimalHeading: true, initialDirection: 0.0, titleText: AppLocalizations.of( context, ).translate('route_planning.route_direction'), optimalHeadingText: AppLocalizations.of( context, ).translate('route_planning.optimal_heading'), routeDirectionText: AppLocalizations.of( context, ).translate('route_planning.route_direction'), onValueChanged: (result) { _angle = result['optimalHeading'] == true ? -1 : result['direction']; if (_markedPoints.length >= 3 && _currentWorkMode != null) { _generatePath(showTips: false); } }, onCancel: () { setState(() => _directionBoxOpen = false); }, ), ), // 列表面板 - 使用BlocBuilder监听DevicesCubit状态 if (_isListBoxOpen) BlocBuilder( builder: (context, state) { final plotList = _convertWorkRecordsToPlotData( state.workRecords ?? [], ); return Positioned.fill( child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Expanded( child: GestureDetector( onTap: () { setState(() { _isListBoxOpen = false; }); }, child: Container( color: Colors.black.withOpacity(0.3), ), ), ), Container( width: double.infinity, decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical( top: Radius.circular(16), ), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 10, offset: Offset(0, -2), ), ], ), constraints: const BoxConstraints( maxHeight: 600, minHeight: 200, ), child: Column( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( AppLocalizations.of(context) .translate( 'route_planning.plot_list_title', ) .replaceAll( '%d', plotList.length.toString(), ), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), IconButton( onPressed: () { setState(() { _isListBoxOpen = false; }); }, icon: const Icon( Icons.close, color: Colors.grey, size: 20, ), ), ], ), ), const Divider(height: 1, color: Colors.grey), Expanded( child: plotList.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.inbox_outlined, color: Colors.grey, size: 48, ), const SizedBox(height: 16), Text( AppLocalizations.of( context, ).translate( 'route_planning.no_plot_data', ), style: const TextStyle( color: Colors.grey, fontSize: 16, ), ), ], ), ) : ListView.builder( padding: const EdgeInsets.symmetric( vertical: 8, ), itemCount: plotList.length, itemBuilder: (context, index) { final plot = plotList[index]; // 传入删除回调(调用Bloc的删除方法) return _buildPlotListItem(plot, ( deletedPlot, ) async { await context .read() .deleteWorkRecord( deletedPlot.plotName, ); final userId = context .read() .state .user ?.userId ?? ""; await context .read() .loadWorkRecords(userId); setState(() {}); }); }, ), ), ], ), ), ], ), ); }, ), if (_showHeadingWarn) Positioned( top: 100, // 调整位置,避免遮挡核心内容 left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.9), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( children: [ const Icon( Icons.warning_amber_rounded, color: Colors.white, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( AppLocalizations.of( context, ).translate('route_planning.heading_not_init'), style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ), ], ), ), ), // 2. 远程模式提示 if (_showControlModeWarn) Positioned( top: 160, // 与上一个提示错开位置 left: 20, right: 20, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: BoxDecoration( color: Colors.red.withOpacity(0.9), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black12, blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( children: [ const Icon( Icons.error_rounded, color: Colors.white, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( AppLocalizations.of( context, ).translate('route_planning.switch_remote_mode'), style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ), ], ), ), ), if (_isWorkPanelOpen) _buildWorkPanel(headingStatus), if (_isVideoDialogOpen) _buildVideoPopup(), ], ), ), ); }, ); } } // 转换单个WGS84坐标到GCJ02 LatLng convertWGS84ToGCJ02(double lat, double lon) { return wgs84ToGcj02(lat, lon); } /// ======================================================= /// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用) /// ======================================================= const double _pi = 3.1415926535897932384626; const double _a = 6378245.0; const double _ee = 0.00669342162296594323; LatLng wgs84ToGcj02(double lat, double lon) { if (_outOfChina(lat, lon)) { return LatLng(lat, lon); } double dLat = _transformLat(lon - 105.0, lat - 35.0); double dLon = _transformLon(lon - 105.0, lat - 35.0); double radLat = lat / 180.0 * _pi; double magic = sin(radLat); magic = 1 - _ee * magic * magic; double sqrtMagic = sqrt(magic); dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi); dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi); double mgLat = lat + dLat; double mgLon = lon + dLon; return LatLng(mgLat, mgLon); } List batchWgs84ToGcj02(List wgs84Points) { // 遍历每个点,调用你已有的单个转换方法 return wgs84Points.map((point) { return wgs84ToGcj02(point.latitude, point.longitude); }).toList(); } LatLng bd09ToGcj02(dynamic bdLng, dynamic bdLat) { // 1. 先将字符串转为double(兼容你的初始值格式) double lng = _safeToDouble(bdLng); double lat = _safeToDouble(bdLat); // 2. BD09转GCJ02核心公式 double x = lng - 0.0065; double y = lat - 0.006; double z = math.sqrt(x * x + y * y) - 0.00002 * math.sin(y * _pi); double theta = math.atan2(y, x) - 0.000003 * math.cos(x * _pi); double gcjLng = z * math.cos(theta); double gcjLat = z * math.sin(theta); return LatLng(gcjLat, gcjLng); } double _safeToDouble(dynamic value) { if (value == null) return 0.0; if (value is double) return value; if (value is int) return value.toDouble(); if (value is String) { // 字符串转数字,失败返回0.0 return double.tryParse(value) ?? 0.0; } return 0.0; } LatLng gcj02ToWgs84(double lat, double lon, {int iterations = 10}) { //if (_outOfChina(lat, lon)) { // return LatLng(lat, lon); //} //double dLat = _transformLat(lon - 105.0, lat - 35.0); //double dLon = _transformLon(lon - 105.0, lat - 35.0); //double radLat = lat / 180.0 * _pi; //double magic = sin(radLat); //magic = 1 - _ee * magic * magic; //double sqrtMagic = sqrt(magic); //dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi); //dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi); //double mgLat = lat - dLat; //double mgLon = lon - dLon; //return LatLng(mgLat, mgLon); if (_outOfChina(lat, lon)) { return LatLng(lat, lon); } double initLat = lat; double initLon = lon; double delta = 1e-6; // 迭代精度(约0.1米) for (int i = 0; i < iterations; i++) { // 1. 用当前猜测值计算GCJ02坐标 LatLng gcjGuess = wgs84ToGcj02(initLat, initLon); // 2. 计算与原始GCJ02的差值 double dLat = gcjGuess.latitude - lat; double dLon = gcjGuess.longitude - lon; // 3. 修正猜测值 initLat -= dLat; initLon -= dLon; // 4. 精度达标则提前退出 if (sqrt(dLat * dLat + dLon * dLon) < delta) { break; } } return LatLng(initLat, initLon); } bool _outOfChina(double lat, double lon) { return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271; } double _transformLat(double x, double y) { double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(x.abs()); ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; ret += (20.0 * sin(y * _pi) + 40.0 * sin(y / 3.0 * _pi)) * 2.0 / 3.0; ret += (160.0 * sin(y / 12.0 * _pi) + 320 * sin(y * _pi / 30.0)) * 2.0 / 3.0; return ret; } double _transformLon(double x, double y) { double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs()); ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0; ret += (20.0 * sin(x * _pi) + 40.0 * sin(x / 3.0 * _pi)) * 2.0 / 3.0; ret += (150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0; return ret; } /// 计算两个经纬度点之间的地面距离(单位:米,GCJ02/WGS84通用) String _calculateLatLngDistance(LatLng point1, LatLng point2) { const double earthRadius = 6371000; // 地球半径(米) // 经纬度转弧度 double lat1Rad = point1.latitude * pi / 180; double lat2Rad = point2.latitude * pi / 180; double lng1Rad = point1.longitude * pi / 180; double lng2Rad = point2.longitude * pi / 180; // 哈维正弦公式(Haversine)计算地球表面两点实际距离 double dLat = lat2Rad - lat1Rad; double dLng = lng2Rad - lng1Rad; double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1Rad) * cos(lat2Rad) * sin(dLng / 2) * sin(dLng / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distance = earthRadius * c; // 格式化显示:短距离显示米,长距离显示千米 if (distance < 1000) { return '${distance.toStringAsFixed(1)} 米'; } else { return '${(distance / 1000).toStringAsFixed(2)} 千米'; } } /// 获取两个坐标点的中点坐标(用于显示距离文本) LatLng _getMiddleLatLng(LatLng point1, LatLng point2) { double middleLat = (point1.latitude + point2.latitude) / 2; double middleLng = (point1.longitude + point2.longitude) / 2; return LatLng(middleLat, middleLng); } // 🔥 万能格式化打印:输出完整结构(层级+字段名+值) String formatPrettyJson(dynamic obj) { try { if (obj == null) return 'null'; if (obj is List) { return obj.map((e) => formatPrettyJson(e)).toList().toString(); } if (obj is Map) { Map map = {}; obj.forEach((k, v) { map[k.toString()] = formatPrettyJson(v); }); return map.toString(); } // 自定义模型:转 Map try { return obj.toJson().toString(); } catch (e) { return obj.toString(); } } catch (e) { return obj.toString(); } }