优化
This commit is contained in:
@@ -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 与本地敏感数据清理 |
|
||||
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
<view class="last-scan-icon">{{ lastScanResult.type === 'success' ? '✓' : '!' }}</view>
|
||||
<view class="last-scan-content">
|
||||
<text class="last-scan-label">最近扫码 · {{ lastScanResult.title }}</text>
|
||||
<text class="last-scan-sku">{{ lastScanResult.sku }}</text>
|
||||
<text class="last-scan-box-code">{{ lastScanResult.boxCode }}</text>
|
||||
<text class="last-scan-detail">{{ lastScanResult.detail }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="scanHistory.length > 1" class="scan-history">
|
||||
<text v-for="entry in scanHistory.slice(1, 3)" :key="entry.id" class="scan-history-item">{{ entry.sku }} · {{ entry.title }}</text>
|
||||
<text v-for="entry in scanHistory.slice(1, 3)" :key="entry.id" class="scan-history-item">{{ entry.boxCode }} · {{ entry.title }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, /<ScanTargetInput v-model="scanValue" name="sku"[\s\S]*?:active="activeScanField === 'sku'"[\s\S]*?@activate="setScanTarget"[\s\S]*?@submit="handleManualSubmit"/)
|
||||
assert.match(page, /<ScanTargetInput v-model="scanValue" name="boxCode"[\s\S]*?:active="activeScanField === 'boxCode'"[\s\S]*?@activate="setScanTarget"[\s\S]*?@submit="handleManualSubmit"/)
|
||||
assert.match(page, /beforeUnmount\(\)/)
|
||||
assert.match(page, /startScanBroadcast/)
|
||||
assert.match(page, /onHide\(\) \{ this\.stopHardwareScanner\(\) \}/)
|
||||
assert.match(page, /handleHardwareScan\(value\)[\s\S]*?this\.scanValue = value[\s\S]*?this\.processScannedValue\(value, 'broadcast'\)/)
|
||||
assert.match(page, /handleManualSubmit\(value\)[\s\S]*?\.trim\(\)\.toUpperCase\(\)[\s\S]*?this\.processScannedValue\(normalizedValue, 'manual'\)/)
|
||||
assert.match(page, /recordScanResult\(type, title, boxCode, detail\)/)
|
||||
assert.match(page, /lastScanResult\.boxCode/)
|
||||
assert.match(page, /boxCode: entry\.boxCode \?\? entry\.sku \?\? ''/)
|
||||
assert.doesNotMatch(page, /lastScanResult\.sku|entry\.sku\s*}}/)
|
||||
const processScanMethod = page.match(/processScannedValue\(scanValue, source = 'broadcast'\) \{[\s\S]*?\n\t\t\t\},\n\t\t\tpickItem/)
|
||||
assert.ok(processScanMethod)
|
||||
assert.doesNotMatch(processScanMethod[0], /this\.scanValue = ''/)
|
||||
@@ -198,7 +202,7 @@ test('picking scan results use the shared success and error audio feedback', asy
|
||||
assert.match(audio, /export function playErrorSound\(\)/)
|
||||
assert.match(picking, /import \{ playErrorSound, playSuccessSound \} from '@\/utils\/audio-feedback\.js'/)
|
||||
assert.match(picking, /result\.type === 'unknown'[\s\S]*?playErrorSound\(\)/)
|
||||
assert.match(picking, /playSuccessSound\(\)[\s\S]*?this\.pickItem\(result\.item\.id, 'scan'\)/)
|
||||
assert.match(picking, /playSuccessSound\(\)[\s\S]*?this\.pickScannedBox\(result\.item, value\)/)
|
||||
})
|
||||
|
||||
test('reusable scan target input separates field selection from page-level broadcast handling', async () => {
|
||||
@@ -209,8 +213,8 @@ test('reusable scan target input separates field selection from page-level broad
|
||||
assert.match(component, /<input[\s\S]*?:value="modelValue"[\s\S]*?@input="handleInput"[\s\S]*?@confirm="handleConfirm"/)
|
||||
assert.doesNotMatch(component, /<input[\s\S]*?disabled/)
|
||||
assert.doesNotMatch(component, /startScanBroadcast|plus\.android/)
|
||||
assert.match(picking, /activeScanField: 'sku'/)
|
||||
assert.match(picking, /handleHardwareScan\(value\)[\s\S]*?activeScanField !== 'sku'[\s\S]*?processScannedValue\(value, 'broadcast'\)/)
|
||||
assert.match(picking, /activeScanField: 'boxCode'/)
|
||||
assert.match(picking, /handleHardwareScan\(value\)[\s\S]*?activeScanField !== 'boxCode'[\s\S]*?processScannedValue\(value, 'broadcast'\)/)
|
||||
})
|
||||
|
||||
test('picking session persists per task identity and clears on logout', async () => {
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user