diff --git a/.zcode/plans/plan-sess_ba68eecf-563f-4382-b242-dd01d49ac538.md b/.zcode/plans/plan-sess_ba68eecf-563f-4382-b242-dd01d49ac538.md index 2db4758..2bb8545 100644 --- a/.zcode/plans/plan-sess_ba68eecf-563f-4382-b242-dd01d49ac538.md +++ b/.zcode/plans/plan-sess_ba68eecf-563f-4382-b242-dd01d49ac538.md @@ -1,43 +1,62 @@ -# 查询菜单键盘模式:设置开关 + 运行时接线 + 冲突矩阵 + 解耦 +# 自研可拖动软键盘组件 + 全面清理系统软键盘适配代码 -## 一、设置页:「该页面是否开启键盘模式」开关(默认否) +## 一、新组件:`AppSoftKeyboard`(lib/common/widgets/app/app_soft_keyboard.dart) -- `ReservationQueryPagePreferences` 新增 `keyboardModeEnabled`(默认 false,旧终端兼容)。 -- `AppSettingTool`:sysJson 新键 `reservationQueryKeyboardModeEnabled`(缺失=false),随查询菜单偏好一起读写。 -- `SettingController`:Rx 字段 + `setReservationQueryKeyboardModeEnabled` + 载入/还原/串行持久化(复用现有模式)。 -- 设置 UI:「快捷按钮」分组顶部加开关行(SettingInfoRow+FSwitch):"开启键盘模式",副文案"开启后进入查询菜单,按已绑定的物理按键将语音播报并执行对应操作"。 +**一个可拖动的键盘弹出框,三个面板(数字/字母/符号),显式绑定 TextEditingController。** -## 二、解耦结构(三个新文件 + controller 最小改动) +### 弹层形态 +- `showGeneralDialog`(`useRootNavigator: true`、`barrierColor: 透明`、`barrierDismissible: true`):点击键盘外任意处收起;键盘浮在页面内容之上,页面可见;生命周期由路由托管,零手工 OverlayEntry 管理。 +- 默认停靠屏幕底部居中,整体可用 `GestureDetector(onPan...)` + `Transform.translate` 自由拖动(实现借鉴 app_dialog.dart:224-274 的基准矩形+安全区钳制方案),拖动手柄在面板顶部。 +- 面板宽度约 760.w,按键高 62.h(与现 _KeyButton 一致),样式沿用 XfjDesignTokens(surface 底/line 描边/controlRadius)。 -键盘输入与动作编排全部移出主 controller,controller 只做接线(项目已有 `OrderMixin` 的 mixin-on-controller 先例): +### 三面板 +- **数字**:1-9、0、退格、清空、确认(3x4)。 +- **字母**:QWERTY 三行 + 第四行(⇧大小写、123 切数字、空格、退格、确认)。 +- **符号**:常用符号网格(`- . _ / * , : % # + @ & ? ! ( ) 空格` 等)+ 退格、确认。 +- 面板切换:顶部手柄行内三个 tab(123 / ABC / #+=)。 -1. **`menu_query/reservation_shortcut_keyboard.dart`(新)— 输入协调器** `ReservationShortcutKeyboard`: - 纯类(无 GetX 依赖、可单测)。职责:持有模式开关与绑定快照、挂/卸 `HardwareKeyboard` 处理器、KeyDownEvent+修饰键过滤、`conflictsWith` 匹配动作、命中后回调 `onAction(action)` 并 TTS「你已点击X」;提供 `handleImeText()` 供输入法文本兜底统一进入同一匹配。TTS/提示经构造注入(复用 controller 的 `_speaker`/`_errorNotifier`)。 -2. **`menu_query/reservation_shortcut_actions.dart`(新)— 动作 mixin** `ReservationMenuQueryShortcutActions on ReservationMenuQueryController`: - 实现五个动作的编排(冲突校验矩阵、防重、补打流程),调用 controller 现有公开方法,主 controller 零业务膨胀。 -3. **`common/widgets/app/app_shortcut_ime_collector.dart`(新)— 公共隐形输入收集器**: - 把设置页的 `_ShortcutImeCollector` 与字母反查函数公共化,设置页与查询菜单页共用(模拟器/输入法合成文本两条路径全覆盖)。 +### API +```dart +static Future show(BuildContext context, { + required TextEditingController controller, // 直接操作光标位置插入/退格 + AppSoftKeyboardMode mode = ...digits, // digits | letters | symbols 初始面板 + RegExp? allowPattern, // 场景字符白名单(替代 inputFormatters 语义) + int? maxLength, +}) +``` +- 插入:`controller.value.replaced(selection, ch)`(光标处插入,插完校验 allowPattern/maxLength,超限忽略); +- 退格/清空/确认同理操作 controller;确认=关闭弹层。 -controller 改动仅:`with` 动作 mixin、`_loadPagePreferences` 补读 `keyboardModeEnabled`+`shortcutKeyBindings`、偏好加载后启动键盘协调器(`onClose` 停止)、新增公开 getter `resultDialogPresented`、`_publishQuickVerificationRequest` 改公开 `requestQuickVerification()`(快速核销与快捷键共用同一入口)、核销成功时记录最近核销订单 ID(供补打)。view.dart 挂条件隐形收集器。 +## 二、接入点改造(第一期:终端高频场景) -## 三、动作映射与冲突矩阵(核心细节) +1. **`TerminalToolbarTextField`**(terminal_toolbar.dart,公共工具栏输入框)加参数 `softKeyboard`(AppSoftKeyboardMode?)与 `softKeyboardAllowPattern`:启用时 `keyboardType: TextInputType.none` + `onTap` 弹出自研键盘。物理键盘/扫码枪输入不受影响(none 只禁系统软键盘)。 +2. **收银搜索框**(cashier_workspace.dart):删除 `softKeyboardEnabled`/`searchSoftKeyboardEnabled` 参数链,食堂与超市统一传 `softKeyboard: letters`(超市实体键盘照常,触摸用自研键盘)。 +3. **开卡页卡号搜索**(card_active/view.dart):传 `softKeyboard: digits`。 +4. **终端操作验证密码弹框**(setting_password_dialog.dart):删除内嵌 AppPinKeyboard,改为密码框 `keyboardType: none` + 打开弹框时主动 `AppSoftKeyboard.show(mode: digits, allowPattern: 数字)`(覆盖 autofocus 场景),点输入框也可再次唤起。 +5. **绑定唯一编号弹框**(setting_baseInfo.dart):同样接 `digits`。 -| 按键 | 动作 | 执行 | 拒绝条件(TTS+Toast 提示) | -|---|---|---|---| -| E | 取消 | `clearForPrivacy()`(现成,幂等) | 弹框展示期:「请先完成当前弹窗操作」 | -| D | 核销 | `requestQuickVerification()`(复用快速核销事件→同一核销弹框,自带"今天+选中餐段+可核销订单"校验) | 弹框期;`recognizing/querying`:「正在识别/查询,请稍后」;提交中 | -| A | 人脸识别 | 未识别时=启动/切换人脸识别(`selectMethod(face)`,自动模式自带启动;已是该方式且空闲则直接 `startRecognition()`,等同"开始识别"按钮) | `recognizing` 且当前方式为饭卡/人脸:「正在进行{当前方式}识别,不能切换为{目标}识别,请先按取消」;`querying`:「正在查询,请稍后」;弹框期 | -| B | 饭卡识别 | 对称同上 | 对称同上 | -| C | 重新打印 | 记录本页最近一次核销订单 → `HardApi.getPrinterRecords(orderId)` 找最新可补打记录 → `showVoucherReprintDialog` 选原因(复用核销记录页弹框)→ `HardApi.reprintVoucher` → `SyncPrintTool.pullPendingTasks()`;无记录时提示「本页暂无可补打的核销小票」 | 弹框期;`recognizing/querying`;补打进行中(防重标志) | +第二期(本次不做):设置页其余搜索/读卡器/Cron/硬件输入弹框/登录页/设备绑定页,以及中文拼音首字母搜索。 -通用规则:所有命中先 TTS「你已点击X」;拒绝时 TTS+Toast 说明原因并保持原状态;`_resultDialogPresented`(弹框占用期)一律拒绝;补打原因弹框打开期间同样进入占用态防重入。 +## 三、清理系统软键盘适配代码(全部删除) -## 四、行为说明 +1. `lib/common/utils/app/app_ime_tool.dart` 整文件 + main.dart 安装点 + utils/index.dart 导出; +2. MainActivity.kt 的 `forceShowSoftInput` 分支(89-113 行)与 `InputMethodManager` import(通道本身保留,AppRuntimeTool 在用); +3. `lib/common/widgets/app/app_shortcut_ime_collector.dart` 整文件 + 设置页绑定区的挂载与 `_handleImeText` 兜底回调(快捷键绑定的 KeyEvent 通道完整保留,真机小键盘/adb 注入仍可绑定); +4. `lib/common/widgets/app/app_pin_keyboard.dart` 整文件(被 AppSoftKeyboard 取代)+ setting_password_dialog 内嵌段 + widgets/index.dart 两处导出更新; +5. 4 处 `SystemChannels.textInput.invokeMethod('TextInput.hide')`(快捷键协调器、查询菜单 controller 的 releaseResidualTextInput、verification_records、order_history)——**各处 `unfocus()` 全部保留**(焦点卫生,防物理输入落入旧输入框),只删 hide 与"等 100ms 键盘退场"类代码,相关注释同步改写。 -- 默认关闭;开关与绑定均为进页面时一次性快照(与现有偏好读取时机一致,改设置需重新进入查询菜单生效——与现有自动识别等配置口径相同)。 -- 与扫码枪监听(BarcodeScanManage)共存:快捷键仅在键盘模式开启且命中已绑定字母时消费事件;查询菜单页无扫码业务,扫码缓冲不受影响。 -- 模拟器验证:页面同样挂隐形收集器(`keyboardType.none` 不弹软键盘),软键盘/注入文本可触发;真机小键盘走按键事件通道。 +## 四、保留不动(说明理由) + +- AndroidManifest `adjustResize`、登录页/AppDialog 的 viewInsets 消费(第二期接键盘后统一清); +- `_hasActiveTextInput` 快捷键让路、扫码枪白名单、各页面 unfocus 焦点管理; +- setting_face/setting_product 搜索框现状(第二期场景)。 ## 五、验证 -`dart format` + `flutter analyze`(0 新增错误)+ 跑 `test/pages/terminal/menu_query_controller_test.dart`(现有 38 例全过);键盘协调器为纯类,动作矩阵逻辑随 mixin 附带单测可后续补充。变更预计 9 个文件、新增约 700 行。 \ No newline at end of file +`dart format` + `flutter analyze`(0 新增错误)+ 38 个 menu_query 测试全过 + 构建 debug APK 装真机: +- 终端操作验证弹框打开 → 自研数字键盘自动弹出 → 触摸输入密码可提交; +- 食堂收银搜索框点击 → 自研键盘(字母默认)弹出 → 可拖动 → 输入+确认触发搜索 → 点击外部收起; +- 查询菜单快捷键 A/B/E 回归不受影响; +- 超市机型语义:实体键盘/扫码枪输入正常(不装真机则靠代码审查+测试保证)。 + +变更预计:新增 1 个组件文件(~400 行),修改约 12 个文件,删除约 350 行老代码。 \ No newline at end of file diff --git a/.zcode/plans/plan-sess_f3c88552-161b-4f92-8a30-63de4604cac3.md b/.zcode/plans/plan-sess_f3c88552-161b-4f92-8a30-63de4604cac3.md new file mode 100644 index 0000000..321ee65 --- /dev/null +++ b/.zcode/plans/plan-sess_f3c88552-161b-4f92-8a30-63de4604cac3.md @@ -0,0 +1,58 @@ +# 打印机「指令打印」模式改造计划(后端拼命令 + 终端写字节) + +## 方案总览 + +现状:后端 `ThermalReceiptImage.java` 把发票/兑换券渲染成 PNG 图片 → 任务只带 `pic` URL 下发(MQTT PRINT 消息 / `/hard/hard-api/ad/printer/tasks`)→ 终端 `PrintManage.printImg` 下载图片、转 ESC/POS 栅格指令、写 USB。图片栅格打印慢。 + +目标:后端在生成图片的同时,用平行的 ESC/POS 命令拼装器产出**命令字符串(Base64 字节流)**,随任务经新字段 `printCmd` 下发;终端在设置「打印机」页新增**打印方式**切换(图片打印=默认 / 指令打印),指令模式下把命令解码后直接写 USB(前后仍按本地 `printerAutoCut` 补走纸/切刀,与图片模式边界一致)。无命令或命令损坏的任务自动回退图片打印,保证兼容与可用性。 + +--- + +## 一、协议契约(前后端共同约定) + +- 新字段 `printCmd`:String、可空、**Base64(ESC/POS 字节流)**,随 MQTT PRINT 消息体和 `/printer/tasks` 接口下发。 +- 命令内容边界:凭证正文(标题、分隔线、字段行、商品明细、合计、打印时间),**不含**末尾走纸与切刀——由终端统一包装 `feed(1) + 命令 + feed(1) + [autoCut ? cut()]`,保持与现有图片模式相同的边界行为,且尊重终端本地的 `printerAutoCut` 配置。 +- 中文编码 GBK,行宽按 80mm/48 列(与终端 `maxPerLine` 一致)。 +- 向后兼容:字段可空可选;旧终端(含基线 `biz_supermarket_cloud`,json_serializable 忽略未知字段)不受影响;旧后端/旧任务无命令时终端回退图片打印。 + +## 二、后端改动(biz-api-cloud-zax / pjcloud-hardware) + +1. **新增 `EscPosReceiptCommand`**(`pjcloud-hardware-api/.../hard/api/utils/`,与 `ThermalReceiptImage` 平行的流式 builder,同款 API:`title/banner/keyValue/sectionTitle/voucherOrderSummary/invoiceItem/voucherProduct/voucherTotal/divider`): + - 内部拼 ESC/POS:`ESC @` 初始化、`ESC a` 对齐、`ESC E` 加粗、`GS !` 放大、GBK 中文编码、按 48 列排版(名称/数量/单价/小计左右布局,长名称折行或截断,避免依赖宽度易错 API)。 + - 输出 `toBase64()`;**不含**走纸/切刀。 +2. **`HardInvoiceServiceImpl`**(1404 行):发票(printType=1)与兑换券(printType=2)生成处,用同一份数据流双输出(图片 `pic` + 命令 `printCmd`),版式对齐两张参考图(智慧食堂发票 / 预约核销兑换券)。 +3. **`HardInvoiceEntity` 加 `print_cmd` 列**(可空,默认 NULL)支撑 tasks 补拉重下发;配套后端数据库迁移(可空列,兼容旧数据)。 +4. **MQTT PRINT 消息体与 `/printer/tasks` 响应**加 `printCmd` 字段。 +5. **单测**:新增 `EscPosReceiptCommandTest`(对照 `ThermalReceiptImageTest` 的用例结构),验证发票/兑换券两种凭证的命令拼装。 + +## 三、终端改动(biz_xfj_cloud,当前工作区) + +1. **`SysSetting` 新字段 `printerOutputMode`**(String,默认 `'IMAGE'`,取值 `'IMAGE'`/`'COMMAND'`,`lib/common/models/database/database_models.dart`)——写入 sysJson JSON 内新键,**不改表结构**;`updatePrinterBinding()` 现有逻辑只逐字段赋值打印机配置,不会覆盖此本地开关。 +2. **`AppSettingTool` 加本地 setter**(如 `setPrinterOutputMode`,走既有 `_updateSysJson` 串行落库模式,参考 `setCashierPagePreferences` 写法)。 +3. **设置页「打印机」**(`lib/pages/terminal/setting/widgets/setting_printer_info.dart`):在「后台打印配置」组附近新增「打印方式」分段选择器(复用 `SettingSegmentedSelector`,选项:图片打印/指令打印),切换即时生效并持久化;附一行说明文案(指令模式需后台支持,无命令的任务自动回退图片打印)。UI 对齐现有分组、间距、风格。 +4. **`MqttInvoiceMsg` 加 `printCmd` 字段**(`lib/common/models/mq/mq_models.dart`,可空 String)→ 跑 build_runner 更新 `mq_models.g.dart`;`SyncPrintTool.pullPendingTasks` 与 `app_mq_tool.dart` 入队处透传。 +5. **`print_history` 表加列 `printCmd`**(TEXT 可空): + - `print_history_entity.dart` 加字段 + 注释; + - `database_migration.dart` 新增连续 `Migration(13, 14, ...)`(`ALTER TABLE print_history ADD COLUMN printCmd TEXT`)并按版本顺序加入 `getDatabaseMigrations()`; + - `database.dart` `@Database(version: 14)`; + - **同步** `android/.../facediscern/database/DatabaseHelper.java` `DATABASE_VERSION = 14`(Floor 先迁移、虹软后访问的既有顺序不变)。 +6. **`PrintManage` 新增 `printCommand(String base64Cmd)`**:Base64 解码(空/损坏 → `lastError` + 返回 false)→ `feed(1) + 命令字节 + feed(1) + [printerAutoCut ? cut()]` → `writeBytes`;设备状态上报(`updatePrinterWorking` 等)与 `printImg` 完全一致。 +7. **`SyncPrintTool._print` 打印方式分支**: + - 图片模式:照旧 `printImg(task.invoiceUrl)`(忽略命令)。 + - 指令模式:有命令 → `printCommand`;**USB 写入失败只按既有 0/5/30s 重试命令,不回退图片**(避免一次任务两种内容重复出纸);仅在**无命令或 Base64 解码失败**时回退 `printImg`。 +8. **测试打印适配**:指令模式下 `printTestPage` 改为终端本地拼简单中文文本指令(`EscGenerator.text(containsChinese: true)` + `hr` + `feed` + `cut`,注意行宽 48 列内),用于验证指令链路;图片模式照旧画图。 +9. 既有重打(`reprintVoucher` 重试版本)、结果上报(`reportPrinterResult`)、幂等逻辑**均不改动**——重打任务经服务端重新下发自然携带命令。 + +## 四、验证 + +- 终端:`fvm dart run build_runner build --delete-conflicting-outputs`(受影响模型);`dart format` 本次改动文件;`fvm flutter analyze` 区分新增/既有告警。 +- 后端:pjcloud-hardware 模块 `mvn` 编译 + 单测(含新 EscPosReceiptCommandTest)。 +- 数据库:迁移需在真机验证「13 升 14」与「全新安装」两条路径,`user_version` 与虹软版本一致。 +- 真机联调(需实际收银机):指令模式打印发票与兑换券(对照参考图核对版式/中文/合计)、无命令回退图片、设置切换即时生效、指令模式测试打印、切刀行为。 +- 执行期间遵守通用规则:会话步骤条 + 变更统计(新增/删除/总变动),首次写入后保持 Diff 预览。 + +## 五、边界与不做的事 + +- 不改 `printImg` 现有行为、不改上报/幂等协议、不改 `printerImageMode`(GS_V0/ESC_STAR 仍只作用于图片模式)。 +- 基线项目 `biz_supermarket_cloud` 本次不引入(协议向后兼容,旧终端忽略新字段);如需同步另开任务。 +- 指令模式版式为「按图片版式近似还原」而非像素级一致,字重/放大效果以后端拼装为准,真机验收时微调。 \ No newline at end of file diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java index b485b57..77d3b44 100644 --- a/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java +++ b/android/app/src/main/java/io/flutter/plugins/facediscern/database/DatabaseHelper.java @@ -16,7 +16,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String DATABASE_PATH = "pj_cashier/database"; private static final String DATABASE_NAME = "cash_register.db"; /** 与 Flutter Floor 的 @Database 版本保持一致,避免原生层提前推进 user_version。 */ - private static final int DATABASE_VERSION = 13; + private static final int DATABASE_VERSION = 14; public DatabaseHelper(Context context) { super(context, getCustomDatabasePath(context), null, DATABASE_VERSION); diff --git a/lib/common/api/hard_api.dart b/lib/common/api/hard_api.dart index e1b1257..43df1d8 100644 --- a/lib/common/api/hard_api.dart +++ b/lib/common/api/hard_api.dart @@ -196,11 +196,11 @@ abstract class HardApi { required TerminalOrderRecordQueryRequest request, }) async { final res = await HttpManage().dio.post( - '/hard/hard-api/ad/order-record/page', - queryParameters: {'current': current, 'size': size}, - data: request.toJson(), - options: dio.Options(headers: {'DEVICE-ID': '${request.hardId}'}), - ); + '/hard/hard-api/ad/order-record/page', + queryParameters: {'current': current, 'size': size}, + data: request.toJson(), + options: dio.Options(headers: {'DEVICE-ID': '${request.hardId}'}), + ); if (res.data == null) return null; return ServiceTerminalOrderRecordPage.fromJson(res.data); } diff --git a/lib/common/models/api/hard_api_models.dart b/lib/common/models/api/hard_api_models.dart index a15d5e5..3144d23 100644 --- a/lib/common/models/api/hard_api_models.dart +++ b/lib/common/models/api/hard_api_models.dart @@ -702,8 +702,7 @@ class TerminalOrderRecordQueryRequest { /// 用餐时段编码,留空或 -1 不区分。 final String? hourType; - factory TerminalOrderRecordQueryRequest.fromJson( - Map json) => + factory TerminalOrderRecordQueryRequest.fromJson(Map json) => _$TerminalOrderRecordQueryRequestFromJson(json); Map toJson() => @@ -772,8 +771,7 @@ class ServiceTerminalOrderRecordPage extends ServicePageBase { final List? records; - factory ServiceTerminalOrderRecordPage.fromJson( - Map json) => + factory ServiceTerminalOrderRecordPage.fromJson(Map json) => _$ServiceTerminalOrderRecordPageFromJson(json); @override diff --git a/lib/common/models/database/database_models.dart b/lib/common/models/database/database_models.dart index 575a38f..594dca7 100644 --- a/lib/common/models/database/database_models.dart +++ b/lib/common/models/database/database_models.dart @@ -46,6 +46,12 @@ class SysSetting extends Equatable { /// 当前绑定打印机的 ESC/POS 图片指令模式,默认使用 GS_V0。 String printerImageMode; + /// 打印输出方式本机开关:IMAGE=下载后端图片并转 ESC/POS 栅格指令(默认), + /// COMMAND=直接写入后端随任务下发的指令流(Base64),打印更快。 + /// 属于本机偏好,不由后台打印机绑定配置下发或覆盖;旧配置缺失时保持 IMAGE。 + @JsonKey(defaultValue: 'IMAGE') + String printerOutputMode; + /// 商户部门id String? merchantDeptId; @@ -165,6 +171,7 @@ class SysSetting extends Equatable { this.printerPaperWidth = 80, this.printerAutoCut = true, this.printerImageMode = 'GS_V0', + this.printerOutputMode = 'IMAGE', this.tenantList, this.manualHardwareInput = false, this.rootAccessStatus = RootAccessStatus.unknown, @@ -195,6 +202,7 @@ class SysSetting extends Equatable { printerPaperWidth, printerAutoCut, printerImageMode, + printerOutputMode, merchantDeptId, hardGroupId, mqSocketAddress, diff --git a/lib/common/models/database/database_models.g.dart b/lib/common/models/database/database_models.g.dart index 76cf86b..a632bd8 100644 --- a/lib/common/models/database/database_models.g.dart +++ b/lib/common/models/database/database_models.g.dart @@ -18,6 +18,7 @@ SysSetting _$SysSettingFromJson(Map json) => SysSetting( printerPaperWidth: (json['printerPaperWidth'] as num?)?.toInt() ?? 80, printerAutoCut: json['printerAutoCut'] as bool? ?? true, printerImageMode: json['printerImageMode'] as String? ?? 'GS_V0', + printerOutputMode: json['printerOutputMode'] as String? ?? 'IMAGE', tenantList: (json['tenantList'] as List?) ?.map((e) => ServiceTenant.fromJson(e as Map)) .toList(), @@ -103,6 +104,7 @@ Map _$SysSettingToJson(SysSetting instance) => 'printerPaperWidth': instance.printerPaperWidth, 'printerAutoCut': instance.printerAutoCut, 'printerImageMode': instance.printerImageMode, + 'printerOutputMode': instance.printerOutputMode, 'merchantDeptId': instance.merchantDeptId, 'hardGroupId': instance.hardGroupId, 'mqSocketAddress': instance.mqSocketAddress, diff --git a/lib/common/models/mq/mq_models.dart b/lib/common/models/mq/mq_models.dart index ce28e64..8aec808 100644 --- a/lib/common/models/mq/mq_models.dart +++ b/lib/common/models/mq/mq_models.dart @@ -57,9 +57,13 @@ class MqttInvoiceMsg { /// 同一服务端任务的人工重试版本,用于终端幂等去重。 final int? retryVersion; - // 图片地址url + /// 图片地址url final String? pic; + /// 后端拼装的凭证打印指令流(Base64 编码的 ESC/POS 字节),指令打印模式直写 USB。 + /// 可空:旧后端或历史任务无指令时终端回退 [pic] 图片打印。 + final String? printCmd; + // 是否设置为全局人脸 final bool? isGlobalFace; @@ -75,6 +79,7 @@ class MqttInvoiceMsg { this.printNo, this.retryVersion, this.pic, + this.printCmd, this.isGlobalFace, }); diff --git a/lib/common/models/mq/mq_models.g.dart b/lib/common/models/mq/mq_models.g.dart index 84cdbfc..0d4d2e1 100644 --- a/lib/common/models/mq/mq_models.g.dart +++ b/lib/common/models/mq/mq_models.g.dart @@ -40,6 +40,7 @@ MqttInvoiceMsg _$MqttInvoiceMsgFromJson(Map json) => printNo: (json['printNo'] as num?)?.toInt(), retryVersion: (json['retryVersion'] as num?)?.toInt(), pic: json['pic'] as String?, + printCmd: json['printCmd'] as String?, isGlobalFace: json['isGlobalFace'] as bool?, ); @@ -56,6 +57,7 @@ Map _$MqttInvoiceMsgToJson(MqttInvoiceMsg instance) => 'printNo': instance.printNo, 'retryVersion': instance.retryVersion, 'pic': instance.pic, + 'printCmd': instance.printCmd, 'isGlobalFace': instance.isGlobalFace, }; diff --git a/lib/common/services/database/database.dart b/lib/common/services/database/database.dart index 6c4d6fe..f3fd0ec 100644 --- a/lib/common/services/database/database.dart +++ b/lib/common/services/database/database.dart @@ -48,7 +48,7 @@ const typeConverters = [ ]; /// 收银终端共享 SQLite 数据库入口,统一暴露各业务 DAO 和带迁移的建库方法。 -@Database(version: 13, entities: entities) +@Database(version: 14, entities: entities) @TypeConverters(typeConverters) abstract class AppDatabase extends FloorDatabase { /// 取餐广播队列 DAO。 diff --git a/lib/common/services/database/database.g.dart b/lib/common/services/database/database.g.dart index 1d53a59..e28beaa 100644 --- a/lib/common/services/database/database.g.dart +++ b/lib/common/services/database/database.g.dart @@ -112,7 +112,7 @@ class _$AppDatabase extends AppDatabase { Callback? callback, ]) async { final databaseOptions = sqflite.OpenDatabaseOptions( - version: 13, + version: 14, onConfigure: (database) async { await database.execute('PRAGMA foreign_keys = ON'); await callback?.onConfigure?.call(database); @@ -148,7 +148,7 @@ class _$AppDatabase extends AppDatabase { await database.execute( 'CREATE TABLE IF NOT EXISTS `classify` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `serviceUniqueCode` TEXT NOT NULL, `sysType` TEXT NOT NULL, `name` TEXT NOT NULL, `categoryType` TEXT NOT NULL, `sort` INTEGER NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, `delFlag` INTEGER NOT NULL)'); await database.execute( - 'CREATE TABLE IF NOT EXISTS `print_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `orderId` TEXT NOT NULL, `serviceOrderId` TEXT NOT NULL, `servicePrintId` TEXT NOT NULL, `invoiceUrl` TEXT NOT NULL, `printType` TEXT NOT NULL, `printNo` INTEGER NOT NULL, `retryVersion` INTEGER NOT NULL, `attemptCount` INTEGER NOT NULL, `lastError` TEXT, `resultPending` INTEGER NOT NULL, `status` TEXT NOT NULL, `printTime` TEXT, `syncTime` TEXT, `createTime` TEXT NOT NULL)'); + 'CREATE TABLE IF NOT EXISTS `print_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `orderId` TEXT NOT NULL, `serviceOrderId` TEXT NOT NULL, `servicePrintId` TEXT NOT NULL, `invoiceUrl` TEXT NOT NULL, `printCmd` TEXT, `printType` TEXT NOT NULL, `printNo` INTEGER NOT NULL, `retryVersion` INTEGER NOT NULL, `attemptCount` INTEGER NOT NULL, `lastError` TEXT, `resultPending` INTEGER NOT NULL, `status` TEXT NOT NULL, `printTime` TEXT, `syncTime` TEXT, `createTime` TEXT NOT NULL)'); await database.execute( 'CREATE TABLE IF NOT EXISTS `user_record` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `userId` TEXT NOT NULL, `nickname` TEXT NOT NULL, `mobile` TEXT, `avatar` TEXT, `walletCardNumber` TEXT, `walletId` TEXT, `walletTypeId` TEXT, `walletBalance` INTEGER, `canteenId` TEXT, `discountsList` TEXT, `discountsUpdateTime` TEXT, `createTime` TEXT, `updateTime` TEXT)'); await database.execute( @@ -1582,6 +1582,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { 'serviceOrderId': item.serviceOrderId, 'servicePrintId': item.servicePrintId, 'invoiceUrl': item.invoiceUrl, + 'printCmd': item.printCmd, 'printType': item.printType, 'printNo': item.printNo, 'retryVersion': item.retryVersion, @@ -1603,6 +1604,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { 'serviceOrderId': item.serviceOrderId, 'servicePrintId': item.servicePrintId, 'invoiceUrl': item.invoiceUrl, + 'printCmd': item.printCmd, 'printType': item.printType, 'printNo': item.printNo, 'retryVersion': item.retryVersion, @@ -1624,6 +1626,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { 'serviceOrderId': item.serviceOrderId, 'servicePrintId': item.servicePrintId, 'invoiceUrl': item.invoiceUrl, + 'printCmd': item.printCmd, 'printType': item.printType, 'printNo': item.printNo, 'retryVersion': item.retryVersion, @@ -1658,7 +1661,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { Future> getAll(String status) async { return _queryAdapter.queryList( 'SELECT * FROM print_history WHERE (?1 = \'-1\' OR status = ?1)', - mapper: (Map row) => PrintHistoryEntity(id: row['id'] as int?, orderId: row['orderId'] as String, serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, printType: row['printType'] as String, printNo: row['printNo'] as int, retryVersion: row['retryVersion'] as int, attemptCount: row['attemptCount'] as int, lastError: row['lastError'] as String?, resultPending: _boolConverter.decode(row['resultPending'] as int), status: _printStatusConverter.decode(row['status'] as String), printTime: row['printTime'] as String?, syncTime: _dateTimeEmptyConverter.decode(row['syncTime'] as String), createTime: _dateTimeConverter.decode(row['createTime'] as String)), + mapper: (Map row) => PrintHistoryEntity(id: row['id'] as int?, orderId: row['orderId'] as String, serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, printCmd: row['printCmd'] as String?, printType: row['printType'] as String, printNo: row['printNo'] as int, retryVersion: row['retryVersion'] as int, attemptCount: row['attemptCount'] as int, lastError: row['lastError'] as String?, resultPending: _boolConverter.decode(row['resultPending'] as int), status: _printStatusConverter.decode(row['status'] as String), printTime: row['printTime'] as String?, syncTime: _dateTimeEmptyConverter.decode(row['syncTime'] as String), createTime: _dateTimeConverter.decode(row['createTime'] as String)), arguments: [status]); } @@ -1673,6 +1676,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, + printCmd: row['printCmd'] as String?, printType: row['printType'] as String, printNo: row['printNo'] as int, retryVersion: row['retryVersion'] as int, @@ -1696,6 +1700,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, + printCmd: row['printCmd'] as String?, printType: row['printType'] as String, printNo: row['printNo'] as int, retryVersion: row['retryVersion'] as int, @@ -1733,6 +1738,7 @@ class _$PrintHistoryDao extends PrintHistoryDao { serviceOrderId: row['serviceOrderId'] as String, servicePrintId: row['servicePrintId'] as String, invoiceUrl: row['invoiceUrl'] as String, + printCmd: row['printCmd'] as String?, printType: row['printType'] as String, printNo: row['printNo'] as int, retryVersion: row['retryVersion'] as int, diff --git a/lib/common/services/database/database_migration.dart b/lib/common/services/database/database_migration.dart index f02eb87..60608f0 100644 --- a/lib/common/services/database/database_migration.dart +++ b/lib/common/services/database/database_migration.dart @@ -410,6 +410,14 @@ abstract class DatabaseMigration { ); }); + /// 版本14为打印任务补充后端指令流列,指令打印模式可离线重试且不改变出纸内容。 + /// + /// 列可空且不回填:旧任务与旧后端无指令,指令模式下回退 invoiceUrl 图片打印。 + static Migration migration13To14 = Migration(13, 14, (database) async { + await database + .execute('ALTER TABLE print_history ADD COLUMN printCmd TEXT'); + }); + /// 按起始版本升序返回全部迁移,Floor 打开旧数据库时据此连续升级到当前版本。 static List getDatabaseMigrations() { return [ @@ -425,6 +433,7 @@ abstract class DatabaseMigration { migration10To11, migration11To12, migration12To13, + migration13To14, ]; } } diff --git a/lib/common/services/database/entity/print_history_entity.dart b/lib/common/services/database/entity/print_history_entity.dart index 6a46e4b..f4bd1f7 100644 --- a/lib/common/services/database/entity/print_history_entity.dart +++ b/lib/common/services/database/entity/print_history_entity.dart @@ -29,6 +29,10 @@ class PrintHistoryEntity { /// 发票图片地址 String invoiceUrl; + /// 后端拼装的凭证打印指令流(Base64 的 ESC/POS 字节)。 + /// 旧后端或历史任务无指令时为空,指令打印模式下回退 [invoiceUrl] 图片打印。 + String? printCmd; + /// 服务端凭证类型、打印序号与重试版本。 String printType; int printNo; @@ -59,6 +63,7 @@ class PrintHistoryEntity { required this.serviceOrderId, required this.servicePrintId, required this.invoiceUrl, + this.printCmd, this.printType = '1', this.printNo = 1, this.retryVersion = 0, diff --git a/lib/common/services/database/entity/print_history_entity.g.dart b/lib/common/services/database/entity/print_history_entity.g.dart index 70b5777..fff412f 100644 --- a/lib/common/services/database/entity/print_history_entity.g.dart +++ b/lib/common/services/database/entity/print_history_entity.g.dart @@ -13,6 +13,7 @@ PrintHistoryEntity _$PrintHistoryEntityFromJson(Map json) => serviceOrderId: json['serviceOrderId'] as String, servicePrintId: json['servicePrintId'] as String, invoiceUrl: json['invoiceUrl'] as String, + printCmd: json['printCmd'] as String?, printType: json['printType'] as String? ?? '1', printNo: (json['printNo'] as num?)?.toInt() ?? 1, retryVersion: (json['retryVersion'] as num?)?.toInt() ?? 0, @@ -34,6 +35,7 @@ Map _$PrintHistoryEntityToJson(PrintHistoryEntity instance) => 'serviceOrderId': instance.serviceOrderId, 'servicePrintId': instance.servicePrintId, 'invoiceUrl': instance.invoiceUrl, + 'printCmd': instance.printCmd, 'printType': instance.printType, 'printNo': instance.printNo, 'retryVersion': instance.retryVersion, diff --git a/lib/common/utils/app/app_mq_tool.dart b/lib/common/utils/app/app_mq_tool.dart index 859ccf7..697a5e3 100644 --- a/lib/common/utils/app/app_mq_tool.dart +++ b/lib/common/utils/app/app_mq_tool.dart @@ -147,6 +147,7 @@ class AppMqTool { androidOrderId: msg.androidOrderCode, historyId: msg.historyId!, pic: msg.pic!, + printCmd: msg.printCmd, printType: msg.printType ?? '1', printNo: msg.printNo ?? 1, retryVersion: msg.retryVersion ?? 0, diff --git a/lib/common/utils/app/app_setting_tool.dart b/lib/common/utils/app/app_setting_tool.dart index 5dcfae6..30815af 100644 --- a/lib/common/utils/app/app_setting_tool.dart +++ b/lib/common/utils/app/app_setting_tool.dart @@ -91,6 +91,15 @@ abstract class AppSettingTool { static const String _productMealPeriodsKey = "productMealPeriods"; static const String _serialCardConfigKey = "serialCardConfig"; + /// 打印输出方式在 sysJson 中的键,与 [SysSetting.printerOutputMode] 字段名一致。 + static const String _printerOutputModeKey = "printerOutputMode"; + + /// 打印输出方式取值:下载后端图片转 ESC/POS 栅格指令(默认,兼容所有后端版本)。 + static const String printerOutputModeImage = 'IMAGE'; + + /// 打印输出方式取值:直接写入后端随打印任务下发的 Base64 指令流,出纸更快。 + static const String printerOutputModeCommand = 'COMMAND'; + /// v8 之前短暂保存定时任务配置的旧 sysJson 键,仅供一次性迁移读取。 static const String _scheduledTaskConfigsKey = "scheduledTaskConfigs"; @@ -947,6 +956,15 @@ abstract class AppSettingTool { return setting; } + /// 切换本机打印输出方式:IMAGE=图片栅格打印,COMMAND=指令流直写。 + /// + /// 仅写 sysJson 局部键,不触碰后台下发的打印机绑定字段;指令模式下 + /// 无指令数据的旧任务会自动回退图片打印,因此可随时来回切换。 + static Future setPrinterOutputMode(String mode) => + _updateSysJson((sysJson) { + sysJson[_printerOutputModeKey] = mode; + }); + /// 绑定设备信息 static Future bindSys(int serviceId) async { var androidId = await AppSysTool.getAndroidId(); diff --git a/lib/common/utils/manage/print_manage.dart b/lib/common/utils/manage/print_manage.dart index 48e3b05..f76830f 100644 --- a/lib/common/utils/manage/print_manage.dart +++ b/lib/common/utils/manage/print_manage.dart @@ -178,6 +178,47 @@ class PrintManage extends BaseFacilityManage { } } + /// 将后端拼装的 ESC/POS 指令流写入 USB;前后补走纸,并按本机配置追加切刀。 + /// + /// 指令内容由服务端按凭证版式拼装,本方法只负责包装边界与写入; + /// 返回 true 只表示指令、走纸和可选切刀命令均被 USB 接口接受,不代表从硬件层确认出纸。 + Future printCommand(List commandBytes) async { + if (ObjectUtil.isEmpty(manager)) { + AppDeviceStatusTool.updatePrinterWorking(false); + return false; + } + try { + if (commandBytes.isEmpty) { + lastError = '打印指令内容为空'; + return false; + } + final setting = await AppSettingTool.getSysSetting(); + final generator = EscGenerator( + paperSizeWidthMM, + maxPerLine, + profile!, + ); + final bytes = [ + ...generator.feed(1), + ...commandBytes, + ...generator.feed(1), + if (setting.printerAutoCut) ...generator.cut(), + ]; + AppDeviceStatusTool.updatePrinterWorking(true); + await manager!.writeBytes(bytes); + lastError = null; + AppDeviceStatusTool.updatePrinter(true); + return true; + } catch (e) { + lastError = e.toString(); + AppDeviceStatusTool.updatePrinter(false); + logger.e("|$_tag|指令打印失败error|$e|"); + return false; + } finally { + AppDeviceStatusTool.updatePrinterWorking(false); + } + } + /// 将测试文字和灰度校准块渲染成 576px PNG,避免依赖打印机中文代码页。 Future _buildTestPageImage( PrinterUsbDeviceInfo device, @@ -351,21 +392,76 @@ class PrintManage extends BaseFacilityManage { DateTime.now(), format: DateFormats.full, ); - final imageMode = - setting.printerImageMode == 'ESC_STAR' ? 'ESC_STAR' : 'GS_V0'; - final testPage = await _buildTestPageImage( - device, - printedAt, - imageMode, - ); - final bytes = [ - ...generator.feed(1), - ...(imageMode == 'ESC_STAR' - ? generator.image(testPage) - : generator.imageRaster(testPage)), - ...generator.feed(2), - if (setting.printerAutoCut) ...generator.cut(), - ]; + // 指令模式下测试打印同样走指令链路,验证的才是本机实际使用的打印通道; + // 文本行控制在 48 列(80mm)以内,避免依赖包内 row 折行的既有缺陷。 + final List bytes; + if (setting.printerOutputMode == + AppSettingTool.printerOutputModeCommand) { + bytes = [ + ...generator.feed(1), + ...generator.text( + 'HQ 打印机测试', + styles: const PosStyles( + align: PosAlign.center, + bold: true, + height: PosTextSize.size2, + width: PosTextSize.size2, + ), + containsChinese: true, + linesAfter: 1, + ), + ...generator.text( + '指令链路测试', + styles: const PosStyles(align: PosAlign.center), + containsChinese: true, + linesAfter: 1, + ), + ...generator.hr(), + ...generator.text('打印时间:$printedAt', containsChinese: true), + ...generator.text( + '打印机:${device.displayName}', + containsChinese: true, + ), + ...generator.text( + 'VID:${device.vendorId}', + containsChinese: true, + ), + ...generator.text( + 'PID:${device.productId}', + containsChinese: true, + ), + ...generator.text('纸宽:80mm / 48列', containsChinese: true), + ...generator.text('打印方式:指令打印', containsChinese: true), + ...generator.hr(), + ...generator.text( + 'TEST PRINT COMPLETED', + styles: const PosStyles(align: PosAlign.center, bold: true), + ), + ...generator.text( + '如能看到本页文字,指令打印链路正常', + styles: const PosStyles(align: PosAlign.center), + containsChinese: true, + ), + ...generator.feed(2), + if (setting.printerAutoCut) ...generator.cut(), + ]; + } else { + final imageMode = + setting.printerImageMode == 'ESC_STAR' ? 'ESC_STAR' : 'GS_V0'; + final testPage = await _buildTestPageImage( + device, + printedAt, + imageMode, + ); + bytes = [ + ...generator.feed(1), + ...(imageMode == 'ESC_STAR' + ? generator.image(testPage) + : generator.imageRaster(testPage)), + ...generator.feed(2), + if (setting.printerAutoCut) ...generator.cut(), + ]; + } testManager = USBPrinterManager( USBPrinter( name: device.productName, diff --git a/lib/common/utils/sync/sync_print_tool.dart b/lib/common/utils/sync/sync_print_tool.dart index 4614749..9c325c1 100644 --- a/lib/common/utils/sync/sync_print_tool.dart +++ b/lib/common/utils/sync/sync_print_tool.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:common_utils/common_utils.dart'; import 'package:get/get.dart'; @@ -34,12 +35,14 @@ abstract class SyncPrintTool { /// 将 MQTT 或 HTTP 待办按服务端任务 ID 转换为唯一的本地任务。 /// /// [orderId] 是服务端订单 ID,[androidOrderId] 允许线上预约订单为空;[historyId] - /// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列。 + /// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列; + /// [printCmd] 是后端拼装的 Base64 指令流,指令打印模式直写 USB,可空。 static Future addTask({ required String orderId, String? androidOrderId, required String historyId, required String pic, + String? printCmd, String printType = '1', int printNo = 1, int retryVersion = 0, @@ -49,6 +52,7 @@ abstract class SyncPrintTool { androidOrderId: androidOrderId, historyId: historyId, pic: pic, + printCmd: printCmd, printType: printType, printNo: printNo, retryVersion: retryVersion, @@ -67,6 +71,7 @@ abstract class SyncPrintTool { String? androidOrderId, required String historyId, required String pic, + required String? printCmd, required String printType, required int printNo, required int retryVersion, @@ -90,6 +95,7 @@ abstract class SyncPrintTool { : orderId ..serviceOrderId = orderId ..invoiceUrl = pic + ..printCmd = printCmd?.trim().isNotEmpty == true ? printCmd : null ..printType = printType ..printNo = printNo ..retryVersion = retryVersion @@ -108,6 +114,7 @@ abstract class SyncPrintTool { serviceOrderId: orderId, servicePrintId: historyId, invoiceUrl: pic, + printCmd: printCmd?.trim().isNotEmpty == true ? printCmd : null, printType: printType, printNo: printNo, retryVersion: retryVersion, @@ -150,6 +157,7 @@ abstract class SyncPrintTool { androidOrderId: task.androidOrderCode, historyId: task.historyId!, pic: task.pic!, + printCmd: task.printCmd, printType: task.printType ?? '1', printNo: task.printNo ?? 1, retryVersion: task.retryVersion ?? 0, @@ -207,6 +215,8 @@ abstract class SyncPrintTool { static Future _print(PrintHistoryEntity task) async { final app = Get.find(); final dao = app.dataBase.getPrintHistoryDao; + // 指令解码在尝试循环外完成一次:Base64 损坏属于任务数据问题,重试不会好转,直接回退图片。 + final commandBytes = await _resolveCommandBytes(task); for (var index = 0; index < attemptDelays.length; index++) { final delay = attemptDelays[index]; if (delay > Duration.zero) { @@ -220,7 +230,11 @@ abstract class SyncPrintTool { if (!opened) { throw StateError(app.printManage.lastError ?? 'USB 打印机连接失败'); } - final accepted = await app.printManage.printImg(task.invoiceUrl); + // 指令字节可用时直写指令流;图片模式、无指令或指令损坏时走图片栅格打印。 + // 指令写入失败只按原节奏重试指令,不回退图片,避免同一任务两种内容先后出纸。 + final accepted = commandBytes != null + ? await app.printManage.printCommand(commandBytes) + : await app.printManage.printImg(task.invoiceUrl); if (!accepted) { throw StateError(app.printManage.lastError ?? 'USB 未完整接收打印数据'); } @@ -250,6 +264,33 @@ abstract class SyncPrintTool { await _report(task); } + /// 指令打印模式下解码任务携带的 Base64 打印指令流。 + /// + /// 图片模式、任务无指令、内容为空或 Base64 损坏时返回 null,调用方回退图片栅格打印; + /// 解码失败不进入 USB 重试节奏,因为重试同一份损坏数据不会改变结果。 + static Future?> _resolveCommandBytes( + PrintHistoryEntity task, + ) async { + final setting = await AppSettingTool.getSysSetting(); + if (setting.printerOutputMode != AppSettingTool.printerOutputModeCommand) { + return null; + } + final raw = task.printCmd?.trim() ?? ''; + if (raw.isEmpty) return null; + try { + final decoded = base64.decode(raw.replaceAll(RegExp(r'\s'), '')); + if (decoded.isEmpty) { + logger.w('|$_tag|打印指令内容为空,回退图片打印|task:${task.servicePrintId}|'); + return null; + } + return decoded; + } catch (error) { + logger.w( + '|$_tag|打印指令解码失败,回退图片打印|task:${task.servicePrintId}|error:$error|'); + return null; + } + } + /// 向服务端提交当前重试版本的最终结果;失败时保留 resultPending 等待网络恢复补报。 static Future _report(PrintHistoryEntity task) async { if (!AppNetworkTool.isAutoNetwork()) { diff --git a/lib/common/values/svgs.dart b/lib/common/values/svgs.dart index d379140..574cd11 100644 --- a/lib/common/values/svgs.dart +++ b/lib/common/values/svgs.dart @@ -1,3 +1,2 @@ /// svgs assets -class AssetsSvgs { -} +class AssetsSvgs {} diff --git a/lib/common/widgets/app/app_shortcut_ime_collector.dart b/lib/common/widgets/app/app_shortcut_ime_collector.dart deleted file mode 100644 index 35f29af..0000000 --- a/lib/common/widgets/app/app_shortcut_ime_collector.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -/// 把输入法提交的文本反查为字母逻辑按键,只接受 26 个英文字母(不区分大小写)。 -/// -/// Flutter 的字母逻辑键使用小写 Unicode 码点作为稳定键 ID(a-z 对应 0x61-0x7A), -/// 大小写输入统一映射到同一个按键;中文候选、标点、多字符提交无法确认到唯一 -/// 按键,返回空由调用方忽略,不关心操作员当前使用什么输入法。 -LogicalKeyboardKey? shortcutLogicalKeyFromImeText(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); -} - -/// 承载输入法焦点的隐形快捷按键收集器。 -/// -/// 模拟器与输入法激活的真机上,字母按键会被合成为文本提交而不产生按键事件, -/// 只有持有焦点的编辑组件能收到这类输入;收集器挂载后立即抢占焦点, -/// 把提交文本原样回调给调用方做按键反查,自身不可见也不保留内容。 -/// 使用 [TextInputType.none] 抑制软键盘弹出,与扫码枪输入框的做法一致。 -class AppShortcutImeCollector extends StatefulWidget { - /// 创建收集器;[onCharacter] 收到输入法每次提交的原始文本。 - const AppShortcutImeCollector({super.key, required this.onCharacter}); - - /// 输入法提交文本时的回调,参数为本次提交的完整字符串。 - final ValueChanged onCharacter; - - @override - State createState() => - _AppShortcutImeCollectorState(); -} - -class _AppShortcutImeCollectorState extends State { - /// 收集器文本控制器,每次提交后立即清空,避免多键累积。 - 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(); - // 监听结束即隐藏输入法窗口,避免软键盘或残留的输入连接跟随到其他页面。 - SystemChannels.textInput.invokeMethod('TextInput.hide'); - 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); - }, - ), - ), - ), - ); - } -} diff --git a/lib/common/widgets/app/app_soft_keyboard.dart b/lib/common/widgets/app/app_soft_keyboard.dart new file mode 100644 index 0000000..2b707d4 --- /dev/null +++ b/lib/common/widgets/app/app_soft_keyboard.dart @@ -0,0 +1,644 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// 自研软键盘的初始面板类型。 +enum AppSoftKeyboardMode { + /// 数字面板(1-9、0、退格、清空)。 + digits, + + /// 字母面板(QWERTY 布局,支持大小写切换与空格)。 + letters, + + /// 符号面板(常用标点与特殊符号)。 + symbols, +} + +/// 自研软键盘面板:数字/字母/符号三面板 + 顶部切换 tab。 +/// +/// 定制 ROM 在检测到物理键盘连接时不渲染系统软键盘窗口,本面板以确定性 +/// 自绘接管触摸输入:显式绑定目标 [TextEditingController],在光标位置插入、 +/// 退格或清空,字符白名单与长度上限由调用场景声明。两种使用方式: +/// +/// - 页面输入框:[AppSoftKeyboard.show] 弹出可拖动的浮层键盘; +/// - 矮弹框内的输入(如终端操作验证):直接把 [AppSoftKeyboardPanel] 放进 +/// 弹框 content,键盘跟随弹框而不与弹框分离。 +/// +/// 目标输入框仍应保持物理输入能力:设置 [TextInputType.none] 只抑制系统 +/// 软键盘,实体键盘与扫码枪的按键事件不受影响。 +class AppSoftKeyboardPanel extends StatefulWidget { + /// 创建内嵌键盘面板;宽度默认跟随父级约束。 + const AppSoftKeyboardPanel({ + super.key, + required this.controller, + this.mode = AppSoftKeyboardMode.digits, + this.allowPattern, + this.maxLength, + this.semanticLabel = '终端软键盘', + this.onCloseRequested, + this.contentWidth, + }); + + /// 绑定的目标输入控制器;按键在光标位置插入或删除。 + final TextEditingController controller; + + /// 初始面板类型。 + final AppSoftKeyboardMode mode; + + /// 可插入字符白名单(正则逐字符匹配);为空时不限制。 + final RegExp? allowPattern; + + /// 目标文本总长度上限;超限时新插入被忽略。 + final int? maxLength; + + /// 无障碍标签,用于区分宿主场景中的多个键盘实例。 + final String semanticLabel; + + /// 请求收起键盘的回调;非空时面板显示通栏“确认”键(浮层模式), + /// 为空表示内嵌在弹框中,提交由弹框自身按钮承担。 + final VoidCallback? onCloseRequested; + + /// 面板内容区宽度;为空时跟随父级约束(内嵌弹框场景)。 + final double? contentWidth; + + @override + State createState() => _AppSoftKeyboardPanelState(); +} + +class _AppSoftKeyboardPanelState extends State { + /// 当前面板类型;由顶部 tab 与字母面板的 123 键切换。 + AppSoftKeyboardMode _mode = AppSoftKeyboardMode.digits; + + /// 字母面板当前是否大写;默认小写,按 ⇧ 切换。 + bool _uppercase = false; + + @override + void initState() { + super.initState(); + _mode = widget.mode; + } + + /// 校验并插入文本到光标位置;替换当前选区,插入后光标落在文本末尾。 + void _insert(String text) { + if (text.isEmpty) return; + final allowPattern = widget.allowPattern; + if (allowPattern != null && + text.runes + .any((code) => !allowPattern.hasMatch(String.fromCharCode(code)))) { + return; + } + final value = widget.controller.value; + final selection = value.selection.isValid + ? value.selection + : TextSelection.collapsed(offset: value.text.length); + final removedLength = selection.end - selection.start; + final maxLength = widget.maxLength; + if (maxLength != null && + value.text.length - removedLength + text.length > maxLength) { + return; + } + widget.controller.value = value.replaced(selection, text); + } + + /// 删除选区或光标前一位字符。 + void _backspace() { + final value = widget.controller.value; + if (value.text.isEmpty) return; + final selection = value.selection.isValid + ? value.selection + : TextSelection.collapsed(offset: value.text.length); + final range = selection.isCollapsed + ? TextRange(start: selection.start - 1, end: selection.end) + : selection; + widget.controller.value = value.replaced(range, ''); + } + + /// 清空全部文本。 + void _clear() { + if (widget.controller.text.isEmpty) return; + widget.controller.clear(); + } + + void _switchMode(AppSoftKeyboardMode mode) { + if (mode == _mode) return; + setState(() => _mode = mode); + } + + @override + Widget build(BuildContext context) { + return Semantics( + label: widget.semanticLabel, + child: LayoutBuilder( + builder: (context, constraints) { + // 内嵌场景宽度跟随弹框;浮层场景使用固定面板宽。 + final contentWidth = widget.contentWidth ?? constraints.maxWidth; + return [ + _buildTabRow(FTheme.of(context)), + SizedBox(height: 10.h), + switch (_mode) { + AppSoftKeyboardMode.digits => + _buildDigitsPanel(context, contentWidth), + AppSoftKeyboardMode.letters => + _buildLettersPanel(context, contentWidth), + AppSoftKeyboardMode.symbols => + _buildSymbolsPanel(context, contentWidth), + }, + ].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch); + }, + ), + ); + } + + /// 顶部面板切换 tab 行。 + Widget _buildTabRow(FThemeData theme) { + Widget tab(String label, AppSoftKeyboardMode mode) { + final active = _mode == mode; + return InkWell( + onTap: () => _switchMode(mode), + borderRadius: BorderRadius.circular(20.w), + child: Text( + label, + style: theme.typography.sm.copyWith( + color: active ? XfjDesignTokens.primary : XfjDesignTokens.muted, + fontWeight: FontWeight.w700, + ), + ).center().tight(width: 84.w, height: 44.h).decorated( + color: active + ? XfjDesignTokens.primarySoft + : XfjDesignTokens.control, + borderRadius: BorderRadius.circular(20.w), + ), + ); + } + + return [ + const Spacer(), + tab('123', AppSoftKeyboardMode.digits), + tab('ABC', AppSoftKeyboardMode.letters), + tab('#+=', AppSoftKeyboardMode.symbols), + ].toRowSpace(space: 8.w).tight(height: 44.h).padding(horizontal: 4.w); + } + + /// 数字面板:三行数字(1-9)+ 操作行(清空/0/退格),浮层模式追加通栏确认。 + Widget _buildDigitsPanel(BuildContext context, double contentWidth) { + final theme = FTheme.of(context); + final keyWidth = (contentWidth - 20.w) / 3 - 1; + Widget key( + String semantic, { + Widget? content, + VoidCallback? onTap, + }) => + _Key( + semanticLabel: semantic, + onTap: onTap ?? () {}, + width: keyWidth, + child: content, + ); + Widget digitRow(List digits) => [ + for (final value in digits) + key( + '输入$value', + onTap: () => _insert(value), + content: Text( + value, + style: theme.typography.xl.copyWith( + color: XfjDesignTokens.ink, + fontWeight: FontWeight.w800, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ].toRowSpace(space: 10.w); + return [ + digitRow(['1', '2', '3']), + digitRow(['4', '5', '6']), + digitRow(['7', '8', '9']), + [ + key( + '清空全部输入', + onTap: _clear, + content: Text( + '清空', + style: theme.typography.sm.copyWith( + color: XfjDesignTokens.muted, + fontWeight: FontWeight.w700, + ), + ), + ), + key( + '输入0', + onTap: () => _insert('0'), + content: Text( + '0', + style: theme.typography.xl.copyWith( + color: XfjDesignTokens.ink, + fontWeight: FontWeight.w800, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + key( + '退格删除一位', + onTap: _backspace, + content: FIcon( + FAssets.icons.delete, + color: XfjDesignTokens.muted, + size: 22.sp, + ), + ), + ].toRowSpace(space: 10.w), + if (widget.onCloseRequested != null) + _Key( + semanticLabel: '确认并收起键盘', + onTap: widget.onCloseRequested!, + width: contentWidth, + height: 54.h, + emphasized: true, + child: Text( + '确认', + style: theme.typography.base.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + ), + ), + ].toColumnSpace(space: 10.h); + } + + /// 字母面板:QWERTY 三行(键等宽、整行居中)+ 操作行(⇧/123/空格/退格/确认)。 + Widget _buildLettersPanel(BuildContext context, double contentWidth) { + final theme = FTheme.of(context); + final keyWidth = (contentWidth - 9 * 8.w) / 10 - 1; + Widget letter(String value) => _Key( + semanticLabel: '输入字母$value', + onTap: () => _insert(_uppercase ? value.toUpperCase() : value), + width: keyWidth, + child: Text( + _uppercase ? value.toUpperCase() : value, + style: theme.typography.lg.copyWith( + color: XfjDesignTokens.ink, + fontWeight: FontWeight.w700, + ), + ), + ); + Widget letterRow(List letters) => [ + for (final value in letters) letter(value), + ].toRowSpace( + space: 8.w, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + ); + final actionStyle = theme.typography.sm.copyWith( + color: XfjDesignTokens.muted, + fontWeight: FontWeight.w700, + ); + final whiteActionStyle = theme.typography.sm.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ); + Widget actionKey( + String semantic, { + required double width, + bool emphasized = false, + Widget? content, + required VoidCallback onTap, + }) => + _Key( + semanticLabel: semantic, + onTap: onTap, + width: width, + emphasized: emphasized, + child: content, + ); + return [ + letterRow(['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p']), + letterRow(['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l']), + letterRow(['z', 'x', 'c', 'v', 'b', 'n', 'm']), + [ + actionKey( + _uppercase ? '切换为小写输入' : '切换为大写输入', + width: 88.w, + emphasized: _uppercase, + content: Text( + _uppercase ? '⇧ 大' : '⇧ 小', + style: _uppercase ? whiteActionStyle : actionStyle, + ), + onTap: () => setState(() => _uppercase = !_uppercase), + ), + actionKey( + '切到数字面板', + width: 88.w, + content: Text('123', style: actionStyle), + onTap: () => _switchMode(AppSoftKeyboardMode.digits), + ), + actionKey( + '输入空格', + width: 264.w, + content: Text('空格', style: actionStyle), + onTap: () => _insert(' '), + ), + actionKey( + '退格删除一位', + width: 130.w, + content: FIcon( + FAssets.icons.delete, + color: XfjDesignTokens.muted, + size: 22.sp, + ), + onTap: _backspace, + ), + if (widget.onCloseRequested != null) + actionKey( + '确认并收起键盘', + width: 130.w, + emphasized: true, + content: Text('确认', style: whiteActionStyle), + onTap: widget.onCloseRequested!, + ), + ].toRowSpace(space: 8.w), + ].toColumnSpace(space: 10.h); + } + + /// 符号面板:常用符号五行五列 + 操作行(空格/退格/确认)。 + Widget _buildSymbolsPanel(BuildContext context, double contentWidth) { + final theme = FTheme.of(context); + const symbols = [ + '-', + '.', + '_', + '/', + '*', + ',', + ':', + ';', + '%', + '#', + '+', + '@', + '&', + '?', + '!', + '(', + ')', + '[', + ']', + '=', + ]; + final keyWidth = (contentWidth - 4 * 8.w) / 5 - 1; + Widget symbol(String value) => _Key( + semanticLabel: '输入符号$value', + onTap: () => _insert(value), + width: keyWidth, + child: Text( + value, + style: theme.typography.lg.copyWith( + color: XfjDesignTokens.ink, + fontWeight: FontWeight.w700, + ), + ), + ); + final actionStyle = theme.typography.sm.copyWith( + color: XfjDesignTokens.muted, + fontWeight: FontWeight.w700, + ); + final whiteActionStyle = theme.typography.sm.copyWith( + color: Colors.white, + fontWeight: FontWeight.w700, + ); + return [ + for (var index = 0; index < symbols.length; index += 5) + [ + for (final value in symbols.skip(index).take(5)) symbol(value), + ].toRowSpace(space: 8.w), + [ + _Key( + semanticLabel: '输入空格', + onTap: () => _insert(' '), + width: contentWidth / 2, + child: Text('空格', style: actionStyle), + ), + _Key( + semanticLabel: '退格删除一位', + onTap: _backspace, + width: contentWidth / 4, + child: FIcon( + FAssets.icons.delete, + color: XfjDesignTokens.muted, + size: 22.sp, + ), + ), + if (widget.onCloseRequested != null) + _Key( + semanticLabel: '确认并收起键盘', + onTap: widget.onCloseRequested!, + width: contentWidth / 4, + emphasized: true, + child: Text('确认', style: whiteActionStyle), + ), + ].toRowSpace(space: 8.w), + ].toColumnSpace(space: 10.h); + } +} + +/// 自研可拖动软键盘弹出框。 +/// +/// 通过 [AppSoftKeyboard.show] 弹出:键盘浮在页面内容之上(透明遮罩), +/// 默认停靠屏幕底部,可按住顶部手柄行自由拖动,点击键盘外任意处或按 +/// “确认”收起。矮弹框内的输入场景应改用 [AppSoftKeyboardPanel] 内嵌。 +abstract final class AppSoftKeyboard { + /// 浮层键盘的面板宽度(内容区,含左右内边距共 760)。 + static final double floatingContentWidth = 736.w; + + /// 弹出浮层键盘并绑定目标控制器;返回的 Future 在键盘关闭时完成。 + static Future show( + BuildContext context, { + required TextEditingController controller, + AppSoftKeyboardMode mode = AppSoftKeyboardMode.digits, + RegExp? allowPattern, + int? maxLength, + String semanticLabel = '终端软键盘', + }) { + return showGeneralDialog( + context: context, + useRootNavigator: true, + barrierDismissible: true, + // 透明遮罩:键盘期间页面内容可见,点击键盘外任意处即收起。 + barrierColor: Colors.transparent, + // barrierDismissible 开启时框架要求提供遮罩语义标签。 + barrierLabel: '收起软键盘', + // 键盘要求即时出现与消失,不做转场动画。 + transitionDuration: Duration.zero, + pageBuilder: (dialogContext, _, __) => _SoftKeyboardDialog( + controller: controller, + mode: mode, + allowPattern: allowPattern, + maxLength: maxLength, + semanticLabel: semanticLabel, + ), + ); + } +} + +/// 浮层键盘宿主:停靠屏幕底部、承载拖动手柄行与内嵌面板。 +class _SoftKeyboardDialog extends StatefulWidget { + const _SoftKeyboardDialog({ + required this.controller, + required this.mode, + required this.semanticLabel, + this.allowPattern, + this.maxLength, + }); + + final TextEditingController controller; + final AppSoftKeyboardMode mode; + final RegExp? allowPattern; + final int? maxLength; + final String semanticLabel; + + @override + State<_SoftKeyboardDialog> createState() => _SoftKeyboardDialogState(); +} + +class _SoftKeyboardDialogState extends State<_SoftKeyboardDialog> { + /// 拖动偏移;相对默认底部居中位置。 + Offset _dragOffset = Offset.zero; + + /// 拖动手势更新偏移;按屏幕半宽钳制,保证键盘不会被完全拖出屏幕。 + void _onDragUpdate(DragUpdateDetails details) { + final halfScreen = MediaQuery.sizeOf(context) / 2; + setState(() { + _dragOffset += details.delta; + _dragOffset = Offset( + _dragOffset.dx.clamp(-halfScreen.width, halfScreen.width), + _dragOffset.dy.clamp(-halfScreen.height, halfScreen.height), + ); + }); + } + + void _close() => Navigator.of(context).pop(); + + @override + Widget build(BuildContext context) { + // 弹层约束为全屏;用 Spacer 把面板可靠地压到屏幕底部,底部留出安全边距。 + return SizedBox.expand( + child: Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: Transform.translate( + offset: _dragOffset, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + Material( + color: Colors.transparent, + child: Container( + width: 760.w, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: XfjDesignTokens.canvas, + borderRadius: + BorderRadius.circular(XfjDesignTokens.cardRadius), + border: Border.all(color: XfjDesignTokens.line), + boxShadow: XfjDesignTokens.surfaceShadow, + ), + child: [ + _buildDragHandle(), + SizedBox(height: 10.h), + AppSoftKeyboardPanel( + controller: widget.controller, + mode: widget.mode, + allowPattern: widget.allowPattern, + maxLength: widget.maxLength, + semanticLabel: widget.semanticLabel, + onCloseRequested: _close, + contentWidth: AppSoftKeyboard.floatingContentWidth, + ), + ].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch), + ), + ), + ], + ), + ), + ), + ); + } + + /// 拖动手柄行:右侧拖动指示,按住整行自由拖动键盘。 + Widget _buildDragHandle() { + return GestureDetector( + onPanUpdate: _onDragUpdate, + child: [ + const Spacer(), + Icon( + Icons.drag_indicator_rounded, + color: XfjDesignTokens.muted, + size: 26.w, + semanticLabel: '拖动键盘', + ), + ].toRowSpace(space: 8.w).tight(height: 32.h).padding(horizontal: 4.w), + ); + } +} + +/// 单个键盘按键;固定宽度布局(弹层处于无界高度约束,禁用 Expanded), +/// 主操作键使用主题色底白字。 +class _Key extends StatelessWidget { + const _Key({ + required this.semanticLabel, + required this.onTap, + required this.width, + required this.child, + this.emphasized = false, + this.height, + }); + + final String semanticLabel; + final VoidCallback onTap; + + /// 按键宽度;由各面板按列数与间距计算。 + final double width; + + /// 按键内容(数字文字或图标)。 + final Widget? child; + + /// 是否主操作键(确认/大写锁定态),使用主题色底与白字。 + final bool emphasized; + + /// 按键高度;默认 62。 + final double? height; + + @override + Widget build(BuildContext context) { + final keyHeight = height ?? 62.h; + return Semantics( + button: true, + label: semanticLabel, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), + child: (child ?? const SizedBox.shrink()) + .center() + .tight(width: width, height: keyHeight) + .decorated( + color: emphasized + ? XfjDesignTokens.primary + : XfjDesignTokens.surface, + border: Border.all( + color: emphasized + ? XfjDesignTokens.primary + : XfjDesignTokens.line, + ), + borderRadius: BorderRadius.circular( + XfjDesignTokens.controlRadius, + ), + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/app/app_wallet_selector.dart b/lib/common/widgets/app/app_wallet_selector.dart index 6f2efd1..4a64873 100644 --- a/lib/common/widgets/app/app_wallet_selector.dart +++ b/lib/common/widgets/app/app_wallet_selector.dart @@ -86,8 +86,6 @@ Widget appWalletSelector( ); } - - // list.add( // ServiceWallet( // id: "1", @@ -105,4 +103,4 @@ Widget appWalletSelector( // walletTypeId: "2", // balance: 200, // ), -// ); \ No newline at end of file +// ); diff --git a/lib/common/widgets/app/terminal_toolbar.dart b/lib/common/widgets/app/terminal_toolbar.dart index 35c77e7..629d2f6 100644 --- a/lib/common/widgets/app/terminal_toolbar.dart +++ b/lib/common/widgets/app/terminal_toolbar.dart @@ -114,9 +114,11 @@ class TerminalToolbarTextField extends StatelessWidget { this.keyboardType, this.onSubmit, this.prefixBuilder, + this.softKeyboard, + this.softKeyboardAllowPattern, + this.softKeyboardMaxLength, }); - /// 输入框宽度;为空时由父级布局分配可用宽度。 final double? width; /// 页面持有的文本控制器,公共组件不接管其生命周期。 @@ -140,9 +142,22 @@ class TerminalToolbarTextField extends StatelessWidget { /// 输入框前缀构建器,用于页面已有的搜索图标等语义提示。 final ValueWidgetBuilder? prefixBuilder; + /// 启用自研软键盘的初始面板;为空时不接管点击(保持现状)。 + /// + /// 启用后输入框抑制系统软键盘并改为弹出 [AppSoftKeyboard];实体键盘与 + /// 扫码枪的物理按键输入不受影响。 + final AppSoftKeyboardMode? softKeyboard; + + /// 自研键盘的可插入字符白名单;为空时不限制。 + final RegExp? softKeyboardAllowPattern; + + /// 自研键盘的文本长度上限;为空时不限制。 + final int? softKeyboardMaxLength; + /// 构建随按钮大小档位变化的单行输入框。 @override Widget build(BuildContext context) { + final keyboardMode = softKeyboard; return TerminalToolbarControl( width: width, child: FTextField( @@ -150,10 +165,20 @@ class TerminalToolbarTextField extends StatelessWidget { focusNode: focusNode, hint: hint, textInputAction: textInputAction, - keyboardType: keyboardType, + // 自研键盘接管时抑制系统软键盘,只保留物理按键输入通道。 + keyboardType: keyboardMode != null ? TextInputType.none : keyboardType, maxLines: 1, onSubmit: onSubmit, prefixBuilder: prefixBuilder, + onTap: keyboardMode == null + ? null + : () => AppSoftKeyboard.show( + context, + controller: controller, + mode: keyboardMode, + allowPattern: softKeyboardAllowPattern, + maxLength: softKeyboardMaxLength, + ), ), ); } diff --git a/lib/common/widgets/cashier/cashier_workspace.dart b/lib/common/widgets/cashier/cashier_workspace.dart index b4cfe24..ab4ee99 100644 --- a/lib/common/widgets/cashier/cashier_workspace.dart +++ b/lib/common/widgets/cashier/cashier_workspace.dart @@ -139,6 +139,7 @@ class CashierWorkspace extends StatelessWidget { final TextEditingController? searchController; final FocusNode? searchFocusNode; final String searchHint; + final VoidCallback? onSearch; final VoidCallback? onReset; final VoidCallback? onScan; @@ -270,8 +271,10 @@ class CashierSearchPanel extends StatelessWidget { child: TerminalToolbarTextField( controller: controller!, focusNode: focusNode, + // 触摸输入统一走自研软键盘(字母面板);实体键盘与 + // 扫码枪的物理输入不受影响。 + softKeyboard: AppSoftKeyboardMode.letters, textInputAction: TextInputAction.none, - keyboardType: TextInputType.none, hint: hint, onSubmit: (_) => onSearch?.call(), ), @@ -742,9 +745,10 @@ class CashierProductCard extends StatelessWidget { fontWeight: FontWeight.w700, ), ).padding(horizontal: 6.w, vertical: 3.h).decorated( - color: XfjDesignTokens.warning.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), + color: + XfjDesignTokens.warning.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), ].toColumnSpace( crossAxisAlignment: CrossAxisAlignment.start, space: 4.h, @@ -901,8 +905,9 @@ class _CashierProductDetailsButton extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.typography.xs.copyWith( - color: - enabled ? XfjDesignTokens.primary : XfjDesignTokens.muted, + color: enabled + ? XfjDesignTokens.primary + : XfjDesignTokens.muted, fontWeight: FontWeight.w800, ), ), diff --git a/lib/common/widgets/index.dart b/lib/common/widgets/index.dart index 872189a..3d7b83c 100644 --- a/lib/common/widgets/index.dart +++ b/lib/common/widgets/index.dart @@ -11,9 +11,9 @@ export './app/hardware_input_history_selector.dart'; export './app/app_img.dart'; export './app/app_message_dialog.dart'; export './app/app_page.dart'; +export './app/app_soft_keyboard.dart'; export './app/app_price.dart'; export './app/app_qr_code_dialog.dart'; -export './app/app_shortcut_ime_collector.dart'; export './app/reservation_schedule_card.dart'; export './app/app_radio_hour_type_dialog.dart'; export './app/app_wallet_selector.dart'; diff --git a/lib/common/widgets/setting/setting_password_dialog.dart b/lib/common/widgets/setting/setting_password_dialog.dart index 9b908db..f850f27 100644 --- a/lib/common/widgets/setting/setting_password_dialog.dart +++ b/lib/common/widgets/setting/setting_password_dialog.dart @@ -130,7 +130,8 @@ class _TerminalAdminPasswordDialogState hint: '请输入管理密码', maxLines: 1, obscureText: _obscureText, - keyboardType: TextInputType.number, + // 系统软键盘在定制 ROM 上不可用,触摸输入由下方内嵌键盘承担。 + keyboardType: TextInputType.none, textInputAction: TextInputAction.done, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, @@ -162,7 +163,7 @@ class _TerminalAdminPasswordDialogState FIcon( FAssets.icons.circleAlert, color: XfjDesignTokens.danger, - size: 18.sp, + size: 18.w, ), SizedBox(width: 8.w), Text( @@ -175,6 +176,16 @@ class _TerminalAdminPasswordDialogState ], ), ], + SizedBox(height: 20.h), + // 弹框高度有限且需要输入,键盘直接内嵌在弹框内跟随展示; + // 管理密码仅数字且最长 12 位。 + AppSoftKeyboardPanel( + controller: _controller, + mode: AppSoftKeyboardMode.digits, + allowPattern: RegExp('[0-9]'), + maxLength: 12, + semanticLabel: '管理密码数字键盘', + ), ], ), ), diff --git a/lib/pages/terminal/card_active/view.dart b/lib/pages/terminal/card_active/view.dart index 8c64c71..acffca4 100644 --- a/lib/pages/terminal/card_active/view.dart +++ b/lib/pages/terminal/card_active/view.dart @@ -20,6 +20,9 @@ class CardActivePage extends GetView { width: 360.w, controller: pageController.carNumberC, hint: '请输入饭卡卡号', + // 饭卡卡号为数字;触摸输入走自研数字键盘,读卡与实体键盘不受影响。 + softKeyboard: AppSoftKeyboardMode.digits, + softKeyboardAllowPattern: RegExp('[0-9]'), onSubmit: (_) => pageController.doPage(page: 1), ), SizedBox(width: 12.w), diff --git a/lib/pages/terminal/menu_query/controller.dart b/lib/pages/terminal/menu_query/controller.dart index da776af..f3bef90 100644 --- a/lib/pages/terminal/menu_query/controller.dart +++ b/lib/pages/terminal/menu_query/controller.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:dio/dio.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:forui/forui.dart'; import 'package:get/get.dart'; @@ -362,15 +361,14 @@ class ReservationMenuQueryController extends GetxController } } - /// 页面重新可见时释放其他页面残留的文本输入焦点并收起软键盘。 + /// 页面重新可见时释放其他页面残留的文本输入焦点。 /// /// 终端主框架通过内嵌 Navigator 替换工作区,本页 Controller 常驻不重建; - /// 从开卡等带输入框的页面切回时,残留焦点会让软键盘跟随显示,并让快捷 - /// 键盘误判为文本输入而让路,因此每次页面挂载都需要清理一次。 + /// 从开卡等带输入框的页面切回时,残留焦点会让物理键盘字符落入旧输入框, + /// 并让快捷键盘误判为文本输入而让路,因此每次页面挂载都清理一次。 void releaseResidualTextInput() { if (_closed) return; FocusManager.instance.primaryFocus?.unfocus(); - SystemChannels.textInput.invokeMethod('TextInput.hide'); } /// 更新预选餐段;等待硬件识别时仍允许调整,身份已获取并查询服务端后锁定本轮目标。 diff --git a/lib/pages/terminal/menu_query/reservation_shortcut_keyboard.dart b/lib/pages/terminal/menu_query/reservation_shortcut_keyboard.dart index 9daa74a..ada66a3 100644 --- a/lib/pages/terminal/menu_query/reservation_shortcut_keyboard.dart +++ b/lib/pages/terminal/menu_query/reservation_shortcut_keyboard.dart @@ -1,6 +1,5 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; - import 'package:pj_supermarket_cloud/common/index.dart'; /// 单独按下的修饰键不作为快捷键来源,与设置页按键绑定保持一致的过滤口径。 @@ -79,12 +78,8 @@ class ReservationShortcutKeyboard { bool _handleKeyEvent(KeyEvent event) { if (!active) return false; // 焦点落在文本编辑组件(如终端操作验证的密码框)时物理按键全部让路 - // 给文本输入:既不拦截按键,也不隐藏软键盘。 + // 给文本输入:既不拦截按键,也不影响任何输入通道。 if (_hasActiveTextInput()) return false; - // 键盘模式激活期间持续压制输入法窗口:部分系统开启“连接物理键盘时显示 - // 虚拟键盘”后,物理按键会被转化为软键盘弹出;命中与否都先隐藏,保证 - // 查询菜单只响应外接小键盘本身。 - SystemChannels.textInput.invokeMethod('TextInput.hide'); if (event is! KeyDownEvent) return false; if (_ignoredModifierKeys.contains(event.logicalKey)) return false; final binding = MenuQueryShortcutKeyBinding.fromKeyEvent(event); diff --git a/lib/pages/terminal/order_history/controller.dart b/lib/pages/terminal/order_history/controller.dart index 4c024b0..b82149a 100644 --- a/lib/pages/terminal/order_history/controller.dart +++ b/lib/pages/terminal/order_history/controller.dart @@ -148,7 +148,8 @@ class OrderHistoryController extends GetxController { /// 切换数据范围;全部数据依赖网络,离线时提示并保持本机数据。 Future doScope(TerminalDataScope target) async { if (scope.value == target) return; - if (target == TerminalDataScope.merchant && !AppNetworkTool.isAutoNetwork()) { + if (target == TerminalDataScope.merchant && + !AppNetworkTool.isAutoNetwork()) { Toast.error(msg: '当前没有网络,无法查询全部数据'); return; } diff --git a/lib/pages/terminal/order_history/view.dart b/lib/pages/terminal/order_history/view.dart index 74f3e25..bbe2494 100644 --- a/lib/pages/terminal/order_history/view.dart +++ b/lib/pages/terminal/order_history/view.dart @@ -1,7 +1,6 @@ import 'package:common_utils/common_utils.dart'; 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'; @@ -429,10 +428,8 @@ class _OrderDateRangeControl extends StatelessWidget { fontWeight: FontWeight.w600, ), onTap: () async { - // 先释放搜索框焦点并等待软键盘退场,避免日期弹窗打开后输入框仍显示聚焦态。 + // 先释放搜索框焦点,避免日期弹窗打开后输入框仍显示聚焦态。 FocusManager.instance.primaryFocus?.unfocus(); - await SystemChannels.textInput.invokeMethod('TextInput.hide'); - await Future.delayed(const Duration(milliseconds: 100)); if (!context.mounted) return; final result = await appShowDatePicker( context, @@ -509,8 +506,7 @@ class _StatusFilterButton extends StatelessWidget { if (showCount) ...[ SizedBox(width: 8.w), Container( - padding: - EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h), decoration: BoxDecoration( color: selected ? Colors.white.withValues(alpha: 0.2) diff --git a/lib/pages/terminal/setting/widgets/setting_baseInfo.dart b/lib/pages/terminal/setting/widgets/setting_baseInfo.dart index 6f863b1..907eee3 100644 --- a/lib/pages/terminal/setting/widgets/setting_baseInfo.dart +++ b/lib/pages/terminal/setting/widgets/setting_baseInfo.dart @@ -39,12 +39,30 @@ Future _bindServiceDialog(BuildContext context, SysSetting sys) async { maxWidth: context.width * 0.55, onClose: () => Navigator.of(dialogContext).pop(), semanticLabel: '绑定设备唯一编号弹框', - content: FTextField( - textInputAction: TextInputAction.none, - controller: uniqueCodeController, - hint: '请输入设备唯一编号', - maxLines: 1, - ).width(double.infinity).marginSymmetric(vertical: 20.h), + content: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 设备唯一编号为纯数字;系统软键盘在定制 ROM 上不可用,弹框 + // 高度有限,键盘直接内嵌跟随展示,实体键盘输入不受影响。 + FTextField( + keyboardType: TextInputType.none, + controller: uniqueCodeController, + hint: '请输入设备唯一编号', + maxLines: 1, + ).width(double.infinity), + SizedBox(height: 16.h), + AppSoftKeyboardPanel( + controller: uniqueCodeController, + mode: AppSoftKeyboardMode.digits, + allowPattern: RegExp('[0-9]'), + semanticLabel: '设备唯一编号数字键盘', + ), + ], + ), + ).marginSymmetric(vertical: 20.h), actions: [ FButton( style: FButtonStyle.outline, diff --git a/lib/pages/terminal/setting/widgets/setting_menu_query_shortcut.dart b/lib/pages/terminal/setting/widgets/setting_menu_query_shortcut.dart index dab2f74..d13cea2 100644 --- a/lib/pages/terminal/setting/widgets/setting_menu_query_shortcut.dart +++ b/lib/pages/terminal/setting/widgets/setting_menu_query_shortcut.dart @@ -37,9 +37,6 @@ const List _shortcutIgnoredModifierKeys = [ LogicalKeyboardKey.metaRight, ]; -/// 把输入法提交的文本反查为字母逻辑按键的逻辑已公共化到 -/// [shortcutLogicalKeyFromImeText],设置页与查询菜单页共用同一套字符兜底规则。 - /// 查询菜单外接快捷小键盘的按键绑定设置组件。 /// /// 操作流程:点击动作行进入监听(行高亮并提示),再按下外接小键盘的物理按键 @@ -268,39 +265,12 @@ class _SettingMenuQueryShortcutKeypadState _testDialogShowing = false; } - /// 输入法文本兜底:把被合成为文本提交的字母按键转换为逻辑键处理。 - /// - /// 模拟器以及输入法处于激活状态的真机上,物理字母键可能被输入法合成为 - /// 文本注入而不产生按键事件;此时由隐形收集器持有焦点接收文本并反查按键, - /// 与按键事件通道互补,覆盖“字符型小键盘”的全部接入场景。 - void _handleImeText(String text) { - final listeningAction = _listeningAction; - if (listeningAction == null && !_testing) return; - final key = shortcutLogicalKeyFromImeText(text); - // 无法确认到唯一字母按键的输入(中文候选、标点等)直接忽略,保持当前模式, - // 不提示错误也不关心操作员的输入法状态。 - if (key == null) return; - // 文本输入只有逻辑键身份,物理键位缺失时绑定仍可运行期兜底匹配。 - final binding = MenuQueryShortcutKeyBinding(logicalKeyId: key.keyId); - if (listeningAction != null) { - logger.i( - '|$_tag|输入法兜底绑定action:${listeningAction.code}|key:${key.debugName}|'); - _applyBinding(listeningAction, binding); - } else { - logger.i('|$_tag|输入法兜底测试key:${key.debugName}|'); - _handleBoundKeyPress(binding); - } - } - /// 分组内依次渲染键盘模式开关、说明头、状态提示、五个动作行和键盘测试区;动作行依赖绑定 Rx 独立重建。 @override Widget build(BuildContext context) { return SettingGroup( title: '快捷按钮', child: [ - // 绑定监听或测试期间挂载隐形输入收集器,承接输入法合成的文本按键输入。 - if (_listeningAction != null || _testing) - AppShortcutImeCollector(onCharacter: _handleImeText), _buildKeyboardModeRow(), _ShortcutBindingHeader( onReset: widget.controller.resetMenuQueryShortcutKeyBindings, diff --git a/lib/pages/terminal/setting/widgets/setting_printer_info.dart b/lib/pages/terminal/setting/widgets/setting_printer_info.dart index 1a2b001..d246824 100644 --- a/lib/pages/terminal/setting/widgets/setting_printer_info.dart +++ b/lib/pages/terminal/setting/widgets/setting_printer_info.dart @@ -219,6 +219,31 @@ class _SettingPrinterInfoState extends State { } } + /// 切换本机打印输出方式并立即持久化;保存失败时回滚显示,保证界面与实际配置一致。 + Future _switchOutputMode(String mode) async { + final setting = _setting; + if (setting == null || setting.printerOutputMode == mode) return; + final previous = setting.printerOutputMode; + setState(() => setting.printerOutputMode = mode); + try { + await AppSettingTool.setPrinterOutputMode(mode); + if (!mounted) return; + setState(() { + _operationMessage = mode == AppSettingTool.printerOutputModeCommand + ? '已切换为指令打印,后续任务优先直写服务端下发的打印命令' + : '已切换为图片打印,后续任务按图片栅格指令输出'; + }); + } catch (error, stack) { + logger.e('|打印机信息|切换打印方式失败|mode:$mode|error:$error|stack:$stack|'); + if (!mounted) return; + setState(() { + setting.printerOutputMode = previous; + _errorMessage = '打印方式保存失败,已恢复原设置'; + }); + Toast.error(msg: '打印方式保存失败'); + } + } + /// 向选中设备输出一张诊断小票,并准确描述 USB 写入层能够确认的结果边界。 Future _printTestPage() async { final device = _selectedDevice; @@ -305,6 +330,12 @@ class _SettingPrinterInfoState extends State { child: _buildConfiguredDetails(setting), ), SizedBox(height: XfjDesignTokens.cardGap.h), + SettingGroup( + title: '本机打印方式', + dense: true, + child: _buildOutputModeSelector(context, setting), + ), + SizedBox(height: XfjDesignTokens.cardGap.h), SettingGroup( title: 'USB 打印机', dense: true, @@ -461,6 +492,37 @@ class _SettingPrinterInfoState extends State { ], ); + /// 构建打印输出方式切换:图片栅格为默认兼容模式,指令直写出纸更快。 + Widget _buildOutputModeSelector(BuildContext context, SysSetting? setting) { + final theme = FTheme.of(context); + final mode = + setting?.printerOutputMode == AppSettingTool.printerOutputModeCommand + ? AppSettingTool.printerOutputModeCommand + : AppSettingTool.printerOutputModeImage; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingSegmentedSelector( + label: '凭证输出', + value: mode, + options: const { + AppSettingTool.printerOutputModeImage: '图片打印', + AppSettingTool.printerOutputModeCommand: '指令打印', + }, + onChanged: _switchOutputMode, + showDivider: false, + ), + Padding( + padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 12.h), + child: Text( + '指令打印由服务端直接下发打印命令,出纸更快;后台未生成命令或历史任务会自动回退图片打印。测试打印跟随当前方式验证对应链路', + style: theme.typography.xs.copyWith(color: XfjDesignTokens.muted), + ), + ), + ], + ); + } + /// 构建 USB 设备空状态或可选择设备行列表。 Widget _buildDeviceList() { if (_refreshing && _devices.isEmpty) { diff --git a/lib/pages/terminal/verification_records/view.dart b/lib/pages/terminal/verification_records/view.dart index 5f2f151..e6a27ab 100644 --- a/lib/pages/terminal/verification_records/view.dart +++ b/lib/pages/terminal/verification_records/view.dart @@ -1,7 +1,6 @@ import 'package:common_utils/common_utils.dart'; 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'; @@ -165,7 +164,6 @@ class _VerificationDateRangeControl extends StatelessWidget { ), onTap: () async { FocusManager.instance.primaryFocus?.unfocus(); - await SystemChannels.textInput.invokeMethod('TextInput.hide'); if (!context.mounted) return; onChanged(await appShowDatePicker(context, datetimeRange: dates)); }, diff --git a/lib/sub_screen/common/values/constants.dart b/lib/sub_screen/common/values/constants.dart index e69de29..8b13789 100644 --- a/lib/sub_screen/common/values/constants.dart +++ b/lib/sub_screen/common/values/constants.dart @@ -0,0 +1 @@ + diff --git a/pubspec.lock b/pubspec.lock index 8bce699..076a952 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: _fe_analyzer_shared sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "76.0.0" _macros: @@ -19,7 +19,7 @@ packages: description: name: analyzer sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.11.0" android_id: @@ -27,7 +27,7 @@ packages: description: name: android_id sha256: f6489be4c4380cdb55a99db5b03764d936e84e9d5258a26571aa500bd1159fe0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.1" archive: @@ -35,7 +35,7 @@ packages: description: name: archive sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.9" args: @@ -43,7 +43,7 @@ packages: description: name: args sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" async: @@ -51,7 +51,7 @@ packages: description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.0" background_downloader: @@ -59,7 +59,7 @@ packages: description: name: background_downloader sha256: "0873a939f5a301872cb7614e12f759fbccef2e3ed0d0d37b540dde7611275b12" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.12.3" barcode: @@ -67,7 +67,7 @@ packages: description: name: barcode sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.9" bidi: @@ -75,7 +75,7 @@ packages: description: name: bidi sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.13" boolean_selector: @@ -83,7 +83,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" build: @@ -91,7 +91,7 @@ packages: description: name: build sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.2" build_config: @@ -99,7 +99,7 @@ packages: description: name: build_config sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" build_daemon: @@ -107,7 +107,7 @@ packages: description: name: build_daemon sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.4" build_resolvers: @@ -115,7 +115,7 @@ packages: description: name: build_resolvers sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.4" build_runner: @@ -123,7 +123,7 @@ packages: description: name: build_runner sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.14" build_runner_core: @@ -131,7 +131,7 @@ packages: description: name: build_runner_core sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.0.0" built_collection: @@ -139,7 +139,7 @@ packages: description: name: built_collection sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: @@ -147,7 +147,7 @@ packages: description: name: built_value sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.13.0" cached_network_image: @@ -155,7 +155,7 @@ packages: description: name: cached_network_image sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.1" cached_network_image_platform_interface: @@ -163,7 +163,7 @@ packages: description: name: cached_network_image_platform_interface sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.1" cached_network_image_web: @@ -171,7 +171,7 @@ packages: description: name: cached_network_image_web sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" characters: @@ -179,7 +179,7 @@ packages: description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" charcode: @@ -187,7 +187,7 @@ packages: description: name: charcode sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" checked_yaml: @@ -195,7 +195,7 @@ packages: description: name: checked_yaml sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.3" clock: @@ -203,7 +203,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" code_builder: @@ -211,7 +211,7 @@ packages: description: name: code_builder sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.10.1" collection: @@ -219,7 +219,7 @@ packages: description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.0" common_utils: @@ -227,7 +227,7 @@ packages: description: name: common_utils sha256: c26884339b13ff99b0739e56f4b02090c84054ed9dd3a045435cd24e7b99c2c1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" connectivity_plus: @@ -235,7 +235,7 @@ packages: description: name: connectivity_plus sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.5" connectivity_plus_platform_interface: @@ -243,7 +243,7 @@ packages: description: name: connectivity_plus_platform_interface sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" convert: @@ -251,7 +251,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.2" cron: @@ -259,7 +259,7 @@ packages: description: name: cron sha256: "2af3de2cabcc81346c00ff9db06296cdaf37ad17bd6f97fb63777a9ddefcacf3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.2" crypto: @@ -267,7 +267,7 @@ packages: description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.7" csslib: @@ -275,7 +275,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" cupertino_icons: @@ -283,7 +283,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.8" dart_style: @@ -291,7 +291,7 @@ packages: description: name: dart_style sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.8" data_table_2: @@ -299,7 +299,7 @@ packages: description: name: data_table_2 sha256: cb31d465dcf1e1598a662d06d3d2c16c73700ae9e837a3d91588f1dda7519abc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.2" dbus: @@ -307,7 +307,7 @@ packages: description: name: dbus sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.12" decimal: @@ -315,7 +315,7 @@ packages: description: name: decimal sha256: "2c3c8b74f2948066d3f42585477aec9cfc48fefd7a723a4d4274a6c71a5c0df7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.6" dio: @@ -323,7 +323,7 @@ packages: description: name: dio sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.7.0" dio_web_adapter: @@ -331,7 +331,7 @@ packages: description: name: dio_web_adapter sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" drago_pos_printer: @@ -339,7 +339,7 @@ packages: description: name: drago_pos_printer sha256: "8b601221dcf804f662228a6ae68d0c644543b6df0026e40808fc0fbf2c19871c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" drago_usb_printer: @@ -354,7 +354,7 @@ packages: description: name: ducafe_ui_core sha256: "92ff9b05654e1b5ae2adbeac7227c669655da73f134bede75ad9aa1c505af904" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.6" dylib: @@ -362,7 +362,7 @@ packages: description: name: dylib sha256: bf609b3eb6492a3309b3d1dbe8f83a4031de5535dd7686be33487051cc760bb0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.3" equatable: @@ -370,7 +370,7 @@ packages: description: name: equatable sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" event_bus: @@ -378,7 +378,7 @@ packages: description: name: event_bus sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" external_path: @@ -386,7 +386,7 @@ packages: description: name: external_path sha256: "68a18a2aa51ec012d7013ea2a80305dc5372f3577a2bbcc7dcc5550b25a5a73b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" fake_async: @@ -394,7 +394,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: @@ -402,7 +402,7 @@ packages: description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" file: @@ -410,7 +410,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" fixnum: @@ -418,7 +418,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" floor: @@ -426,7 +426,7 @@ packages: description: name: floor sha256: "52a8eac2c8d274e7c0c54251226f59786bb5b749365a2d8537d8095aa5132d92" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.2" floor_annotation: @@ -434,7 +434,7 @@ packages: description: name: floor_annotation sha256: a40949580a7ab0eee572686e2d3b1638fd6bd6a753e661d792ab4236b365b23b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.0" floor_generator: @@ -442,7 +442,7 @@ packages: description: name: floor_generator sha256: "1499b3ab878a807e6fbe6f140dc014124845cd1df3090a113aae5fa7577a1e77" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.0" flutter: @@ -455,7 +455,7 @@ packages: description: name: flutter_cache_manager sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.4.1" flutter_easyloading: @@ -463,7 +463,7 @@ packages: description: name: flutter_easyloading sha256: ba21a3c883544e582f9cc455a4a0907556714e1e9cf0eababfcb600da191d17c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.5" flutter_html: @@ -471,7 +471,7 @@ packages: description: name: flutter_html sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" flutter_libserialport: @@ -479,7 +479,7 @@ packages: description: name: flutter_libserialport sha256: d193b5ac819db6540d20cc8d20d9f5ec9e3396edb4d2c4a23c97863fa9132336 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.0" flutter_lints: @@ -487,7 +487,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" flutter_localizations: @@ -500,7 +500,7 @@ packages: description: name: flutter_spinkit sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.2.2" flutter_subscreen_plugin: @@ -508,7 +508,7 @@ packages: description: name: flutter_subscreen_plugin sha256: b89bc8ba87956d441726557ece140ccea418ca4569e8f06e028e1017740ff6f3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.2+5" flutter_svg: @@ -516,7 +516,7 @@ packages: description: name: flutter_svg sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" flutter_test: @@ -529,7 +529,7 @@ packages: description: name: flutter_tts sha256: baa3cb6b4990318460fe28bfa8c7869399e97223971532c02bd97c5e876aa3c5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.2.2" flutter_web_plugins: @@ -542,7 +542,7 @@ packages: description: name: fluttertoast sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.2.14" forui: @@ -550,7 +550,7 @@ packages: description: name: forui sha256: "3ebbf39d57f0667afc47cbab86b0afe679bba2173a51afc21855053663dc5062" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.1+1" forui_assets: @@ -558,7 +558,7 @@ packages: description: name: forui_assets sha256: df0f24b47ff0c3e16811bb4ad597b426b8e1a62fcf2fea7d95923ef4d6e8aa2c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.1" frontend_server_client: @@ -566,7 +566,7 @@ packages: description: name: frontend_server_client sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.0" gbk_codec: @@ -574,7 +574,7 @@ packages: description: name: gbk_codec sha256: "3af5311fc9393115e3650ae6023862adf998051a804a08fb804f042724999f61" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.0" get: @@ -582,7 +582,7 @@ packages: description: name: get sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.7.3" glob: @@ -590,7 +590,7 @@ packages: description: name: glob sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" google_fonts: @@ -598,7 +598,7 @@ packages: description: name: google_fonts sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.0" graphs: @@ -606,7 +606,7 @@ packages: description: name: graphs sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.2" hex: @@ -614,7 +614,7 @@ packages: description: name: hex sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" html: @@ -622,7 +622,7 @@ packages: description: name: html sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.15.5" http: @@ -630,7 +630,7 @@ packages: description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.6.0" http_multi_server: @@ -638,7 +638,7 @@ packages: description: name: http_multi_server sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" http_parser: @@ -646,7 +646,7 @@ packages: description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.2" icons_launcher: @@ -654,7 +654,7 @@ packages: description: name: icons_launcher sha256: a7c83fbc837dc6f81944ef35c3756f533bb2aba32fcca5cbcdb2dbcd877d5ae9 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" image: @@ -662,7 +662,7 @@ packages: description: name: image sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.5.4" intl: @@ -670,7 +670,7 @@ packages: description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.19.0" io: @@ -678,7 +678,7 @@ packages: description: name: io sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" js: @@ -686,7 +686,7 @@ packages: description: name: js sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.1" json_annotation: @@ -694,7 +694,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.9.0" json_serializable: @@ -702,7 +702,7 @@ packages: description: name: json_serializable sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.8.0" leak_tracker: @@ -710,7 +710,7 @@ packages: description: name: leak_tracker sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.0.7" leak_tracker_flutter_testing: @@ -718,7 +718,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.8" leak_tracker_testing: @@ -726,7 +726,7 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.1" libserialport: @@ -734,7 +734,7 @@ packages: description: name: libserialport sha256: "392e1592def65282429832ec66fa25e9e163d3b37716b97691482e2406720727" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.0+1" lints: @@ -742,7 +742,7 @@ packages: description: name: lints sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.1.1" list_counter: @@ -750,7 +750,7 @@ packages: description: name: list_counter sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" logger: @@ -758,7 +758,7 @@ packages: description: name: logger sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" logging: @@ -766,7 +766,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" macros: @@ -774,7 +774,7 @@ packages: description: name: macros sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.3-main.0" matcher: @@ -782,7 +782,7 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.16+1" material_color_utilities: @@ -790,7 +790,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.11.1" meta: @@ -798,7 +798,7 @@ packages: description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.15.0" mime: @@ -806,7 +806,7 @@ packages: description: name: mime sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.6" mqtt_client: @@ -814,7 +814,7 @@ packages: description: name: mqtt_client sha256: "37aae360fac0b3322cb37267696c1535e1ad2984a13064d877273ffe4b16ca7a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.5.1" network_info_plus: @@ -822,7 +822,7 @@ packages: description: name: network_info_plus sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.4" network_info_plus_platform_interface: @@ -830,7 +830,7 @@ packages: description: name: network_info_plus_platform_interface sha256: "7e7496a8a9d8136859b8881affc613c4a21304afeb6c324bcefc4bd0aff6b94b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" nitrogen_flutter_svg: @@ -838,7 +838,7 @@ packages: description: name: nitrogen_flutter_svg sha256: "103313861b182fd4069bac896519b1ee64cbe1af0023d818f6c300b81c16853c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.0" nitrogen_types: @@ -846,7 +846,7 @@ packages: description: name: nitrogen_types sha256: "5d4ec453aea14e34607bc6021ff1381192e8fcd8541bf103638baa4d013e668f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.0+1" nm: @@ -854,7 +854,7 @@ packages: description: name: nm sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.0" number_pagination: @@ -862,7 +862,7 @@ packages: description: name: number_pagination sha256: "75d3a28616196e7c8df431d0fb7c48e811e462155f4cf3b5b4167b3408421327" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.6" octo_image: @@ -870,7 +870,7 @@ packages: description: name: octo_image sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" package_config: @@ -878,7 +878,7 @@ packages: description: name: package_config sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" package_info_plus: @@ -886,7 +886,7 @@ packages: description: name: package_info_plus sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "8.2.1" package_info_plus_platform_interface: @@ -894,7 +894,7 @@ packages: description: name: package_info_plus_platform_interface sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.1" path: @@ -902,7 +902,7 @@ packages: description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.0" path_parsing: @@ -910,7 +910,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" path_provider: @@ -918,7 +918,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_android: @@ -926,7 +926,7 @@ packages: description: name: path_provider_android sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.17" path_provider_foundation: @@ -934,7 +934,7 @@ packages: description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" path_provider_linux: @@ -942,7 +942,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -950,7 +950,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" path_provider_windows: @@ -958,7 +958,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" pdf: @@ -966,7 +966,7 @@ packages: description: name: pdf sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.11.3" pdf_widget_wrapper: @@ -974,7 +974,7 @@ packages: description: name: pdf_widget_wrapper sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.4" permission_handler: @@ -982,7 +982,7 @@ packages: description: name: permission_handler sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.4.0" permission_handler_android: @@ -990,7 +990,7 @@ packages: description: name: permission_handler_android sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "12.1.0" permission_handler_apple: @@ -998,7 +998,7 @@ packages: description: name: permission_handler_apple sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "9.6.1" permission_handler_html: @@ -1006,7 +1006,7 @@ packages: description: name: permission_handler_html sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.4+1" permission_handler_platform_interface: @@ -1014,7 +1014,7 @@ packages: description: name: permission_handler_platform_interface sha256: ed86a61c190258fdd65de395ea0632822e3415c1faec38eae0c31b479c28a531 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.4.1" permission_handler_windows: @@ -1022,7 +1022,7 @@ packages: description: name: permission_handler_windows sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.2" petitparser: @@ -1030,7 +1030,7 @@ packages: description: name: petitparser sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.2" pinyin: @@ -1038,7 +1038,7 @@ packages: description: name: pinyin sha256: "240f271a3c71af20c8d2757756b5ee8e9d79a955d37abad4b3568fd406b22411" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.3.0" platform: @@ -1046,7 +1046,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.6" plugin_platform_interface: @@ -1054,7 +1054,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" pointycastle: @@ -1062,7 +1062,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.9.1" pool: @@ -1070,7 +1070,7 @@ packages: description: name: pool sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.3" posix: @@ -1078,7 +1078,7 @@ packages: description: name: posix sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.5.2" printing: @@ -1086,7 +1086,7 @@ packages: description: name: printing sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.14.3" pub_semver: @@ -1094,7 +1094,7 @@ packages: description: name: pub_semver sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.1" pubspec_parse: @@ -1102,7 +1102,7 @@ packages: description: name: pubspec_parse sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.0" qr: @@ -1110,7 +1110,7 @@ packages: description: name: qr sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" qr_flutter: @@ -1118,7 +1118,7 @@ packages: description: name: qr_flutter sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.0" rational: @@ -1126,7 +1126,7 @@ packages: description: name: rational sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.3" rxdart: @@ -1134,7 +1134,7 @@ packages: description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.28.0" shelf: @@ -1142,7 +1142,7 @@ packages: description: name: shelf sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.2" shelf_web_socket: @@ -1150,7 +1150,7 @@ packages: description: name: shelf_web_socket sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" simple_sparse_list: @@ -1158,7 +1158,7 @@ packages: description: name: simple_sparse_list sha256: aa648fd240fa39b49dcd11c19c266990006006de6699a412de485695910fbc1f - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.4" sky_engine: @@ -1171,7 +1171,7 @@ packages: description: name: source_gen sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.5.0" source_helper: @@ -1179,7 +1179,7 @@ packages: description: name: source_helper sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.5" source_span: @@ -1187,7 +1187,7 @@ packages: description: name: source_span sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.0" sqflite: @@ -1195,7 +1195,7 @@ packages: description: name: sqflite sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" sqflite_android: @@ -1203,7 +1203,7 @@ packages: description: name: sqflite_android sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" sqflite_common: @@ -1211,7 +1211,7 @@ packages: description: name: sqflite_common sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.4+6" sqflite_common_ffi: @@ -1219,7 +1219,7 @@ packages: description: name: sqflite_common_ffi sha256: "883dd810b2b49e6e8c3b980df1829ef550a94e3f87deab5d864917d27ca6bf36" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.4+4" sqflite_darwin: @@ -1227,7 +1227,7 @@ packages: description: name: sqflite_darwin sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1+1" sqflite_platform_interface: @@ -1235,7 +1235,7 @@ packages: description: name: sqflite_platform_interface sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.0" sqlite3: @@ -1243,7 +1243,7 @@ packages: description: name: sqlite3 sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.9.4" sqlparser: @@ -1251,7 +1251,7 @@ packages: description: name: sqlparser sha256: "91f47610aa54d8abf9d795a7b4e49b2a788f65d7493d5a68fbf180c3cbcc6f38" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.27.0" stack_trace: @@ -1259,7 +1259,7 @@ packages: description: name: stack_trace sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.0" stream_channel: @@ -1267,7 +1267,7 @@ packages: description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" stream_transform: @@ -1275,7 +1275,7 @@ packages: description: name: stream_transform sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" string_scanner: @@ -1283,7 +1283,7 @@ packages: description: name: string_scanner sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" strings: @@ -1291,7 +1291,7 @@ packages: description: name: strings sha256: "482f1511d2cd8ab9f2c4cc148e6837be8a00c03b9a8eaedbc981a84d9c6305d8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.0" sugar: @@ -1299,7 +1299,7 @@ packages: description: name: sugar sha256: a0a97f1d3552a3ef4cbec7c6e62c7b52c151cd5c11e984e8311700a918f73dc5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.0.0" syncfusion_flutter_core: @@ -1307,7 +1307,7 @@ packages: description: name: syncfusion_flutter_core sha256: f1d2b52697543e13bdefdc62d15868124a265987577f53224a7dbe176c8448f0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "28.2.12" syncfusion_flutter_datepicker: @@ -1315,7 +1315,7 @@ packages: description: name: syncfusion_flutter_datepicker sha256: cfc91ebacee63b2c5220e541736f8df211d4f0bfbf34265778862ae20faf094f - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "28.2.12" synchronized: @@ -1323,7 +1323,7 @@ packages: description: name: synchronized sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.3.0+3" term_glyph: @@ -1331,7 +1331,7 @@ packages: description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: @@ -1339,7 +1339,7 @@ packages: description: name: test_api sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.3" timing: @@ -1347,7 +1347,7 @@ packages: description: name: timing sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" typed_data: @@ -1355,7 +1355,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" unicode: @@ -1363,7 +1363,7 @@ packages: description: name: unicode sha256: a6f7bcfc8ea1d5ce1f6c0b1c39117a9919f4953edd9fd7a64090a9796c499b57 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.9" universal_html: @@ -1371,7 +1371,7 @@ packages: description: name: universal_html sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" universal_io: @@ -1379,7 +1379,7 @@ packages: description: name: universal_io sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.1" uuid: @@ -1387,7 +1387,7 @@ packages: description: name: uuid sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.6.0" vector_graphics: @@ -1395,7 +1395,7 @@ packages: description: name: vector_graphics sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.19" vector_graphics_codec: @@ -1403,7 +1403,7 @@ packages: description: name: vector_graphics_codec sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.13" vector_graphics_compiler: @@ -1411,7 +1411,7 @@ packages: description: name: vector_graphics_compiler sha256: ca81fdfaf62a5ab45d7296614aea108d2c7d0efca8393e96174bf4d51e6725b0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.18" vector_math: @@ -1419,7 +1419,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" vm_service: @@ -1427,7 +1427,7 @@ packages: description: name: vm_service sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "14.3.0" watcher: @@ -1435,7 +1435,7 @@ packages: description: name: watcher sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" web: @@ -1443,7 +1443,7 @@ packages: description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" web_socket: @@ -1451,7 +1451,7 @@ packages: description: name: web_socket sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" web_socket_channel: @@ -1459,7 +1459,7 @@ packages: description: name: web_socket_channel sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.3" win32: @@ -1467,7 +1467,7 @@ packages: description: name: win32 sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.10.1" xdg_directories: @@ -1475,7 +1475,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" xml: @@ -1483,7 +1483,7 @@ packages: description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.5.0" yaml: @@ -1491,7 +1491,7 @@ packages: description: name: yaml sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.4" sdks: diff --git a/test/widgets/app_soft_keyboard_test.dart b/test/widgets/app_soft_keyboard_test.dart new file mode 100644 index 0000000..093bea4 --- /dev/null +++ b/test/widgets/app_soft_keyboard_test.dart @@ -0,0 +1,162 @@ +import 'package:ducafe_ui_core/ducafe_ui_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:forui/forui.dart'; + +import 'package:pj_supermarket_cloud/common/index.dart'; + +/// 自研软键盘组件的渲染与输入联动测试。 +void main() { + Widget wrapHost(Widget Function(BuildContext context) child) => + ScreenUtilInit( + designSize: const Size(1920, 1080), + minTextAdapt: true, + splitScreenMode: true, + builder: (_, __) => MaterialApp( + home: FTheme( + data: FThemes.zinc.light, + child: Builder(builder: child), + ), + ), + ); + + testWidgets('弹出后渲染面板并可输入目标控制器', (tester) async { + final controller = TextEditingController(); + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + wrapHost( + (context) => Scaffold( + body: Center( + child: FButton( + onPress: () => AppSoftKeyboard.show( + context, + controller: controller, + mode: AppSoftKeyboardMode.digits, + ), + label: const Text('打开键盘'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('打开键盘')); + await tester.pumpAndSettle(); + + // 面板、tab 与确认键都应渲染。 + expect(find.text('确认'), findsOneWidget); + + // 键盘应停靠在屏幕下半部(底部 16h + 面板高度内)。 + final confirmRect = tester.getRect(find.text('确认')); + expect(confirmRect.bottom, lessThan(1080)); + expect(confirmRect.bottom, greaterThan(1080 * 0.5)); + + // 点击数字 1 与 5 后字符进入目标控制器。 + await tester.tap(find.text('1')); + await tester.pump(); + await tester.tap(find.text('5')); + await tester.pump(); + expect(controller.text, '15'); + }); + + testWidgets('在 AppDialog 弹框内弹出键盘仍然渲染', (tester) async { + final passwordController = TextEditingController(); + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + wrapHost( + (context) => Scaffold( + body: Center( + child: FButton( + onPress: () => showAppDialog( + context: context, + barrierDismissible: false, + canPop: false, + builder: (dialogContext) => AppDialog( + title: const Text('终端操作验证'), + width: 680, + maxWidth: 680, + content: FTextField( + controller: passwordController, + keyboardType: TextInputType.none, + onTap: () => AppSoftKeyboard.show( + dialogContext, + controller: passwordController, + mode: AppSoftKeyboardMode.digits, + ), + ), + actions: [ + FButton( + style: FButtonStyle.outline, + label: const Text('取消'), + onPress: () => Navigator.of(dialogContext).pop(), + ), + ], + ), + ), + label: const Text('打开验证弹框'), + ), + ), + ), + ), + ); + + await tester.tap(find.text('打开验证弹框')); + await tester.pumpAndSettle(); + + // 弹框内的输入框应可见。 + expect(find.text('终端操作验证'), findsOneWidget); + + await tester.tap(find.byType(TextField).first); + await tester.pumpAndSettle(); + + // 键盘面板在弹框之上仍应渲染。 + expect(find.text('确认'), findsOneWidget); + }); + + testWidgets('内嵌面板在弹框内渲染并输入', (tester) async { + final controller = TextEditingController(); + tester.view.physicalSize = const Size(1920, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + await tester.pumpWidget( + wrapHost( + (context) => Scaffold( + body: Center( + child: AppSoftKeyboardPanel( + controller: controller, + mode: AppSoftKeyboardMode.digits, + allowPattern: RegExp('[0-9]'), + maxLength: 12, + ).tight(width: 624), + ), + ), + ), + ); + + // 面板切换 tab 与数字键应渲染;内嵌模式没有确认键。 + expect(find.text('123'), findsOneWidget); + expect(find.text('确认'), findsNothing); + + await tester.tap(find.text('2')); + await tester.pump(); + await tester.tap(find.text('5')); + await tester.pump(); + await tester.tap(find.text('0')); + await tester.pump(); + await tester.tap(find.text('3')); + await tester.pump(); + expect(controller.text, '2503'); + + // 清空键一键清空。 + await tester.tap(find.text('清空')); + await tester.pump(); + expect(controller.text, ''); + }); +}