diff --git a/AGENTS.md b/AGENTS.md
index b47c4f1..5e182d2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -16,6 +16,7 @@
## 作业流程原则
- 扫码是主路径:输入焦点、连续扫描、防抖、重复码、无效码、错箱和扫描反馈必须作为一等场景处理。
+- 拣货扫描身份是后端下发的稳定箱唛箱码 `boxCode`;一次有效且未重复的箱码扫描表示拣出一箱。SKU、商品件数、入库单号和页面展示文案只用于辅助识别,不得作为扫码匹配键或拣货计数单位。
- 进入任务时由服务端标记“拣货中”;扫描结果先写入绑定作业员、任务和柜次的设备持久化草稿,仅在点击提交时批量写后端,禁止逐码提交。
- 允许部分拣货和跳拣,但每个未完成项必须填写异常原因;存在未解决异常时任务挂起,不能完成或推进 `LOADED`。
- 作业提交使用对员工不可见的轻量幂等标识和服务端版本;重复提交返回原结果,版本冲突提示刷新后重试,不静默覆盖。
@@ -46,7 +47,7 @@
| 页面注册与启动 | `pages.json`、`main.js`、`App.vue` | 全局样式和应用生命周期 |
| 登录与会话 | `pages/login/`、`stores/session.js` | 远程鉴权适配和会话清理 |
| 工作台与模块入口 | `pages/home/`、`pages/module/`、`pages/operation/` | `data/demo-data.js` 与页面禁用规则 |
-| 拣货任务与扫码 | `pages/picking/`、`stores/picking.js` | `api/pda-loading.js`、`utils/scan-broadcast.js`、`components/ScanTargetInput.vue` |
+| 拣货任务与箱码扫码 | `pages/picking/`、`stores/picking.js` | `api/pda-loading.js`、`utils/scan-broadcast.js`、`components/ScanTargetInput.vue`;服务端契约见 `../../baiyue-firstleg/firstleg/src/main/resources/openapi/phase3-pda-convergence-v1.yaml` |
| 扫描诊断 | `pages/scanner-test/` | `utils/scan-broadcast.js` 与广播 Action/Extra |
| 作业记录与人员 | `pages/history/`、`pages/profile/` | session 与本地敏感数据清理 |
diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue
index baec523..ecba9d7 100644
--- a/pages/picking/picking.vue
+++ b/pages/picking/picking.vue
@@ -41,12 +41,12 @@
{{ lastScanResult.type === 'success' ? '✓' : '!' }}
最近扫码 · {{ lastScanResult.title }}
- {{ lastScanResult.sku }}
+ {{ lastScanResult.boxCode }}
{{ lastScanResult.detail }}
- {{ entry.sku }} · {{ entry.title }}
+ {{ entry.boxCode }} · {{ entry.title }}
@@ -479,6 +479,14 @@
return false
},
methods: {
+ normalizeScanHistory(entries) {
+ if (!Array.isArray(entries)) return []
+ return entries.map((entry) => {
+ const normalized = { ...entry, boxCode: entry.boxCode ?? entry.sku ?? '' }
+ delete normalized.sku
+ return normalized
+ })
+ },
draftIdentity() {
const operatorId = session.currentUser?.id || session.currentUser?.userId || session.currentUser?.accountId
return { operatorId, taskId: this.remoteTaskId || this.currentBox.taskId || this.currentBox.inboundId, containerNo: this.currentBox.boxNo, warehouseId: this.currentBox.warehouseId || session.currentUser?.warehouseId || session.currentUser?.warehouse }
@@ -492,7 +500,7 @@
this.scannedBoxCodes = { ...(restored.scannedBoxCodes || {}) }
this.boxDrafts = restored.boxDrafts || {}
this.completedBoxes = restored.completedBoxes || {}
- this.scanHistory = restored.scanHistory || []
+ this.scanHistory = this.normalizeScanHistory(restored.scanHistory)
return true
},
handleOverlayChange(name, event) {
@@ -555,7 +563,7 @@
this.itemStates = { ...draft.itemStates }
this.partialQuantities = { ...draft.partialQuantities }
this.scannedBoxCodes = { ...(draft.scannedBoxCodes || {}) }
- this.scanHistory = draft.scanHistory.map((entry) => ({ ...entry }))
+ this.scanHistory = this.normalizeScanHistory(draft.scanHistory)
} else {
this.itemStates = createItemStates(this.currentBox.items)
this.partialQuantities = {}
@@ -687,6 +695,7 @@
const firstItem = this.currentBox.items[0]
this.currentBox.orderNo = firstItem.orderNo || firstItem.inboundCode || this.currentBox.orderNo
this.currentBox.inboundId = firstItem.inboundId || this.currentBox.inboundId
+ this.currentBox.tenantId = firstItem.tenantId || this.currentBox.tenantId
this.itemStates = createItemStates(this.currentBox.items)
this.partialQuantities = {}
this.scannedBoxCodes = {}
@@ -812,8 +821,8 @@
this.showToast(item ? `已确认拣出 ${item.qty} 箱` : '已确认拣出', 'success')
this.persistSession()
},
- recordScanResult(type, title, sku, detail) {
- const entry = { id: `${Date.now()}-${this.scanHistory.length}-${sku}`, type, title, sku, detail }
+ recordScanResult(type, title, boxCode, detail) {
+ const entry = { id: `${Date.now()}-${this.scanHistory.length}-${boxCode}`, type, title, boxCode, detail }
this.scanHistory = [entry, ...this.scanHistory].slice(0, 3)
},
undoItem(id) {
diff --git a/tests/page-contract.test.mjs b/tests/page-contract.test.mjs
index 7890cb5..685d2e9 100644
--- a/tests/page-contract.test.mjs
+++ b/tests/page-contract.test.mjs
@@ -102,12 +102,16 @@ test('picking page keeps scan, draft and risky transition contracts', async () =
assert.match(page, /submitStatusHint\(\)[\s\S]*?提交时需选择原因/)
assert.match(page, /onBackPress\(\)/)
assert.doesNotMatch(page, /scanFocused|restoreScanFocus|hideKeyboard|@confirm="handleScan"/)
- assert.match(page, / {
@@ -209,8 +213,8 @@ test('reusable scan target input separates field selection from page-level broad
assert.match(component, / {
diff --git a/tests/pda-loading-api.test.mjs b/tests/pda-loading-api.test.mjs
index d669203..701beae 100644
--- a/tests/pda-loading-api.test.mjs
+++ b/tests/pda-loading-api.test.mjs
@@ -7,7 +7,7 @@ const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.
test('PDA loading adapter keeps C04 read and write paths', () => {
for (const path of [
- '/orders?',
+ '/first-leg/pda-loading/tasks/pending',
'/loading/picking?tenantId=',
'/loading/picking/start?tenantId=',
'/loading/picking/complete?tenantId=',
@@ -56,6 +56,7 @@ test('picking page exposes five evidence slots, retry and remote completion stat
})
test('PDA cabinet picking adapter claims online and submits one atomic batch', () => {
+ assert.match(source, /\/first-leg\/pda-loading\/tasks\/pending/)
assert.match(source, /\/first-leg\/pda-loading\/tasks\?/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/)
assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/)
diff --git a/tests/picking-state.test.mjs b/tests/picking-state.test.mjs
index 7d376a0..3034aad 100644
--- a/tests/picking-state.test.mjs
+++ b/tests/picking-state.test.mjs
@@ -69,25 +69,25 @@ test('submitStates turns only pending items into skipped', () => {
})
})
-test('classifyScan distinguishes pending, repeated, and unknown SKU codes', () => {
- const scanItems = [{ id: 1, sku: 'EL-38201' }, { id: 2, sku: 'EL-38202' }]
+test('classifyScan distinguishes pending, repeated, and unknown carton codes', () => {
+ const scanItems = [{ id: 1, boxCodes: ['BOX-001'] }, { id: 2, boxCodes: ['BOX-002', 'BOX-003'] }]
assert.deepEqual(state.classifyScan(scanItems, {
1: state.ITEM_STATUS.PICKED,
2: state.ITEM_STATUS.PENDING,
- }, 'EL-38202'), {
+ }, 'BOX-002'), {
type: 'pending',
item: scanItems[1],
})
assert.deepEqual(state.classifyScan(scanItems, {
1: state.ITEM_STATUS.PARTIAL,
2: state.ITEM_STATUS.PENDING,
- }, 'EL-38201'), {
+ }, 'BOX-001', { 'BOX-001': 1 }), {
type: 'processed',
item: scanItems[0],
status: state.ITEM_STATUS.PARTIAL,
})
- assert.deepEqual(state.classifyScan(scanItems, {}, 'NO-SUCH-SKU'), {
+ assert.deepEqual(state.classifyScan(scanItems, {}, 'NO-SUCH-BOX'), {
type: 'unknown',
item: null,
})