This commit is contained in:
2026-09-11 18:44:04 +08:00
parent 35f245d067
commit c69e9411c7
6 changed files with 65 additions and 26 deletions
+1
View File
@@ -20,6 +20,7 @@
- 扫码是主路径:输入焦点、连续扫描、防抖、重复码、无效码、错箱和扫描反馈必须作为一等场景处理。
- 普通散箱拣货的扫描身份是后端下发的稳定箱唛箱码 `boxCode`,一次有效且未重复的箱码扫描表示拣出一箱;一件代发已封托的 `SEALED_PALLET` 明细允许扫描系统 `palletNo`、扫描托内任一成员 `boxCode` 或在明细卡片手工确认整托,但三种入口都必须按计划快照中的 `palletNo` 以 `PICK_PALLET` 一次性提交,不能逐箱累计、部分拣货或跳拣。SKU、商品件数、入库单号和页面展示文案只用于辅助识别,不得作为扫码匹配键。
- 拣货明细卡片按现场找货优先级展示库位、托盘码或入仓码、待拣箱数和处理状态;库位从明细 `inventoryResources.locationCode` 去重汇总,缺失时明确显示“库位 —”。商品名称和 SKU 数据可以保留在响应模型中,但不占用 PDA 拣货卡片主区域。
- 进入任务时由服务端标记“拣货中”;每次有效箱码或托盘码扫描,以及确认拣出、部分拣货、跳拣操作,都必须立即写入服务端,设备持久化草稿只用于恢复页面状态。底部提交只原子收尾尚未处理的明细并进入装柜取证,不重复提交已落库动作。
- 允许部分拣货和跳拣,但每个未完成项必须填写异常原因;存在未解决异常时任务挂起,不能完成或推进 `LOADED`。
- 作业提交使用对员工不可见的轻量幂等标识和服务端版本;重复提交返回原结果,版本冲突提示刷新后重试,不静默覆盖。
+26 -22
View File
@@ -95,7 +95,7 @@
</view>
<view class="pick-item-row2">
<text class="pick-item-desc">{{ itemDescription(item) }}</text>
<text v-if="item.location" class="pick-item-location">{{ item.location }}</text>
<text class="pick-item-location" :class="{ 'location-missing': !item.location }">库位 {{ item.location || '—' }}</text>
</view>
<view v-if="itemState(item) === ITEM_STATUS.PENDING && isSealedPalletItem(item)" class="pick-item-actions">
@@ -336,7 +336,7 @@
</template>
<script>
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, submitStates } from '@/utils/picking-state.js'
import { ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, submitStates, summarizeInventoryResources } from '@/utils/picking-state.js'
import { SKIP_REASONS, INCOMPLETE_REASONS } from '@/data/demo-data.js'
import { session, requireSession } from '@/stores/session.js'
import { clearLoadingCompletionAttempt, clearPickingDraft, getPickingSession, restoreLoadingCompletionAttempt, restorePickingSession, saveLoadingCompletionAttempt, savePickingSession } from '@/stores/picking.js'
@@ -970,8 +970,7 @@
const reason = this.itemState(item) === ITEM_STATUS.SKIPPED && item.skipReason
? ` · ${item.skipReason}`
: ''
const inbound = isSealedPalletItem(item) && item.inboundCode ? `${item.inboundCode} · ` : ''
return `${inbound}${item.desc}${reason}`
return `入仓码 ${item.inboundCode || '—'}${reason}`
},
async processScannedValue(scanValue, source = 'broadcast') {
if (!this.requirePickingOperation()) return
@@ -1102,24 +1101,29 @@
const remoteItems = task.items || task.pickItems || task.details || task.lines
if (!Array.isArray(remoteItems) || !remoteItems.length) throw new Error('当前拣货任务暂无可作业明细,请返回刷新')
{
this.currentBox.items = remoteItems.map((item) => ({
...item,
id: item.id ?? item.itemId,
// The batch endpoint expects the PDA task-item primary key (`id`),
// not a nested loading-plan item alias when both are returned.
taskItemId: item.id ?? item.itemId,
boxCodes: Array.isArray(item.boxCodes) ? item.boxCodes.map(String) : [],
orderNo: item.orderNo ?? '', inboundCode: item.inboundCode ?? '',
desc: item.cargoName ?? item.desc ?? item.description ?? '箱级拣货明细',
qty: Number(item.qty ?? item.plannedPackageCount ?? item.packageCount ?? 0),
unit: '箱',
location: item.location ?? item.locationCode ?? '',
availablePackageCount: item.availablePackageCount ?? item.locationAvailablePackageCount ?? item.availableQty ?? item.stockAvailableQty ?? null,
locationIds: item.locationIds ?? item.reservationIds ?? [],
locationAvailable: item.locationAvailable ?? item.locationUsable ?? null,
locationStatus: item.locationStatus ?? null,
skipReason: item.skipReason || '', finalState: item.finalState || '', finalPartialQuantity: item.finalPartialQuantity || 0
}))
this.currentBox.items = remoteItems.map((item) => {
const inventoryResources = Array.isArray(item.inventoryResources) ? item.inventoryResources : []
const inventorySummary = summarizeInventoryResources(inventoryResources)
return {
...item,
id: item.id ?? item.itemId,
// The batch endpoint expects the PDA task-item primary key (`id`),
// not a nested loading-plan item alias when both are returned.
taskItemId: item.id ?? item.itemId,
boxCodes: Array.isArray(item.boxCodes) ? item.boxCodes.map(String) : [],
inventoryResources,
orderNo: item.orderNo ?? '', inboundCode: item.inboundCode ?? '',
desc: item.cargoName ?? item.desc ?? item.description ?? '箱级拣货明细',
qty: Number(item.qty ?? item.plannedPackageCount ?? item.packageCount ?? 0),
unit: '箱',
location: item.location || item.locationCode || inventorySummary.location,
availablePackageCount: item.availablePackageCount ?? item.locationAvailablePackageCount ?? item.availableQty ?? item.stockAvailableQty ?? inventorySummary.availablePackageCount,
locationIds: item.locationIds ?? item.reservationIds ?? inventoryResources.map((resource) => resource.locationId).filter(Boolean),
locationAvailable: item.locationAvailable ?? item.locationUsable ?? null,
locationStatus: item.locationStatus ?? null,
skipReason: item.skipReason || '', finalState: item.finalState || '', finalPartialQuantity: item.finalPartialQuantity || 0
}
})
const firstItem = this.currentBox.items[0]
this.currentBox.orderNo = firstItem.orderNo || task.orderNo || this.currentBox.orderNo
this.currentBox.inboundCode = firstItem.inboundCode || task.inboundCode || this.currentBox.inboundCode
+5
View File
@@ -597,6 +597,11 @@
background: #e9f4ff;
}
.pick-item-location.location-missing {
color: #b45309;
background: #fff7e8;
}
.pick-item-actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
+6 -4
View File
@@ -4,8 +4,10 @@ import { readFile } from 'node:fs/promises'
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
test('missing picking location does not render an empty badge or dangling separator', () => {
assert.match(page, /<text v-if="item\.location" class="pick-item-location">\{\{ item\.location \}\}<\/text>/)
assert.match(page, /item\.location \? `\$\{item\.sku\} · \$\{item\.location\}` : item\.sku/)
assert.doesNotMatch(page, /<text class="pick-item-location">\{\{ item\.location \}\}/)
test('picking card prioritizes location and inbound code without cargo or SKU text', () => {
assert.match(page, /<text class="pick-item-location" :class="\{ 'location-missing': !item\.location \}">库位 \{\{ item\.location \|\| '—' \}\}<\/text>/)
assert.match(page, /return `入仓码 \$\{item\.inboundCode \|\| '—'\}\$\{reason\}`/)
assert.match(page, /summarizeInventoryResources\(inventoryResources\)/)
assert.match(page, /location: item\.location \|\| item\.locationCode \|\| inventorySummary\.location/)
assert.doesNotMatch(page.match(/itemDescription\(item\) \{[\s\S]*?\n\t\t\t\}/)?.[0] || '', /item\.desc|cargoName|sku/i)
})
+9
View File
@@ -7,6 +7,15 @@ const state = await import(`data:text/javascript;base64,${Buffer.from(source).to
const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
test('inventory resources expose distinct locations and total available cartons', () => {
assert.deepEqual(state.summarizeInventoryResources([
{ locationCode: 'A0', availablePackageCount: 4 },
{ locationCode: 'A0', availablePackageCount: 2 },
{ locationCode: 'B-03', availablePackageCount: 1 },
]), { location: 'A0、B-03', availablePackageCount: 7 })
assert.deepEqual(state.summarizeInventoryResources(null), { location: '', availablePackageCount: null })
})
test('createItemStates initializes every item as pending', () => {
assert.deepEqual(state.createItemStates(items), {
1: state.ITEM_STATUS.PENDING,
+18
View File
@@ -11,6 +11,24 @@ export function isSealedPalletItem(item) {
|| item?.indivisible === true
}
export function summarizeInventoryResources(resources) {
const list = Array.isArray(resources) ? resources : []
const locationCodes = [...new Set(list
.map((resource) => String(resource?.locationCode || '').trim())
.filter(Boolean))]
const availableCounts = list
.map((resource) => resource?.availablePackageCount)
.filter((value) => value !== null && value !== undefined && value !== '')
.map(Number)
.filter(Number.isFinite)
return {
location: locationCodes.join('、'),
availablePackageCount: availableCounts.length
? availableCounts.reduce((total, value) => total + Math.max(0, value), 0)
: null
}
}
export function createItemStates(items) {
return items.reduce((states, item) => {
states[item.id] = ITEM_STATUS.PENDING