feat(client): 扫码出库(扫码枪扫二维码,明细自动 +1)

出库单新建页桌面端加「扫码枪扫码」框:扫商品二维码 URL → 取其中
的 ?code= → 本地整仓索引精确命中 → 明细自动新增/累加数量(每扫
+1)。本地未命中(大仓库存超前端加载上限 1000)时走服务端按编码
精确查兜底,复用现有库存搜索接口,无需新后端接口。

- 前端 stock_out_form_screen:扫码框 + _onScan + _codeIndex +
  parseScanCode + 服务端兜底 + 300ms 防抖 + 提示音;桌面专属,
  窄屏隐藏。
- 后端 product.go:二维码 URL 路径 /app/product/ → /product/
  统一到 SSR 公开页路由。
- 原型 stock-in.js/html:出库明细头同步加扫码框(design-first)。
- 测试 parse_scan_code_test(8 例)+ 出库表单桌面 golden 重生成。
- 设计/实现计划文档 docs/design/scan-stock-out-*。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bupi8Kdqkfx2N5acFsHTx5
This commit is contained in:
wangjia
2026-08-25 19:11:15 +08:00
parent b5715aec8d
commit 22047b725c
11 changed files with 556 additions and 5 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 130 KiB

+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jiu_client/screens/stock_out/stock_out_form_screen.dart';
/// 扫码出库:从扫码枪内容里取商品编码。
/// 扫码枪扫二维码得到 URL(形如 …/product/{public_id}?code={code})或纯编码。
void main() {
group('parseScanCode', () {
test('新路径 /product/xxx?code=', () {
expect(
parseScanCode('https://jiu.51yanmei.com/product/abc-123?code=P1001'),
'P1001',
);
});
test('旧路径 /app/product/xxx?code=(兼容)', () {
expect(
parseScanCode('https://jiu.51yanmei.com/app/product/abc?code=P1002'),
'P1002',
);
});
test('纯编码直接扫(无 URL', () {
expect(parseScanCode('P1003'), 'P1003');
});
test('前后空白被去除', () {
expect(parseScanCode(' P1004 '), 'P1004');
});
test('code 带 URL 编码字符被解码', () {
expect(parseScanCode('https://x/product/y?code=P%2D9'), 'P-9');
});
test('多参数里的 code 也能取到', () {
expect(parseScanCode('https://x/product/y?foo=1&code=P1005&bar=2'), 'P1005');
});
test('空串返回空', () {
expect(parseScanCode(''), '');
expect(parseScanCode(' '), '');
});
test('无 code 参数的 URL:整串兜底当编码', () {
// 无 ?code= 时不误判,返回原串(由上层按编码匹配,匹配不到再报未找到)
const raw = 'https://x/product/only-public-id';
expect(parseScanCode(raw), raw);
});
});
}