feat:查询菜谱适配小键盘硬件
This commit is contained in:
1 parent
a179729e79
commit
8e9bcba3c3
11 files changed
+861
-7
No files matched your search
@@ -0,0 +1,34 @@
|
||||
# 修复"绑定按键按下 e 后无响应/软键盘弹出"问题
|
||||
|
||||
## 根因
|
||||
|
||||
截图证明"取消"行已进入"等待按键…"监听态,但按"e"后**没有任何 Toast、没有任何绑定变化** —— 说明按键事件根本没到达 `HardwareKeyboard` 的监听 handler(若到达,成功/冲突都必有 Toast)。
|
||||
|
||||
结合"按 e 时软键盘弹出",根因是 **Android IME(输入法)拦截**:
|
||||
|
||||
1. 当输入法连接处于激活状态(软键盘弹出、或此前某个输入框残留焦点未释放)时,Android 会把物理键盘的字符键(如 e)先交给输入法,**合成为文本提交**,不再作为 KeyEvent 分发给 Flutter —— 我们的 handler 因此收不到任何 KeyDownEvent。
|
||||
2. 若用户按的是**屏幕软键盘上的"e"**(小键盘尚未接入、用软键盘模拟测试),同样只产生 IME 文本提交,Flutter 侧本来就收不到"按了哪个键"的按键事件 —— 软键盘按键**机制上无法**用于绑定。
|
||||
|
||||
排查确认:设置模块存在多个可激活 IME 的入口(日志预览关键词筛选框、读写卡密钥三个输入框、食堂机型人脸姓名搜索框、`autofocus: true` 的人工硬件输入弹窗);而我们的 `_startListening` **只注册了键盘监听器,没有断开输入焦点** —— 项目里 `BarcodeScanManage`(扫码枪)正是因为同样的问题,在监听前和每次事件时都先 `unfocus()`。
|
||||
|
||||
## 修复内容(仅改 1 个文件)
|
||||
|
||||
`lib/pages/terminal/setting/widgets/setting_menu_query_shortcut.dart`:
|
||||
|
||||
1. **`_startListening` 进入监听时断开输入焦点并隐藏软键盘**(对齐项目既有防御写法 `barcode_scan_manage.dart:63` 与 `order_history/view.dart:433-434` 组合):
|
||||
```dart
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
SystemChannels.textInput.invokeMethod<void>('TextInput.hide');
|
||||
```
|
||||
确保真实小键盘的物理按键走 KeyEvent 路径直达监听器,不被输入法吞掉。
|
||||
|
||||
2. **提示文案说明软键盘不可用**:等待提示与空闲提示明确"请按外接小键盘的物理按键,屏幕软键盘按键无法完成绑定",避免现场用软键盘测试时误判功能损坏。
|
||||
|
||||
## 验证
|
||||
|
||||
- `flutter analyze` + `dart format` 改动文件;
|
||||
- 说明清楚:软键盘按键在 Flutter 中不产生按键事件,属于平台机制,绑定功能需接入真实外接小键盘后才能完整验证(本次修复保证接入后 IME 不再拦截)。
|
||||
|
||||
## 变更统计
|
||||
|
||||
预计新增约 12 行 / 删除 2 行(单文件小改)。
|
||||
@@ -7,7 +7,7 @@ flutter pub run build_runner build --delete-conflicting-outputs~
|
||||
## flutter打包
|
||||
|
||||
```
|
||||
flutter build apk --build-name 1.4.8 --build-number 148
|
||||
flutter build apk --build-name 1.5.0 --build-number 150
|
||||
--build-name为版本号(展示)
|
||||
--build-number为版本号(内部比较)
|
||||
```
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../api/hard_api_models.dart';
|
||||
@@ -92,6 +93,103 @@ enum ReservationVerificationDateScope {
|
||||
final String title;
|
||||
}
|
||||
|
||||
/// 查询菜单外接快捷小键盘上可绑定的动作。
|
||||
///
|
||||
/// [code] 会保存到终端设置中,必须保持稳定;动作语义与查询菜单页面操作一一对应。
|
||||
enum MenuQueryShortcutAction {
|
||||
/// 取消当前识别流程并清空查询结果。
|
||||
cancel('cancel', '取消'),
|
||||
|
||||
/// 对当前识别出的用户提交核销。
|
||||
verification('verification', '核销'),
|
||||
|
||||
/// 切换到人脸识别并立即启动识别。
|
||||
faceRecognition('face', '人脸识别'),
|
||||
|
||||
/// 切换到饭卡识别并立即启动识别。
|
||||
cardRecognition('card', '饭卡识别'),
|
||||
|
||||
/// 重新打印最近一次小票。
|
||||
reprint('reprint', '重新打印'),
|
||||
;
|
||||
|
||||
/// 创建与稳定编码绑定的快捷动作。
|
||||
const MenuQueryShortcutAction(this.code, this.title);
|
||||
|
||||
/// 终端设置中持久化的稳定动作编码。
|
||||
final String code;
|
||||
|
||||
/// 设置页展示、查询菜单运行时复用的动作名称。
|
||||
final String title;
|
||||
}
|
||||
|
||||
/// 单个快捷动作与物理按键的绑定关系,保存在本机 sysJson 中,不涉及数据库结构。
|
||||
@immutable
|
||||
@JsonSerializable()
|
||||
class MenuQueryShortcutKeyBinding {
|
||||
/// 创建按键绑定;设备未提供物理键位时 [usbHidUsage] 允许为 0,仅以逻辑键兜底匹配。
|
||||
const MenuQueryShortcutKeyBinding({
|
||||
this.usbHidUsage = 0,
|
||||
this.logicalKeyId = 0,
|
||||
});
|
||||
|
||||
/// 从真实硬件键盘事件提取绑定;物理键位是首选匹配依据,逻辑键仅兜底。
|
||||
factory MenuQueryShortcutKeyBinding.fromKeyEvent(KeyEvent event) {
|
||||
return MenuQueryShortcutKeyBinding(
|
||||
usbHidUsage: event.physicalKey.usbHidUsage,
|
||||
logicalKeyId: event.logicalKey.keyId,
|
||||
);
|
||||
}
|
||||
|
||||
factory MenuQueryShortcutKeyBinding.fromJson(Map<String, dynamic> json) =>
|
||||
_$MenuQueryShortcutKeyBindingFromJson(json);
|
||||
|
||||
/// 按键的 USB HID 物理键位;同一物理按键不随键盘布局变化,0 表示设备未提供。
|
||||
final int usbHidUsage;
|
||||
|
||||
/// 按键的逻辑键 ID;仅作为物理键位缺失时的兜底匹配与展示依据,0 表示未记录。
|
||||
final int logicalKeyId;
|
||||
|
||||
Map<String, dynamic> toJson() => _$MenuQueryShortcutKeyBindingToJson(this);
|
||||
|
||||
/// 判断两个绑定是否指向同一个按键。
|
||||
///
|
||||
/// 物理或逻辑键任一相同即视为冲突:物理键相同说明是同一个物理按键;
|
||||
/// 物理键不同但逻辑键相同时,运行期匹配存在歧义风险,同样不允许重复绑定。
|
||||
bool conflictsWith(MenuQueryShortcutKeyBinding other) {
|
||||
if (usbHidUsage != 0 &&
|
||||
other.usbHidUsage != 0 &&
|
||||
usbHidUsage == other.usbHidUsage) {
|
||||
return true;
|
||||
}
|
||||
return logicalKeyId != 0 &&
|
||||
other.logicalKeyId != 0 &&
|
||||
logicalKeyId == other.logicalKeyId;
|
||||
}
|
||||
|
||||
/// 展示按键的可读名称;优先物理键名,其次逻辑键名,均缺失时回退到键值,便于现场排查。
|
||||
String get displayLabel {
|
||||
final physicalName =
|
||||
PhysicalKeyboardKey.findKeyByCode(usbHidUsage)?.debugName;
|
||||
if (physicalName != null && physicalName.isNotEmpty) return physicalName;
|
||||
final logicalName =
|
||||
LogicalKeyboardKey.findKeyByKeyId(logicalKeyId)?.debugName;
|
||||
if (logicalName != null && logicalName.isNotEmpty) return logicalName;
|
||||
return '键码 0x${usbHidUsage.toRadixString(16).padLeft(4, '0').toUpperCase()}';
|
||||
}
|
||||
|
||||
/// 按全部持久化字段比较,用于恢复配置与测试断言。
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is MenuQueryShortcutKeyBinding &&
|
||||
other.usbHidUsage == usbHidUsage &&
|
||||
other.logicalKeyId == logicalKeyId;
|
||||
|
||||
/// 与相等判断字段一致的哈希值。
|
||||
@override
|
||||
int get hashCode => Object.hash(usbHidUsage, logicalKeyId);
|
||||
}
|
||||
|
||||
/// 查询菜单在本机保存的页面交互偏好,不改变预约接口和硬件协议。
|
||||
@immutable
|
||||
class ReservationQueryPagePreferences {
|
||||
@@ -107,6 +205,7 @@ class ReservationQueryPagePreferences {
|
||||
this.allowManualMealPeriodChange = true,
|
||||
this.selectedMealSlot = ReservationMealSlot.morning,
|
||||
this.verificationDateScope = ReservationVerificationDateScope.today,
|
||||
this.shortcutKeyBindings = const {},
|
||||
});
|
||||
|
||||
/// 未保存页面设置时使用的默认查询偏好。
|
||||
@@ -121,6 +220,7 @@ class ReservationQueryPagePreferences {
|
||||
allowManualMealPeriodChange: true,
|
||||
selectedMealSlot: ReservationMealSlot.morning,
|
||||
verificationDateScope: ReservationVerificationDateScope.today,
|
||||
shortcutKeyBindings: {},
|
||||
);
|
||||
|
||||
/// 查询方式的页面展示顺序。
|
||||
@@ -154,6 +254,10 @@ class ReservationQueryPagePreferences {
|
||||
|
||||
/// 允许显示核销入口的预约日期范围。
|
||||
final ReservationVerificationDateScope verificationDateScope;
|
||||
|
||||
/// 外接快捷小键盘的动作按键绑定;键为动作,值为绑定的物理按键,未配置时为空集合。
|
||||
final Map<MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>
|
||||
shortcutKeyBindings;
|
||||
}
|
||||
|
||||
/// 预约订单中的套餐组成展示模型,统一处理缺失名称和异常份数。
|
||||
|
||||
@@ -6,6 +6,20 @@ part of 'reservation_menu_page_models.dart';
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
MenuQueryShortcutKeyBinding _$MenuQueryShortcutKeyBindingFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
MenuQueryShortcutKeyBinding(
|
||||
usbHidUsage: (json['usbHidUsage'] as num?)?.toInt() ?? 0,
|
||||
logicalKeyId: (json['logicalKeyId'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MenuQueryShortcutKeyBindingToJson(
|
||||
MenuQueryShortcutKeyBinding instance) =>
|
||||
<String, dynamic>{
|
||||
'usbHidUsage': instance.usbHidUsage,
|
||||
'logicalKeyId': instance.logicalKeyId,
|
||||
};
|
||||
|
||||
ReservationMenuMealData _$ReservationMenuMealDataFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
ReservationMenuMealData(
|
||||
|
||||
@@ -77,6 +77,11 @@ abstract class AppSettingTool {
|
||||
static const String _reservationQueryVerificationDateScopeKey =
|
||||
"reservationQueryVerificationDateScope";
|
||||
|
||||
/// 查询菜单外接快捷小键盘动作绑定在现有 sysJson 中的稳定存储键;
|
||||
/// 值为“动作编码 -> 按键绑定对象”,缺失或空对象表示全部未绑定。
|
||||
static const String _menuQueryShortcutKeyBindingsKey =
|
||||
"menuQueryShortcutKeyBindings";
|
||||
|
||||
/// 早餐、中餐和晚餐的本地时间范围;值使用现有 ProductHourType 后端编码。
|
||||
static const String _productMealPeriodsKey = "productMealPeriods";
|
||||
static const String _serialCardConfigKey = "serialCardConfig";
|
||||
@@ -629,6 +634,9 @@ abstract class AppSettingTool {
|
||||
)
|
||||
.firstOrNull ??
|
||||
ReservationMealSlot.morning;
|
||||
final shortcutKeyBindings = _parseMenuQueryShortcutKeyBindings(
|
||||
sysJson[_menuQueryShortcutKeyBindingsKey],
|
||||
);
|
||||
return ReservationQueryPagePreferences(
|
||||
methodOrder: List.unmodifiable(restored),
|
||||
defaultMethod: defaultMethod,
|
||||
@@ -649,9 +657,42 @@ abstract class AppSettingTool {
|
||||
sysJson[_reservationQueryAllowManualMealPeriodChangeKey] != false,
|
||||
selectedMealSlot: selectedMealSlot,
|
||||
verificationDateScope: verificationDateScope,
|
||||
shortcutKeyBindings: shortcutKeyBindings,
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析快捷小键盘绑定为强类型映射;损坏条目跳过,重复按键只保留首个动作。
|
||||
///
|
||||
/// sysJson 属于存储边界,可能被旧版本或人工编辑产生异常结构,
|
||||
/// 单条解析失败不允许阻断其余查询菜单配置读取。
|
||||
static Map<MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>
|
||||
_parseMenuQueryShortcutKeyBindings(Object? source) {
|
||||
// 始终返回可变 Map:调用方可能直接写入,const 空 Map 会导致写入抛异常。
|
||||
if (source is! Map) {
|
||||
return <MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>{};
|
||||
}
|
||||
final byCode = {
|
||||
for (final action in MenuQueryShortcutAction.values) action.code: action,
|
||||
};
|
||||
final bindings = <MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>{};
|
||||
for (final entry in source.entries) {
|
||||
final action = byCode[entry.key];
|
||||
if (action == null || entry.value is! Map) continue;
|
||||
try {
|
||||
final binding = MenuQueryShortcutKeyBinding.fromJson(
|
||||
Map<String, dynamic>.from(entry.value as Map),
|
||||
);
|
||||
// 空绑定没有运行期匹配价值;与已恢复动作冲突的按键保留先出现的动作。
|
||||
if (binding.usbHidUsage == 0 && binding.logicalKeyId == 0) continue;
|
||||
if (bindings.values.any(binding.conflictsWith)) continue;
|
||||
bindings[action] = binding;
|
||||
} on Object {
|
||||
// 忽略无法解析的历史绑定,继续恢复其余动作。
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/// 顺序与默认项一起保存,避免连续操作后出现“默认项不在当前列表中”的矛盾状态。
|
||||
static Future<void> setReservationQueryPagePreferences({
|
||||
required List<ReservationRecognitionMethod> order,
|
||||
@@ -663,6 +704,8 @@ abstract class AppSettingTool {
|
||||
required bool quickVerificationEnabled,
|
||||
required bool allowManualMealPeriodChange,
|
||||
required ReservationVerificationDateScope verificationDateScope,
|
||||
required Map<MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>
|
||||
shortcutKeyBindings,
|
||||
}) {
|
||||
final normalized = <ReservationRecognitionMethod>[];
|
||||
for (final method in order) {
|
||||
@@ -689,6 +732,12 @@ abstract class AppSettingTool {
|
||||
allowManualMealPeriodChange;
|
||||
sysJson[_reservationQueryVerificationDateScopeKey] =
|
||||
verificationDateScope.code;
|
||||
// 持久化边界再次收敛空绑定,保证落盘结构与“未绑定即不写入”的读取约定一致。
|
||||
sysJson[_menuQueryShortcutKeyBindingsKey] = {
|
||||
for (final entry in shortcutKeyBindings.entries)
|
||||
if (entry.value.usbHidUsage != 0 || entry.value.logicalKeyId != 0)
|
||||
entry.key.code: entry.value.toJson(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,6 @@ class _DeviceSetupPageState extends State<DeviceSetupPage> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.print_outlined,
|
||||
size: 42, color: XfjDesignTokens.primary),
|
||||
const SizedBox(height: 20),
|
||||
const Text('绑定终端设备',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -93,6 +93,10 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
final reservationQueryVerificationDateScope =
|
||||
ReservationVerificationDateScope.today.obs;
|
||||
|
||||
/// 查询菜单外接快捷小键盘的动作按键绑定;键为动作,值为最近一次绑定的物理按键。
|
||||
final menuQueryShortcutKeyBindings =
|
||||
<MenuQueryShortcutAction, MenuQueryShortcutKeyBinding>{}.obs;
|
||||
|
||||
/// 早餐、中餐和晚餐的本地时间范围,设置加载后由 sysJson 中的持久化值覆盖。
|
||||
final productMealPeriodSchedule = ProductMealPeriodSchedule.defaults.obs;
|
||||
|
||||
@@ -276,6 +280,11 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
reservationQueryPreferences.allowManualMealPeriodChange;
|
||||
reservationQueryVerificationDateScope.value =
|
||||
reservationQueryPreferences.verificationDateScope;
|
||||
// GetX 的 assignAll 对 RxMap 是引用赋值,而偏好默认值可能是 const 空 Map,
|
||||
// 必须拷贝成可变副本,否则后续写入绑定会抛“Cannot modify unmodifiable map”。
|
||||
menuQueryShortcutKeyBindings.assignAll(
|
||||
Map.of(reservationQueryPreferences.shortcutKeyBindings),
|
||||
);
|
||||
productMealPeriodSchedule.value = restoredProductMealPeriodSchedule;
|
||||
if (setting.usePayDisplayType != null) {
|
||||
setting.usePayDisplayType = restoredPayTypeOrder
|
||||
@@ -402,6 +411,42 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
_scheduleReservationQueryPreferencesPersist();
|
||||
}
|
||||
|
||||
/// 绑定查询菜单快捷小键盘动作按键并安排持久化。
|
||||
///
|
||||
/// 同一物理按键已被其他动作占用时拒绝写入并提示,返回是否绑定成功;
|
||||
/// 冲突校验收敛在 Controller,保证设置页与后续其他入口执行同一规则。
|
||||
bool setMenuQueryShortcutKeyBinding(
|
||||
MenuQueryShortcutAction action,
|
||||
MenuQueryShortcutKeyBinding binding,
|
||||
) {
|
||||
final occupied = menuQueryShortcutKeyBindings.entries
|
||||
.where(
|
||||
(entry) => entry.key != action && entry.value.conflictsWith(binding),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (occupied != null) {
|
||||
Toast.error(msg: '该按键已绑定“${occupied.key.title}”,请先清除或改按其他按键');
|
||||
return false;
|
||||
}
|
||||
menuQueryShortcutKeyBindings[action] = binding;
|
||||
_scheduleReservationQueryPreferencesPersist();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 清除指定动作的快捷按键绑定并安排持久化。
|
||||
void clearMenuQueryShortcutKeyBinding(MenuQueryShortcutAction action) {
|
||||
if (!menuQueryShortcutKeyBindings.containsKey(action)) return;
|
||||
menuQueryShortcutKeyBindings.remove(action);
|
||||
_scheduleReservationQueryPreferencesPersist();
|
||||
}
|
||||
|
||||
/// 清空全部快捷按键绑定并安排持久化,供“还原”操作复用。
|
||||
void resetMenuQueryShortcutKeyBindings() {
|
||||
if (menuQueryShortcutKeyBindings.isEmpty) return;
|
||||
menuQueryShortcutKeyBindings.clear();
|
||||
_scheduleReservationQueryPreferencesPersist();
|
||||
}
|
||||
|
||||
/// 将预约查询页面偏好恢复为产品默认值并安排持久化。
|
||||
void resetReservationQueryPreferences() {
|
||||
reservationQueryMethodOrder.assignAll(
|
||||
@@ -416,6 +461,8 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
reservationQueryAllowManualMealPeriodChange.value = true;
|
||||
reservationQueryVerificationDateScope.value =
|
||||
ReservationVerificationDateScope.today;
|
||||
// 快捷按键绑定属于可整体还原的本机偏好,还原后回到全部未绑定状态。
|
||||
menuQueryShortcutKeyBindings.clear();
|
||||
_scheduleReservationQueryPreferencesPersist();
|
||||
}
|
||||
|
||||
@@ -446,6 +493,7 @@ class SettingController extends GetxController with WidgetsBindingObserver {
|
||||
allowManualMealPeriodChange:
|
||||
reservationQueryAllowManualMealPeriodChange.value,
|
||||
verificationDateScope: reservationQueryVerificationDateScope.value,
|
||||
shortcutKeyBindings: menuQueryShortcutKeyBindings,
|
||||
);
|
||||
} catch (e, stack) {
|
||||
logger.e('|$_tag|保存查询菜单页面配置失败error:|$e|stack:|$stack|');
|
||||
|
||||
@@ -8,6 +8,7 @@ export './setting_face_plugin.dart';
|
||||
export './setting_kiosk_exit_button.dart';
|
||||
export './setting_log_preview.dart';
|
||||
export './setting_meal_periods.dart';
|
||||
export './setting_menu_query_shortcut.dart';
|
||||
export './setting_pay_type_editor.dart';
|
||||
export './setting_page.dart';
|
||||
export './setting_printer_info.dart';
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
import '../controller.dart';
|
||||
import 'setting_components.dart';
|
||||
|
||||
/// 组件统一日志标签,用于过滤快捷按键设置的现场诊断日志。
|
||||
const String _tag = '快捷按键设置';
|
||||
|
||||
/// 等待物理按键的超时时间;需覆盖现场切换输入法英文模式的时间,超时自动退出监听。
|
||||
const Duration _shortcutListeningTimeout = Duration(seconds: 30);
|
||||
|
||||
/// 单独按下的修饰键不作为绑定目标,继续等待组合中的实际按键。
|
||||
///
|
||||
/// 修饰键不携带独立业务含义,允许其触发绑定会误占动作按键;
|
||||
/// 使用常量列表而非集合,避免 LogicalKeyboardKey 自定义相等性无法参与常量集合。
|
||||
const List<LogicalKeyboardKey> _shortcutIgnoredModifierKeys = [
|
||||
LogicalKeyboardKey.shift,
|
||||
LogicalKeyboardKey.shiftLeft,
|
||||
LogicalKeyboardKey.shiftRight,
|
||||
LogicalKeyboardKey.control,
|
||||
LogicalKeyboardKey.controlLeft,
|
||||
LogicalKeyboardKey.controlRight,
|
||||
LogicalKeyboardKey.alt,
|
||||
LogicalKeyboardKey.altLeft,
|
||||
LogicalKeyboardKey.altRight,
|
||||
LogicalKeyboardKey.altGraph,
|
||||
LogicalKeyboardKey.meta,
|
||||
LogicalKeyboardKey.metaLeft,
|
||||
LogicalKeyboardKey.metaRight,
|
||||
];
|
||||
|
||||
/// 把输入法提交的文本反查为字母逻辑按键,只接受 26 个英文字母(不区分大小写)。
|
||||
///
|
||||
/// Flutter 的字母逻辑键使用小写 Unicode 码点作为稳定键 ID(a-z 对应 0x61-0x7A),
|
||||
/// 大小写输入统一映射到同一个按键;中文候选、标点、多字符提交无法确认到唯一
|
||||
/// 按键,返回空由调用方忽略,不关心操作员当前使用什么输入法。
|
||||
LogicalKeyboardKey? _logicalKeyFromImeText(String text) {
|
||||
final normalized = text.trim().toLowerCase();
|
||||
if (normalized.length != 1) return null;
|
||||
final codeUnit = normalized.codeUnitAt(0);
|
||||
if (codeUnit < 0x61 || codeUnit > 0x7A) return null;
|
||||
return LogicalKeyboardKey.findKeyByKeyId(codeUnit);
|
||||
}
|
||||
|
||||
/// 查询菜单外接快捷小键盘的按键绑定设置组件。
|
||||
///
|
||||
/// 操作流程:点击动作行进入监听(行高亮并提示),再按下外接小键盘的物理按键
|
||||
/// 完成绑定;重复点击同一动作或按 Esc 取消监听。绑定冲突校验与持久化统一由
|
||||
/// [SettingController] 收口,本组件只负责监听状态与交互反馈。
|
||||
class SettingMenuQueryShortcutKeypad extends StatefulWidget {
|
||||
/// 创建快捷小键盘设置组件;控制器必须由设置页 Binding 注册。
|
||||
const SettingMenuQueryShortcutKeypad({super.key, required this.controller});
|
||||
|
||||
/// 设置模块共享控制器,提供绑定读写、冲突校验和持久化调度。
|
||||
final SettingController controller;
|
||||
|
||||
@override
|
||||
State<SettingMenuQueryShortcutKeypad> createState() =>
|
||||
_SettingMenuQueryShortcutKeypadState();
|
||||
}
|
||||
|
||||
class _SettingMenuQueryShortcutKeypadState
|
||||
extends State<SettingMenuQueryShortcutKeypad> {
|
||||
/// 当前正在等待物理按键的动作;空表示未在监听。
|
||||
MenuQueryShortcutAction? _listeningAction;
|
||||
|
||||
/// 监听超时计时器;进入监听时启动,收到按键、取消或销毁时清空。
|
||||
Timer? _listeningTimer;
|
||||
|
||||
/// 硬件键盘处理器是否已注册,保证 add/remove 严格配对。
|
||||
bool _handlerAttached = false;
|
||||
|
||||
/// 组件销毁时退出监听并释放硬件键盘处理器与计时器。
|
||||
@override
|
||||
void dispose() {
|
||||
_stopListening();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 开始等待指定动作的物理按键;再次点击同一动作视为取消监听。
|
||||
void _startListening(MenuQueryShortcutAction action) {
|
||||
if (_listeningAction == action) {
|
||||
_stopListening();
|
||||
return;
|
||||
}
|
||||
// 断开页面上残留的输入焦点,避免文本进入旧输入框;
|
||||
// 监听期间由隐形输入收集器接管焦点,承接被输入法合成为文本的按键输入。
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
setState(() => _listeningAction = action);
|
||||
// 硬件键盘处理器只在监听期间注册,避免常态拦截设置页的普通按键。
|
||||
if (!_handlerAttached) {
|
||||
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
|
||||
_handlerAttached = true;
|
||||
}
|
||||
_listeningTimer?.cancel();
|
||||
_listeningTimer = Timer(_shortcutListeningTimeout, () {
|
||||
if (!mounted) return;
|
||||
_stopListening();
|
||||
});
|
||||
}
|
||||
|
||||
/// 退出监听:取消计时器、移除键盘处理器并清除高亮状态。
|
||||
void _stopListening() {
|
||||
_listeningTimer?.cancel();
|
||||
_listeningTimer = null;
|
||||
if (_handlerAttached) {
|
||||
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
|
||||
_handlerAttached = false;
|
||||
}
|
||||
if (_listeningAction == null) return;
|
||||
if (mounted) {
|
||||
setState(() => _listeningAction = null);
|
||||
} else {
|
||||
_listeningAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 监听期间的硬件键盘事件处理。
|
||||
///
|
||||
/// 仅响应按下事件(忽略长按重复和抬起);Esc 取消本次绑定;修饰键忽略;
|
||||
/// 其余按键交给 Controller 校验并绑定。监听期间消费所有非修饰按键,
|
||||
/// 避免物理按键落入焦点链触发页面其他行为。
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
final action = _listeningAction;
|
||||
if (action == null) return false;
|
||||
if (event is! KeyDownEvent) return false;
|
||||
// 记录原始按键信息,用于区分“事件未到达”与“到达后处理失败”两类现场问题。
|
||||
logger.i(
|
||||
'|$_tag|收到按键logical:${event.logicalKey.debugName}|physical:${event.physicalKey.debugName}|usb:${event.physicalKey.usbHidUsage}|',
|
||||
);
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_stopListening();
|
||||
return true;
|
||||
}
|
||||
if (_shortcutIgnoredModifierKeys.contains(event.logicalKey)) return false;
|
||||
final binding = MenuQueryShortcutKeyBinding.fromKeyEvent(event);
|
||||
if (widget.controller.setMenuQueryShortcutKeyBinding(action, binding)) {
|
||||
Toast.success(msg: '「${binding.displayLabel}」已绑定为「${action.title}」');
|
||||
_stopListening();
|
||||
}
|
||||
// 冲突时保留监听,操作员可直接补按正确按键,无需重新点击动作行。
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 输入法文本兜底:把被合成为文本提交的字母按键转换为逻辑键绑定。
|
||||
///
|
||||
/// 模拟器以及输入法处于激活状态的真机上,物理字母键可能被输入法合成为
|
||||
/// 文本注入而不产生按键事件;此时由隐形收集器持有焦点接收文本并反查按键,
|
||||
/// 与按键事件通道互补,覆盖“字符型小键盘”的全部接入场景。
|
||||
void _handleImeText(String text) {
|
||||
final action = _listeningAction;
|
||||
if (action == null) return;
|
||||
final key = _logicalKeyFromImeText(text);
|
||||
// 无法确认到唯一字母按键的输入(中文候选、标点等)直接忽略,保持监听,
|
||||
// 不提示错误也不关心操作员的输入法状态。
|
||||
if (key == null) return;
|
||||
// 文本输入只有逻辑键身份,物理键位缺失时绑定仍可运行期兜底匹配。
|
||||
final binding = MenuQueryShortcutKeyBinding(logicalKeyId: key.keyId);
|
||||
logger.i('|$_tag|输入法兜底绑定action:${action.code}|key:${key.debugName}|');
|
||||
if (widget.controller.setMenuQueryShortcutKeyBinding(action, binding)) {
|
||||
Toast.success(msg: '「${binding.displayLabel}」已绑定为「${action.title}」');
|
||||
_stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/// 分组内依次渲染说明头、状态提示和五个动作行;动作行依赖绑定 Rx 独立重建。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingGroup(
|
||||
title: '快捷按钮',
|
||||
child: <Widget>[
|
||||
// 监听期间挂载隐形输入收集器,承接输入法合成的文本按键输入。
|
||||
if (_listeningAction != null)
|
||||
_ShortcutImeCollector(onCharacter: _handleImeText),
|
||||
_ShortcutBindingHeader(
|
||||
onReset: widget.controller.resetMenuQueryShortcutKeyBindings,
|
||||
),
|
||||
_buildListeningNotice(),
|
||||
Obx(() {
|
||||
final bindings = widget.controller.menuQueryShortcutKeyBindings;
|
||||
final actions = MenuQueryShortcutAction.values;
|
||||
return Column(
|
||||
children: [
|
||||
for (final (index, action) in actions.indexed)
|
||||
_ShortcutActionRow(
|
||||
action: action,
|
||||
binding: bindings[action],
|
||||
listening: _listeningAction == action,
|
||||
showDivider: index < actions.length - 1,
|
||||
onTap: () => _startListening(action),
|
||||
onClear: () => widget.controller
|
||||
.clearMenuQueryShortcutKeyBinding(action),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
].toColumn(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 监听状态下切换为警示提示,指导操作员完成“选中动作 -> 按键”流程。
|
||||
Widget _buildListeningNotice() {
|
||||
final listeningAction = _listeningAction;
|
||||
final listening = listeningAction != null;
|
||||
return SettingNotice(
|
||||
text: listening
|
||||
? '正在等待「${listeningAction.title}」的按键:请按下外接小键盘上要绑定的字母按键;再次点击本行取消。'
|
||||
: '点击动作行的“绑定按键”后,再按下外接小键盘的物理按键完成绑定;同一按键只能绑定一个动作。',
|
||||
icon: listening ? FAssets.icons.keyboard : FAssets.icons.circleAlert,
|
||||
color: listening ? XfjDesignTokens.warning : XfjDesignTokens.muted,
|
||||
background: listening ? const Color(0xFFFFF8E1) : XfjDesignTokens.control,
|
||||
borderColor:
|
||||
listening ? XfjDesignTokens.warning.withValues(alpha: 0.35) : null,
|
||||
).padding(left: 20.w, top: 14.h, right: 20.w, bottom: 4.h);
|
||||
}
|
||||
}
|
||||
|
||||
/// 快捷按钮分组的说明头,右侧“还原”负责清空全部按键绑定。
|
||||
class _ShortcutBindingHeader extends StatelessWidget {
|
||||
/// 创建说明头;[onReset] 由 Controller 提供并已包含持久化调度。
|
||||
const _ShortcutBindingHeader({required this.onReset});
|
||||
|
||||
/// 还原按钮回调:清空全部快捷按键绑定。
|
||||
final VoidCallback onReset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = FTheme.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'小键盘按键绑定',
|
||||
style: theme.typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'外接快捷小键盘的五个物理按键可分别绑定为取消、核销、人脸识别、饭卡识别和重新打印。',
|
||||
style:
|
||||
theme.typography.sm.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FButton(
|
||||
style: FButtonStyle.outline,
|
||||
prefix: FIcon(
|
||||
FAssets.icons.rotateCcw,
|
||||
color: XfjDesignTokens.ink,
|
||||
size: 18.sp,
|
||||
),
|
||||
label: Text(
|
||||
'还原',
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
onPress: onReset,
|
||||
),
|
||||
],
|
||||
).padding(left: 20.w, top: 16.h, right: 20.w);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个快捷动作行:展示动作、已绑定的按键名,并提供绑定与清除入口。
|
||||
class _ShortcutActionRow extends StatelessWidget {
|
||||
/// 创建动作行;[binding] 为当前绑定,空表示未绑定。
|
||||
const _ShortcutActionRow({
|
||||
required this.action,
|
||||
required this.binding,
|
||||
required this.listening,
|
||||
required this.showDivider,
|
||||
required this.onTap,
|
||||
required this.onClear,
|
||||
});
|
||||
|
||||
/// 当前行展示的快捷动作。
|
||||
final MenuQueryShortcutAction action;
|
||||
|
||||
/// 当前动作已绑定的按键;空表示未绑定。
|
||||
final MenuQueryShortcutKeyBinding? binding;
|
||||
|
||||
/// 是否正在等待该动作的物理按键。
|
||||
final bool listening;
|
||||
|
||||
/// 是否绘制底部分隔线;监听高亮时由高亮边框替代分隔线。
|
||||
final bool showDivider;
|
||||
|
||||
/// 点击整行的回调:进入或取消监听。
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// 清除该动作绑定的回调。
|
||||
final VoidCallback onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = FTheme.of(context);
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: listening,
|
||||
label: '绑定${action.title}快捷按键',
|
||||
child: Material(
|
||||
color: listening ? XfjDesignTokens.primarySoft : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: 68.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
|
||||
decoration: BoxDecoration(
|
||||
border: listening
|
||||
? Border.all(
|
||||
color: XfjDesignTokens.primary.withValues(alpha: 0.35),
|
||||
)
|
||||
: showDivider
|
||||
? const Border(
|
||||
bottom: BorderSide(color: XfjDesignTokens.line),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
// 监听高亮时底色换成白色,保持图标容器在浅蓝背景上的辨识度。
|
||||
color: listening
|
||||
? XfjDesignTokens.surface
|
||||
: XfjDesignTokens.control,
|
||||
borderRadius: BorderRadius.circular(
|
||||
XfjDesignTokens.controlRadius,
|
||||
),
|
||||
),
|
||||
child: FIcon(
|
||||
_shortcutActionIcon(action),
|
||||
color: XfjDesignTokens.muted,
|
||||
size: 22.sp,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16.w),
|
||||
Expanded(
|
||||
child: Text(
|
||||
action.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildKeyBadge(theme),
|
||||
if (binding != null && !listening) ...[
|
||||
SizedBox(width: 8.w),
|
||||
_ClearBindingButton(
|
||||
semanticLabel: '清除${action.title}按键绑定',
|
||||
onTap: onClear,
|
||||
),
|
||||
],
|
||||
SizedBox(width: 8.w),
|
||||
_buildBindChip(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 已绑定动作展示按键名徽标,未绑定展示占位文案,保持各行右侧基线对齐。
|
||||
Widget _buildKeyBadge(FThemeData theme) {
|
||||
final currentBinding = binding;
|
||||
if (currentBinding == null) {
|
||||
return SizedBox(
|
||||
width: 96.w,
|
||||
child: Text(
|
||||
'未绑定',
|
||||
textAlign: TextAlign.right,
|
||||
style: theme.typography.base.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: 96.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: XfjDesignTokens.primarySoft,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
),
|
||||
child: Text(
|
||||
currentBinding.displayLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 右侧操作提示:监听中为等待态警示样式,其余为可点击的绑定入口样式。
|
||||
Widget _buildBindChip(FThemeData theme) {
|
||||
if (listening) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14.w * XfjDesignTokens.buttonSizeScale,
|
||||
vertical: 8.h * XfjDesignTokens.buttonSizeScale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF8E1),
|
||||
border: Border.all(
|
||||
color: XfjDesignTokens.warning.withValues(alpha: 0.45),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FIcon(
|
||||
FAssets.icons.keyboard,
|
||||
color: XfjDesignTokens.warning,
|
||||
size: 18.sp,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Text(
|
||||
'等待按键…',
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.warning,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 14.w * XfjDesignTokens.buttonSizeScale,
|
||||
vertical: 8.h * XfjDesignTokens.buttonSizeScale,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: XfjDesignTokens.surface,
|
||||
border: Border.all(color: XfjDesignTokens.line),
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
),
|
||||
child: Text(
|
||||
binding == null ? '绑定按键' : '重新绑定',
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除单个动作按键绑定的图标按钮,尺寸与触控面积对齐设置页既有操作按钮。
|
||||
class _ClearBindingButton extends StatelessWidget {
|
||||
/// 创建清除按钮;[onTap] 为空时禁用。
|
||||
const _ClearBindingButton({
|
||||
required this.semanticLabel,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
/// 无障碍语义标签,包含动作名以便读屏区分各行按钮。
|
||||
final String semanticLabel;
|
||||
|
||||
/// 点击回调;由父行提供,已包含 Controller 清除与持久化。
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: semanticLabel,
|
||||
child: Material(
|
||||
color: XfjDesignTokens.control,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
child: SizedBox(
|
||||
width: 48.w,
|
||||
height: 48.h,
|
||||
child: FIcon(
|
||||
FAssets.icons.x,
|
||||
color: XfjDesignTokens.muted,
|
||||
size: 18.sp,
|
||||
).center(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 监听期间承载输入法焦点的隐形收集器。
|
||||
///
|
||||
/// 模拟器与输入法激活的真机上,字符按键会被合成为文本提交而不产生按键事件,
|
||||
/// 只有持有焦点的编辑组件能收到这类输入;收集器挂载后立即抢占焦点,
|
||||
/// 把提交文本原样回调给设置组件做按键反查,自身不可见也不保留内容。
|
||||
class _ShortcutImeCollector extends StatefulWidget {
|
||||
/// 创建收集器;[onCharacter] 收到输入法每次提交的原始文本。
|
||||
const _ShortcutImeCollector({required this.onCharacter});
|
||||
|
||||
/// 输入法提交文本时的回调,参数为本次提交的完整字符串。
|
||||
final ValueChanged<String> onCharacter;
|
||||
|
||||
@override
|
||||
State<_ShortcutImeCollector> createState() => _ShortcutImeCollectorState();
|
||||
}
|
||||
|
||||
class _ShortcutImeCollectorState extends State<_ShortcutImeCollector> {
|
||||
/// 收集器文本控制器,每次提交后立即清空,避免多键累积。
|
||||
final TextEditingController _textContent = TextEditingController();
|
||||
|
||||
/// 收集器焦点节点,随组件销毁释放。
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 挂载后下一帧抢占输入焦点,让输入法把合成的按键文本提交到这里。
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textContent.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 1,
|
||||
height: 1,
|
||||
child: ExcludeSemantics(
|
||||
child: Opacity(
|
||||
opacity: 0,
|
||||
child: TextField(
|
||||
controller: _textContent,
|
||||
focusNode: _focusNode,
|
||||
showCursor: false,
|
||||
autofocus: true,
|
||||
// 使用该功能时现场必有外接键盘;none 输入类型抑制软键盘弹出
|
||||
//(与商品搜索框承接扫码枪输入的做法一致),输入连接仍然保留,
|
||||
// 被输入法合成为文本的按键依旧能提交到收集器。
|
||||
keyboardType: TextInputType.none,
|
||||
textInputAction: TextInputAction.none,
|
||||
style: const TextStyle(fontSize: 1),
|
||||
decoration: const InputDecoration(
|
||||
isDense: true,
|
||||
counterText: '',
|
||||
),
|
||||
onChanged: (text) {
|
||||
if (text.isEmpty) return;
|
||||
// 立即清空保证每个按键触发一次回调,长按重复字符也不会误绑定后续字符。
|
||||
_textContent.clear();
|
||||
widget.onCharacter(text);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 动作到图标的映射,与查询菜单页面各操作的语义保持一致。
|
||||
SvgAsset _shortcutActionIcon(MenuQueryShortcutAction action) =>
|
||||
switch (action) {
|
||||
MenuQueryShortcutAction.cancel => FAssets.icons.ban,
|
||||
MenuQueryShortcutAction.verification => FAssets.icons.circleCheck,
|
||||
MenuQueryShortcutAction.faceRecognition => FAssets.icons.scanFace,
|
||||
MenuQueryShortcutAction.cardRecognition => FAssets.icons.idCard,
|
||||
MenuQueryShortcutAction.reprint => FAssets.icons.printer,
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
import '../controller.dart';
|
||||
import 'setting_components.dart';
|
||||
import 'setting_menu_query_shortcut.dart';
|
||||
import 'setting_pay_type_editor.dart';
|
||||
|
||||
/// 页面设置采用与“基本信息”一致的分组表面,并用二级菜单承载各业务页配置。
|
||||
@@ -175,7 +176,7 @@ class _ReservationQuerySettings extends StatelessWidget {
|
||||
|
||||
final SettingController controller;
|
||||
|
||||
/// 组合查询方式、自动识别、隐私、核销和餐段设置,并保持各区域独立响应式刷新。
|
||||
/// 组合查询方式、自动识别、隐私、核销、餐段和快捷小键盘设置,并保持各区域独立响应式刷新。
|
||||
@override
|
||||
Widget build(BuildContext context) => <Widget>[
|
||||
_queryMethodSettings(controller),
|
||||
@@ -183,8 +184,10 @@ class _ReservationQuerySettings extends StatelessWidget {
|
||||
_nameMaskSettings(controller),
|
||||
_verificationSettings(controller),
|
||||
_mealPeriodSettings(controller),
|
||||
SettingMenuQueryShortcutKeypad(controller: controller),
|
||||
SettingNotice(
|
||||
text: '查询顺序、默认项、自动识别、持续识别、姓名脱敏、核销、快速核销和餐段修改权限配置会在下次进入“查询菜单”时读取。',
|
||||
text:
|
||||
'查询顺序、默认项、自动识别、持续识别、姓名脱敏、核销、快速核销、餐段修改权限和快捷按键绑定配置会在下次进入“查询菜单”时读取。',
|
||||
icon: FAssets.icons.circleAlert,
|
||||
),
|
||||
].toColumnSpace(space: XfjDesignTokens.cardGap.h).scrollable();
|
||||
@@ -464,7 +467,8 @@ class _CashierSettings extends StatelessWidget {
|
||||
_cashierActionSettings(controller),
|
||||
_cashierMealPeriodSettings(controller),
|
||||
SettingNotice(
|
||||
text: '支付方式顺序、右下角快捷操作和餐段修改权限都会同步到本机收银页;隐藏仅影响界面展示,不改变服务端权限、钱包范围和渠道支付能力。',
|
||||
text:
|
||||
'支付方式顺序、右下角快捷操作和餐段修改权限都会同步到本机收银页;隐藏仅影响界面展示,不改变服务端权限、钱包范围和渠道支付能力。',
|
||||
icon: FAssets.icons.circleAlert,
|
||||
),
|
||||
].toColumnSpace(space: XfjDesignTokens.cardGap.h).scrollable();
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: pj_supermarket_cloud
|
||||
description: "资安信智慧后勤-测试"
|
||||
publish_to: "none"
|
||||
version: 1.4.8+148
|
||||
version: 1.5.0+150
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
|
||||
Reference in new issue
Block a user