diff --git a/api/pda-loading.js b/api/pda-loading.js index e674bb4..b8b7157 100644 --- a/api/pda-loading.js +++ b/api/pda-loading.js @@ -97,6 +97,16 @@ export function pickContainerItem(taskId, itemId, payload = {}) { return itemAction(taskId, itemId, 'pick', payload, payload.operationId) } +export function pickContainerPallet(taskId, payload = {}) { + if (!taskId || !payload.palletNo) throw new Error('缺少拣货任务或托盘码,无法提交') + return request({ + url: `/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}/pallets/pick`, + method: 'POST', + header: { 'Idempotency-Key': stableIdempotencyKey(payload.operationId || `pick-pallet-${taskId}-${payload.palletNo}`) }, + data: payload + }) +} + export function partialContainerItem(taskId, itemId, payload = {}) { return itemAction(taskId, itemId, 'partial', payload, payload.operationId) } diff --git a/pages/picking/picking.vue b/pages/picking/picking.vue index 808cd93..778a295 100644 --- a/pages/picking/picking.vue +++ b/pages/picking/picking.vue @@ -107,7 +107,7 @@ - + 已同步服务端 @@ -343,7 +343,7 @@ import { navigateBackOr } from '@/utils/navigation.js' import { startScanBroadcast } from '@/utils/scan-broadcast.js' import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js' - import { claimContainerPickingTask, completeContainerPickingTask, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, isRemoteReady, listLoadingOrders, normalizePickingTask, submitContainerPickingBatch, uploadEvidenceFile, voidEvidence } from '@/api/pda-loading.js' + import { claimContainerPickingTask, completeContainerPickingTask, downloadEvidencePreview, EVIDENCE_SLOTS, getContainerPickingTask, getEvidence, getEvidencePreviewUrl, getPickingInventorySnapshot, isRemoteReady, listLoadingOrders, normalizePickingTask, partialContainerItem, pickContainerItem, pickContainerPallet, skipContainerItem, submitContainerPickingBatch, uploadEvidenceFile, voidEvidence } from '@/api/pda-loading.js' import { createEvidenceUploadQueue } from '@/utils/evidence-upload-queue.js' import { isOnline } from '@/utils/network.js' import { stableOperationId } from '@/utils/idempotency.js' @@ -973,7 +973,7 @@ const pallet = isSealedPalletItem(item) && item.palletNo ? `${item.palletNo} · ` : '' return `${pallet}${item.desc}${reason}` }, - processScannedValue(scanValue, source = 'broadcast') { + async processScannedValue(scanValue, source = 'broadcast') { if (!this.requirePickingOperation()) return const value = scanValue.trim() if (!value) { @@ -1001,9 +1001,10 @@ this.showToast(`未找到匹配的待拣条码:${value}`, 'error') return } - playSuccessSound() - if (result.type === 'pallet') this.pickScannedPallet(result.item, value) - else this.pickScannedBox(result.item, value) + const submitted = result.type === 'pallet' + ? await this.pickScannedPallet(result.item, value) + : await this.pickScannedBox(result.item, value) + if (submitted) playSuccessSound() }, evidenceEntries(type) { return Array.isArray(this.evidenceFiles[type]) ? this.evidenceFiles[type].filter((item) => item.status !== 'VOID' && item.status !== 'VOIDED') : (this.evidenceFiles[type] ? [this.evidenceFiles[type]] : []) }, evidencePreviewSource(photo) { return photo?.localPath || photo?.fileUrl || photo?.downloadUrl || photo?.url || '' }, @@ -1427,8 +1428,11 @@ if (this.isEvidenceContextActive(contextId, identity)) this.submitting = false } }, - pickScannedBox(item, boxCode) { - const pickedCount = Number(this.partialQuantities[item.id] || 0) + 1 + async pickScannedBox(item, boxCode) { + const submitted = await this.submitImmediateItemAction(item, 'PICK', { pickedPackageCount: 1, boxCode }) + if (!submitted) return false + const latest = this.currentBox.items.find((entry) => String(entry.id) === String(item.id)) || item + const pickedCount = Number(latest.pickedPackageCount || 0) const plannedCount = Number(item.qty || 0) this.scannedBoxCodes = { ...this.scannedBoxCodes, [boxCode]: item.id } this.partialQuantities = { ...this.partialQuantities, [item.id]: pickedCount } @@ -1438,8 +1442,11 @@ `当前明细 · 已拣 ${pickedCount}/${plannedCount} 箱`) this.showToast(`已拣箱 ${pickedCount}/${plannedCount}`, 'success') this.persistSession() + return true }, - pickScannedPallet(item, palletNo) { + async pickScannedPallet(item, palletNo) { + const submitted = await this.submitImmediateItemAction(item, 'PICK_PALLET', { palletNo: item.palletNo }) + if (!submitted) return false const plannedCount = Number(item.qty || 0) this.scannedBoxCodes = { ...this.scannedBoxCodes, [item.palletNo]: item.id, [palletNo]: item.id } this.partialQuantities = { ...this.partialQuantities, [item.id]: plannedCount } @@ -1447,13 +1454,16 @@ this.recordScanResult('success', '整托扫描成功', palletNo, `已定位托盘 ${item.palletNo} · 共 ${plannedCount} 箱`) this.showToast(`托盘 ${item.palletNo} 已确认装柜`, 'success') this.persistSession() + return true }, - pickItem(id, source = 'manual') { + async pickItem(id, source = 'manual') { if (!this.requirePickingOperation()) return const item = this.currentBox.items.find((entry) => entry.id === id) if (isSealedPalletItem(item)) { this.showToast(`已封托货物必须扫描托盘码 ${item.palletNo}`, 'warning'); return } const available = this.availablePickQuantity(item) if (available !== null && available < Number(item.qty || 0)) { this.showToast(`库存/库位可用量不足:可拣 ${available}`, 'error'); return } + const remaining = Number(item?.qty || 0) - Number(item?.pickedPackageCount || 0) + if (remaining < 1 || !await this.submitImmediateItemAction(item, 'PICK', { pickedPackageCount: remaining, source })) return this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PICKED } this.partialQuantities = { ...this.partialQuantities, [id]: Number(item?.qty || 0) } this.showToast(item ? `已确认拣出 ${item.qty} 箱` : '已确认拣出', 'success') @@ -1463,19 +1473,6 @@ const entry = { id: `${Date.now()}-${this.scanHistory.length}-${boxCode}`, type, title, boxCode, detail } this.scanHistory = [entry, ...this.scanHistory].slice(0, 3) }, - undoItem(id) { - if (!this.requirePickingOperation()) return - const item = this.currentBox.items.find((entry) => entry.id === id) - if (item) item.skipReason = '' - this.itemStates = { ...this.itemStates, [id]: ITEM_STATUS.PENDING } - const nextPartialQuantities = { ...this.partialQuantities } - delete nextPartialQuantities[id] - this.partialQuantities = nextPartialQuantities - this.scannedBoxCodes = Object.fromEntries(Object.entries(this.scannedBoxCodes) - .filter(([, itemId]) => String(itemId) !== String(id))) - this.showToast('已撤销操作', 'success') - this.persistSession() - }, openSkip(id) { if (!this.requirePickingOperation()) return const item = this.currentBox.items.find((entry) => entry.id === id) @@ -1484,16 +1481,17 @@ this.selectedSkipReason = '' this.openOverlay('skip') }, - confirmSkip() { + async confirmSkip() { if (!this.requirePickingOperation()) return if (!this.selectedSkipReason) { this.showToast('请先选择跳拣原因', 'warning') return } const item = this.currentBox.items.find((entry) => entry.id === this.skipTargetId) + const reason = this.selectedSkipReason + if (!item || !await this.submitImmediateItemAction(item, 'SKIP', { reason })) return if (item) item.skipReason = this.selectedSkipReason this.itemStates = { ...this.itemStates, [this.skipTargetId]: ITEM_STATUS.SKIPPED } - const reason = this.selectedSkipReason this.closeOverlay(false) this.showToast(`已标记跳拣:${reason}`, 'warning') this.persistSession() @@ -1516,7 +1514,7 @@ this.partialFocused = true }) }, - confirmPartial() { + async confirmPartial() { if (!this.requirePickingOperation()) return const item = this.partialTargetItem if (!item) return @@ -1524,6 +1522,7 @@ const value = parseInt(this.partialInput, 10) const available = this.availablePickQuantity(item) if (available !== null && value > available) { this.showToast(`超过库存/库位可用量:可拣 ${available}`, 'error'); return } + if (!await this.submitImmediateItemAction(item, 'PARTIAL', { finalPickedPackageCount: value })) return this.itemStates = { ...this.itemStates, [item.id]: ITEM_STATUS.PARTIAL } this.partialQuantities = { ...this.partialQuantities, [item.id]: value } this.closeOverlay(false)