147 lines
5.6 KiB
Dart
147 lines
5.6 KiB
Dart
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<NavigatorState>? _navigatorKey;
|
|
|
|
/// 当前活跃终端实例实际显示的工作区路由,用于拦截重复返回同一菜单。
|
|
static String? _currentWorkspaceRoute;
|
|
|
|
/// 管理密码弹层是否正在验证,用于阻止重复创建弹层。
|
|
static bool _verificationInProgress = false;
|
|
|
|
/// 为新挂载的终端页面创建并登记实例级导航 Key。
|
|
///
|
|
/// GetX 的 `nestedKey(id)` 会永久复用同一 GlobalKey;根路由替换期间新旧终端页
|
|
/// 可能短暂共存,因此必须由各页面实例持有独立 Key,再只把菜单操作发给最新实例。
|
|
static GlobalKey<NavigatorState> attachNavigator(Object owner) {
|
|
final key = GlobalKey<NavigatorState>(
|
|
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<void> 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<void>(route, (_) => false);
|
|
}
|
|
|
|
/// 退出前统一执行管理密码校验,避免侧栏与顶栏出口出现不同的安全规则。
|
|
static Future<void> logout() async {
|
|
if (!await _verifyAdminSession(actionLabel: '退出登录')) return;
|
|
Get.find<TerminalAdminSessionControl>().lock();
|
|
// 是否忘记已保存的登录密码由基本设置开关决定;离线时始终保留以便离线重进。
|
|
await AppSettingTool.applyManualLogoutPasswordPolicy();
|
|
Get.offAllNamed(RouteNames.login);
|
|
}
|
|
|
|
/// 顶栏锁状态可主动发起验证,不需要先进入设置或退出流程。
|
|
static Future<void> unlockAdminSession() async {
|
|
await _verifyAdminSession(actionLabel: '解锁管理操作');
|
|
}
|
|
|
|
/// 复用已解锁会话或弹出管理密码校验,返回本次管理操作是否获准。
|
|
static Future<bool> _verifyAdminSession({
|
|
required String actionLabel,
|
|
}) async {
|
|
final session = Get.find<TerminalAdminSessionControl>();
|
|
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<void>.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<bool> _checkIsBind() async {
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
if (ObjectUtil.isEmpty(setting.serviceMerchantId)) {
|
|
Toast.error(msg: '请前往设置绑定设备');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|