356 lines
13 KiB
Dart
356 lines
13 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/canteen_order_total/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/canteen_order_total/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/canteen_reserve/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/canteen_reserve/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/card_active/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/card_active/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/cashier/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/order_history/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/order_history/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/setting/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/setting/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/take_food/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/take_food/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/verification_records/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/verification_records/view.dart';
|
|
|
|
/// 终端一级路由,仅创建一次公共外壳,业务页面通过内层 Navigator 切换。
|
|
///
|
|
/// 内层路由继续使用独立 Binding,保证 Controller、扫码监听、硬件资源和副屏回调
|
|
/// 仍跟随各业务页面创建与释放,布局复用不能改变原有生命周期边界。
|
|
class TerminalPage extends StatefulWidget {
|
|
/// 创建承载终端顶栏、侧栏与独立业务导航栈的页面。
|
|
const TerminalPage({super.key});
|
|
|
|
/// 创建持有实例级 Navigator Key 与路由同步状态的页面状态。
|
|
@override
|
|
State<TerminalPage> createState() => _TerminalPageState();
|
|
}
|
|
|
|
/// 终端外壳的路由状态,确保新旧终端页重叠退场时各自持有独立 Navigator Key。
|
|
class _TerminalPageState extends State<TerminalPage> {
|
|
/// 当前页面实例的导航所有权标识,用于避免旧实例销毁时误解绑新实例。
|
|
final Object _navigatorOwner = Object();
|
|
|
|
/// 当前页面实例独占的终端工作区 Navigator Key。
|
|
late final GlobalKey<NavigatorState> _navigatorKey;
|
|
|
|
/// 终端菜单显隐与排序控制器,由应用入口统一注册。
|
|
late final TerminalMenuControl _menuController;
|
|
|
|
/// 当前终端页面首次创建时应装载的二级路由。
|
|
late final String _initialRoute;
|
|
|
|
/// 当前实际显示的终端二级路由配置。
|
|
late _TerminalRouteConfig _currentConfig;
|
|
|
|
/// 监听可见菜单变化并自动纠正唯一业务入口的 GetX Worker。
|
|
Worker? _menuWorker;
|
|
|
|
/// 控制路由切换遮罩最短展示时长的计时器。
|
|
Timer? _routeLoadingTimer;
|
|
|
|
/// 是否显示阻止重复操作的短时路由切换遮罩。
|
|
bool _routeLoading = false;
|
|
|
|
/// 是否已经安排唯一可见菜单的帧后同步。
|
|
bool _menuSyncScheduled = false;
|
|
|
|
/// 是否已经安排 Navigator 路由状态的帧后同步。
|
|
bool _routeStateSyncScheduled = false;
|
|
|
|
/// 同一帧内最后一次收到的终端二级路由名称。
|
|
String? _pendingRouteName;
|
|
|
|
/// 登记实例级导航 Key,并初始化菜单与默认工作区。
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_navigatorKey = TerminalNavigationTool.attachNavigator(_navigatorOwner);
|
|
_menuController = Get.find<TerminalMenuControl>();
|
|
final soleVisibleRoute = _routeForSoleVisibleMenu();
|
|
// 设置页必须继续经过管理密码校验,不能因为它恰好是唯一菜单就在初始化路由中绕过守卫。
|
|
_initialRoute = soleVisibleRoute == RouteNames.setting
|
|
? RouteNames.cashier
|
|
: soleVisibleRoute ?? RouteNames.cashier;
|
|
_currentConfig = _terminalRoutes[_initialRoute]!;
|
|
_menuWorker = everAll(
|
|
[_menuController.menuOrder, _menuController.hiddenMenus],
|
|
(_) => _scheduleVisibleMenuSync(),
|
|
);
|
|
_scheduleVisibleMenuSync();
|
|
}
|
|
|
|
/// 释放菜单监听、计时器和当前实例的导航登记。
|
|
@override
|
|
void dispose() {
|
|
_menuWorker?.dispose();
|
|
_routeLoadingTimer?.cancel();
|
|
TerminalNavigationTool.detachNavigator(_navigatorOwner);
|
|
super.dispose();
|
|
}
|
|
|
|
/// 构建常驻终端外壳和由实例级 Key 管理的业务 Navigator。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return TerminalShell(
|
|
title: _currentConfig.title,
|
|
activeNav: _currentConfig.activeNav,
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
ColoredBox(
|
|
color: XfjDesignTokens.canvas,
|
|
child: Navigator(
|
|
key: _navigatorKey,
|
|
initialRoute: _initialRoute,
|
|
// 内层路由名称包含 /terminal 前缀,默认生成逻辑会额外创建 / 和
|
|
// /terminal 中间路由;这里只创建目标页,避免误装载收银页及其 Binding。
|
|
onGenerateInitialRoutes: (_, initialRoute) => [
|
|
_generateRoute(RouteSettings(name: initialRoute)),
|
|
],
|
|
observers: [_TerminalRouteObserver(_handleRouteChanged)],
|
|
onGenerateRoute: _generateRoute,
|
|
),
|
|
),
|
|
if (_routeLoading) const _TerminalRouteLoading(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 根据终端二级路由配置创建带独立 Binding 的无动画页面路由。
|
|
Route<dynamic> _generateRoute(RouteSettings settings) {
|
|
final config =
|
|
_terminalRoutes[settings.name] ?? _terminalRoutes[RouteNames.cashier]!;
|
|
return GetPageRoute<void>(
|
|
routeName: config.name,
|
|
settings: RouteSettings(name: config.name, arguments: settings.arguments),
|
|
page: config.page,
|
|
binding: config.binding,
|
|
transition: Transition.noTransition,
|
|
transitionDuration: Duration.zero,
|
|
);
|
|
}
|
|
|
|
/// 返回当前唯一可见业务菜单对应的路由;不存在唯一菜单时返回空。
|
|
String? _routeForSoleVisibleMenu() {
|
|
final menu = _menuController.soleVisibleBusinessMenu;
|
|
return menu == null ? null : _routeForMenu(menu);
|
|
}
|
|
|
|
/// 在当前帧结束后同步唯一可见菜单,避免构建期间直接修改 Navigator。
|
|
void _scheduleVisibleMenuSync() {
|
|
if (_menuSyncScheduled) return;
|
|
_menuSyncScheduled = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_menuSyncScheduled = false;
|
|
if (!mounted) return;
|
|
final route = _routeForSoleVisibleMenu();
|
|
if (route == null || route == _currentConfig.name) return;
|
|
|
|
// 配置收敛到单入口后直接切换工作区,避免侧栏收起时仍停留在已隐藏页面。
|
|
if (route == RouteNames.setting) {
|
|
unawaited(TerminalNavigationTool.open(route));
|
|
} else {
|
|
TerminalNavigationTool.replaceWorkspace(route);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 接收 Navigator 的真实路由变化,并在帧后刷新标题、菜单高亮与切换遮罩。
|
|
void _handleRouteChanged(String? routeName) {
|
|
TerminalNavigationTool.handleRouteChanged(_navigatorOwner, routeName);
|
|
if (routeName == null || !_terminalRoutes.containsKey(routeName)) return;
|
|
_pendingRouteName = routeName;
|
|
if (_routeStateSyncScheduled) return;
|
|
_routeStateSyncScheduled = true;
|
|
// Navigator 会在自身构建和历史栈刷新期间通知 Observer,父页面状态必须延后更新。
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_routeStateSyncScheduled = false;
|
|
if (!mounted) return;
|
|
final pendingRouteName = _pendingRouteName;
|
|
_pendingRouteName = null;
|
|
final config = _terminalRoutes[pendingRouteName];
|
|
if (config == null || config == _currentConfig) return;
|
|
_routeLoadingTimer?.cancel();
|
|
setState(() {
|
|
_currentConfig = config;
|
|
_routeLoading = true;
|
|
});
|
|
// Loading 只遮住二级路由重建的短暂帧,页面自己的接口加载状态仍由 Controller 负责。
|
|
_routeLoadingTimer = Timer(const Duration(milliseconds: 180), () {
|
|
if (mounted) setState(() => _routeLoading = false);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
String _routeForMenu(TerminalMenuItemType type) => switch (type) {
|
|
TerminalMenuItemType.cashier => RouteNames.cashier,
|
|
TerminalMenuItemType.menuQuery => RouteNames.menuQuery,
|
|
TerminalMenuItemType.reservation => RouteNames.canteenReserve,
|
|
TerminalMenuItemType.verification => RouteNames.takeFood,
|
|
TerminalMenuItemType.transactions => RouteNames.orderHistory,
|
|
TerminalMenuItemType.verificationRecords =>
|
|
RouteNames.verificationRecords,
|
|
TerminalMenuItemType.statistics => RouteNames.canteenOrderTotal,
|
|
TerminalMenuItemType.card => RouteNames.cardActive,
|
|
TerminalMenuItemType.settings => RouteNames.setting,
|
|
TerminalMenuItemType.logout => RouteNames.login,
|
|
};
|
|
|
|
class _TerminalRouteLoading extends StatelessWidget {
|
|
const _TerminalRouteLoading();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const AbsorbPointer(
|
|
child: ColoredBox(
|
|
color: XfjDesignTokens.canvas,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 34,
|
|
height: 34,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
color: XfjDesignTokens.primary,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TerminalRouteObserver extends NavigatorObserver {
|
|
_TerminalRouteObserver(this.onChanged);
|
|
|
|
final ValueChanged<String?> onChanged;
|
|
|
|
@override
|
|
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
onChanged(route.settings.name);
|
|
}
|
|
|
|
@override
|
|
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
|
|
onChanged(newRoute?.settings.name);
|
|
}
|
|
|
|
@override
|
|
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
|
onChanged(previousRoute?.settings.name);
|
|
}
|
|
}
|
|
|
|
class _TerminalRouteConfig {
|
|
const _TerminalRouteConfig({
|
|
required this.name,
|
|
required this.title,
|
|
required this.activeNav,
|
|
required this.page,
|
|
this.binding,
|
|
});
|
|
|
|
final String name;
|
|
final String title;
|
|
final TerminalNavType activeNav;
|
|
final GetPageBuilder page;
|
|
final Bindings? binding;
|
|
}
|
|
|
|
final Map<String, _TerminalRouteConfig> _terminalRoutes = {
|
|
RouteNames.cashier: _TerminalRouteConfig(
|
|
name: RouteNames.cashier,
|
|
title: '智慧收银',
|
|
activeNav: TerminalNavType.cashier,
|
|
page: () => const CashierPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => SelectPayTypeDialogController());
|
|
Get.lazyPut(() => SteelyardController());
|
|
}),
|
|
),
|
|
RouteNames.menuQuery: _TerminalRouteConfig(
|
|
name: RouteNames.menuQuery,
|
|
title: '查询菜单',
|
|
activeNav: TerminalNavType.menuQuery,
|
|
page: () => const ReservationMenuQueryPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => ReservationMenuQueryController());
|
|
}),
|
|
),
|
|
RouteNames.canteenReserve: _TerminalRouteConfig(
|
|
name: RouteNames.canteenReserve,
|
|
title: '预订管理',
|
|
activeNav: TerminalNavType.reservation,
|
|
page: () => const CanteenReservePage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => CanteenReserveController());
|
|
}),
|
|
),
|
|
RouteNames.takeFood: _TerminalRouteConfig(
|
|
name: RouteNames.takeFood,
|
|
title: '核销',
|
|
activeNav: TerminalNavType.verification,
|
|
page: () => const TakeFoodPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => TakeFoodController());
|
|
}),
|
|
),
|
|
RouteNames.canteenOrderTotal: _TerminalRouteConfig(
|
|
name: RouteNames.canteenOrderTotal,
|
|
title: '交易统计',
|
|
activeNav: TerminalNavType.statistics,
|
|
page: () => const CanteenOrderTotalPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => CanteenOrderTotalController());
|
|
}),
|
|
),
|
|
RouteNames.orderHistory: _TerminalRouteConfig(
|
|
name: RouteNames.orderHistory,
|
|
title: '交易查询',
|
|
activeNav: TerminalNavType.transactions,
|
|
page: () => const OrderHistoryPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => OrderHistoryController());
|
|
}),
|
|
),
|
|
RouteNames.verificationRecords: _TerminalRouteConfig(
|
|
name: RouteNames.verificationRecords,
|
|
title: '核销记录',
|
|
activeNav: TerminalNavType.verificationRecords,
|
|
page: () => const VerificationRecordsPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => VerificationRecordsController());
|
|
}),
|
|
),
|
|
RouteNames.cardActive: _TerminalRouteConfig(
|
|
name: RouteNames.cardActive,
|
|
title: '开卡管理',
|
|
activeNav: TerminalNavType.card,
|
|
page: () => const CardActivePage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => CardActiveController());
|
|
}),
|
|
),
|
|
RouteNames.setting: _TerminalRouteConfig(
|
|
name: RouteNames.setting,
|
|
title: '设置',
|
|
activeNav: TerminalNavType.settings,
|
|
page: () => const SettingPage(),
|
|
binding: BindingsBuilder(() {
|
|
// 设置页还有独立的二级 Navigator;保留工厂可避免嵌套路由回收后子页面失去共享控制器。
|
|
Get.lazyPut(() => SettingController(), fenix: true);
|
|
}),
|
|
),
|
|
};
|