@@ -1,7 +1,7 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { ArrowLeft , Eye , Plus , Search , Trash2 } from '@lucide/vue'
import { ArrowLeft , Download , Eye , Plus , Search , Trash2 , Upload } from '@lucide/vue'
import {
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer ,
listDiscardCandidates , listDiscards , listLastlegOrders , listRelabels , listTransfers ,
@@ -9,7 +9,7 @@ import {
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail'
interface CargoRow { id : number ; sku : string ; name : string ; quantity : number ; availableQuantity ? : number ; sourceOrderId ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string }
interface CargoRow { id : number ; sku : string ; name : string ; quantity : number ; availableQuantity ? : number ; sourceOrderId ? : string ; fnsku : string ; shipmentNo : string ; newSkuCode : string ; shippingMark : string ; cartonMark : string ; fbaCartonMark : string ; destinationWarehouseId : string ; remark : string }
interface ServiceConfig { title : string ; createTitle : string ; numberLabel : string ; emptyText : string ; columns : string [ ] }
const props = defineProps < { service : ServiceType } > ( )
@@ -27,7 +27,7 @@ const statusOptions = computed(() => props.service === 'labeling'
: [ { label : '待审核' , value : 'PENDING_REVIEW' } , { label : '已通过' , value : 'APPROVED' } , { label : '待分配' , value : 'PENDING_ASSIGNMENT' } , { label : '已分配' , value : 'ALLOCATED' } , { label : '已出库' , value : 'OUTBOUNDED' } , { label : '部分收货' , value : 'PARTIALLY_RECEIVED' } , { label : '已收货' , value : 'RECEIVED' } , { label : '已拒绝' , value : 'REJECTED' } ] )
const mode = ref < PageMode > ( 'list' )
const filters = reactive ( { number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' } )
const form = reactive ( { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 } )
const form = reactive ( { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 , chargeAmount : 0 } )
const cargoRows = ref < CargoRow [ ] > ( [ ] )
const submittedRows = ref < Record < string , string > [ ] > ( [ ] )
const selectedDetail = ref < Record < string , string > | null > ( null )
@@ -36,13 +36,14 @@ const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref ( false )
const loading = ref ( false )
const saving = ref ( false )
const templateInput = ref < HTMLInputElement | null > ( null )
let cargoSequence = 0
watch ( ( ) => props . service , ( ) => { mode . value = 'list' ; resetFilters ( ) ; resetForm ( ) ; void load ( ) } )
onMounted ( ( ) => { void load ( ) } )
function resetFilters ( ) { Object . assign ( filters , { number : '' , inboundNo : '' , warehouse : '' , destination : '' , status : '' , start : '' , end : '' } ) }
function resetForm ( ) {
Object . assign ( form , { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 } )
Object . assign ( form , { customerNo : '' , inboundNo : '' , sourceWarehouse : '' , warehouse : '' , destination : '' , address : '' , contact : '' , phone : '' , deliveryMethod : '' , trackingNo : '' , reason : '' , requirement : '' , relabel : false , remark : '' , labelDeadline : '' , estimatedArrivalAt : '' , fbaShipmentNo : '' , volumeCbm : 0.01 , chargeAmount : 0 } )
cargoRows . value = [ ]
discardCandidates . value = [ ]
selectedDiscardOrderId . value = ''
@@ -55,8 +56,47 @@ async function openCreate() {
catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '可废弃入库单加载失败' ) }
finally { discardCandidatesLoading . value = false }
}
function addCargoRow ( ) { cargoRows . value . push ( { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' } ) }
function addCargoRow ( ) { cargoRows . value . push ( { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } ) }
function removeCargoRow ( id : number ) { cargoRows . value = cargoRows . value . filter ( ( row ) => row . id !== id ) }
function csvCell ( value : string ) { return ` " ${ value . replaceAll ( '"' , '""' ) } " ` }
function downloadRelabelTemplate ( ) {
const header = [ '商品SKU(选填)' , '中文品名' , 'FBA货件编号' , '换标新SKU' , '我方箱唛' , 'FBA箱唛' , '目的仓' , '备注' ]
const example = [ '' , '示例商品' , 'FBA-SHIPMENT-001' , 'LX1000-NEW' , 'LX1000箱唛' , 'FBA-LX1000箱唛' , 'ONT8' , '' ]
const blob = new Blob ( [ ` \ uFEFF ${ header . map ( csvCell ) . join ( ',' ) } \ r \ n ${ example . map ( csvCell ) . join ( ',' ) } \ r \ n ` ] , { type : 'text/csv;charset=utf-8' } )
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url ; anchor . download = '贴标申请-关联面单模板.csv' ; anchor . click ( ) ; URL . revokeObjectURL ( url )
}
function parseCsvLine ( line : string ) {
const values : string [ ] = [ ] ; let value = '' ; let quoted = false
for ( let index = 0 ; index < line . length ; index += 1 ) {
const char = line [ index ]
if ( char === '"' ) { if ( quoted && line [ index + 1 ] === '"' ) { value += '"' ; index += 1 } else quoted = ! quoted }
else if ( char === ',' && ! quoted ) { values . push ( value . trim ( ) ) ; value = '' }
else value += char
}
values . push ( value . trim ( ) ) ; return values
}
async function importRelabelTemplate ( event : Event ) {
const input = event . target as HTMLInputElement ; const file = input . files ? . [ 0 ] ; input . value = ''
if ( ! file ) return
const lines = ( await file . text ( ) ) . replace ( /^\uFEFF/ , '' ) . split ( /\r?\n/ ) . filter ( ( line ) => line . trim ( ) )
if ( lines . length < 2 ) { ElMessage . error ( '模板中没有可导入的明细' ) ; return }
const headers = parseCsvLine ( lines [ 0 ] ) ; const required = [ '中文品名' , 'FBA货件编号' , '换标新SKU' , '我方箱唛' , 'FBA箱唛' ]
const skuHeader = headers . includes ( '商品SKU(选填)' ) ? '商品SKU(选填)' : '商品SKU'
if ( ! headers . includes ( skuHeader ) ) { ElMessage . error ( '模板格式不正确,请重新下载模板填写' ) ; return }
if ( required . some ( ( name ) => ! headers . includes ( name ) ) ) { ElMessage . error ( '模板格式不正确,请重新下载模板填写' ) ; return }
const imported = lines . slice ( 1 ) . map ( parseCsvLine ) . map ( ( values ) => Object . fromEntries ( headers . map ( ( key , index ) => [ key , values [ index ] || '' ] ) ) ) . filter ( ( item ) => required . some ( ( key ) => item [ key ] ) )
if ( ! imported . length || imported . some ( ( item ) => required . some ( ( key ) => ! item [ key ] ) ) ) { ElMessage . error ( '模板中的商品、货件、换标 SKU 和箱唛信息必须填写完整' ) ; return }
if ( new Set ( imported . map ( ( item ) => item [ '我方箱唛' ] ) ) . size !== imported . length ) { ElMessage . error ( '模板中的我方箱唛不能重复' ) ; return }
cargoRows . value = imported . map ( ( item ) => ( {
id : ++ cargoSequence , sku : item [ skuHeader ] , name : item [ '中文品名' ] , quantity : 1 , fnsku : '' ,
shipmentNo : item [ 'FBA货件编号' ] , newSkuCode : item [ '换标新SKU' ] , shippingMark : '' ,
cartonMark : item [ '我方箱唛' ] , fbaCartonMark : item [ 'FBA箱唛' ] ,
destinationWarehouseId : item [ '目的仓' ] || '' , remark : item [ '备注' ] || '' ,
} ) )
ElMessage . success ( ` 已导入 ${ cargoRows . value . length } 箱货物明细 ` )
}
function selectDiscardInbound ( orderId : string ) {
const candidate = discardCandidates . value . find ( ( item ) => item . orderId === orderId )
if ( ! candidate ) { cargoRows . value = [ ] ; return }
@@ -65,17 +105,25 @@ function selectDiscardInbound(orderId: string) {
cargoRows . value = candidate . skus . map ( ( sku ) => ( {
id : ++ cargoSequence , sku : sku . skuCode , name : sku . productName , quantity : 0 ,
availableQuantity : sku . availablePackages , sourceOrderId : candidate . orderId ,
fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' ,
fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' ,
} ) )
}
function dateTime ( value : string , end = false ) { return value ? ` ${ value } T ${ end ? '23:59:59' : '00:00:00' } ` : undefined }
function row ( values : Array < string | number | undefined | null > ) { return Object . fromEntries ( values . map ( ( value , index ) => [ String ( index ) , value == null || value === '' ? '—' : String ( value ) ] ) ) }
function warehouseDisplay ( value ? : string ) {
if ( ! value ) return '—'
const labels : Record < string , string > = { ONT8 : '美西海外仓-ONT8' , ABE8 : '美东海外仓-ABE8' , MDW2 : '美中海外仓-MDW2' }
return value . split ( ',' ) . map ( ( warehouse ) => labels [ warehouse ] || warehouse ) . join ( '、' )
}
async function load ( ) {
loading . value = true
try {
if ( props . service === 'labeling' ) {
const result = await listRelabels ( { page : 1 , size : 20 , relabelNo : filters . number || undefined , overseasWarehouseId : filters . warehouse || undefined , status : filters . status || undefined , createdFrom : dateTime ( filters . start ) , createdTo : dateTime ( filters . end , true ) } )
submittedRows . value = result . records . map ( ( item ) => row ( [ item . relabelNo , item . customerOrderNo , '—' , '—' , item . itemCount , item . totalPackageCount , item . status , item . createdAt ] ) )
submittedRows . value = result . records . map ( ( item ) => {
const destinationWarehouses = warehouseDisplay ( item . overseasWarehouseId )
return row ( [ item . relabelNo , item . customerOrderNo , destinationWarehouses , '—' , item . itemCount , item . totalPackageCount , item . status , item . createdAt ] )
} )
} else if ( props . service === 'disposal' ) {
const result = await listDiscards ( { page : 1 , size : 20 , discardNo : filters . number || undefined , inboundCode : filters . inboundNo || undefined , overseasWarehouseId : filters . warehouse || undefined , status : filters . status || undefined , createdFrom : dateTime ( filters . start ) , createdTo : dateTime ( filters . end , true ) } )
submittedRows . value = result . records . map ( ( item ) => row ( [ item . discardNo , item . inboundCodes , item . overseasWarehouseName || '—' , '—' , item . totalPackages , ` ${ item . currency } ${ item . totalFee } ` , item . status , item . createdAt ] ) )
@@ -94,12 +142,18 @@ async function submit() {
const invalidCargo = props . service === 'disposal'
? ! selectedDiscardOrderId . value || cargoRows . value . every ( ( item ) => item . quantity < 1 )
|| cargoRows . value . some ( ( item ) => item . quantity < 0 || item . quantity > Number ( item . availableQuantity || 0 ) )
: cargoRows . value . length === 0 || cargoRows . value . some ( ( item ) => ! item . sku || ! item . name || item . quantity < 1 )
if ( ( ! form . customerNo && ! form . inboundNo ) || ! primaryWarehouse || invalidCargo ) {
: cargoRows . value . length === 0 || cargoRows . value . some ( ( item ) => ( props . service !== 'labeling' && ! item . sku ) || ! item . name || item . quantity < 1 )
if ( ( ! form . customerNo && ! form . inboundNo ) || ( props . service !== 'labeling' && ! primaryWarehouse ) || invalidCargo ) {
ElMessage . warning ( '请填写必填信息和完整的货物明细' ) ; return
}
if ( props . service === 'labeling' && ( ! form . labelDeadline || ! form . contact || ! form . phone || cargoRows . value . some ( ( item ) => ! item . shipmentNo || ! item . newSkuCode ) ) ) {
ElMessage . warning ( '请填写贴标截止时间、联系人以及货物的货件编号和新 SKU' ) ; return
if ( props . service === 'labeling' && ( ! form . labelDeadline || ! form . contact || ! form . phone || form . chargeAmount <= 0 || cargoRows . value . some ( ( item ) => ! item . shipmentNo || ! item . newSkuCode ) ) ) {
ElMessage . warning ( '请填写贴标截止时间、联系人、贴标费用 以及货物的货件编号和新 SKU' ) ; return
}
if ( props . service === 'labeling' && cargoRows . value . some ( ( item ) => ! item . cartonMark || ! item . fbaCartonMark ) ) {
ElMessage . warning ( '请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整' ) ; return
}
if ( props . service === 'labeling' && cargoRows . value . some ( ( item ) => ! item . destinationWarehouseId && ! form . warehouse ) ) {
ElMessage . warning ( '请填写统一目的海外仓,或为每箱货物填写目的仓' ) ; return
}
if ( props . service === 'disposal' && ! form . reason ) {
ElMessage . warning ( '请选择废弃原因' ) ; return
@@ -112,7 +166,7 @@ async function submit() {
try {
const idempotencyKey = crypto . randomUUID ( )
if ( props . service === 'labeling' ) {
await createRelabel ( { customerOrderNo : form . customerNo , overseasWarehouseId : form . warehouse , labelDeadline : form . labelDeadline , contactName : form . contact , contactPhone : form . phone , remark : form . remark || undefined , idempotencyKey , items : cargoRows . value . map ( ( item ) => ( { skuCode : item . sku , productNameZh : item . name , totalPackageCount : item . quantity , notifiedPackageCount : item . quantity , shipmentNo : item . shipmentNo , trackingCode : form . trackingNo || undefined , newSkuCode : item . newSkuCode , labelRequirement : form . requirement || undefined } ) ) } )
await createRelabel ( { customerOrderNo : form . customerNo , overseasWarehouseId : form . warehouse || undefined , labelDeadline : form . labelDeadline , contactName : form . contact , contactPhone : form . phone , chargeAmount : form . chargeAmount , remark : form . remark || undefined , idempotencyKey , items : cargoRows . value . map ( ( item ) => ( { skuCode : item . sku , productNameZh : item . name , totalPackageCount : 1 , notifiedPackageCount : 1 , shipmentNo : item . shipmentNo , trackingCode : form . trackingNo || undefined , newSkuCode : item . newSkuCode , labelRequirement : form . requirement || undefined , cartonMark : item . cartonMark , fbaCartonMark : item . fbaCartonMark , destinationWarehouseId : item . destinationWarehouseId || undefined , remark : item . remark || undefined } ) ) } )
} else if ( props . service === 'disposal' ) {
const items = cargoRows . value . filter ( ( item ) => item . quantity > 0 ) . map ( ( item ) => {
if ( ! item . sourceOrderId || item . sourceOrderId !== selectedDiscardOrderId . value || item . quantity > Number ( item . availableQuantity || 0 ) ) {
@@ -173,7 +227,7 @@ async function openDetail(value: Record<string, string>) {
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>进仓编号</span><el-input v-model="form.inboundNo" placeholder="请输入已有入仓编号" / > < / label >
< label v-if = "props.service === 'relabel'"><span><i>*</i>来源 FBA 仓库</span><el-select v-model="form.sourceWarehouse" placeholder="请选择"><el-option label="ONT8" value="ONT8" /><el-option label="ABE8" value="ABE8" /><el-option label="CLT2" value="CLT2" / > < / el-select > < / label >
< label v-else-if = "props.service === 'disposal'"><span>进仓编号</span><el-input v-model="form.inboundNo" readonly / > < / label >
< label v-else > < span > < i > * < / i > { { props . service === 'labeling' ? '目的海外仓' : '操作仓' } } < / span > < el-select v-model = "form.warehouse" placeholder="请选择 "><el-option label="美西海外仓-ONT8" value="ONT8" /><el-option label="美东海外仓-ABE8" value="ABE8" /><el-option label="美中海外仓-MDW2" value="MDW2" / > < / el-select > < / label >
< label v-else > < span > < i v-if = "props.service !== 'labeling'">*</i>{{ props.service === 'labeling' ? '统一目的海外仓' : '操作仓' }}</span><el-select v-model= "form.warehouse" clearable placeholder="留空则按货物明细目的仓 "><el-option label="美西海外仓-ONT8" value="ONT8" /><el-option label="美东海外仓-ABE8" value="ABE8" /><el-option label="美中海外仓-MDW2" value="MDW2" /></el-select><small v-if="props.service === 'labeling'" class="warehouse-hint" > 填写后 , 未单独指定目的仓的货物将使用此仓库 。 < / small > < / label >
< label v-if = "props.service === 'disposal'"><span>操作仓</span><el-input :model-value="discardCandidates.find((item) => item.orderId === selectedDiscardOrderId)?.overseasWarehouseName || form.warehouse" readonly / > < / label >
< label v-if = "props.service === 'relabel' || props.service === 'transfer'"><span><i>*</i>目的 FBA 仓</span><el-input v-model="form.destination" placeholder="请输入仓代码" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>派送方式</span><el-select v-model="form.deliveryMethod" placeholder="请选择"><el-option label="卡车派送" value="卡车派送" /><el-option label="快递派送" value="快递派送" / > < / el-select > < / label >
@@ -181,6 +235,7 @@ async function openDetail(value: Record<string, string>) {
< label v-if = "props.service === 'labeling'"><span><i>*</i>联系人</span><el-input v-model="form.contact" placeholder="请输入联系人" / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>联系电话</span><el-input v-model="form.phone" placeholder="请输入联系电话" / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>贴标截止时间</span><el-date-picker v-model="form.labelDeadline" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择截止时间" / > < / label >
< label v-if = "props.service === 'labeling'"><span><i>*</i>贴标费用(CNY) </span><el-input-number v-model="form.chargeAmount" :min="0" :precision="2" controls-position="right" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'" class="warehouse-span-2"><span><i>*</i>详细派送地址</span><el-input v-model="form.address" placeholder="请输入派送地址" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>总体积(CBM) </span><el-input-number v-model="form.volumeCbm" :min="0.0001" :precision="4" controls-position="right" / > < / label >
< label v-if = "props.service === 'transfer' || props.service === 'relabel'"><span><i>*</i>预计到达时间</span><el-date-picker v-model="form.estimatedArrivalAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="请选择预计到达时间" / > < / label >
@@ -190,10 +245,10 @@ async function openDetail(value: Record<string, string>) {
< label class = "warehouse-span-2" > < span > 订单备注 < / span > < el-input v-model = "form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" / > < / label >
< / div >
< / div >
< div class = "warehouse-card warehouse-form-section" > < div class = "warehouse-section-heading" > < h3 > 货物明细 < / h3 > < div v-if = "props.service !== 'disposal'"><el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []" > 清空 < / el -button > < / div > < / div >
< div class = "warehouse-table-scroll" > < table class = "warehouse-edit-table" > < thead > < tr > < th > 商品 SKU * < / th > < th > 中文品名 * < / th > < th v-if = "props.service === 'disposal'">可用箱数</th><th v-else>{{ props.service === 'labeling' ? '货件编号 *' : 'FNSKU / 箱唛' }}</th><th v-if="props.service === 'labeling'">换标新 SKU *</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : props.service === 'relabel' ? '退件数量' : '箱数' }} *</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'" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-else><el-input v-if="props.service === 'labeling'" v-model="row.shipmentNo" /><el-input v-else v-model="row.shippingMark" /></td><td v-if="props.service === 'labeling'"><el-input v-model="row.newSkuCode" /></td><td><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="props.service === 'disposal' ? row.availableQuantity : undefined" controls-position="right" /></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 === 'labeling' ? 6 : 5"><div class="warehouse-empty" > {{ props.service = = = ' disposal ' ? ' 请先选择入库单 , 系统将自动带出 SKU 和可用箱数 ' : ' 暂无货物明细 , 请点击 “ 添加行 ” ' }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< div class = "warehouse-card warehouse-form-section" > < div class = "warehouse-section-heading" > < h3 > 货物明细 < / h3 > < div v-if = "props.service !== 'disposal'"><template v-if="props.service === 'labeling'"><el-button size="small" @click="downloadRelabelTemplate"><Download :size="15" />下载关联模板</el-button><el-button size="small" @click="templateInput?.click()"><Upload :size="15" />上传关联面单</el-button><input ref="templateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate"></template>< el-button type="primary" size="small" @click="addCargoRow"><Plus :size="15" />添加行</el-button><el-button size="small" @click="cargoRows = []" > 清空 < / el -button > < / div > < / div >
< p v-if = "props.service === 'labeling'" class="warehouse-attachment-tip" > 模板每行代表一箱 ; 上传后直接回显到下方明细 。 明细目的仓优先 , 留空时使用顶部统一目的海外仓 。 < / p >
< div class = "warehouse-table-scroll" > < table class = "warehouse-edit-table" : class = "{ 'warehouse-label-table': props.service === 'labeling' }" > < thead > < tr > < th > 商品 SKU < span v-if = "props.service !== 'labeling'"> *</span></th><th>中文品名 *</th><th v-if="props.service === 'disposal'">可用箱数</th><th v-else>{{ props.service === 'labeling' ? 'FBA 货件编号 *' : 'FNSKU / 箱唛' }}</th><th v-if="props.service === 'labeling'">换标新 SKU *</th><th v-if="props.service !== 'labeling'">{{ props.service === 'disposal' ? '本次废弃箱数' : props.service === 'relabel' ? '退件数量' : '箱数' }} *</th><template v-if="props.service === 'labeling'"><th>我方箱唛</th><th>FBA 箱唛</th><th>目的仓</th><th>备注</th></template><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="选填" /></td><td><el-input v-model="row.name" :readonly="props.service === 'disposal'" /></td><td v-if="props.service === 'disposal'"><strong>{{ row.availableQuantity }} 箱</strong></td><td v-else><el-input v-if="props.service === 'labeling'" v-model="row.shipmentNo" /><el-input v-else v-model="row.shippingMark" /></td><td v-if="props.service === 'labeling'"><el-input v-model="row.newSkuCode" /></td><td v-if="props.service !== 'labeling'"><el-input-number v-model="row.quantity" :min="props.service === 'disposal' ? 0 : 1" :max="props.service === 'disposal' ? row.availableQuantity : undefined" controls-position="right" /></td><template v-if="props.service === 'labeling'"><td><el-input v-model="row.cartonMark" placeholder="请输入我方箱唛" /></td><td><el-input v-model="row.fbaCartonMark" placeholder="请输入 FBA 箱唛" /></td><td><el-select v-model="row.destinationWarehouseId" clearable placeholder="使用顶部统一目的仓"><el-option label="美西海外仓-ONT8" value="ONT8" /><el-option label="美东海外仓-ABE8" value="ABE8" /><el-option label="美中海外仓-MDW2" value="MDW2" /></el-select></td><td><el-input v-model="row.remark" type="textarea" :rows="2" maxlength="1000" show-word-limit placeholder="请输入明细备注" /></td></template><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 === 'labeling' ? 8 : 5"><div class="warehouse-empty" > {{ props.service = = = ' disposal ' ? ' 请先选择入库单 , 系统将自动带出 SKU 和可用箱数 ' : ' 暂无货物明细 , 请点击 “ 添加行 ” ' }} < / div > < / td > < / tr > < / tbody > < / table > < / div >
< / div >
< div v-if = "props.service === 'transfer' || props.service === 'relabel'" class="warehouse-card warehouse-form-section"><h3>换标设置</h3><el-checkbox v-model="form.relabel">是否换标</el-checkbox><p v-if="form.relabel" class="warehouse-hint" > 请确认 SKU 与 FNSKU 信息准确 , 提交后海外仓将按明细执行换标 。 < / p > < / div >
< div class = "warehouse-submit-bar" > < el-button @click ="resetForm" > 重置 < / el -button > < el-button type = "primary" native -type = " submit " :loading = "saving" > 提交 { { config . title } } < / el-button > < / div >
< / form >
< / template >
@@ -204,4 +259,5 @@ async function openDetail(value: Record<string, string>) {
< style scoped >
. warehouse - service - page { color : # 25324 a } . warehouse - breadcrumb { display : flex ; align - items : center ; gap : 9 px ; margin : 0 0 14 px ; color : # 9 aa3b2 ; font - size : 12 px } . warehouse - breadcrumb b { font - weight : 400 } . warehouse - breadcrumb strong { color : # 606 b7c } . warehouse - breadcrumb button { padding : 0 ; border : 0 ; background : transparent ; color : # 4376 b8 ; cursor : pointer } . warehouse - page - heading { display : flex ; align - items : center ; justify - content : space - between ; margin - bottom : 16 px } . warehouse - page - heading h2 { margin : 0 ; color : # 17233 a ; font - size : 22 px ; letter - spacing : 0 } . warehouse - card { margin - bottom : 12 px ; border : 1 px solid # e7ebf1 ; border - radius : 4 px ; background : # fff ; box - shadow : 0 1 px 3 px rgba ( 25 , 42 , 70 , .04 ) } . warehouse - filter - card { padding : 20 px 22 px 16 px } . warehouse - filter - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 180 px , 1 fr ) ) ; gap : 16 px 20 px } . warehouse - filter - grid label , . warehouse - form - grid label { display : flex ; min - width : 0 ; flex - direction : column ; gap : 7 px } . warehouse - filter - grid label > span , . warehouse - form - grid label > span { color : # 596579 ; font - size : 12 px } . warehouse - filter - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; margin - top : 18 px } . warehouse - table - card { overflow : hidden } . warehouse - table - scroll { overflow - x : auto } table { width : 100 % ; min - width : 940 px ; border - collapse : collapse ; font - size : 12 px } th { height : 44 px ; padding : 0 14 px ; border - bottom : 1 px solid # e8edf3 ; background : # f7f9fc ; color : # 4 d5a6e ; font - weight : 650 ; text - align : left ; white - space : nowrap } td { height : 48 px ; padding : 8 px 14 px ; border - bottom : 1 px solid # eef1f5 ; color : # 526176 ; white - space : nowrap } . warehouse - empty { display : grid ; height : 150 px ; color : # a0a9b7 ; place - items : center } . warehouse - link , . warehouse - delete { display : inline - flex ; align - items : center ; gap : 5 px ; padding : 4 px ; border : 0 ; background : transparent ; color : # 2176 d2 ; cursor : pointer } . warehouse - delete { color : # d64545 } . warehouse - pagination { display : flex ; align - items : center ; justify - content : flex - end ; gap : 8 px ; padding : 14 px 18 px ; color : # 778398 ; font - size : 12 px } . warehouse - pagination span { margin - right : 8 px } . warehouse - page - number { width : 28 px ; height : 28 px ; border : 1 px solid # 2176 d2 ; border - radius : 3 px ; background : # 2176 d2 ; color : # fff } . warehouse - form - section { padding : 22 px } . warehouse - form - section h3 { margin : 0 0 18 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f4 ; color : # 26364 f ; font - size : 15 px ; letter - spacing : 0 } . warehouse - form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 240 px , 1 fr ) ) ; gap : 18 px 28 px } . warehouse - form - grid i { margin - right : 3 px ; color : # e24b4b ; font - style : normal } . warehouse - span - 2 { grid - column : 1 / - 1 } . warehouse - section - heading { display : flex ; align - items : center ; justify - content : space - between } . warehouse - section - heading h3 { flex : 1 } . warehouse - edit - table td { padding : 9 px 10 px } . warehouse - edit - table : deep ( . el - input - number ) { width : 130 px } . warehouse - hint { margin : 12 px 0 0 ; color : # 8792 a2 ; font - size : 12 px } . warehouse - submit - bar { display : flex ; justify - content : center ; gap : 10 px ; padding : 12 px 0 30 px } . warehouse - detail - grid { display : grid ; grid - template - columns : repeat ( 3 , 1 fr ) ; gap : 24 px } . warehouse - detail - grid div { display : flex ; flex - direction : column ; gap : 7 px } . warehouse - detail - grid span { color : # 8 a95a5 ; font - size : 12 px } . warehouse - detail - grid strong { color : # 334157 ; font - size : 13 px } : deep ( . el - button ) { border - radius : 4 px } : deep ( . el - button span ) { display : inline - flex ; align - items : center ; gap : 6 px } : deep ( . el - select ) , : deep ( . el - date - editor . el - input ) { width : 100 % } @ media ( max - width : 1100 px ) { . warehouse - filter - grid { grid - template - columns : repeat ( 2 , 1 fr ) } . warehouse - detail - grid { grid - template - columns : repeat ( 2 , 1 fr ) } }
. warehouse - hidden - input { display : none } . warehouse - label - table { min - width : 1320 px } . warehouse - attachment - tip { margin : 10 px 0 12 px ; color : # 7 d8797 ; font - size : 12 px } . warehouse - mark - mapping { display : flex ; flex - wrap : wrap ; gap : 8 px ; margin : 4 px 0 8 px ; padding : 12 px ; border : 1 px solid # dce7f5 ; border - radius : 5 px ; background : # f7faff } . warehouse - mark - mapping strong { width : 100 % ; color : # 34445 c } . warehouse - mark - mapping span { padding : 5 px 9 px ; border - radius : 3 px ; background : # eaf2fc ; color : # 3 e6593 ; font - size : 12 px }
< / style >