From c4d085cb456c0c2e32f4e5c867bd65184c965b73 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Tue, 24 Mar 2026 15:23:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B7=AF=E5=BE=84=E8=A7=84?= =?UTF-8?q?=E5=88=92=E5=92=8C=20=E9=80=80=E5=87=BA=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/toast.dart | 2 + .../widgets/map/testmap_pages.dart | 438 ++++++++++-------- .../web_view/user_info_pages.dart | 100 ++-- 3 files changed, 299 insertions(+), 241 deletions(-) diff --git a/lib/components/toast.dart b/lib/components/toast.dart index 8672b948..28114169 100644 --- a/lib/components/toast.dart +++ b/lib/components/toast.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +enum ToastType { success, error, warn, info, loading } + // 自定义Toast工具类 - 气泡式提示 class ToastUtils { static OverlayEntry? _overlayEntry; diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index 63f88c21..957899b8 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -64,6 +64,7 @@ const String kSavedWorkStatus = 'saved_work_status'; // 作业状态(idle/work const String kSavedIsStartWork = 'saved_is_start_work'; // 是否开始作业 const String kSavedIsStopWork = 'saved_is_stop_work'; // 是否点击过停止 const String kSavedTPMode = "saved_tp_mode"; +const String kSavedCurrentRobotMode = "kSavedCurrentRobotMode"; // 保持 PlotData 类不变 class PlotData { @@ -97,6 +98,8 @@ class _MapPageEnterpriseState extends State { bool isreceiveFirstCompletePoint = false; Offset _videoPopupPos = const Offset(0, 100); // 弹窗初始位置 final GlobalKey _mapRepaintKey = GlobalKey(); + // 用于存储当前页面的 Toast OverlayEntry + List _toastEntries = []; final MapController _mapController = MapController(); // 初始化轨迹管理器(泛型指定为LatLng) @@ -237,7 +240,22 @@ class _MapPageEnterpriseState extends State { await LocationUtils.clearCache(LocationUtils.kSavedMarkedPoints); await LocationUtils.clearCache(LocationUtils.kSavedObstacleHoles); await LocationUtils.clearCache(LocationUtils.kSavedIsWorkAreaCompleted); - debugPrint('本地存储的绘制数据已清空'); + 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); } catch (e) { debugPrint('清空本地数据失败:$e'); } @@ -256,6 +274,22 @@ class _MapPageEnterpriseState extends State { _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; @@ -280,7 +314,7 @@ class _MapPageEnterpriseState extends State { 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().toList(); + _markedPoints = markedList.map((item) => LatLng(double.parse(item['lat'].toString()), double.parse(item['lng'].toString()))).toList(); } // 加载障碍物数据 @@ -298,11 +332,6 @@ class _MapPageEnterpriseState extends State { // 1. 恢复作业面板状态 _isWorkPanelOpen = prefs.getBool(kSavedIsWorkPanelOpen) ?? false; - // if (isStartWork && !isStopWork) { - // _isWorkPanelOpen = true; - // } else { - // _isWorkPanelOpen = false; - // } // 2. 恢复作业模式 workMode = prefs.getString(kSavedWorkMode) ?? "弓字模式"; @@ -325,6 +354,19 @@ class _MapPageEnterpriseState extends State { ); } + // ==================== ✅ 核心修复 ==================== + // 1. 从缓存恢复后,强制让 _markedPoints 同步给外边框(解决不显示) + if (_markedPoints.isNotEmpty && gcjOuterPoints.isEmpty) { + gcjOuterPoints = List.from(_markedPoints); + } + + // 2. 关键:同步机器人模式坐标(保存时边框不丢失) + if (_currentRobotMode == RobotMode.point) { + _robotModeWgsPoints = _markedPoints.map((p) { + return gcj02ToWgs84(p.latitude, p.longitude); + }).toList(); + } + if (mounted) { setState(() {}); // 移动地图到绘制内容中心 @@ -363,6 +405,9 @@ class _MapPageEnterpriseState extends State { }).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); @@ -384,6 +429,8 @@ class _MapPageEnterpriseState extends State { // 保存当前模式到本地缓存 prefs.setString(kSavedTPMode, _traceManager.getMode().toString()); + prefs.setString(kSavedCurrentRobotMode, _currentRobotMode.toString()); + // 4. 保存选中地块 if (_selectedPlot != null) { final plotMap = { @@ -469,12 +516,14 @@ class _MapPageEnterpriseState extends State { if (isStartWork && !isStopWork) { if (headingStatus == 0) { if (!_showHeadingWarn) { - setState(() { - _showHeadingWarn = true; - }); + if (mounted) { + setState(() { + _showHeadingWarn = true; + }); + } } } else { - if (_showHeadingWarn) { + if (_showHeadingWarn && mounted) { setState(() { _showHeadingWarn = false; }); @@ -486,13 +535,13 @@ class _MapPageEnterpriseState extends State { // ============================================== String mode = controlMode?.toString() ?? ""; if (mode != "3") { - if (!_showControlModeWarn) { + if (!_showControlModeWarn && mounted) { setState(() { _showControlModeWarn = true; }); } } else { - if (_showControlModeWarn) { + if (_showControlModeWarn && mounted) { setState(() { _showControlModeWarn = false; }); @@ -500,7 +549,7 @@ class _MapPageEnterpriseState extends State { } } else { // 未作业 → 自动隐藏所有警告 - if (_showHeadingWarn || _showControlModeWarn) { + if ((_showHeadingWarn || _showControlModeWarn) && mounted) { setState(() { _showHeadingWarn = false; _showControlModeWarn = false; @@ -560,7 +609,9 @@ class _MapPageEnterpriseState extends State { } else { // 截图失败时,允许空图片保存(可根据业务调整为强制失败) _savePlotData(plotName, null); - ToastUtils.showWarn(context, '地块保存成功,但地图截图生成失败!'); + _showPageToast(message: "地块保存成功,但地图截图生成失败!", type: ToastType.warn); + + //ToastUtils.showWarn(context, '地块保存成功,但地图截图生成失败!'); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块保存成功,但地图截图生成失败!'), backgroundColor: Colors.amber)); } } @@ -621,7 +672,9 @@ class _MapPageEnterpriseState extends State { // 1. 获取用户ID final userId = context.read().state.user?.userId ?? ""; if (userId.isEmpty) { - ToastUtils.showError(context, '用户ID为空,无法保存!'); + _showPageToast(message: "用户ID为空,无法保存!", type: ToastType.error); + + //ToastUtils.showError(context, '用户ID为空,无法保存!'); return; } debugPrint("===打印生成的路径开始 "); @@ -629,10 +682,20 @@ class _MapPageEnterpriseState extends State { debugPrint(typedPathList[i].toString()); } debugPrint("===打印生成的路径结束"); + debugPrint("$_robotModeWgsPoints $gcjOuterPoints $_currentRobotMode _robotModeWgsPointsgcjOuterPoints "); // 2. 构造 SavePath 数据 (严格对应你的 JS 结构) final Map savePath = { //'img': imgBase64 ?? '', // 截图的 Base64 字符串 'name': plotName, // 地块名称 + 'outer': + // 核心修改:根据机器人模式选择不同的坐标源 + (_currentRobotMode == RobotMode.robot ? _robotModeWgsPoints : gcjOuterPoints).map((point) { + final wgs84 = _currentRobotMode == RobotMode.robot + ? point // Robot模式直接使用原始WGS84坐标 + : gcj02ToWgs84(point.latitude, point.longitude); // Point模式转换 + + return {'lng': wgs84.longitude, 'lat': wgs84.latitude}; + }).toList(), 'path': _currentWorkMode == WorkMode.custom ? [] // 自定义模式 path 为空数组 : typedPathList @@ -645,18 +708,9 @@ class _MapPageEnterpriseState extends State { ) .toList(), - 'outer': - // 核心修改:根据机器人模式选择不同的坐标源 - (_currentRobotMode == RobotMode.robot ? _robotModeWgsPoints : gcjOuterPoints).map((point) { - final wgs84 = _currentRobotMode == RobotMode.robot - ? point // Robot模式直接使用原始WGS84坐标 - : gcj02ToWgs84(point.latitude, point.longitude); // Point模式转换 - - return {'lng': wgs84.longitude, 'lat': wgs84.latitude}; - }).toList(), - 'planModel': _currentWorkMode == WorkMode.bow ? 0 : 2, // 作业模式值 }; + debugPrint("$savePath savePath"); // 3. 构造 workRecord (包裹一层) final Map workRecord = { @@ -690,11 +744,11 @@ class _MapPageEnterpriseState extends State { ), ); - //_debugPrintMultipartRequest(request); - // 7. 发送请求 try { - ToastUtils.showLoading(context, '正在保存地块「$plotName」...'); + _showPageToast(message: "正在保存地块「$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(); @@ -702,7 +756,9 @@ class _MapPageEnterpriseState extends State { debugPrint('保存地块响应体: $responseBody'); if (response.statusCode == 200) { - ToastUtils.showSuccess(context, '地块「$plotName」保存成功!'); + _showPageToast(message: "地块「$plotName」保存成功!", type: ToastType.success); + + //ToastUtils.showSuccess(context, '地块「$plotName」保存成功!'); _clearLocalData(); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('地块「$plotName」保存成功!'), backgroundColor: Colors.green)); // 保存成功 @@ -711,6 +767,7 @@ class _MapPageEnterpriseState extends State { gcjPathPoints.clear(); // 清空路径 gcjOuterPoints.clear(); // 清空外边界 _robotModeWgsPoints.clear(); + _obstacleHoles.clear(); _isPanelOpen = false; // 关闭操作面板 _isWorkAreaCompleted = false; // 重置作业区域完成状态 @@ -720,7 +777,9 @@ class _MapPageEnterpriseState extends State { } } catch (e) { debugPrint('保存失败: $e'); - ToastUtils.showError(context, '保存失败: ${e.toString()}'); + _showPageToast(message: "保存失败: ${e.toString()}", type: ToastType.error); + + //ToastUtils.showError(context, '保存失败: ${e.toString()}'); //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}'), backgroundColor: Colors.red)); } } @@ -739,17 +798,21 @@ class _MapPageEnterpriseState extends State { // 1. 作业区域模式 if (_currentAreaMode == AreaMode.work) { if (_markedPoints.isNotEmpty) { - _robotModeWgsPoints.removeLast(); - _markedPoints.removeLast(); + // 单独判断,单独删除 + if (_robotModeWgsPoints.isNotEmpty) { + _robotModeWgsPoints.removeLast(); + } + if (_markedPoints.isNotEmpty) { + _markedPoints.removeLast(); + } + // 作业点撤回后,若路径已生成,重新生成路径(保持同步) if (_isWorkAreaCompleted && _markedPoints.length >= 3) { _generatePath(showTips: false); } - ToastUtils.showInfo(context, '已撤回作业区域最后一个点,剩余${_markedPoints.length}个点'); - //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回作业区域最后一个点,剩余${_markedPoints.length}个点'))); + _showPageToast(message: "已撤回作业区域最后一个点,剩余${_markedPoints.length}个点", type: ToastType.info); } else { - ToastUtils.showInfo(context, '暂无作业区域点可撤回'); - //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可撤回'))); + _showPageToast(message: "暂无作业区域点可撤回", type: ToastType.info); } return; } @@ -757,15 +820,19 @@ class _MapPageEnterpriseState extends State { // 2. 空洞模式(完成后优先撤回已完成的空洞) if (_currentAreaMode == AreaMode.obstacle) { if (_currentObstaclePoints.isNotEmpty) { - _currentObstaclePoints.removeLast(); - _robotModeObsWgsPoints.removeLast(); + // 单独判断,单独删除 + if (_currentObstaclePoints.isNotEmpty) { + _currentObstaclePoints.removeLast(); + } + if (_robotModeObsWgsPoints.isNotEmpty) { + _robotModeObsWgsPoints.removeLast(); + } + if (_currentObstaclePoints.isEmpty) { _isObstacleEditing = false; } - ToastUtils.showInfo(context, '已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点'); - - //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回当前空洞最后1个点,剩余${_currentObstaclePoints.length}个点'))); - return; // 撤完点就结束,不执行后面的逻辑 + _showPageToast(message: "已撤回当前空洞最后一个点,剩余${_currentObstaclePoints.length}个点", type: ToastType.info); + return; } // 第二步:当前空洞无点可撤 → 再撤回「上一个完整空洞」 @@ -775,15 +842,12 @@ class _MapPageEnterpriseState extends State { if (_isWorkAreaCompleted) { _generatePath(showTips: false); } - ToastUtils.showInfo(context, '已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞'); - - //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞'))); + _showPageToast(message: "已撤回上一个完整空洞,剩余${_obstacleHoles.length}个空洞", type: ToastType.info); return; } // 第三步:无任何可撤内容 - ToastUtils.showInfo(context, '暂无空洞可撤回'); - //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无空洞可撤回'))); + _showPageToast(message: "暂无空洞可撤回", type: ToastType.info); } }); _saveDataToLocal(); @@ -795,7 +859,9 @@ class _MapPageEnterpriseState extends State { // 1. 作业区域模式:清空作业点 + 重置路径 if (_currentAreaMode == AreaMode.work) { if (_markedPoints.isEmpty) { - ToastUtils.showInfo(context, '暂无作业区域点可删除'); + _showPageToast(message: "暂无作业区域点可删除", type: ToastType.info); + + //ToastUtils.showInfo(context, '暂无作业区域点可删除'); //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('暂无作业区域点可删除'))); return; @@ -804,17 +870,21 @@ class _MapPageEnterpriseState extends State { gcjPathPoints.clear(); gcjOuterPoints.clear(); _robotModeWgsPoints.clear(); + _obstacleHoles.clear(); _isWorkAreaCompleted = false; _saveBoxOpen = false; typedPathList.clear(); - ToastUtils.showSuccess(context, '已清空所有作业区域点和路径'); + _showPageToast(message: "已清空所有作业区域点和路径", 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) { - ToastUtils.showInfo(context, '暂无障碍物可删除'); + _showPageToast(message: "暂无障碍物可删除", type: ToastType.info); + + //ToastUtils.showInfo(context, '暂无障碍物可删除'); return; } _obstacleHoles.clear(); @@ -825,7 +895,8 @@ class _MapPageEnterpriseState extends State { if (_isWorkAreaCompleted) { _generatePath(showTips: false); } - ToastUtils.showSuccess(context, '已清空所有障碍物'); + _showPageToast(message: "已清空所有障碍物", type: ToastType.success); + //ToastUtils.showSuccess(context, '已清空所有障碍物'); } }); _saveDataToLocal(); @@ -844,100 +915,82 @@ class _MapPageEnterpriseState extends State { // ========== 新增:页面刷新初始化方法 ========== Future _handleRefresh() async { - debugPrint("点击刷新"); - // 防止重复刷新 + _cancelAllToast(); // 👇 加这行 if (_isRefreshing) return; - // 🔥 修复1:保存当前有效的地图中心和缩放级别(而非定位坐标) + final savedZoom = _mapController.camera?.zoom ?? 17.0; setState(() { - _isRefreshing = true; // 标记开始刷新 + _isRefreshing = true; }); - debugPrint("点击刷新111"); try { if (_currentLatLng != null && _isValidLatLng(_currentLatLng!.latitude, _currentLatLng!.longitude)) { _mapController.move(_currentLatLng!, 18); - debugPrint("点击刷新333"); } - debugPrint("点击刷新2222"); - // 2. 重置所有页面状态(恢复初始值) + setState(() { _traceManager.setMode(TPMode.LOCATION); isreceiveFirstCompletePoint = false; - // 2. 清空打点和轨迹数据 + // 清空所有绘制数据 _markedPoints.clear(); gcjPathPoints.clear(); _robotModeObsWgsPoints.clear(); gcjOuterPoints.clear(); _robotModeWgsPoints.clear(); + _obstacleHoles.clear(); - // 3. 重置作业区域状态 + // 重置作业区域状态 _isWorkAreaCompleted = false; _saveBoxOpen = false; - _showControlModeWarn = false; - _showHeadingWarn = false; - // 4. 重置面板状态 - _isPanelOpen = false; - _directionBoxOpen = false; - _isListBoxOpen = false; - _isWorkPanelOpen = false; + // ============================================== + // 🔥 核心修复:强制关闭所有弹窗/面板/警告 + // ============================================== + _showControlModeWarn = false; // 关闭远程模式警告 + _showHeadingWarn = false; // 关闭航向警告 + _isPanelOpen = false; // 关闭底部操作面板 + _directionBoxOpen = false; // 关闭航线方向面板 + _isListBoxOpen = false; // 关闭地块列表面板 + _isWorkPanelOpen = false; // 关闭作业面板 + _isVideoDialogOpen = false; // 关闭视频弹窗 + _isObstacleEditing = false; // 退出障碍物编辑 + _selectedPlot = null; // 清空选中地块 - // 5. 重置障碍物数据 - _obstacleHoles.clear(); - _currentObstaclePoints.clear(); - _isObstacleEditing = false; - - // 6. 重置选中状态 - _selectedPlot = null; - - // 7. 重置作业模式 + // 重置模式 _currentRobotMode = RobotMode.robot; _currentAreaMode = AreaMode.work; _currentWorkMode = WorkMode.bow; - // 8. 重置作业参数 + // 重置参数 _workDistance = 0.0; _angle = 0.0; - // 9. 重置地图与定位状态(关键修复) + // 重置定位 _currentLatLng = null; _currentWgsLatLng = null; - _mapCenter = _getDefaultValidCenter(); // 仅用兜底坐标初始化地图中心 + _mapCenter = _getDefaultValidCenter(); - // 10. 重置作业状态 + // 重置作业状态 isDjMapShow = false; _workStatus = WorkStatus.idle; isStartWork = false; isStopWork = false; }); - _clearLocalData(); - // 4. 重置轨迹管理器 + _clearLocalData(); _traceManager.reset(); - // 5. 重新加载作业记录(保持数据最新) - // final userId = context.read().state.user?.userId ?? ""; - // if (userId.isNotEmpty) { - // await context.read().loadWorkRecords(userId); - // } - - // 刷新成功提示 if (mounted) { - ToastUtils.showSuccess(context, '页面已刷新完成'); - //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('页面已刷新完成'), backgroundColor: Colors.green, duration: Duration(seconds: 2))); + _showPageToast(message: "页面已刷新完成", type: ToastType.success); + //ToastUtils.showSuccess(context, '页面已刷新完成'); } } catch (e) { - // 错误处理 if (mounted) { - ToastUtils.showError(context, '刷新失败:${e.toString().substring(0, 50)}'); - //ScaffoldMessenger.of( - // context, - //).showSnackBar(SnackBar(content: Text('刷新失败:${e.toString().substring(0, 50)}'), backgroundColor: Colors.red, duration: Duration(seconds: 3))); + _showPageToast(message: "刷新失败:${e.toString().substring(0, 50)}", type: ToastType.error); + //ToastUtils.showError(context, '刷新失败:${e.toString().substring(0, 50)}'); } } finally { - // 标记刷新结束 if (mounted) { setState(() { _isRefreshing = false; @@ -955,7 +1008,8 @@ class _MapPageEnterpriseState extends State { // ========== 障碍物模式:完成当前障碍物绘制 ========== void _completeObstacle() { if (_currentObstaclePoints.length < 3) { - ToastUtils.showError(context, '障碍物区域至少需要3个打点!'); + _showPageToast(message: "障碍物区域至少需要3个打点!", type: ToastType.error); + //ToastUtils.showError(context, '障碍物区域至少需要3个打点!'); return; } @@ -972,15 +1026,16 @@ class _MapPageEnterpriseState extends State { _isObstacleEditing = false; // 退出编辑状态 }); _saveDataToLocal(); - - ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域'); + _showPageToast(message: "已添加第${_obstacleHoles.length}个障碍物区域", type: ToastType.success); + //ToastUtils.showSuccess(context, '已添加第${_obstacleHoles.length}个障碍物区域'); } // ========== 核心方法:打点逻辑 ========== void _addMarkedPoint() { _traceManager.reset(); if (_currentAreaMode == AreaMode.work && _isWorkAreaCompleted) { - ToastUtils.showError(context, '作业区域已完成,无法继续添加打点!'); + _showPageToast(message: "作业区域已完成,无法继续添加打点!", type: ToastType.error); + //ToastUtils.showError(context, '作业区域已完成,无法继续添加打点!'); return; } setState(() { @@ -996,7 +1051,8 @@ class _MapPageEnterpriseState extends State { } } else { if (_currentLatLng == null || !_isValidLatLng(_currentLatLng!.latitude, _currentLatLng!.longitude)) { - ToastUtils.showError(context, '设备坐标无效,无法添加打点!'); + _showPageToast(message: "设备坐标无效,无法添加打点!", type: ToastType.error); + //ToastUtils.showError(context, '设备坐标无效,无法添加打点!'); return; } @@ -1030,6 +1086,7 @@ class _MapPageEnterpriseState extends State { _mapController.dispose(); _pathPlanningSub?.cancel(); // 🔥 取消订阅 _saveDataToLocal(); // 🔥 页面销毁时保存最终状态 + _cancelAllToast(); // 退出页面 → 关闭所有本页面的Toast super.dispose(); } @@ -1140,6 +1197,7 @@ class _MapPageEnterpriseState extends State { List outerList = []; final rawOuter = parsedJson['outer']; + debugPrint("$rawOuter rawOuterrawOuterrawOuter"); if (rawOuter is String) { try { outerList = jsonDecode(rawOuter) as List; @@ -1275,8 +1333,6 @@ class _MapPageEnterpriseState extends State { tracePoint = _traceManager.getTracePoint(); gctracePoint = batchWgs84ToGcj02(tracePoint!); - debugPrint("$gctracePoint gctracePoint"); - //_currentLatLng = gcjPoint; // ✅ 状态变量在setState内更新 if (_isValidLatLng(gcjPoint.latitude, gcjPoint.longitude)) { _currentLatLng = gcjPoint; @@ -1569,7 +1625,8 @@ class _MapPageEnterpriseState extends State { _traceManager.reset(); if (startWorkList.isEmpty) { - ToastUtils.showInfo(context, '作业列表为空,请重新选择路径'); + _showPageToast(message: "作业列表为空,请重新选择路径", type: ToastType.info); + //ToastUtils.showInfo(context, '作业列表为空,请重新选择路径'); return; } _traceManager.setMode(TPMode.NAVIGATION); @@ -1601,7 +1658,8 @@ class _MapPageEnterpriseState extends State { ///context.read().updateAppState(AppState.routePlanning); // 可选:显示作业提示 - ToastUtils.showSuccess(context, '作业已开始'); + _showPageToast(message: "作业已开始", type: ToastType.success); + //ToastUtils.showSuccess(context, '作业已开始'); _saveDataToLocal(); //ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green)); } @@ -1618,21 +1676,23 @@ class _MapPageEnterpriseState extends State { } void _stopWork() async { - setState(() { - isStartWork = false; // 🔥 关键:停止作业标志 - _workStatus = WorkStatus.idle; - _showHeadingWarn = false; - _showControlModeWarn = false; - isStopWork = true; //已经点击过停止 - isreceiveFirstCompletePoint = false; - }); + if (mounted) { + setState(() { + isStartWork = false; // 🔥 关键:停止作业标志 + _workStatus = WorkStatus.idle; + _showHeadingWarn = false; + _showControlModeWarn = false; + isStopWork = true; //已经点击过停止 + isreceiveFirstCompletePoint = false; + }); + } //context.read().updateAppState(AppState.none); await context.read().stopRoutePlanning(); _traceManager.reset(); _saveDataToLocal(); // 🔥 保存暂停状态 - - ToastUtils.showError(context, '作业已停止'); + _showPageToast(message: "作业已停止", type: ToastType.error); + _traceManager.setMode(TPMode.LOCATION); } /// 继续作业 @@ -1975,68 +2035,57 @@ class _MapPageEnterpriseState extends State { if (_currentWorkMode == WorkMode.custom) { setState(() { - _saveBoxOpen = true; // 显示保存按钮 - _isWorkAreaCompleted = true; // 标记为完成,允许保存 - gcjPathPoints = List.from(_markedPoints); // outer直接使用打点坐标 + _saveBoxOpen = true; + _isWorkAreaCompleted = true; + gcjPathPoints = List.from(_markedPoints); gcjOuterPoints = List.from(_markedPoints); - typedPathList = []; // 路径列表置空 + typedPathList = []; }); return; } - // 1. 校验必要条件 if (_currentAreaMode == AreaMode.work && _markedPoints.isEmpty) { if (showTips) { - ToastUtils.showError(context, '请先添加作业区域打点!'); + _showPageToast(message: '请先添加作业区域打点!', type: ToastType.error); } return; } if (_currentAreaMode == AreaMode.work && _markedPoints.length < 3) { if (showTips) { - ToastUtils.showError(context, '作业区域至少需要3个打点才能生成路径!'); + _showPageToast(message: '作业区域至少需要3个打点才能生成路径!', type: ToastType.error); } return; } if (_currentWorkMode == null) { if (showTips) { - ToastUtils.showError(context, '请先选择作业模式!'); + _showPageToast(message: '请先选择作业模式!', type: ToastType.error); } return; } try { - // 2. 转换页面数据为 generatePath 所需的参数格式 - // 2.1 参考点(区分Robot/Point模式) - LatLng firstPointWgs; // 最终传给接口的WGS84格式参考点 + LatLng firstPointWgs; if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) { - // Robot模式:直接使用原始WGS84坐标(无需转换) firstPointWgs = _robotModeWgsPoints.first; } else if (_currentRobotMode == RobotMode.point && _markedPoints.isNotEmpty) { - // Point模式:先取十字准星的GCJ02坐标 → 转换为WGS84 LatLng firstPointGcj = _markedPoints.first; firstPointWgs = gcj02ToWgs84(firstPointGcj.latitude, firstPointGcj.longitude); } else { if (showTips) { - ToastUtils.showError(context, '参考点坐标为空,请先添加作业区域打点!'); + _showPageToast(message: '参考点坐标为空,请先添加作业区域打点!', type: ToastType.error); } - return; // 终止方法执行,避免后续错误 + return; } - // 参考点使用转换后的WGS84坐标 final referencePoint = work_area_model.ReferencePoint(lat: firstPointWgs.latitude, lon: firstPointWgs.longitude); - - // 2.2 航向角 final heading = _angle == -1 ? -1 : _angle.toInt(); - // 2.3 外边界(作业区域)- 核心区分Robot/Point模式 List outerPositions = []; if (_currentRobotMode == RobotMode.robot && _robotModeWgsPoints.isNotEmpty) { - // Robot模式:直接使用原始WGS84坐标,跳过GCJ02→WGS84转换 outerPositions = _robotModeWgsPoints.map((latLng) { return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude); }).toList(); } else if (_currentRobotMode == RobotMode.point && _markedPoints.isNotEmpty) { - // Point模式:十字准星的GCJ02坐标 → 转换为WGS84 outerPositions = _markedPoints.map((latLng) { final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude); return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude); @@ -2050,17 +2099,14 @@ class _MapPageEnterpriseState extends State { }).toList(); final outerBoundary = work_area_model.OuterBoundary(position: outerPositions, sideWidth: _workDistance); - work_area_model.HoleBoundary? holeBoundary; if (_obstacleHoles.isNotEmpty) { - // 1. 构造二维 Position 数组 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(); @@ -2069,8 +2115,6 @@ class _MapPageEnterpriseState extends State { } else if (_currentRobotMode == RobotMode.point && _obstacleHoles.isNotEmpty) { for (var holePoints in _obstacleHoles) { List innerPositionList = []; - - // Point模式:GCJ02→WGS84转换 innerPositionList = holePoints.map((latLng) { final wgs84Point = gcj02ToWgs84(latLng.latitude, latLng.longitude); return work_area_model.Position(lat: wgs84Point.latitude, lon: wgs84Point.longitude); @@ -2079,70 +2123,39 @@ class _MapPageEnterpriseState extends State { } } - //for (var holePoints in _obstacleHoles) { - // List innerPositionList = []; - - // if (_currentRobotMode == RobotMode.robot && _robotModeObsWgsPoints.isNotEmpty) { - // // Robot模式:使用障碍物原始WGS84坐标 - // innerPositionList = holePoints.map((latLng) { - // return work_area_model.Position(lat: latLng.latitude, lon: latLng.longitude); - // }).toList(); - // } else { - // // Point模式:GCJ02→WGS84转换 - // 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); - //} - - // 2. 创建 HoleBoundary 对象(无外层 Map) holeBoundary = work_area_model.HoleBoundary(position: positionList, sideWidth: 0.0); } - // 🔥 第二步:构造符合方法参数要求的 Map - // 用一个临时 Map 来满足类型要求,后续在 Cubit 中取值即可 Map holes = {}; if (holeBoundary != null) { - holes = {'tempKey': holeBoundary}; // 键名任意,仅为满足类型要求 + holes = {'tempKey': holeBoundary}; } final workType = _currentWorkMode == WorkMode.bow ? 0 : 2; - print('=================================================='); - print('✅ generatePath 完整参数结构:'); - print('reference: ${formatPrettyJson(referencePoint)}'); - print('heading: $heading'); - print('outer: ${formatPrettyJson(outerBoundary)}'); - print('holes: ${formatPrettyJson(holes)}'); - print('workType: $workType'); - print('=================================================='); - // 3. 调用 Cubit 的 generatePath 方法(传入holes参数) + await context.read().generatePath( reference: referencePoint, heading: heading, outer: outerBoundary, - holes: holes.isNotEmpty ? holes : {}, // 无障碍物时传空对象 + holes: holes.isNotEmpty ? holes : {}, workType: workType, ); - // 4. 处理生成结果(原有逻辑不变) final cubitState = context.read().state; if (cubitState.errorMessage != null) { if (showTips) { - ToastUtils.showError(context, '路径生成失败:${cubitState.errorMessage}'); - //ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('路径生成失败:${cubitState.errorMessage}'), backgroundColor: Colors.red)); + _showPageToast(message: '路径生成失败:${cubitState.errorMessage}', type: ToastType.error); } } else if (cubitState.generatedPath != null) { if (showTips) { - ToastUtils.showSuccess(context, '路径生成成功!'); + _showPageToast(message: '路径生成成功!', type: ToastType.success); } if (cubitState.generatedPath is List) { setState(() { _isWorkAreaCompleted = true; _saveBoxOpen = true; - _currentObstaclePoints.clear(); // 清空当前绘制的障碍物点 - _robotModeObsWgsPoints.clear(); // 清空Robot模式下的障碍物WGS84坐标 + _currentObstaclePoints.clear(); + _robotModeObsWgsPoints.clear(); final pathList = cubitState.generatedPath as List; typedPathList = pathList.whereType().toList(); gcjPathPoints = typedPathList.map((point) { @@ -2151,8 +2164,6 @@ class _MapPageEnterpriseState extends State { final gcjPoint = wgs84ToGcj02(wgs84Lat, wgs84Lon); return LatLng(gcjPoint.latitude, gcjPoint.longitude); }).toList(); - // 外边界保持原有逻辑 - //gcjOuterPoints = List.from(_markedPoints); }); _saveDataToLocal(); moveMapToPointsCenter(gcjPathPoints); @@ -2161,11 +2172,63 @@ class _MapPageEnterpriseState extends State { } catch (e) { debugPrint('路径生成异常:$e'); if (showTips) { - ToastUtils.showError(context, '路径生成异常:${e.toString().substring(0, 50)}'); + _showPageToast(message: '路径生成异常:${e.toString().substring(0, 50)}', type: ToastType.error); } } } + void _cancelAllToast() { + for (final entry in _toastEntries) { + if (entry.mounted) { + entry.remove(); + } + } + _toastEntries.clear(); + } + + // 只在当前页面显示安全 Toast,退出自动消失 + void _showPageToast({required String message, required ToastType type}) { + _cancelAllToast(); + + 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(type), borderRadius: BorderRadius.circular(8)), + child: Text(message, style: const TextStyle(color: Colors.white, fontSize: 14)), + ), + ), + ), + ); + + _toastEntries.add(entry); + Overlay.of(context)?.insert(entry); + + // 2秒后自动关闭 + Future.delayed(const Duration(seconds: 2), () { + if (entry.mounted) entry.remove(); + _toastEntries.remove(entry); + }); + } + + 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; @@ -2208,11 +2271,14 @@ class _MapPageEnterpriseState extends State { //有停止信号 if (isFinishWork) { _workStatus = WorkStatus.idle; - ToastUtils.showError(context, '作业已停止'); + + _showPageToast(message: "作业已停止", type: ToastType.error); + //ToastUtils.showError(context, '作业已停止'); // 👇 延迟 1 秒钟再执行 reset(不会阻塞UI,安全) Future.delayed(const Duration(seconds: 1), () { if (mounted) { _traceManager.reset(); + _traceManager.setMode(TPMode.LOCATION); } }); isStopWork = true; diff --git a/lib/features/my/presentation/web_view/user_info_pages.dart b/lib/features/my/presentation/web_view/user_info_pages.dart index 3a30bd9d..5d5f983a 100644 --- a/lib/features/my/presentation/web_view/user_info_pages.dart +++ b/lib/features/my/presentation/web_view/user_info_pages.dart @@ -23,12 +23,11 @@ class UserInfoCard extends StatefulWidget { class _UserInfoCardState extends State { late TextEditingController _nicknameController; final GetIt _getIt = GetIt.I; - MyCubit? _myCubit; // 可空类型 + MyCubit? _myCubit; @override void initState() { super.initState(); - // 优先从 BlocProvider 获取,获取失败则从 GetIt 兜底 try { _myCubit = BlocProvider.of(context); } catch (e) { @@ -48,9 +47,35 @@ class _UserInfoCardState extends State { super.dispose(); } - // 打开修改昵称的底部抽屉 + // ✅ 登录失效提示弹窗 + 2秒后跳登录 + void _showSessionExpiredDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => WillPopScope( + onWillPop: () async => false, + child: AlertDialog( + title: const Text('提示'), + content: const Text('登录状态失效,请重新登录!'), + actions: [ + Center( + child: TextButton(onPressed: () {}, child: const Text('确定')), + ), + ], + ), + ), + ); + + // 2秒后关闭弹窗并跳登录 + Future.delayed(const Duration(seconds: 2), () { + if (mounted) { + Navigator.pop(context); // 关闭弹窗 + context.go(RoutePaths.login); + } + }); + } + void _openEditNicknameBottomSheet() { - // 修复1:先校验 _myCubit 是否为 null,避免空断言 final myCubit = _myCubit; if (myCubit == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Cubit 初始化失败,无法修改昵称'), backgroundColor: Colors.red)); @@ -64,13 +89,14 @@ class _UserInfoCardState extends State { backgroundColor: Colors.transparent, constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.8), builder: (context) => BlocProvider.value( - // 使用非空的局部变量,无需空断言 value: myCubit, child: BlocBuilder( builder: (context, myState) { - final cubit = context.read(); + // ==================== ✅ 核心修复 ==================== if (myState.errorMessage == '登录状态失效,请重新登录') { - context.go(RoutePaths.login); + WidgetsBinding.instance.addPostFrameCallback((_) { + _showSessionExpiredDialog(); // 弹窗提示,不直接跳转 + }); } return Padding( @@ -96,16 +122,12 @@ class _UserInfoCardState extends State { TextField( controller: _nicknameController, style: const TextStyle(fontSize: 16), - decoration: InputDecoration( - border: const OutlineInputBorder(), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - ), + decoration: const InputDecoration(border: OutlineInputBorder(), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10)), autofocus: true, enabled: !myState.isLoading, ), const SizedBox(height: 20), - // 核心:根据 MyState 显示加载/按钮/错误提示 if (myState.isLoading) const Center( child: Column( @@ -119,14 +141,11 @@ class _UserInfoCardState extends State { else Column( children: [ - // 修复2:使用 ?. 和 ?? 避免空断言,安全判断错误信息 if (myState.errorMessage?.isNotEmpty ?? false) Padding( padding: const EdgeInsets.only(bottom: 12), child: Text(myState.errorMessage ?? "修改失败", style: const TextStyle(color: Colors.red, fontSize: 14)), ), - - // 确定修改按钮 SizedBox( width: double.infinity, child: ElevatedButton( @@ -152,7 +171,6 @@ class _UserInfoCardState extends State { ); } - // 提交新昵称的业务逻辑 Future _submitNewNickname(BuildContext context) async { final newNickname = _nicknameController.text.trim(); if (newNickname.isEmpty) { @@ -169,25 +187,19 @@ class _UserInfoCardState extends State { return; } - // 移除重复的加载弹窗(由 BlocBuilder 显示加载状态) await myCubit.updateName(newNickname); - // 成功后由 BlocBuilder 监听状态并处理弹窗关闭和提示 - if (context.mounted && myCubit.state.errorMessage!.isEmpty ?? true) { + if (context.mounted && (myCubit.state.errorMessage?.isEmpty ?? true)) { setState(() { _nicknameController.text = newNickname; }); Navigator.pop(context); - // 触发 AuthCubit 重新获取用户信息 } } - // 扫码绑定设备逻辑(保留) -// 扫码绑定设备逻辑(修复版) Future _handleScanAndBind(BuildContext context) async { bool isScanned = false; - // 1. 扫码逻辑 (保持不变) final String? deviceCode = await Navigator.push( context, MaterialPageRoute( @@ -196,10 +208,7 @@ class _UserInfoCardState extends State { appBar: AppBar( title: const Text('扫码绑定设备'), backgroundColor: Colors.black54, - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), + leading: IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context)), ), body: MobileScanner( onDetect: (BarcodeCapture capture) { @@ -226,7 +235,6 @@ class _UserInfoCardState extends State { return; } - // 2. 确认对话框 (保持不变) final bool? confirmBind = await showDialog( context: context, barrierDismissible: false, @@ -235,19 +243,10 @@ class _UserInfoCardState extends State { content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('设备信息:'), - const SizedBox(height: 8), - Text('设备编码:$deviceCode'), - const Text('设备状态:未绑定'), - const Text('设备类型:智能设备'), - ], + children: [const Text('设备信息:'), const SizedBox(height: 8), Text('设备编码:$deviceCode'), const Text('设备状态:未绑定'), const Text('设备类型:智能设备')], ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context, false), - child: const Text('取消'), - ), + TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('取消')), ElevatedButton( onPressed: () => Navigator.pop(context, true), child: const Text('确定绑定'), @@ -261,13 +260,12 @@ class _UserInfoCardState extends State { return; } - // 3. 显示加载框:保存加载弹窗的 BuildContext - BuildContext? loadingContext; // 新增:保存加载弹窗的上下文 + BuildContext? loadingContext; showDialog( context: context, barrierDismissible: false, builder: (context) { - loadingContext = context; // 保存这个 context + loadingContext = context; return const Center(child: CircularProgressIndicator()); }, ); @@ -276,30 +274,22 @@ class _UserInfoCardState extends State { final devicesCubit = GetIt.I(); await devicesCubit.bindDevice(deviceCode, ''); - // ✅ 成功:用 loadingContext 关闭弹窗 if (loadingContext != null && Navigator.canPop(loadingContext!)) { Navigator.pop(loadingContext!); } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('设备绑定成功!'), backgroundColor: Colors.green), - ); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('设备绑定成功!'), backgroundColor: Colors.green)); } } catch (e) { - // ❌ 失败:用 loadingContext 关闭弹窗 if (loadingContext != null && Navigator.canPop(loadingContext!)) { Navigator.pop(loadingContext!); } if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('绑定失败:${e.toString()}'), - backgroundColor: Colors.red, - duration: const Duration(seconds: 4), - ), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('绑定失败:${e.toString()}'), backgroundColor: Colors.red, duration: const Duration(seconds: 4))); } debugPrint('设备绑定失败详情:$e'); } @@ -314,7 +304,7 @@ class _UserInfoCardState extends State { height: 48, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - image: DecorationImage(image: NetworkImage('https://pic4.zhimg.com/v2-c34c61a90095abb8713de9d1dca7ec7b_r.jpg'), fit: BoxFit.cover), + image: const DecorationImage(image: NetworkImage('https://pic4.zhimg.com/v2-c34c61a90095abb8713de9d1dca7ec7b_r.jpg'), fit: BoxFit.cover), ), ), const SizedBox(width: 12),