ParcelDesk 包裹核销输入防重与状态一致性技术封面

一个包裹的条码被连续扫描两次,界面上的“已核销数量”是否应该增加两次?答案显然是否定的。但在实际终端上,同一段文本既可能来自人工粘贴,也可能来自外接扫描枪构造的键盘输入;如果把 TextInput.onChange() 直接等同于一次“核销”,输入 PKG-2402 的每个字符都有机会触发业务处理,计数和错误消息很快就会混乱。

这篇文章独立构建一个小型 ParcelDesk 包裹核销页,不接摄像头权限、不假定有硬件扫描 SDK,只处理已经进入输入框的文本。工程目标是:明确何时才提交、拒绝格式错误的编号、不接受清单之外的包裹、同一编号重复提交只能有一次状态变化,并让列表、进度和提示信息保持一致。项目中的三个示例包裹码分别为 PKG-2401、PKG-2402、PKG-2403;初始只有第一个已核销。本地示例不代表仓库真实出库记录,但状态机的边界与实际业务一致。

事件层不要代替业务层做决定

输入控件有两个常用事件:onChange 告诉页面文本发生变化,onSubmit 告诉页面用户触发输入法的提交动作。它们不能混为一谈。onChange 应只维护输入缓冲区并适时清理旧提示;真正的业务校验只在 onSubmit 或“确认核销”按钮触发时执行。这也解释了为什么扫描枪如果只是构造键盘,必须先确认其是否发送末尾 Enter;不发送时用户仍可点按钮,而不能把每次输入变化当成完成扫描。

将校验规则写成固定的业务约束,而不是散落在控件回调里。本次示例接受 PKG- 加四位数字,例如 PKG-2402。为兼容人工输入,可去掉字符串两端空白,并统一转大写;中间插入空格则按错误处理。这样 pkg-2402 会被规范化为正确编号,而 PKG-24 02 不会被悄悄改写成另一个有效码。

interface PackageRecord {
  code: string
  recipient: string
  verified: boolean
}

function normalizeCode(raw: string): string {
  return raw.trim().toUpperCase()
}

function validCode(code: string): boolean {
  return /^PKG-[0-9]{4}$/.test(code)
}

normalizeCode() 不应顺手执行过度修复,例如补齐缺少的位数、替换相似字符 O/0,或移除中间的非预期符号。这些动作容易把一个非法条码变成另一个真实存在的包裹。对真实物流单据还应考虑校验位、批次规则、作业仓库和数据来源,但它们属于业务协议,不是 HarmonyOS 文本框替开发者决定的事情。

DevEco Studio 白色主题中 ParcelDesk 核销页面与 ArkTS 输入事件处理

把幂等性落实在记录上,不依赖按钮动画

所谓核销幂等,不是“按钮点完后变灰三秒”,而是同一个包裹编号在同一作业范围内,无论提交多少次,都不会被重复记账。本地示例用包裹码作为记录键,针对 verified 状态执行检查;已核销就只反馈重复,不再次增加计数。由于它是示例清单,数据在页面重建时恢复初始值。若要跨设备、跨班次实现真正的业务幂等,必须交给服务端的唯一约束或幂等令牌完成,客户端只能优化体验,不能替代后端判重。

页面采用 @State 保存可变记录数组和提示。进度不是单独维护的计数器,而是每次从记录派生。初始记录中 PKG-2401 已核销,其余两条待核销,因此初始进度是1/3。用户输入 PKG-2402 并提交后,应变成2/3;重复提交 PKG-2402 时仍是2/3,不能凭一次事件多算一个。

@Entry
@Component
struct ParcelPage {
  @State private inputCode: string = ''
  @State private feedback: string = '等待录入包裹码'
  @State private records: PackageRecord[] = [
    { code: 'PKG-2401', recipient: '海棠路站点', verified: true },
    { code: 'PKG-2402', recipient: '文昌街站点', verified: false },
    { code: 'PKG-2403', recipient: '云杉路站点', verified: false }
  ]

