@@ -11,8 +11,8 @@ import CompletionConfirmationDialog from '../components/CompletionConfirmationDi
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import {
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer , resubmitRelabel ,
getTransferCartonLabel , submitTransfer , updateTransfer ,
confirmTransferMeasurement , downloadRelabelCompletionPhoto , downloadTransferMeasurementPhoto , downloadDiscardPhoto , type DiscardPhoto , uploadRelabelShippingLabel ,
getTransferCartonLabel , submitTransfer , updateTransfer , increaseTransferForecastCartons , getTransferSourceCartons , type TransferSourceCarton ,
downloadRelabelCompletionPhoto , downloadTransferMeasurementPhoto , downloadDiscardPhoto , type DiscardPhoto , uploadRelabelShippingLabel ,
uploadRelabelFile ,
listDiscards , listDiscardCandidates , listRelabelInventoryCandidates , listRelabelHistoricalOrders , listRelabels , listTransfers , cancelDiscard ,
type RelabelInventoryCandidate ,
@@ -58,8 +58,8 @@ const canConfirmTransfer = computed(() => auth.hasPermission('merchant:overseas-
const canConfirmDiscard = computed ( ( ) => auth . hasPermission ( 'merchant:overseas-warehouse:discard:confirm' ) )
const completionConfirmOpen = ref ( false )
const completionConfirmNo = ref ( '' )
const completionConfirmKind = ref < 'disposal' | 'labeling' > ( 'disposal' )
function openCompletionConfirmation ( no : string , kind : 'disposal' | 'labeling' ) {
const completionConfirmKind = ref < 'disposal' | 'labeling' | 'transfer' > ( 'disposal' )
function openCompletionConfirmation ( no : string , kind : 'disposal' | 'labeling' | 'transfer' ) {
if ( ! no ) return
completionConfirmNo . value = no ; completionConfirmKind . value = kind ; completionConfirmOpen . value = true
}
@@ -149,6 +149,34 @@ interface ServiceDetail extends Record<string, unknown> {
cartons ? : TransferCarton [ ] ; pallets ? : TransferPallet [ ] ; measurementPhotos ? : TransferMeasurementPhoto [ ] ; customerConfirmedAt ? : string
}
const selectedDetail = ref < ServiceDetail | null > ( null )
// 预报箱唛在提交后即可打印,与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 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 }
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 } )
selectedDetail . value = result as ServiceDetail ; increaseForecastOpen . value = false
ElMessage . success ( '预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点' )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '增加预报箱数失败' ) }
finally { increasingForecast . value = false }
}
const showTransferCartons = ref ( false )
function transferCartonItem ( carton : TransferCarton ) {
return selectedDetail . value ? . items ? . find ( ( item ) => carton . transferItemId != null && String ( item . id ) === String ( carton . transferItemId ) )
@@ -295,6 +323,40 @@ const availableSkusCollapsed = ref(false)
const relabelCandidatesLoading = ref ( false )
const transferInventoryCandidates = ref < RelabelInventoryCandidate [ ] > ( [ ] )
const selectedTransferInventory = ref ( '' )
const inventoryTransfer = computed ( ( ) => props . service === 'transfer' && Boolean ( selectedTransferInventory . value ) )
const transferSourceCartons = ref < TransferSourceCarton [ ] > ( [ ] )
const transferSourceLoading = ref ( false )
const transferSourceError = ref ( '' )
let transferSourceRequest = 0
async function loadTransferSourceCartons ( ) {
const request = ++ transferSourceRequest
transferSourceCartons . value = [ ] ; transferSourceError . value = ''
if ( ! inventoryTransfer . value ) return
transferSourceLoading . value = true
try {
// 退货候选本身就是逐箱库存,使用与下拉一致的实物箱数据,不能按普通入仓快照回查。
if ( selectedTransferInventory . value . startsWith ( 'RETURN_ORDER:' ) ) {
const returnNo = selectedTransferInventory . value . slice ( 'RETURN_ORDER:' . length )
transferSourceCartons . value = transferInventoryCandidates . value
. 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 ) )
. map ( item => {
const contents = item . cartonSkuItems ? . length ? item . cartonSkuItems : item . skuItems || [ ]
return { cartonMark : item . cartonMark ! , productNameZh : item . productNameZh ,
quantity : contents . reduce ( ( total , sku ) => total + Number ( sku . quantity || 0 ) , 0 ) ,
destinationWarehouseCode : item . destinationFbaWarehouseCode ,
destinationWarehouseName : item . destinationFbaWarehouseName ,
items : contents . map ( sku => ( { skuCode : sku . skuCode , quantity : Number ( sku . quantity || 0 ) } ) ) }
} )
if ( ! transferSourceCartons . value . length ) throw new Error ( '当前退货单没有可转运箱,请刷新库存后重试' )
return
}
const cartons = await getTransferSourceCartons ( form . warehouse , selectedTransferInventory . value )
if ( request === transferSourceRequest ) transferSourceCartons . value = cartons
} catch ( error ) { if ( request === transferSourceRequest ) transferSourceError . value = error instanceof Error ? error . message : '原单箱明细加载失败' }
finally { if ( request === transferSourceRequest ) transferSourceLoading . value = false }
}
// Snowflake IDs exceed JavaScript's safe integer range. Keep them as strings
// all the way to JSON so the backend receives the exact source order ID.
const selectedTransferSourceOrderIds = ref < string [ ] > ( [ ] )
@@ -546,6 +608,8 @@ async function openEdit(transferNo: string) {
const sourceInboundCode = String ( detail . inboundCode || '' )
if ( transferInventoryOptions . value . some ( ( option ) => option . value === sourceInboundCode ) ) {
selectedTransferInventory . value = sourceInboundCode
await loadTransferSourceCartons ( )
if ( transferDestinationType . value === 'NONE' ) transferDestinationType . value = 'FBA'
form . inboundNo = sourceInboundCode
selectedTransferSourceOrderIds . value = ( detail . sourceOrders || [ ] )
. map ( ( item ) => String ( item . lastlegOrderId || '' ) ) . filter ( Boolean )
@@ -577,36 +641,6 @@ async function selectDiscardWarehouse() {
}
function createCargoRow ( ) : CargoRow { return { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , expectedPackageCount : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } }
function addCargoRow ( ) { cargoRows . value . push ( createCargoRow ( ) ) }
function escapePrintHtml ( value : string ) {
return value . replaceAll ( '&' , '&' ) . replaceAll ( '<' , '<' ) . replaceAll ( '>' , '>' ) . replaceAll ( '"' , '"' ) . replaceAll ( "'" , ''' )
}
function printTransferDraftCartonMarks ( ) {
if ( props . service !== 'transfer' ) return
const validRows = cargoRows . value . filter ( ( row ) => row . sku . trim ( ) && Number ( row . expectedPackageCount ) > 0 )
if ( ! validRows . length || validRows . some ( ( row ) => ! Number . isInteger ( Number ( row . expectedPackageCount ) ) ) ) {
ElMessage . warning ( '请先填写商品 SKU 和对应的整数箱数' ) ; return
}
const totalCartons = validRows . reduce ( ( sum , row ) => sum + Number ( row . expectedPackageCount ) , 0 )
if ( totalCartons > 500 ) { ElMessage . warning ( '单次最多打印 500 张箱唛' ) ; return }
const batch = new Date ( ) . toISOString ( ) . replace ( /\D/g , '' ) . slice ( 2 , 14 )
const labels : Array < { mark : string ; sku : string ; index : number } > = [ ]
let cartonIndex = 0
for ( const [ rowIndex , row ] of validRows . entries ( ) ) {
let marks = transferCustomerMarks ( row )
if ( marks . length !== Number ( row . expectedPackageCount ) ) {
marks = Array . from ( { length : Number ( row . expectedPackageCount ) } , ( _ , index ) => ` CT- ${ batch } - ${ String ( rowIndex + 1 ) . padStart ( 2 , '0' ) } - ${ String ( index + 1 ) . padStart ( 3 , '0' ) } ` )
row . customerCartonMarks = marks . join ( '\n' )
}
for ( const mark of marks ) labels . push ( { mark , sku : row . sku . trim ( ) , index : ++ cartonIndex } )
}
const printWindow = window . open ( '' , '_blank' )
if ( ! printWindow ) { ElMessage . warning ( '浏览器阻止了打印窗口,请允许弹窗后重试' ) ; return }
printWindow . opener = null
const sections = labels . map ( ( label ) => ` <section class="label"><div class="title">仓储转运客户箱唛</div><div class="mark"> ${ escapePrintHtml ( label . mark ) } </div><div class="sku">SKU: ${ escapePrintHtml ( label . sku ) } </div><div class="meta"> ${ label . index } / ${ labels . length } </div></section> ` ) . join ( '' )
printWindow . document . open ( )
printWindow . document . write ( ` <!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body> ${ sections } </body></html> ` )
printWindow . document . close ( ) ; printWindow . focus ( ) ; printWindow . print ( )
}
function addTransferRelabelRow ( ) {
const source = cargoRows . value [ Math . min ( transferRelabelRows . value . length , Math . max ( 0 , cargoRows . value . length - 1 ) ) ]
transferRelabelRows . value . push ( { ... createCargoRow ( ) , sku : source ? . sku || '' } )
@@ -1059,7 +1093,7 @@ function transferRelabelStatusTagType(status: string): StatusTagType {
function deliveryMethodLabel ( value : string ) {
const labels : Record < string , string > = {
TRUCK : '卡车派送' , EXPRESS : '快递派送' , AMAZON _PICKUP : '亚马逊自提' , CUSTOMER _PICKUP : '客户自提' ,
WAREHOUSE _PICKUP : '仓库上门提货' , CUSTOMER _DELIVERY : '客户送货到仓' ,
INVENTORY _TRANSFER : '库存转运' , WAREHOUSE _PICKUP : '仓库上门提货' , CUSTOMER _DELIVERY : '客户送货到仓' ,
// Keep the legacy value readable for historical applications created before the four-way split.
PICKUP : '仓库自提' ,
}
@@ -1161,7 +1195,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if ( props . service === 'relabel' && ( ! form . destination || ! form . address || ! form . deliveryMethod || ! form . estimatedArrivalAt || ! form . fbaShipmentNo || form . volumeCbm <= 0 ) ) {
ElMessage . warning ( '请填写完整的派送信息、体积、预计到达时间和 FBA 货件号' ) ; return
}
if ( props . service === 'transfer' && ( ! form . warehouse || ! form . deliveryMethod || ! form . address || ! form . estimatedArrivalAt || ( form . deliveryMethod === 'WAREHOUSE_PICKUP' && ( ! form . pickupContact || ! form . pickupPhone ) ) ) ) {
if ( props . service === 'transfer' && ! inventoryTransfer . value && ( ! form . warehouse || ! form . deliveryMethod || ! form . address || ! form . estimatedArrivalAt || ( form . deliveryMethod === 'WAREHOUSE_PICKUP' && ( ! form . pickupContact || ! form . pickupPhone ) ) ) ) {
const missing = [ ! form . warehouse && '海外仓' , ! form . deliveryMethod && '送货方式' , ! form . address && '地址' , form . deliveryMethod === 'WAREHOUSE_PICKUP' && ! form . pickupContact && '提货联系人' , form . deliveryMethod === 'WAREHOUSE_PICKUP' && ! form . pickupPhone && '联系人电话' , ! form . estimatedArrivalAt && '预计入仓时间' ] . filter ( Boolean ) . join ( '、' )
ElMessage . warning ( ` 请填写: ${ missing || '必填信息' } ` ) ; return
}
@@ -1169,6 +1203,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
! Number . isSafeInteger ( row . quantity ) || row . quantity < 1 || ! Number . isSafeInteger ( row . expectedPackageCount ) || Number ( row . expectedPackageCount ) < 1 ) ) ) {
ElMessage . warning ( '请填写每个 SKU 的数量(件)和预计箱数,均须为正整数' ) ; return
}
if ( inventoryTransfer . value && ( transferSourceLoading . value || transferSourceError . value ) ) { ElMessage . warning ( '请等待原单箱明细加载成功后再保存' ) ; return }
if ( inventoryTransfer . value && transferDestinationType . value === 'NONE' ) { ElMessage . warning ( '库存转运请选择 FBA 仓或私人仓' ) ; return }
if ( props . service === 'transfer' && transferDestinationType . value === 'FBA' && ! form . destination ) {
ElMessage . warning ( '请选择 FBA 目的仓' ) ; return
}
@@ -1182,6 +1218,9 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if ( action === 'submit' && ! requiresBothTotalFiles . value && ! cartonTotalFile . value && ! skuTotalFile . value ) { ElMessage . warning ( '请上传箱唛总文件或 SKU 总文件(二选一)' ) ; return }
const relabelRows = transferRelabelRows . value
const allMarks = relabelRows . flatMap ( transferCustomerMarks )
if ( inventoryTransfer . value && allMarks . some ( mark => ! transferSourceCartons . value . some ( box => box . cartonMark === mark ) ) ) {
ElMessage . error ( '当前箱子不存在或不属于当前订单' ) ; return
}
if ( ! relabelRows . length || relabelRows . some ( ( row ) => transferCustomerMarks ( row ) . length !== row . quantity ) ) { ElMessage . warning ( '每个换标明细的客户箱唛数量必须与换标数量一致' ) ; return }
if ( new Set ( allMarks . map ( ( mark ) => mark . toUpperCase ( ) ) ) . size !== allMarks . length ) { ElMessage . warning ( '客户箱唛号不能重复' ) ; return }
if ( allMarks . join ( ',' ) . length > 2000 || relabelRows . some ( ( row ) => transferCustomerMarks ( row ) . join ( ',' ) . length > 255 ) ) { ElMessage . warning ( '客户箱唛内容过长,请减少单次提交箱数或缩短箱唛号' ) ; return }
@@ -1233,7 +1272,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 : form . address , packageCount , volumeCbm : 0 , deliveryMethod : form . deliveryMethod , estimatedArrivalAt : form . estimatedArrivalAt , estimatedShipAt : form . estimatedShipAt || undefined , fbaShipmentNo : transferDestinationType . value === 'FBA' ? form . fbaShipmentNo || undefined : undefined , trackingNo : 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 : form . deliveryMethod === 'WAREHOUSE_PICKUP' ? form . pickupContact : undefined , pickupContactPhone : 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 } ) ) , 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
@@ -1298,9 +1337,7 @@ async function loadDetailPage() {
async function confirmTransferMeasurements ( ) {
const transferNo = String ( selectedDetail . value ? . transferNo || selectedDetail . value ? . [ '0' ] || '' )
if ( ! transferNo ) return
try { await ElMessageBox . confirm ( '请核对 PDA 回传的箱数、尺寸、重量和打托数据。' , '确认测量数据' , { confirmButtonText : '确认数据' , cancelButtonText : '取消' } ) } catch { return }
try { selectedDetail . value = await confirmTransferMeasurement ( transferNo ) as ServiceDetail ; ElMessage . success ( '仓库实测数据已确认' ) }
catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '确认失败' ) }
openCompletionConfirmation ( transferNo , 'transfer' )
}
async function printTransferCartons ( transferNo : string ) {
if ( props . service !== 'transfer' ) return
@@ -1321,9 +1358,7 @@ function clearCompletionPhotos() {
for ( const key of Object . keys ( completionPhotoErrors ) ) delete completionPhotoErrors [ key ]
}
async function confirmTransferFromList ( transferNo : string ) {
try { await ElMessageBox . confirm ( '请核对 PDA 回传的箱数、尺寸、重量和打托数据。' , '确认测量数据' , { confirmButtonText : '确认数据' , cancelButtonText : '取消' } ) } catch { return }
try { await confirmTransferMeasurement ( transferNo ) ; ElMessage . success ( '仓库实测数据已确认' ) ; await load ( ) }
catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '确认失败' ) }
openCompletionConfirmation ( transferNo , 'transfer' )
}
async function loadCompletionPhoto ( fileId : string ) {
const relabelNo = selectedRelabelDetail . value ? . application . relabelNo
@@ -1414,7 +1449,11 @@ async function loadTransferInventoryCandidates(warehouseId: string) {
if ( ! warehouseId ) return
relabelCandidatesLoading . value = true
try {
transferInventoryCandidates . value = await listRelabelInventoryCandidates ( warehouseId )
// 共用接口包含退货历史及占用中的箱子,转运只展示当前可整箱使用的来源。
transferInventoryCandidates . value = ( await listRelabelInventoryCandidates ( warehouseId ) ) . filter ( item =>
item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? Number ( item . cartonAvailablePackages || 0 ) > 0
: Number ( item . availablePackages || 0 ) > 0 )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '当前海外仓库存来源加载失败' )
} finally { relabelCandidatesLoading . value = false }
@@ -1465,39 +1504,57 @@ async function resubmitRejectedRelabel() {
const transferInventoryOptions = computed ( ( ) => {
const grouped = new Map < string , { value : string ; label : string ; packages : number } > ( )
for ( const item of transferInventoryCandidates . value ) {
const value = String ( item . inboundCode || '' )
// 退货库存没有普通入仓码,使用带前缀的退货来源编码,避免提交时被当作普通入仓单查询。
const returnNo = String ( item . inboundOrderNo || '' ) . trim ( )
const value = item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? ( returnNo ? ` RETURN_ORDER: ${ returnNo } ` : '' )
: String ( item . inboundCode || '' )
if ( ! value ) continue
const packages = inventoryQuantity ( item . availablePackages )
const packages = item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL'
? inventoryQuantity ( item . cartonAvailablePackages || 0 )
: inventoryQuantity ( item . availablePackages )
const current = grouped . get ( value )
if ( current ) current . packages += packages
else grouped . set ( value , { value , label : ` ${ item . inboundOrderNo || value } | ${ value } ` , packages } )
else grouped . set ( value , { value , label : value . startsWith ( 'RETURN_ORDER:' ) ? ` 退货单| ${ returnNo } ` : ` ${ item . inboundOrderNo || value } | ${ value } ` , packages } )
}
return [ ... grouped . values ( ) ]
} )
async function selectTransferInventory ( value : string ) {
selectedTransferInventory . value = value || ''
selectedTransferSourceOrderIds . value = [ ]
if ( ! value ) return
transferSourceCartons . value = [ ] ; transferSourceError . value = '' ; ++ transferSourceRequest
if ( ! value ) { form . inboundNo = '' ; cargoRows . value = [ createCargoRow ( ) ] ; transferSourceLoading . value = false ; return }
if ( transferDestinationType . value === 'NONE' ) transferDestinationType . value = 'FBA'
void loadTransferSourceCartons ( )
form . inboundNo = value
// The inventory-candidates response already contains the merchant-visible
// SKU/name/package data. Do not call the platform-only last-leg orders API
// here: merchants are not authorized for that endpoint (A0403), and the
// transfer service can derive source orders from inboundCode when IDs are
// omitted. Aggregate candidates for the selected inbound into cargo rows.
const candidates = transferInventoryCandidates . value . filter ( ( item ) => String ( item . inboundCode || '' ) === value )
const isReturnSource = value . startsWith ( 'RETURN_ORDER:' )
const returnNo = isReturnSource ? value . slice ( 'RETURN_ORDER:' . length ) : ''
const candidates = transferInventoryCandidates . value . filter ( ( item ) => isReturnSource
? ( item . sourceType === 'RETURN_CARTON' || item . sourceType === 'RETURN_PARCEL' ) && String ( item . inboundOrderNo || '' ) === returnNo
: String ( item . inboundCode || '' ) === value )
const grouped = new Map < string , CargoRow > ( )
for ( const candidate of candidates ) {
const sku = String ( candidate . skuCode || '' )
const current = grouped . get ( sku )
const quantity = inventoryQuantity ( candidate . availablePackages )
if ( current ) current . quantity += quantity
else grouped . set ( sku , { ... createCargoRow ( ) , sku , name : candidate . productNameZh || '' , quantity , availableQuantity : quantity } )
const candidateRows = isReturnSource
? ( candidate . cartonSkuItems ? . length ? candidate . cartonSkuItems : candidate . skuItems || [ ] ) . map ( item => ( { sku : item . skuCode , name : candidate . productNameZh || '' , quantity : inventoryQuantity ( item . quantity ) , packageCount : inventoryQuantity ( candidate . cartonAvailablePackages || 0 ) } ) )
: [ { sku : String ( candidate . skuCode || '' ) , name : candidate . productNameZh || '' , quantity : inventoryQuantity ( candidate . availablePackages ) , packageCount : inventoryQuantity ( candidate . availablePackages ) } ]
for ( const row of candidateRows ) {
if ( ! row . sku ) continue
const current = grouped . get ( row . sku )
if ( current ) { current . quantity += row . quantity ; current . expectedPackageCount = Number ( current . expectedPackageCount || 0 ) + row . packageCount ; current . availableQuantity = Number ( current . availableQuantity || 0 ) + row . quantity }
else grouped . set ( row . sku , { ... createCargoRow ( ) , sku : row . sku , name : row . name , quantity : row . quantity , expectedPackageCount : row . packageCount , availableQuantity : row . quantity } )
}
}
cargoRows . value = [ ... grouped . values ( ) ]
try {
if ( isReturnSource ) return
const historical = await listRelabelHistoricalOrders ( form . warehouse , value )
const sourceIds = historical . map ( ( item ) => String ( item . id || '' ) ) . filter ( Boolean )
selectedTransferSourceOrderIds . value = sourceIds
if ( selectedTransferInventory . value === value ) selectedTransferSourceOrderIds . value = sourceIds
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '库存来源订单加载失败' )
}
@@ -1613,14 +1670,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
< label v-if = "props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" placeholder="请选择平台维护的 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" / > < / el-select > < / label >
< label v-else-if = "props.service === 'disposal'" class="warehouse-disposal-field"><span><i>*</i>废弃原因</span><el-select v-model="form.reason" placeholder="请选择"><el-option label="客户退货" value="客户退货" /><el-option label="亚马逊移除" value="亚马逊移除" /><el-option label="残损不可售" value="残损不可售" / > < / el-select > < / label >
< label v-else > < span > < i > * < / i > { { props . service === 'labeling' ? '海外操作仓(中转仓)' : props . service === 'transfer' ? '入库海外仓' : '操作仓' } } < / span > < el-select v-model = "form.warehouse" :loading="fbaWarehousesLoading || operationWarehouseDetailLoading" :disabled="Boolean(operationWarehousesError)" clearable :placeholder="props.service === 'transfer' ? '请选择海外仓' : '请选择国外中转仓'" @change="props.service === 'labeling' ? selectOperationWarehouse() : props.service === 'transfer' && selectTransferWarehouse()"><el-option v-for="warehouse in operationWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="String(warehouse.id)" /></el-select><small v-if="operationWarehousesError" class="warehouse-hint" > {{ operationWarehousesError }} < / small > < / label >
< label v-if = "props.service === 'labeling'" class="warehouse-span-2" ><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="搜索单号或选择可用库存" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" /></el-select><small class="warehouse-hint" > 在仓数量包含被占用货物 ; 本次可选数量才可加入申请 。 合箱后的库存按当前包裹 / 箱展示 , 来源退货单保留 。 < / small > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>选择入仓单或退货单</span><el-select v-model="selectedRelabelInboundOrderId" filterable clearable :filter-method="(value: string) => relabelInventorySearch = value" :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="暂无匹配的可选库存" placeholder="搜索单号或选择可用库存" @change="selectRelabelInventory"><el-option v-for="option in visibleRelabelInboundOptions.filter(item => !relabelInventorySearch.trim() || item.inboundOrderNo.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()) || item.inboundCode.toUpperCase().includes(relabelInventorySearch.trim().toUpperCase()))" :key="option.value" :label="relabelOptionLabel(option)" :value="option.value" :disabled="option.availablePackages <= 0" / > < / el-select > < / label >
< label v-if = "props.service === 'transfer'"><span>库存转运来源(可选)</span><el-select v-model="selectedTransferInventory" filterable clearable :loading="relabelCandidatesLoading" :disabled="!form.warehouse" no-data-text="当前海外仓暂无可用库存" placeholder="不选则按新货转运;选择后免 PDA 测量" @change="selectTransferInventory"><el-option v-for="option in transferInventoryOptions" :key="option.value" :label="`${option.label}|可用 ${option.packages} 箱`" :value="option.value" /></el-select><small class="warehouse-hint" > 库存转运可直接进入目的仓 、 换标和排车流程 , PDA 可拆托 、 重打或同目的仓合托 。 < / small > < / label >
< label v-if = "props.service === 'labeling' && labelingMode !== 'sku'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择本张申请统一的 FBA 目的仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" / > < / el-select > < / label >
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY" > 客户送货到仓 < / el -radio -button > < / el-radio-group > < / div >
< label v-if = "props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" / > < / label >
< label v-if = "props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" / > < / label >
< label v-if = "props.service === 'transfer' && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" / > < / label >
< div v-if = "props.service === 'transfer' && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2" >
< div v-if = "props.service === 'transfer' && !inventoryTransfer " class="warehouse-choice-field warehouse-span-2"><span><i>*</i>入仓方式</span><el-radio-group v-model="form.deliveryMethod" @change="selectTransferInboundMethod"><el-radio-button value="WAREHOUSE_PICKUP">仓库上门提货</el-radio-button><el-radio-button value="CUSTOMER_DELIVERY" > 客户送货到仓 < / el -radio -button > < / el-radio-group > < / div >
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'" class="pickup-address-field"><span><i>*</i>提货地址</span><el-input v-model="form.address" placeholder="请输入仓库上门提货的详细地址" / > < / label >
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人</span><el-input v-model="form.pickupContact" placeholder="请输入提货联系人" maxlength="128" / > < / label >
< label v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'WAREHOUSE_PICKUP'"><span><i>*</i>提货联系人电话</span><el-input v-model="form.pickupPhone" placeholder="请输入提货联系人电话" maxlength="64" / > < / label >
< div v-if = "props.service === 'transfer' && !inventoryTransfer && form.deliveryMethod === 'CUSTOMER_DELIVERY'" v-loading="operationWarehouseDetailLoading" class="warehouse-receiving-card warehouse-span-2" >
< div > < span > 海外仓收货信息 < / span > < strong > { { operationWarehouseAddress || '该海外仓暂未维护收货地址' } } < / strong > < / div >
< label class = "warehouse-span-2" > < span > < i > * < / i > 送货地址 < / span > < el-input v-model = "form.address" placeholder="请输入实际送货地址" / > < / label >
< div > < span > 收货联系人 < / span > < strong > { { selectedOperationWarehouse ? . warehouse . contactName || '未维护' } } < / strong > < / div >
@@ -1637,9 +1694,9 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
< label v-if = "canManageRelabelFiles && props.service === 'labeling' && labelingMode !== 'sku' && form.deliveryMethod === 'EXPRESS' && form.relabel"><span><i>*</i>上传面单文件</span><input class="warehouse-native-file" type="file" accept="application/pdf,image/*,.pdf" @change="selectShippingLabel" > < / label >
< label v-if = "props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" / > < / label >
< label v-if = "props.service === 'relabel'"><span><i>*</i>预估总体积(CBM) </span><el-input-number v-model="form.volumeCbm" :min="0" :precision="4" :step="0.0001" controls-position="right" placeholder="请输入预估总体积" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" / > < / label >
< label v-if = "( props.service === 'transfer' && !inventoryTransfer) || props.service === 'relabel'"><span><i>*</i>{{ props.service === 'transfer' ? '预计入仓时间' : '预计到达时间' }}</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" :placeholder="props.service === 'transfer' ? '请选择预计入仓时间' : '请选择预计到达时间'" / > < / label >
< label v-if = "props.service === 'transfer'"><span>期望发货时间</span><el-date-picker v-model="form.estimatedShipAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选填" / > < / label >
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 < / el -radio -button > < / el-radio-group > < / div >
< div v-if = "props.service === 'transfer'" class="warehouse-choice-field warehouse-span-2"><span>目的去向</span><el-radio-group v-model="transferDestinationType" @change="selectTransferDestinationType"><el-radio-button v-if="!inventoryTransfer" value="NONE">暂未确定</el-radio-button><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 < / el -radio -button > < / el-radio-group > < / div >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'FBA'"><span><i>*</i>FBA 目的仓</span><el-select v-model="form.destination" :loading="fbaWarehousesLoading" :disabled="Boolean(fbaWarehousesError)" filterable placeholder="请选择 FBA 仓"><el-option v-for="warehouse in fbaWarehouses" :key="warehouse.id" :label="warehouseLabel(warehouse)" :value="warehouse.warehouseCode" / > < / el-select > < / label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'FBA'"><span>FBA 货件编号</span><el-input v-model="form.fbaShipmentNo" placeholder="选填,可后续补充" / > < / label >
< label v-if = "props.service === 'transfer' && transferDestinationType === 'PRIVATE'"><span><i>*</i>私人仓地址</span><el-input v-model="form.privateDestinationAddress" maxlength="1000" placeholder="请输入私人仓完整地址" / > < / label >
@@ -1720,13 +1777,22 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
</template>
<el-alert v-else type=" info " :closable=" false " :title=" discardSelectedCandidate . cartonSelectionBlockReason || '当前库存缺少完整箱级记录,暂时只能选择全部库存废弃。' " />
</section>
<div v-if=" 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 " :disabled=" ! cargoRows . some ( ( row ) => row . sku . trim ( ) && Number ( row . expectedPackageCount ) > 0 ) " @click=" printTransferDraftCartonMarks "><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=" 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>
<section v-if=" inventoryTransfer " v-loading ="transferSourceLoading ">
<div class=" warehouse - section - heading "><h3>原单箱明细 <small>共 {{ transferSourceCartons.length }} 箱 · 只读</small></h3 ></div>
<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>
</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 - 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>
<div v-if=" props . service === 'transfer' && form . relabel " class=" warehouse - card warehouse - form - section warehouse - transfer - relabel ">
<div class=" warehouse - section - heading warehouse - labeling - heading warehouse - step - heading "><div><h3><span class=" warehouse - step - number ">2</span>换标申请</h3></div></div>
<div class=" warehouse - form - grid warehouse - transfer - relabel - fields "><label><span><i>*</i>换标截止时间</span><el-date-picker v-model=" form . labelDeadline " type=" datetime " value-format=" YYYY - MM - DDTHH : mm : ss " placeholder=" 请选择截止时间 " /></label><label><span><i>*</i>换标联系人</span><el-input v-model=" form . relabelContact " placeholder=" 选择海外仓后自动带出 , 也可手动填写 " maxlength=" 128 " /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model=" form . relabelPhone " placeholder=" 选择海外仓后自动带出 , 也可手动填写 " maxlength=" 64 " /></label><label v-if=" labelingMode !== 'sku' "><span><i>*</i>换标后派送方式</span><el-select v-model=" form . relabelDeliveryMethod " placeholder=" 请选择派送方式 "><el-option label=" 卡车派送 " value=" TRUCK " /><el-option label=" 快递派送 " value=" EXPRESS " /><el-option label=" 亚马逊自提 " value=" AMAZON _PICKUP " /><el-option label=" 客户自提 " value=" CUSTOMER _PICKUP " /></el-select></label><label class=" warehouse - span - 2 "><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model=" transferRelabelDefaultDestination " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " filterable placeholder=" 请选择整票货物的目的仓 "><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select><small class=" warehouse - hint ">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></label><label class=" warehouse - span - 2 "><span>贴标要求</span><el-input v-model=" form . requirement " type=" textarea " :rows=" 2 " maxlength=" 1000 " show-word-limit placeholder=" 请输入贴标要求 ( 选填 ) " /></label></div>
<div class=" warehouse - form - grid warehouse - transfer - relabel - fields "><label><span><i>*</i>换标截止时间</span><el-date-picker v-model=" form . labelDeadline " type=" datetime " value-format=" YYYY - MM - DDTHH : mm : ss " placeholder=" 请选择截止时间 " /></label><label><span><i>*</i>换标联系人</span><el-input v-model=" form . relabelContact " placeholder=" 选择海外仓后自动带出 , 也可手动填写 " maxlength=" 128 " /></label><label><span><i>*</i>换标联系人电话</span><el-input v-model=" form . relabelPhone " placeholder=" 选择海外仓后自动带出 , 也可手动填写 " maxlength=" 64 " /></label><label v-if=" labelingMode !== 'sku' "><span><i>*</i>换标后派送方式</span><el-select v-model=" form . relabelDeliveryMethod " placeholder=" 请选择派送方式 "><el-option label=" 卡车派送 " value=" TRUCK " /><el-option label=" 快递派送 " value=" EXPRESS " /><el-option label=" 亚马逊自提 " value=" AMAZON _PICKUP " /><el-option label=" 客户自提 " value=" CUSTOMER _PICKUP " /></el-select></label><label class=" warehouse - span - 2 "><span><i>*</i>整票换标货物目的 FBA 仓</span><el-select v-model=" transferRelabelDefaultDestination " :loading=" fbaWarehousesLoading " :disabled=" Boolean ( fbaWarehousesError ) " filterable placeholder=" 请选择整票货物的目的仓 "><el-option v-for=" warehouse in fbaWarehouses " :key=" warehouse . id " :label=" warehouseLabel ( warehouse ) " :value=" warehouse . warehouseCode " /></el-select><small class=" warehouse - hint ">本票所有换标货物统一前往该目的仓,暂不支持一票多个目的仓。</small></label><label><span>FBA 货件编号</span><el-input v-model=" form . fbaShipmentNo " placeholder=" 请输入 FBA 货件编号 ( 选填 ) " /></label><label class="warehouse - span - 2 "><span>贴标要求</span><el-input v-model=" form . requirement " type=" textarea " :rows=" 2 " maxlength=" 1000 " show-word-limit placeholder=" 请输入贴标要求 ( 选填 ) " /></label></div>
<div class=" warehouse - labeling - mode "><span>换标类型</span><el-radio-group v-model=" labelingMode " aria-label=" 换标类型 "><el-radio-button value=" carton ">只换箱唛</el-radio-button><el-radio-button value=" sku ">只换 SKU 标签</el-radio-button><el-radio-button value=" carton _and _sku ">SKU 和箱唛都换</el-radio-button></el-radio-group></div>
<div class=" warehouse - transfer - file - groups warehouse - file - groups -- compact ">
<section v-if=" canManageRelabelFiles " class=" warehouse - file - group warehouse - transfer - total - files ">
@@ -1743,11 +1809,11 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<input ref=" skuTemplateInput " class=" warehouse - hidden - input " type=" file " accept=" . csv , text / csv " @change=" importRelabelTemplate ( $event , 'sku' ) "><input ref=" cartonTemplateInput " class=" warehouse - hidden - input " type=" file " accept=" . csv , text / csv " @change=" importRelabelTemplate ( $event , 'carton' ) "><input ref=" mixedTemplateInput " class=" warehouse - hidden - input " type=" file " accept=" . csv , text / csv " @change=" importRelabelTemplate ( $event , 'mixed' ) ">
</section>
</div>
<div v-if=" importedRelabelBindings . length " class=" warehouse - import - preview "><div class=" warehouse - section - heading "><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value=" importedRelabelBindings . every ( ( item ) => item . selected ) " @change=" selectAllImportedBindings ( Boolean ( $event ) ) ">全选</el-checkbox><el-button type=" primary " size=" small " @click=" importSelectedBindingsToDetails ">导入到明细</el-button></div></div><div class=" warehouse - table - scroll "><table class=" warehouse - binding - preview - table "><thead><tr><th>选择</th><th>关系类型</th><th>客户箱唛号 </th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for=" item in importedRelabelBindings " :key=" item . id "><td><el-checkbox v-model=" item . selected " /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
<div v-if=" importedRelabelBindings . length " class=" warehouse - import - preview "><div class=" warehouse - section - heading "><div><strong>待导入绑定关系</strong><p>可只勾选本次需要换标的部分,也可以全选后一次导入。</p></div><div><el-checkbox :model-value=" importedRelabelBindings . every ( ( item ) => item . selected ) " @change=" selectAllImportedBindings ( Boolean ( $event ) ) ">全选</el-checkbox><el-button type=" primary " size=" small " @click=" importSelectedBindingsToDetails ">导入到明细</el-button></div></div><div class=" warehouse - table - scroll "><table class=" warehouse - binding - preview - table "><thead><tr><th>选择</th><th>关系类型</th><th>仓库唛头 </th><th>旧 SKU</th><th>新 SKU</th><th>数量</th><th>新 FBA 箱唛</th></tr></thead><tbody><tr v-for=" item in importedRelabelBindings " :key=" item . id "><td><el-checkbox v-model=" item . selected " /></td><td>{{ item.templateType === 'mixed' ? 'SKU 混箱换标' : item.templateType === 'carton' ? '箱号关联' : 'SKU 关联' }}</td><td>{{ item.cartonMark }}</td><td>{{ item.originalSku || '—' }}</td><td>{{ item.newSku || '—' }}</td><td>{{ item.quantity }}</td><td>{{ item.fbaCartonMark || '—' }}</td></tr></tbody></table></div></div>
<div class=" warehouse - section - heading warehouse - tab - actions warehouse - binding - heading "><div><strong><span class=" warehouse - step - number ">4</span>绑定明细</strong></div><div><el-button type=" primary " size=" small " @click=" addTransferRelabelRow "><Plus :size=" 15 " />添加 SKU 明细</el-button><el-button size=" small " @click=" clearTransferRelabelRows ">清空</el-button></div></div>
<div v-if=" labelingMode === 'carton' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>客户箱唛号 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " type=" textarea " :rows=" 2 " placeholder="请输入客户箱唛号 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 5 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加换标明细”</div></td></tr></tbody></table></div>
<div v-else-if=" labelingMode === 'sku' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " type=" textarea " :rows=" 2 " placeholder="请输入客户箱唛号 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " placeholder=" 请输入新 SKU " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入箱数 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 6 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-else class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>客户箱唛号 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " type=" textarea " :rows=" 2 " placeholder="请输入客户箱唛号 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " placeholder=" 请输入新 SKU " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入数量 " /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 7 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-if=" labelingMode === 'carton' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " placeholder=" 请输入仓库唛头 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 5 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加换标明细”</div></td></tr></tbody></table></div>
<div v-else-if=" labelingMode === 'sku' " class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " placeholder=" 请输入仓库唛头 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " placeholder=" 请输入新 SKU " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入箱数 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 6 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
<div v-else class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - mark - table "><thead><tr><th>仓库唛头 *</th><th>旧 SKU *</th><th>新 SKU *</th><th>换标数量(件)*</th><th>新 FBA 箱唛 *</th><th>备注</th><th>操作</th></tr></thead><tbody><tr v-for=" row in transferRelabelRows " :key=" row . id "><td><el-input v-model=" row . customerCartonMarks " placeholder=" 请输入仓库唛头 " /></td><td><el-input :model-value=" row . sku " readonly /></td><td><el-input v-model=" row . newSkuCode " placeholder=" 请输入新 SKU " /></td><td><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入数量 " /></td><td><el-input v-model=" row . fbaCartonMark " placeholder=" 请输入新的 FBA 箱唛 " /></td><td><el-input v-model=" row . remark " type=" textarea " :rows=" 2 " placeholder=" 请输入备注 " /></td><td><button type=" button " class=" warehouse - delete " title=" 删除该明细 " @click=" transferRelabelRows = transferRelabelRows . filter ( ( item ) => item . id !== row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" transferRelabelRows . length === 0 "><td colspan=" 7 "><div class=" warehouse - empty ">暂无绑定明细,请点击“添加 SKU 明细”</div></td></tr></tbody></table></div>
</div>
<div class=" warehouse - submit - bar warehouse - transfer - submit - bar "><el-button v-if=" mode === 'create' " native-type=" button " @click=" resetForm ">重置</el-button><el-button v-if=" props . service === 'transfer' " native-type=" button " :loading=" saving " @click=" submit ( 'draft' ) ">保存草稿</el-button><el-button v-if=" props . service === 'transfer' " type=" primary " native-type=" submit " :loading=" saving ">提交订单</el-button><el-button v-if=" props . service !== 'transfer' " type=" primary " native-type=" submit " :loading=" saving ">提交{{ config.title }}</el-button></div>
</form>
@@ -1766,6 +1832,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<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>
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . status !== 'DRAFT' " class=" warehouse - card warehouse - form - section ">
<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 " :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>
<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>
@@ -1788,7 +1862,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class=" warehouse - evidence warehouse - measurement - evidence "><span>测量打托现场照片</span><div v-if=" selectedDetail ? . measurementPhotos ? . length " class=" warehouse - photo - grid warehouse - measurement - photo - grid "><div v-for=" ( photo , index ) in selectedDetail . measurementPhotos " :key=" photo . fileId " class=" warehouse - photo - item warehouse - measurement - photo - item " :title=" photo . fileName || ` 测量照片 ${ index + 1 } ` "><el-image v-if=" measurementPhotoUrls [ photo . fileId ] " :src=" measurementPhotoUrls [ photo . fileId ] " :preview-src-list=" measurementPhotoPreviewUrls " :initial-index=" measurementPhotoPreviewUrls . indexOf ( measurementPhotoUrls [ photo . fileId ] ) " :alt=" photo . fileName || ` 测量照片 ${ index + 1 } ` " fit=" cover " preview-teleported /><div v-else-if=" measurementPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片 {{ index + 1 }} 加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadMeasurementPhoto ( photo . fileId ) ">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传测量打托照片</strong></div>
</div>
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - received - card ">
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button v-if=" selectedDetail ? . status !== 'DRAFT' " native-type=" button " type=" primary " size=" small " @click=" printTransferCartons ( String ( selectedDetail ? . transferNo || selectedDetail ? . [ '0' ] || '' ) ) "><Printer :size=" 14 " />打印箱唛</el-button><el-button type="primary " size=" small " :disabled=" ! countedTransferCartons ( ) . length " @click=" showTransferCartons = true ">查看箱号明细</el-button></div></div>
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button type=" primary " size=" small " :disabled=" ! countedTransferCartons ( ) . length " @click=" showTransferCartons = true ">查看箱号明细</el-button></div></div>
<div v-if=" ! countedTransferCartons ( ) . length " class=" warehouse - empty ">暂无实际签收箱,等待 PDA 清点。</div>
<el-dialog v-model=" showTransferCartons " title=" 签收箱号明细 " width=" 1120 px " align-center destroy-on-close append-to-body>
<div class=" warehouse - cartons - dialog - body ">
@@ -1811,7 +1885,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . needRelabel " class=" warehouse - card warehouse - form - section ">
<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 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 class=" warehouse - section - heading "><h3>换标完成与出库</h3></div>