@@ -1,4 +1,5 @@
< script setup lang = "ts" >
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
import { computed , nextTick , onActivated , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import { ElMessage , ElMessageBox } from 'element-plus'
@@ -7,7 +8,9 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/components/com
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import type { TableAction } from '@/components/common/tableActions'
import { useAuthStore } from '@/modules/auth/public'
import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting'
import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue'
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import {
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer , resubmitRelabel ,
@@ -28,13 +31,14 @@ import {
} from '@/modules/first-leg/api/firstLeg'
import { loadAllWarehouseRecords } from '@/modules/first-leg/utils/warehousePagination'
import { createIdempotencyKey } from '@/utils/idempotencyKey'
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
import { relabelSkuCodes , relabelSkuCount } from '../utils/relabelSkuDisplay'
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'edit' | 'detail'
type LabelingMode = 'carton' | 'sku' | 'carton_and_sku'
type RelabelTemplateType = 'carton' | 'sku' | 'mixed'
interface CargoRow { discardCartonIds ? : string [ ] ; id : number ; included ? : boolean ; sku : string ; name : string ; quantity : number ; expectedPackageCount ? : number ; availableQuantity ? : number ; sourceOrderId ? : string ; sourceType ? : 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY' ; sourceInventoryBalanceId ? : string ; sourceInventoryPositionId ? : string ; sourceInboundOrderId ? : string ; returnCartonId ? : string ; sourceInboundOrderNo ? : string ; sourceInboundCode ? : string ; sourceWarehouseName ? : string ; palletNo ? : string ; warehouseLocationCode ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; customerCartonMarks : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface CargoRow { generatedCartonMarks ? : string [ ] ; discardCartonIds ? : string [ ] ; id : number ; included ? : boolean ; sku : string ; name : string ; quantity : number ; expectedPackageCount ? : number ; availableQuantity ? : number ; sourceOrderId ? : string ; sourceType ? : 'DROPSHIP' | 'RETURN_CARTON' | 'RETURN_PARCEL' | 'INVENTORY' ; sourceInventoryBalanceId ? : string ; sourceInventoryPositionId ? : string ; sourceInboundOrderId ? : string ; returnCartonId ? : string ; sourceInboundOrderNo ? : string ; sourceInboundCode ? : string ; sourceWarehouseName ? : string ; palletNo ? : string ; warehouseLocationCode ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; customerCartonMarks : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface SkuRelabelRow { id : number ; sourceRowId : number | null ; cartonMark : string ; originalSku : string ; newSku : string ; quantity : number ; fbaCartonMark : string ; shipmentNo : string ; destinationWarehouseId : string ; remark : string }
interface ImportedRelabelBinding extends SkuRelabelRow { templateType : RelabelTemplateType ; selected : boolean ; shipmentNo : string }
interface ServiceConfig { title : string ; createTitle : string ; numberLabel : string ; emptyText : string ; columns : string [ ] }
@@ -133,6 +137,7 @@ const editingTransferNo = ref('')
interface TransferPallet { id ? : string | number ; sequenceNo ? : number ; palletNo ? : string ; warehouseLocationCode ? : string ; cartonCount ? : number ; lengthCm ? : number ; widthCm ? : number ; heightCm ? : number ; grossWeightKg ? : number ; volumeCbm ? : number ; status ? : string ; completedAt ? : string }
interface TransferMeasurementPhoto { fileId : string | number ; fileName ? : string ; contentType ? : string ; sizeBytes ? : number ; uploadedAt ? : string }
interface ServiceDetail extends Record < string , unknown > {
reviewRemark ? : string
discardNo ? : string ; photos ? : DiscardPhoto [ ]
transferNo ? : string ; status ? : string ; operatingWarehouseId ? : string ; fbaWarehouseCode ? : string
destinationType ? : 'NONE' | 'FBA' | 'PRIVATE' ; destinationWarehouseCode ? : string ; destinationWarehouseName ? : string
@@ -149,29 +154,34 @@ interface ServiceDetail extends Record<string, unknown> {
cartons ? : TransferCarton [ ] ; pallets ? : TransferPallet [ ] ; measurementPhotos ? : TransferMeasurementPhoto [ ] ; customerConfirmedAt ? : string
}
const selectedDetail = ref < ServiceDetail | null > ( null )
/** 仅汇总客户原始预报件数,旧单缺失时明确提示,不用实收件数冒充。 */
const transferForecastQuantity = computed ( ( ) => {
const items = selectedDetail . value ? . items || [ ]
if ( ! items . length || items . some ( item => item . quantity == null ) ) return '未记录'
return items . reduce ( ( sum , item ) => sum + Number ( item . quantity ) , 0 ) + ' 件'
} )
// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。
const showForecastCartons = ref ( false )
const increaseForecastOpen = ref ( false )
const increasingForecast = ref ( false )
const forecastSnapshot = ref < ServiceDetail | null > ( null )
const increaseForecastForm = reactive ( { itemId : '' , additionalCount : 1 } )
const increaseForecastForm = reactive ( { additionalCount : 1 } )
const canIncreaseForecast = computed ( ( ) => canCreate . value && selectedDetail . value ? . transferNo === selectedDetail . value ? . inboundCode && [ 'PENDING_REVIEW' , 'WAIT_PDA_RECEIVING' ] . includes ( selectedDetail . value ? . status || '' ) )
/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */
function openIncreaseForecast ( ) {
if ( ! canIncreaseForecast . value || ! selectedDetail . value ) return
forecastSnapshot . value = selectedDetail . value
increaseForecastForm . itemId = String ( selectedDetail . value . items ? . [ 0 ] ? . id || '' )
increaseForecastForm . additionalCount = 1 ; increaseForecastOpen . value = true
}
/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */
async function saveIncreaseForecast ( ) {
if ( increasingForecast . value || ! forecastSnapshot . value ? . transferNo ) return
const snapshot = forecastSnapshot . value
const item = snapshot . items ? . find ( row => String ( row . id ) === increaseForecastForm . itemId )
if ( ! item || ! Number . isSafeInteger ( increaseForecastForm . additionalCount ) || increaseForecastForm . additionalCount < 1 ) { ElMessage . warning ( '请选择SKU并填写新增箱数' ) ; return }
if ( ! Number . isSafeInteger ( increaseForecastForm . additionalCount ) || increaseForecastForm . additionalCount < 1 ) { ElMessage . warning ( '请填写有效新增箱数' ) ; return }
increasingForecast . value = true
try {
const result = await increaseTransferForecastCartons ( String ( snapshot . transferNo ) , { expectedPackageCount : Number ( snapshot . packageCount ) , transferItemId : String ( item . id ) , expectedItemPackageCount : Number ( item . packageCount ) , packageCount : Number ( item . packageCount ) + increaseForecastForm . additionalCount } )
const result = await increaseTransferForecastCartons ( String ( snapshot . transferNo ) , { expectedPackageCount : Number ( snapshot . packageCount ) , packageCount : Number ( snapshot . packageCount ) + increaseForecastForm . additionalCount } )
selectedDetail . value = result as ServiceDetail ; increaseForecastOpen . value = false
ElMessage . success ( '预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点' )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '增加预报箱数失败' ) }
@@ -324,6 +334,9 @@ const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref < RelabelInventoryCandidate [ ] > ( [ ] )
const selectedTransferInventory = ref ( '' )
const inventoryTransfer = computed ( ( ) => props . service === 'transfer' && Boolean ( selectedTransferInventory . value ) )
// 来源库存发往FBA时默认贴标,私人仓和外来货保留原有选择。
const transferRelabelRequired = computed ( ( ) => inventoryTransfer . value && transferDestinationType . value === 'FBA' )
watch ( transferRelabelRequired , ( required ) => { if ( required ) form . relabel = true } , { flush : 'sync' } )
const transferSourceCartons = ref < TransferSourceCarton [ ] > ( [ ] )
const transferSourceLoading = ref ( false )
const transferSourceError = ref ( '' )
@@ -338,6 +351,8 @@ async function loadTransferSourceCartons() {
if ( selectedTransferInventory . value . startsWith ( 'RETURN_ORDER:' ) ) {
const returnNo = selectedTransferInventory . value . slice ( 'RETURN_ORDER:' . length )
transferSourceCartons . value = transferInventoryCandidates . value
. filter ( item => ! [ 'COMPLETED' , 'PARTIALLY_COMPLETED' ] . includes ( item . relabelStatus || '' )
&& Number ( item . relabelledQuantity || item . relabelledPackages || 0 ) === 0 )
. filter ( item => ( item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL' )
&& String ( item . inboundOrderNo || '' ) . trim ( ) === returnNo && Number ( item . cartonAvailablePackages || 0 ) > 0 )
. filter ( item => Boolean ( item . cartonMark ) )
@@ -582,6 +597,7 @@ async function openEdit(transferNo: string) {
... createCargoRow ( ) , sku : item . skuCode || '' , name : item . productNameZh || '' ,
quantity : Number ( item . quantity || 0 ) , expectedPackageCount : Number ( item . packageCount || 0 ) , shippingMark : item . shippingMark || '' ,
customerCartonMarks : item . shippingMark || '' ,
generatedCartonMarks : ( detail . cartons || [ ] ) . filter ( box => String ( box . transferItemId ) === String ( item . id ) ) . map ( box => box . cartonMark ) ,
} ) )
const bindings = detail . relabelBindings || [ ]
if ( bindings . length ) {
@@ -1150,7 +1166,20 @@ watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) =>
if ( nextSize !== previousSize && nextPage !== 1 ) { page . value = 1 ; return }
void load ( )
} )
function printForecastCartons ( ) {
if ( saving . value ) return
let html : string
try { html = prepareTransferCartonLabels ( cargoRows . value ) }
catch ( error ) { ElMessage . warning ( error instanceof Error ? error . message : '箱唛生成失败' ) ; return }
const printWindow = window . open ( '' , '_blank' )
if ( ! printWindow ) { ElMessage . warning ( '请允许浏览器打开打印窗口' ) ; return }
printWindow . opener = null
printWindow . document . open ( ) ; printWindow . document . write ( html ) ; printWindow . document . close ( )
printWindow . focus ( ) ; printWindow . print ( )
}
async function submit ( action : 'draft' | 'submit' = 'submit' ) {
if ( saving . value ) return
if ( transferRelabelRequired . value ) form . relabel = true
const primaryWarehouse = props . service === 'relabel' ? form . sourceWarehouse : form . warehouse
const relabelRows = labelingMode . value === 'carton' ? cartonDetailRows . value : skuRelabelRows . value
const invalidCargo = props . service === 'disposal'
@@ -1272,7 +1301,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
const transferRemark = [ form . remark , form . relabel && form . requirement ? ` 换标要求: ${ form . requirement } ` : '' ] . filter ( Boolean ) . join ( '; ' )
const operationType = labelingMode . value === 'carton' ? 'CARTON_LABEL' : labelingMode . value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
const selectedFba = fbaWarehouses . value . find ( ( warehouse ) => warehouse . warehouseCode === form . destination )
const payload : TransferCreate = { operatingWarehouseId : primaryWarehouse , destinationType : transferDestinationType . value , fbaWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseId : transferDestinationType . value === 'FBA' ? String ( selectedFba ? . id || '' ) || undefined : undefined , destinationWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseName : transferDestinationType . value === 'FBA' ? selectedFba ? . warehouseName : undefined , privateDestinationAddress : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationAddress : undefined , privateDestinationPostalCode : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPostalCode : undefined , privateDestinationContactName : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationContact : undefined , privateDestinationContactPhone : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPhone : undefined , deliveryAddress : inventoryTransfer . value ? undefined : form . address , packageCount , volumeCbm : 0 , deliveryMethod : inventoryTransfer . value ? undefined : form . deliveryMethod , estimatedArrivalAt : inventoryTransfer . value ? undefined : form . estimatedArrivalAt , estimatedShipAt : form . estimatedShipAt || undefined , fbaShipmentNo : transferDestinationType . value === 'FBA' ? form . fbaShipmentNo || undefined : undefined , trackingNo : ! inventoryTransfer . value && form . deliveryMethod === 'CUSTOMER_DELIVERY' ? form . trackingNo || undefined : undefined , fbaBoxNos : customerCartonMarks . length ? customerCartonMarks . join ( ',' ) : undefined , remark : transferRemark || undefined , needRelabel : props . service === 'relabel' || form . relabel , labelDeadline : form . relabel ? form . labelDeadline : undefined , pickupContactName : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupContact : undefined , pickupContactPhone : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupPhone : undefined , relabelContactName : form . relabel ? form . relabelContact : undefined , relabelContactPhone : form . relabel ? form . relabelPhone : undefined , relabelDeliveryMethod : form . relabel && labelingMode . value !== 'sku' ? form . relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined , idempotencyKey , items : cargoRows . value . map ( ( item ) => ( { skuCode : item . sku , productNameZh : item . name , quantity : item . quantity , packageCount : Number ( item . expectedPackageCount ) , shippingMark : transferCustomerMarks ( item ) . join ( ',' ) || undefined } ) ) , relabelItems : form . relabel ? transferRelabelRows . value . map ( ( item ) => ( { operationType , customerCartonMarks : transferCustomerMarks ( item ) , originalSku : item . sku , newSku : item . newSkuCode || undefined , quantity : item . quantity , fbaCartonMark : item . fbaCartonMark || undefined , destinationWarehouseCode : transferRelabelDefaultDestination . value || undefined , remark : item . remark || undefined } ) ) : undefined }
const payload : TransferCreate = { operatingWarehouseId : primaryWarehouse , destinationType : transferDestinationType . value , fbaWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseId : transferDestinationType . value === 'FBA' ? String ( selectedFba ? . id || '' ) || undefined : undefined , destinationWarehouseCode : transferDestinationType . value === 'FBA' ? form . destination || undefined : undefined , destinationWarehouseName : transferDestinationType . value === 'FBA' ? selectedFba ? . warehouseName : undefined , privateDestinationAddress : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationAddress : undefined , privateDestinationPostalCode : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPostalCode : undefined , privateDestinationContactName : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationContact : undefined , privateDestinationContactPhone : transferDestinationType . value === 'PRIVATE' ? form . privateDestinationPhone : undefined , deliveryAddress : inventoryTransfer . value ? undefined : form . address , packageCount , volumeCbm : 0 , deliveryMethod : inventoryTransfer . value ? undefined : form . deliveryMethod , estimatedArrivalAt : inventoryTransfer . value ? undefined : form . estimatedArrivalAt , estimatedShipAt : form . estimatedShipAt || undefined , fbaShipmentNo : transferDestinationType . value === 'FBA' ? form . fbaShipmentNo || undefined : undefined , trackingNo : ! inventoryTransfer . value && form . deliveryMethod === 'CUSTOMER_DELIVERY' ? form . trackingNo || undefined : undefined , fbaBoxNos : customerCartonMarks . length ? customerCartonMarks . join ( ',' ) : undefined , remark : transferRemark || undefined , needRelabel : props . service === 'relabel' || form . relabel , labelDeadline : form . relabel ? form . labelDeadline : undefined , pickupContactName : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupContact : undefined , pickupContactPhone : ! inventoryTransfer . value && form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupPhone : undefined , relabelContactName : form . relabel ? form . relabelContact : undefined , relabelContactPhone : form . relabel ? form . relabelPhone : undefined , relabelDeliveryMethod : form . relabel && labelingMode . value !== 'sku' ? form . relabelDeliveryMethod as 'TRUCK' | 'EXPRESS' | 'AMAZON_PICKUP' | 'CUSTOMER_PICKUP' : undefined , idempotencyKey , items : cargoRows . value . map ( ( item ) => ( { skuCode : item . sku , productNameZh : item . name , quantity : item . quantity , packageCount : Number ( item . expectedPackageCount ) , shippingMark : transferCustomerMarks ( item ) . join ( ',' ) || undefined , cartonMarks : item . generatedCartonMarks ? . slice ( 0 , Number ( item . expectedPackageCount ) ) } ) ) , relabelItems : form . relabel ? transferRelabelRows . value . map ( ( item ) => ( { operationType , customerCartonMarks : transferCustomerMarks ( item ) , originalSku : item . sku , newSku : item . newSkuCode || undefined , quantity : item . quantity , fbaCartonMark : item . fbaCartonMark || undefined , destinationWarehouseCode : transferRelabelDefaultDestination . value || undefined , remark : item . remark || undefined } ) ) : undefined }
if ( props . service === 'transfer' && selectedTransferInventory . value ) {
; ( payload as TransferCreate ) . inboundCode = selectedTransferInventory . value
; ( payload as TransferCreate ) . sourceLastlegOrderIds = selectedTransferSourceOrderIds . value
@@ -1450,7 +1479,7 @@ async function loadTransferInventoryCandidates(warehouseId: string) {
relabelCandidatesLoading . value = true
try {
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
transferInventoryCandidates . value = ( await listRelabelInventoryCandidates ( warehouseId ) ) . filter ( item =>
transferInventoryCandidates . value = ( await listRelabelInventoryCandidates ( warehouseId ) ) . filter ( isUnlabelledTransferCandidate ) . filter ( item =>
item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? Number ( item . cartonAvailablePackages || 0 ) > 0
: Number ( item . availablePackages || 0 ) > 0 )
@@ -1519,6 +1548,12 @@ const transferInventoryOptions = computed(() => {
}
return [ ... grouped . values ( ) ]
} )
/** 已完成或部分完成换标的退货实物箱不再作为原箱来源;普通来源由逐箱接口过滤。 */
function isUnlabelledTransferCandidate ( item : RelabelInventoryCandidate ) {
if ( item . sourceType !== 'RETURN_CARTON' && item . sourceType !== 'RETURN_PARCEL' ) return true
return ! [ 'COMPLETED' , 'PARTIALLY_COMPLETED' ] . includes ( item . relabelStatus || '' )
&& inventoryQuantity ( item . relabelledQuantity || item . relabelledPackages ) === 0
}
async function selectTransferInventory ( value : string ) {
selectedTransferInventory . value = value || ''
selectedTransferSourceOrderIds . value = [ ]
@@ -1707,7 +1742,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
< label v-if = "props.service === 'labeling'"><span>贴标要求</span><el-input v-model="form.requirement" placeholder="请输入贴标要求" / > < / label >
< label v-else-if = "props.service === 'relabel' || (props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY')"><span>物流跟踪号</span><el-input v-model="form.trackingNo" placeholder="请输入物流跟踪号(选填)" / > < / label >
< label class = "warehouse-span-2" > < span > 备注 < / span > < el-input v-model = "form.remark" type="textarea" :rows="2" placeholder="选填" / > < / label >
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button :value="false">不需要贴标</el-radio-button><el-radio-button :value="true" > 需要贴标 < / el -radio -button > < / el-radio-group > < small > { { form . relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' } } < / small > < / div >
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-choice-field--relabel warehouse-span-2"><span><i>*</i>是否贴标</span><el-radio-group v-model="form.relabel"><el-radio-button v-if="!transferRelabelRequired" :value="false">不需要贴标</el-radio-button><el-radio-button :value="true" > 需要贴标 < / el -radio -button > < / el-radio-group > < small > { { form . relabel ? '已开启贴标,请在右侧填写贴标资料' : 'FBA 仓和私人仓均可选择是否贴标' } } < / small > < / div >
< / div >
< / div >
< div class = "warehouse-card warehouse-form-section warehouse-transfer-cargo-card" >
@@ -1782,11 +1817,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<el-alert v-if=" transferSourceError " :title=" transferSourceError " type=" error " :closable=" false "><el-button link @click=" loadTransferSourceCartons ">重新加载</el-button></el-alert>
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>箱号</th><th>商品 SKU</th><th>中文品名</th><th>箱内数量(件)</th><th>原目的仓</th></tr></thead><tbody>
<tr v-for=" box in transferSourceCartons " :key=" box . cartonMark "><td>{{ box.cartonMark }}</td><td>{{ box.items.map(item => item.skuCode || '—').join(' / ') }}</td><td>{{ box.productNameZh || '—' }}</td><td>{{ box.quantity ?? '未记录' }}</td><td>{{ [box.destinationWarehouseCode, box.destinationWarehouseName].filter(Boolean).join(' · ') || '未指定' }}</td></tr>
<tr v-if=" ! transferSourceLoading && ! transferSourceCartons . length "><td colspan=" 5 "><div class=" warehouse - empty ">{{ transferSourceError ? '加载失败,请重试' : '原库存单据未记录逐箱明细,不虚构箱号;按原库存汇总转运 ' }}</div></td></tr>
<tr v-if=" ! transferSourceLoading && ! transferSourceCartons . length "><td colspan=" 5 "><div class=" warehouse - empty ">{{ transferSourceError ? '加载失败,请重试' : '当前来源单暂无未换标的箱明细 ' }}</div></td></tr>
</tbody></table></div>
<div v-if=" ! transferSourceLoading && ! transferSourceError && ! transferSourceCartons . length " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU</th><th>中文品名</th><th>库存箱数</th><th>原目的仓</th></tr></thead><tbody><tr v-for=" row in cargoRows " :key=" row . id "><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
</section>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - section - heading " :class=" { 'warehouse-step-heading' : props . service === 'transfer' } "><div><h3><span v-if=" props . service === 'transfer' " class=" warehouse - step - number ">1</span>货物明细</h3></div><div v-if=" props . service !== 'disposal' " class=" warehouse - heading - actions "><el-button type=" primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [ ] ">清空</el-button></div></div>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - section - heading " :class=" { 'warehouse-step-heading' : props . service === 'transfer' } "><div><h3><span v-if=" props . service === 'transfer' " class=" warehouse - step - number ">1</span>货物明细</h3></div><div v-if=" props . service !== 'disposal' " class=" warehouse - heading - actions "><el-button v-if=" props . service === 'transfer' " size=" small " :loading=" saving " @click=" printForecastCartons "><Printer :size=" 15 " />打印唛头</el-button><el-button type="primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [ ] ">清空</el-button></div></div>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if=" props . service === 'disposal' ">可用箱数</th><th v-if=" props . service === 'relabel' ">FNSKU / 箱唛</th><th v-if=" props . service === 'transfer' ">SKU 数量(件)*</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if=" props . service !== 'disposal' ">操作</th></tr></thead><tbody><tr v-for=" row in cargoRows " :key=" row . id "><td><el-input v-model=" row . sku " :readonly=" props . service === 'disposal' " :placeholder=" props . service === 'transfer' ? '选填' : '请输入商品 SKU' " /></td><td><el-input v-model=" row . name " :readonly=" props . service === 'disposal' " :placeholder=" props . service === 'transfer' ? '选填' : '请输入中文品名' " /></td><td v-if=" props . service === 'disposal' "><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if=" props . service === 'relabel' "><el-input v-model=" row . shippingMark " placeholder=" 请输入FNSKU或箱唛 " /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入件数 " /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . expectedPackageCount " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入预计箱数 " /></td><td v-else><el-input-number v-model=" row . quantity " :disabled=" props . service === 'disposal' " :min=" props . service === 'disposal' ? 0 : 1 " :max=" row . availableQuantity " controls-position=" right " placeholder=" 请输入箱数 " /></td><td v-if=" props . service !== 'disposal' "><button type=" button " class=" warehouse - delete " title=" 删除 " @click=" removeCargoRow ( row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" cargoRows . length === 0 "><td :colspan=" props . service === 'disposal' ? 4 : props . service === 'relabel' || props . service === 'transfer' ? 5 : 4 "><div class=" warehouse - empty ">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
</template>
</div>
@@ -1826,8 +1861,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class=" warehouse - detail - actions "><el-button v-if=" canCreate && props . service === 'labeling' && selectedRelabelDetail ? . application . status === 'REJECTED' " type=" primary " @click=" resubmitRejectedRelabel ">修改后重新提交</el-button><el-button v-if=" canConfirmRelabel && props . service === 'labeling' && selectedRelabelDetail ? . application . status === 'CUSTOMER_CONFIRM_PENDING' && ! selectedRelabelDetail ? . application . customerConfirmedAt " type=" primary " @click=" confirmSelectedRelabel ">确认换标完成</el-button><el-button v-if=" canConfirmDiscard && props . service === 'disposal' && selectedDetail ? . status === 'PDA_COMPLETED' " type=" primary " @click=" openCompletionConfirmation ( String ( selectedDetail ? . discardNo || '' ) , 'disposal' ) ">废弃确认</el-button><el-button v-if=" canCancelDiscard && props . service === 'disposal' && selectedDetail ? . status === 'PENDING_REVIEW' " type=" danger " @click=" cancelSelectedDiscard ">取消申请</el-button><el-button v-if=" canCreate && props . service === 'transfer' && [ 'DRAFT' , 'REJECTED' ] . includes ( selectedDetail ? . status || '' ) " type=" primary " @click=" openEdit ( String ( selectedDetail ? . transferNo || selectedDetail ? . [ '0' ] || '' ) ) ">编辑</el-button><el-button v-if=" canConfirmTransfer && props . service === 'transfer' && selectedDetail ? . status === 'WAIT_CUSTOMER_CONFIRMATION' " type=" primary " @click=" confirmTransferMeasurements ">确认测量数据</el-button><el-tag v-if=" props . service !== 'transfer' " :type=" props . service === 'labeling' ? relabelStatusTagType ( selectedRelabelDetail ? . application . status || '' ) : undefined " effect=" light ">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
</header>
<div v-if=" detailError " class=" warehouse - detail - error " role=" alert "><strong>{{ detailError }}</strong><el-button @click=" loadDetailPage ">重新加载</el-button></div>
<el-alert v-else-if =" props . service === 'labeling' && selectedRelabelDetail ? . application . status === 'REJECTED' " class=" warehouse - rejected - alert " type=" error " :closable=" false " show-icon title=" 贴标单审核已驳回 " :description=" selectedRelabelDetail . application . reviewRemark || '平台未填写驳回备注' " />
<div v-else class=" warehouse - card warehouse - form - section "><h3>订单概览</h3><div v-if=" props . service === 'labeling' " class=" warehouse - detail - grid "><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>客户参考 号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if=" props . service === 'transfer' " class=" warehouse - detail - grid warehouse - transfer - overview - grid "><div><span>状态</span><el-tag :type=" transferStatusTagType ( selectedDetail ? . status || '' ) " effect=" light ">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' " class=" warehouse - overview - wide "><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type=" transferRelabelStatusTagType ( transferRelabelStatus ( selectedDetail || { } ) . code ) " effect=" light ">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class=" warehouse - detail - grid "><div v-for=" ( column , index ) in config . columns . slice ( 1 ) " :key=" column "><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
<ReviewRejectionNotice v-if=" ! detailError " :status ="props . service === 'labeling' ? selectedRelabelDetail ? . application . status : selectedDetail ? . status " :reason=" props . service === 'labeling' ? selectedRelabelDetail ? . application . reviewRemark : selectedDetail ? . reviewRemark " />
<div v-if=" ! detailError " class="warehouse - card warehouse - form - section "><h3>订单概览</h3><div v-if=" props . service === 'labeling' " class=" warehouse - detail - grid "><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>关联单 号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if=" props . service === 'transfer' " class=" warehouse - detail - grid warehouse - transfer - overview - grid "><div><span>状态</span><el-tag :type=" transferStatusTagType ( selectedDetail ? . status || '' ) " effect=" light ">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if=" selectedDetail ? . destinationType !== 'PRIVATE' "><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' " class=" warehouse - overview - wide "><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if=" selectedDetail ? . destinationType === 'PRIVATE' "><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预报 SKU 总件数</span><strong>{{ transferForecastQuantity }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type=" transferRelabelStatusTagType ( transferRelabelStatus ( selectedDetail || { } ) . code ) " effect=" light ">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class=" warehouse - detail - grid "><div v-for=" ( column , index ) in config . columns . slice ( 1 ) " :key=" column "><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
<TransferPickupProgressPanel v-if=" ! detailError && props . service === 'transfer' && selectedDetail ? . deliveryMethod === 'WAREHOUSE_PICKUP' " :transfer-no=" String ( selectedDetail . transferNo || '' ) " />
<div v-if=" props . service === 'disposal' " class=" warehouse - card warehouse - form - section ">
<h3>本次废弃箱子</h3><div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [ ] " :key=" index "><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div>
<h3>废弃现场照片</h3><div v-if=" selectedDetail ? . photos ? . length " class=" warehouse - photo - grid "><div v-for=" photo in selectedDetail . photos " :key=" photo . fileId " class=" warehouse - photo - item "><el-image v-if=" discardPhotoUrls [ photo . fileId ] " :src=" discardPhotoUrls [ photo . fileId ] " :preview-src-list=" discardPhotoPreviewUrls " :initial-index=" discardPhotoPreviewUrls . indexOf ( discardPhotoUrls [ photo . fileId ] ) " fit=" cover " preview-teleported /><div v-else-if=" discardPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadDiscardPhoto ( photo . fileId ) ">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div><div v-else class=" warehouse - empty ">暂无已提交的废弃现场照片</div>
@@ -1836,13 +1872,13 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button v-if=" canIncreaseForecast " type=" primary " size=" small " @click=" openIncreaseForecast "><Plus :size=" 14 " />增加预报箱数</el-button><el-button type=" primary " size=" small " @click=" printTransferCartons ( String ( selectedDetail ? . transferNo || '' ) ) "><Printer :size=" 14 " />打印预报箱唛</el-button><el-button size=" small " @click=" showForecastCartons = true ">查看预报箱唛</el-button></div></div>
<el-dialog v-model=" showForecastCartons " title=" 预报箱唛明细 " width=" 900 px " align-center destroy-on-close append-to-body><el-table :data=" selectedDetail ? . cartons || [ ] " size=" small " border><el-table-column prop=" cartonMark " label=" 仓库唛头 " min-width=" 300 " /><el-table-column label=" 预报SKU " min-width=" 240 "><template #default=" { row } ">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label=" 清点状态 " width=" 130 "><template #default=" { row } ">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click=" showForecastCartons = false ">关闭</el-button></template></el-dialog>
<el-dialog v-model=" increaseForecastOpen " title=" 增加预报箱数 " width=" 560 px " align-center destroy-on-close append-to-body :close-on-click-modal=" ! increasingForecast " :show-close=" ! increasingForecast ">
<el-form label-width=" 110 px "><el-form-item label=" 预报SKU "><el-select v-model=" increaseForecastForm . itemId " :disabled=" increasingForecast " style=" width : 100 % "><el-option v-for=" item in forecastSnapshot ? . items || [ ] " :key=" String ( item . id ) " :value=" String ( item . id ) " :label=" item . skuCode || item . productNameZh || '未命名SKU' " /></el-select></el-form-item><el-form-item label="新增箱数 "><el-input-number v-model=" increaseForecastForm . additionalCount " placeholder=" 请输入新增箱数 " :disabled=" increasingForecast " :min=" 1 " :max=" Math . max ( 1 , 5000 - Number ( forecastSnapshot ? . packageCount || 0 ) ) " :precision=" 0 " /></el-form-item></el-form><p class=" warehouse - form - hint ">保留原箱唛,仅补生成新增箱唛;SKU预报件数保持不变。</p>
<el-form label-width=" 110 px "><el-form-item label=" 新增箱数 "><el-input-number v-model=" increaseForecastForm . additionalCount " placeholder=" 请输入新增箱数 " :disabled=" increasingForecast " :min=" 1 " :max=" Math . max ( 1 , 5000 - Number ( forecastSnapshot ? . packageCount || 0 ) ) " :precision=" 0 " /></el-form-item></el-form><p class=" warehouse - form - hint ">保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准, SKU预报总 件数保持不变。</p>
<template #footer><el-button :disabled=" increasingForecast " @click=" increaseForecastOpen = false ">取消</el-button><el-button type=" primary " :loading=" increasingForecast " @click=" saveIncreaseForecast ">确认增加</el-button></template>
</el-dialog>
</div>
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - cargo - card ">
<div class=" warehouse - section - heading "><h3>预报与实际签收</h3></div>
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - comparison - table "><colgroup><col style=" width : 500 px " /><col style=" width : 140 px " /><col style=" width : 130 px " /><col style=" width : 100 px " /><col style=" width : 130 px " /><col style=" width : 130 px " /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [ ] " :key=" String ( item . id || index ) "><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '— ' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if=" ! selectedDetail ? . items ? . length "><td colspan=" 6 "><div class=" warehouse - empty ">暂无货物明细</div></td></tr></tbody></table></div>
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - comparison - table "><colgroup><col style=" width : 500 px " /><col style=" width : 140 px " /><col style=" width : 130 px " /><col style=" width : 100 px " /><col style=" width : 130 px " /><col style=" width : 130 px " /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [ ] " :key=" String ( item . id || index ) "><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '未记录 ' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if=" ! selectedDetail ? . items ? . length "><td colspan=" 6 "><div class=" warehouse - empty ">暂无货物明细</div></td></tr></tbody></table></div>
<details class=" warehouse - transfer - pallet - group " open>
<summary class=" warehouse - section - heading warehouse - transfer - pallet - heading warehouse - transfer - pallet - summary "><h3>PDA 打托实测数据</h3><span class=" warehouse - transfer - pallet - count ">{{ selectedDetail?.pallets?.length || 0 }} 托</span><span class=" warehouse - transfer - pallet - toggle "><span class=" when - collapsed ">展开托盘</span><span class=" when - expanded ">收起托盘</span><ChevronDown :size=" 14 " /></span></summary>
<div v-if=" selectedDetail ? . pallets ? . length " class=" warehouse - transfer - pallet - list ">
@@ -1887,16 +1923,18 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<h3>换标箱级去向</h3>
<div class=" warehouse - table - scroll "><table><thead><tr><th>换标类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>新 FBA 箱唛</th><th>实际 FBA 目的仓</th><th>数量</th><th>备注</th></tr></thead><tbody><tr v-for=" ( binding , index ) in selectedDetail ? . relabelBindings || [ ] " :key=" ` ${ binding . customerCartonMark } - ${ index } ` "><td>{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}</td><td>{{ binding.customerCartonMark || '—' }}</td><td>{{ binding.originalSku || '—' }}</td><td>{{ binding.newSku || '—' }}</td><td>{{ binding.fbaCartonMark || '—' }}</td><td>{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}</td><td>{{ binding.quantity ?? '—' }}</td><td>{{ binding.remark || '—' }}</td></tr><tr v-if=" ! selectedDetail ? . relabelBindings ? . length "><td colspan=" 8 "><div class=" warehouse - empty ">暂无换标绑定明细</div></td></tr></tbody></table></div>
</div>
<div v-if=" props . service === 'labeling' && canManageRelabelFiles " class=" warehouse - card warehouse - form - section ">
<div v-if=" props . service === 'labeling' && selectedRelabelDetail " class=" warehouse - card warehouse - form - section ">
<div class=" warehouse - section - heading "><h3>换标完成与出库</h3></div>
<div class=" warehouse - detail - grid warehouse - fulfillment - grid ">
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
<div><span>履约状态</span><el-tag :type=" relabelFulfillmentTagType ( selectedRelabelDetail ? . application . fulfillmentStatus ) " effect=" light ">{{ relabelFulfillmentLabel(selectedRelabelDetail?.application.fulfillmentStatus) }}</el-tag></div>
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
<div v-if=" selectedRelabelDetail "><RelabelDocumentsPanel inline :relabel-no=" selectedRelabelDetail . application . relabelNo " :detail=" selectedRelabelDetail " /></div>
</div>
<div class=" warehouse - evidence "><span>仓库完工照片</span><div v-if=" selectedRelabelDetail ? . completionEvidence ? . photoFileIds ? . length " class=" warehouse - photo - grid "><div v-for=" ( fileId , index ) in selectedRelabelDetail . completionEvidence . photoFileIds " :key=" fileId " class=" warehouse - photo - item "><el-image v-if=" completionPhotoUrls [ fileId ] " :src=" completionPhotoUrls [ fileId ] " :preview-src-list=" completionPhotoPreviewUrls " :initial-index=" completionPhotoPreviewUrls . indexOf ( completionPhotoUrls [ fileId ] ) " fit=" cover " preview-teleported /><div v-else-if=" completionPhotoLoading [ fileId ] " class=" warehouse - photo - placeholder ">照片 {{ index + 1 }} 加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadCompletionPhoto ( fileId ) ">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传完工照片</strong></div>
<p v-if=" selectedRelabelDetail ? . completionEvidence ? . completionRemark " class=" warehouse - completion - remark ">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . relabelNo " class=" warehouse - card warehouse - form - section "><RelabelDocumentsPanel :relabel-no=" selectedDetail . relabelNo " /></div>
<div v-if=" props . service === 'labeling' " class=" warehouse - card warehouse - form - section ">
<div class=" warehouse - section - heading "><h3>贴标明细资料</h3><input ref=" relabelFileInput " class=" warehouse - hidden - input " type=" file " accept=" application / pdf , . pdf " @change=" uploadSelectedRelabelFile "></div>
<div class=" warehouse - hint ">需要补充或替换资料时,可在对应明细行上传 SKU 或箱唛文件。</div>
@@ -2132,10 +2170,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
. warehouse - transfer - pallet - sku { display : inline - flex ; align - items : center ; gap : 8 px ; max - width : 100 % ; box - sizing : border - box ; padding : 4 px 8 px ; background : # f5f7ff ; border : 1 px solid # e5e9f8 ; border - radius : 5 px ; font - size : 12 px ; line - height : 1.6 }
. warehouse - transfer - pallet - sku strong { font - weight : 500 ; overflow - wrap : anywhere ; min - width : 0 ; color : var ( -- navy ) }
. warehouse - transfer - pallet - sku > span { flex - shrink : 0 ; padding : 0 7 px ; border : 1 px solid # e0e6ff ; border - radius : 4 px ; background : # eef1ff ; color : var ( -- primary ) ; font - weight : 600 ; white - space : nowrap }
. warehouse - service - page -- detail . warehouse - transfer - received - card . warehouse - transfer - receipt - heading { justify - content : flex - start ; flex - wrap : wrap ; gap : 10 px 16 px }
. warehouse - service - page -- detail . warehouse - transfer - receipt - heading h3 { flex : 0 0 auto ; margin : 0 ; padding : 0 ; border : 0 }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - section - heading { box - sizing : border - box ; min - height : 44 px ; justify - content : flex - start ; flex - wrap : wrap ; gap : 10 px 16 px ; padding : 8 px 16 px ; border - bottom : 1 px solid # edf1f5 }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - section - heading h3 { flex : 0 0 auto ; min - height : 0 ; margin : 0 ; padding : 0 ; border : 0 ; line - height : 24 px }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - transfer - cargo - card > . warehouse - table - scroll { box - sizing : border - box ; padding : 0 16 px }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - transfer - comparison - table th : first - child , . warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - transfer - comparison - table td : first - child { padding - left : 0 }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - transfer - pallet - list { padding : 0 16 px }
. warehouse - service - page -- detail . warehouse - service - page -- transfer . warehouse - measurement - evidence { margin : 12 px 16 px }
. warehouse - transfer - receipt - actions { display : flex ; flex - wrap : wrap ; gap : 8 px } . warehouse - transfer - receipt - actions : deep ( . el - button + . el - button ) { margin - left : 0 }
. warehouse - service - page -- detail > . warehouse - detail - header + . warehouse - card . warehouse - transfer - overview - grid { width : 100 % ; max - width : 1440 px ; box - sizing : border - box ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; column - gap : 18 px ; row - gap : 4 px ; padding : 8 px 14 px 10 px }
. warehouse - service - page -- detail > . warehouse - detail - header + . warehouse - card . warehouse - transfer - overview - grid > div { min - height : 24 px ; gap : 8 px ; padding : 1 px 0 }
. warehouse - service - page -- detail > . warehouse - detail - header + . warehouse - card . warehouse - transfer - overview - grid > div > span { flex : 0 0 84 px ; min - width : 0 }
. warehouse - service - page -- detail . warehouse - service - page -- transfer > . warehouse - card . warehouse - transfer - overview - grid { width : 100 % ; max - width : 1440 px ; box - sizing : border - box ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; column - gap : 18 px ; row - gap : 4 px ; padding : 8 px 16 px 10 px }
. warehouse - service - page -- detail . warehouse - service - page -- transfer > . warehouse - card . warehouse - transfer - overview - grid > div { min - height : 24 px ; gap : 8 px ; padding : 1 px 0 }
. warehouse - service - page -- detail . warehouse - service - page -- transfer > . warehouse - card . warehouse - transfer - overview - grid > div > span { flex : 0 0 84 px ; min - width : 0 }
< / style >