Files
biz_xfj_cloud/lib/pages/terminal/setting/view.dart
T
2026-09-04 16:23:30 +08:00

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