From a1d0c50d56533b08551ab3423c614e660a67bce3 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Fri, 6 Mar 2026 17:19:26 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A7=84=E5=88=92=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=20=E8=8E=B7=E5=8F=96=E5=AE=9E=E6=97=B6=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/device_http_datasource_impl.dart | 24 +- .../pages/running_status_page.dart | 22 +- .../presentation/widgets/common/commonFn.dart | 41 + .../widgets/map/testmap_pages.dart | 1340 +++++++++-------- .../home/presentation/widgets/obsToast.dart | 136 ++ 5 files changed, 912 insertions(+), 651 deletions(-) create mode 100644 lib/features/home/presentation/widgets/common/commonFn.dart create mode 100644 lib/features/home/presentation/widgets/obsToast.dart diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index 47021418..8f5d89fd 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -18,16 +18,13 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { // 🔥 辅助方法:获取 Token Future _getToken() async { final user = await _userStorage.getUser(); - debugPrint('用户信息: $user') ; + debugPrint('用户信息: $user'); return user?.token; } @override Future bindDevice(String deviceId, String deviceAlias) async { - var response = await dio.post( - HttpApiConsts.bindDevice, - data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}, - ); + var response = await dio.post(HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}); if (response.statusCode != 200) { throw Exception('网络请求失败:${response.statusCode}'); } @@ -51,6 +48,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { options: Options( headers: { 'Authorization': token != null ? 'Bearer $token' : '', + // 如果后端不需要 Bearer 前缀,直接写 token 即可 }, ), @@ -104,10 +102,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future unbindDevice(String deviceId, String deviceName) async { - final response = await dio.post( - '/forward/device/unbind', - data: {"deviceId": deviceId, "deviceName": deviceName}, - ); + final response = await dio.post('/forward/device/unbind', data: {"deviceId": deviceId, "deviceName": deviceName}); final data = response.data; @@ -123,10 +118,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { } Future updateDeviceName(String deviceId, String deviceName) async { - final response = await dio.post( - '/forward/device/updateDeviceAlias', - data: {"deviceId": deviceId, "deviceAlias": deviceName}, - ); + final response = await dio.post('/forward/device/updateDeviceAlias', data: {"deviceId": deviceId, "deviceAlias": deviceName}); final data = response.data; @@ -163,11 +155,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { if (data['code'] == 200) { //return data['data']; // 返回设备数据 final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化 - return DeviceLocationEntity( - deviceName: locationData['deviceName'], - latitude: locationData['latitude'], - longitude: locationData['longitude'], - ); + return DeviceLocationEntity(deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude']); } else { throw Exception(data['msg'] ?? '业务异常'); } diff --git a/lib/features/home/presentation/pages/running_status_page.dart b/lib/features/home/presentation/pages/running_status_page.dart index deb963e7..a6bbc82a 100644 --- a/lib/features/home/presentation/pages/running_status_page.dart +++ b/lib/features/home/presentation/pages/running_status_page.dart @@ -5,6 +5,7 @@ import 'dart:math' as math; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; import 'package:flutter/animation.dart'; @@ -78,23 +79,6 @@ class _RunningStatusPageState extends State { } } - String _parseLocationQuality(int locationValue) { - switch (locationValue) { - case 0: - return '无效'; - case 1: - return 'GPS 单点定位'; - case 2: - return 'DGPS 伪距差分或 SBAS'; - case 4: - return 'RTK 固定解'; - case 5: - return 'RTK 浮点解'; - default: - return '未知($locationValue)'; - } - } - void _appendChartData(DeviceStatusUpdated state) { final status = state.status; @@ -711,6 +695,7 @@ class _RunningStatusPageState extends State { TableRow(children: [_buildTableCell("电量(%)"), _buildTableCell(status.battery)]), TableRow(children: [_buildTableCell("芯片温度(°C)"), _buildTableCell(status.chipTemp.toStringAsFixed(2))]), TableRow(children: [_buildTableCell("割刀速度(rpm)"), _buildTableCell(status.knifeCuttingSpeed)]), + TableRow(children: [_buildTableCell("控制模式"), _buildTableCell(LocationUtils.parseControlMode(int.parse(status.controlMode)))]), TableRow(children: [_buildTableCell("经度(°)"), _buildTableCell(status.longitude.toStringAsFixed(6))]), TableRow(children: [_buildTableCell("纬度(°)"), _buildTableCell(status.latitude.toStringAsFixed(6))]), ], @@ -793,7 +778,7 @@ class _RunningStatusPageState extends State { } catch (e) { qualValue = 0; } - qual = _parseLocationQuality(qualValue); + qual = LocationUtils.parseLocationQuality(qualValue); satelliteCnt = state.status.satelliteCnt.toString(); } else if (state is DeviceStatusError) { qual = '-'; @@ -880,7 +865,6 @@ class _RunningStatusPageState extends State { builder: (context, state) { debugPrint('🎨 [UI-Build] BlocBuilder 重建!当前状态类型:${state.runtimeType}'); - // 检测到设备切换(状态重置)时,清空图表历史数据 if (state is DeviceStatusInitial) { debugPrint('🧹 [UI] 检测到 Initial 状态,准备重置图表数据'); diff --git a/lib/features/home/presentation/widgets/common/commonFn.dart b/lib/features/home/presentation/widgets/common/commonFn.dart new file mode 100644 index 00000000..ca843347 --- /dev/null +++ b/lib/features/home/presentation/widgets/common/commonFn.dart @@ -0,0 +1,41 @@ +// common/location_utils.dart +/// 定位质量解析工具类 +class LocationUtils { + /// 解析定位质量值为可读字符串 + /// [locationValue] 定位质量编码(0/1/2/4/5) + /// 返回:对应的中文描述 + static String parseLocationQuality(int locationValue) { + switch (locationValue) { + case 0: + return '无效'; + case 1: + return 'GPS 单点定位'; + case 2: + return 'DGPS 伪距差分或 SBAS'; + case 4: + return 'RTK 固定解'; + case 5: + return 'RTK 浮点解'; + default: + return '未知($locationValue)'; + } + } + + static String parseControlMode(int value) { + switch (value) { + case 3: + return '远程控制'; + + default: + return '本地控制'; + } + } + + // 【可选扩展】可添加更多定位相关工具方法 + // 示例:校验定位质量是否有效 + static bool isLocationValid(int locationValue) { + return locationValue != 0; + } + + +} diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index 37fe8f34..1cef43d8 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -23,6 +23,9 @@ import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_p 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/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/home/presentation/widgets/BottomDirectionLine.dart'; @@ -31,6 +34,7 @@ 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/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'; @@ -73,6 +77,8 @@ class _MapPageEnterpriseState extends State { Offset _videoPopupPos = const Offset(0, 100); // 弹窗初始位置 // 🔥 关键修复:添加截图全局Key final GlobalKey _mapRepaintKey = GlobalKey(); + // 4. 标记点集合(flutter_map需要Set类型) + Set _markers = {}; final MapController _mapController = MapController(); // 初始化轨迹管理器(泛型指定为LatLng) @@ -80,6 +86,7 @@ class _MapPageEnterpriseState extends State { late String workMode = "弓字模式"; // 作业模式:默认值为"弓字模式" WorkStatus _workStatus = WorkStatus.idle; + bool isStartWork = false; //是否开始作业 LatLng? _currentLatLng; StreamSubscription? _positionSub; @@ -101,10 +108,13 @@ class _MapPageEnterpriseState extends State { List gcjPathPoints = []; List gcjOuterPoints = []; + List? tracePoint = []; //从tracepoint中获取的经纬度坐标 + List? gctracePoint = []; + // ========== 核心新增状态 ========== final List _markedPoints = []; // 存储所有打点坐标 LatLng _mapCenter = const LatLng(39.9042, 116.4074); - final double _headingAngle = 0.0; // 当前机器航向角(单位:度) + double _headingAngle = 0.0; // 当前机器航向角(单位:度) List? _currentTracePoints; //收到机器的点坐标 List typedPathList = []; // 存储生成路径的坐标列表(已转换为LatLng) @@ -122,18 +132,16 @@ class _MapPageEnterpriseState extends State { super.initState(); _traceManager = TracePoint(); _initLocationEnterprise(); - _sub = _dispatcher.onCommand(0x12).listen(_onDeviceData); - // 设置事件回调(更新UI) - _traceManager.onCurrentPointUpdated = (point) { - setState(() { - _currentTracePoints = _traceManager.getTracePoint(); - }); - }; - _traceManager.onCompletePointAdded = (point) { - setState(() { - _currentTracePoints = _traceManager.getTracePoint(); - }); - }; + //_traceManager.onCurrentPointUpdated = (point) { + // setState(() { + // _currentTracePoints = _traceManager.getTracePoint(); + // }); + //}; + //_traceManager.onCompletePointAdded = (point) { + // setState(() { + // _currentTracePoints = _traceManager.getTracePoint(); + // }); + //}; // 示例:切换到导航模式 _traceManager.setMode(TPMode.LOCATION); @@ -196,35 +204,82 @@ class _MapPageEnterpriseState extends State { return null; } - void _onDeviceData(RawPacket packet) { + bool _isValidLatLng(double lat, double lng) { + return lat != 0 && lng != 0 && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180; + } + + //void _updateMarkers() { + // if (_currentLatLng == null) { + // _markers = {}; + // return; + // } + + // // 构建flutter_map的Marker + // _markers = { + // Marker( + // // 标记点坐标 + // point: _currentLatLng!, + // // 标记点宽高 + // width: 40, + // height: 40, + // // 标记点样式(替换为你的自定义样式,先试用默认容器验证) + // child: Container( + // decoration: BoxDecoration( + // color: Colors.red, + // borderRadius: BorderRadius.circular(20), + // border: Border.all(color: Colors.white, width: 2), + // ), + // child: const Center(child: Text('🚗', style: TextStyle(fontSize: 20))), + // // 如果你有自定义Painter,替换为: + // // child: CustomPaint( + // // size: const Size(40, 40), + // // painter: HeadingMarkerPainter(headingAngle: _headingAngle), + // // ), + // ), + // ), + // }; + + // // 触发UI刷新(flutter_map会自动更新标记点) + // if (mounted) { + // setState(() {}); + // } + //} + + void _onDeviceData(double lng, double lat, bool obfFlag, int headingStatus, controlMode) { try { - debugPrint('>>> 收到0x12设备数据: ${packet}'); - debugPrint('>>> 收到设备数据: ${utf8.decode(packet.payload)}'); - final csv = utf8.decode(packet.payload); - final fields = csv.split(','); - // 至少需 16 个字段(索引 10=航向角, 13=卫星数, 14=定位质量) - if (fields.length >= 16) { - //setState(() { - // _voltage = fields[0]; // 电压 - // _leftSpeed = fields[1]; // 左轮目标速度 - // _rightSpeed = fields[2]; // 右轮目标速度 - // _pitch = fields[11]; // 俯仰角 - // _roll = fields[12]; // 翻滚角 - // _latitude = fields[16]; // 纬度 - // _longitude = fields[17]; // 经度 - // _timeStamp = '${fields[16]} ${fields[17]}'; // 时间戳(合并字段) - // _knifeCuttingSpeed = fields[19]; // 割刀速度 - // _controlMode = fields[20]; // 控制模式 - // _workingArea = fields[21]; // 作业面积 - // _battery = fields[22]; // 电量 - // _obstacleFlag = fields[23]; // 障碍物标志 - // _yaw = fields[10]; // 航向角 - // _satelliteCnt = fields[13]; // 卫星数 - // _qual = fields[14]; // 定位质量 - //}); + // 1. 校验坐标有效性 + if (!_isValidLatLng(lat, lng)) { + debugPrint('坐标无效:纬度=$lat, 经度=$lng,跳过处理'); + return; } - } catch (_) { - // 解析失败时忽略,避免崩溃 + + if (isStartWork && headingStatus == 0) { + ToastUtils.showWarn(context, '航向角未初始化'); + return; + } + if (isStartWork && controlMode != "3") { + ToastUtils.showWarn(context, '请切换到远程模式'); + } + //if (isStartWork && obfFlag) { + // ObsToastWidget.show(context: context, message: "小迈提醒您前方有障碍物哦!"); + //} else { + // ObsToastWidget.dismiss(); + //} + + final wgsPoint = LatLng(lat, lng); + + // 2. 转换为高德GCJ02坐标系 + + // 可选:更新航向角(如果有角度数据) + // _headingAngle = updatedState.status.heading ?? 0.0; + // 可选:插入队列 + _traceManager.upsert(wgsPoint, TPAction.UPDATE); + tracePoint = _traceManager.getTracePoint(); + gctracePoint = batchWgs84ToGcj02(tracePoint!); + + debugPrint("$gctracePoint gctracePoint"); + } catch (e) { + debugPrint('处理经纬度数据失败:$e'); } } @@ -565,31 +620,18 @@ class _MapPageEnterpriseState extends State { /// =============================== /// 企业级定位初始化(已修复坐标系) - /// =============================== Future _initLocationEnterprise() async { - final serviceEnabled = await Geolocator.isLocationServiceEnabled(); - if (!serviceEnabled) return; + // 1. 设置轨迹模式为 LOCATION(核心:确保模式正确) + _traceManager.setMode(TPMode.LOCATION); - LocationPermission permission = await Geolocator.checkPermission(); - if (permission == LocationPermission.denied) { - permission = await Geolocator.requestPermission(); - } - if (permission != LocationPermission.whileInUse && permission != LocationPermission.always) { - return; - } + // 2. 清空旧的定位监听(如果有) + await _positionSub?.cancel(); + _positionSub = null; - // 1️⃣ 首次强制获取定位 - final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.bestForNavigation); - - final gcj = wgs84ToGcj02(position.latitude, position.longitude); - _updateLocation(gcj, moveMap: true); - - // 2️⃣ 实时监听(不再强制移动地图) - _positionSub = Geolocator.getPositionStream(locationSettings: const LocationSettings(accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 2)) - .listen((pos) { - final gcj = wgs84ToGcj02(pos.latitude, pos.longitude); - _updateLocation(gcj, moveMap: false); - }); + // 3. 初始化默认坐标(可选,避免地图空白) + setState(() { + _currentLatLng = const LatLng(39.9042, 116.4074); // 北京默认坐标 + }); } // ========== 新增:页面刷新初始化方法 ========== @@ -607,6 +649,7 @@ class _MapPageEnterpriseState extends State { // 2. 重置所有页面状态(恢复初始值) setState(() { + _traceManager.setMode(TPMode.LOCATION); // 清空打点和轨迹数据 _markedPoints.clear(); gcjPathPoints.clear(); @@ -689,9 +732,12 @@ class _MapPageEnterpriseState extends State { _currentLatLng = latLng; }); - if (moveMap && !_hasMovedOnce) { - _hasMovedOnce = true; - _mapController.move(latLng, 17); + if (moveMap) { + //_hasMovedOnce = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _hasMovedOnce = true; + _mapController.move(latLng, 17); + }); } } @@ -1108,9 +1154,270 @@ class _MapPageEnterpriseState extends State { ); } + Widget _buildMap(double lat, double lng, bool obfFlag, int headingStatus, String controlMode) { + if (lat != 0 && lat != 0 && (lat != _currentLatLng?.latitude || lng != _currentLatLng?.longitude)) { + _onDeviceData(lng, lat, obfFlag, headingStatus, controlMode); + } + final gcjPoint = wgs84ToGcj02(lat, lng); + + // 关键:判断Widget是否还挂载,避免空指针 + _currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新 + return FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _currentLatLng ?? const LatLng(39.9042, 116.4074), + initialZoom: 20, + maxZoom: 22, + // 禁止地图点击事件(避免和中心标冲突) + onTap: (_, __) {}, // 空实现,禁用地图点击响应 + ), + children: [ + /// 高德瓦片(GCJ-02) + TileLayer( + urlTemplate: + 'https://webrd02.is.autonavi.com/appmaptile' + '?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1' + '&key=bbb1f0f20eed6bf679eddf2625630aba', + ), + + // 绘制path折线(Line模式) + if (gcjPathPoints.isNotEmpty) + PolylineLayer( + polylines: [ + Polyline( + points: gcjPathPoints, // 转换后的GCJ02坐标 + color: const Color.fromARGB(255, 223, 228, 116), // 折线颜色(可自定义) + strokeWidth: 1.0, // 折线宽度 + isDotted: false, // 非虚线(Line模式) + borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 + borderStrokeWidth: 0.5, + ), + ], + ), + + // 绘制outer边框(Polygon模式) + if (gcjOuterPoints.isNotEmpty && _currentWorkMode == WorkMode.bow) + PolygonLayer( + polygons: [ + Polygon( + points: gcjOuterPoints, // 转换后的GCJ02坐标 + color: Colors.green.withOpacity(0.1), // 内部填充色(透明) + borderColor: Colors.green, // 边框颜色 + borderStrokeWidth: 1.0, // 边框宽度 + isFilled: true, // 开启填充(即使透明,也需要开启才能显示边框) + ), + ], + ), + if (gcjPathPoints.isNotEmpty && _currentWorkMode == WorkMode.custom) + PolylineLayer( + polylines: [ + Polyline( + points: gcjPathPoints, // 转换后的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) + PolylineLayer( + polylines: [ + Polyline(points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5), + ], + ), + + /// 当前定位 Marker + if (_currentLatLng != null) + MarkerLayer( + markers: [ + Marker( + point: _currentLatLng!, + width: 40, + height: 40, + child: CustomPaint( + size: const Size(40, 40), + painter: HeadingMarkerPainter(headingAngle: _headingAngle), + ), + ), + ], + ), + if (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted) + 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(), + ), + if (gctracePoint!.isNotEmpty) + PolylineLayer( + polylines: [ + // 已绘制的障碍物点连线 + for (int i = 0; i < gctracePoint!.length - 1; i++) + Polyline( + points: [gctracePoint![i], gctracePoint![i + 1]], + color: Colors.white, + strokeWidth: 1.5, + isDotted: true, // 虚线区分作业区域 + ), + ], + ), + + // 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, // 虚线区分作业区域 + ), + // 最后一个点到地图中心的连线 + 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), + ), + ), + ), + ], + ), + ), + ], + ), + 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(0xFF00C853), // 绿色主题色 + 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(), + ), + ], + ); + } + /// 开始作业 void _startWork() async { - if (gcjPathPoints.isEmpty) return; + if (gcjPathPoints.isEmpty) { + ToastUtils.showInfo(context, '作业列表为空,请重新选择路径'); + return; + } + isStartWork = true; + _traceManager.setMode(TPMode.NAVIGATION); setState(() { _workStatus = WorkStatus.working; @@ -1144,7 +1451,6 @@ class _MapPageEnterpriseState extends State { }); await context.read().pauseRoutePlanning(); - // 模拟暂停逻辑(替换为你的实际暂停代码) debugPrint('暂停作业:${_selectedPlot!.plotName}'); } @@ -1153,11 +1459,8 @@ class _MapPageEnterpriseState extends State { _workStatus = WorkStatus.idle; }); await context.read().stopRoutePlanning(); - // 模拟停止逻辑(替换为你的实际停止代码) debugPrint('停止作业:${_selectedPlot!.plotName}'); - // 可选:显示停止提示 ToastUtils.showError(context, '作业已停止'); - //ScaffoldMessenger.of(co ntext).showSnackBar(const SnackBar(content: Text('作业已停止'), backgroundColor: Colors.redAccent)); } /// 继续作业 @@ -1585,578 +1888,380 @@ class _MapPageEnterpriseState extends State { final maxTop = screenHeight - menuHeight; final userState = context.watch().state; final deviceId = context.watch().state.selectedDevice?.deviceName; + if (deviceId != null && userState.user?.token != null) { _videoStreamUrl = "webrtc://${TCPConsts.TCP_IP}/live/livestream/$deviceId?token=${userState.user!.token}"; } else { _videoStreamUrl = ''; // 无设备/用户信息时置空 print("设备未选中或用户未登录,无法生成视频流地址"); } - return Scaffold( - body: SafeArea( - child: Stack( - children: [ - RepaintBoundary( - key: _mapRepaintKey, - child: FlutterMap( - mapController: _mapController, - options: MapOptions( - initialCenter: _currentLatLng ?? const LatLng(39.9042, 116.4074), - initialZoom: 20, - maxZoom: 22, - // 禁止地图点击事件(避免和中心标冲突) - onTap: (_, __) {}, // 空实现,禁用地图点击响应 - ), - children: [ - /// 高德瓦片(GCJ-02) - TileLayer( - urlTemplate: - 'https://webrd02.is.autonavi.com/appmaptile' - '?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1' - '&key=bbb1f0f20eed6bf679eddf2625630aba', + + return BlocBuilder( + builder: (context, state) { + // 初始化默认值 + String yaw = '--'; + String satelliteCnt = '--'; + double currentLat = 0.0; + double currentLng = 0.0; + bool obfFlag = false; //障碍物标志位 + int headingStatus = 0; + String controlMode = "0"; + DeviceStatusUpdated? updatedState; + + 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("${updatedState.status} 路径规划四十数据"); + + debugPrint("路径规划四十数据 - 经纬度:"); + debugPrint(" RunningStatus纬度:${updatedState.status.latitude}"); + debugPrint(" RunningStatus经度:${updatedState.status.longitude}"); + debugPrint("${updatedState.status.obstacleFlag},${updatedState.status.qual},${updatedState.status.controlMode}"); + debugPrint("${updatedState.status.headingStatus},${updatedState.status.yaw}"); + + // 提取经纬度并更新本地变量 + 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)], + ), + ), + ), ), - // 绘制path折线(Line模式) - if (gcjPathPoints.isNotEmpty) - PolylineLayer( - polylines: [ - Polyline( - points: gcjPathPoints, // 转换后的GCJ02坐标 - color: const Color.fromARGB(255, 223, 228, 116), // 折线颜色(可自定义) - strokeWidth: 1.0, // 折线宽度 - isDotted: false, // 非虚线(Line模式) - borderColor: Colors.white, // 可选:添加白色描边,提升辨识度 - borderStrokeWidth: 0.5, - ), - ], - ), - // 绘制outer边框(Polygon模式) - if (gcjOuterPoints.isNotEmpty && _currentWorkMode == WorkMode.bow) - PolygonLayer( - polygons: [ - Polygon( - points: gcjOuterPoints, // 转换后的GCJ02坐标 - color: Colors.green.withOpacity(0.1), // 内部填充色(透明) - borderColor: Colors.green, // 边框颜色 - borderStrokeWidth: 1.0, // 边框宽度 - isFilled: true, // 开启填充(即使透明,也需要开启才能显示边框) - ), - ], - ), - if (gcjPathPoints.isNotEmpty && _currentWorkMode == WorkMode.custom) - PolylineLayer( - polylines: [ - Polyline( - points: gcjPathPoints, // 转换后的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) - PolylineLayer( - polylines: [ - Polyline(points: [_mapCenter, _markedPoints.last], color: Colors.blue.withOpacity(0.5), strokeWidth: 1.5), - ], - ), - - /// 当前定位 Marker - if (_currentLatLng != null) - MarkerLayer( - markers: [ - Marker( - point: _currentLatLng!, - width: 40, - height: 40, - child: CustomPaint( - size: const Size(40, 40), - painter: HeadingMarkerPainter(headingAngle: _headingAngle), + // 悬浮返回按钮(核心修改) + 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 (_currentWorkMode == WorkMode.bow && _markedPoints.isNotEmpty && !_isWorkAreaCompleted) - PolygonLayer( - polygons: [ - Polygon( - points: _getPolygonPoints(), - color: Colors.green.withOpacity(0.2), - borderColor: Colors.green.withOpacity(0.5), - borderStrokeWidth: 1, - isFilled: true, - ), - ], + ), + ), + + /// 核心:地图正中心固定定位标 + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: Center( + // 自定义十字标(中心精准对齐地图中心) + child: SizedBox( + width: 16, // 十字整体宽度 + height: 16, // 十字整体高度 + child: CustomPaint( + painter: CrosshairPainter(), // 自定义十字画笔 + ), ), - 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, + ), + ), + + //保存按钮 + 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( + onEditTap: (bool isOpen) { + setState(() { + _isPanelOpen = isOpen; + }); + }, + onListBox: (bool isOpen) { + // 🔥 改动5:加载作业记录(原有逻辑保留) + final userId = context.read().state.user?.userId ?? ""; + print('加载作业记录,当前用户ID:$userId'); + context.read().loadWorkRecords(userId); + + setState(() { + _isListBoxOpen = isOpen; + _isWorkPanelOpen = false; + }); + }, + onWorkModeSelected: (mode) { + _currentWorkMode = mode; + debugPrint('外部收到作业模式:$mode'); + }, + onRefreshTap: _handleRefresh, // 推荐用局部刷新 + onVideoTap: _handleVideo, + ), + ), + ), + + // 底部操作面板 + if (_isPanelOpen) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: BottomOperationPanel( + initialRobotMode: RobotMode.point, + initialAreaMode: AreaMode.work, + initialWorkMode: _currentWorkMode!, + canUndo: _isWorkAreaCompleted && _currentAreaMode == AreaMode.work ? false : true, + onComplete: () async { + debugPrint( + '操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle', ); - }).toList(), - ), + if (_currentAreaMode == AreaMode.obstacle) { + // 障碍物模式:完成当前障碍物绘制 + _completeObstacle(); + await _generatePath(showTips: true); + } else { + // 作业区域模式:生成路径 + await _generatePath(showTips: true); + } + // 5. 关闭操作面板 + setState(() { + //_isPanelOpen = false; // 关闭操作面板 + }); + }, + onUndoTap: () { + // 区分模式:作业区域撤回/障碍物撤回 + //if (_currentAreaMode == AreaMode.work) { + // _undoLastPoint(); + //} else { + // _undoObstaclePoint(); + //} + _undoAction(); + }, + onDeleteTap: () { + _deleteAllAction(); - // 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, // 虚线区分作业区域 - ), - // 最后一个点到地图中心的连线 - Polyline(points: [_mapCenter, _currentObstaclePoints.last], color: Colors.red.withOpacity(0.5), strokeWidth: 1.5, isDotted: true), - ], + // 区分模式:作业区域清空/障碍物清空 + //if (_currentAreaMode == AreaMode.work) { + // _deleteAllPoints(); + //} else { + // _clearAllObstacles(); + //} + }, + 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; + }); + debugPrint('外部处理设置按钮点击'); + }, + onLandTap: () { + debugPrint('外部处理地块标签点击'); + }, + onRouteTap: () { + debugPrint('外部处理航线标签点击'); + }, ), + ), - // 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, + // 航线方向面板 + if (_directionBoxOpen) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: RouteDirectionPanel( + initialOptimalHeading: true, + initialDirection: 0.0, + onValueChanged: (result) { + debugPrint('最优航向:${result['optimalHeading']},角度:${result['direction']}'); + _angle = result['optimalHeading'] == true ? -1 : result['direction']; + + debugPrint('外部处理航线方向设置,当前角度:${_angle}'); + if (_markedPoints.length >= 3 && _currentWorkMode != null) { + debugPrint('外部处理航线方向设置11222:${_angle}'); + _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( - 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), - ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '地块列表(共${plotList.length}条)', + 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 + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.inbox_outlined, color: Colors.grey, size: 48), + SizedBox(height: 16), + Text('暂无地块数据', style: 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(() {}); + }); + }, + ), + ), ], ), ), - // 正在绘制的障碍物打点 - 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), - ), - ), - ), - ], - ), - ), - ], - ), - 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(0xFF00C853), // 绿色主题色 - 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(), - ), - ], - ), - ), - - // 地图核心组件 - 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), - ], - ), - ), - ), - ), - - /// 核心:地图正中心固定定位标 - 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: new VerticalFloatMenu( - onEditTap: (bool isOpen) { - setState(() { - _isPanelOpen = isOpen; - }); - }, - onListBox: (bool isOpen) { - // 🔥 改动5:加载作业记录(原有逻辑保留) - final userId = context.read().state.user?.userId ?? ""; - print('加载作业记录,当前用户ID:$userId'); - context.read().loadWorkRecords(userId); - - setState(() { - _isListBoxOpen = isOpen; - _isWorkPanelOpen = false; - }); - }, - onWorkModeSelected: (mode) { - _currentWorkMode = mode; - debugPrint('外部收到作业模式:$mode'); - }, - onRefreshTap: _handleRefresh, // 推荐用局部刷新 - onVideoTap: _handleVideo, - ), - ), + if (_isWorkPanelOpen) _buildWorkPanel(), + if (_isVideoDialogOpen) _buildVideoPopup(), + ], ), - - // 底部操作面板 - if (_isPanelOpen) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: BottomOperationPanel( - initialRobotMode: RobotMode.point, - initialAreaMode: AreaMode.work, - initialWorkMode: _currentWorkMode!, - 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: () { - // 区分模式:作业区域撤回/障碍物撤回 - //if (_currentAreaMode == AreaMode.work) { - // _undoLastPoint(); - //} else { - // _undoObstaclePoint(); - //} - _undoAction(); - }, - onDeleteTap: () { - _deleteAllAction(); - - // 区分模式:作业区域清空/障碍物清空 - //if (_currentAreaMode == AreaMode.work) { - // _deleteAllPoints(); - //} else { - // _clearAllObstacles(); - //} - }, - 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; - }); - debugPrint('外部处理设置按钮点击'); - }, - onLandTap: () { - debugPrint('外部处理地块标签点击'); - }, - onRouteTap: () { - debugPrint('外部处理航线标签点击'); - }, - ), - ), - - // 航线方向面板 - if (_directionBoxOpen) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: RouteDirectionPanel( - initialOptimalHeading: true, - initialDirection: 0.0, - onValueChanged: (result) { - debugPrint('最优航向:${result['optimalHeading']},角度:${result['direction']}'); - _angle = result['optimalHeading'] == true ? -1 : result['direction']; - - debugPrint('外部处理航线方向设置,当前角度:${_angle}'); - if (_markedPoints.length >= 3 && _currentWorkMode != null) { - debugPrint('外部处理航线方向设置11222:${_angle}'); - _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( - '地块列表(共${plotList.length}条)', - 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 - ? const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.inbox_outlined, color: Colors.grey, size: 48), - SizedBox(height: 16), - Text('暂无地块数据', style: 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 (_isWorkPanelOpen) _buildWorkPanel(), - if (_isVideoDialogOpen) _buildVideoPopup(), - ], - ), - ), + ), + ); + }, ); } } @@ -2190,6 +2295,13 @@ LatLng wgs84ToGcj02(double lat, double lon) { return LatLng(mgLat, mgLon); } +List batchWgs84ToGcj02(List wgs84Points) { + // 遍历每个点,调用你已有的单个转换方法 + return wgs84Points.map((point) { + return wgs84ToGcj02(point.latitude, point.longitude); + }).toList(); +} + LatLng gcj02ToWgs84(double lat, double lon) { if (_outOfChina(lat, lon)) { return LatLng(lat, lon); diff --git a/lib/features/home/presentation/widgets/obsToast.dart b/lib/features/home/presentation/widgets/obsToast.dart new file mode 100644 index 00000000..fb002015 --- /dev/null +++ b/lib/features/home/presentation/widgets/obsToast.dart @@ -0,0 +1,136 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ObsToastWidget extends StatefulWidget { + final String message; + final VoidCallback? onDismiss; + + const ObsToastWidget({super.key, required this.message, this.onDismiss}); + + static OverlayEntry? _currentToast; + static _ObsToastWidgetState? _currentState; + + // 显示 Toast + static void show({required BuildContext context, required String message}) { + dismiss(); + + _currentToast = OverlayEntry( + builder: (context) => ObsToastWidget( + message: message, + onDismiss: () { + _currentToast?.remove(); + _currentToast = null; + _currentState = null; + }, + ), + ); + + Overlay.of(context).insert(_currentToast!); + } + + // 主动消失(你要的) + static void dismiss() { + if (_currentState != null) { + _currentState!._dismissToast(); + } else if (_currentToast != null) { + _currentToast!.remove(); + _currentToast = null; + } + } + + @override + State createState() => _ObsToastWidgetState(); +} + +class _ObsToastWidgetState extends State with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + ObsToastWidget._currentState = this; + + _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); + + Future.delayed(const Duration(milliseconds: 50), () { + if (mounted) _animationController.forward(); + }); + + // 已删除:自动消失逻辑 + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + // 主动消失逻辑 + void _dismissToast() { + if (_animationController.status == AnimationStatus.completed) { + _animationController.reverse().then((_) { + widget.onDismiss?.call(); + ObsToastWidget._currentState = null; + }); + } else { + widget.onDismiss?.call(); + ObsToastWidget._currentState = null; + } + } + + @override + Widget build(BuildContext context) { + return Positioned( + // 定位:屏幕顶部居中(可根据需求调整) + top: 100, + left: MediaQuery.of(context).size.width * 0.1, + right: MediaQuery.of(context).size.width * 0.1, + child: FadeTransition( + opacity: _fadeAnimation, + child: Listener( + child: GestureDetector( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 2))], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // SVG 图标(替换为你的 SVG 路径/字符串) + SvgPicture.string( + ''' + + + + + + + + + + + + +''', + width: 32, + height: 32, + ), + const SizedBox(width: 8), // 图标和文字间距 + // 提示文字 + Text(widget.message, style: const TextStyle(fontSize: 14, color: Colors.black87)), + ], + ), + ), + ), + ), + ), + ); + } +}