@@ -7,6 +7,7 @@ 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 { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import {
@@ -35,7 +36,7 @@ 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 [ ] }
@@ -588,6 +589,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 ) {
@@ -1156,7 +1158,18 @@ watch([page, pageSize], ([nextPage, nextSize], [previousPage, previousSize]) =>
if ( nextSize !== previousSize && nextPage !== 1 ) { page . value = 1 ; return }
void load ( )
} )
async function submit ( action : 'draft' | 'submit' | 'print' = 'submit' ) {
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
const primaryWarehouse = props . service === 'relabel' ? form . sourceWarehouse : form . warehouse
const relabelRows = labelingMode . value === 'carton' ? cartonDetailRows . value : skuRelabelRows . value
@@ -1236,9 +1249,6 @@ async function submit(action: 'draft' | 'submit' | 'print' = 'submit') {
if ( props . service !== 'transfer' || action === 'submit' ) {
try { await ElMessageBox . confirm ( ` 确认提交 ${ config . value . title } 吗? ` , '提交确认' , { confirmButtonText : '提交' , cancelButtonText : '取消' } ) } catch { return }
}
const draftPrintWindow = action === 'print' ? window . open ( '' , '_blank' ) : null
if ( action === 'print' && ! draftPrintWindow ) { ElMessage . warning ( '请允许浏览器打开打印窗口' ) ; return }
if ( draftPrintWindow ) draftPrintWindow . opener = null
saving . value = true
try {
const idempotencyKey = createIdempotencyKey ( )
@@ -1282,7 +1292,7 @@ async function submit(action: 'draft' | 'submit' | 'print' = '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
@@ -1292,17 +1302,6 @@ async function submit(action: 'draft' | 'submit' | 'print' = 'submit') {
: await createTransfer ( payload )
const transferNo = String ( created . transferNo || '' )
if ( ! transferNo ) throw new Error ( '转运草稿已保存,但未返回转运单号' )
if ( action === 'print' ) {
// 保存服务端草稿后打印同一套箱唛,后续编辑及正式提交保留原号。
editingTransferNo . value = transferNo
const label = await getTransferCartonLabel ( transferNo )
if ( draftPrintWindow ) {
draftPrintWindow . document . open ( ) ; draftPrintWindow . document . write ( label . printableHtml ) ; draftPrintWindow . document . close ( )
draftPrintWindow . focus ( ) ; draftPrintWindow . print ( )
}
ElMessage . success ( '草稿已保存,正在打印箱唛;请继续填写并提交订单' )
return
}
if ( action === 'submit' ) {
const submitted = await submitTransfer ( transferNo )
if ( form . relabel ) {
@@ -1329,7 +1328,7 @@ async function submit(action: 'draft' | 'submit' | 'print' = 'submit') {
}
ElMessage . success ( props . service === 'transfer' ? '转运单草稿保存成功' : ` ${ config . value . title } 提交成功 ` )
await goToServiceList ( )
} catch ( error ) { draftPrintWindow ? . close ( ) ; ElMessage . error ( error instanceof Error ? error . message : '提交失败' ) }
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '提交失败' ) }
finally { saving . value = false }
}
async function openDetail ( value : Record < string , string > ) {
@@ -1807,7 +1806,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
</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 v-if=" props . service === 'transfer' " size=" small " :loading=" saving " @click=" submit ( 'print' ) "><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 - 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>