@@ -1,6 +1,6 @@
< script setup lang = "ts" >
import '@/styles/adminDensity.css'
import { computed , onMounted , ref } from 'vue'
import { computed , onMounted , ref , watch } from 'vue'
import { Plus , Search } from '@lucide/vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
@@ -38,6 +38,7 @@ const returnStockRows = ref<ReturnStockRow[]>([])
const returnCartonRows = ref < ReturnCartonRow [ ] > ( [ ] )
const selectedReturnSkus = ref < string [ ] > ( [ ] )
const returnPickMode = ref < 'BOX' | 'SKU' > ( 'BOX' )
const returnQuantity = ref < number | undefined > ( )
const returnSkuQueryInput = ref ( '' ) ; const returnSkuQuery = ref ( '' )
const returnCartonQueryInput = ref ( '' ) ; const returnCartonQuery = ref ( '' )
const returnSkuPage = ref ( 1 ) ; const returnCartonPage = ref ( 1 ) ; const returnUnitType = ref < 'BOX' | 'PALLET' > ( 'BOX' )
@@ -64,7 +65,15 @@ function transferCustomerName(row: WarehouseTransferOutboundCandidate) {
const customer = tenantOptions . value . find ( item => String ( item . id ) === String ( row . customerId ) )
return customer ? . legalName ? . trim ( ) || customer ? . tenantName ? . trim ( ) || ` 客户 ${ row . customerId } `
}
const selectedReturnStock = computed ( ( ) => returnStockRows . value . filter ( ( item ) => Number ( returnPickMode . value === 'SKU' ? item . looseAvailableQuantity || 0 : item . availableQuantity || 0 ) > 0 ) )
const selectedReturnStock = computed ( ( ) => returnStockRows . value . map ( ( item ) => {
if ( returnPickMode . value !== 'SKU' ) return item
const quantity = selectedReturnCartons . value . reduce ( ( sum , carton ) => {
if ( selectedReturnNo . value && ! ( carton . sourceReturnNo || '' ) . split ( ',' ) . includes ( selectedReturnNo . value ) ) return sum
const items = returnCartonDetails . value [ carton . cartonMark ] ? . items
return items ? . length === 1 && items [ 0 ] ? . skuCode === item . skuCode ? sum + Number ( items [ 0 ] . availableQuantity ? ? items [ 0 ] . quantity ) : sum
} , 0 )
return { ... item , availableQuantity : quantity }
} ) . filter ( item => Number ( item . availableQuantity || 0 ) > 0 ) )
const selectedReturnCartons = computed ( ( ) => returnCartonRows . value . filter ( ( item ) => item . status === 'AVAILABLE' ) )
const returnOrderOptions = computed ( ( ) => [ ... new Set ( [
... returnCandidates . value . flatMap ( item => ( item . returnNo || '' ) . split ( ',' ) ) ,
@@ -76,11 +85,11 @@ const queryTokens = (value: string) => value.split(/[,, \n]+/).map((item) => it
const matchesAnyToken = ( value : string , query : string ) => { const tokens = queryTokens ( query ) ; const normalized = value . replace ( /\s+/g , '' ) . toLowerCase ( ) ; return ! tokens . length || tokens . some ( ( token ) => normalized . includes ( token ) ) }
const filteredReturnStocks = computed ( ( ) => selectedReturnStock . value . filter ( ( item ) => matchesAnyToken ( ` ${ item . skuCode } , ${ item . productNameZh || '' } ` , returnSkuQuery . value ) ) )
const filteredReturnCartons = computed ( ( ) => selectedReturnCartons . value . filter ( ( item ) => {
if ( returnPickMode . value === 'BOX' && item . sourceUnitType === 'PARCEL' ) return false
if ( selectedReturnNo . value && ! ( item . sourceReturnNo || returnCandidateByMark . value [ item . cartonMark ] ? . returnNo || '' ) . split ( ',' ) . includes ( selectedReturnNo . value ) ) return false
if ( returnPickMode . value === 'SKU' ) {
const detail = returnCartonDetails . value [ item . cartonMark ]
if ( ! selectedReturnSkus . value . length || item . sourceUnitType !== 'PARCEL' || ! returnCandidateByMark . value [ item . cartonMark ]
|| detail ? . items ? . length !== 1 || detail . items [ 0 ] ? . skuCode !== selectedReturnSkus . value [ 0 ] ) return false
if ( ! selectedReturnSkus . value . length || detail ? . items ? . length !== 1 || detail . items [ 0 ] ? . skuCode !== selectedReturnSkus . value [ 0 ] ) return false
}
const detailSkus = returnCartonDetails . value [ item . cartonMark ] ? . items ? . map ( ( detail ) => detail . skuCode ) . join ( ',' ) || ''
return matchesAnyToken ( ` ${ item . cartonMark } , ${ item . fbaCartonMark || '' } , ${ detailSkus } ` , returnCartonQuery . value )
@@ -88,6 +97,11 @@ const filteredReturnCartons = computed(() => selectedReturnCartons.value.filter(
const pagedReturnStocks = computed ( ( ) => filteredReturnStocks . value . slice ( ( returnSkuPage . value - 1 ) * returnSkuPageSize . value , returnSkuPage . value * returnSkuPageSize . value ) )
const pagedReturnCartons = computed ( ( ) => filteredReturnCartons . value . slice ( ( returnCartonPage . value - 1 ) * returnCartonPageSize . value , returnCartonPage . value * returnCartonPageSize . value ) )
const effectiveReturnMarks = computed ( ( ) => [ ... new Set ( selectedReturnMarks . value ) ] . filter ( Boolean ) as string [ ] )
const selectedReturnQuantity = computed ( ( ) => effectiveReturnMarks . value . reduce ( ( sum , mark ) => {
const line = returnCartonDetails . value [ mark ] ? . items . find ( item => item . skuCode === selectedReturnSkus . value [ 0 ] )
return sum + Number ( line ? . availableQuantity ? ? line ? . quantity ? ? 0 )
} , 0 ) )
watch ( selectedReturnQuantity , quantity => { returnQuantity . value = quantity || undefined } )
const returnMarksModel = computed ( { get : ( ) => selectedReturnMarks . value , set : ( value : string [ ] ) => { selectedReturnMarks . value = value } } )
const filteredDropshipCandidates = computed ( ( ) => dropshipCandidates . value . filter ( item => ! dropshipQuery . value . trim ( ) || [ item . orderNo , item . inboundCode , item . customerName , item . palletNo , item . skuCode , item . productName ] . join ( ' ' ) . toLowerCase ( ) . includes ( dropshipQuery . value . trim ( ) . toLowerCase ( ) ) ) )
const pagedDropshipCandidates = computed ( ( ) => filteredDropshipCandidates . value . slice ( ( dropshipPage . value - 1 ) * dropshipPageSize . value , dropshipPage . value * dropshipPageSize . value ) )
@@ -203,13 +217,16 @@ async function submit() {
if ( ! recipient . value . trim ( ) ) { ElMessage . warning ( '请填写收件信息' ) ; return }
if ( ! selectedReturnNo . value ) { ElMessage . warning ( '请选择退货单' ) ; return }
if ( returnPickMode . value === 'SKU' && selectedReturnSkus . value . length !== 1 ) { ElMessage . warning ( '请选择一个 SKU' ) ; return }
if ( returnPickMode . value === 'SKU' && ( ! Number . isSafeInteger ( returnQuantity . value ) || Number ( returnQuantity . value ) <= 0 || Number ( returnQuantity . value ) > selectedReturnQuantity . value ) ) {
ElMessage . warning ( ` 请填写 1- ${ selectedReturnQuantity . value } 件的出库数量 ` ) ; return
}
if ( ! selectedReturnMarks . value . length ) {
ElMessage . warning ( returnPickMode . value === 'SKU'
? filteredReturnCartons . value . length ? '请在右侧选择该 SKU 的散件 来源记录' : '该退货单没有此 SKU 可出库的散件库存;已装箱货物请切换箱/托出库 '
? filteredReturnCartons . value . length ? '请在右侧选择该 SKU 的来源箱或散件 记录' : '该退货单没有此 SKU 的可用单 SKU 箱或散件库存;混合箱请先拆箱 '
: '请选择出库箱号或托盘' )
return
}
await createReturnOutbound ( selectedCustomerId . value , { recipient : recipient . value . trim ( ) , overseasWarehouseId : selectedWarehouseId . value , returnNo : selectedReturnNo . value , cartonMarks : selectedReturnMarks . value , skuCode : returnPickMode . value === 'SKU' ? selectedReturnSkus . value [ 0 ] : undefined , unitType : returnPickMode . value === 'SKU' ? 'BOX' : returnUnitType . value , remark : remark . value . trim ( ) || undefined , idempotencyKey : ` web-return- ${ Date . now ( ) } ` } )
await createReturnOutbound ( selectedCustomerId . value , { recipient : recipient . value . trim ( ) , overseasWarehouseId : selectedWarehouseId . value , returnNo : selectedReturnNo . value , cartonMarks : selectedReturnMarks . value , skuCode : returnPickMode . value === 'SKU' ? selectedReturnSkus . value [ 0 ] : undefined , quantity : returnPickMode . value === 'SKU' ? returnQuantity . value : undefined , unitType : returnPickMode . value === 'SKU' ? 'BOX' : returnUnitType . value , remark : remark . value . trim ( ) || undefined , idempotencyKey : ` web-return- ${ Date . now ( ) } ` } )
ElMessage . success ( '退货出库单和待派送单已创建,作业已推送 PDA' )
} else {
const selected = dropshipCandidates . value . filter ( item => selectedDropshipIds . value . includes ( dropshipCandidateKey ( item ) ) )
@@ -329,21 +346,21 @@ onMounted(load)
< el -form -item label = "收件信息" required > < el-input v-model = "recipient" placeholder="请输入收件人、电话和地址" / > < / el-form-item >
< el-form-item label = "海外仓" required > < el-select v-model = "selectedWarehouseId" :disabled="!selectedCustomerId" filterable placeholder="请选择海外仓" @change="loadReturnCandidates"><el-option v-for="item in warehouseOptions" :key="item.value" :value="item.value" :label="item.label" / > < / el-select > < / el-form-item >
< el-form-item label = "退货单" required > < el-select v-model = "selectedReturnNo" filterable placeholder="选择已签收入库的退货单" @change="selectedReturnMarks = []"><el-option v-for="no in returnOrderOptions" :key="no" :label="no" :value="no" / > < / el-select > < / el-form-item >
< el-form-item label = "出库维度" > < el-radio-group v-model = "returnPickMode" @change="changeReturnPickMode"><el-radio-button value="BOX">箱 / 托</el-radio-button><el-radio-button value="SKU" > 单 SKU < / el -radio -button > < / el-radio-group > < / el-form-item >
< p v-if = "returnPickMode === 'SKU'" class="candidate-hint">单 SKU 模式可填写全部或部分件数;已装箱货物由 PDA 扫箱唛按计划件数拣货,剩余数量保留在原箱。</p><el-form-item v-if="returnPickMode === 'SKU'" label="出库数量(件)" required><el-input-number v-model="returnQuantity" :min="1" :max="selectedReturnQuantity || 1" :precision="0" placeholder="选择来源后填写件数" /><span class="candidate-hint">所选来源可用 {{ selectedReturnQuantity }} 件;按来源箱号顺序分配</span></el-form-item><el-form-item label="出库维度"><el-radio-group v-model= "returnPickMode" @change="changeReturnPickMode"><el-radio-button value="BOX">箱 / 托</el-radio-button><el-radio-button value="SKU" > 单 SKU < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item v-if = "returnPickMode === 'BOX'" label="箱托方式"><el-radio-group v-model="returnUnitType" @change="changeReturnUnitType"><el-radio-button value="BOX">按箱号出库</el-radio-button><el-radio-button value="PALLET" > 按托盘出库 < / el -radio -button > < / el-radio-group > < / el-form-item >
< el-form-item v-if = "returnPickMode === 'BOX' && returnUnitType === 'PALLET' && returnPallets.length" label="整托选择"><div class="return-pallet-options"><el-checkbox v-for="code in returnPallets" :key="code" :model-value="returnCandidates.filter(item => item.palletCode === code && item.cartonMark).every(item => selectedReturnMarks.includes(item.cartonMark!))" @change="toggleReturnPallet(code, $event)" > {{ code }} < / el -checkbox > < / div > < / el-form-item >
< div class = "return-lists" >
< section class = "return-list-card" >
< div class = "list-title" > < span > SKU库存 < / span > < small > 按 SKU 查询 < / small > < / div >
< div class = "candidate-query" > < el-input v-model = "returnSkuQueryInput" clearable placeholder="如:SKU001,SKU002" /><el-button type="primary" @click="searchReturnSkus" > 查询 < / el -button > < / div >
< el-table :data = "pagedReturnStocks" border stripe class = "return-result-table" max -height = " 400 " > < el-table-column v-if = "returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" show-overflow-tooltip /><el-table-column prop="productNameZh" label="品名" min-width="150" show-overflow-tooltip /><el-table-column : label="returnPickMode === 'SKU' ? '散件可用数量' : '可用数量' " width="110" align="right"><template #default="{ row }" > {{ returnPickMode = = = ' SKU ' ? row.looseAvailableQuantity : row.availableQuantity }} < / template > < / el -table -column > < / el-table >
< el-table :data = "pagedReturnStocks" border stripe class = "return-result-table" max -height = " 400 " > < el-table-column v-if = "returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><el-table-column prop="skuCode" label="SKU" min-width="150" show-overflow-tooltip /><el-table-column prop="productNameZh" label="品名" min-width="150" show-overflow-tooltip /><el-table-column label="可用数量 " width="110" align="right"><template #default="{ row }" > {{ row.availableQuantity }} < / template > < / el -table -column > < / el-table >
< div class = "return-pagination" > < span > { { returnPickMode === 'SKU' ? ` 已选择 ${ selectedReturnSkus . length } 个 SKU ` : 'SKU 库存' } } < / span > < el-pagination v -model :current-page = "returnSkuPage" v -model :page-size = "returnSkuPageSize" : page -sizes = " [ 5 , 10 , 20 , 50 ] " :total = "filteredReturnStocks.length" layout = "total, sizes, prev, pager, next" @ size -change = " returnSkuPage = 1 " / > < / div >
< / section >
< section class = "return-list-card" >
< div class = "list-title" > < span > 箱号列表 < / span > < small > 支持多个箱号或 SKU , 用英文逗号分隔 < / small > < / div >
< div class = "list-title" > < span > { { returnPickMode === 'SKU' ? 'SKU 来源明细' : '箱号列表' } } < / span > < small > 支持多个箱号或 SKU , 用英文逗号分隔 < / small > < / div >
< div class = "candidate-query" > < el-input v-model = "returnCartonQueryInput" clearable placeholder="如:RC001,RC002 或 SKU001,SKU002" /><el-button type="primary" @click="searchReturnCartons" > 查询 < / el -button > < / div >
< el-table :data = "pagedReturnCartons" border stripe class = "return-result-table" max -height = " 400 " > < el-table-column label = "选择" width = "60" align = "center" > < template # default = "{ row }" > < el-checkbox :model-value = "selectedReturnMarks.includes(row.cartonMark)" @change ="toggleReturnCarton(row.cartonMark, $event)" / > < / template > < / el-table-column > < el-table-column prop = "cartonMark" label = " 箱号" min -width = " 190 " show -overflow -tooltip / > < el-table-column label = "托盘" min -width = " 150 " > < template # default = "{ row }" > { { returnCandidateByMark [ row . cartonMark ] ? . palletCode || '散箱' } } < / template > < / el-table-column > < el-table-column label = "箱内SKU及数量" min -width = " 230 " > < template # default = "{ row }" > { { returnCartonDetails [ row . cartonMark ] ? . items ? . map ( ( item ) => ` ${ item . skuCode } × ${ item . quantity } ` ) . join ( ', ' ) || '加载中' } } < / template > < / el-table-column > < / el-table >
< div class = "return-pagination" > < span > 已选择 { { selectedReturnMarks . length } } 个箱号 < / span > < el-pagination v -model :current-page = "returnCartonPage" v -model :page-size = "returnCartonPageSize" : page -sizes = " [ 5 , 10 , 20 , 50 ] " :total = "filteredReturnCartons.length" layout = "total, sizes, prev, pager, next" @ size -change = " returnCartonPage = 1 " / > < / div >
< el-table :data = "pagedReturnCartons" border stripe class = "return-result-table" max -height = " 400 " > < el-table-column label = "选择" width = "60" align = "center" > < template # default = "{ row }" > < el-checkbox :model-value = "selectedReturnMarks.includes(row.cartonMark)" @change ="toggleReturnCarton(row.cartonMark, $event)" / > < / template > < / el-table-column > < el-table-column : label = "returnPickMode === 'SKU' ? '库存来源' : ' 箱号' " min -width = " 190 " show -overflow -tooltip > < template # default = "{ row }" > { { row . sourceUnitType === 'PARCEL' ? '包裹散件(按 SKU 拣货)' : row . cartonMark } } < / template > < / el-table-column > < el-table-column label = "托盘" min -width = " 150 " > < template # default = "{ row }" > { { row . sourceUnitType === 'PARCEL' ? '无需托盘' : returnCandidateByMark [ row . cartonMark ] ? . palletCode || '散箱' } } < / template > < / el-table-column > < el-table-column label = "箱内SKU及数量" min -width = " 230 " > < template # default = "{ row }" > { { returnCartonDetails [ row . cartonMark ] ? . items ? . map ( ( item ) => ` ${ item . skuCode } × ${ item . quantity } ` ) . join ( ', ' ) || '加载中' } } < / template > < / el-table-column > < / el-table >
< div class = "return-pagination" > < span > 已选择 { { selectedReturnMarks . length } } 个 { { returnPickMode === 'SKU' ? '来源' : '箱号' } } < / span > < el-pagination v -model :current-page = "returnCartonPage" v -model :page-size = "returnCartonPageSize" : page -sizes = " [ 5 , 10 , 20 , 50 ] " :total = "filteredReturnCartons.length" layout = "total, sizes, prev, pager, next" @ size -change = " returnCartonPage = 1 " / > < / div >
< / section >
< / div >
< / template >