优化
This commit is contained in:
@@ -9,13 +9,14 @@
|
|||||||
<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>
|
<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>
|
||||||
<view v-if="rebindSelection"><text class="hint">正在改绑:{{ rebindSelection.mark }},当前托盘:{{ rebindSelection.pallet }}</text><button :disabled="busy" @tap="rebindSelection = null; targetCode = ''; target = null">取消改绑</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)">
|
<view v-for="row in operableSourceRows" :key="row.positionId" class="source-row" :class="{ selected: selectedSource?.positionId === row.positionId }" @tap="selectSource(row)">
|
||||||
<view class="source-identifiers"><view class="source-identifier"><text class="source-caption">托盘号</text><text class="source-code">{{ row.palletCode }}</text></view></view>
|
<view class="source-identifiers"><view class="source-identifier"><text class="source-caption">托盘号</text><text class="source-code">{{ row.palletCode }}</text></view></view>
|
||||||
<text class="source-quantity">{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.stockPackages }} · 可拆{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.movablePackages }}</text>
|
<text class="source-quantity">{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.stockPackages }} · 可拆{{ isReturnBulk ? '件数' : '箱数' }}:{{ row.movablePackages }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text v-if="!busy && !operableSourceRows.length" class="hint">当前没有可操作的来源托盘</text>
|
<text v-if="!busy && !operableSourceRows.length" class="hint">当前没有可操作的来源托盘</text>
|
||||||
<ScanTargetInput v-if="!isReturnBulk" v-model="sourceCode" name="relabel-source" label="实际来源托盘" placeholder="扫描实际来源托盘" :active="active === 'source'" @activate="activate('source')" @submit="confirmSourceScan" />
|
<ScanTargetInput v-if="!isReturnBulk" v-model="sourceCode" name="relabel-source" label="实际来源托盘" placeholder="扫描实际来源托盘" :active="active === 'source'" @activate="activate('source')" @submit="confirmSourceScan" />
|
||||||
<text v-if="source" class="hint">已确认:{{ source.palletCode }}</text>
|
<text v-if="source" class="hint">来源已确认:{{ source.palletCode }};尚未拆出,请完成贴标登记、核对拆出数量并扫描目标托盘,绑定成功后才保存拆托明细。</text>
|
||||||
<view v-if="selectedSource">
|
<view v-if="selectedSource">
|
||||||
<text v-if="isReturnBulk" class="hint">本次{{ wholeSource ? '整托换标数量' : '拆出件数' }}(可分多个来源托盘,逐批提交)</text>
|
<text v-if="isReturnBulk" class="hint">本次{{ wholeSource ? '整托换标数量' : '拆出件数' }}(可分多个来源托盘,逐批提交)</text>
|
||||||
<template v-if="isReturnBulk">
|
<template v-if="isReturnBulk">
|
||||||
@@ -28,9 +29,14 @@
|
|||||||
<template v-else><view class="split-quantity-row"><text>本次拆出箱数</text><input v-model="quantity" type="number" :disabled="busy" placeholder="箱数" @input="marks = []" /><text>箱</text></view></template>
|
<template v-else><view class="split-quantity-row"><text>本次拆出箱数</text><input v-model="quantity" type="number" :disabled="busy" placeholder="箱数" @input="marks = []" /><text>箱</text></view></template>
|
||||||
</view>
|
</view>
|
||||||
<view class="split-progress"><template v-if="!isReturnBulk"><text>本次需换标拆 <strong>{{ requiredSplitPackages }}</strong> 箱</text><text>已拆 <strong>{{ splitPackages }}</strong> 箱</text></template><button class="create-pallet-button" :disabled="busy || !!newPallet" @tap="createPallet">新增托盘号</button></view>
|
<view class="split-progress"><template v-if="!isReturnBulk"><text>本次需换标拆 <strong>{{ requiredSplitPackages }}</strong> 箱</text><text>已拆 <strong>{{ splitPackages }}</strong> 箱</text></template><button class="create-pallet-button" :disabled="busy || !!newPallet" @tap="createPallet">新增托盘号</button></view>
|
||||||
|
</template>
|
||||||
|
<template v-if="!allBindingsComplete || rebindSelection">
|
||||||
<ScanTargetInput v-model="targetCode" name="relabel-target" label="目标托盘" placeholder="扫描同目的仓目标托盘或新托盘号" :active="active === 'target'" @activate="activate('target')" @submit="confirmTargetAndBind" />
|
<ScanTargetInput v-model="targetCode" name="relabel-target" label="目标托盘" placeholder="扫描同目的仓目标托盘或新托盘号" :active="active === 'target'" @activate="activate('target')" @submit="confirmTargetAndBind" />
|
||||||
|
<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>
|
<text v-if="target" class="hint">已识别目标:{{ target.palletCode }},尚未绑定成功</text>
|
||||||
<view v-if="newPallet" class="hint"><text>托盘 {{ newPallet.palletCode }} 待绑定</text><button @tap="printPallet">查看 / 打印托盘标签</button></view>
|
<view v-if="newPallet" class="hint"><text>托盘 {{ newPallet.palletCode }} 待绑定</text><button @tap="printPallet">查看 / 打印托盘标签</button></view>
|
||||||
|
</template>
|
||||||
<text v-if="message" class="hint">{{ message }}</text><text v-if="error" class="error">{{ error }}</text>
|
<text v-if="message" class="hint">{{ message }}</text><text v-if="error" class="error">{{ error }}</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,6 +51,11 @@ 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: '' } },
|
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() },
|
mounted() { this.loadSources() },
|
||||||
computed: {
|
computed: {
|
||||||
|
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))
|
||||||
|
return required.length > 0 && required.every(item => bound.has(item.cartonMark))
|
||||||
|
},
|
||||||
requiredSplitPackages() { return (this.task.items || []).filter(item => ['CARTON_LABEL', 'CARTON_AND_SKU'].includes(item.operationType)).reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0) },
|
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) },
|
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() {
|
currentBindings() {
|
||||||
@@ -65,6 +76,13 @@ export default {
|
|||||||
isReturnBulk() { return this.task.operationType === 'RETURN_BULK_SKU_LABEL' },
|
isReturnBulk() { return this.task.operationType === 'RETURN_BULK_SKU_LABEL' },
|
||||||
operableSourceRows() { return this.sourceRows.filter(row => Number(row.movablePackages) > 0) },
|
operableSourceRows() { return this.sourceRows.filter(row => Number(row.movablePackages) > 0) },
|
||||||
plannedPackages() { return this.eligibleItems.reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0) },
|
plannedPackages() { return this.eligibleItems.reduce((sum, item) => sum + Number(item.expectedPackages || 0), 0) },
|
||||||
|
labelBlockReason() {
|
||||||
|
const pending = this.sourceItems().filter(item => Number(item.relabelledPackages || 0) < Number(item.expectedPackages || 1))
|
||||||
|
if (!pending.length) return ''
|
||||||
|
const missingSku = pending.some(item => item.operationType === 'CARTON_AND_SKU' && Number(item.skuProcessedPackages || 0) < Number(item.expectedPackages || 1))
|
||||||
|
if (missingSku) return '本托盘还有未完成的 SKU 贴标登记。混合换标须完成箱唛和 SKU 两项,请返回 SKU 页签处理后再确认绑定。'
|
||||||
|
return '本托盘还有未完成的换标明细,请先完成贴标登记,再确认目标托盘绑定。'
|
||||||
|
},
|
||||||
wholeSource() { return this.isReturnBulk && this.selectedSource && (!this.allowDisassemble || this.plannedPackages === Number(this.selectedSource.stockPackages || 0)) },
|
wholeSource() { return this.isReturnBulk && this.selectedSource && (!this.allowDisassemble || this.plannedPackages === Number(this.selectedSource.stockPackages || 0)) },
|
||||||
readyToBind() { return this.source && this.target && this.selectedSource && 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.source && this.target && this.selectedSource && 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() }
|
||||||
},
|
},
|
||||||
@@ -157,13 +175,14 @@ export default {
|
|||||||
await this.resolve('source')
|
await this.resolve('source')
|
||||||
if (this.error || !this.source) return
|
if (this.error || !this.source) return
|
||||||
const source = this.source; this.source = null
|
const source = this.source; this.source = null
|
||||||
if (await this.confirmBindingChoice(`是否绑定来源托盘 ${source.palletCode}?`)) this.source = source
|
if (await this.confirmBindingChoice(`是否绑定来源托盘 ${source.palletCode}?`)) { this.source = source; this.activate('target') }
|
||||||
},
|
},
|
||||||
showBindingMessage(title, content) { uni.showModal({ title, content, showCancel: false, confirmText: '知道了' }) },
|
showBindingMessage(title, content) { uni.showModal({ title, content, showCancel: false, confirmText: '知道了' }) },
|
||||||
async confirmTargetAndBind() {
|
async confirmTargetAndBind() {
|
||||||
if (this.busy) return
|
if (this.busy) return
|
||||||
if (this.rebindSelection) return this.submitRebind()
|
if (this.rebindSelection) return this.submitRebind()
|
||||||
if (!this.isReturnBulk && this.selectedSource && this.samePallet(this.source, this.selectedSource)) {
|
if (!this.isReturnBulk && this.selectedSource && this.samePallet(this.source, this.selectedSource)) {
|
||||||
|
if (!this.completedItems().length && this.labelBlockReason) { this.error = this.labelBlockReason; this.showBindingMessage('尚未绑定', this.error); return }
|
||||||
this.selectedDestination = this.completedItems()[0]?.destinationFbaWarehouseCode || ''
|
this.selectedDestination = this.completedItems()[0]?.destinationFbaWarehouseCode || ''
|
||||||
this.marks = []
|
this.marks = []
|
||||||
this.fillBatch()
|
this.fillBatch()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<view v-if="availableTabs.length > 1" class="tab-row" role="tablist"><button v-for="tab in availableTabs" :key="tab.key" class="tab-button" role="tab" :aria-selected="taskTab === tab.key" :class="{ active: taskTab === tab.key }" :disabled="busy" @tap="changeTab(tab.key)">{{ tab.name }}</button></view>
|
<view v-if="availableTabs.length > 1" class="tab-row" role="tablist"><button v-for="tab in availableTabs" :key="tab.key" class="tab-button" role="tab" :aria-selected="taskTab === tab.key" :class="{ active: taskTab === tab.key }" :disabled="busy" @tap="changeTab(tab.key)">{{ tab.name }}</button></view>
|
||||||
<text class="card-title section-gap">{{ isReturnBulk ? '退货贴标 SKU 明细' : taskTab === 'sku' ? 'SKU 换标明细' : '箱唛换标明细' }}</text>
|
<text class="card-title section-gap">{{ isReturnBulk ? '退货贴标 SKU 明细' : taskTab === 'sku' ? 'SKU 换标明细' : '箱唛换标明细' }}</text>
|
||||||
<view v-if="isReturnTask" class="section-gap"><text class="hint">先扫描来源包裹/箱,再核对商品 SKU、目标标签和实际数量。</text><ScanTargetInput name="returnCarton" v-model="returnCartonCode" label="来源包裹/箱码" placeholder="扫描真实来源包裹/箱" :active="activeTarget === 'return-carton'" @activate="activeTarget = 'return-carton'" @submit="verifyReturnCarton" /><button class="secondary" :disabled="busy" @tap="verifyReturnCarton(returnCartonCode)">确认来源包裹/箱</button><text v-if="verifiedReturnCarton" class="success-title">已确认:{{ verifiedReturnCarton }}</text></view>
|
<view v-if="isReturnTask && taskTab !== 'carton'" class="section-gap"><text class="hint">先扫描来源包裹/箱,再核对商品 SKU、目标标签和实际数量。</text><ScanTargetInput name="returnCarton" v-model="returnCartonCode" label="来源包裹/箱码" placeholder="扫描真实来源包裹/箱" :active="activeTarget === 'return-carton'" @activate="activeTarget = 'return-carton'" @submit="verifyReturnCarton" /><button class="secondary" :disabled="busy" @tap="verifyReturnCarton(returnCartonCode)">确认来源包裹/箱</button><text v-if="verifiedReturnCarton" class="success-title">已确认:{{ verifiedReturnCarton }}</text></view>
|
||||||
<view v-if="isReturnBulk" class="section-gap"><ScanTargetInput name="returnSku" v-model="returnSkuCode" label="商品 SKU" placeholder="扫描本来源待换标 SKU" :active="activeTarget === 'return-sku'" @activate="activeTarget = 'return-sku'" @submit="verifyReturnSku" /><button class="secondary" @tap="verifyReturnSku(returnSkuCode)">核对商品 SKU</button><text v-for="group in returnBulkSkuGroups" :key="group.key" class="meta-line">{{ group.skuCode }} · 汇总待贴 {{ group.remaining }} 件(不自动分配)</text></view>
|
<view v-if="isReturnBulk" class="section-gap"><ScanTargetInput name="returnSku" v-model="returnSkuCode" label="商品 SKU" placeholder="扫描本来源待换标 SKU" :active="activeTarget === 'return-sku'" @activate="activeTarget = 'return-sku'" @submit="verifyReturnSku" /><button class="secondary" @tap="verifyReturnSku(returnSkuCode)">核对商品 SKU</button><text v-for="group in returnBulkSkuGroups" :key="group.key" class="meta-line">{{ group.skuCode }} · 汇总待贴 {{ group.remaining }} 件(不自动分配)</text></view>
|
||||||
<template v-if="isReturnBulk"><view v-for="item in returnBulkSources" :key="item.itemId" class="item-card section-gap"><text class="business-code">{{ item.cartonMark }} · {{ item.skuCode }}</text><text class="meta-line">目标商品标签:{{ item.newSkuCode || '未提供,禁止登记' }} · 本来源剩余 {{ remainingReturnQuantity(item) }} 件</text><ScanTargetInput :name="'target-' + item.itemId" v-model="returnTargetLabels[item.itemId]" label="目标商品 SKU 标签" placeholder="扫描目标商品标签" :active="activeTarget === 'target-' + item.itemId" @activate="activeTarget = 'target-' + item.itemId" /><input v-model="bulkQuantities[item.itemId]" type="number" placeholder="必填:本来源本次实际贴标件数" /><button class="secondary" :disabled="busy" @tap="fillAllReturn(item)">本来源全部(需确认)</button><button class="primary" :disabled="busy" @tap="completeBulkSku(item)">登记本来源实际数量</button></view></template>
|
<template v-if="isReturnBulk"><view v-for="item in returnBulkSources" :key="item.itemId" class="item-card section-gap"><text class="business-code">{{ item.cartonMark }} · {{ item.skuCode }}</text><text class="meta-line">目标商品标签:{{ item.newSkuCode || '未提供,禁止登记' }} · 本来源剩余 {{ remainingReturnQuantity(item) }} 件</text><ScanTargetInput :name="'target-' + item.itemId" v-model="returnTargetLabels[item.itemId]" label="目标商品 SKU 标签" placeholder="扫描目标商品标签" :active="activeTarget === 'target-' + item.itemId" @activate="activeTarget = 'target-' + item.itemId" /><input v-model="bulkQuantities[item.itemId]" type="number" placeholder="必填:本来源本次实际贴标件数" /><button class="secondary" :disabled="busy" @tap="fillAllReturn(item)">本来源全部(需确认)</button><button class="primary" :disabled="busy" @tap="completeBulkSku(item)">登记本来源实际数量</button></view></template>
|
||||||
<view v-if="taskTab === 'carton' && !isReturnBulk" class="section-gap"><ScanTargetInput name="quickCarton" v-model="quickCarton" label="仓库唛头" placeholder="扫描箱唛自动确认,手输后按完成" :active="activeTarget === 'quick-carton'" @activate="activeTarget = 'quick-carton'" @submit="submitCarton" /></view>
|
<view v-if="taskTab === 'carton' && !isReturnBulk" class="section-gap"><ScanTargetInput name="quickCarton" v-model="quickCarton" label="仓库唛头" placeholder="扫描箱唛自动确认,手输后按完成" :active="activeTarget === 'quick-carton'" @activate="activeTarget = 'quick-carton'" @submit="submitCarton" /></view>
|
||||||
@@ -46,8 +46,9 @@
|
|||||||
<button class="primary" :disabled="busy || !canSubmitUnit" @tap="submitUnit">{{ busyAction === 'unit' ? '提交中…' : unitActionText }}</button>
|
<button class="primary" :disabled="busy || !canSubmitUnit" @tap="submitUnit">{{ busyAction === 'unit' ? '提交中…' : unitActionText }}</button>
|
||||||
</template>
|
</template>
|
||||||
<view class="section-gap">
|
<view class="section-gap">
|
||||||
<ReturnRelabelPackWork v-if="isReturnTask" :key="current.relabelNo + ':' + (current.reviewedAt || 'INITIAL')" :task="current" :locked="busy" @saved="applyCurrent" @refresh="reloadCurrent" @error="setError" @busy="busy = $event" />
|
<ReturnRelabelPackWork v-if="showReturnPack" :key="current.relabelNo + ':' + (current.reviewedAt || 'INITIAL')" :task="current" :locked="busy" @saved="applyCurrent" @refresh="reloadCurrent" @error="setError" @busy="busy = $event" />
|
||||||
<RelabelPalletWork v-if="requiresPalletBinding && (taskTab === 'carton' || isReturnTask)" ref="palletWork" :key="current.relabelNo" :task="palletTask" :allow-disassemble="true" @activate="activeTarget = 'pallet-work'" @busy="busy = $event" @saved="reloadCurrent" />
|
<RelabelPalletWork v-if="requiresPalletBinding && (taskTab === 'carton' || isReturnTask)" ref="palletWork" :key="current.relabelNo" :task="palletTask" :allow-disassemble="true" @activate="activeTarget = 'pallet-work'" @busy="busy = $event" @saved="reloadCurrent" />
|
||||||
|
<button class="primary" :disabled="busy || Boolean(completionBlockReason)" @tap="completeTask">{{ busyAction === 'complete' ? '提交中…' : allProcessed ? '完成现场换标并等待复核' : '手动完成并登记异常' }}</button>
|
||||||
<view class="row-head"><text class="card-title">完成照片</text><text class="status-pill">{{ photoFileIds.length }} / 20 张</text></view>
|
<view class="row-head"><text class="card-title">完成照片</text><text class="status-pill">{{ photoFileIds.length }} / 20 张</text></view>
|
||||||
<button class="secondary" :disabled="busy || photoFileIds.length >= 20" @tap="choosePhoto">拍照或从相册选择</button>
|
<button class="secondary" :disabled="busy || photoFileIds.length >= 20" @tap="choosePhoto">拍照或从相册选择</button>
|
||||||
<view class="photo-grid"><view v-for="(fileId,index) in photoFileIds" :key="fileId" class="photo-tile">
|
<view class="photo-grid"><view v-for="(fileId,index) in photoFileIds" :key="fileId" class="photo-tile">
|
||||||
@@ -58,7 +59,6 @@
|
|||||||
</view></view>
|
</view></view>
|
||||||
<textarea v-model="remark" class="reason-input section-gap" maxlength="900" :placeholder="allProcessed ? '完成备注(选填)' : '必填:未全部扫描完成的仓库异常作业备注'" />
|
<textarea v-model="remark" class="reason-input section-gap" maxlength="900" :placeholder="allProcessed ? '完成备注(选填)' : '必填:未全部扫描完成的仓库异常作业备注'" />
|
||||||
<view v-if="completionBlockReason" class="error-box section-gap"><text class="error-title">还不能完成换标</text><text>{{ completionBlockReason }}</text></view>
|
<view v-if="completionBlockReason" class="error-box section-gap"><text class="error-title">还不能完成换标</text><text>{{ completionBlockReason }}</text></view>
|
||||||
<button class="primary" :disabled="busy || Boolean(completionBlockReason)" @tap="completeTask">{{ busyAction === 'complete' ? '提交中…' : allProcessed ? '完成现场换标并等待复核' : '手动完成并登记异常' }}</button>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
return { ...this.current, operationType: 'CARTON_LABEL', items: [...boxes.values()] }
|
return { ...this.current, operationType: 'CARTON_LABEL', items: [...boxes.values()] }
|
||||||
},
|
},
|
||||||
isReturnTask() { return (this.current?.items || []).some(item => item.returnCartonId) },
|
isReturnTask() { return (this.current?.items || []).some(item => item.returnCartonId) },
|
||||||
|
showReturnPack() { return this.isReturnTask && (requiresReturnPack(this.current) || returnPackFacts(this.current).length > 0) },
|
||||||
requiresPalletBinding() { return this.isReturnTask ? this.current?.requiresPalletBinding === true || this.current?.deliveryMethod === 'TRUCK' : true },
|
requiresPalletBinding() { return this.isReturnTask ? this.current?.requiresPalletBinding === true || this.current?.deliveryMethod === 'TRUCK' : true },
|
||||||
returnBulkSources() { return (this.current?.items || []).filter(item => item.returnCartonId && item.operationType === 'RETURN_BULK_SKU_LABEL' && item.cartonMark === this.verifiedReturnCarton && item.skuCode.toUpperCase() === this.returnSkuCode.trim().toUpperCase() && this.remainingReturnQuantity(item) > 0) },
|
returnBulkSources() { return (this.current?.items || []).filter(item => item.returnCartonId && item.operationType === 'RETURN_BULK_SKU_LABEL' && item.cartonMark === this.verifiedReturnCarton && item.skuCode.toUpperCase() === this.returnSkuCode.trim().toUpperCase() && this.remainingReturnQuantity(item) > 0) },
|
||||||
fbaDestinations() { return [...new Set((this.current?.items || []).map(item => item.destinationFbaWarehouseCode).filter(Boolean))] },
|
fbaDestinations() { return [...new Set((this.current?.items || []).map(item => item.destinationFbaWarehouseCode).filter(Boolean))] },
|
||||||
@@ -278,7 +279,7 @@
|
|||||||
if (!mark || this.busy || !this.current) return
|
if (!mark || this.busy || !this.current) return
|
||||||
const item = this.current.items.find((row) => row.cartonMark === mark && row.operationType !== 'SKU_LABEL')
|
const item = this.current.items.find((row) => row.cartonMark === mark && row.operationType !== 'SKU_LABEL')
|
||||||
if (!item) return this.setError(new Error('该仓库唛头不属于本单箱唛换标明细'))
|
if (!item) return this.setError(new Error('该仓库唛头不属于本单箱唛换标明细'))
|
||||||
if (this.isReturnTask && (!this.verifiedReturnCarton || this.returnCartonCode.trim() !== this.verifiedReturnCarton || item.cartonMark !== this.verifiedReturnCarton)) return this.setError(new Error('退货箱唛换标必须先确认同一来源包裹/箱'))
|
// The carton scan identifies the physical return source and records its label stage in one request.
|
||||||
const payload = { relabelNo: this.current.relabelNo, cartonMark: mark, expectedVersion: this.current.version }
|
const payload = { relabelNo: this.current.relabelNo, cartonMark: mark, expectedVersion: this.current.version }
|
||||||
const key = warehouseIdempotencyKey('relabel', 'carton-scan', payload)
|
const key = warehouseIdempotencyKey('relabel', 'carton-scan', payload)
|
||||||
await this.run('carton-scan', async () => {
|
await this.run('carton-scan', async () => {
|
||||||
@@ -303,7 +304,7 @@
|
|||||||
async loadCandidates() { if (this.loading) return; this.loading = true; this.error = null; try { this.candidates = await listRelabelCandidates() } catch (error) { this.setError(error, '换标任务加载失败') } finally { this.loading = false } },
|
async loadCandidates() { if (this.loading) return; this.loading = true; this.error = null; try { this.candidates = await listRelabelCandidates() } catch (error) { this.setError(error, '换标任务加载失败') } finally { this.loading = false } },
|
||||||
async openTask(raw, sound = true) { const no = String(raw?.detail?.value ?? raw ?? this.taskCode).trim(); if (!no || this.busy) return; this.taskCode = no; await this.run('detail', async () => { this.applyCurrent(await getRelabelTask(no)); if (sound) playSuccessSound() }, false) },
|
async openTask(raw, sound = true) { const no = String(raw?.detail?.value ?? raw ?? this.taskCode).trim(); if (!no || this.busy) return; this.taskCode = no; await this.run('detail', async () => { this.applyCurrent(await getRelabelTask(no)); if (sound) playSuccessSound() }, false) },
|
||||||
applyCurrent(value) { const changed = this.current?.relabelNo !== value.relabelNo; this.current = value; this.taskCode = value.relabelNo; if (changed) { this.unitDrafts = {}; this.documents = []; this.quickCarton = ''; this.lastCarton = null; this.returnCartonCode = ''; this.verifiedReturnCarton = ''; this.returnTargetLabels = {}; this.quickTargetLabel = ''; this.quickActualQuantity = ''; this.returnSkuCode = ''; this.verifiedReturnSkuKey = ''; this.bulkQuantities = {}; if (warehouseWork.relabel.photoTaskNo !== value.relabelNo) updateWarehouseWork('relabel', { photoTaskNo: value.relabelNo, photoFileIds: [], photoNames: [], remark: '' }); this.changeTab(value.items.some((item) => item.operationType === 'SKU_LABEL') ? 'sku' : 'carton'); this.loadDocuments() } if (this.selectedItem) this.selectedItem = this.visibleItems.find((item) => item.itemId === this.selectedItem.itemId) || null },
|
applyCurrent(value) { const changed = this.current?.relabelNo !== value.relabelNo; this.current = value; this.taskCode = value.relabelNo; if (changed) { this.unitDrafts = {}; this.documents = []; this.quickCarton = ''; this.lastCarton = null; this.returnCartonCode = ''; this.verifiedReturnCarton = ''; this.returnTargetLabels = {}; this.quickTargetLabel = ''; this.quickActualQuantity = ''; this.returnSkuCode = ''; this.verifiedReturnSkuKey = ''; this.bulkQuantities = {}; if (warehouseWork.relabel.photoTaskNo !== value.relabelNo) updateWarehouseWork('relabel', { photoTaskNo: value.relabelNo, photoFileIds: [], photoNames: [], remark: '' }); this.changeTab(value.items.some((item) => item.operationType === 'SKU_LABEL') ? 'sku' : 'carton'); this.loadDocuments() } if (this.selectedItem) this.selectedItem = this.visibleItems.find((item) => item.itemId === this.selectedItem.itemId) || null },
|
||||||
changeTab(value) { this.taskTab = value; this.selectedItem = null; this.reason = ''; this.quickSku = ''; this.skuChoices = []; this.activeTarget = this.isReturnTask ? 'return-carton' : value === 'carton' ? 'quick-carton' : 'quick-sku' },
|
changeTab(value) { this.taskTab = value; this.selectedItem = null; this.reason = ''; this.quickSku = ''; this.skuChoices = []; this.activeTarget = value === 'carton' ? 'quick-carton' : this.isReturnTask ? 'return-carton' : 'quick-sku' },
|
||||||
skuDetails(item) { return item.skuItems?.length ? item.skuItems : [{ skuCode: item.skuCode, newSkuCode: item.newSkuCode, quantity: null }] },
|
skuDetails(item) { return item.skuItems?.length ? item.skuItems : [{ skuCode: item.skuCode, newSkuCode: item.newSkuCode, quantity: null }] },
|
||||||
selectItem(item) { if (this.taskTab === 'carton') { this.activeTarget = 'quick-carton'; return } if (this.remaining(item) <= 0) return; if (!this.unitDrafts[item.itemId]) this.unitDrafts[item.itemId] = { cartonMark: '', fbaCartonMark: '', skuVerifications: this.skuDetails(item).map((sku) => ({ skuCode: sku.skuCode || '', newSkuCode: '', quantity: sku.quantity })) }; this.selectedItem = item; this.reason = ''; this.activeTarget = 'old' },
|
selectItem(item) { if (this.taskTab === 'carton') { this.activeTarget = 'quick-carton'; return } if (this.remaining(item) <= 0) return; if (!this.unitDrafts[item.itemId]) this.unitDrafts[item.itemId] = { cartonMark: '', fbaCartonMark: '', skuVerifications: this.skuDetails(item).map((sku) => ({ skuCode: sku.skuCode || '', newSkuCode: '', quantity: sku.quantity })) }; this.selectedItem = item; this.reason = ''; this.activeTarget = 'old' },
|
||||||
focusNextSku(index) { const next = index + 1; this.activeTarget = next < this.unitDraft.skuVerifications.length ? `sku-new-${next}` : 'old' },
|
focusNextSku(index) { const next = index + 1; this.activeTarget = next < this.unitDraft.skuVerifications.length ? `sku-new-${next}` : 'old' },
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ test('pallet comparison tolerates mixed API versions and canonical code formatti
|
|||||||
test('quantity disassembly has no extra carton scanner', () => {
|
test('quantity disassembly has no extra carton scanner', () => {
|
||||||
assert.doesNotMatch(source, /relabel-move-carton/)
|
assert.doesNotMatch(source, /relabel-move-carton/)
|
||||||
assert.match(source, /actualSource:/)
|
assert.match(source, /actualSource:/)
|
||||||
|
assert.match(source, /来源已确认:.*尚未拆出.*绑定成功后才保存拆托明细/)
|
||||||
assert.match(source, /this\.message = `托盘 \$\{this\.target\.palletCode\} 已绑定`/)
|
assert.match(source, /this\.message = `托盘 \$\{this\.target\.palletCode\} 已绑定`/)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -107,9 +108,29 @@ test('target confirmation submits binding directly, with no second submit button
|
|||||||
await value.confirmTargetAndBind()
|
await value.confirmTargetAndBind()
|
||||||
assert.equal(binds, 1)
|
assert.equal(binds, 1)
|
||||||
assert.match(source, /@submit="confirmTargetAndBind"/)
|
assert.match(source, /@submit="confirmTargetAndBind"/)
|
||||||
|
assert.match(source, /@tap="confirmTargetAndBind"/)
|
||||||
assert.doesNotMatch(source, /@tap="bind"/)
|
assert.doesNotMatch(source, /@tap="bind"/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('mixed carton scan alone explains missing SKU and does not bind; completing SKU allows binding', async () => {
|
||||||
|
const value = context()
|
||||||
|
value.task.items = [{ cartonMark: 'A', operationType: 'CARTON_AND_SKU', expectedPackages: 1, relabelledPackages: 0, cartonProcessedPackages: 1, skuProcessedPackages: 0, destinationFbaWarehouseCode: 'ONT8' }]
|
||||||
|
value.source = { palletCode: 'SOURCE' }; value.targetCode = 'TARGET'; value.quantity = '1'
|
||||||
|
let message = ''; let binds = 0
|
||||||
|
value.showBindingMessage = (title, content) => { message = content }
|
||||||
|
value.resolve = async () => { value.target = { palletCode: 'TARGET' } }
|
||||||
|
value.bind = async () => { binds++ }
|
||||||
|
await value.confirmTargetAndBind()
|
||||||
|
assert.match(message, /SKU 贴标登记/)
|
||||||
|
assert.equal(binds, 0)
|
||||||
|
assert.equal(value.target, null)
|
||||||
|
value.task.items[0].skuProcessedPackages = 1
|
||||||
|
value.task.items[0].relabelledPackages = 1
|
||||||
|
assert.equal(value.labelBlockReason, '')
|
||||||
|
await value.confirmTargetAndBind()
|
||||||
|
assert.equal(binds, 1)
|
||||||
|
})
|
||||||
|
|
||||||
test('source scan asks before accepting a listed pallet and rejects unknown pallets', async () => {
|
test('source scan asks before accepting a listed pallet and rejects unknown pallets', async () => {
|
||||||
const value = context()
|
const value = context()
|
||||||
value.sourceRows = [{ positionId: '1', palletCode: 'SOURCE', movablePackages: 2 }]
|
value.sourceRows = [{ positionId: '1', palletCode: 'SOURCE', movablePackages: 2 }]
|
||||||
|
|||||||
@@ -3,11 +3,50 @@ import assert from 'node:assert/strict'
|
|||||||
import { readFile } from 'node:fs/promises'
|
import { readFile } from 'node:fs/promises'
|
||||||
const page = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
const page = await readFile(new URL('../src/pages/relabel/relabel.vue', import.meta.url), 'utf8')
|
||||||
const helper = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8')
|
const helper = await readFile(new URL('../src/utils/return-relabel.js', import.meta.url), 'utf8')
|
||||||
const { returnDeductions } = await import('data:text/javascript;base64,' + Buffer.from(helper).toString('base64'))
|
const { returnDeductions, requiresReturnPack, returnPackFacts } = await import('data:text/javascript;base64,' + Buffer.from(helper).toString('base64'))
|
||||||
test('return carton label path requires source confirmation before carton scan', () => {
|
const body = page.split('<script>')[1].split('</script>')[0].replace(/^\s*import .*$/gm, '').replace('export default', 'return')
|
||||||
assert.match(page, /退货箱唛换标必须先确认同一来源包裹\/箱/)
|
const component = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'requiresReturnPack', 'returnPackFacts', body)({}, {}, {}, {}, requiresReturnPack, returnPackFacts)
|
||||||
|
test('return carton label identifies source with one carton scan and retains source check for SKU tab', () => {
|
||||||
|
assert.doesNotMatch(page, /退货箱唛换标必须先确认同一来源包裹\/箱/)
|
||||||
|
assert.match(page, /v-if="isReturnTask && taskTab !== 'carton'"/)
|
||||||
|
assert.match(page, /this.activeTarget = value === 'carton' \? 'quick-carton'/)
|
||||||
assert.match(page, /this\.taskTab === 'carton' \? 'quick-carton' : 'quick-sku'/)
|
assert.match(page, /this\.taskTab === 'carton' \? 'quick-carton' : 'quick-sku'/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('whole carton and full SKU relabel hide new box dimensions; partial SKU keeps isolation form', () => {
|
||||||
|
const value = { isReturnTask: true, current: { items: [{ returnCartonId: '1', cartonMark: 'C1', operationType: 'CARTON_LABEL', returnQuantity: 15 }], history: [] } }
|
||||||
|
assert.equal(component.computed.showReturnPack.call(value), false)
|
||||||
|
value.current.items[0].operationType = 'SKU_LABEL'
|
||||||
|
value.current.items[0].skuItems = [{ skuCode: 'A', quantity: 15 }]
|
||||||
|
assert.equal(component.computed.showReturnPack.call(value), false)
|
||||||
|
value.current.items[0].skuItems[0].quantity = 5
|
||||||
|
assert.equal(component.computed.showReturnPack.call(value), true)
|
||||||
|
assert.match(page, /ReturnRelabelPackWork v-if="showReturnPack"/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('one return carton scan saves label stage without a prior source scan; unrelated carton is rejected', async () => {
|
||||||
|
const calls = []
|
||||||
|
const item = { itemId: 'I1', cartonMark: 'C1', operationType: 'CARTON_LABEL', fbaCartonMark: 'FBA1' }
|
||||||
|
const result = { relabelNo: 'RL1', version: 1, items: [{ ...item, relabelledPackages: 1 }] }
|
||||||
|
const subject = new Function('PortalHeader', 'RelabelPalletWork', 'ScanTargetInput', 'ReturnRelabelPackWork', 'scanRelabelCarton', 'warehouseIdempotencyKey', 'releaseWarehouseKey', 'uni', body)(
|
||||||
|
{}, {}, {}, {},
|
||||||
|
async (no, payload) => { calls.push({ no, payload }); return result },
|
||||||
|
() => ({ value: 'KEY', signature: 'SIG' }), () => {},
|
||||||
|
{ showModal: options => options.complete() }
|
||||||
|
)
|
||||||
|
const value = {
|
||||||
|
busy: false, current: { relabelNo: 'RL1', version: 0, items: [item] }, verifiedReturnCarton: '',
|
||||||
|
run: async (action, work) => work(), applyCurrent(task) { this.current = task },
|
||||||
|
setError(error) { this.error = error.message }
|
||||||
|
}
|
||||||
|
await subject.methods.submitCarton.call(value, ' C1\n')
|
||||||
|
assert.deepEqual(calls, [{ no: 'RL1', payload: { cartonMark: 'C1', expectedVersion: 0, idempotencyKey: 'KEY' } }])
|
||||||
|
assert.equal(value.current.version, 1)
|
||||||
|
assert.equal(value.lastCarton.fbaCartonMark, 'FBA1')
|
||||||
|
await subject.methods.submitCarton.call(value, 'OTHER')
|
||||||
|
assert.equal(calls.length, 1)
|
||||||
|
assert.match(value.error, /不属于本单/)
|
||||||
|
})
|
||||||
test('mixed return carton keeps one physical package while SKU deductions use pieces', () => {
|
test('mixed return carton keeps one physical package while SKU deductions use pieces', () => {
|
||||||
const task={items:[{returnCartonId:'1',cartonMark:'P1',operationType:'CARTON_AND_SKU',returnQuantity:5,skuItems:[{skuCode:'S',quantity:5}]}]}
|
const task={items:[{returnCartonId:'1',cartonMark:'P1',operationType:'CARTON_AND_SKU',returnQuantity:5,skuItems:[{skuCode:'S',quantity:5}]}]}
|
||||||
assert.deepEqual(returnDeductions(task),[{cartonMark:'P1',skuCode:'S',quantity:5}])
|
assert.deepEqual(returnDeductions(task),[{cartonMark:'P1',skuCode:'S',quantity:5}])
|
||||||
|
|||||||
Reference in New Issue
Block a user