ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序连续扫码:camera组件与wx.scanCode降级实践

微信小程序连续扫码:camera组件与wx.scanCode降级实践 1. 连续扫码的需求拆解与方案选型1.1 单次扫码和连续扫码的体验差在哪微信小程序里做扫码很多人第一反应就是wx.scanCode。这个 API 确实简单一行代码就能拉起原生扫码界面扫完拿到结果然后写业务逻辑。问题是它本质上是“一次调用换一次结果”。你扫完一个码原生扫码界面关闭回到小程序页面想再扫下一个就得再点一次按钮再调一次wx.scanCode。这种模式放在低频场景里没问题比如扫一张门票、扫一个付款码、扫一个设备编号用户等一两秒完全能接受。但连续扫码的需求完全不同。仓库盘点时操作员可能要在半小时内扫几百个货架标签门店核销时店员要连续扫几十张券码快递入库时快递员要一边走一边扫手不能停。这个时候如果每扫一次都弹一次原生界面、等一次加载、点一次按钮效率直接砍半用户会边用边骂。连续扫码要的是“页面不跳、相机不关、结果一条接一条进列表”中间最好只有震动和短提示音而不是反复弹窗。所以判断能不能用wx.scanCode关键看两个指标扫码频率和操作连续性。如果一分钟只扫一两次wx.scanCode足够稳如果一分钟要扫十次以上或者操作员需要单手连续操作那就必须换成页面内持续识别的方案。这也是微信小程序里连续扫码和单次扫码最大的分水岭。1.2 wx.scanCode 的边界能用但不适合高频wx.scanCode的优势是兼容性好、调用简单、不需要自己处理相机权限和画面。它支持二维码、一维码、Data Matrix、PDF417 等类型返回结果也直接。很多小程序项目一开始就是用它顶上的。但它的缺点在高频场景里会被放大。第一每次调用都会拉起原生扫码界面这个界面会覆盖当前页面。用户扫完一个码界面关闭再调一次界面又打开。视觉上闪来闪去操作节奏被打断。第二原生扫码界面通常有自己的提示和交互你很难在里面加“已扫 23 条”“上一码是什么”这类业务信息。第三连续调用需要递归封装如果用户点了取消递归要停下来否则会反复弹。第四部分 Android 机型在频繁调用相机时会出现启动慢、黑屏、对焦慢的问题。我试过在盘点类项目里用wx.scanCode做连续扫码做法是在success回调里再调一次wx.scanCode形成递归。代码能跑但体验只能算“能用”。扫到第十几个的时候操作员会明显感觉每次都要等界面切换。后来换成camera组件之后扫码速度才真正提上来。所以我的经验是wx.scanCode适合低频、单次、对体验要求不高的场景高频连续扫码把它当降级方案就好。1.3 camera 组件 modescanCode 为什么是主线方案微信小程序提供了camera组件它可以直接在页面里显示相机画面。从基础库 2.7.0 开始camera组件支持modescanCode开启扫码模式后相机画面会持续识别二维码和条形码识别到内容就触发bindscancode事件。这个事件不是只触发一次而是可以连续触发这才是连续扫码的核心。它的工作方式可以这样理解页面里放一个camera组件用户把相机对准码组件在后台不断取帧、识别一旦识别成功就把结果抛给你的 JS 逻辑。你拿到结果后可以做震动、录音效、写入列表、调用接口然后继续等下一个码。整个过程都在当前页面内完成没有原生扫码界面的覆盖和关闭操作员可以一直拿着手机扫节奏非常顺。当然camera组件也不是没有代价。它是原生组件层级较高普通view盖不上去叠加 UI 要用cover-view开发者工具对它的模拟有限必须真机调试权限被拒绝后处理起来比wx.scanCode麻烦长时间开启相机还会发热、耗电。但这些代价换来的是连续扫码体验的质变。对于盘点、核销、入库这类场景我觉得这笔账非常划算。1.4 几个典型场景盘点、核销、入库、巡检连续扫码不是一个孤立功能它背后对应的是几类很具体的业务。第一类是仓库盘点操作员拿着手机或 PDA沿着货架逐个扫商品条码扫完一批后统一提交。这个场景要求扫码快、去重准、能看已扫数量最好还能导出结果。第二类是门店核销顾客出示券码店员连续扫码核销要求即时反馈、防止同一张券重复核销。第三类是快递入库快递员扫运单号连续扫几十上百个要求列表不卡、网络断了也能先存本地。第四类是设备巡检巡检人员扫设备二维码记录巡检时间和设备编号要求能识别重复设备并提示。这些场景有一个共同点扫码只是入口真正的重头戏是结果管理。你不能扫完就完了还要去重、清洗、校验、记录时间、批量提交、处理失败。所以连续扫码的实现不能只盯着camera组件还要把后面的数据流一起设计好。否则扫码很快列表一长就卡死或者同一条码扫了五次全进列表业务方照样不满意。2. 开发前的准备基础库、权限、页面布局2.1 基础库版本和真机调试的最低要求camera组件的modescanCode和bindscancode事件依赖基础库版本。我一般会把最低基础库设到 2.7.0 以上实际项目里建议更高一些因为一些机型兼容性和同层渲染优化在高版本更好。你可以在微信开发者工具的“详情”里设置调试基础库也可以在app.json里通过requiredBackgroundModes之类的配置影响运行环境但更关键的是在发布时选择合适的最低基础库。这里有一个很容易踩的坑开发者工具里 camera 组件能显示画面但扫码回调不一定和真机一致。我遇到过在工具里怎么扫都不触发bindscancode换到真机立刻正常也遇到过工具里能触发但 Android 真机上手电筒不生效。所以我的习惯是页面骨架可以在工具里写但扫码回调、权限弹窗、震动、音效、手电筒、相机切换这些全部用真机调试。iOS 和 Android 各准备一台至少各测一轮。另外camera组件在部分旧机型上启动较慢。你可以加一个 loading 状态相机binderror触发时给出提示不要让用户对着黑屏发呆。如果项目必须覆盖很低的基础库那就准备降级方案检测wx.canIUse或基础库版本不支持camera扫码模式时退回wx.scanCode递归调用。2.2 摄像头权限、隐私声明和审核口径摄像头权限是连续扫码的第一道门槛。camera组件需要scope.camera权限。用户第一次进入页面时小程序会弹出授权窗口如果用户拒绝后续再调wx.authorize通常不会再次弹出必须引导用户去设置页开启。我的处理方式是先用wx.getSetting查权限状态如果是undefined调wx.authorize如果是false弹一个说明弹窗用户点确认后调wx.openSetting。不要一上来就反复弹授权用户会很烦。在app.json里建议声明摄像头用途{ permission: { scope.camera: { desc: 用于连续扫描二维码和条形码完成盘点、核销等操作 } } }这个描述要写得具体不要只写“需要使用摄像头”。审核时小程序后台的用户隐私保护指引里也要声明摄像头用途说明收集的是扫码画面用于识别二维码/条形码。页面里最好也有明确提示比如“请将二维码放入框内扫描结果仅用于本次业务”。不要隐藏相机用途也不要在用户不知情的情况下采集画面。合规这件事前期多写几行说明后期能省掉很多审核沟通。注意如果用户拒绝摄像头权限页面不能只留一个黑屏。要给出“去设置开启权限”的按钮并且提供wx.scanCode降级入口让业务还能继续。2.3 页面布局相机区、状态区、结果区怎么放连续扫码页面的布局核心原则是相机区域要够大操作按钮要顺手结果列表不能挤占扫码区。我一般会把页面分成三块上方是相机区占屏幕高度的 45% 到 60%中间是状态和工具栏显示“已扫多少条”“当前是否暂停”“手电筒开关”下方是结果列表可以滚动查看最近扫到的码。相机区直接放camera组件设置device-positionback、modescanCode、flash{{flash}}。如果需要叠加扫码框或提示文字要用cover-view因为camera是原生组件普通view盖不上去。但cover-view的样式能力有限圆角、阴影、复杂动画支持不好所以我的建议是扫码框尽量简单用四个角或者一条横线就够别在相机上堆太多 UI。结果列表放在相机下方用普通scroll-view就行不受原生组件层级影响。按钮方面暂停/继续、手电筒、清空、批量提交这几个最常用。暂停按钮很重要连续扫码时用户可能需要停下来整理货物如果相机一直识别容易误扫。手电筒在仓库暗处非常实用camera的flash设为torch就能常亮但部分 Android 机型不支持需要做兼容提示。批量提交按钮放在底部固定显示已扫数量防止用户扫了一堆忘了提交。3. 核心实现连续扫码回调、节流去重、数据入库3.1 camera 组件的关键属性与 bindscancode 回调camera组件有几个关键属性必须搞清楚。mode设为scanCode才会开启扫码模式device-position控制前/后置摄像头连续扫码一般用backflash控制闪光灯取值有auto、on、off、torch手电筒常亮用torchbindscancode是扫码识别回调识别成功后会触发事件对象里包含detail.result、detail.scanType、detail.charSet、detail.path、detail.rawData等字段。最常用的是detail.result也就是扫码内容的字符串。detail.scanType可以告诉你扫到的是二维码还是条形码如果业务需要区分码类型可以用这个字段做判断。detail.rawData是原始数据一般用不到。回调触发频率和相机帧率、码的清晰度有关同一个码在画面里停留时可能会连续触发多次。如果你不处理列表里会瞬间多出好几条重复记录。所以拿到bindscancode之后第一件事不是急着入库而是节流和去重。提示camera组件在页面隐藏时通常会停止相机但不同机型表现有差异。保险起见在onHide里把扫码状态置为暂停回到页面再恢复。3.2 防重复触发全局节流与同一码去重连续扫码最核心的细节就是防重复。我一般会做两层过滤。第一层是全局节流两次扫码回调之间至少间隔 200 到 300 毫秒。因为相机识别是连续取帧同一个码可能在极短时间内触发多次回调加上一个全局锁可以避免同一帧里连续处理。第二层是同一码去重如果本次扫到的内容和上一次相同并且距离上次时间小于 1500 毫秒就直接忽略。这个时间窗可以根据业务调整仓库盘点通常 1.5 秒够用如果用户可能故意连续扫同一个码比如同一商品多件那就不能按内容去重而要改成计数模式。实现时不要把这些状态放在data里频繁setData因为setData是异步的高频调用会卡而且可能拿到旧值。我习惯把_lastScan、_scanLock挂在页面实例上作为普通属性使用。比如this._lastScan { code: , time: 0 }; this._scanLock false;在回调里判断if (this._scanLock) return; if (now - this._lastScan.time 260) return; if (code this._lastScan.code now - this._lastScan.time 1500) return;然后立刻设置锁和时间再处理业务。处理完 260 毫秒后释放锁。这样既不会漏掉不同码也不会让同一个码疯狂入列表。实测下来这个参数在大部分 Android 和 iOS 真机上都比较稳。3.3 结果清洗、业务校验和重复码处理扫码结果拿到后不能直接信任。有些条码会带换行、空格、不可见字符有些二维码内容前后有空白直接入库可能导致后续查询对不上。我一般会先做清洗code.trim()再把中间的空格和不可见字符去掉比如code.replace(/[\s\u0000-\u001f]/g, )。如果业务条码有固定前缀比如P-、SKU-可以用正则去掉前缀但一定要确认所有码都有这个规则否则会误伤。业务校验也很重要。比如核销场景扫码结果可能是一个券码你需要判断它是否符合长度和格式是否已经核销过。盘点场景扫码结果可能要在本地商品库里匹配匹配不到的要单独标记。我的做法是扫码回调里只做轻量校验比如非空、长度范围、正则格式重业务校验放到批量提交时由后端处理。这样扫码过程不会被网络请求打断体验更顺。重复码的处理要看业务。如果业务要求“同一条码只能出现一次”那就用Set维护已扫集合重复时震动提示“已扫过”但不写入列表。如果业务要求“同一条码可以扫多次但记录次数”那就用Map统计列表里显示“数量 1”。我见过一个项目仓库盘点时同一箱货有多个相同条码操作员需要扫多次结果开发按去重做了导致数量永远不对。所以去重策略一定要和业务方确认清楚不能想当然。3.4 数据列表与批量提交的性能控制连续扫码扫到几十条以后列表渲染压力会明显上升。如果你每扫一条就this.setData({ records: 新数组 })列表越长setData传输的数据量越大页面会越来越卡。我的优化习惯是内存里维护一个完整数组this._records页面只渲染最近 100 条每次setData只更新这 100 条。如果业务需要查看全部就分页加载或者跳转到独立的结果页。批量提交也有讲究。不要每扫一条就调一次接口那样网络请求太多也容易因为弱网导致失败。正确的做法是本地先攒着用户点“批量提交”时一次性发送。接口设计上建议加一个batchNo和timestamp后端做幂等处理避免用户重复点击提交导致重复入库。提交成功后清空本地列表提交失败要保留数据并给出重试入口。注意如果扫码结果要用来换取业务凭证比如登录 token、核销 token务必通过你自己的后端接口完成小程序端只拿短期票据不要在小程序代码里硬编码任何密钥。这是安全底线。4. 从零写一个连续扫码页面完整实操4.1 app.json 权限与页面配置先在app.json里声明页面和摄像头权限。页面路径按你的项目结构来权限描述写清楚用途{ pages: [ pages/scan/scan ], permission: { scope.camera: { desc: 用于连续扫描二维码和条形码完成盘点、核销等操作 } }, window: { navigationBarTitleText: 连续扫码 } }如果项目使用了较新的隐私协议流程还需要在小程序后台配置用户隐私保护指引声明摄像头用途。代码里处理权限时先查wx.getSetting再决定是调wx.authorize还是wx.openSetting。不要在onLoad里直接调wx.authorize因为用户可能已经拒绝过直接调不会弹窗还会进fail。4.2 WXML 与 WXSS 骨架WXML 结构尽量简单相机区、工具栏、结果列表、底部提交栏。相机上的提示用cover-view按钮放在相机外面避免原生组件层级问题。view classscan-page view classcamera-wrap camera classcamera device-positionback flash{{flash}} modescanCode bindscancodehandleScanCode binderrorhandleCameraError /camera cover-view classscan-tip{{tipText}}/cover-view /view view classtoolbar button sizemini bindtaptoggleFlash{{flash off ? 开灯 : 关灯}}/button button sizemini bindtaptoggleScan{{scanning ? 暂停 : 继续}}/button button sizemini bindtapclearList清空/button /view scroll-view scroll-y classresult-list view wx:for{{records}} wx:keyid classrecord-item text classcode{{item.code}}/text text classtime{{item.timeText}}/text /view /scroll-view view classfooter text已扫 {{recordCount}} 条/text button sizemini typeprimary bindtapsubmitBatch loading{{submitting}}批量提交/button /view /viewWXSS 方面camera宽度给 100%高度可以用50vh或固定像素。结果列表设置flex: 1让页面撑满屏幕。cover-view的文字要设置颜色和背景保证在相机画面上看得清。按钮区域用flex横向排列底部提交栏固定在底部避免被列表顶下去。.scan-page { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; } .camera-wrap { position: relative; width: 100%; height: 50vh; background: #000; } .camera { width: 100%; height: 100%; } .scan-tip { position: absolute; left: 0; right: 0; bottom: 20rpx; text-align: center; color: #fff; font-size: 28rpx; background: rgba(0, 0, 0, 0.45); padding: 12rpx 0; } .toolbar { display: flex; justify-content: space-around; padding: 16rpx 0; background: #fff; } .result-list { flex: 1; background: #fff; } .record-item { display: flex; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #eee; } .code { font-size: 28rpx; color: #333; word-break: break-all; } .time { font-size: 24rpx; color: #999; margin-left: 16rpx; } .footer { display: flex; align-items: center; justify-content: space-between; padding: 16rpx 24rpx; background: #fff; border-top: 1rpx solid #eee; }4.3 JS 逻辑扫码、反馈、列表、提交JS 部分我把关键逻辑整理成可直接参考的版本。重点看_lastScan、_scanLock、handleScanCode和submitBatch。Page({ data: { scanning: true, flash: off, tipText: 对准二维码/条形码连续扫描中, records: [], recordCount: 0, submitting: false }, onLoad() { this._lastScan { code: , time: 0 }; this._scanLock false; this._records []; this._audio wx.createInnerAudioContext(); this._audio.src /assets/beep.mp3; this._audio.volume 0.8; wx.setKeepScreenOn({ keepScreenOn: true }); this.checkCameraAuth(); }, onHide() { this.setData({ scanning: false, tipText: 已暂停 }); }, onShow() { if (this._needRecheckAuth) { this._needRecheckAuth false; this.checkCameraAuth(); } }, onUnload() { wx.setKeepScreenOn({ keepScreenOn: false }); if (this._audio) { this._audio.destroy(); } }, checkCameraAuth() { wx.getSetting({ success: (res) { const auth res.authSetting[scope.camera]; if (auth false) { wx.showModal({ title: 需要摄像头权限, content: 连续扫码需要访问摄像头请在设置中开启, confirmText: 去设置, success: (modalRes) { if (modalRes.confirm) { this._needRecheckAuth true; wx.openSetting(); } } }); } else if (auth undefined) { wx.authorize({ scope: scope.camera, fail: () { wx.showToast({ title: 未授权摄像头, icon: none }); } }); } } }); }, handleScanCode(e) { if (!this.data.scanning) return; const now Date.now(); let code (e.detail.result || ).trim(); if (!code) return; if (this._scanLock) return; if (now - this._lastScan.time 260) return; if (code this._lastScan.code now - this._lastScan.time 1500) { return; } this._scanLock true; this._lastScan { code, time: now }; this.handleValidCode(code); setTimeout(() { this._scanLock false; }, 260); }, handleValidCode(code) { wx.vibrateShort({ type: medium }); if (this._audio) { this._audio.stop(); this._audio.play(); } const record { id: ${Date.now()}_${Math.random().toString(36).slice(2, 8)}, code, timeText: this.formatTime(new Date()) }; this._records.push(record); const records this._records.slice(-100); this.setData({ records, recordCount: this._records.length, tipText: 已扫 ${this._records.length} 条 }); }, formatTime(date) { const pad (n) String(n).padStart(2, 0); return ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}; }, toggleFlash() { this.setData({ flash: this.data.flash off ? torch : off }); }, toggleScan() { const scanning !this.data.scanning; this.setData({ scanning, tipText: scanning ? 对准二维码/条形码连续扫描中 : 已暂停 }); }, clearList() { wx.showModal({ title: 确认清空, content: 清空后无法恢复是否继续, success: (res) { if (res.confirm) { this._records []; this._lastScan { code: , time: 0 }; this.setData({ records: [], recordCount: 0, tipText: 对准二维码/条形码 }); } } }); }, submitBatch() { if (this.data.submitting) return; if (!this._records.length) { wx.showToast({ title: 暂无数据, icon: none }); return; } this.setData({ submitting: true }); const codes this._records.map((item) item.code); wx.request({ url: https://your-domain.com/api/scan/batch, method: POST, data: { codes, deviceId: device-demo, timestamp: Date.now() }, success: (res) { if (res.data res.data.success) { wx.showToast({ title: 提交成功, icon: success }); this._records []; this.setData({ records: [], recordCount: 0 }); } else { wx.showToast({ title: 提交失败, icon: none }); } }, fail: () { wx.showToast({ title: 网络异常, icon: none }); }, complete: () { this.setData({ submitting: false }); } }); }, handleCameraError(e) { console.error(camera error, e.detail); wx.showToast({ title: 相机启动失败, icon: none }); } });这段代码里_records保存完整数据页面只渲染最近 100 条避免长列表卡顿。震动和音效放在handleValidCode里扫到有效码立刻反馈。暂停/继续只控制scanning变量不销毁camera组件避免相机反复重启。批量提交时把 codes 一次性发给后端后端做幂等。4.4 降级方案wx.scanCode 递归调用的封装如果用户基础库太低或者摄像头权限一直拿不到可以降级到wx.scanCode。做法是封装一个递归函数每次扫码成功后继续调用直到用户取消或主动暂停。注意fail里判断用户取消不要继续递归。Page({ data: { scanning: true, records: [] }, startContinuousScan() { if (!this.data.scanning) return; wx.scanCode({ scanType: [qrCode, barCode, datamatrix, pdf417], success: (res) { this.handleValidCode(res.result); setTimeout(() { this.startContinuousScan(); }, 300); }, fail: (err) { if (err.errMsg err.errMsg.indexOf(cancel) -1) { this.setData({ scanning: false }); wx.showToast({ title: 已停止连续扫码, icon: none }); } else { wx.showToast({ title: 扫码失败, icon: none }); } } }); }, handleValidCode(code) { const records this.data.records.concat({ code, timeText: new Date().toLocaleTimeString() }); this.setData({ records }); wx.vibrateShort({ type: medium }); } });这个方案的体验不如camera顺滑但兼容性最好适合做兜底。你可以根据wx.getSystemInfoSync().SDKVersion或者wx.canIUse(camera)来判断是否走降级。实际项目里我一般优先用camera失败或权限拒绝时提示用户切换到单次扫码模式。5. 常见问题与排查技巧实录5.1 相机黑屏、无回调、重复触发速查表连续扫码出问题时排查顺序很重要。下面这张表是我在实际项目中整理出来的基本覆盖了八成常见现象。现象可能原因解决方式相机黑屏未授权摄像头、隐私协议未确认、基础库过低检查scope.camera配置隐私声明升级基础库真机调试bindscancode不触发mode没设scanCode、事件名拼写错误、基础库低于 2.7.0设置modescanCode检查bindscancode升级基础库同一个码连续入列表没有节流和去重加全局锁和同一码时间窗去重不同码漏记全局节流时间太长把全局节流降到 200 到 300 毫秒同一码去重保持 1000 到 1500 毫秒列表越来越卡setData数据量太大只渲染最近 100 条完整数据放内存或本地存储手电筒不生效部分 Android 机型不支持torch捕获错误提示用户手动补光扫码对焦慢镜头脏、光线暗、码太小、反光提示用户调整距离开手电筒擦拭镜头审核被拒摄像头用途不明确在隐私协议和页面提示中写清楚扫码用途排查时我建议先看真机日志不要只信开发者工具。camera组件的binderror会返回错误信息console.error打出来。权限问题用wx.getSetting确认状态。回调不触发先检查mode和基础库。重复触发先检查去重逻辑是不是写在了setData之后因为setData异步可能导致判断失效。5.2 性能、发热、耗电和列表卡顿连续扫码最容易被忽略的问题是发热和耗电。相机持续开启加上连续识别手机会很快热起来尤其是 Android 中低端机。我的优化手段有几个第一提供暂停按钮用户不扫的时候手动暂停第二扫到码后如果业务允许短暂关闭识别 200 毫秒但不要频繁销毁camera组件否则重启更慢第三页面隐藏时立刻暂停回到页面再恢复第四设置屏幕常亮但页面卸载时取消常亮避免用户忘了关。列表卡顿通常是因为setData数据太大。前面代码里用了this._records.slice(-100)只把最近 100 条传给视图。如果业务必须展示全部可以用分页加载或者把结果页做成独立页面主扫码页只显示最近几条和总数。另外列表项里的时间字符串可以提前格式化不要在 WXML 里做复杂计算。wx:key一定要用唯一 id不要用 index否则列表更新时可能错位。网络提交也要控制频率。不要每扫一条就请求一次批量提交是更合理的做法。如果担心数据丢失可以在本地wx.setStorageSync存一份草稿提交成功后再清除。弱网环境下先存本地再异步提交用户体验会好很多。5.3 我踩过的坑与独家经验第一个坑是去重时间窗太短。我最早设了 500 毫秒结果同一个码在相机画面里停留时还是会偶尔触发两次。后来改成 1500 毫秒并且加了全局锁才稳定下来。但时间窗也不能太长如果用户故意连续扫同一个码比如同一商品多件1500 毫秒可能会漏记。所以这个参数一定要跟业务确认是“唯一码”还是“可重复码”。第二个坑是音效文件太长。我一开始用了一个 2 秒的提示音连续扫码时音效重叠听起来很乱。后来换成 100 毫秒以内的短“嘀”声并且用InnerAudioContext提前创建每次stop再play体验就干净了。震动反馈也要用wx.vibrateShort不要用长震动否则连续扫几十次手会麻。第三个坑是开发者工具和真机差异。我在工具里调通了bindscancode真机上却因为基础库版本低不触发。后来把最低基础库设到 2.7.0 以上并且在代码里加了版本判断才解决问题。所以我的建议是所有扫码相关功能必须在真机上验证iOS 和 Android 各一台低端机和高端机各一台别偷懒。第四个坑是相机上的 UI 被覆盖。我一开始用普通view在相机上画扫码框结果完全看不到。后来换成cover-view才显示出来。但cover-view不支持复杂的 CSS动画和阴影基本别想。所以现在的做法是相机上只放最简单的提示文字和四个角其他信息全部放到相机下方。这样既不影响识别也方便布局。最后一个经验是批量提交的幂等。用户可能因为网络慢连续点了两次提交如果后端没做幂等数据就重复了。我会在提交时生成一个batchNo后端根据batchNo去重。提交成功后清空本地列表失败则保留并提示“网络异常请重试”。这样即使弱网也不会丢数据或重复入库。提示连续扫码页面最好加一个“最近扫到”的大字提示让用户知道自己刚才扫了什么。很多人扫太快根本不看列表如果扫错了没有即时提示就很难发现。
返回列表