  get verifiedCount(): number {
    return this.records.filter((item: PackageRecord) => item.verified).length
  }
  // submitCode() 与 build() 见后文
}

使用 @State 数组时,本例不依赖深层对象修改的隐式观察能力,而是通过 map() 创建新的数组和被改变的记录对象。这样列表依赖的顶层状态发生替换,通知路径更容易观察;如果后来把记录拆成复杂的子组件对象,需要结合组件输入和 @Observed / @ObjectLink 的语义决定状态传递方式,不能把所有嵌套属性修改都当成天然会刷新。

核心提交函数可以独立理解,它没有异步网络调用,也不假装持有仓库后端接口。真正接入时应让服务端确认后再把 verified 改为 true,请求失败时保留待核销状态。

private submitCode(): void {
  const code: string = normalizeCode(this.inputCode)
  if (!validCode(code)) {
    this.feedback = '编号格式应为 PKG- 加四位数字'
    return
  }
  const index: number = this.records.findIndex(
    (item: PackageRecord) => item.code === code)
  if (index < 0) {
    this.feedback = `未在当前清单找到 ${code}`
    return
  }
  if (this.records[index].verified) {
    this.feedback = `${code} 已核销,请勿重复提交`
    this.inputCode = ''
    return
  }
  this.records = this.records.map((item: PackageRecord) =>
    item.code === code ? { ...item, verified: true } : item)
  this.feedback = `核销成功:${code}`
  this.inputCode = ''
}

这里保留输入清洗后的值只用于判断,不直接改写所有包裹 ID。找不到记录不是“核销失败再试一次”那么简单:它可能属于其他作业批次、出现拼写错误或尚未同步清单,因此界面要清楚说是“当前清单未找到”,不要暗示远端的真实库存状态。对于格式失败保留原输入,便于人工修正;对于已核销和成功状态清空输入,便于下一次录入。

UI 不需要复杂,但每一处反馈都有明确的来源

页面的交互区在上,清单在下,进度紧贴标题。重点不是使用多少组件,而是让单据状态一眼能看懂。TextInput 的 text 绑定输入状态,.onChange() 只负责缓冲;onSubmit() 与按钮最终走同一个 submitCode(),不会出现“键盘确认走一套规则、按钮又走另一套规则”的分叉。ForEach 使用稳定的 code 作为键,而不是数组索引,这样状态改变时不会把错误样式复用到另一条记录上。

build() {
  Column({ space: 18 }) {
    Text('包裹核销').fontSize(28).fontWeight(FontWeight.Bold)
    Text(`已核销 ${this.verifiedCount}/${this.records.length}`)
      .fontSize(18).fontColor('#285CC2')
    TextInput({ text: this.inputCode, placeholder: '输入或扫描包裹码' })
      .onChange((value: string) => { this.inputCode = value })
      .onSubmit(() => { this.submitCode() })
    Button('确认核销').width('100%')
      .onClick(() => { this.submitCode() })
    Text(this.feedback).fontSize(14).fontColor('#3D6FB7')
    ForEach(this.records, (item: PackageRecord) => {
      Row() {
        Column({ space: 4 }) {
          Text(item.code).fontSize(18).fontWeight(FontWeight.Medium)
          Text(item.recipient).fontSize(13).fontColor('#7A8596')
        }
        Blank()
        Text(item.verified ? '已核销' : '待核销')
          .fontColor(item.verified ? '#17836B' : '#A46C22')
      }
      .padding(15).backgroundColor(Color.White).borderRadius(14)
      .width('100%')
    }, (item: PackageRecord) => item.code)
  }
  .padding(24).width('100%').height('100%')
  .backgroundColor('#F5F8FC')
}

开发时应该在 Stage 模型的 entry/src/main/ets/pages/ParcelPage.ets 放置此页面,将文件作为工程的入口页面或在现有导航中注册;不要在同一工程同时启用多个无关的 @Entry 页面后,又期待它们都在首页出现。示例只使用系统内建的 ArkUI 基础组件,不依赖第三方库,也不需要相机、蓝牙或网络权限。需要注意具体扫码枪能否触发输入焦点、是否自动回车,属于硬件与输入法组合的兼容性测试,不能从 TextInput 接口本身推断。

