diff --git a/output/gray-h5-scan/gray-release-candidate.json b/output/gray-h5-scan/gray-release-candidate.json new file mode 100644 index 0000000..d2c4f50 --- /dev/null +++ b/output/gray-h5-scan/gray-release-candidate.json @@ -0,0 +1,16 @@ +{ + "base": "3133aac01953a2b7034517171cb098148407cd6e", + "commit": "a7388914cdd1ae654efa08867edb51fc4e0b6e5c", + "files": [ + "src/pages/dispatch/dispatch.vue", + "src/utils/dispatch-error.js", + "tests/fulfillment-workflows.test.mjs" + ], + "supersedes": "cf984ab4812d64b33e1ad2c98bc2804a272c6c77", + "deployed": false, + "checks": { + "tests": 215, + "h5Build": "PASS", + "browser": "local corrected H5 with gray API" + } +} diff --git a/src/api/fulfillment.js b/src/api/fulfillment.js index eb2eeba..a8b4f03 100644 --- a/src/api/fulfillment.js +++ b/src/api/fulfillment.js @@ -138,6 +138,7 @@ export function outboundPickPayload(item, position, packages, scannedCode = '') outboundPalletItemId: item.outboundPalletItemId, inventoryPositionId: position.inventoryPositionId || null, packages: Number(packages), + ...(item.returnSkuQuantity != null ? { productQuantity: Number(item.returnSkuQuantity) } : {}), cartonMarks: skuScan ? [] : position.cartonMark ? [position.cartonMark] : [], ...(skuScan ? { skuCode: item.skuCode } : {}) } diff --git a/src/api/return-counting.js b/src/api/return-counting.js new file mode 100644 index 0000000..93705e4 --- /dev/null +++ b/src/api/return-counting.js @@ -0,0 +1,20 @@ +import { overseasRequest } from './overseas.js' +const BASE = '/pda/v1/last-leg/return-inventory/counting' +export function getCurrentReturnCountTask() { + return overseasRequest('GET', `${BASE}/current`) +} +export function beginReturnCountTask(receiptNo) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}/begin`) +} +export function scanReturnCountTasks(code) { + return overseasRequest('GET', `${BASE}/scan/${encodeURIComponent(String(code).trim())}`) +} +export function listReturnCountTasks({ page = 1, size = 10, keyword = '' } = {}) { + return overseasRequest('GET', `${BASE}/tasks?page=${page}&size=${size}&keyword=${encodeURIComponent(keyword.trim())}`) +} +export function scanReturnCountCarton(mark) { + return overseasRequest('GET', `${BASE}/cartons/${encodeURIComponent(String(mark).trim())}`) +} +export function confirmReturnCount(receiptNo, request) { + return overseasRequest('POST', `${BASE}/receipts/${encodeURIComponent(receiptNo)}`, request) +} diff --git a/src/components/RelabelPalletWork.vue b/src/components/RelabelPalletWork.vue index bd24a41..268aa6d 100644 --- a/src/components/RelabelPalletWork.vue +++ b/src/components/RelabelPalletWork.vue @@ -6,20 +6,18 @@ 已绑定箱唛:选择后扫描新目标托盘可改绑(已排车或出库不可直接改绑)。 - + 正在改绑:{{ rebindSelection.mark }},当前托盘:{{ rebindSelection.pallet }} - 已记录组托:{{ entry.cartonMarks.join('、') }} → {{ entry.targetPallets.join('、') }}(含历史轮次) - 托盘号:{{ row.palletCode }} · 入仓码:{{ row.inboundCode }} - {{ isReturnBulk ? '件数' : '箱数' }}:{{ row.stockPackages }} · 可拆{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.movablePackages }} + 托盘号{{ row.palletCode }} + {{ isReturnBulk ? '件数' : '箱数' }}:{{ row.stockPackages }} · 可拆{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.movablePackages }} 当前没有可操作的来源托盘 - - + 已确认:{{ source.palletCode }} - 本次{{ isReturnBulk && wholeSource ? '整托换标数量' : `拆出${isReturnBulk ? '件数' : '箱数'}` }}(可分多个来源托盘,逐批提交) + 本次{{ wholeSource ? '整托换标数量' : '拆出件数' }}(可分多个来源托盘,逐批提交) - + + - 已识别目标:{{ target.palletCode }},尚未绑定成功 - 新托盘号:{{ newPallet.palletCode }},请贴好托盘标签后扫描确认。 + 托盘 {{ newPallet.palletCode }} 待绑定 {{ message }}{{ error }} @@ -47,6 +45,8 @@ export default { data() { return { rebindSelection: null, active: 'source', sourceCode: '', targetCode: '', source: null, target: null, marks: [], busy: false, error: '', message: '', newPallet: null, sourceRows: [], selectedSource: null, quantity: '', selectedDestination: '' } }, mounted() { this.loadSources() }, computed: { + requiredSplitPackages() { return (this.task.items || []).filter(item => ['CARTON_LABEL', 'CARTON_AND_SKU'].includes(item.operationType)).reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0) }, + splitPackages() { const bound = new Set(this.currentBindings.map(binding => binding.mark)); return (this.task.items || []).filter(item => ['CARTON_LABEL', 'CARTON_AND_SKU'].includes(item.operationType) && bound.has(item.cartonMark)).reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0) }, currentBindings() { const latest = new Map() for (const entry of this.task.history || []) { @@ -60,7 +60,6 @@ export default { } return [...latest.values()] }, - bindingHistory() { return (this.task.history || []).filter(entry => entry.action === 'PALLET_BIND').flatMap(entry => { try { const data = JSON.parse(entry.detail); return Array.isArray(data.cartonMarks) && Array.isArray(data.targetPallets) ? [data] : [] } catch { return [] } }) }, destinations() { return [...new Set((this.isReturnBulk ? this.sourceItems() : this.completedItems()).map(item => item.destinationFbaWarehouseCode).filter(Boolean))] }, eligibleItems() { return (this.isReturnBulk ? this.sourceItems() : this.completedItems()).filter(item => item.destinationFbaWarehouseCode === this.selectedDestination) }, isReturnBulk() { return this.task.operationType === 'RETURN_BULK_SKU_LABEL' }, @@ -99,7 +98,7 @@ export default { if (!binding || !targetCode || this.busy) return if (targetCode === binding.pallet.toUpperCase()) { this.showBindingMessage('无需改绑', '该箱唛已经绑定此托盘,不会重复扣库存。'); return } this.busy = true; this.$emit('busy', true) - const confirmed = await new Promise(resolve => uni.showModal({ title: '确认改绑', content: `将 ${binding.mark} 从 ${binding.pallet} 移至 ${targetCode}?旧托盘扣箱,新托盘加箱。`, confirmText: '确认改绑', cancelText: '取消', success: result => resolve(result.confirm), fail: () => resolve(false) })) + const confirmed = await new Promise(resolve => uni.showModal({ title: '确认改绑', content: `将 ${binding.mark} 从 ${binding.pallet} 移至 ${targetCode}?旧托盘扣箱,新托盘加箱。`, confirmText: '是', cancelText: '否', success: result => resolve(result.confirm), fail: () => resolve(false) })) this.busy = false; this.$emit('busy', false) if (!confirmed) return await this.run(async () => { @@ -111,17 +110,17 @@ export default { releaseWarehouseKey('relabel', key.signature) this.rebindSelection = null; this.targetCode = ''; this.target = null this.$emit('saved') - this.showBindingMessage('绑定成功', `${binding.mark} 已从 ${binding.pallet} 移至 ${target.palletCode}`) + this.showBindingMessage('绑定成功', `托盘 ${target.palletCode} 已绑定`) }) if (this.error) this.showBindingMessage('改绑失败', this.error) }, sourceItems() { return (this.task.items || []).filter(item => this.selectedSource?.cartonMarks.includes(item.cartonMark)) }, - completedItems() { return this.sourceItems().filter(item => this.isReturnBulk ? Number(item.expectedPackages) > 0 && Number(item.relabelledPackages) >= Number(item.expectedPackages) : Number(item.relabelledPackages) >= 1) }, + completedItems() { const bound = new Set(this.currentBindings.map(binding => binding.mark)); return this.sourceItems().filter(item => this.isReturnBulk ? Number(item.expectedPackages) > 0 && Number(item.relabelledPackages) >= Number(item.expectedPackages) : Number(item.relabelledPackages) >= 1 && !bound.has(item.cartonMark)) }, async loadSources() { await this.run(async () => { this.sourceRows = await findRelabelSources(this.task.relabelNo); this.selectedSource = null; this.source = null; this.marks = []; this.quantity = '' }) }, async selectSource(row) { if (this.busy) return this.rebindSelection = null; this.selectedSource = row; this.source = null; this.sourceCode = ''; this.quantity = ''; this.marks = [] - this.selectedDestination = this.sourceItems()[0]?.destinationFbaWarehouseCode || '' + this.selectedDestination = (this.isReturnBulk ? this.sourceItems() : this.completedItems())[0]?.destinationFbaWarehouseCode || '' if (this.isReturnBulk) { const items = this.eligibleItems this.marks = items.map(item => item.cartonMark) @@ -144,23 +143,45 @@ export default { fillBatch() { const count = Number(this.quantity); const packages = this.eligibleItems.reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0); const invalid = !Number.isSafeInteger(count) || count <= 0 || count > this.selectedSource.movablePackages || (this.isReturnBulk ? count !== packages : count > this.eligibleItems.length); if (invalid) { this.error = this.isReturnBulk ? '拆出件数必须等于本批已完成贴标件数,且不能超过来源托盘可拆数量' : '拆出数量必须为正整数,且不超过来源托盘可拆箱数和已完成换标箱数'; return } this.marks = (this.isReturnBulk ? this.eligibleItems : this.eligibleItems.slice(0, count)).map(item => item.cartonMark); this.error = '' }, toggleMark(mark) { if (this.busy) return; this.marks = this.marks.includes(mark) ? this.marks.filter(value => value !== mark) : [...this.marks, mark] }, activate(value) { this.active = value; this.$emit('activate') }, - handleScan(value) { if (this.busy) return; this[this.active + 'Code'] = value; return this.active === 'target' ? this.confirmTargetAndBind() : this.resolve(this.active) }, + handleScan(value) { if (this.busy) return; this[this.active + 'Code'] = value; return this.active === 'target' ? this.confirmTargetAndBind() : this.confirmSourceScan() }, + async confirmBindingChoice(content) { + this.busy = true; this.$emit('busy', true) + try { return await new Promise(resolve => uni.showModal({ title: '是否绑定', content, confirmText: '是', cancelText: '否', success: result => resolve(!!result.confirm), fail: () => resolve(false) })) } + finally { this.busy = false; this.$emit('busy', false) } + }, + async confirmSourceScan() { + if (this.busy || !this.sourceCode.trim()) return + const matched = this.operableSourceRows.find(row => this.samePallet(row, { palletCode: this.sourceCode })) + this.source = null + if (!matched) { this.error = '该托盘不在本单可操作的来源托盘列表中'; this.showBindingMessage('来源托盘不匹配', this.error); return } + await this.resolve('source') + if (this.error || !this.source) return + const source = this.source; this.source = null + if (await this.confirmBindingChoice(`是否绑定来源托盘 ${source.palletCode}?`)) this.source = source + }, showBindingMessage(title, content) { uni.showModal({ title, content, showCancel: false, confirmText: '知道了' }) }, async confirmTargetAndBind() { if (this.busy) return if (this.rebindSelection) return this.submitRebind() + if (!this.isReturnBulk && this.selectedSource && this.samePallet(this.source, this.selectedSource)) { + this.selectedDestination = this.completedItems()[0]?.destinationFbaWarehouseCode || '' + this.marks = [] + this.fillBatch() + if (this.error) { this.showBindingMessage('尚未绑定', this.error); return } + } this.message = '' let reason = '' if (!this.selectedSource) reason = '请先选择实际拆出货物的来源托盘。' else if (!this.samePallet(this.source, this.selectedSource)) reason = this.isReturnBulk ? '来源托盘识别失败,请重新选择。' : '请先扫描并确认实际来源托盘;选择来源列表不等于扫描确认。' else if (this.isReturnBulk && this.completedItems().length !== this.eligibleItems.length) reason = '请先完成本托盘的 SKU 贴标登记,再绑定新托盘;整托无需填写拆箱数量。' else if (!Number.isSafeInteger(Number(this.quantity)) || Number(this.quantity) <= 0 || Number(this.quantity) > this.selectedSource.movablePackages) reason = this.isReturnBulk ? '本托盘可操作件数不足,请刷新来源托盘。' : '请填写有效的拆出箱数,不能超过来源托盘可拆数量。' - else if (!this.selectedDestination || !this.bindingSelectionValid()) reason = this.isReturnBulk ? `本托盘拆箱数量必须等于贴标预报分配数量 ${this.plannedPackages} 件。` : '请选择本批目的仓和对应箱唛,勾选箱数必须等于拆出箱数。' + else if (!this.selectedDestination || !this.bindingSelectionValid()) reason = this.isReturnBulk ? `本托盘拆箱数量必须等于贴标预报分配数量 ${this.plannedPackages} 件。` : '本次拆出箱数必须对应已完成换标且尚未拆出的同目的仓箱唛。' else if (!this.targetCode.trim()) reason = '请扫描或输入目标托盘号。' if (reason) { this.error = reason; this.showBindingMessage('尚未绑定', reason); return } await this.resolve('target') if (this.error) { this.showBindingMessage('绑定失败', this.error); return } if (!this.readyToBind) { this.error = '来源和目标托盘不能相同,请核对后重新确认。'; this.showBindingMessage('尚未绑定', this.error); return } + if (!await this.confirmBindingChoice(`是否绑定目标托盘 ${this.target.palletCode}(${this.quantity} ${this.isReturnBulk ? '件' : '箱'})?`)) { this.target = null; return } await this.bind() }, destination(mark) { return this.task.items.find(item => item.cartonMark === mark)?.destinationFbaWarehouseCode || '目的仓待维护' }, @@ -198,7 +219,7 @@ export default { const payload = { relabelNo: this.task.relabelNo, source: expectation(this.source), target: expectation(this.target), cartonMarks: [...this.marks], actualSource: { positionId: this.selectedSource.positionId, expectedPositionVersion: this.selectedSource.positionVersion, packages: Number(this.quantity) }, reason: 'PDA 按原入仓单箱数拆托并按新目的仓合托' } const key = warehouseIdempotencyKey('relabel', 'bind-pallet', payload) let saved = false - await this.run(async () => { await bindRelabelPallet({ ...payload, idempotencyKey: key.value }); saved = true; releaseWarehouseKey('relabel', key.signature); const createdKey = warehouseIdempotencyKey('relabel', 'new-pallet', { relabelNo: this.task.relabelNo }); releaseWarehouseKey('relabel', createdKey.signature); this.message = `${this.marks.length} 箱已从 ${this.source.palletCode} 合入 ${this.target.palletCode},仍归属 ${this.selectedSource.inboundCode}`; this.marks = []; this.source = null; this.target = null; this.newPallet = null; this.sourceCode = ''; this.targetCode = ''; this.$emit('saved'); this.activate('source') }) + await this.run(async () => { await bindRelabelPallet({ ...payload, idempotencyKey: key.value }); saved = true; releaseWarehouseKey('relabel', key.signature); const createdKey = warehouseIdempotencyKey('relabel', 'new-pallet', { relabelNo: this.task.relabelNo }); releaseWarehouseKey('relabel', createdKey.signature); this.message = `托盘 ${this.target.palletCode} 已绑定`; this.marks = []; this.source = null; this.target = null; this.newPallet = null; this.sourceCode = ''; this.targetCode = ''; this.$emit('saved'); this.activate('source') }) if (saved) { this.showBindingMessage('绑定成功', this.message); await this.loadSources() } else if (this.error) this.showBindingMessage('绑定失败', this.error) }, @@ -219,8 +240,16 @@ export default { .heading{display:block;font-size:18px;font-weight:700} .refresh-button{flex:none;margin:0;padding:6px 14px;font-size:13px;line-height:1.5} .hint{display:block;font-size:13px;color:#52647c;margin:10px 0;white-space:normal} -.source-row{padding:12px;margin:10px 0;border:1px solid #d7e1ef;border-radius:8px} +.source-row{display:flex;align-items:center;gap:8px;padding:8px;margin:8px 0;border:1px solid #d7e1ef;border-radius:8px;white-space:nowrap} +.source-identifiers{flex:1;min-width:0} +.source-identifier{display:flex;align-items:baseline;gap:5px;max-width:100%;min-width:0} +.source-caption{flex-shrink:0;color:#718095;font-size:10px} +.source-code{min-width:0;color:#243951;font-size:11px;line-height:18px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.source-quantity{flex-shrink:0;color:#52647c;font-size:9px;line-height:16px} +.split-quantity-row{display:flex;align-items:center;gap:8px;margin:10px 0;color:#52647c;font-size:12px}.split-quantity-row input{box-sizing:border-box;width:72px;height:34px;padding:6px 8px;text-align:center;font-size:13px} +.split-progress{display:flex;align-items:center;flex-wrap:wrap;gap:8px 12px;margin:10px 0;color:#52647c;font-size:12px;line-height:20px}.split-progress strong{color:#1f5fca;font-weight:700}.split-progress .create-pallet-button{margin:0 0 0 auto;padding:0 10px;border-radius:6px;background:#22c55e;color:#fff;font-size:10px;font-weight:700;line-height:28px}.split-progress .create-pallet-button[disabled]{background:#bbf7d0;color:#6b987b}.split-progress .create-pallet-button::after{border:none} .source-row.selected{border-color:#2674d9;background:#eff6ff} +.binding-row{display:flex;align-items:center;gap:6px;box-sizing:border-box;width:100%;min-width:0;padding:8px 9px;font-size:11px;line-height:20px;text-align:left;white-space:nowrap}.binding-mark{max-width:25%;overflow:hidden;text-overflow:ellipsis}.binding-arrow,.binding-action{flex-shrink:0}.binding-pallet{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}.binding-action{color:#1f5fca;font-size:10px} .line{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin:8px 0}.line text{flex:1;overflow-wrap:anywhere} input{padding:12px;background:#f3f6fa;border-radius:6px}button{font-size:13px;margin:8px 0} .error{display:block;color:#bc3030;font-size:13px} diff --git a/src/data/overseas-workbench.js b/src/data/overseas-workbench.js index d99c613..ec649be 100644 --- a/src/data/overseas-workbench.js +++ b/src/data/overseas-workbench.js @@ -4,7 +4,8 @@ export const MODULES = [ { key: 'warehouseTransferList', name: '仓储转运测量列表', nameKey: 'modules.warehouseTransferList.name', icon: '列', iconType: 'list', description: '查看待测量的仓储转运单', descriptionKey: 'modules.warehouseTransferList.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=list' }, { key: 'warehouseTransferWork', name: '转运测量作业', nameKey: 'modules.warehouseTransferWork.name', icon: '转', iconType: 'compose', description: '进入转运单详情完成测量、打托和拍照', descriptionKey: 'modules.warehouseTransferWork.description', available: true, route: '/pages/warehouse-transfer/warehouse-transfer?mode=work' }, { key: 'returnReceipt', name: '退货入库', nameKey: 'modules.returnReceipt.name', icon: '退', iconType: 'undo', description: '扫描退货单或 SKU,按本包实收数量生成退货库存', descriptionKey: 'modules.returnReceipt.description', available: true, route: '/pages/return-receipt/return-receipt' }, - { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' } + { key: 'returnPacking', name: '退货合箱', nameKey: 'modules.returnPacking.name', icon: '合', iconType: 'folder-add', description: '选择同客户已签收退货单,按 SKU 数量生成新箱', descriptionKey: 'modules.returnPacking.description', available: true, route: '/pages/return-packing/return-packing' }, + { key: 'returnCounting', name: '退货箱内清点', icon: '清', iconType: 'list', description: '逐箱清点托盘签收货物,核对 SKU 和数量', available: true, route: '/pages/return-counting/return-counting' } ] }, { key: 'outbound', name: '出库履约', nameKey: 'modules.outbound.name', icon: '出', description: '排车拣货、出库任务与发车闭环', descriptionKey: 'modules.outbound.description', operations: [ { key: 'dispatchPicking', name: '排车拣货', nameKey: 'modules.dispatchPicking.name', icon: '车', iconType: 'paperplane', description: '按排车任务扫描托盘拣货,完成门禁后执行发车', descriptionKey: 'modules.dispatchPicking.description', available: true, route: '/pages/dispatch/dispatch' }, diff --git a/src/pages.json b/src/pages.json index 31803c6..efd3a9a 100644 --- a/src/pages.json +++ b/src/pages.json @@ -6,6 +6,7 @@ { "path": "pages/operation/operation", "style": { "navigationStyle": "custom", "navigationBarTitleText": "柜号入库与现场作业", "backgroundColor": "#F5F7FA" } }, { "path": "pages/warehouse-transfer/warehouse-transfer", "style": { "navigationStyle": "custom", "navigationBarTitleText": "仓储转运测量", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-receipt/return-receipt", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货入库", "backgroundColor": "#F5F7FA" } }, + { "path": "pages/return-counting/return-counting", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货箱内清点", "backgroundColor": "#F5F7FA" } }, { "path": "pages/return-packing/return-packing", "style": { "navigationStyle": "custom", "navigationBarTitleText": "退货合箱", "backgroundColor": "#F5F7FA" } }, { "path": "pages/pallet/pallet", "style": { "navigationStyle": "custom", "navigationBarTitleText": "托盘管理", "backgroundColor": "#F5F7FA" } }, { "path": "pages/stocktake/stocktake", "style": { "navigationStyle": "custom", "navigationBarTitleText": "库存盘点", "backgroundColor": "#F5F7FA" } }, diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue index 033ed8a..723da3c 100644 --- a/src/pages/dispatch/dispatch.vue +++ b/src/pages/dispatch/dispatch.vue @@ -5,14 +5,14 @@ 排车拣货与发车 - 客户与当前海外仓由登录令牌和服务端确定;扫描排车单号或任务中的托盘码开始作业 + 扫描排车单号打开任务,再扫描任务中的托盘码进行拣货 当前任务排车拣货详情 - 网络失败不会清空草稿,再次提交会沿用同一幂等键。 + 网络中断会保留当前作业,恢复后可重试。 正在加载排车任务… @@ -21,7 +21,7 @@ 错误码:{{ error.code }} requestId:{{ error.requestId }} traceId:{{ error.traceId }} - 网络连接失败,未完成作业与幂等键已保留。 + 网络连接失败,当前作业已保留,请恢复网络后重试。 @@ -154,17 +154,21 @@ const code = String(raw?.detail?.value ?? raw ?? this.scanCode).trim(); if (!code || this.busy || this.loading) return console.info('[Overseas PDA dispatch] scan', { code, task: this.current?.dispatchNo || '' }) this.scanCode = code + this.error = null; this.successMessage = '' + updateFulfillmentDraft('dispatch', { palletCode: '', dispatchItemId: '', cartonCode: '', packages: 1 }) if (!this.current || code === this.current.dispatchNo || code === this.current.taskNo) return this.openTask(code === this.current?.taskNo ? this.current.dispatchNo : code) const cartonItem = this.current.items.find((item) => item.returnCartons?.some((carton) => carton.cartonMark === code)) if (cartonItem) { if (cartonItem.returnCartons.find((carton) => carton.cartonMark === code)?.status !== 'OUTBOUND_PENDING') return this.setError(new Error('该箱已拣货或不可出库'), '扫码未匹配') this.selectPallet(cartonItem); updateFulfillmentDraft('dispatch', { cartonCode: code, packages: 1 }); this.scanCode = ''; playSuccessSound(); return } - const item = this.current.items.find((value) => [value.palletNo, value.fbaWarehouseCode].includes(code)) - if (!item) return this.setError(new Error('扫码未匹配当前排车任务的托盘或稳定业务码'), '扫码未匹配') + const matches = this.current.items.filter((value) => value.palletNo === code && value.scheduledPackages > value.pickedPackages) + if (matches.length > 1) return this.setError(new Error('此托盘包含多条待拣明细,请选择对应货物卡片'), '请选择拣货明细') + const item = matches[0] + if (!item) return this.setError(new Error(this.current.items.some((value) => value.palletNo === code) ? '该托盘已完成拣货,请勿重复操作' : '扫码未匹配当前任务的托盘码或来源箱唛'), '扫码未匹配') this.selectPallet(item); this.scanCode = ''; playSuccessSound() }, - selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, + selectPallet(item) { if (!item?.palletNo || item.scheduledPackages <= item.pickedPackages) return; this.error = null; this.successMessage = ''; updateFulfillmentDraft('dispatch', { palletCode: item.palletNo, dispatchItemId: item.dispatchItemId, cartonCode: '', packages: Math.max(1, Math.min(item.scheduledPackages - item.pickedPackages, item.positions?.[0]?.availablePackages || 1)) }) }, async submitPick() { if (!this.canPick) return const pick = this.selectedReturnCarton ? { positionId: null, cartonMark: this.selectedReturnCarton.cartonMark, packages: 1, productQuantity: this.selectedReturnCarton.productQuantity } : { positionId: this.selectedPosition.id, packages: Number(this.packages), productQuantity: 0 } diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index ee1cf5c..1deda49 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -6,7 +6,7 @@ 出库任务整托扫描托盘码,散箱扫描箱唛;扫描排车业务号会转入“排车拣货” 当前任务出库作业详情 - + 任务号直接恢复;退货整托出库扫托盘码,散箱出库扫箱唛。 @@ -27,18 +27,18 @@ {{ current.taskNo || '—' }}{{ statusLabel(current.status) }}出库单 {{ current.outboundNo || '—' }} · {{ taskType(current) }}{{ plannedPackages }}计划箱数{{ pickedPackages }}已拣箱数{{ remainingPackages }}待拣箱数 - + {{ item.palletNo || '散件出库' }}{{ item.pickedPackages }}/{{ item.plannedPackages }} 箱 {{ lineBusinessCode(position) }}{{ position.inboundCode || '无入仓码' }} · 已拣 {{ position.pickedPackages }} 箱 · 可拣 {{ position.availablePackages }} 箱 - 整托出库请扫描托盘码 {{ item.palletNo }} + 整托出库请扫描托盘号 {{ item.palletNo }} 当前明细缺少可扫码库存业务码,已禁止拣货;请刷新或联系平台确认任务下发。 任务详情尚无可拣货明细请刷新;若持续为空,请联系平台确认出库货物是否已下发。 - 本次拣货:{{ lineBusinessCode(selectedPosition) }}拣货箱数最多 {{ selectedRemaining }} 箱 + 本次拣货:{{ lineBusinessCode(selectedPosition) }}本来源计划出库 {{ selectedItem.returnSkuQuantity }} 件,请清点这些件数后确认;余数保留原箱。拣货箱数最多 {{ selectedRemaining }} 箱 完成出库任务尚有 {{ remainingPackages }} 箱未拣,不能完成出库。出库任务已完成,不可重复提交。全部计划货物已扫描核平,服务端将再次校验库存门禁。 @@ -68,6 +68,28 @@ components: { PortalHeader, ScanTargetInput }, data() { return { user: session.currentUser, tasks: [], discardTasks: [], current: null, discardCurrent: null, discardTask: null, discardActuals: {}, discardPalletCode: '', discardLoading: false, discardBusy: false, selectedItem: null, selectedPosition: null, selectedScanCode: '', scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } }, computed: { + displayItems() { + const items = this.current?.items || [] + if (this.current?.outboundType !== 'RETURN_OUTBOUND') return items + const pallets = new Map() + const rows = [] + for (const item of items) { + if (!item.palletNo) { rows.push(item); continue } + let pallet = pallets.get(item.palletNo) + if (!pallet) { + pallet = { ...item, displayKey: `pallet:${item.palletNo}`, plannedPackages: 0, pickedPackages: 0, positions: [] } + pallets.set(item.palletNo, pallet) + rows.push(pallet) + } + pallet.plannedPackages += Number(item.plannedPackages || 0) + pallet.pickedPackages += Number(item.pickedPackages || 0) + pallet.positions.push(...(item.positions || [])) + } + return rows + }, + hasReturnPallet() { return this.current?.outboundType === 'RETURN_OUTBOUND' && (this.current.items || []).some(item => item.palletNo) }, + scanLabel() { return this.discardCurrent ? '废弃订单托盘码' : this.hasReturnPallet ? '扫描托盘号(退货整托出库)' : '任务号、出库单号、排车单号、托盘号或箱码' }, + scanPlaceholder() { return this.discardCurrent ? '扫描废弃订单对应托盘码' : this.hasReturnPallet ? '扫描托盘号;散箱扫描箱唛' : '扫描任务号 / 出库单号 / 排车单号 / 托盘号 / 箱码' }, recentTaskNo() { return fulfillment.outbound.currentTaskNo || '' }, packages() { return fulfillment.outbound.packages }, plannedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.plannedPackages, 0) }, diff --git a/src/pages/relabel/relabel.vue b/src/pages/relabel/relabel.vue index 8c6f3bd..63837c6 100644 --- a/src/pages/relabel/relabel.vue +++ b/src/pages/relabel/relabel.vue @@ -2,27 +2,27 @@ - {{ title }}{{ stageSubtitle }} - 当前任务{{ stage === 'review' ? '换标复核详情' : '换标执行详情' }} + {{ title }}{{ stageSubtitle }} + 换标类型:{{ relabelTypeName(current.operationType) }} {{ current.relabelNo }}客户订单:{{ current.customerOrderNo || '—' }}标签截止:{{ formatTime(current.labelDeadline) }}{{ statusLabel(current.taskStatus || current.applicationStatus) }} - {{ current.expectedPackages }}应处理{{ current.relabelledPackages }}已换标{{ current.skippedPackages }}跳过{{ current.exceptionPackages }}异常 + FBA 目的仓:{{ destination }}待维护应处理 {{ current.expectedPackages }}已换标 {{ current.relabelledPackages }}跳过 {{ current.skippedPackages }}异常 {{ current.exceptionPackages }} 标签文件暂无标签文件{{ file.scope === 'CARTON_TOTAL' ? '总箱唛' : file.scope === 'SKU_TOTAL' ? '总 SKU 标签' : '明细标签' }}:{{ file.fileName }}