364 lines
14 KiB
Dart
364 lines
14 KiB
Dart
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:forui/forui.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/setting/routes/index.dart';
|
|
import 'package:pj_supermarket_cloud/pages/terminal/setting/widgets/index.dart';
|
|
|
|
import 'controller.dart';
|
|
|
|
/// 设置模块外壳,左侧菜单和设备绑定状态保持常驻,右侧内容由独立 Navigator 管理。
|
|
class SettingPage extends GetView<SettingController> {
|
|
/// 创建由终端工作区 Binding 提供 SettingController 的设置页。
|
|
const SettingPage({super.key});
|
|
|
|
/// 构建固定设置菜单与可路由切换的内容区。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return <Widget>[
|
|
SizedBox(width: 240.w, child: _SettingMenu(controller: controller)),
|
|
SizedBox(width: XfjDesignTokens.cardGap.w),
|
|
const _SettingContent().expanded(),
|
|
].toRow(crossAxisAlignment: CrossAxisAlignment.stretch);
|
|
}
|
|
}
|
|
|
|
/// 设置页左侧常驻菜单和设备绑定状态区。
|
|
class _SettingMenu extends StatelessWidget {
|
|
/// 创建读取同一设置控制器的菜单区域。
|
|
const _SettingMenu({required this.controller});
|
|
|
|
/// 终端工作区 Binding 注册的设置控制器,用于菜单可见性和设备状态。
|
|
final SettingController controller;
|
|
|
|
/// 构建路由菜单、设备绑定提示和可选的 Kiosk 退出入口。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = FTheme.of(context);
|
|
return TerminalSurface(
|
|
padding: EdgeInsets.all(12.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(12.w, 4.h, 12.w, 12.h),
|
|
child: Text(
|
|
'终端设置',
|
|
style: theme.typography.sm.copyWith(
|
|
color: XfjDesignTokens.muted,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Obx(
|
|
() => ListView(
|
|
padding: EdgeInsets.zero,
|
|
children: controller.visibleMenuTypes
|
|
.map(
|
|
(type) => _SettingMenuButton(
|
|
type: type,
|
|
selected:
|
|
SettingNavigation.currentMenuType.value == type,
|
|
onTap: () => SettingNavigation.openMenu(type),
|
|
).marginOnly(bottom: 8.h),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
Obx(() {
|
|
final bound = controller.isBound.value;
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF7F8FA),
|
|
border: Border.all(color: XfjDesignTokens.line),
|
|
borderRadius: BorderRadius.circular(
|
|
XfjDesignTokens.controlRadius,
|
|
),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 8.w,
|
|
height: 8.w,
|
|
decoration: BoxDecoration(
|
|
color: bound
|
|
? XfjDesignTokens.success
|
|
: XfjDesignTokens.warning,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
bound ? '设备已绑定' : '设备未绑定',
|
|
style: theme.typography.sm.copyWith(
|
|
color: bound
|
|
? XfjDesignTokens.success
|
|
: XfjDesignTokens.warning,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (controller.kioskStatus.value.canExitToAndroidHome) ...[
|
|
SizedBox(height: 8.h),
|
|
const SettingKioskExitButton(),
|
|
],
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 设置菜单中的单个路由入口。
|
|
class _SettingMenuButton extends StatelessWidget {
|
|
/// 创建带选中态和点击回调的菜单按钮。
|
|
const _SettingMenuButton({
|
|
required this.type,
|
|
required this.selected,
|
|
required this.onTap,
|
|
});
|
|
|
|
/// 菜单稳定类型,用于展示标题、图标和映射路由。
|
|
final SettingMenuType type;
|
|
|
|
/// 当前实际子路由是否归属于本菜单。
|
|
final bool selected;
|
|
|
|
/// 点击后切换设置二级路由的回调。
|
|
final VoidCallback onTap;
|
|
|
|
/// 根据路由选中态构建图标、标题和确认标识。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = FTheme.of(context);
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
|
child: Container(
|
|
constraints: BoxConstraints(minHeight: 56.h),
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
|
|
decoration: BoxDecoration(
|
|
color: selected ? XfjDesignTokens.primary : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
FIcon(
|
|
_menuIcon(type),
|
|
color: selected ? Colors.white : const Color(0xFF344054),
|
|
size: 23.sp,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Text(
|
|
type.title,
|
|
style: theme.typography.base.copyWith(
|
|
color: selected ? Colors.white : const Color(0xFF344054),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
if (selected)
|
|
FIcon(FAssets.icons.check, color: Colors.white, size: 18.sp),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 设置页右侧常驻内容外壳,页头跟随路由变化,正文由二级 Navigator 生成。
|
|
class _SettingContent extends StatefulWidget {
|
|
/// 创建不持有具体菜单正文的路由内容区。
|
|
const _SettingContent();
|
|
|
|
/// 创建持有设置实例级 Navigator Key 的内容区状态。
|
|
@override
|
|
State<_SettingContent> createState() => _SettingContentState();
|
|
}
|
|
|
|
/// 设置内容区状态,负责实例级导航 Key 的登记、使用与释放。
|
|
class _SettingContentState extends State<_SettingContent> {
|
|
/// 当前设置内容区的导航所有权标识,用于区分重叠退场的新旧页面。
|
|
final Object _navigatorOwner = Object();
|
|
|
|
/// 当前设置内容区独占的 Navigator Key。
|
|
late final GlobalKey<NavigatorState> _navigatorKey;
|
|
|
|
/// 登记当前内容区的实例级 Navigator Key。
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_navigatorKey = SettingNavigation.attachNavigator(_navigatorOwner);
|
|
}
|
|
|
|
/// 只解除当前实例仍持有的导航登记,避免旧页面销毁影响新设置页。
|
|
@override
|
|
void dispose() {
|
|
SettingNavigation.detachNavigator(_navigatorOwner);
|
|
super.dispose();
|
|
}
|
|
|
|
/// 构建路由驱动的页头与设置模块独立导航栈。
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = FTheme.of(context);
|
|
return TerminalSurface(
|
|
clipBehavior: Clip.antiAlias,
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
constraints: BoxConstraints(minHeight: 92.h),
|
|
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 14.h),
|
|
decoration: const BoxDecoration(
|
|
border: Border(bottom: BorderSide(color: XfjDesignTokens.line)),
|
|
),
|
|
child: Obx(() {
|
|
final type = SettingNavigation.currentMenuType.value;
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
type.title,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: theme.typography.xl2.copyWith(
|
|
color: XfjDesignTokens.ink,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
_menuDescription(type),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: theme.typography.sm.copyWith(
|
|
color: XfjDesignTokens.muted,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Navigator(
|
|
key: _navigatorKey,
|
|
initialRoute: RouteNames.settingBaseInfo,
|
|
// 直接创建目标页,避免完整路径自动生成无意义的中间路由。
|
|
onGenerateInitialRoutes: (_, initialRoute) => [
|
|
SettingRouteFactory.generate(
|
|
RouteSettings(name: initialRoute),
|
|
),
|
|
],
|
|
observers: [
|
|
_SettingRouteObserver(
|
|
(routeName) => SettingNavigation.handleRouteChanged(
|
|
_navigatorOwner,
|
|
routeName,
|
|
),
|
|
),
|
|
],
|
|
onGenerateRoute: SettingRouteFactory.generate,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 将设置菜单类型映射为当前 Forui 图标资产。
|
|
SvgAsset _menuIcon(SettingMenuType type) {
|
|
return switch (type) {
|
|
SettingMenuType.baseInfo => FAssets.icons.settings,
|
|
SettingMenuType.backup => FAssets.icons.databaseBackup,
|
|
SettingMenuType.syncSetting => FAssets.icons.refreshCw,
|
|
SettingMenuType.mealPeriodSetting => FAssets.icons.utensils,
|
|
SettingMenuType.paySetting => FAssets.icons.creditCard,
|
|
SettingMenuType.faceSetting => FAssets.icons.scanFace,
|
|
SettingMenuType.facePluginSetting => FAssets.icons.shieldCheck,
|
|
SettingMenuType.pageSetting => FAssets.icons.layoutDashboard,
|
|
SettingMenuType.productSetting => FAssets.icons.package,
|
|
SettingMenuType.logPreview => FAssets.icons.logs,
|
|
SettingMenuType.printerInfo => FAssets.icons.printer,
|
|
SettingMenuType.subScreenRoutes => FAssets.icons.monitor,
|
|
SettingMenuType.versionSetting => FAssets.icons.rocket,
|
|
SettingMenuType.cardReaderSetting => FAssets.icons.nfc,
|
|
};
|
|
}
|
|
|
|
/// 将设置菜单类型转换为常驻页头的功能摘要。
|
|
String _menuDescription(SettingMenuType type) {
|
|
return switch (type) {
|
|
SettingMenuType.baseInfo => '查看终端身份、绑定状态和字体显示设置',
|
|
SettingMenuType.backup => '查看备份路径并导出本地数据库和日志',
|
|
SettingMenuType.syncSetting => '配置自动规则、空闲执行,并立即运行终端同步和日志任务',
|
|
SettingMenuType.mealPeriodSetting => '设置早餐、中餐和晚餐在一天内对应的时间范围',
|
|
SettingMenuType.paySetting => '支付配置已迁入页面设置的收银页面',
|
|
SettingMenuType.faceSetting => '查询、同步和维护本地人脸样本',
|
|
SettingMenuType.facePluginSetting => '查看插件授权、引擎状态和内存人脸库注册进度',
|
|
SettingMenuType.pageSetting => '配置查询菜单、收银等终端页面的显示顺序和默认操作',
|
|
SettingMenuType.productSetting => '查询本地商品、同步分类商品和维护默认菜品',
|
|
SettingMenuType.logPreview => '按目录切换历史日志,查看脱敏后的最近内容',
|
|
SettingMenuType.printerInfo => '查看实际 USB 参数,手动上报并输出测试小票',
|
|
SettingMenuType.subScreenRoutes => '选择顾客屏演示状态并在当前应用窗口中全屏检查',
|
|
SettingMenuType.versionSetting => '查看可用终端版本、更新内容和下载状态',
|
|
SettingMenuType.cardReaderSetting => '配置 DK25-ST 读取策略、M1 数据块和 KEY A/B 密钥',
|
|
};
|
|
}
|
|
|
|
/// 监听设置模块二级 Navigator 的真实路由变化。
|
|
class _SettingRouteObserver extends NavigatorObserver {
|
|
/// 创建向菜单高亮状态回传路由名称的观察器。
|
|
_SettingRouteObserver(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);
|
|
}
|
|
}
|