ParcelDesk 包裹核销成功后的完整竖屏应用界面

图中的界面以“刚刚输入 PKG-2402 并核销”为状态:顶部显示“已核销 2/3”,PKG-2401 与 PKG-2402 标为已核销,PKG-2403 仍待核销,反馈文案为“核销成功:PKG-2402”。输入框此时应为空,符合成功后清空的代码。图中不出现摄像头取景、联网成功、仓储设备配对等未实现能力;示意的是屏幕交互布局,而不是已接通真实仓库系统。

从“重复提交”扩展为可检查的故障矩阵

只测一次成功并不足以发现输入事件的陷阱。建议在 DevEco Studio 的预览或可用测试设备上逐一执行以下操作,并观察 records、feedback 与 verifiedCount,不要仅凭某张绿色提示卡片判定逻辑没有问题。

输入与动作当前状态预期反馈及数量
pkg-2402 后回车2402待核销转为已核销,2/3
再次输入 PKG-24022402已核销提示已核销,保持2/3
PKG-24 03 点击按钮编号不合规显示格式错误,保持2/3
PKG-9999 后回车清单中不存在提示未找到,保持2/3
PKG-2403 点击按钮2403待核销转为已核销,3/3
空输入按确认无有效编号拒绝提交,数量不变

其中最值得仔细测试的是“扫码枪一次写入多次触发 onChange”的情况。只要业务处理没有放到 onChange,字符逐个进入就不会改变清单;只有提交动作才触发校验。若实际设备一次扫描可能同时发出回车和某个独立的硬件回调,产品层就要决定只采用一个提交入口;即使意外进入同一个 submitCode() 两次,也应该由 verified 门禁阻止重复计数。不过本地门禁无法保证两个进程同时核销时的跨设备原子性。

输入缓冲、格式校验、记录幂等更新与反馈的状态流程

另一项容易遗漏的行为是多次切换输入焦点。用户可能扫描半截后切出页面,又重新返回;此时未提交的输入只是草稿,不该影响核销进度。若产品希望恢复草稿,需要单独保存输入缓冲并在回到页面时提醒继续编辑,而不是自动调用提交函数。对于展示中的“已核销 2/3”,分母来自当前作业清单,如果切换批次应整体替换清单和批次标识,避免把不同批次的记录混在同一组统计中。

真实线上系统还有三项不能省略的改造。其一,服务器确认:请求应携带操作员、作业批次、包裹唯一编号与幂等键,客户端等待后端确认再变更最终状态。其二,失败重试:网络异常不能直接显示“核销成功”,也不能因为 UI 超时就假设服务端没有执行;可以用同一个幂等键查询或重试。其三,审计与权限:包裹可能关联收件人隐私,实际展示应遵循最小权限原则,日志不能无条件记录完整个人身份信息。这里的“海棠路站点”等是纯示例地点标签,并无个人资料。

如果引入离线队列,必须区分“待上传”“服务器已确认”“需人工处理”三个业务态,不要把离线缓存写入成功等同于出库成功。本篇特意使用最小的 boolean verified,是为了把输入提交和幂等性讲清楚,而不是鼓励所有仓储项目只用一个布尔值解决事务。页面的记录源将来可以替换成带版本和服务器确认状态的数据结构,表单入口与反馈逻辑则无需推倒重写。

这套方案适用于键盘式扫描、人工输入和受控本地核验原型。它的价值不在于花哨的扫描动效,而在于划清三个边界:文本改变不是提交,重复提交不是重复业务,客户端反馈不是服务器事实。只要这三条规则始终成立,后续添加真实扫码硬件、同步接口或批次切换时,系统仍然有可靠的错误定位起点。

参考:TextInput 单行输入框与 onSubmit 事件、ArkUI 状态管理总览、状态对象的嵌套观察机制。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