Files
biz_xfj_cloud/lib/pages/terminal/view.dart
T
2026-08-06 20:55:09 +08:00

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);
}),
),
};