优化
This commit is contained in:
@@ -4,6 +4,8 @@ const BASE = '/pda/v1/last-leg/pallet-operations'
|
||||
export const findRelabelSources = (relabelNo) => overseasRequest('POST', `${BASE}/relabel-sources`, { relabelNo })
|
||||
export const createRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-pallet-label`, payload)
|
||||
export const bindRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-disassemble`, payload)
|
||||
export const listRelabelPalletMeasurements = (relabelNo) => overseasRequest('POST', `${BASE}/relabel-pallet-measurements`, { relabelNo })
|
||||
export const measureRelabelPallet = (payload) => overseasRequest('POST', `${BASE}/relabel-pallet-measure`, payload)
|
||||
|
||||
export function resolvePallet(code) {
|
||||
return overseasRequest('POST', `${BASE}/pallets/resolve`, { code: String(code || '').trim() })
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
<button v-for="binding in currentBindings" :key="binding.mark" class="binding-row" :disabled="busy" @tap="selectRebind(binding)"><text class="binding-mark">{{ binding.mark }}</text><text class="binding-arrow">→</text><text class="binding-pallet">{{ binding.pallet }}</text><text class="binding-action">重新绑定</text></button>
|
||||
</view>
|
||||
<view v-for="pallet in boundPalletTotals" :key="pallet.palletCode" class="hint"><text>{{ pallet.palletCode }} · {{ pallet.totalPackages }} 箱 · 体积 {{ pallet.volumeCbm == null ? '待补测' : pallet.volumeCbm + ' m³' }} · 重量 {{ pallet.grossWeightKg == null ? '待补测' : pallet.grossWeightKg + ' kg' }}</text></view>
|
||||
<view v-for="pallet in measurements" :key="pallet.palletCode" class="measurement-card">
|
||||
<text class="heading">{{ pallet.palletCode }} · {{ pallet.packages }} 箱</text>
|
||||
<text class="hint">{{ pallet.inherited ? '原托盘货物未变,沿用原测量;包装变化请重新测量。' : '拆托、合托后请确认本托最终尺寸和毛重,原托盘剩余货物也需补测。' }}</text>
|
||||
<text v-if="pallet.volumeCbm != null" class="hint">已确认:{{ pallet.volumeCbm }} m³ · {{ pallet.grossWeightKg }} kg</text>
|
||||
<view class="measurement-inputs">
|
||||
<label v-for="field in measurementFields" :key="field.key"><text>{{ field.label }}</text><input v-model="pallet[field.key]" type="digit" :disabled="busy" :placeholder="field.label" /></label>
|
||||
</view>
|
||||
<button :disabled="busy" @tap="saveMeasurement(pallet)">确认本托实测</button>
|
||||
</view>
|
||||
<view v-if="rebindSelection"><text class="hint">正在改绑:{{ rebindSelection.mark }},当前托盘:{{ rebindSelection.pallet }}</text><button :disabled="busy" @tap="rebindSelection = null; targetCode = ''; target = null">取消改绑</button></view>
|
||||
<template v-if="!allBindingsComplete && !rebindSelection">
|
||||
<view v-for="row in operableSourceRows" :key="row.positionId" class="source-row" :class="{ selected: selectedSource?.positionId === row.positionId }" @tap="selectSource(row)">
|
||||
@@ -34,6 +43,7 @@
|
||||
</template>
|
||||
<template v-if="!allBindingsComplete || rebindSelection">
|
||||
<ScanTargetInput v-model="targetCode" name="relabel-target" label="目标托盘" placeholder="扫描同目的仓目标托盘或新托盘号" :active="active === 'target'" @activate="activate('target')" @submit="confirmTargetAndBind" />
|
||||
<button v-if="canRetainOriginal" :disabled="busy" @tap="quantity = String(source.totalPackages); targetCode = source.palletCode; confirmTargetAndBind()">整托全部贴标,保留原托盘</button>
|
||||
<button :disabled="busy || !targetCode.trim()" @tap="confirmTargetAndBind">{{ rebindSelection ? '确认改绑目标托盘' : '确认绑定目标托盘' }}</button>
|
||||
<text v-if="labelBlockReason" class="hint">{{ labelBlockReason }}</text>
|
||||
<text v-if="target" class="hint">已识别目标:{{ target.palletCode }},尚未绑定成功</text>
|
||||
@@ -44,16 +54,17 @@
|
||||
</template>
|
||||
<script>
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { resolvePallet, createRelabelPallet, bindRelabelPallet, findRelabelSources } from '@/api/pallet.js'
|
||||
import { resolvePallet, createRelabelPallet, bindRelabelPallet, findRelabelSources, listRelabelPalletMeasurements, measureRelabelPallet } from '@/api/pallet.js'
|
||||
import { warehouseIdempotencyKey, releaseWarehouseKey } from '@/stores/warehouse-work.js'
|
||||
export default {
|
||||
components: { ScanTargetInput },
|
||||
props: { task: { type: Object, required: true }, allowDisassemble: { type: Boolean, default: true } },
|
||||
emits: ['activate', 'busy', 'saved'],
|
||||
data() { return { palletTotals: {}, rebindSelection: null, active: 'source', sourceCode: '', targetCode: '', source: null, target: null, marks: [], busy: false, error: '', message: '', newPallet: null, sourceRows: [], selectedSource: null, quantity: '', selectedDestination: '' } },
|
||||
data() { return { measurements: [], measurementFields: [{ key: 'lengthCm', label: '长 cm' }, { key: 'widthCm', label: '宽 cm' }, { key: 'heightCm', label: '高 cm' }, { key: 'grossWeightKg', label: '毛重 kg' }], palletTotals: {}, 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: {
|
||||
boundPalletTotals() { const codes = new Set(this.currentBindings.map(binding => binding.pallet)); return Object.values(this.palletTotals).filter(pallet => codes.has(pallet.palletCode)) },
|
||||
canRetainOriginal() { return !this.isReturnBulk && !this.unboundSourceMode && this.source && this.selectedSource && this.plannedPackages === Number(this.source.totalPackages) },
|
||||
boundPalletTotals() { const codes = new Set(this.currentBindings.map(binding => binding.pallet)); return Object.values(this.palletTotals).filter(pallet => codes.has(pallet.palletCode) && !this.measurements.some(measured => measured.palletCode === pallet.palletCode)) },
|
||||
allBindingsComplete() {
|
||||
const required = (this.task.items || []).filter(item => Number(item.expectedPackages || 0) > 0 && (item.returnCartonId || ['CARTON_LABEL', 'CARTON_AND_SKU', 'RETURN_BULK_SKU_LABEL'].includes(item.operationType)))
|
||||
const bound = new Set(this.currentBindings.map(binding => binding.mark))
|
||||
@@ -89,9 +100,21 @@ export default {
|
||||
},
|
||||
wholeSource() { return this.isReturnBulk && this.selectedSource && (!this.allowDisassemble || this.plannedPackages === Number(this.selectedSource.stockPackages || 0)) },
|
||||
unboundSourceMode() { return !this.isReturnBulk && this.returnCartonItems.length > 0 && (this.selectedSource ? (!this.selectedSource.palletCode || this.selectedSource.palletCode === 'UNBOUND') : !this.operableSourceRows.some(row => row.palletCode && row.palletCode !== 'UNBOUND')) },
|
||||
readyToBind() { return this.target && this.selectedSource && (this.unboundSourceMode || (this.source && this.samePallet(this.source, this.selectedSource) && !this.samePallet(this.source, this.target))) && Number.isSafeInteger(Number(this.quantity)) && Number(this.quantity) > 0 && Number(this.quantity) <= this.selectedSource.movablePackages && this.bindingSelectionValid() }
|
||||
readyToBind() { return this.target && this.selectedSource && (this.unboundSourceMode || (this.source && this.samePallet(this.source, this.selectedSource) && (!this.samePallet(this.source, this.target) || this.canRetainOriginal))) && Number.isSafeInteger(Number(this.quantity)) && Number(this.quantity) > 0 && Number(this.quantity) <= this.selectedSource.movablePackages && this.bindingSelectionValid() }
|
||||
},
|
||||
methods: {
|
||||
async saveMeasurement(pallet) {
|
||||
const dimensions = Object.fromEntries(this.measurementFields.map(field => [field.key, Number(pallet[field.key])]))
|
||||
if (!Object.values(dimensions).every(value => Number.isFinite(value) && value > 0)) { this.error = '请填写有效的长、宽、高和毛重'; return }
|
||||
const payload = { relabelNo: this.task.relabelNo, palletCode: pallet.palletCode, stockVersion: pallet.stockVersion, ...dimensions }
|
||||
const key = warehouseIdempotencyKey('relabel', 'measure-pallet', payload)
|
||||
await this.run(async () => {
|
||||
this.measurements = await measureRelabelPallet({ ...payload, idempotencyKey: key.value })
|
||||
releaseWarehouseKey('relabel', key.signature)
|
||||
this.message = `托盘 ${pallet.palletCode} 实测已保存`
|
||||
this.$emit('saved')
|
||||
})
|
||||
},
|
||||
savePalletTotals(result) { for (const pallet of result?.pallets || []) this.palletTotals = { ...this.palletTotals, [pallet.palletCode]: pallet } },
|
||||
samePallet(left, right) {
|
||||
if (!left || !right) return false
|
||||
@@ -140,7 +163,7 @@ export default {
|
||||
},
|
||||
sourceItems() { return (this.task.items || []).filter(item => this.selectedSource?.cartonMarks.includes(item.cartonMark)) },
|
||||
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 () => { try { this.sourceRows = await findRelabelSources(this.task.relabelNo) } catch (error) { if (!this.isReturnBulk && this.returnCartonItems.length) this.sourceRows = []; else throw error } if (!this.isReturnBulk && this.returnCartonItems.length && !this.sourceRows.some(row => Number(row.movablePackages) > 0)) this.sourceRows = [{ positionId: this.returnCartonItems[0].returnCartonId, positionVersion: 0, palletCode: 'UNBOUND', stockPackages: this.returnCartonItems.length, movablePackages: this.returnCartonItems.length, cartonMarks: this.returnCartonItems.map(item => item.cartonMark).filter(Boolean) }]; for (const code of new Set(this.currentBindings.map(binding => binding.pallet))) { const pallet = await resolvePallet(code); this.savePalletTotals({ pallets: [pallet] }) } this.selectedSource = null; this.source = null; this.marks = []; this.quantity = ''; if (this.unboundSourceMode && this.operableSourceRows.length === 1) { this.selectedSource = this.operableSourceRows[0]; this.selectedDestination = this.returnCartonItems[0]?.destinationFbaWarehouseCode || ''; this.marks = this.completedItems().map(item => item.cartonMark); this.quantity = String(this.marks.length); this.activate('target') } }) },
|
||||
async loadSources() { await this.run(async () => { try { this.sourceRows = await findRelabelSources(this.task.relabelNo) } catch (error) { if (!this.isReturnBulk && this.returnCartonItems.length) this.sourceRows = []; else throw error } if (!this.isReturnBulk && this.returnCartonItems.length && !this.sourceRows.some(row => Number(row.movablePackages) > 0)) this.sourceRows = [{ positionId: this.returnCartonItems[0].returnCartonId, positionVersion: 0, palletCode: 'UNBOUND', stockPackages: this.returnCartonItems.length, movablePackages: this.returnCartonItems.length, cartonMarks: this.returnCartonItems.map(item => item.cartonMark).filter(Boolean) }]; for (const code of new Set(this.currentBindings.map(binding => binding.pallet))) { const pallet = await resolvePallet(code); this.savePalletTotals({ pallets: [pallet] }) } this.measurements = await listRelabelPalletMeasurements(this.task.relabelNo); this.selectedSource = null; this.source = null; this.marks = []; this.quantity = ''; if (this.unboundSourceMode && this.operableSourceRows.length === 1) { this.selectedSource = this.operableSourceRows[0]; this.selectedDestination = this.returnCartonItems[0]?.destinationFbaWarehouseCode || ''; this.marks = this.completedItems().map(item => item.cartonMark); this.quantity = String(this.marks.length); this.activate('target') } }) },
|
||||
async selectSource(row) {
|
||||
if (this.busy) return
|
||||
this.rebindSelection = null; this.selectedSource = row; this.source = null; this.sourceCode = ''; this.quantity = ''; this.marks = []
|
||||
@@ -223,7 +246,7 @@ export default {
|
||||
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 }
|
||||
if (!await this.confirmBindingChoice(`是否绑定目标托盘 ${this.target.palletCode}(${this.quantity} ${this.isReturnBulk ? '件' : '箱'})?${this.samePallet(this.source, this.target) ? '整托保留原托盘且货物和包装未变可沿用原测量;包装变化请重新补测。' : ''}`)) { this.target = null; return }
|
||||
await this.bind()
|
||||
},
|
||||
destination(mark) { return this.task.items.find(item => item.cartonMark === mark)?.destinationFbaWarehouseCode || '目的仓待维护' },
|
||||
@@ -278,6 +301,9 @@ export default {
|
||||
</script>
|
||||
<style scoped>
|
||||
.pallet-work{margin-top:18px;padding:14px;border:1px solid #d7e1ef;border-radius:10px}
|
||||
.measurement-card{margin:12px 0;padding:12px;border:1px solid #d7e1ef;border-radius:8px}
|
||||
.measurement-inputs{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:10px 0}
|
||||
.measurement-inputs input{padding:8px;border:1px solid #d7e1ef;border-radius:6px}
|
||||
.pallet-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
||||
.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}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
const source = await readFile(new URL('../src/components/RelabelPalletWork.vue', import.meta.url), 'utf8')
|
||||
const script = source.split('<script>')[1].split('</script>')[0].replace(/^import .*$/gm, '').replace('export default', 'return')
|
||||
function setup() {
|
||||
const calls = []
|
||||
const component = new Function('ScanTargetInput', 'measureRelabelPallet', 'warehouseIdempotencyKey', 'releaseWarehouseKey', script)(
|
||||
{}, async payload => { calls.push(payload); return [{ palletCode: payload.palletCode, volumeCbm: 0.25, grossWeightKg: payload.grossWeightKg }] },
|
||||
() => ({ value: 'stable-key', signature: 'signature' }), () => {})
|
||||
const value = { ...component.data(), task: { relabelNo: 'RL-1' }, $emit() {} }
|
||||
for (const [key, method] of Object.entries(component.methods)) value[key] = method.bind(value)
|
||||
return { component, value, calls }
|
||||
}
|
||||
test('pallet measurement submits actual dimensions, weight and current stock version and refreshes results', async () => {
|
||||
const { value, calls } = setup()
|
||||
await value.saveMeasurement({ palletCode: 'NEW', stockVersion: 'position:version:count', lengthCm: '100', widthCm: '50', heightCm: '50', grossWeightKg: '40' })
|
||||
assert.deepEqual(calls, [{ relabelNo: 'RL-1', palletCode: 'NEW', stockVersion: 'position:version:count', lengthCm: 100, widthCm: 50, heightCm: 50, grossWeightKg: 40, idempotencyKey: 'stable-key' }])
|
||||
assert.equal(value.measurements[0].volumeCbm, 0.25)
|
||||
assert.equal(value.busy, false)
|
||||
await value.saveMeasurement({ palletCode: 'NEW', lengthCm: 0, widthCm: 50, heightCm: 50, grossWeightKg: 40 })
|
||||
assert.equal(calls.length, 1)
|
||||
assert.match(value.error, /有效/)
|
||||
})
|
||||
test('only whole physical pallet quantities offer original pallet reuse', () => {
|
||||
const { component } = setup()
|
||||
const canRetain = component.computed.canRetainOriginal
|
||||
const ctx = { source: { totalPackages: 10 }, selectedSource: {}, plannedPackages: 10, isReturnBulk: false, unboundSourceMode: false }
|
||||
assert.equal(canRetain.call(ctx), true)
|
||||
ctx.plannedPackages = 4
|
||||
assert.equal(canRetain.call(ctx), false)
|
||||
ctx.plannedPackages = 10;ctx.isReturnBulk = true
|
||||
assert.equal(canRetain.call(ctx), false)
|
||||
})
|
||||
Reference in New Issue
Block a user