扫码出库 · 实现计划

目标页:新建/修改出库单stock_out_form_screen.dart)· 扫码枪扫商品 → 明细自动 +1 · 2026-08-25 · 待批准
在「新建出库单」页,操作员用扫码枪扫商品标签上已印的二维码,商品明细里自动新增该商品并数量 +1;同一商品重复扫则数量累加。 核心省力点:二维码 URL 里带 ?code={商品编码},而本页 _loadInventory 已把整仓库存(≤1000 个,stockOutPickerPageSize=1000) 聚合进本地 _inventoryPickerItems(每条含 productCode)——扫码→解析 code→本地命中→加行,零网络、零后端改动

1. 原理(一句话回顾)

扫码枪 = HID 键盘:扫到内容当作键盘输入敲进当前聚焦的输入框、末尾补回车。本页放一个聚焦的「扫码框」承接即可,零摄像头依赖。 扫到的是 URL https://…/app/product/{public_id}?code={code};我们取其中的 code 与整仓已加载的库存做精确匹配。

2. 数据流

① 选出库仓库(本页已有)→ _loadInventory 建两张本地表: _inventoryPickerItems(现有) + _codeIndex: Map<String code, _PickerItem>新增② 扫码框聚焦(失焦自动重夺) ③ 扣扳机 ──▶ URL + 回车 落入扫码框,onSubmitted 触发 _onScan(raw) ④ _onScan: 解析 code = Uri.parse(raw).queryParameters['code'] (拿不到则整串 trim 当 code 兜底) item = _codeIndex[code] ├─ 命中 && 已在明细 ──▶ 该行 数量 +1(超可用量则黄字提醒,仍加) ├─ 命中 && 不在明细 ──▶ 新增一行,数量=1,带 productId/售价/可用量 └─ 未命中 ───────────▶ 红 toast「未找到该商品/不在本仓」,不加行 ⑤ 清空扫码框 + 重新聚焦 + 轻提示音,等下一扫 ⑥ 扫完照常「提交审核」(提交时后端 CheckInventoryAvailability 兜底校验)

数量口径 = A(每扫 +1,已定)。注意:与「从库存选择」默认带出整箱可用量不同,扫码是按扫的次数点件数,故首扫为 1、再扫累加。数量列仍可手动改。

3. 前端改动(唯一必改文件:stock_out_form_screen.dart

改动点具体
本地 code 索引_loadInventory 里,聚合完 _inventoryPickerItems 后顺手建 _codeIndex = { for it in items : it.productCode : it }(productCode 非空才入)。
扫码框明细头 DetailHead(actions:[…]) 里,在「从库存选择」左侧加一个紧凑 DsInput(图标 LucideIcons.scanLine,占位「扫码枪扫码…」)+ 独立 FocusNode _scanFocus,桌面端 autofocus。窄屏(context.isMobile)不显示(扫码枪是桌面外设)。
_onScan(raw)新增方法:无仓库→toast「请先选择出库仓库」;解析 code;查 _codeIndex;命中则 _addOrBumpByScan(item);未命中红 toast。末尾 _scanCtrl.clear() + _scanFocus.requestFocus()
_addOrBumpByScan(item)_itemsproductId==item.productId:有→qtyCtrl.text = (now+1);无→复用 _addItem 的建行逻辑但 qty 固定 1setState 刷新。超可用量(>availableQty)→黄字 toast 提醒但不拦。
去重防抖_lastScan(code,ts),同 code 300ms 内重复直接忽略(防扳机抖动连发)。
提示音成功 SystemSound.play(SystemSoundType.click);失败可 HapticFeedback/仅红 toast。轻量,主反馈仍是 toast(操作员眼在货上)。
清理dispose() 里释放 _scanCtrl/_scanFocus

解析函数抽成纯函数 String? parseScanCode(String raw)(放本文件或 util),便于单测:覆盖 /app/product/x?code=1/product/x?code=1、纯 code、脏串。

不新增依赖:不引 mobile_scanner/camerapubspec.yaml 不动。

4. 原型改动(design-first,先改再改代码,同一提交

按项目铁律「原型与真实 100% 一致,前端改动必须先改原型」:在 design/prototype/screens/stock-out.html 的明细头,与「从库存选择」并排加一个扫码输入框原件(用登记过的 atoms,占位「扫码枪扫码…」)。评审原型 → 代码随后落地 → 原型与代码同提交。若涉及新图标 scan-line 未登记,先在 index.html + icons.js 登记再用(L1 单源)。

5. 二维码路径统一(顺带,小改)

后端 product.go:290 生成 URL 用的是 /app/product/,与 SSR 路由 /product/ 不一致。改成 /product/ 统一(已定)。对本方案非必需(我们只取 ?code=,不依赖路径段),但顺手消歧、利于将来按 public_id 解析。属 server 侧一行改动,随下次 server 发版生效。

⚑ 唯一待你拍板:要不要现在就做后端精确查接口?

本地 code 索引覆盖整仓 ≤1000 个商品,绝大多数门店够用。但两种情况本地会漏:①单仓商品 >1000(超分页);②某商品无 code(QR 只有 public_id,则无 ?code=)。

方案范围建议
A · 仅前端(本地匹配)零后端改动、零部署,扫→加立即生效;漏网情况给红 toast 提示改用「从库存选择」推荐先做,最快落地
B · 加后端 GET /inventory/scan本地未命中时回退查后端(精确 by code/public_id、shop 隔离、排除 sold),覆盖 >1000 与 public_id-only;需 server 发版作 Phase-2,等确有大仓再加

我的建议:先做 A(纯前端) 把这页跑通,B 留作后续。你也可以要求一步到位做 A+B。

6. 验证(无扫码枪也能全验,你已有 macOS app)

  1. 端到端:手动往扫码框粘贴一条真实商品 URL(形如 …/app/product/xxx?code=P1234)再按回车——效果与扫码枪 100% 等价。看是否新增行、再粘同一条是否 +1、粘不存在的 code 是否红 toast。
  2. 解析单测flutter test 覆盖 parseScanCode 各输入。
  3. DoDflutter analyze --no-fatal-infos --no-fatal-warnings 无 error;flutter test 全过。
  4. 上线后拿 2D 扫码枪扫一张真标签确认「带回车后缀」即可(1D 枪读不了 QR,见原理文档附录 B)。

7. 改动文件清单 & 执行顺序

落地评级:方案 A 为前端单文件 + 原型 + 测试,属中等改动;方案 B 追加后端接口即升为跨模块大改。均遵守 design-first。本文件仅计划评审,你批准后再实现。

8. 待确认

  1. 先做方案 A(纯前端),还是一步到位 A+B(含后端精确查)?(建议 A)
  2. 二维码路径 /app/product//product/ 这次一并改吗?(已定要改,确认随本次一起)