2026-08-19 16:45:51 +08:00
< script setup lang = "ts" >
2026-09-14 20:03:27 +08:00
import '@/styles/adminDensity.css'
2026-10-15 17:11:03 +08:00
import { appointmentReturnTarget } from '@/modules/appointment-calendar'
2026-08-20 18:02:52 +08:00
import { computed , onMounted , ref } from 'vue'
2026-09-22 09:38:56 +08:00
import { ElMessage } from 'element-plus'
2026-08-19 16:45:51 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-08-20 18:02:52 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-19 16:45:51 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-09-22 09:38:56 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-20 18:02:52 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-09-22 09:38:56 +08:00
import { getInboundPlan , moveInboundPallet , type InboundPlanDetailDto } from '../api/inboundPlans'
import { listOverseasLocations , type OverseasLocationDto } from '../api/locations'
2026-08-19 16:45:51 +08:00
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
2026-09-18 11:41:27 +08:00
import { formatOverseasDateTime , inboundPlanStatusLabel , lastMileOrderTypeLabel } from '../lastMilePresentation'
2026-08-19 16:45:51 +08:00
2026-08-20 18:02:52 +08:00
const route = useRoute (); const router = useRouter ()
const planNo = computed (() => String ( route . params . orderId || '' ))
const customerId = computed (() => String ( route . query . customerId || '' ))
const detail = ref < InboundPlanDetailDto | null >( null ); const loading = ref ( false ); const loadError = ref < ApiErrorDetails | null >( null )
2026-09-22 09:38:56 +08:00
const moveOpen = ref ( false ); const moveSaving = ref ( false ); const moveRow = ref < { palletNo : string ; palletVersion : number } | null > ( null ); const targetLocationCode = ref ( '' ); const moveReason = ref ( '入库托盘上架' ); const locationOptions = ref < OverseasLocationDto [] >([])
2026-09-11 10:32:29 +08:00
const referenceValue = computed (() => detail . value ? . planNo || ( /^\d+$/ . test ( planNo . value ) ? '—' : planNo . value ))
2026-08-20 18:02:52 +08:00
function text ( value : unknown ) { return String ( value ?? '' ). trim () || '—' }
2026-09-08 14:15:28 +08:00
function receivingWarehouse ( code : unknown , name : unknown ) {
const normalizedCode = String ( code ?? '' ). trim ()
const normalizedName = String ( name ?? '' ). trim ()
return normalizedCode && normalizedName ? ` ${ normalizedCode } · ${ normalizedName } ` : '—'
}
2026-09-18 11:41:27 +08:00
function status ( value : unknown ) { return inboundPlanStatusLabel ( value ) }
2026-08-20 18:02:52 +08:00
const firstLine = computed (() => detail . value ? . lines [ 0 ])
const fieldValues = computed < Record < string , string | number > > (() => ({
2026-09-11 10:32:29 +08:00
入库单号 : text ( detail . value ? . planNo ), 入仓码 : text ( firstLine . value ? . inboundCode ), 类型 : lastMileOrderTypeLabel ( detail . value ? . orderType ), 货主 : text ( detail . value ? . customerName ), 关联订单 : text ( detail . value ? . sourceOrderNo ),
2026-09-08 14:15:28 +08:00
海外仓 : receivingWarehouse ( detail . value ? . overseasWarehouseCode , detail . value ? . overseasWarehouseName ), FBA仓库 : text ( firstLine . value ? . fbaWarehouseCode ), 总箱数 : detail . value ? . expectedPackageCount ?? detail . value ? . lines . reduce (( sum , line ) => sum + line . requiredPackageCount , 0 ) ?? 0 ,
2026-08-25 10:34:27 +08:00
应拣货数 : detail . value ? . expectedPackageCount ?? detail . value ? . lines . reduce (( sum , line ) => sum + line . requiredPackageCount , 0 ) ?? 0 , 实际拣货数 : detail . value ? . pickedPackageCount ?? detail . value ? . allocations . reduce (( sum , item ) => sum + item . pickedPackageCount , 0 ) ?? 0 ,
2026-09-18 14:58:40 +08:00
'货物毛重(kg)' : Number ( detail . value ? . totalGrossWeightKg || 0 ), '货物净重(kg)' : Number ( detail . value ? . totalNetWeightKg || 0 ),
2026-09-20 19:33:53 +08:00
'计费体积(m³)' : Number ( detail . value ? . totalVolumeCbm || 0 ), 总托盘数 : Number ( detail . value ? . palletCount || 0 ),
2026-09-11 10:32:29 +08:00
预约状态 : '—' , 预约送仓时间 : '—' , 预计装车时间 : '—' , 排车状态 : '—' , 创建时间 : formatOverseasDateTime ( detail . value ? . createdAt ), 更新时间 : formatOverseasDateTime ( detail . value ? . updatedAt ), 备注 : text ( detail . value ? . remark ),
2026-08-20 18:02:52 +08:00
}))
2026-09-20 19:33:53 +08:00
const summaryGroups = [
2026-08-19 16:45:51 +08:00
{ title : '单据信息' , fields : [ '入库单号' , '入仓码' , '类型' , '货主' , '关联订单' ] },
2026-09-18 14:58:40 +08:00
{ title : '仓储信息' , fields : [ '海外仓' , 'FBA仓库' , '总箱数' , '应拣货数' , '实际拣货数' , '货物毛重(kg)' , '货物净重(kg)' , '计费体积(m³)' , '总托盘数' ] },
2026-08-19 16:45:51 +08:00
]
2026-09-20 19:33:53 +08:00
const appointmentFields = [ '预约状态' , '预约送仓时间' , '预计装车时间' , '排车状态' , '创建时间' , '更新时间' , '备注' ]
2026-08-20 18:02:52 +08:00
const feeColumns : DataTableColumn [] = [{ key : 'index' , label : '序号' , width : 70 , align : 'center' }, { key : 'name' , label : '费用项目' , minWidth : 180 }, { key : 'currency' , label : '币种' , width : 100 }, { key : 'amount' , label : '金额' , width : 140 , align : 'right' }, { key : 'remark' , label : '备注' , minWidth : 200 }]
2026-08-25 10:34:27 +08:00
const palletColumns : DataTableColumn [] = [
2026-09-22 09:38:56 +08:00
{ key : 'actions' , label : '操作' , width : 100 , fixed : 'left' , slotName : 'actions' },
2026-08-25 10:34:27 +08:00
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 150 },
{ key : 'palletNo' , prop : 'palletNo' , label : '托盘号' , minWidth : 150 },
2026-09-11 16:11:31 +08:00
{ key : 'palletOwner' , prop : 'palletOwner' , label : '托盘所属计划' , minWidth : 190 },
2026-08-25 10:34:27 +08:00
{ key : 'packageCount' , prop : 'packageCount' , label : '对应箱数' , width : 110 , align : 'right' },
2026-09-22 09:38:56 +08:00
{ key : 'location' , label : '当前库位' , minWidth : 150 , slotName : 'location' },
{ key : 'locationType' , label : '库位类型' , width : 120 , prop : 'locationType' },
{ key : 'putawayStatus' , label : '上架状态' , width : 110 , prop : 'putawayStatus' },
2026-08-25 10:34:27 +08:00
]
const palletRows = computed (() => ( detail . value ? . allocations || []). map (( allocation , index ) => {
const line = detail . value ? . lines . find ( item => item . id === allocation . lineId )
2026-09-22 09:38:56 +08:00
const pallet = detail . value ? . pallets . find ( item => item . id === allocation . palletId || item . warehousePalletId === allocation . palletId )
const locationCode = pallet ? . warehouseLocationCode || null
const locationType = pallet ? . sharedOverflow ? 'SHARED_OVERFLOW' : ( pallet ? . warehouseLocationType || null )
2026-08-25 10:34:27 +08:00
return {
id : ` ${ allocation . lineId } - ${ allocation . palletId } - ${ index } ` ,
inboundCode : text ( line ? . inboundCode ),
2026-09-11 16:11:31 +08:00
palletNo : text ( allocation . palletNo ),
palletOwner : allocation . palletOwnedByCurrentPlan ? ` ${ allocation . palletOwnerPlanNo } (本计划)` : ` ${ allocation . palletOwnerPlanNo } (跨计划合托)` ,
2026-08-25 10:34:27 +08:00
packageCount : Number ( allocation . pickedPackageCount ?? allocation . countedPackageCount ?? allocation . plannedPackageCount ?? 0 ),
2026-09-22 09:38:56 +08:00
palletId : String ( allocation . palletId ), palletVersion : pallet ? . version ?? 0 , sourceLocationId : pallet ? . warehouseLocationId || null , sourceLocationCode : locationCode ,
sourceLocationVersion : null , location : locationCode || ( pallet ? . warehouseLocationId ? '—' : '虚拟收货位' ), locationType : locationType === 'SHARED_OVERFLOW' ? 'A0共享溢出位' : locationType === 'NORMAL' ? '普通库位' : '虚拟收货位' , putawayStatus : locationType === 'NORMAL' || locationType === 'SHARED_OVERFLOW' ? '已上架' : '待上架' ,
2026-08-25 10:34:27 +08:00
}
}))
2026-09-22 09:38:56 +08:00
const availableLocations = computed (() => locationOptions . value . filter ( item => item . status !== 'DISABLED' ))
async function openMove ( row : typeof palletRows . value [ number ]) {
if ( ! detail . value ? . overseasWarehouseCode ) { ElMessage . warning ( '入库单未返回海外仓范围,暂不能移库' ); return }
moveRow . value = { palletNo : row . palletNo , palletVersion : row . palletVersion }; targetLocationCode . value = '' ; moveReason . value = '入库托盘上架' ; moveOpen . value = true
try { const result = await listOverseasLocations ( String (( detail . value as InboundPlanDetailDto & { overseasWarehouseId ?: string }). overseasWarehouseId || '' ), { page : 1 , size : 200 }); locationOptions . value = result . records . filter ( item => item . locationType !== 'VIRTUAL' && item . status !== 'DISABLED' ) } catch { locationOptions . value = []; ElMessage . error ( '可用库位加载失败,请重试' ) }
}
async function submitMove () {
if ( ! moveRow . value || ! targetLocationCode . value || ! moveReason . value . trim ()) { ElMessage . warning ( '请选择目标库位并填写操作原因' ); return }
moveSaving . value = true
try { const warehouseId = String ( detail . value ? . overseasWarehouseId || '' ); if ( ! warehouseId ) { ElMessage . error ( '入库单未返回海外仓 ID,暂不能移库' ); return }; const result = await moveInboundPallet ({ overseasWarehouseId : warehouseId , sourcePalletCode : moveRow . value . palletNo , targetLocationCode : targetLocationCode . value , expectedPalletVersion : moveRow . value . palletVersion , reason : moveReason . value . trim (), idempotencyKey : `admin-inbound-move- ${ moveRow . value . palletNo } - ${ Date . now () } ` }); if ( result . canMove === false ) { ElMessage . error ( result . blockReason || '当前托盘不可移库' ); return }; ElMessage . success ( '移库完成' ); moveOpen . value = false ; await load () } catch ( error ) { ElMessage . error ( getApiErrorDetails ( error , '移库失败' ). message ) } finally { moveSaving . value = false }
}
2026-08-20 18:02:52 +08:00
async function load () { if ( ! planNo . value || ! customerId . value ) { loadError . value = { message : '详情缺少客户范围,请从入库单列表重新进入' }; return }; loading . value = true ; loadError . value = null ; try { detail . value = await getInboundPlan ( planNo . value , customerId . value ) } catch ( error ) { loadError . value = getApiErrorDetails ( error , '入库单详情加载失败,请重试' ) } finally { loading . value = false } }
2026-10-15 17:11:03 +08:00
function back () { void router . push ( appointmentReturnTarget ( route . query ) || { name : 'overseas-inbound-orders' }) }
2026-08-20 18:02:52 +08:00
onMounted ( load )
2026-08-19 16:45:51 +08:00
</ script >
2026-10-15 17:11:03 +08:00
< template >< OverseasDetailShell class = "admin-density" dense title = "入库单详情" reference -label =" 入库单号 " :reference-value = "referenceValue" back -label =" 返回入库单 " : back -text =" appointmentReturnTarget ( route.query ) ? ' 返回预约工作 ' : ' 返回列表 '" domain = "入库单详情" :pending = "false" : status -text =" detail ? status ( detail.status ) : ' 加载中 '" @back ="back" >
2026-08-20 18:02:52 +08:00
< LoadErrorState v-if = "loadError" title="入库单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
2026-09-20 19:33:53 +08:00
< template v-else >< section v-loading = "loading" class="detail-section" aria-label="入库单基本信息"><div class="detail-groups"><section v-for="group in summaryGroups" :key="group.title" class="detail-group" :aria-label="group.title"><h2>{{ group.title }}</h2><dl class="detail-fields density-fields"><div v-for="label in group.fields" :key="label" >< dt >{{ label }}</ dt >< dd >{{ fieldValues [ label ] }}</ dd ></ div ></ dl ></ section ></ div ></ section >
2026-09-22 09:38:56 +08:00
< section class = "detail-section pallet-section" aria -labelledby =" inbound -pallet-heading " >< div class = "section-heading" >< div >< h2 id = "inbound-pallet-heading" > 入库单托盘明细 </ h2 >< p class = "pallet-note" > PDA 合托后 , 参与合托的入库单会分别展示同一托盘及本单对应箱数 ; A0 为系统共享溢出位 。 </ p ></ div >< span class = "section-count" >{{ palletRows . length }} 条 </ span ></ div >< div class = "detail-table-wrap" >< DataTable :data = "palletRows" :columns = "palletColumns" column -settings-mode =" hide " row -key =" id " empty -text =" 暂无托盘明细 " >< template # actions = "{ row }" >< el-button class = "density-action" size = "small" : disabled = "!(row as typeof palletRows[number]).palletNo || !(row as typeof palletRows[number]).palletVersion" @click ="openMove(row as typeof palletRows[number])" > 移库 </ el -button ></ template ></ DataTable ></ div ></ section >
2026-09-20 19:33:53 +08:00
< section class = "detail-section appointment-section" aria -labelledby =" inbound -appointment-heading " >< div class = "compact-section-heading" >< h2 id = "inbound-appointment-heading" > 预约与排车 </ h2 ></ div >< dl class = "detail-fields appointment-fields density-fields" >< div v-for = "label in appointmentFields" :key="label" :class="{ 'is-wide': label === '备注' }" >< dt >{{ label }}</ dt >< dd >{{ fieldValues [ label ] }}</ dd ></ div ></ dl ></ section >
2026-09-20 19:45:27 +08:00
< section class = "detail-section fee-section" aria -labelledby =" inbound -fees-heading " >< div class = "compact-section-heading" >< h2 id = "inbound-fees-heading" > 应收费用明细 </ h2 >< span > 费用结果以服务端为准 </ span ></ div >< div class = "detail-table-wrap" >< DataTable :data = "[]" :columns = "feeColumns" column -settings-mode =" hide " empty -text =" 当前入库计划详情接口未提供应收费用 " /></ div >< footer class = "fee-total" >< span > 应收费用合计 ( USD ) </ span >< strong > — </ strong ></ footer ></ section ></ template >
2026-09-28 15:24:33 +08:00
</ OverseasDetailShell >< AppDialog v-model = "moveOpen" class="admin-density-dialog" title="托盘移库" width="min(560px, calc(100vw - 28px))" confirm-text="提交移库" :confirm-loading="moveSaving" :confirm-disabled="!targetLocationCode" @confirm="submitMove"><el-form class="density-form" label-position="top"><el-form-item label="托盘号"><el-input :model-value="moveRow?.palletNo" readonly /></el-form-item><el-form-item label="目标库位" required><el-select v-model="targetLocationCode" filterable placeholder="请选择普通库位或 A0"><el-option v-for="item in availableLocations" :key="item.id" :value="item.locationCode" :label="`${item.locationCode}${item.sharedOverflow || item.locationType === 'SHARED_OVERFLOW' ? ' · A0共享溢出位' : ''}`" /></el-select></el-form-item><el-form-item label="操作原因" required><el-input v-model="moveReason" maxlength="200" placeholder="请输入移库原因" /></ el-form-item ></ el-form ></ AppDialog ></ template >
2026-08-19 16:45:51 +08:00
< style scoped >
2026-09-20 19:33:53 +08:00
. detail - section { min - width : 0 ; padding : 0 ; border : 0 ; background : # fff ; }
. detail - group h2 { margin : 0 0 var ( -- admin - space - 3 ); color : var ( -- cc - text ); font - size : var ( -- admin - font - section ); }
. compact - section - heading { display : flex ; min - height : 42 px ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 2 ) var ( -- admin - space - 5 ); border - bottom : 1 px solid var ( -- cc - border ); }
. compact - section - heading h2 { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - section ); }
. compact - section - heading span { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }
2026-09-20 19:45:27 +08:00
. fee - section { overflow : hidden ; }. fee - total { display : flex ; min - height : 44 px ; align - items : center ; justify - content : flex - end ; gap : 40 px ; margin : 0 var ( -- admin - space - 5 ) var ( -- admin - space - 4 ); padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ); border : 1 px solid var ( -- cc - border ); border - top : 0 ; }. fee - total strong { min - width : 120 px ; font - size : var ( -- admin - font - section ); }
2026-09-20 19:33:53 +08:00
. pallet - section { overflow : hidden ; }. section - heading { display : flex ; min - height : 50 px ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ); border - bottom : 1 px solid var ( -- cc - border ); }. pallet - section h2 { margin : 0 0 var ( -- admin - space - 1 ); color : var ( -- cc - text ); font - size : var ( -- admin - font - section ); }. pallet - note { margin : 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); }. section - count { flex : 0 0 auto ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption ); font - variant - numeric : tabular - nums ; }
. appointment - section ,. fee - section { border - top : 1 px solid var ( -- cc - border ); }
. appointment - section { padding : 0 var ( -- admin - space - 5 ) var ( -- admin - space - 4 ); }
. appointment - section . compact - section - heading { margin : 0 calc ( 0 px - var ( -- admin - space - 5 )) var ( -- admin - space - 3 ); }
. appointment - fields { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); }
. appointment - fields > . is - wide { grid - column : span 2 ; }
@ media ( max - width : 1400 px ){. detail - fields { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ))}}
@ media ( max - width : 1200 px ){. detail - fields ,. appointment - fields { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ))}. appointment - fields > . is - wide { grid - column : span 2 }}
2026-09-14 20:03:27 +08:00
. detail - groups { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ); }
2026-09-20 19:33:53 +08:00
. detail - group { min - width : 0 ; padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ) var ( -- admin - space - 4 ); border - bottom : 1 px solid var ( -- cc - border ); }
. detail - fields { -- admin - field - label - width : 108 px ; grid - template - columns : repeat ( 5 , minmax ( 0 , 1 fr )); }
: deep (. density - detail - header ) { border - bottom : 1 px solid var ( -- cc - border ); }
2026-09-20 19:45:27 +08:00
. detail - table - wrap { padding : var ( -- admin - space - 3 ) var ( -- admin - space - 5 ) var ( -- admin - space - 4 ); }
. fee - section . detail - table - wrap { padding - bottom : 0 ; }
2026-09-14 20:03:27 +08:00
2026-08-19 16:45:51 +08:00
</ style >