diff --git a/lib/common/models/page/reservation_menu_page_models.dart b/lib/common/models/page/reservation_menu_page_models.dart index f446018..05239b4 100644 --- a/lib/common/models/page/reservation_menu_page_models.dart +++ b/lib/common/models/page/reservation_menu_page_models.dart @@ -104,6 +104,7 @@ class ReservationQueryPagePreferences { this.nameMaskEnabled = true, this.verificationEnabled = false, this.quickVerificationEnabled = false, + this.allowManualMealPeriodChange = true, this.selectedMealSlot = ReservationMealSlot.morning, this.verificationDateScope = ReservationVerificationDateScope.today, }); @@ -117,6 +118,7 @@ class ReservationQueryPagePreferences { nameMaskEnabled: true, verificationEnabled: false, quickVerificationEnabled: false, + allowManualMealPeriodChange: true, selectedMealSlot: ReservationMealSlot.morning, verificationDateScope: ReservationVerificationDateScope.today, ); @@ -144,6 +146,9 @@ class ReservationQueryPagePreferences { /// 识别查询成功后是否直接打开今天预选餐段的核销弹框,默认关闭以保持旧终端流程。 final bool quickVerificationEnabled; + /// 是否允许操作员在查询菜单主动修改餐段;关闭后严格按餐段配置时间自动切换。 + final bool allowManualMealPeriodChange; + /// 查询菜单最近一次选中的餐段,旧终端未保存时默认早餐。 final ReservationMealSlot selectedMealSlot; diff --git a/lib/common/utils/app/app_setting_tool.dart b/lib/common/utils/app/app_setting_tool.dart index 18e3c6a..96e5b7d 100644 --- a/lib/common/utils/app/app_setting_tool.dart +++ b/lib/common/utils/app/app_setting_tool.dart @@ -35,6 +35,10 @@ abstract class AppSettingTool { static const String _cashierShowHoldActionKey = "cashierShowHoldAction"; static const String _cashierShowMoreActionKey = "cashierShowMoreAction"; + /// 收银页是否允许主动修改餐段的稳定存储键;缺失时按允许处理,兼容旧终端。 + static const String _cashierAllowManualMealPeriodChangeKey = + "cashierAllowManualMealPeriodChange"; + /// 顶部隐藏设备编码列表在现有 sysJson 中的稳定存储键;缺失或空列表表示五项全部显示。 static const String _hiddenTerminalDeviceStatusesKey = "hiddenTerminalDeviceStatuses"; @@ -63,6 +67,10 @@ abstract class AppSettingTool { static const String _reservationQueryQuickVerificationEnabledKey = "reservationQueryQuickVerificationEnabled"; + /// 查询菜单是否允许主动修改餐段的稳定存储键;缺失时按允许处理,兼容旧终端。 + static const String _reservationQueryAllowManualMealPeriodChangeKey = + "reservationQueryAllowManualMealPeriodChange"; + /// 查询菜单最近餐段在现有 sysJson 中的稳定存储键,值使用后端餐段编码且不新增数据库字段。 static const String _reservationQuerySelectedMealSlotKey = "reservationQuerySelectedMealSlot"; @@ -472,25 +480,34 @@ abstract class AppSettingTool { }); } - /// 读取收银页底部快捷操作显隐;旧终端没有配置时保持两个入口都显示。 - static Future<({bool showHoldAction, bool showMoreAction})> - getCashierPagePreferences() async { + /// 读取收银页底部快捷操作显隐及餐段修改权限;旧终端没有配置时保持默认允许。 + static Future< + ({ + bool showHoldAction, + bool showMoreAction, + bool allowManualMealPeriodChange, + })> getCashierPagePreferences() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return ( showHoldAction: sysJson[_cashierShowHoldActionKey] != false, showMoreAction: sysJson[_cashierShowMoreActionKey] != false, + allowManualMealPeriodChange: + sysJson[_cashierAllowManualMealPeriodChangeKey] != false, ); } - /// 收银快捷操作属于本机页面布局配置,继续复用 sysJson,避免增加数据库字段。 + /// 收银快捷操作与餐段修改权限属于本机页面布局配置,继续复用 sysJson。 static Future setCashierPagePreferences({ required bool showHoldAction, required bool showMoreAction, + required bool allowManualMealPeriodChange, }) => _updateSysJson((sysJson) { sysJson[_cashierShowHoldActionKey] = showHoldAction; sysJson[_cashierShowMoreActionKey] = showMoreAction; + sysJson[_cashierAllowManualMealPeriodChangeKey] = + allowManualMealPeriodChange; }); /// 读取商品餐段时间;旧终端无配置或持久化值损坏时返回产品默认时间。 @@ -627,6 +644,9 @@ abstract class AppSettingTool { // 快速核销从属于核销入口,旧设置缺键或核销关闭时均不得隐式生效。 quickVerificationEnabled: verificationEnabled && sysJson[_reservationQueryQuickVerificationEnabledKey] == true, + // 旧设置没有该键时保持允许手动修改,避免升级后收银流程被意外锁死。 + allowManualMealPeriodChange: + sysJson[_reservationQueryAllowManualMealPeriodChangeKey] != false, selectedMealSlot: selectedMealSlot, verificationDateScope: verificationDateScope, ); @@ -641,6 +661,7 @@ abstract class AppSettingTool { required bool nameMaskEnabled, required bool verificationEnabled, required bool quickVerificationEnabled, + required bool allowManualMealPeriodChange, required ReservationVerificationDateScope verificationDateScope, }) { final normalized = []; @@ -664,6 +685,8 @@ abstract class AppSettingTool { sysJson[_reservationQueryVerificationEnabledKey] = verificationEnabled; sysJson[_reservationQueryQuickVerificationEnabledKey] = verificationEnabled && quickVerificationEnabled; + sysJson[_reservationQueryAllowManualMealPeriodChangeKey] = + allowManualMealPeriodChange; sysJson[_reservationQueryVerificationDateScopeKey] = verificationDateScope.code; }); diff --git a/lib/common/widgets/app/app_radio_hour_type_dialog.dart b/lib/common/widgets/app/app_radio_hour_type_dialog.dart index 219c21f..5085d07 100644 --- a/lib/common/widgets/app/app_radio_hour_type_dialog.dart +++ b/lib/common/widgets/app/app_radio_hour_type_dialog.dart @@ -13,6 +13,7 @@ import 'package:pj_supermarket_cloud/common/index.dart'; /// 收银、预订和设置页面继续使用原有的图标与强调色样式。 /// [checkConfiguredTime] 开启后会在页面初始化完成时校验当前餐段;[ready] 用于等待调用方恢复自身状态。 /// [currentHourTypeLoader] 允许调用方提供当前配置餐段读取器,未提供时读取本机餐段配置。 +/// [allowManualChange] 关闭后禁止人工切换餐段:选择器只读,且与餐段配置不符时静默切换为配置餐段。 Widget appRadioHourTypeDialog( BuildContext context, FRadioSelectGroupController controller, { @@ -21,6 +22,7 @@ Widget appRadioHourTypeDialog( bool enabled = true, bool compact = false, bool checkConfiguredTime = false, + bool allowManualChange = true, Future? ready, Future Function()? currentHourTypeLoader, }) { @@ -28,13 +30,14 @@ Widget appRadioHourTypeDialog( return _MealPeriodSelectionGuard( controller: controller, ready: ready, + allowManualChange: allowManualChange, currentHourTypeLoader: currentHourTypeLoader, selectorBuilder: (context, candidateController) => _buildHourTypeSelector( context, candidateController, key: key, title: title, - enabled: enabled, + enabled: enabled && allowManualChange, compact: compact, ), ); @@ -44,7 +47,7 @@ Widget appRadioHourTypeDialog( controller, key: key, title: title, - enabled: enabled, + enabled: enabled && allowManualChange, compact: compact, ); } @@ -191,6 +194,7 @@ class _MealPeriodSelectionGuard extends StatefulWidget { required this.controller, required this.selectorBuilder, this.ready, + this.allowManualChange = true, this.currentHourTypeLoader, }); @@ -200,6 +204,9 @@ class _MealPeriodSelectionGuard extends StatefulWidget { /// 页面业务初始化信号;为空表示无需等待额外初始化。 final Future? ready; + /// 是否允许人工切换餐段;关闭后忽略人工候选并静默跟随餐段配置。 + final bool allowManualChange; + /// 当前时间命中餐段的读取器;测试可注入固定结果。 final Future Function()? currentHourTypeLoader; @@ -258,7 +265,8 @@ class _MealPeriodSelectionGuardState extends State<_MealPeriodSelectionGuard> { _selectionRevision++; _scheduleCheck(); } else if (oldWidget.ready != widget.ready || - oldWidget.currentHourTypeLoader != widget.currentHourTypeLoader) { + oldWidget.currentHourTypeLoader != widget.currentHourTypeLoader || + oldWidget.allowManualChange != widget.allowManualChange) { _scheduleCheck(); } } @@ -289,7 +297,9 @@ class _MealPeriodSelectionGuardState extends State<_MealPeriodSelectionGuard> { if (_syncingCandidate) return; final candidate = ProductHourType.values.fromCode(values.firstOrNull); final committedCode = widget.controller.value.firstOrNull; - if (candidate == null || + // 禁止人工修改时任何候选都不受理,候选显示立即回退为已提交餐段。 + if (!widget.allowManualChange || + candidate == null || candidate == ProductHourType.TOTAL || candidate.code == committedCode) { _syncCandidateFromCommitted(); @@ -333,6 +343,9 @@ class _MealPeriodSelectionGuardState extends State<_MealPeriodSelectionGuard> { } /// 校验人工候选或当前缓存餐段;人工确认后提交候选,初始化确认后提交配置餐段。 + /// + /// 禁止人工修改时不弹确认框:与餐段配置不符直接静默切换为配置餐段, + /// 当前时间未命中任何餐段时保留原选择,避免把餐段清空成不可用状态。 Future _checkSelection() async { if (_checking || !mounted) return; _checking = true; @@ -350,6 +363,12 @@ class _MealPeriodSelectionGuardState extends State<_MealPeriodSelectionGuard> { final configured = await (widget.currentHourTypeLoader?.call() ?? AppSettingTool.getCurrentProductHourType()); if (!mounted || revision != _selectionRevision) return; + if (!widget.allowManualChange) { + if (configured != null && configured != selected) { + _commitSelection(configured); + } + return; + } if (candidate != null && (configured == null || configured == candidate)) { _commitSelection(candidate); diff --git a/lib/common/widgets/order/select_pay_type_dialog.dart b/lib/common/widgets/order/select_pay_type_dialog.dart index 4aacf28..cba97a1 100644 --- a/lib/common/widgets/order/select_pay_type_dialog.dart +++ b/lib/common/widgets/order/select_pay_type_dialog.dart @@ -1557,7 +1557,7 @@ class _CheckoutCartItem extends StatelessWidget { } } -/// 套餐购物车行,默认展示全部组成,并将展开热区与数量步进器分离。 +/// 套餐购物车行,默认收起组成明细,并将展开热区与数量步进器分离。 class _CheckoutComboCartItem extends StatefulWidget { /// 创建一条可由主商品整行切换展开状态的套餐记录。 const _CheckoutComboCartItem({ @@ -1591,8 +1591,8 @@ class _CheckoutComboCartItem extends StatefulWidget { /// 管理单条套餐的展开状态;状态只属于当前购物车快照条目。 class _CheckoutComboCartItemState extends State<_CheckoutComboCartItem> { - /// 首次打开结算弹框时默认展开,方便收银员直接核对套餐组成。 - bool _expanded = true; + /// 首次打开结算弹框时默认收起组成明细,与查询菜单核销弹框的套餐行为一致。 + bool _expanded = false; /// 切换套餐明细;没有组成时不制造无反馈的点击行为。 void _toggleExpanded() { diff --git a/lib/pages/terminal/canteen_product/view.dart b/lib/pages/terminal/canteen_product/view.dart index 6315a88..44f3bbd 100644 --- a/lib/pages/terminal/canteen_product/view.dart +++ b/lib/pages/terminal/canteen_product/view.dart @@ -16,6 +16,7 @@ class CanteenProductPage extends GetView { super.key, this.showHoldAction = true, this.showMoreAction = true, + this.allowManualMealPeriodChange = true, }); /// 是否展示挂单操作入口,由外层收银工作区配置。 @@ -24,6 +25,9 @@ class CanteenProductPage extends GetView { /// 是否展示更多操作入口,由外层收银工作区配置。 final bool showMoreAction; + /// 是否允许收银页主动修改餐段;关闭后严格按餐段配置时间自动切换。 + final bool allowManualMealPeriodChange; + /// 构建食堂商品、购物车和餐段筛选工作区。 @override Widget build(BuildContext context) { @@ -90,6 +94,7 @@ class CanteenProductPage extends GetView { title: '餐段', compact: true, checkConfiguredTime: true, + allowManualChange: allowManualMealPeriodChange, ready: controller.initialization, ).width(280.w), cartTitle: '已点菜品', diff --git a/lib/pages/terminal/cashier/view.dart b/lib/pages/terminal/cashier/view.dart index 17fbd2e..9acdc12 100644 --- a/lib/pages/terminal/cashier/view.dart +++ b/lib/pages/terminal/cashier/view.dart @@ -22,6 +22,7 @@ class _CashierPageState extends State { SysType? _sysType; bool _showHoldAction = true; bool _showMoreAction = true; + bool _allowManualMealPeriodChange = true; @override void initState() { @@ -37,6 +38,8 @@ class _CashierPageState extends State { _sysType = setting.sysType; _showHoldAction = pagePreferences.showHoldAction; _showMoreAction = pagePreferences.showMoreAction; + _allowManualMealPeriodChange = + pagePreferences.allowManualMealPeriodChange; }); } @@ -60,6 +63,7 @@ class _CashierPageState extends State { return CanteenProductPage( showHoldAction: _showHoldAction, showMoreAction: _showMoreAction, + allowManualMealPeriodChange: _allowManualMealPeriodChange, ); } } diff --git a/lib/pages/terminal/menu_query/controller.dart b/lib/pages/terminal/menu_query/controller.dart index 85d5ec8..a6ae738 100644 --- a/lib/pages/terminal/menu_query/controller.dart +++ b/lib/pages/terminal/menu_query/controller.dart @@ -148,6 +148,9 @@ class ReservationMenuQueryController extends GetxController { /// 当前操作员在识别前选择的餐段,页面会在多位用户间保留选择以支持连续核销。 final selectedMealSlot = ReservationMealSlot.morning.obs; + /// 是否允许在查询菜单主动修改餐段;关闭后严格按餐段配置时间自动切换。 + final allowManualMealPeriodChange = true.obs; + /// 查询菜单与收银页面共用餐段组件的单选 Controller,值使用后端稳定餐段编码。 FRadioSelectGroupController get mealSlotC => _hourTypeControl.selection; @@ -274,6 +277,8 @@ class ReservationMenuQueryController extends GetxController { selectedMealSlot.value = _mealSlotForProductHourType( _hourTypeControl.selected.value, ); + allowManualMealPeriodChange.value = + preferences.allowManualMealPeriodChange; verificationDateScope.value = preferences.verificationDateScope; } catch (error, stack) { // 配置损坏或读取失败时继续使用安全默认值,不能让查询入口不可用。 diff --git a/lib/pages/terminal/menu_query/view.dart b/lib/pages/terminal/menu_query/view.dart index d71087d..6bfcd49 100644 --- a/lib/pages/terminal/menu_query/view.dart +++ b/lib/pages/terminal/menu_query/view.dart @@ -102,6 +102,8 @@ class ReservationMenuQueryPage extends GetView { continuousRecognitionEnabled: controller.continuousRecognitionEnabled.value, quickVerificationEnabled: controller.quickVerificationEnabled.value, + allowManualMealPeriodChange: + controller.allowManualMealPeriodChange.value, mealPeriodReady: controller.initialization, onSelect: controller.selectMethod, onStart: controller.startRecognition, diff --git a/lib/pages/terminal/menu_query/widgets/query_method_panel.dart b/lib/pages/terminal/menu_query/widgets/query_method_panel.dart index 8d208a2..d9f5dee 100644 --- a/lib/pages/terminal/menu_query/widgets/query_method_panel.dart +++ b/lib/pages/terminal/menu_query/widgets/query_method_panel.dart @@ -18,6 +18,7 @@ class QueryMethodPanel extends StatelessWidget { required this.autoRecognitionEnabled, required this.continuousRecognitionEnabled, required this.quickVerificationEnabled, + required this.allowManualMealPeriodChange, required this.onSelect, required this.onStart, this.mealPeriodReady, @@ -44,6 +45,9 @@ class QueryMethodPanel extends StatelessWidget { /// 是否开启查询后的快速核销;仅开启时展示核销目标餐段的预选入口。 final bool quickVerificationEnabled; + /// 是否允许在查询菜单主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。 + final bool allowManualMealPeriodChange; + /// 查询页面偏好初始化完成信号,用于在恢复最近餐段后校验当前时间配置。 final Future? mealPeriodReady; @@ -77,6 +81,7 @@ class QueryMethodPanel extends StatelessWidget { enabled: mealSelectionEnabled, compact: true, checkConfiguredTime: mealPeriodReady != null, + allowManualChange: allowManualMealPeriodChange, ready: mealPeriodReady, ), if (autoRecognitionEnabled) diff --git a/lib/pages/terminal/setting/controller.dart b/lib/pages/terminal/setting/controller.dart index 08b621e..c010300 100644 --- a/lib/pages/terminal/setting/controller.dart +++ b/lib/pages/terminal/setting/controller.dart @@ -60,6 +60,9 @@ class SettingController extends GetxController with WidgetsBindingObserver { /// 收银页右下角“更多”快捷操作是否展示。 final cashierShowMoreAction = true.obs; + /// 收银页是否允许主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。 + final cashierAllowManualMealPeriodChange = true.obs; + /// 预约查询识别方式的展示顺序,持久化到现有设置 JSON。 final reservationQueryMethodOrder = [ ...ReservationRecognitionMethod.values, @@ -83,6 +86,9 @@ class SettingController extends GetxController with WidgetsBindingObserver { /// 查询菜单是否在识别成功后直接打开今天预选餐段的核销弹框。 final reservationQueryQuickVerificationEnabled = false.obs; + /// 查询菜单是否允许主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。 + final reservationQueryAllowManualMealPeriodChange = true.obs; + /// 查询菜单核销允许的日期范围,默认仅限当天。 final reservationQueryVerificationDateScope = ReservationVerificationDateScope.today.obs; @@ -249,6 +255,8 @@ class SettingController extends GetxController with WidgetsBindingObserver { cashierReidentifiablePayTypes.assignAll(reidentifiablePayTypes); cashierShowHoldAction.value = cashierPagePreferences.showHoldAction; cashierShowMoreAction.value = cashierPagePreferences.showMoreAction; + cashierAllowManualMealPeriodChange.value = + cashierPagePreferences.allowManualMealPeriodChange; reservationQueryMethodOrder.assignAll( reservationQueryPreferences.methodOrder, ); @@ -264,6 +272,8 @@ class SettingController extends GetxController with WidgetsBindingObserver { reservationQueryPreferences.verificationEnabled; reservationQueryQuickVerificationEnabled.value = reservationQueryPreferences.quickVerificationEnabled; + reservationQueryAllowManualMealPeriodChange.value = + reservationQueryPreferences.allowManualMealPeriodChange; reservationQueryVerificationDateScope.value = reservationQueryPreferences.verificationDateScope; productMealPeriodSchedule.value = restoredProductMealPeriodSchedule; @@ -385,6 +395,13 @@ class SettingController extends GetxController with WidgetsBindingObserver { _scheduleReservationQueryPreferencesPersist(); } + /// 切换查询菜单是否允许主动修改餐段并安排持久化。 + void setReservationQueryAllowManualMealPeriodChange(bool allowed) { + if (reservationQueryAllowManualMealPeriodChange.value == allowed) return; + reservationQueryAllowManualMealPeriodChange.value = allowed; + _scheduleReservationQueryPreferencesPersist(); + } + /// 将预约查询页面偏好恢复为产品默认值并安排持久化。 void resetReservationQueryPreferences() { reservationQueryMethodOrder.assignAll( @@ -396,6 +413,7 @@ class SettingController extends GetxController with WidgetsBindingObserver { reservationQueryNameMaskEnabled.value = true; reservationQueryVerificationEnabled.value = false; reservationQueryQuickVerificationEnabled.value = false; + reservationQueryAllowManualMealPeriodChange.value = true; reservationQueryVerificationDateScope.value = ReservationVerificationDateScope.today; _scheduleReservationQueryPreferencesPersist(); @@ -425,6 +443,8 @@ class SettingController extends GetxController with WidgetsBindingObserver { verificationEnabled: reservationQueryVerificationEnabled.value, quickVerificationEnabled: reservationQueryQuickVerificationEnabled.value, + allowManualMealPeriodChange: + reservationQueryAllowManualMealPeriodChange.value, verificationDateScope: reservationQueryVerificationDateScope.value, ); } catch (e, stack) { @@ -654,10 +674,18 @@ class SettingController extends GetxController with WidgetsBindingObserver { _scheduleCashierPagePreferencesPersist(); } + /// 切换收银页是否允许主动修改餐段并安排持久化。 + void setCashierAllowManualMealPeriodChange(bool allowed) { + if (cashierAllowManualMealPeriodChange.value == allowed) return; + cashierAllowManualMealPeriodChange.value = allowed; + _scheduleCashierPagePreferencesPersist(); + } + /// 将收银页面快捷操作恢复为默认显示状态。 void resetCashierPagePreferences() { cashierShowHoldAction.value = true; cashierShowMoreAction.value = true; + cashierAllowManualMealPeriodChange.value = true; _scheduleCashierPagePreferencesPersist(); } @@ -678,6 +706,7 @@ class SettingController extends GetxController with WidgetsBindingObserver { await AppSettingTool.setCashierPagePreferences( showHoldAction: cashierShowHoldAction.value, showMoreAction: cashierShowMoreAction.value, + allowManualMealPeriodChange: cashierAllowManualMealPeriodChange.value, ); } catch (e, stack) { logger.e('|$_tag|保存收银页面配置失败error:|$e|stack:|$stack|'); diff --git a/lib/pages/terminal/setting/widgets/setting_page.dart b/lib/pages/terminal/setting/widgets/setting_page.dart index dd447f7..6b1d624 100644 --- a/lib/pages/terminal/setting/widgets/setting_page.dart +++ b/lib/pages/terminal/setting/widgets/setting_page.dart @@ -175,15 +175,16 @@ class _ReservationQuerySettings extends StatelessWidget { final SettingController controller; - /// 组合查询方式、自动识别、隐私和核销设置,并保持各区域独立响应式刷新。 + /// 组合查询方式、自动识别、隐私、核销和餐段设置,并保持各区域独立响应式刷新。 @override Widget build(BuildContext context) => [ _queryMethodSettings(controller), _autoRecognitionSettings(controller), _nameMaskSettings(controller), _verificationSettings(controller), + _mealPeriodSettings(controller), SettingNotice( - text: '查询顺序、默认项、自动识别、持续识别、姓名脱敏、核销和快速核销配置会在下次进入“查询菜单”时读取。', + text: '查询顺序、默认项、自动识别、持续识别、姓名脱敏、核销、快速核销和餐段修改权限配置会在下次进入“查询菜单”时读取。', icon: FAssets.icons.circleAlert, ), ].toColumnSpace(space: XfjDesignTokens.cardGap.h).scrollable(); @@ -344,6 +345,29 @@ Widget _verificationSettings(SettingController controller) => Obx(() { return SettingGroup(title: '核销设置', child: rows.toColumn()); }); +/// 查询菜单餐段修改权限独立观察,开关变化不会重建其他查询配置区域。 +Widget _mealPeriodSettings(SettingController controller) => Obx(() { + final allowed = + controller.reservationQueryAllowManualMealPeriodChange.value; + return SettingGroup( + title: '餐段设置', + child: SettingInfoRow( + icon: allowed ? FAssets.icons.sun : FAssets.icons.clock, + label: '允许主动修改餐段', + subtitle: allowed + ? '查询菜单可手动预选餐段;与餐段配置时间不符时仍会弹框提示确认。' + : '查询菜单餐段严格按“餐段配置”的时间自动切换,不允许手动修改。', + value: allowed ? '允许' : '不允许', + showDivider: false, + trailing: FSwitch( + semanticLabel: '查询菜单允许主动修改餐段', + value: allowed, + onChange: controller.setReservationQueryAllowManualMealPeriodChange, + ), + ), + ); + }); + /// 排序行由查询方式数据驱动,拖动和上下移动最终都进入同一个 Controller 方法。 Widget _queryMethodList({ required SettingController controller, @@ -438,8 +462,9 @@ class _CashierSettings extends StatelessWidget { ), ), _cashierActionSettings(controller), + _cashierMealPeriodSettings(controller), SettingNotice( - text: '支付方式顺序和右下角快捷操作都会同步到本机收银页;隐藏仅影响界面展示,不改变服务端权限、钱包范围和渠道支付能力。', + text: '支付方式顺序、右下角快捷操作和餐段修改权限都会同步到本机收银页;隐藏仅影响界面展示,不改变服务端权限、钱包范围和渠道支付能力。', icon: FAssets.icons.circleAlert, ), ].toColumnSpace(space: XfjDesignTokens.cardGap.h).scrollable(); @@ -480,6 +505,28 @@ Widget _cashierActionSettings(SettingController controller) => Obx(() { ); }); +/// 收银餐段修改权限独立观察,开关变化不会重建上方支付方式拖动列表。 +Widget _cashierMealPeriodSettings(SettingController controller) => Obx(() { + final allowed = controller.cashierAllowManualMealPeriodChange.value; + return SettingGroup( + title: '餐段设置', + child: SettingInfoRow( + icon: allowed ? FAssets.icons.sun : FAssets.icons.clock, + label: '允许主动修改餐段', + subtitle: allowed + ? '收银页可手动切换餐段;与餐段配置时间不符时仍会弹框提示确认。' + : '收银页餐段严格按“餐段配置”的时间自动切换,不允许手动修改。', + value: allowed ? '允许' : '不允许', + showDivider: false, + trailing: FSwitch( + semanticLabel: '收银允许主动修改餐段', + value: allowed, + onChange: controller.setCashierAllowManualMealPeriodChange, + ), + ), + ); + }); + String _defaultPayLabel(OrderPayDisplayType type) { return switch (type) { OrderPayDisplayType.qrCodeScan => '扫一扫', diff --git a/test/common/app_radio_hour_type_dialog_test.dart b/test/common/app_radio_hour_type_dialog_test.dart index 8fad518..a89b984 100644 --- a/test/common/app_radio_hour_type_dialog_test.dart +++ b/test/common/app_radio_hour_type_dialog_test.dart @@ -236,6 +236,81 @@ void main() { expect(find.textContaining('当前选择的餐段可能有问题'), findsNothing); expect(controller.value, {'2'}); }); + + testWidgets('禁止主动修改餐段时初始化静默切换为配置餐段且不弹框', (tester) async { + final controller = FRadioSelectGroupController(value: '1'); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _testApp( + Builder( + builder: (context) => appRadioHourTypeDialog( + context, + controller, + checkConfiguredTime: true, + allowManualChange: false, + ready: Future.value(), + currentHourTypeLoader: () async => ProductHourType.NOON, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.textContaining('当前选择的餐段可能有问题'), findsNothing); + expect(controller.value, {'2'}); + }); + + testWidgets('禁止主动修改餐段时选择器禁用且无法打开餐段菜单', (tester) async { + final controller = FRadioSelectGroupController(value: '2'); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _testApp( + Builder( + builder: (context) => appRadioHourTypeDialog( + context, + controller, + checkConfiguredTime: true, + allowManualChange: false, + ready: Future.value(), + currentHourTypeLoader: () async => ProductHourType.NOON, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(FSelectMenuTile)); + await tester.pumpAndSettle(); + + expect(find.byKey(const ValueKey('meal-slot-option-1')), findsNothing); + expect(controller.value, {'2'}); + }); + + testWidgets('禁止主动修改餐段时当前时间未命中餐段配置则保持原选择', (tester) async { + final controller = FRadioSelectGroupController(value: '1'); + addTearDown(controller.dispose); + + await tester.pumpWidget( + _testApp( + Builder( + builder: (context) => appRadioHourTypeDialog( + context, + controller, + checkConfiguredTime: true, + allowManualChange: false, + ready: Future.value(), + currentHourTypeLoader: () async => null, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.textContaining('当前选择的餐段可能有问题'), findsNothing); + expect(controller.value, {'1'}); + }); } Widget _testApp(Widget child) { diff --git a/test/common/cashier_checkout_dialog_test.dart b/test/common/cashier_checkout_dialog_test.dart index cc4bddb..ddcdf1b 100644 --- a/test/common/cashier_checkout_dialog_test.dart +++ b/test/common/cashier_checkout_dialog_test.dart @@ -153,6 +153,10 @@ void main() { await tester.pumpAndSettle(); expect(find.text('库存 2'), findsOneWidget); + // 套餐组成默认收起,点击套餐主行展开后才能看到组成明细。 + expect(find.text('时令青菜'), findsNothing); + await tester.tap(find.byKey(const ValueKey('checkout-combo-canteen-1'))); + await tester.pumpAndSettle(); expect(find.text('时令青菜'), findsOneWidget); expect(find.textContaining('可在支付前调整数量'), findsOneWidget); expect(find.textContaining('确认支付').evaluate().single.widget, isA()); @@ -348,7 +352,8 @@ void main() { tester.getTopLeft(cartPane).dy, tester.getTopLeft(paymentPane).dy); expect(find.textContaining('x9999'), findsOneWidget); expect(find.text('套餐'), findsOneWidget); - expect(find.text('清炒时蔬'), findsOneWidget); + // 套餐组成默认收起,首次进入不渲染组成行。 + expect(find.text('清炒时蔬'), findsNothing); expect( find.byKey( const ValueKey('checkout-product-image-long-name-product'), @@ -357,7 +362,7 @@ void main() { ); expect( find.byKey(const ValueKey('checkout-combo-image-combo-child-1')), - findsOneWidget, + findsNothing, ); expect(find.text('选择支付方式'), findsOneWidget); final methodFinders = [ @@ -385,10 +390,10 @@ void main() { await tester.tap(find.text('营养套餐')); await tester.pumpAndSettle(); - expect(find.text('清炒时蔬').hitTestable(), findsNothing); + expect(find.text('清炒时蔬').hitTestable(), findsOneWidget); await tester.tap(find.text('营养套餐')); await tester.pumpAndSettle(); - expect(find.text('清炒时蔬').hitTestable(), findsOneWidget); + expect(find.text('清炒时蔬').hitTestable(), findsNothing); final wallet = ServiceWallet( id: 'wallet-1', diff --git a/test/pages/terminal/query_method_panel_test.dart b/test/pages/terminal/query_method_panel_test.dart index 50f7634..b21919c 100644 --- a/test/pages/terminal/query_method_panel_test.dart +++ b/test/pages/terminal/query_method_panel_test.dart @@ -26,6 +26,7 @@ void main() { autoRecognitionEnabled: false, continuousRecognitionEnabled: false, quickVerificationEnabled: false, + allowManualMealPeriodChange: true, onSelect: (method) => selectedMethod = method, onStart: () => startCalls++, ), @@ -47,6 +48,7 @@ void main() { autoRecognitionEnabled: true, continuousRecognitionEnabled: true, quickVerificationEnabled: false, + allowManualMealPeriodChange: true, onSelect: (method) => selectedMethod = method, onStart: () => startCalls++, ), @@ -78,6 +80,7 @@ void main() { autoRecognitionEnabled: true, continuousRecognitionEnabled: true, quickVerificationEnabled: false, + allowManualMealPeriodChange: true, onSelect: (_) {}, onStart: () {}, ), @@ -100,6 +103,7 @@ void main() { autoRecognitionEnabled: true, continuousRecognitionEnabled: false, quickVerificationEnabled: true, + allowManualMealPeriodChange: true, onSelect: (_) {}, onStart: () {}, ), @@ -139,6 +143,7 @@ void main() { autoRecognitionEnabled: false, continuousRecognitionEnabled: true, quickVerificationEnabled: true, + allowManualMealPeriodChange: true, onSelect: (_) {}, onStart: () {}, ), @@ -180,6 +185,7 @@ void main() { autoRecognitionEnabled: false, continuousRecognitionEnabled: true, quickVerificationEnabled: true, + allowManualMealPeriodChange: true, onSelect: (_) {}, onStart: () {}, ), @@ -205,6 +211,7 @@ void main() { autoRecognitionEnabled: false, continuousRecognitionEnabled: true, quickVerificationEnabled: true, + allowManualMealPeriodChange: true, onSelect: (_) {}, onStart: () {}, ), @@ -226,6 +233,42 @@ void main() { ); expect(tester.takeException(), isNull); }); + + testWidgets('禁止主动修改餐段时选择器禁用且无法打开餐段菜单', (tester) async { + await tester.binding.setSurfaceSize(const Size(1920, 1080)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + final mealSlotController = FRadioSelectGroupController(value: '1'); + addTearDown(mealSlotController.dispose); + + await tester.pumpWidget( + _testApp( + QueryMethodPanel( + methods: ReservationRecognitionMethod.values, + selected: ReservationRecognitionMethod.card, + status: ReservationMenuQueryStatus.idle, + mealSlotController: mealSlotController, + autoRecognitionEnabled: false, + continuousRecognitionEnabled: true, + quickVerificationEnabled: true, + allowManualMealPeriodChange: false, + onSelect: (_) {}, + onStart: () {}, + ), + ), + ); + + final trigger = find.byKey( + const ValueKey('reservation-meal-slot-trigger'), + ); + expect(find.text('早餐'), findsOneWidget); + expect(tester.widget>(trigger).enabled, isFalse); + + await tester.tap(trigger); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('meal-slot-option-2')), findsNothing); + expect(mealSlotController.value, {'1'}); + expect(tester.takeException(), isNull); + }); } /// 构建与终端设计基准一致的主题和尺寸环境,只验证本组件的可见状态与点击链路。