diff --git a/README.md b/README.md index 4397170..c1bac2e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ flutter pub run build_runner build --delete-conflicting-outputs~ ## flutter打包 ``` -flutter build apk --build-name 1.5.6 --build-number 156 +flutter build apk --build-name 1.5.8 --build-number 158 --build-name为版本号(展示) --build-number为版本号(内部比较) ``` diff --git a/lib/common/models/plugs/serial_card_models.dart b/lib/common/models/plugs/serial_card_models.dart index 457d70f..759ba04 100644 --- a/lib/common/models/plugs/serial_card_models.dart +++ b/lib/common/models/plugs/serial_card_models.dart @@ -5,6 +5,9 @@ part 'serial_card_models.g.dart'; /// 服务端 mch_manage.card_key 的建库默认值;终端尚未同步到商户密码时按此兜底认证。 const String _serialCardDefaultKeyHex = '424324548800'; +/// 旧配置及首次安装的默认 M1 绝对块地址,对应扇区 1 的第二个数据块。 +const int _serialCardDefaultBusinessBlockAddress = 5; + /// DK25-ST 可以识别的卡片类型。 enum SerialCardType { undefined(0x00, '未定义', null), @@ -40,19 +43,23 @@ enum SerialMifareKeyType { /// 读写卡配置保存在已有 setting.sysJson 扩展键中,不改变数据库结构。 /// -/// 新规范(与后台开卡管理一致):业务卡号固定存扇区 1 的第二个数据块(绝对块 5), +/// 业务卡号默认存扇区 1 的第二个数据块(绝对块 5),可在本机设置中调整; /// 认证固定密码 B;密钥来自服务端 mch_manage.card_key,由设备信息同步链路写入 /// [SerialCardConfig.keyBHex] 缓存,设置页不再手工编辑。读不出业务块时回退返回 UID。 @JsonSerializable() class SerialCardConfig { + /// 尚未同步到商户密码时使用的认证兜底值。 static const String defaultKeyHex = _serialCardDefaultKeyHex; - /// 饭卡业务卡号所在的 M1 绝对块地址:扇区 1 的扇区内块 1(与 Web 端 area*4+1 对齐)。 - static const int mifareBusinessBlockAddress = 5; + /// 默认 M1 绝对块地址,保持与后台开卡默认位置一致;缺失或非法配置均回退到此值。 + static const int mifareBusinessBlockAddress = + _serialCardDefaultBusinessBlockAddress; + /// 创建本机读写卡配置,密码 B 来自服务端,数据位置与扫描入口由终端维护。 const SerialCardConfig({ this.keyBHex = _serialCardDefaultKeyHex, this.showMifareScanButton = false, + this.businessBlockAddress = _serialCardDefaultBusinessBlockAddress, }); /// 当前商户的饭卡密码 B(6 字节 hex);同步失败或旧配置非法时回退服务端默认值。 @@ -65,19 +72,39 @@ class SerialCardConfig { /// M1 全块扫描属于现场诊断能力,默认隐藏,避免普通业务读卡误触耗时扫描。 final bool showMifareScanButton; + /// 本机选定的 M1 绝对数据块号;使用新键以免重新启用旧版已忽略的 mifareBlockAddress。 + @JsonKey(fromJson: _serialCardBlockFromJson, toJson: _serialCardBlockFromJson) + final int businessBlockAddress; + + /// 当前数据块所在扇区;S70 的 32 至 39 扇区每区 16 块,不能统一按 4 块计算。 + int get businessSector => businessBlockAddress < 128 + ? businessBlockAddress ~/ 4 + : 32 + (businessBlockAddress - 128) ~/ 16; + + /// 校验可用于业务卡号的普通数据块,排除厂商块及小、大扇区的密钥控制块。 + static bool isValidBusinessBlockAddress(int address) => + address > 0 && + address <= 255 && + (address < 128 ? (address + 1) % 4 != 0 : (address - 143) % 16 != 0); + + /// 复制配置并保留未修改项,供本地位置编辑和商户密钥同步分别更新。 SerialCardConfig copyWith({ String? keyBHex, bool? showMifareScanButton, + int? businessBlockAddress, }) { return SerialCardConfig( keyBHex: keyBHex ?? this.keyBHex, showMifareScanButton: showMifareScanButton ?? this.showMifareScanButton, + businessBlockAddress: businessBlockAddress ?? this.businessBlockAddress, ); } + /// 从已有 sysJson 扩展配置恢复,缺少数据位置时沿用默认块 5。 factory SerialCardConfig.fromJson(Map json) => _$SerialCardConfigFromJson(json); + /// 生成本机缓存配置,密钥与数据块地址在存储边界统一清洗。 Map toJson() => _$SerialCardConfigToJson(this); } @@ -155,3 +182,9 @@ String _serialCardKeyFromJson(Object? value) { } String _serialCardKeyToJson(String value) => _serialCardKeyFromJson(value); + +/// 缺失、类型异常或指向保留块的地址均回退默认值,避免异常缓存误写密钥或厂商信息。 +int _serialCardBlockFromJson(Object? value) => + value is int && SerialCardConfig.isValidBusinessBlockAddress(value) + ? value + : SerialCardConfig.mifareBusinessBlockAddress; diff --git a/lib/common/models/plugs/serial_card_models.g.dart b/lib/common/models/plugs/serial_card_models.g.dart index f27fb4a..6db337f 100644 --- a/lib/common/models/plugs/serial_card_models.g.dart +++ b/lib/common/models/plugs/serial_card_models.g.dart @@ -12,10 +12,15 @@ SerialCardConfig _$SerialCardConfigFromJson(Map json) => ? _serialCardDefaultKeyHex : _serialCardKeyFromJson(json['keyBHex']), showMifareScanButton: json['showMifareScanButton'] as bool? ?? false, + businessBlockAddress: json['businessBlockAddress'] == null + ? _serialCardDefaultBusinessBlockAddress + : _serialCardBlockFromJson(json['businessBlockAddress']), ); Map _$SerialCardConfigToJson(SerialCardConfig instance) => { 'keyBHex': _serialCardKeyToJson(instance.keyBHex), 'showMifareScanButton': instance.showMifareScanButton, + 'businessBlockAddress': + _serialCardBlockFromJson(instance.businessBlockAddress), }; diff --git a/lib/common/utils/app/app_setting_tool.dart b/lib/common/utils/app/app_setting_tool.dart index 07d9b91..db299e7 100644 --- a/lib/common/utils/app/app_setting_tool.dart +++ b/lib/common/utils/app/app_setting_tool.dart @@ -639,6 +639,20 @@ abstract class AppSettingTool { sysJson[_serialCardConfigKey] = config.toJson(); }); + /// 仅保存本机可编辑项;保留并发同步得到的最新密码 B,避免页面旧快照覆盖认证密钥。 + static Future setSerialCardLocalConfig(SerialCardConfig config) => + _patchSerialCardConfig(config.toJson()..remove('keyBHex')); + + /// 在已有设置写入队列中合并读写卡配置,位置编辑与密钥同步互不覆盖。 + static Future _patchSerialCardConfig(Map patch) => + _updateSysJson((sysJson) { + final source = sysJson[_serialCardConfigKey]; + sysJson[_serialCardConfigKey] = { + if (source is Map) ...Map.from(source), + ...patch, + }; + }); + /// 查询方式顺序和默认项属于页面交互配置,继续保存在 sysJson 中,不增加数据库字段。 static Future getReservationQueryPagePreferences() async { @@ -948,14 +962,11 @@ abstract class AppSettingTool { // 商户饭卡密码 B 单独保存在读写卡配置里;服务端未返回或值非法时不覆盖本机缓存, // 保证离线场景仍能用上一次同步成功的密钥。 final cardKey = data.cardKey?.trim(); - if (cardKey != null && - RegExp(r'^[0-9a-fA-F]{12}$').hasMatch(cardKey)) { + if (cardKey != null && RegExp(r'^[0-9a-fA-F]{12}$').hasMatch(cardKey)) { final cardConfig = await getSerialCardConfig(); final normalizedKey = cardKey.toUpperCase(); if (cardConfig.keyBHex != normalizedKey) { - await setSerialCardConfig( - cardConfig.copyWith(keyBHex: normalizedKey), - ); + await _patchSerialCardConfig({'keyBHex': normalizedKey}); logger.i('同步商户饭卡密码B成功'); } } diff --git a/lib/common/utils/manage/serial_card_manage.dart b/lib/common/utils/manage/serial_card_manage.dart index c6b7000..edc04ed 100644 --- a/lib/common/utils/manage/serial_card_manage.dart +++ b/lib/common/utils/manage/serial_card_manage.dart @@ -18,7 +18,7 @@ enum _SerialCardStep { /// DK25-ST 串口读写管理。 /// /// 串口帧可能被拆包或粘包,因此先按协议长度字段解帧,再根据卡类型执行 UID 或 M1 块操作。 -/// 新规范:M1 业务卡号固定读写扇区 1 的块 5,认证固定密码 B(mch_manage.card_key 同步缓存), +/// M1 业务卡号按本机配置读写(默认扇区 1 块 5),认证固定密码 B(mch_manage.card_key 同步缓存), /// 业务块读不出时回退返回 UID;密钥仅用于扇区访问认证,不适用于其他卡型。 class SerialCardManage extends SerialBase { /// 读卡器保持响应但在单轮等待期内没有读取到卡片时返回的稳定提示,用于业务层区分设备故障。 @@ -135,7 +135,7 @@ class SerialCardManage extends SerialBase { )); } - /// 读卡:M1 用密码 B 读扇区 1 业务块,读不出或其他卡型返回完整 UID。 + /// 读卡:M1 用密码 B 读取本机选定业务块,读不出或其他卡型返回完整 UID。 void sendRead() { unawaited(_startOperation(command: SerialCardCmdEnum.read)); } @@ -164,7 +164,7 @@ class SerialCardManage extends SerialBase { _config = await AppSettingTool.getSerialCardConfig(); // 只记录非敏感协议参数,严禁记录密钥内容。 logger.i(logMsg( - '加载读写卡配置block:|${SerialCardConfig.mifareBusinessBlockAddress}|keyType:|B|', + '加载读写卡配置block:|${_config.businessBlockAddress}|keyType:|B|', )); // 用户可能在读取本地设置期间关闭弹窗或重新发起操作,旧异步任务不得重新占用串口。 if (operationEpoch != _operationEpoch || @@ -278,8 +278,7 @@ class SerialCardManage extends SerialBase { case _SerialCardStep.readBlock: if (frame.command != SerialReadCardTool.mifareReadBlockCommand || frame.payload.length != 17 || - frame.payload.first != - SerialCardConfig.mifareBusinessBlockAddress) { + frame.payload.first != _config.businessBlockAddress) { return; } _finishMifareBlockRead(frame.payload.sublist(1)); @@ -388,7 +387,7 @@ class SerialCardManage extends SerialBase { return; } - // 读卡固定行为:M1 先读扇区 1 业务块,其余卡型直接读取 UID。 + // M1 先读本轮加载的业务块,操作中不切换位置;其余卡型直接读取 UID。 if (_cardType == SerialCardType.mifare) { _requestActiveMifareKey(); } else { @@ -439,7 +438,7 @@ class SerialCardManage extends SerialBase { ); } - /// 把服务端同步的商户饭卡密码作为 KEY B 写入模块,供扇区 1 业务块认证使用。 + /// 把服务端同步的商户饭卡密码作为 KEY B 写入模块,供当前业务块所在扇区认证使用。 void _requestActiveMifareKey() { final keyBytes = SerialTool.stringToBytes(_config.keyBHex); _setRequest( @@ -463,16 +462,18 @@ class SerialCardManage extends SerialBase { ); } + /// 请求本轮配置指定的业务块,响应也必须匹配同一地址。 void _requestMifareBlockRead() { _setRequest( step: _SerialCardStep.readBlock, request: SerialReadCardTool.readMifareBlockRequest( - SerialCardConfig.mifareBusinessBlockAddress, + _config.businessBlockAddress, ), - label: '读取 M1 块 ${SerialCardConfig.mifareBusinessBlockAddress}', + label: '读取 M1 块 ${_config.businessBlockAddress}', ); } + /// 仅向本轮配置指定的业务数据块写入,继续使用既有单块文本编码。 void _requestMifareBlockWrite() { final blockData = SerialReadCardTool.encodeMifareBlockData( _pendingWriteCardNumber ?? '', @@ -485,10 +486,10 @@ class SerialCardManage extends SerialBase { _setRequest( step: _SerialCardStep.writeBlock, request: SerialReadCardTool.writeMifareBlockRequest( - blockAddress: SerialCardConfig.mifareBusinessBlockAddress, + blockAddress: _config.businessBlockAddress, blockData: blockData, ), - label: '写入 M1 块 ${SerialCardConfig.mifareBusinessBlockAddress}', + label: '写入 M1 块 ${_config.businessBlockAddress}', ); } diff --git a/lib/common/utils/manage/tool/serial_read_card_tool.dart b/lib/common/utils/manage/tool/serial_read_card_tool.dart index f473738..c2e6a0b 100644 --- a/lib/common/utils/manage/tool/serial_read_card_tool.dart +++ b/lib/common/utils/manage/tool/serial_read_card_tool.dart @@ -155,9 +155,7 @@ abstract class SerialReadCardTool { /// 第 0 块保存 UID/厂商信息,扇区尾块保存密钥;业务卡号只能使用其余数据块。 static bool isMifareBusinessDataBlock(int blockAddress) { - return blockAddress > 0 && - blockAddress <= 255 && - !isMifareTrailerBlock(blockAddress); + return SerialCardConfig.isValidBusinessBlockAddress(blockAddress); } /// 返回 S70 最大容量范围内全部 215 个安全数据块;扫描诊断按该列表完整发起读块请求。 diff --git a/lib/pages/terminal/setting/view.dart b/lib/pages/terminal/setting/view.dart index bb6824e..2114c34 100644 --- a/lib/pages/terminal/setting/view.dart +++ b/lib/pages/terminal/setting/view.dart @@ -331,7 +331,7 @@ String _menuDescription(SettingMenuType type) { SettingMenuType.printerInfo => '查看实际 USB 参数,手动上报并输出测试小票', SettingMenuType.subScreenRoutes => '选择顾客屏演示状态并在当前应用窗口中全屏检查', SettingMenuType.versionSetting => '查看可用终端版本、更新内容和下载状态', - SettingMenuType.cardReaderSetting => '同步商户饭卡密码 B,查看固定读取规范和读卡器诊断', + SettingMenuType.cardReaderSetting => '设置饭卡数据位置,同步商户密码 B,进行读卡器诊断', }; } diff --git a/lib/pages/terminal/setting/widgets/setting_card_location_dialog.dart b/lib/pages/terminal/setting/widgets/setting_card_location_dialog.dart new file mode 100644 index 0000000..2c044d3 --- /dev/null +++ b/lib/pages/terminal/setting/widgets/setting_card_location_dialog.dart @@ -0,0 +1,144 @@ +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:pj_supermarket_cloud/common/index.dart'; + +/// 编辑 M1 绝对数据块号,确认后返回安全地址;取消不改变已保存配置。 +Future showCardLocationDialog( + BuildContext context, { + required int currentBlockAddress, +}) => + showAppDialog( + context: context, + builder: (_) => _CardLocationDialog(blockAddress: currentBlockAddress), + ); + +/// 数据位置编辑弹窗;扇区由绝对块号计算,避免两项独立输入产生矛盾。 +class _CardLocationDialog extends StatefulWidget { + /// 使用当前已保存块号初始化草稿。 + const _CardLocationDialog({required this.blockAddress}); + + /// 本次打开弹窗时的绝对块地址。 + final int blockAddress; + + /// 创建持有输入控制器及表单校验的局部状态。 + @override + State<_CardLocationDialog> createState() => _CardLocationDialogState(); +} + +/// 仅维护地址草稿,持久化由读写卡设置页负责。 +class _CardLocationDialogState extends State<_CardLocationDialog> { + /// 提交时执行必填、范围和保留块校验的表单键。 + final _formKey = GlobalKey(); + + /// 弹窗生命周期内的块号输入控制器,退场结束后统一释放。 + late final TextEditingController _controller; + + /// 路由退场期间屏蔽重复关闭,避免快速连续提交弹出下层设置页。 + bool _closing = false; + + /// 创建当前地址草稿,并监听触屏键盘与实体键盘输入以刷新扇区预览。 + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: '${widget.blockAddress}') + ..addListener(_refreshPreview); + } + + /// 释放弹窗独占的控制器和监听,避免关闭动画期间访问已销毁控制器。 + @override + void dispose() { + _controller.removeListener(_refreshPreview); + _controller.dispose(); + super.dispose(); + } + + /// 所有输入渠道共用控制器监听,直接赋值的软键盘和恢复默认也能刷新预览。 + void _refreshPreview() => setState(() {}); + + /// 校验通过后返回草稿,设置页保存成功才作为本机数据位置使用。 + void _submit() { + if (_closing) return; + if (_formKey.currentState?.validate() != true) return; + _close(int.parse(_controller.text)); + } + + /// 统一取消和确认出口,只允许当前弹窗关闭一次。 + void _close([int? address]) { + if (_closing) return; + _closing = true; + FocusManager.instance.primaryFocus?.unfocus(); + Navigator.of(context).pop(address); + } + + /// 构建统一表单、实时扇区预览和终端数字键盘。 + @override + Widget build(BuildContext context) { + final theme = FTheme.of(context); + final dialogWidth = MediaQuery.sizeOf(context).width * 0.55; + final address = int.tryParse(_controller.text); + final valid = address != null && + SerialReadCardTool.isMifareBusinessDataBlock(address); + final preview = valid + ? '扇区 ${SerialCardConfig(businessBlockAddress: address).businessSector} · 块 $address' + : '请输入有效的数据块号'; + return AppDialog( + title: Text('编辑数据位置', style: theme.typography.xl2), + width: dialogWidth, + maxWidth: dialogWidth, + semanticLabel: '饭卡数据位置编辑弹框', + onClose: _close, + content: Form( + key: _formKey, + child: [ + FTextField( + controller: _controller, + label: const Text('数据块(绝对块号)'), + description: + const Text('输入 1–255 的数据块号,扇区自动计算;不允许厂商块和密钥块。请与实际发卡位置保持一致。'), + keyboardType: TextInputType.none, + maxLines: 1, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(3) + ], + validator: (value) { + final block = int.tryParse(value ?? ''); + if (block == null || block < 1 || block > 255) { + return '请输入 1–255 的数据块号'; + } + return SerialReadCardTool.isMifareBusinessDataBlock(block) + ? null + : '该块是密钥控制块,请选择普通数据块'; + }, + onSubmit: (_) => _submit(), + ), + Text(preview, style: theme.typography.base), + AppSoftKeyboardPanel( + controller: _controller, + mode: AppSoftKeyboardMode.digits, + allowPattern: RegExp('[0-9]'), + maxLength: 3, + semanticLabel: '饭卡数据块号数字键盘', + ), + ].toColumnSpace(space: 16.h, mainAxisSize: MainAxisSize.min), + ), + actions: [ + FButton( + style: FButtonStyle.outline, + label: const Text('恢复默认'), + onPress: () => _controller.text = + '${SerialCardConfig.mifareBusinessBlockAddress}', + ), + FButton( + style: FButtonStyle.outline, + label: const Text('取消'), + onPress: _close, + ), + FButton(label: const Text('保存'), onPress: _submit), + ], + ); + } +} diff --git a/lib/pages/terminal/setting/widgets/setting_card_reader.dart b/lib/pages/terminal/setting/widgets/setting_card_reader.dart index 6432787..cc74275 100644 --- a/lib/pages/terminal/setting/widgets/setting_card_reader.dart +++ b/lib/pages/terminal/setting/widgets/setting_card_reader.dart @@ -8,9 +8,10 @@ import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; import 'setting_components.dart'; +import 'setting_card_location_dialog.dart'; -/// 读/写卡设置页:新规范下扇区、业务块和密钥类型固定(扇区 1 块 5、密码 B), -/// 密钥来自服务端 mch_manage.card_key 同步缓存,本页只维护同步入口和诊断工具。 +/// 读/写卡设置页:本机可编辑业务数据位置,默认扇区 1 块 5;认证固定使用密码 B。 +/// 密钥来自服务端 mch_manage.card_key 同步缓存,本页同时维护同步入口和诊断工具。 class SettingCardReader extends StatefulWidget { const SettingCardReader({super.key}); @@ -25,7 +26,10 @@ class _SettingCardReaderState extends State { bool _showKeyB = false; bool _testOperationActive = false; - /// 开关配置是否正在写入本地 setting.sysJson,用于合并连续点击产生的保存任务。 + /// 数据位置正在保存时禁止再次编辑或启动诊断,失败时回退原位置。 + bool _savingLocation = false; + + /// 本机配置是否正在写入 setting.sysJson,用于合并连续点击产生的保存任务。 bool _configPersisting = false; /// 当前持久化期间是否又发生了配置变化;为 true 时完成本轮后继续保存最新界面状态。 @@ -50,13 +54,14 @@ class _SettingCardReaderState extends State { super.dispose(); } + /// 先恢复本机完整配置,再异步同步商户密钥,防止初始加载覆盖已恢复的位置。 Future _load() async { try { - // 打开页面时顺带从服务端刷新一次设备信息,尽快拿到最新商户饭卡密码。 - unawaited(_syncCardKeyFromService(showSuccess: false)); final config = await AppSettingTool.getSerialCardConfig(); if (!mounted) return; setState(() => _config = config); + // 打开页面时顺带从服务端刷新一次设备信息,尽快拿到最新商户饭卡密码。 + unawaited(_syncCardKeyFromService(showSuccess: false)); } catch (e, stack) { logger.e('加载读写卡设置失败error:|$e|stack:|$stack|'); Toast.error(msg: '读写卡设置加载失败'); @@ -79,7 +84,8 @@ class _SettingCardReaderState extends State { } final config = await AppSettingTool.getSerialCardConfig(); if (!mounted) return; - setState(() => _config = config); + // 同步只刷新密钥,保留用户在等待网络期间编辑的位置与扫描开关。 + setState(() => _config = _config.copyWith(keyBHex: config.keyBHex)); if (showSuccess) { Toast.success( msg: config.keyBHex == previousKey ? '饭卡密码已是最新' : '饭卡密码已同步', @@ -99,6 +105,38 @@ class _SettingCardReaderState extends State { return '${key.substring(0, 2)}********${key.substring(key.length - 2)}'; } + /// 编辑绝对数据块号,扇区随块号计算;保存失败时恢复原位置,避免界面与实际读写不一致。 + Future _editDataLocation() async { + if (_testOperationActive || _savingLocation || _configPersisting) return; + final address = await showCardLocationDialog( + context, + currentBlockAddress: _config.businessBlockAddress, + ); + if (!mounted || + address == null || + address == _config.businessBlockAddress) { + return; + } + final previousAddress = _config.businessBlockAddress; + setState(() { + _savingLocation = true; + _config = _config.copyWith(businessBlockAddress: address); + }); + try { + await AppSettingTool.setSerialCardLocalConfig(_config); + if (mounted) Toast.success(msg: '数据位置已保存'); + } catch (e, stack) { + logger.e('保存饭卡数据位置失败error:|$e|stack:|$stack|'); + if (!mounted) return; + setState(() => _config = _config.copyWith( + businessBlockAddress: previousAddress, + )); + Toast.error(msg: '数据位置保存失败,请重试'); + } finally { + if (mounted) setState(() => _savingLocation = false); + } + } + /// 更新扫描入口显隐并立即持久化,保证再次进入设置页时保持当前开关状态。 void _setMifareScanButtonVisible(bool value) { if (_config.showMifareScanButton == value) return; @@ -112,27 +150,29 @@ class _SettingCardReaderState extends State { if (!_configPersisting) unawaited(_flushConfigPersist()); } - /// 串行保存开关配置;密码 B 由服务端同步链路单独写入,不经本页手工编辑。 + /// 串行保存本机配置;密码 B 由服务端同步链路单独写入,不经本页手工编辑。 Future _flushConfigPersist() async { _configPersisting = true; + if (mounted) setState(() {}); while (_configPersistPending) { _configPersistPending = false; final config = _config; try { - await AppSettingTool.setSerialCardConfig(config); + await AppSettingTool.setSerialCardLocalConfig(config); } catch (e, stack) { logger.e('自动保存读写卡设置失败error:|$e|stack:|$stack|'); Toast.error(msg: '读写卡设置自动保存失败,请稍后重试'); } } _configPersisting = false; + if (mounted) setState(() {}); } /// 设置页只验证本机读卡结果,不查询用户或触发开卡业务。 /// /// 串口管理器沿用 AppControl 的应用级实例,避免创建第二个实例争用同一端口。 Future _openReadCardTest() async { - if (_testOperationActive) return; + if (_testOperationActive || _savingLocation || _configPersisting) return; setState(() => _testOperationActive = true); _testReadLoading.value = true; _testDetail.value = null; @@ -285,8 +325,17 @@ class _SettingCardReaderState extends State { SettingInfoRow( icon: FAssets.icons.nfc, label: '数据位置', - subtitle: '与后台开卡管理一致,固定读写扇区 1 的第二个数据块。', - value: '扇区 1 · 块 5', + subtitle: '按本机配置读写 M1 业务数据块,默认扇区 1 块 5。', + value: + '扇区 ${_config.businessSector} · 块 ${_config.businessBlockAddress}', + trailing: SettingActionButton( + label: _savingLocation ? '保存中...' : '编辑', + icon: FAssets.icons.pencil, + enabled: !_testOperationActive && + !_savingLocation && + !_configPersisting, + onTap: _editDataLocation, + ), ), SettingInfoRow( icon: FAssets.icons.lock, @@ -314,7 +363,8 @@ class _SettingCardReaderState extends State { icon: FAssets.icons.keyRound, label: '当前密钥', subtitle: '来源为后台商户资料的饭卡密码,随设备信息同步自动更新,离线沿用本机缓存。', - value: _showKeyB ? _config.keyBHex : _maskKey(_config.keyBHex), + value: + _showKeyB ? _config.keyBHex : _maskKey(_config.keyBHex), trailing: InkWell( onTap: () => setState(() => _showKeyB = !_showKeyB), child: FIcon( @@ -352,18 +402,21 @@ class _SettingCardReaderState extends State { trailing: FSwitch( semanticLabel: '在读卡弹框显示 M1 数据块扫描', value: _config.showMifareScanButton, - enabled: !_testOperationActive, + enabled: !_testOperationActive && !_savingLocation, onChange: _setMifareScanButtonVisible, ), ), SettingInfoRow( icon: FAssets.icons.nfc, label: '读卡器测试', - subtitle: '按当前规范读取饭卡:M1 读扇区 1 业务块,读不出时返回 8 位 UID;上方开关开启时可同时执行 M1 扫描。', + subtitle: + 'M1 按上方数据位置读取业务块,读不出时返回 8 位 UID;上方开关开启时可同时执行 M1 扫描。', trailing: SettingActionButton( label: _testOperationActive ? '测试中...' : '打开测试', icon: FAssets.icons.nfc, - enabled: !_testOperationActive, + enabled: !_testOperationActive && + !_savingLocation && + !_configPersisting, onTap: _openReadCardTest, ), showDivider: false, diff --git a/pubspec.yaml b/pubspec.yaml index dcb56af..50391d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pj_supermarket_cloud description: "资安信智慧后勤-测试" publish_to: "none" -version: 1.5.6+156 +version: 1.5.8+158 environment: sdk: ^3.6.0