import 'package:common_utils/common_utils.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; /// 新终端侧边栏的统一导航守卫。 /// /// 统一终端导航仍需保留设备绑定、联网限制和设置密码校验, /// 否则视觉重构会意外放开原有业务入口。 abstract final class TerminalNavigationTool { /// 当前仍负责接收菜单跳转的终端页面实例标识。 static Object? _navigatorOwner; /// 当前终端页面实例持有的导航 Key;页面重叠退场时不同实例必须使用不同 Key。 static GlobalKey? _navigatorKey; /// 当前活跃终端实例实际显示的工作区路由,用于拦截重复返回同一菜单。 static String? _currentWorkspaceRoute; /// 管理密码弹层是否正在验证,用于阻止重复创建弹层。 static bool _verificationInProgress = false; /// 为新挂载的终端页面创建并登记实例级导航 Key。 /// /// GetX 的 `nestedKey(id)` 会永久复用同一 GlobalKey;根路由替换期间新旧终端页 /// 可能短暂共存,因此必须由各页面实例持有独立 Key,再只把菜单操作发给最新实例。 static GlobalKey attachNavigator(Object owner) { final key = GlobalKey( debugLabel: 'Terminal workspace navigator', ); _navigatorOwner = owner; _navigatorKey = key; _currentWorkspaceRoute = null; return key; } /// 终端页面销毁时解除登记;旧页面延迟销毁不得清除后来页面的导航 Key。 static void detachNavigator(Object owner) { if (!identical(_navigatorOwner, owner)) return; _navigatorOwner = null; _navigatorKey = null; _currentWorkspaceRoute = null; } /// 接收活跃终端 Navigator 的真实路由,旧实例的延迟回调会被所有权校验丢弃。 static void handleRouteChanged(Object owner, String? route) { if (!identical(_navigatorOwner, owner) || route == null) return; _currentWorkspaceRoute = route; } /// 校验入口权限后切换到指定终端工作区路由。 static Future open(String route) async { if (_requiresAdminUnlock(route)) { if (!await _verifyAdminSession(actionLabel: '进入设置')) return; _replaceWorkspace(route); return; } if (!await _checkIsBind()) return; if (_requiresNetwork(route) && !AppNetworkTool.isAutoNetwork()) { Toast.error(msg: '请先连接网络'); return; } _replaceWorkspace(route); } /// 仅替换终端二级页面,保留一级外壳及应用级资源。 static void replaceWorkspace(String route) => _replaceWorkspace(route); /// 清空当前终端内部历史并装载目标页面,不影响根 Navigator 的终端外壳。 static void _replaceWorkspace(String route) { final navigator = _navigatorKey?.currentState; if (navigator == null || !navigator.mounted || _currentWorkspaceRoute == route) { return; } navigator.pushNamedAndRemoveUntil(route, (_) => false); } /// 退出前统一执行管理密码校验,避免侧栏与顶栏出口出现不同的安全规则。 static Future logout() async { if (!await _verifyAdminSession(actionLabel: '退出登录')) return; Get.find().lock(); if (AppNetworkTool.isAutoNetwork()) { await AppSettingTool.setUserSetting(UserSetting()); } Get.offAllNamed(RouteNames.login); } /// 顶栏锁状态可主动发起验证,不需要先进入设置或退出流程。 static Future unlockAdminSession() async { await _verifyAdminSession(actionLabel: '解锁管理操作'); } /// 复用已解锁会话或弹出管理密码校验,返回本次管理操作是否获准。 static Future _verifyAdminSession({ required String actionLabel, }) async { final session = Get.find(); if (session.isUnlocked) return true; session.lock(); // 快速连续点击设置与退出时只允许存在一个密码弹层,避免重复 Overlay 和 GlobalKey。 if (_verificationInProgress) return false; final context = Get.context; if (context == null) return false; _verificationInProgress = true; try { final verified = await terminalAdminPasswordDialog( context, actionLabel: actionLabel, ); // 弹层取消与验证成功都要等退场结束,避免快速再次点击叠加两个 Overlay。 await Future.delayed(const Duration(milliseconds: 200)); if (!verified) return false; session.unlock(); return true; } finally { _verificationInProgress = false; } } /// 查询菜单是学生本人自助入口,不纳入管理密码校验。 static bool _requiresAdminUnlock(String route) { return route == RouteNames.setting; } /// 判断目标业务是否必须在服务网络可用时进入。 static bool _requiresNetwork(String route) { return route == RouteNames.menuQuery || route == RouteNames.canteenReserve || route == RouteNames.takeFood || route == RouteNames.canteenOrderTotal; } /// 检查终端是否已经绑定服务商户,未绑定时统一提示并拒绝进入业务页。 static Future _checkIsBind() async { final setting = await AppSettingTool.getSysSetting(); if (ObjectUtil.isEmpty(setting.serviceMerchantId)) { Toast.error(msg: '请前往设置绑定设备'); return false; } return true; } }