feat:餐段设置
This commit is contained in:
1 parent
67950343a7
commit
07a3141934
14 files changed
+285
-18
No files matched your search
@@ -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;
|
||||
|
||||
|
||||
@@ -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<String, dynamic>.from(json.decode(setting.sysJson));
|
||||
return (
|
||||
showHoldAction: sysJson[_cashierShowHoldActionKey] != false,
|
||||
showMoreAction: sysJson[_cashierShowMoreActionKey] != false,
|
||||
allowManualMealPeriodChange:
|
||||
sysJson[_cashierAllowManualMealPeriodChangeKey] != false,
|
||||
);
|
||||
}
|
||||
|
||||
/// 收银快捷操作属于本机页面布局配置,继续复用 sysJson,避免增加数据库字段。
|
||||
/// 收银快捷操作与餐段修改权限属于本机页面布局配置,继续复用 sysJson。
|
||||
static Future<void> 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 = <ReservationRecognitionMethod>[];
|
||||
@@ -664,6 +685,8 @@ abstract class AppSettingTool {
|
||||
sysJson[_reservationQueryVerificationEnabledKey] = verificationEnabled;
|
||||
sysJson[_reservationQueryQuickVerificationEnabledKey] =
|
||||
verificationEnabled && quickVerificationEnabled;
|
||||
sysJson[_reservationQueryAllowManualMealPeriodChangeKey] =
|
||||
allowManualMealPeriodChange;
|
||||
sysJson[_reservationQueryVerificationDateScopeKey] =
|
||||
verificationDateScope.code;
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
/// 收银、预订和设置页面继续使用原有的图标与强调色样式。
|
||||
/// [checkConfiguredTime] 开启后会在页面初始化完成时校验当前餐段;[ready] 用于等待调用方恢复自身状态。
|
||||
/// [currentHourTypeLoader] 允许调用方提供当前配置餐段读取器,未提供时读取本机餐段配置。
|
||||
/// [allowManualChange] 关闭后禁止人工切换餐段:选择器只读,且与餐段配置不符时静默切换为配置餐段。
|
||||
Widget appRadioHourTypeDialog(
|
||||
BuildContext context,
|
||||
FRadioSelectGroupController<String> controller, {
|
||||
@@ -21,6 +22,7 @@ Widget appRadioHourTypeDialog(
|
||||
bool enabled = true,
|
||||
bool compact = false,
|
||||
bool checkConfiguredTime = false,
|
||||
bool allowManualChange = true,
|
||||
Future<void>? ready,
|
||||
Future<ProductHourType?> 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<void>? ready;
|
||||
|
||||
/// 是否允许人工切换餐段;关闭后忽略人工候选并静默跟随餐段配置。
|
||||
final bool allowManualChange;
|
||||
|
||||
/// 当前时间命中餐段的读取器;测试可注入固定结果。
|
||||
final Future<ProductHourType?> 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<void> _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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -16,6 +16,7 @@ class CanteenProductPage extends GetView<CanteenProductController> {
|
||||
super.key,
|
||||
this.showHoldAction = true,
|
||||
this.showMoreAction = true,
|
||||
this.allowManualMealPeriodChange = true,
|
||||
});
|
||||
|
||||
/// 是否展示挂单操作入口,由外层收银工作区配置。
|
||||
@@ -24,6 +25,9 @@ class CanteenProductPage extends GetView<CanteenProductController> {
|
||||
/// 是否展示更多操作入口,由外层收银工作区配置。
|
||||
final bool showMoreAction;
|
||||
|
||||
/// 是否允许收银页主动修改餐段;关闭后严格按餐段配置时间自动切换。
|
||||
final bool allowManualMealPeriodChange;
|
||||
|
||||
/// 构建食堂商品、购物车和餐段筛选工作区。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -90,6 +94,7 @@ class CanteenProductPage extends GetView<CanteenProductController> {
|
||||
title: '餐段',
|
||||
compact: true,
|
||||
checkConfiguredTime: true,
|
||||
allowManualChange: allowManualMealPeriodChange,
|
||||
ready: controller.initialization,
|
||||
).width(280.w),
|
||||
cartTitle: '已点菜品',
|
||||
|
||||
@@ -22,6 +22,7 @@ class _CashierPageState extends State<CashierPage> {
|
||||
SysType? _sysType;
|
||||
bool _showHoldAction = true;
|
||||
bool _showMoreAction = true;
|
||||
bool _allowManualMealPeriodChange = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -37,6 +38,8 @@ class _CashierPageState extends State<CashierPage> {
|
||||
_sysType = setting.sysType;
|
||||
_showHoldAction = pagePreferences.showHoldAction;
|
||||
_showMoreAction = pagePreferences.showMoreAction;
|
||||
_allowManualMealPeriodChange =
|
||||
pagePreferences.allowManualMealPeriodChange;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,6 +63,7 @@ class _CashierPageState extends State<CashierPage> {
|
||||
return CanteenProductPage(
|
||||
showHoldAction: _showHoldAction,
|
||||
showMoreAction: _showMoreAction,
|
||||
allowManualMealPeriodChange: _allowManualMealPeriodChange,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,9 @@ class ReservationMenuQueryController extends GetxController {
|
||||
/// 当前操作员在识别前选择的餐段,页面会在多位用户间保留选择以支持连续核销。
|
||||
final selectedMealSlot = ReservationMealSlot.morning.obs;
|
||||
|
||||
/// 是否允许在查询菜单主动修改餐段;关闭后严格按餐段配置时间自动切换。
|
||||
final allowManualMealPeriodChange = true.obs;
|
||||
|
||||
/// 查询菜单与收银页面共用餐段组件的单选 Controller,值使用后端稳定餐段编码。
|
||||
FRadioSelectGroupController<String> 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) {
|
||||
// 配置损坏或读取失败时继续使用安全默认值,不能让查询入口不可用。
|
||||
|
||||
@@ -102,6 +102,8 @@ class ReservationMenuQueryPage extends GetView<ReservationMenuQueryController> {
|
||||
continuousRecognitionEnabled:
|
||||
controller.continuousRecognitionEnabled.value,
|
||||
quickVerificationEnabled: controller.quickVerificationEnabled.value,
|
||||
allowManualMealPeriodChange:
|
||||
controller.allowManualMealPeriodChange.value,
|
||||
mealPeriodReady: controller.initialization,
|
||||
onSelect: controller.selectMethod,
|
||||
onStart: controller.startRecognition,
|
||||
|
||||
@@ -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<void>? mealPeriodReady;
|
||||
|
||||
@@ -77,6 +81,7 @@ class QueryMethodPanel extends StatelessWidget {
|
||||
enabled: mealSelectionEnabled,
|
||||
compact: true,
|
||||
checkConfiguredTime: mealPeriodReady != null,
|
||||
allowManualChange: allowManualMealPeriodChange,
|
||||
ready: mealPeriodReady,
|
||||
),
|
||||
if (autoRecognitionEnabled)
|
||||
|
||||
@@ -60,6 +60,9 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
/// 收银页右下角“更多”快捷操作是否展示。
|
||||
final cashierShowMoreAction = true.obs;
|
||||
|
||||
/// 收银页是否允许主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。
|
||||
final cashierAllowManualMealPeriodChange = true.obs;
|
||||
|
||||
/// 预约查询识别方式的展示顺序,持久化到现有设置 JSON。
|
||||
final reservationQueryMethodOrder = <ReservationRecognitionMethod>[
|
||||
...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|');
|
||||
|
||||
@@ -175,15 +175,16 @@ class _ReservationQuerySettings extends StatelessWidget {
|
||||
|
||||
final SettingController controller;
|
||||
|
||||
/// 组合查询方式、自动识别、隐私和核销设置,并保持各区域独立响应式刷新。
|
||||
/// 组合查询方式、自动识别、隐私、核销和餐段设置,并保持各区域独立响应式刷新。
|
||||
@override
|
||||
Widget build(BuildContext context) => <Widget>[
|
||||
_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 => '扫一扫',
|
||||
|
||||
@@ -236,6 +236,81 @@ void main() {
|
||||
expect(find.textContaining('当前选择的餐段可能有问题'), findsNothing);
|
||||
expect(controller.value, {'2'});
|
||||
});
|
||||
|
||||
testWidgets('禁止主动修改餐段时初始化静默切换为配置餐段且不弹框', (tester) async {
|
||||
final controller = FRadioSelectGroupController<String>(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<String>(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<String>));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const ValueKey('meal-slot-option-1')), findsNothing);
|
||||
expect(controller.value, {'2'});
|
||||
});
|
||||
|
||||
testWidgets('禁止主动修改餐段时当前时间未命中餐段配置则保持原选择', (tester) async {
|
||||
final controller = FRadioSelectGroupController<String>(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) {
|
||||
|
||||
@@ -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<Text>());
|
||||
@@ -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 = <Finder>[
|
||||
@@ -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',
|
||||
|
||||
@@ -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<String>(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<FSelectMenuTile<String>>(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);
|
||||
});
|
||||
}
|
||||
|
||||
/// 构建与终端设计基准一致的主题和尺寸环境,只验证本组件的可见状态与点击链路。
|
||||
|
||||
Reference in new issue
Block a user