346 lines
13 KiB
Dart
346 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/canteen_order_total/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/canteen_order_total/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/canteen_product/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/canteen_product/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/canteen_reserve/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/canteen_reserve/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/card_active/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/card_active/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/order_history/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/order_history/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/order_history_detail/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/order_history_detail/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/face_manage_controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/face_setting_controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/offline_user_controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/sync_logs/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/sync_logs/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/setting/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/supermarket_order/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/supermarket_order/view.dart';
|
|
import 'package:pj_supermarket_cloud/pages/take_food/controller.dart';
|
|
import 'package:pj_supermarket_cloud/pages/take_food/view.dart';
|
|
|
|
/// 终端一级路由,仅创建一次公共外壳,业务页面通过内层 Navigator 切换。
|
|
///
|
|
/// 本项目业务入口统一放在 panel 主菜单页,外壳只保留顶部状态栏;
|
|
/// 内层路由继续使用独立 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;
|
|
|
|
/// 终端首次创建时 panel 通过 Get.arguments 传入的二级路由。
|
|
late final String? _requestedRoute;
|
|
|
|
/// 当前终端页面首次创建时应装载的二级路由。
|
|
late final String _initialRoute;
|
|
|
|
/// 当前实际显示的终端二级路由配置。
|
|
late _TerminalRouteConfig _currentConfig;
|
|
|
|
/// 是否已完成初始路由解析;解析前仅展示轻量加载占位。
|
|
bool _ready = false;
|
|
|
|
/// 控制路由切换遮罩最短展示时长的计时器。
|
|
Timer? _routeLoadingTimer;
|
|
|
|
/// 是否显示阻止重复操作的短时路由切换遮罩。
|
|
bool _routeLoading = false;
|
|
|
|
/// 是否已经安排 Navigator 路由状态的帧后同步。
|
|
bool _routeStateSyncScheduled = false;
|
|
|
|
/// 同一帧内最后一次收到的终端二级路由名称。
|
|
String? _pendingRouteName;
|
|
|
|
/// 登记实例级导航 Key,并解析 panel 请求或按设备类型选择的初始工作区。
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_navigatorKey = TerminalNavigationTool.attachNavigator(_navigatorOwner);
|
|
// GetX 的路由参数只在路由装载初期可靠,必须在任何 await 之前同步取出。
|
|
_requestedRoute = Get.arguments is String ? Get.arguments as String : null;
|
|
unawaited(_resolveInitialRoute());
|
|
}
|
|
|
|
/// 解析初始二级路由:panel 指定优先,未指定时按系统类型装载对应收银页。
|
|
Future<void> _resolveInitialRoute() async {
|
|
var initial = _requestedRoute ?? '';
|
|
if (!_terminalRoutes.containsKey(initial)) {
|
|
final sysSetting = await AppSettingTool.getSysSetting();
|
|
initial = sysSetting.sysType == SysType.SUPERMARKET
|
|
? RouteNames.supermarketProduct
|
|
: RouteNames.canteenProduct;
|
|
}
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_initialRoute = initial;
|
|
_currentConfig = _terminalRoutes[initial]!;
|
|
_ready = true;
|
|
});
|
|
}
|
|
|
|
/// 释放计时器和当前实例的导航登记。
|
|
@override
|
|
void dispose() {
|
|
_routeLoadingTimer?.cancel();
|
|
TerminalNavigationTool.detachNavigator(_navigatorOwner);
|
|
super.dispose();
|
|
}
|
|
|
|
/// 构建常驻终端外壳和由实例级 Key 管理的业务 Navigator。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_ready) {
|
|
return const ColoredBox(
|
|
color: XfjDesignTokens.canvas,
|
|
child: Center(
|
|
child: SizedBox(
|
|
width: 34,
|
|
height: 34,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
color: XfjDesignTokens.primary,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return TerminalShell(
|
|
title: _currentConfig.title,
|
|
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.supermarketProduct]!;
|
|
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,
|
|
);
|
|
}
|
|
|
|
/// 接收 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);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
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.page,
|
|
this.binding,
|
|
});
|
|
|
|
final String name;
|
|
final String title;
|
|
final GetPageBuilder page;
|
|
final Bindings? binding;
|
|
}
|
|
|
|
final Map<String, _TerminalRouteConfig> _terminalRoutes = {
|
|
RouteNames.supermarketProduct: _TerminalRouteConfig(
|
|
name: RouteNames.supermarketProduct,
|
|
title: '超市收银',
|
|
page: () => const SupermarketOrderPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => SelectPayTypeDialogController(), fenix: true);
|
|
Get.lazyPut(() => SteelyardController(), fenix: true);
|
|
Get.lazyPut(() => SupermarketOrderController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.canteenProduct: _TerminalRouteConfig(
|
|
name: RouteNames.canteenProduct,
|
|
title: '点餐收银',
|
|
page: () => const CanteenProductPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => SelectPayTypeDialogController(), fenix: true);
|
|
Get.lazyPut(() => CanteenProductController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.canteenReserve: _TerminalRouteConfig(
|
|
name: RouteNames.canteenReserve,
|
|
title: '预订管理',
|
|
page: () => const CanteenReservePage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => CanteenReserveController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.takeFood: _TerminalRouteConfig(
|
|
name: RouteNames.takeFood,
|
|
title: '核销',
|
|
page: () => const TakeFoodPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => TakeFoodController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.canteenOrderTotal: _TerminalRouteConfig(
|
|
name: RouteNames.canteenOrderTotal,
|
|
title: '交易统计',
|
|
page: () => const CanteenOrderTotalPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => CanteenOrderTotalController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.orderHistory: _TerminalRouteConfig(
|
|
name: RouteNames.orderHistory,
|
|
title: '交易查询',
|
|
page: () => const OrderHistoryPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => OrderHistoryController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.orderHistoryDetail: _TerminalRouteConfig(
|
|
name: RouteNames.orderHistoryDetail,
|
|
title: '交易详情',
|
|
page: () => const OrderHistoryDetailPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => OrderHistoryController(), fenix: true);
|
|
Get.lazyPut(() => OrderHistoryDetailController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.cardActive: _TerminalRouteConfig(
|
|
name: RouteNames.cardActive,
|
|
title: '开卡管理',
|
|
page: () => const CardActivePage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => SettingController(), fenix: true);
|
|
Get.lazyPut(() => CardActiveController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.setting: _TerminalRouteConfig(
|
|
name: RouteNames.setting,
|
|
title: '设置',
|
|
page: () => const SettingPage(),
|
|
binding: BindingsBuilder(() {
|
|
Get.lazyPut(() => SettingController(), fenix: true);
|
|
Get.lazyPut(() => FaceManageController(), fenix: true);
|
|
Get.lazyPut(() => FaceSettingController(), fenix: true);
|
|
Get.lazyPut(() => OfflineUserController(), fenix: true);
|
|
}),
|
|
),
|
|
RouteNames.settingSyncLogs: _TerminalRouteConfig(
|
|
name: RouteNames.settingSyncLogs,
|
|
title: '定时任务日志',
|
|
page: () => const ScheduledTaskLogsPage(),
|
|
binding: BindingsBuilder(() {
|
|
// 任务类型由页面首帧从 RouteSettings 注入后再触发首查。
|
|
Get.lazyPut(() => ScheduledTaskLogsController(), fenix: true);
|
|
}),
|
|
),
|
|
};
|