@@ -1,12 +1,13 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft , Printer } from '@lucide/vue'
import { ArrowLeft , Printer , Plus } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
import type { TableAction } from '@/components/common/tableActions'
import { ApiError } from '@/api/types'
import { createIdempotencyKey } from '@/utils/idempotencyKey'
import { getMerchantWarehouses , type MerchantWarehouse } from '@/modules/first-leg/public'
import {
getReturnInventoryCarton ,
@@ -18,9 +19,12 @@ import {
type ReturnInventoryCartonDetail ,
type ReturnInventoryItem ,
type ReturnInventorySummaryItem ,
createReturnPackingPlan ,
listReturnPackingPlans ,
type ReturnPackingPlan ,
} from '../api/returnInventory'
const activeTab = ref < 'summary' | 'cartons' > ( 'summary' )
const activeTab = ref < 'summary' | 'cartons' | 'plans' > ( 'summary' )
const props = defineProps < { cartonOnly ? : boolean ; detailPage ? : boolean } > ( )
const route = useRoute ( )
const router = useRouter ( )
@@ -57,6 +61,11 @@ async function loadWarehouses() {
const summaryRows = ref < ReturnInventorySummaryItem [ ] > ( [ ] )
const cartonRows = ref < ReturnInventoryItem [ ] > ( [ ] )
const cartonSummaryRows = ref < ReturnInventoryCartonSummary [ ] > ( [ ] )
const packingPlans = ref < ReturnPackingPlan [ ] > ( [ ] )
const packingPlanTotal = ref ( 0 )
const showPackingCreate = ref ( false )
const packingSaving = ref ( false )
const packingForm = reactive ( { returnNos : '' , selectedSkus : { } as Record < string , number > , length : 1 , width : 1 , height : 1 , weight : 1 , remark : '' } )
const total = ref ( 0 )
const cartonDetail = ref < ReturnInventoryCartonDetail > ( )
const query = reactive ( { page : 1 , size : 20 , overseasWarehouseId : '' , skuCode : '' , cartonMark : '' , status : 'AVAILABLE' , returnNo : '' } )
@@ -122,6 +131,10 @@ async function load() {
loading . value = true
loadError . value = ''
try {
if ( activeTab . value === 'plans' ) {
const page = await listReturnPackingPlans ( { page : query . page , size : query . size } )
packingPlans . value = page . records ; packingPlanTotal . value = Number ( page . total ) ; total . value = packingPlanTotal . value ; return
}
if ( props . cartonOnly ) {
const page = await listReturnInventoryCartons ( params ( true ) )
cartonSummaryRows . value = page . records
@@ -155,6 +168,18 @@ async function load() {
function search ( ) { if ( query . page === 1 ) void load ( ) ; else query . page = 1 }
function resetSearch ( ) { query . status = 'AVAILABLE' ; search ( ) }
function changeTab ( ) { query . page = 1 ; void load ( ) }
function openPackingCreate ( ) { packingForm . returnNos = '' ; packingForm . selectedSkus = { } ; packingForm . length = 1 ; packingForm . width = 1 ; packingForm . height = 1 ; packingForm . weight = 1 ; packingForm . remark = '' ; showPackingCreate . value = true }
function togglePackingSku ( skuCode : string , checked : boolean ) { if ( checked ) packingForm . selectedSkus [ skuCode ] = 1 ; else delete packingForm . selectedSkus [ skuCode ] }
async function submitPackingPlan ( ) {
const returnNos = packingForm . returnNos . split ( /[\s,, ]+/ ) . map ( item => item . trim ( ) ) . filter ( Boolean )
const items = Object . entries ( packingForm . selectedSkus ) . filter ( ( [ , quantity ] ) => Number ( quantity ) > 0 ) . map ( ( [ skuCode , quantity ] ) => ( { skuCode , quantity : Number ( quantity ) } ) )
if ( ! returnNos . length || ! items . length ) { ElMessage . warning ( '请填写已完成退货单号并至少选择一个 SKU' ) ; return }
packingSaving . value = true
try {
await createReturnPackingPlan ( { returnNos , mode : 'MERGE' , boxes : [ { items , length : Number ( packingForm . length ) , width : Number ( packingForm . width ) , height : Number ( packingForm . height ) , weight : Number ( packingForm . weight ) } ] , remark : packingForm . remark || undefined , idempotencyKey : createIdempotencyKey ( ) } )
ElMessage . success ( '合箱计划已提交审核' ) ; showPackingCreate . value = false ; activeTab . value = 'plans' ; query . page = 1 ; await load ( )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '合箱计划创建失败' ) } finally { packingSaving . value = false }
}
function openSkuCartons ( skuCode : string ) {
query . skuCode = skuCode
activeTab . value = 'cartons'
@@ -217,11 +242,19 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
< template >
< section class = "return-inventory-page" >
< template v-if = "!props.detailPage" >
< el -tabs v-if = "!props.cartonOnly" v-model="activeTab" @tab-change="changeTab"><el-tab-pane label="SKU 汇总库存" name="summary" /><el-tab-pane label="箱号明细" name="cartons" / > < / el-tabs >
< div class = "inventory-toolbar" > < el-tabs v-if = "!props.cartonOnly" v-model="activeTab" @tab-change="changeTab"><el-tab-pane label="SKU 汇总库存" name="summary" /><el-tab-pane label="箱号明细" name="cartons" /><el-tab-pane label="合箱计划" name="plans" /></el-tabs><el-button type="primary" @click="openPackingCreate"><Plus :size="15" / > 创建合箱计划 < / el-button > < / div >
< QueryToolbar v-model = "queryModel" layout="grid" :ariaLabel="`${props.cartonOnly ? '退货箱号' : '退货库存'}筛选`" :fields="queryFields" :search-on-reset="false" @search="search" @reset="resetSearch" / >
< el-alert v-if = "loadError" :title="loadError" type="error" show-icon :closable="false" / >
< el-alert v-if = "warehouseError" :title="warehouseError" type="warning" show-icon :closable="false"><el-button link type="primary" @click="loadWarehouses" > 重试加载海外仓 < / el -button > < / el-alert >
< div class = "return-inventory-table" >
< div v-if = "activeTab === 'plans'" class=" return-inventory-table" >
< el -table :data = "packingPlans" v-loading = "loading" size="small" >
< el -table -column prop = "planNo" label = "计划号" min -width = " 220 " / >
< el-table-column prop = "status" label = "状态" width = "150" > < template # default = "{ row }" > < el-tag > { { row . status === 'PENDING_REVIEW' ? '等待平台审核' : row . status === 'READY' ? '待作业' : row . status === 'COMPLETED' ? '已完成' : row . status === 'CANCELLED' ? '已取消' : row . status } } < / el-tag > < / template > < / el-table-column >
< el-table-column prop = "returnNos" label = "退货单" min -width = " 300 " > < template # default = "{ row }" > { { row . returnNos . join ( '、' ) } } < / template > < / el-table-column >
< / el-table >
< div class = "plan-pagination" > < el-pagination v -model :current-page = "query.page" v -model :page-size = "query.size" :total = "packingPlanTotal" layout = "total, sizes, prev, pager, next" : page -sizes = " [ 20 , 50 , 100 ] " / > < / div >
< / div >
< div v-else class = "return-inventory-table" >
< DataTable v -model :page = "query.page" v -model :page-size = "query.size" :data = "currentRows" :columns = "currentColumns" :loading = "loading" :total = "total" : page -sizes = " [ 20 , 50 , 100 ] " :row-actions = "rowActions" : empty -text = " loadError ? ' 库存加载失败 , 请核对权限或重试 ' : ' 暂无数据 ' " >
< template # warehouse = "{ row }" > { { warehouseLabel ( row . overseasWarehouseId ) } } < / template >
< template # status = "{ row }" > < el-tag effect = "light" > { { cartonStatusLabel ( row . status ) } } < / el-tag > < / template >
@@ -230,6 +263,7 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
< template # time = "{ row }" > < span class = "inventory-time" > { { formatInventoryTime ( row . updatedAt || row . createdAt || row . receivedAt ) } } < / span > < / template >
< / DataTable >
< / div >
< el-dialog v-model = "showPackingCreate" title="创建合箱计划" width="720px" destroy-on-close><el-form label-width="110px"><el-form-item label="已完成退货单"><el-input v-model="packingForm.returnNos" type="textarea" :rows="2" placeholder="输入退货单号,多个用空格或逗号分隔" /></el-form-item><el-form-item label="合箱 SKU"><div class="packing-sku-grid"><div v-for="item in summaryRows" :key="item.skuCode" class="packing-sku-row"><el-checkbox :model-value="packingForm.selectedSkus[item.skuCode] !== undefined" @change="togglePackingSku(item.skuCode, Boolean($event))">{{ item.skuCode }}</el-checkbox><span>可用 {{ item.availableQuantity }}</span><el-input-number v-if="packingForm.selectedSkus[item.skuCode] !== undefined" v-model="packingForm.selectedSkus[item.skuCode]" :min="1" :max="item.availableQuantity" :controls="false" placeholder="数量" /></div></div></el-form-item><el-form-item label="箱规"><div class="packing-dimensions"><el-input-number v-model="packingForm.length" :min="1" placeholder="长 cm" /><el-input-number v-model="packingForm.width" :min="1" placeholder="宽 cm" /><el-input-number v-model="packingForm.height" :min="1" placeholder="高 cm" /><el-input-number v-model="packingForm.weight" :min="1" placeholder="重量 kg" /></div></el-form-item><el-form-item label="备注"><el-input v-model="packingForm.remark" placeholder="可填写合箱说明" /></el-form-item></el-form><template #footer><el-button @click="showPackingCreate = false">取消</el-button><el-button type="primary" :loading="packingSaving" @click="submitPackingPlan" > 提交审核 < / el -button > < / template > < / el-dialog >
< / template >
< article v-else v-loading = "loading" class="carton-detail-surface" >
< template v-if = "cartonDetail" >
@@ -243,7 +277,7 @@ onMounted(() => { if (props.detailPage) void Promise.all([loadCartonDetail(), lo
< / template >
< style scoped >
. return - inventory - page { display : grid ; gap : 12 px ; } . return - inventory - page : deep ( . el - tabs _ _header ) { margin : 0 ; } . return - inventory - table { overflow : hidden ; border : 1 px solid var ( -- client - border ) ; border - radius : var ( -- client - radius ) ; background : # fff ; } . inventory - time { white - space : nowrap ; }
. return - inventory - page { display : grid ; gap : 12 px ; } . return - inventory - page : deep ( . el - tabs _ _header ) { margin : 0 ; } . inventory - toolbar { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px } . return - inventory - table { overflow : hidden ; border : 1 px solid var ( -- client - border ) ; border - radius : var ( -- client - radius ) ; background : # fff ; } . inventory - time { white - space : nowrap ; } . plan - pagination { display : flex ; justify - content : flex - end ; padding : 10 px } . packing - sku - grid { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 8 px ; width : 100 % } . packing - sku - row { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto 92 px ; align - items : center ; gap : 8 px ; padding : 6 px 8 px ; border : 1 px solid var ( -- client - border ) ; border - radius : 6 px } . packing - sku - row > span { color : var ( -- client - muted ) ; font - size : 12 px } . packing - dimensions { display : grid ; grid - template - columns : repeat ( 4 , 1 fr ) ; gap : 8 px ; width : 100 % }
. carton - detail - surface { overflow : hidden ; min - height : 240 px ; border : 1 px solid var ( -- client - border ) ; border - radius : var ( -- client - radius ) ; background : # fff } . carton - detail - header { display : grid ; grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 14 px ; min - height : 64 px ; padding : 10 px 16 px ; border - bottom : 1 px solid var ( -- client - border ) } . carton - detail - back { display : inline - flex ; align - items : center ; gap : 5 px ; padding : 5 px 0 ; border : 0 ; background : transparent ; color : var ( -- el - color - primary ) ; cursor : pointer } . carton - detail - header > div : nth - child ( 2 ) { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px } . carton - detail - header strong { overflow : hidden ; color : # 1 f2d45 ; font - size : 18 px ; text - overflow : ellipsis ; white - space : nowrap } . carton - detail - header span { color : # 7 c899c ; font - size : 12 px } . carton - detail - actions { display : flex ; align - items : center ; gap : 8 px } . carton - detail - section + . carton - detail - section { border - top : 1 px solid var ( -- client - border ) } . carton - detail - section > h3 { display : flex ; min - height : 40 px ; align - items : center ; margin : 0 ; padding : 0 16 px ; border - bottom : 1 px solid var ( -- client - border ) ; background : var ( -- client - header - bg ) ; font - size : 14 px } . carton - detail - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; margin : 0 } . carton - detail - grid div { display : grid ; min - height : 42 px ; grid - template - columns : 88 px minmax ( 0 , 1 fr ) ; align - items : center ; gap : 8 px ; padding : 5 px 12 px ; border - right : 1 px solid var ( -- client - border ) ; border - bottom : 1 px solid var ( -- client - border ) } . carton - detail - grid dt { color : var ( -- client - muted ) ; font - size : 12 px } . carton - detail - grid dd { margin : 0 ; overflow : hidden ; color : var ( -- client - text ) ; font - size : 13 px ; font - weight : 600 ; text - overflow : ellipsis ; white - space : nowrap } @ media ( max - width : 1100 px ) { . carton - detail - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) } } @ media ( max - width : 720 px ) { . carton - detail - header { grid - template - columns : 1 fr auto } . carton - detail - back { grid - column : 1 / - 1 } . carton - detail - grid { grid - template - columns : 1 fr } }
. carton - detail - error { display : flex ; min - height : 220 px ; align - items : center ; justify - content : center ; gap : 12 px ; color : # b42318 }
< / style >