feat:餐段设置

This commit is contained in:
dzc committed 2026-09-07 12:53:05 +08:00
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;
+27 -4
View File
@@ -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: '已点菜品',
+4
View File
@@ -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) {
// 配置损坏或读取失败时继续使用安全默认值,不能让查询入口不可用。
+2
View File
@@ -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);
});
}
/// 构建与终端设计基准一致的主题和尺寸环境,只验证本组件的可见状态与点击链路。