This commit is contained in:
2026-09-15 11:54:24 +08:00
parent f8f21b9f37
commit 4895970ea0
2 changed files with 31 additions and 4 deletions
@@ -31,15 +31,17 @@
<view v-if="carton" class="carton-card"> <view v-if="carton" class="carton-card">
<view v-for="(line, index) in skuCounts" :key="line.inboundDetailId" class="sku-count-row"> <view v-for="(line, index) in skuCounts" :key="line.inboundDetailId" class="sku-count-row">
<view class="sku-copy"><text class="sku-label">SKU {{ index + 1 }}</text><text class="sku-code">{{ line.skuCode || '—' }}</text></view> <view class="sku-copy"><text class="sku-label">SKU {{ index + 1 }}</text><text class="sku-code">{{ line.skuCode || '—' }}</text></view>
<view class="quantity-stepper"> <view v-if="!isResolvedPendingCarton" class="quantity-stepper">
<button :aria-label="`减少 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, -1)">−</button> <button :aria-label="`减少 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, -1)">−</button>
<input v-model="line.actualQuantity" type="number" :aria-label="`${line.skuCode} 清点数量`" /> <input v-model="line.actualQuantity" type="number" :aria-label="`${line.skuCode} 清点数量`" />
<button :aria-label="`增加 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, 1)">+</button> <button :aria-label="`增加 ${line.skuCode} 清点数量`" @tap="adjustQuantity(index, 1)">+</button>
</view> </view>
<view v-if="lineDiffers(line)" class="field stacked difference-field"><text>差异说明</text><textarea v-model="line.discrepancyRemark" maxlength="500" placeholder="数量不一致时必填" /></view> <text v-else class="counted-quantity">已清点 {{ line.actualQuantity }} 件</text>
<view v-if="!isResolvedPendingCarton && lineDiffers(line)" class="field stacked difference-field"><text>差异说明</text><textarea v-model="line.discrepancyRemark" maxlength="500" placeholder="数量不一致时必填" /></view>
</view> </view>
<text v-if="currentPallet" class="pallet-hint">归入 {{ currentPallet.palletNo }}</text> <text v-if="currentPallet" class="pallet-hint">归入 {{ currentPallet.palletNo }}</text>
<button class="primary-button" :disabled="!canCount || submitting" @tap="countCarton">确认清点</button> <text v-else-if="isResolvedPendingCarton" class="pallet-hint">本箱已清点,请先扫描目标托盘</text>
<button class="primary-button" :disabled="!canSubmitCarton || submitting" @tap="confirmCarton">{{ cartonActionLabel }}</button>
</view> </view>
<view class="pallet-scan-block"> <view class="pallet-scan-block">
<text class="field-label">扫托盘 <text>可选</text></text> <text class="field-label">扫托盘 <text>可选</text></text>
@@ -88,6 +90,10 @@
palletSetupMessage() { return this.task?.palletSetupMessage || `预计 ${this.task?.expectedPalletCount || 0} 托,当前仅生成 ${this.task?.generatedPalletCount || 0} 个托盘码,请联系后台处理后刷新` }, palletSetupMessage() { return this.task?.palletSetupMessage || `预计 ${this.task?.expectedPalletCount || 0} 托,当前仅生成 ${this.task?.generatedPalletCount || 0} 个托盘码,请联系后台处理后刷新` },
canResolveCarton() { return Boolean(this.palletSetupReady && this.task?.taskStatus === 'CLAIMED' && String(this.boxCode || '').trim() && !this.loading && !this.submitting) }, canResolveCarton() { return Boolean(this.palletSetupReady && this.task?.taskStatus === 'CLAIMED' && String(this.boxCode || '').trim() && !this.loading && !this.submitting) },
canCount() { return Boolean(this.carton?.canCount && this.skuCounts.length && this.skuCounts.every((line) => Number.isInteger(Number(line.actualQuantity)) && Number(line.actualQuantity) >= 0 && (!this.lineDiffers(line) || String(line.discrepancyRemark || '').trim()))) }, canCount() { return Boolean(this.carton?.canCount && this.skuCounts.length && this.skuCounts.every((line) => Number.isInteger(Number(line.actualQuantity)) && Number(line.actualQuantity) >= 0 && (!this.lineDiffers(line) || String(line.discrepancyRemark || '').trim()))) },
isResolvedPendingCarton() { return this.carton?.countStatus === 'COUNTED_UNBOUND' },
canRebindResolvedCarton() { return Boolean(this.isResolvedPendingCarton && this.carton?.cartonId && this.currentPallet?.palletNo) },
canSubmitCarton() { return this.isResolvedPendingCarton ? this.canRebindResolvedCarton : this.canCount },
cartonActionLabel() { return this.isResolvedPendingCarton ? '确认归入当前托盘' : '确认清点' },
currentPalletDetails() { return (this.task?.pallets || []).find((item) => item.palletNo === this.selectedPalletNo) || null }, currentPalletDetails() { return (this.task?.pallets || []).find((item) => item.palletNo === this.selectedPalletNo) || null },
currentPallet() { return this.currentPalletDetails?.status === 'OPEN' ? this.currentPalletDetails : (this.selectedPalletNo ? { palletNo: this.selectedPalletNo, status: 'OPEN' } : null) } currentPallet() { return this.currentPalletDetails?.status === 'OPEN' ? this.currentPalletDetails : (this.selectedPalletNo ? { palletNo: this.selectedPalletNo, status: 'OPEN' } : null) }
}, },
@@ -111,7 +117,7 @@
if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return } if (!this.palletSetupReady) { this.error = this.palletSetupMessage; return }
const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return const code = String(value || this.boxCode || '').trim().toUpperCase(); if (!code || this.loading) return
this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null this.boxCode = code; this.loading = true; this.resetMessages(); this.carton = null
try { this.carton = await resolveDropshipCarton(this.taskId, this.warehouseId, code); this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.expectedQuantity || ''), discrepancyRemark: '' })) } try { this.carton = await resolveDropshipCarton(this.taskId, this.warehouseId, code); this.skuCounts = (this.carton.skuLines || []).map((line) => ({ ...line, actualQuantity: String(line.actualQuantity ?? line.expectedQuantity ?? ''), discrepancyRemark: line.discrepancyRemark || '' })) }
catch (error) { this.error = formatApiError(error, '箱码解析失败') } catch (error) { this.error = formatApiError(error, '箱码解析失败') }
finally { this.loading = false } finally { this.loading = false }
}, },
@@ -120,6 +126,18 @@
const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, palletNo: this.currentPallet?.palletNo || null, skuLines }), this.currentPallet ? '本箱已清点并归托' : '本箱已完成入库清点') const result = await this.runMutation(() => countDropshipCarton(this.taskId, { warehouseId: this.warehouseId, boxCode: this.carton.boxCode, palletNo: this.currentPallet?.palletNo || null, skuLines }), this.currentPallet ? '本箱已清点并归托' : '本箱已完成入库清点')
if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] } if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] }
}, },
async confirmCarton() {
if (this.isResolvedPendingCarton) { await this.rebindResolvedCarton(); return }
await this.countCarton()
},
async rebindResolvedCarton() {
if (!this.canRebindResolvedCarton) return
const targetPalletNo = this.currentPallet.palletNo
const payload = { warehouseId: this.warehouseId, targetPalletNo }
payload.operationId = stableOperationId('dropship-move', String(this.user?.id) + '-' + this.taskId, { cartonId: this.carton.cartonId, taskVersion: this.task.version, ...payload })
const result = await this.runMutation(() => moveDropshipCarton(this.taskId, this.carton.cartonId, payload), `本箱已归入 ${targetPalletNo}`)
if (result) { this.boxCode = ''; this.carton = null; this.skuCounts = [] }
},
lineDiffers(line) { return Number(line.actualQuantity) !== Number(line.expectedQuantity) }, lineDiffers(line) { return Number(line.actualQuantity) !== Number(line.expectedQuantity) },
adjustQuantity(index, step) { const line = this.skuCounts[index]; if (line) line.actualQuantity = String(Math.max(0, Number(line.actualQuantity || 0) + step)) }, adjustQuantity(index, step) { const line = this.skuCounts[index]; if (line) line.actualQuantity = String(Math.max(0, Number(line.actualQuantity || 0) + step)) },
async selectPallet(value) { async selectPallet(value) {
@@ -175,6 +193,7 @@
.count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; } .count-entry { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; padding: 5px 7px; border: 0; border-radius: 0; color: #667085; font-size: 13px; background: #f6f9ff; }.compact-action { width: 86px; height: 34px; margin: 0; border: 0; border-radius: 6px; color: #fff; font-size: 13px; line-height: 34px; background: #1a73e8; }.compact-action[disabled] { opacity: .35; }.count-unavailable { padding: 12px 8px; color: #b42318; font-size: 13px; line-height: 18px; text-align: center; background: #fff1f0; }.pallet-scan-block { margin-top: 8px; padding-top: 8px; border-top: 1px solid #e6edf5; }.field-label { display: block; margin-bottom: 5px; color: #344054; font-size: 15px; font-weight: 700; }.field-label text { margin-left: 3px; color: #98a2b3; font-size: 12px; font-weight: 400; }
.carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; } .carton-card, .pallet-card { margin-top: 8px; padding: 8px 0 0; border: 0; border-top: 1px solid #e3eaf2; border-radius: 0; background: transparent; }.sku-count-row { display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #edf1f5; }.sku-copy { min-width: 0; }.sku-label { display: block; color: #667085; font-size: 11px; }.sku-code { display: block; margin-top: 2px; color: #12233f; font-size: 16px; font-weight: 700; overflow-wrap: anywhere; }.pallet-meta { display: block; margin-top: 5px; color: #667085; font-size: 14px; line-height: 20px; }
.quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; } .quantity-stepper { display: grid; grid-template-columns: 38px 56px 38px; overflow: hidden; border: 1px solid #b8c6d8; border-radius: 8px; background: #fff; }.quantity-stepper button,.quantity-stepper input { box-sizing: border-box; height: 40px; margin: 0; border: 0; border-radius: 0; text-align: center; background: #fff; }.quantity-stepper button { color: #155eef; font-size: 20px; line-height: 40px; }.quantity-stepper input { width: 56px; border-right: 1px solid #dce4ed; border-left: 1px solid #dce4ed; color: #12233f; font-size: 15px; }.difference-field { grid-column: 1 / -1; margin-top: 0; }.pallet-hint { display: block; margin-top: 10px; color: #155eef; font-size: 11px; text-align: center; }
.counted-quantity { flex: 0 0 auto; color: #067647; font-size: 14px; font-weight: 700; }
.field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: #344054; font-size: 12px; }.field input { box-sizing: border-box; width: 120px; height: 42px; padding: 0 10px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }.field.stacked { align-items: stretch; flex-direction: column; gap: 6px; }.field textarea { box-sizing: border-box; width: 100%; height: 70px; padding: 9px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; } .field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; color: #344054; font-size: 12px; }.field input { box-sizing: border-box; width: 120px; height: 42px; padding: 0 10px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }.field.stacked { align-items: stretch; flex-direction: column; gap: 6px; }.field textarea { box-sizing: border-box; width: 100%; height: 70px; padding: 9px; border: 1px solid #cfd8e3; border-radius: 8px; background: #fff; }
.pallet-code { color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }.text-button { min-width: 58px; height: 40px; border: 0; color: #155eef; font-size: 14px; line-height: 40px; background: transparent; }.empty { padding: 18px 0 8px; color: #98a2b3; font-size: 14px; text-align: center; } .pallet-code { color: #12233f; font-size: 19px; font-weight: 700; overflow-wrap: anywhere; }.text-button { min-width: 58px; height: 40px; border: 0; color: #155eef; font-size: 14px; line-height: 40px; background: transparent; }.empty { padding: 18px 0 8px; color: #98a2b3; font-size: 14px; text-align: center; }
.palletization-page :deep(.scan-target-value) { font-size: 17px; } .palletization-page :deep(.scan-target-value) { font-size: 17px; }
+8
View File
@@ -40,3 +40,11 @@ test('count and move use the formal multi-SKU contract with versioned retries',
assert.match(api, /stableOperationId/) assert.match(api, /stableOperationId/)
assert.doesNotMatch(api, /actualSkuQuantity|create-and-bind/) assert.doesNotMatch(api, /actualSkuQuantity|create-and-bind/)
}) })
test('a counted carton moved back to pending can be confirmed into the active pallet without recounting', () => {
assert.match(page, /isResolvedPendingCarton\(\) \{ return this\.carton\?\.countStatus === 'COUNTED_UNBOUND' \}/)
assert.match(page, /canRebindResolvedCarton/)
assert.match(page, /确认归入当前托盘/)
assert.match(page, /moveDropshipCarton\(this\.taskId, this\.carton\.cartonId, payload\)/)
assert.match(page, /line\.actualQuantity \?\? line\.expectedQuantity/)
})