2026-08-04 21:01:39 +08:00
< script setup lang = "ts" >
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue'
import { useRouter } from 'vue-router'
import { ChevronDown , ChevronUp , ClipboardCheck , Eye , RefreshCw , SlidersHorizontal } from '@lucide/vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { getPlatformInbound , listPlatformInbounds } from '../core/api'
import InboundHistoryPanel from '../core/InboundHistoryPanel.vue'
import InboundSummaryPanel from '../core/InboundSummaryPanel.vue'
import type { InboundListTab , InboundOrderDetail , InboundOrderSummary } from '../core/types'
type FilterModel = QueryModel & {
customerId : string
inboundCode : string
orderNo : string
warehouseId : string
deliveryWarehouseId : string
processStatus : string
orderStatus : string
measureReviewStatus : string
customerName : string
destinationCode : string
feeStatus : string
feeReviewStatus : string
holdFlag : string
expectedArrivalRange : string []
}
const router = useRouter ()
const tab = ref < InboundListTab | '' >( '' )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
const detailOpen = ref ( false )
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null >( null )
const detail = ref < InboundOrderDetail | null >( null )
const detailTarget = ref < InboundOrderSummary | null >( null )
const result = ref ({ records : [] as InboundOrderSummary [], total : 0 })
const filters = ref < FilterModel >({ customerId : '' , inboundCode : '' , orderNo : '' , warehouseId : '' , deliveryWarehouseId : '' , processStatus : '' , orderStatus : '' , measureReviewStatus : '' , customerName : '' , destinationCode : '' , feeStatus : '' , feeReviewStatus : '' , holdFlag : '' , expectedArrivalRange : [] })
const selectedCustomerId = ref ( '' )
const customers = ref < PlatformTenantResource [] >([])
const customersLoading = ref ( false )
const customerLoadError = ref < ApiErrorDetails | null >( null )
const warehouseOptions = ref < Array < { label : string ; value : string }> > ([])
const warehousesLoading = ref ( false )
const warehousesUnavailable = ref ( false )
const showAdvancedFilters = ref ( false )
let loadRequestId = 0
let detailRequestId = 0
let customerRequestId = 0
let warehouseRequestId = 0
let disposed = false
const tabs : Array < { value : InboundListTab | '' ; label : string ; description : string } > = [
{ value : '' , label : '全部' , description : '全部入库单' },
{ value : 'WAIT_MEASURE' , label : '待测量' , description : '等待测量或补正' },
{ value : 'WAIT_REVIEW' , label : '待审核' , description : '等待测量审核' },
{ value : 'WAIT_FEE_SEND' , label : '待费用/入库' , description : '等待费用编排完成' },
{ value : 'INBOUNDED' , label : '已入库' , description : '费用已发布并完成入库' },
]
const advancedFilterKeys = new Set ([ 'orderStatus' , 'feeReviewStatus' , 'customerName' , 'destinationCode' , 'holdFlag' ])
const allQueryFields = computed < QueryField [] >(() => [
{
key : 'customerId' ,
type : 'select' ,
width : 260 ,
placeholder : customerLoadError . value ? '所属企业加载失败' : customersLoading . value ? '正在加载客户' : '全部客户' ,
options : customers . value . map (( customer ) => ({
value : String ( customer . id ),
label : ` ${ customer . legalName ? . trim () || customer . tenantName } ( ${ customer . tenantCode || '无客户编码' } ) `,
})),
props : { 'aria-label' : '所属企业' , loading : customersLoading . value , filterable : true , disabled : Boolean ( customerLoadError . value ) || customers . value . length === 0 },
},
{ key : 'warehouseId' , type : 'select' , width : 180 , placeholder : warehousesUnavailable . value ? '定位仓库加载失败' : '定位仓库' , options : warehouseOptions . value , props : { 'aria-label' : '定位仓库' , loading : warehousesLoading . value , filterable : true , disabled : ! selectedCustomerId . value || warehousesUnavailable . value } },
{ key : 'deliveryWarehouseId' , type : 'select' , width : 180 , placeholder : warehousesUnavailable . value ? '交货仓加载失败' : '交货仓' , options : warehouseOptions . value , props : { 'aria-label' : '交货仓' , loading : warehousesLoading . value , filterable : true , disabled : ! selectedCustomerId . value || warehousesUnavailable . value } },
{ key : 'inboundCode' , type : 'input' , width : 220 , placeholder : '入仓码' },
{ key : 'orderNo' , type : 'input' , width : 220 , placeholder : '头程订单号' },
{ key : 'processStatus' , type : 'select' , width : 160 , placeholder : '流程状态' , options : [
{ label : '待测量' , value : 'WAIT_MEASURE' }, { label : '测量中' , value : 'MEASURING' },
{ label : '待测量审核' , value : 'MEASURE_PENDING' }, { label : '测量已驳回' , value : 'MEASURE_REJECTED' },
{ label : '待计费' , value : 'WAIT_PRICING' }, { label : '费用处理中' , value : 'FEE_CONFIRMING' },
{ label : '入库处理中' , value : 'INBOUNDING' }, { label : '已入库' , value : 'INBOUNDED' }, { label : '已作废' , value : 'VOIDED' },
] },
{ key : 'orderStatus' , type : 'input' , width : 200 , placeholder : '订单状态码' },
{ key : 'measureReviewStatus' , type : 'select' , width : 160 , placeholder : '测量审核状态' , options : [
{ label : '待审核' , value : 'PENDING' }, { label : '审核通过' , value : 'APPROVED' }, { label : '审核驳回' , value : 'REJECTED' },
] },
{ key : 'feeStatus' , type : 'select' , width : 160 , placeholder : '费用状态' , options : [
{ label : '未计费' , value : 'UNPRICED' }, { label : '已试算' , value : 'CALCULATED' },
{ label : '已确认' , value : 'CONFIRMED' }, { label : '已发送' , value : 'SENT' }, { label : '已作废' , value : 'VOIDED' },
] },
{ key : 'feeReviewStatus' , type : 'input' , width : 200 , placeholder : '费用审核状态码' },
{ key : 'customerName' , type : 'input' , width : 260 , placeholder : '客户名称' },
{ key : 'destinationCode' , type : 'input' , width : 220 , placeholder : '目的仓编码' },
{ key : 'holdFlag' , type : 'select' , width : 160 , placeholder : '挂起状态' , options : [{ label : '已挂起' , value : 'true' }, { label : '未挂起' , value : 'false' }] },
{ key : 'expectedArrivalRange' , type : 'daterange' , width : 320 , placeholder : '预计到达日期' },
])
const queryFields = computed (() => showAdvancedFilters . value
? allQueryFields . value
: allQueryFields . value . filter (( field ) => ! advancedFilterKeys . has ( field . key )))
const columns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 152 , fixed : 'left' , slotName : 'actions' },
{ key : 'inboundCode' , label : '入仓码' , minWidth : 166 , slotName : 'inbound' },
{ key : 'orderStatus' , label : '订单 / 流程状态' , minWidth : 156 , slotName : 'orderStatus' },
{ key : 'measureReviewStatus' , label : '测量数据审核状态' , minWidth : 142 , slotName : 'measureReviewStatus' },
{ key : 'expectedArrivalAt' , label : '预计到达时间' , minWidth : 164 , slotName : 'expectedArrivalAt' },
{ key : 'feeStatus' , label : '费用状态' , minWidth : 124 , slotName : 'feeStatus' },
{ key : 'customerName' , label : '客户' , minWidth : 142 , slotName : 'customerName' },
{ key : 'warehouseName' , label : '仓库' , minWidth : 140 , slotName : 'warehouseName' },
{ key : 'deliveryWarehouseName' , label : '交货仓' , minWidth : 140 , slotName : 'deliveryWarehouseName' },
{ key : 'loadingStatus' , label : '配载状态' , minWidth : 110 , slotName : 'loadingStatus' },
{ key : 'holdFlag' , label : '是否扣货' , width : 96 , slotName : 'holdFlag' },
{ key : 'currentHoldReason' , label : '扣货理由' , minWidth : 150 , slotName : 'currentHoldReason' },
{ key : 'containerNo' , label : '集装箱号' , minWidth : 132 , slotName : 'containerNo' },
{ key : 'cabinetNo' , label : '柜号' , minWidth : 118 , slotName : 'cabinetNo' },
{ key : 'companyName' , label : '公司' , minWidth : 160 , slotName : 'companyName' },
{ key : 'businessType' , label : '业务类型' , minWidth : 112 , slotName : 'businessType' },
{ key : 'channelName' , label : '渠道' , minWidth : 138 , slotName : 'channelName' },
{ key : 'taxMode' , label : '计税模式' , minWidth : 108 , slotName : 'taxMode' },
{ key : 'containerMode' , label : '装载模式' , minWidth : 108 , slotName : 'containerMode' },
{ key : 'customsInspection' , label : '商检验' , width : 92 , slotName : 'customsInspection' },
{ key : 'sailingAt' , label : '开船时间' , minWidth : 164 , slotName : 'sailingAt' },
{ key : 'portArrivalAt' , label : '到港时间' , minWidth : 164 , slotName : 'portArrivalAt' },
{ key : 'totalPackageCount' , label : '总箱数' , width : 90 , slotName : 'totalPackageCount' },
{ key : 'actualGrossWeightKg' , label : '总毛重(kg)' , minWidth : 118 , slotName : 'actualGrossWeightKg' },
{ key : 'chargeableWeightKg' , label : '计费重量(kg)' , minWidth : 126 , slotName : 'chargeableWeightKg' },
{ key : 'actualVolumeCbm' , label : '总体积(CBM)' , minWidth : 122 , slotName : 'actualVolumeCbm' },
{ key : 'chargeableVolumeCbm' , label : '计费体积(CBM)' , minWidth : 132 , slotName : 'chargeableVolumeCbm' },
{ key : 'palletCount' , label : '总托盘数' , width : 100 , slotName : 'palletCount' },
{ key : 'locationCodes' , label : '库位位置' , minWidth : 124 , slotName : 'locationCodes' },
{ key : 'volumeWeightKg' , label : '总材积重(kg)' , minWidth : 126 , slotName : 'volumeWeightKg' },
{ key : 'actualNetWeightKg' , label : '总净重(kg)' , minWidth : 118 , slotName : 'actualNetWeightKg' },
{ key : 'destinationType' , label : '目的仓类型' , minWidth : 112 , slotName : 'destinationType' },
{ key : 'destinationName' , label : '目的仓' , minWidth : 132 , slotName : 'destinationName' },
{ key : 'measureCompletedAt' , label : '测量完成时间' , minWidth : 164 , slotName : 'measureCompletedAt' },
{ key : 'inboundedAt' , label : '入库完成时间' , minWidth : 164 , slotName : 'inboundedAt' },
{ key : 'expressCompanyName' , label : '快递公司' , minWidth : 120 , slotName : 'expressCompanyName' },
{ key : 'expressTrackingNo' , label : '快递单号' , minWidth : 150 , slotName : 'expressTrackingNo' },
{ key : 'createdAt' , label : '创建日期' , minWidth : 164 , slotName : 'createdAt' },
]
const selectedCustomerName = computed (() => {
const customer = customers . value . find (( item ) => String ( item . id ) === selectedCustomerId . value )
return customer ? . legalName ? . trim () || customer ? . tenantName ? . trim () || '全部客户'
})
const listEmptyText = '暂无符合条件的入库单'
function queryValue ( value : string ) { return value . trim () || undefined }
function handleTabKeydown ( event : KeyboardEvent , currentIndex : number ) {
const lastIndex = tabs . length - 1
let nextIndex = currentIndex
if ( event . key === 'ArrowRight' ) nextIndex = currentIndex === lastIndex ? 0 : currentIndex + 1
else if ( event . key === 'ArrowLeft' ) nextIndex = currentIndex === 0 ? lastIndex : currentIndex - 1
else if ( event . key === 'Home' ) nextIndex = 0
else if ( event . key === 'End' ) nextIndex = lastIndex
else return
event . preventDefault ()
tab . value = tabs [ nextIndex ]. value
const tabButtons = ( event . currentTarget as HTMLElement ). parentElement ? . querySelectorAll < HTMLElement >( '[role="tab"]' )
tabButtons ? .[ nextIndex ] ? . focus ()
}
function dateParam ( value : string | undefined , end = false ) {
if ( ! value ) return undefined
return ` ${ value } T ${ end ? '23:59:59' : '00:00:00' } `
}
async function loadWarehouseOptions () {
const requestId = ++ warehouseRequestId
warehouseOptions . value = []
warehousesUnavailable . value = false
if ( ! selectedCustomerId . value ) {
warehousesLoading . value = false
return
}
warehousesLoading . value = true
try {
const response = await listPlatformWarehouses ( selectedCustomerId . value , 1 , 200 )
if ( disposed || requestId !== warehouseRequestId ) return
warehouseOptions . value = response . records . map (( warehouse ) => ({
label : ` ${ warehouse . warehouseName } · ${ warehouse . warehouseCode } ` ,
value : String ( warehouse . id ),
}))
} catch {
if ( disposed || requestId !== warehouseRequestId ) return
warehousesUnavailable . value = true
} finally {
if ( ! disposed && requestId === warehouseRequestId ) warehousesLoading . value = false
}
}
async function loadCustomers () {
const requestId = ++ customerRequestId
customersLoading . value = true
customerLoadError . value = null
try {
const records : PlatformTenantResource [] = []
let currentPage = 1
let pages = 1
do {
const response = await listPlatformTenants ({ page : currentPage , size : 200 })
records . push (... response . records )
pages = Math . max ( 1 , response . pages )
currentPage += 1
} while ( currentPage <= pages )
if ( disposed || requestId !== customerRequestId ) return
customers . value = records
} catch ( error ) {
if ( disposed || requestId !== customerRequestId ) return
customers . value = []
customerLoadError . value = getApiErrorDetails ( error , '客户列表加载失败,暂时无法按所属企业筛选;入库单仍按全部客户查询' )
} finally {
if ( ! disposed && requestId === customerRequestId ) customersLoading . value = false
}
}
async function loadInbounds () {
const requestId = ++ loadRequestId
loading . value = true
loadError . value = null
const range = filters . value . expectedArrivalRange
try {
const response = await listPlatformInbounds ( selectedCustomerId . value || undefined , {
page : page . value ,
size : pageSize . value ,
tab : tab . value || undefined ,
inboundCode : queryValue ( filters . value . inboundCode ),
orderNo : queryValue ( filters . value . orderNo ),
warehouseId : queryValue ( filters . value . warehouseId ),
deliveryWarehouseId : queryValue ( filters . value . deliveryWarehouseId ),
processStatus : queryValue ( filters . value . processStatus ),
orderStatus : queryValue ( filters . value . orderStatus ),
measureReviewStatus : queryValue ( filters . value . measureReviewStatus ),
customerName : queryValue ( filters . value . customerName ),
destinationCode : queryValue ( filters . value . destinationCode ),
feeStatus : queryValue ( filters . value . feeStatus ),
feeReviewStatus : queryValue ( filters . value . feeReviewStatus ),
holdFlag : filters . value . holdFlag === '' ? undefined : filters . value . holdFlag === 'true' ,
expectedArrivalFrom : dateParam ( range ? .[ 0 ]),
expectedArrivalTo : dateParam ( range ? .[ 1 ], true ),
})
if ( disposed || requestId !== loadRequestId ) return
result . value = { records : response . records , total : response . total }
} catch ( error ) {
if ( disposed || requestId !== loadRequestId ) return
loadError . value = getApiErrorDetails ( error , '入库单加载失败' )
} finally {
if ( ! disposed && requestId === loadRequestId ) loading . value = false
}
}
function applyFilters () {
if ( page . value === 1 ) void loadInbounds ()
else page . value = 1
}
function resetDetail () {
detailRequestId += 1
detail . value = null
detailTarget . value = null
detailError . value = null
detailLoading . value = false
}
async function openDetail ( row : InboundOrderSummary ) {
const requestId = ++ detailRequestId
detailOpen . value = true
detailTarget . value = row
detailLoading . value = true
detailError . value = null
detail . value = null
try {
2026-08-05 09:11:48 +08:00
const response = await getPlatformInbound ( row . tenantId , row . id )
2026-08-04 21:01:39 +08:00
if ( disposed || requestId !== detailRequestId ) return
detail . value = response
} catch ( error ) {
if ( disposed || requestId !== detailRequestId ) return
detailError . value = getApiErrorDetails ( error , '入库单详情加载失败' )
} finally {
if ( ! disposed && requestId === detailRequestId ) detailLoading . value = false
}
}
function closeDetail () { resetDetail () }
function openWorkspace ( row : InboundOrderSummary ) {
2026-08-05 09:11:48 +08:00
void router . push ({ name : 'inbound-order-detail' , params : { inboundId : row . id }, query : { tenantId : row . tenantId } })
2026-08-04 21:01:39 +08:00
}
function formatNumber ( value : number | null , digits = 3 ) { return value === null ? '—' : value . toFixed ( digits ) }
function formatDate ( value : string | null ) {
if ( ! value ) return '—'
const date = new Date ( value )
return Number . isNaN ( date . getTime ()) ? value : date . toLocaleString ( 'zh-CN' , { hour12 : false })
}
function text ( value : string | number | null | undefined ) { return value === null || value === undefined || value === '' ? '—' : String ( value ) }
function statusLabel ( status : string ) {
return ({ WAIT_MEASURE : '待测量' , MEASURING : '测量中' , MEASURE_PENDING : '待审核' , MEASURE_REJECTED : '测量驳回' , WAIT_PRICING : '待计费' , FEE_CONFIRMING : '待发送费用' , INBOUNDING : '入库处理中' , INBOUNDED : '已入库' , VOIDED : '已作废' } as Record < string , string >)[ status ] || status || '未知状态'
}
function statusType ( status : string ) {
if ( status === 'INBOUNDED' ) return 'success'
if ( status === 'MEASURE_REJECTED' || status === 'VOIDED' ) return 'danger'
if ( status === 'MEASURING' || status === 'INBOUNDING' ) return 'info'
return 'warning'
}
function codeLabel ( status : string | null | undefined , labels : Record < string , string >) { return status ? labels [ status ] || status : '—' }
const measureReviewLabels : Record < string , string > = { PENDING : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' }
const feeStatusLabels : Record < string , string > = { UNPRICED : '未计费' , CALCULATED : '已试算' , CONFIRMED : '已确认' , SENT : '已发送' , VOIDED : '已作废' }
const loadingStatusLabels : Record < string , string > = { NOT_LOADED : '未配载' , LOADING : '配载中' , LOADED : '已配载' }
const taxModeLabels : Record < string , string > = { TAX_INCLUDED : '包税' , TAX_EXCLUDED : '不包税' , DDP : '包税' , DDU : '不包税' }
const containerModeLabels : Record < string , string > = { FCL : '整柜' , LCL : '拼箱' }
function resetFilters () {
selectedCustomerId . value = ''
showAdvancedFilters . value = false
filters . value = { ... filters . value , customerId : '' , warehouseId : '' , deliveryWarehouseId : '' }
warehouseOptions . value = []
warehousesUnavailable . value = false
resetDetail ()
detailOpen . value = false
applyFilters ()
}
function toggleAdvancedFilters () {
if ( ! showAdvancedFilters . value ) {
showAdvancedFilters . value = true
return
}
showAdvancedFilters . value = false
filters . value = {
... filters . value ,
orderStatus : '' ,
feeReviewStatus : '' ,
customerName : '' ,
destinationCode : '' ,
holdFlag : '' ,
}
applyFilters ()
}
watch (() => filters . value . customerId , ( customerId ) => {
if ( customerId === selectedCustomerId . value ) return
selectedCustomerId . value = customerId
page . value = 1
tab . value = ''
filters . value = { ... filters . value , warehouseId : '' , deliveryWarehouseId : '' }
resetDetail ()
detailOpen . value = false
void loadWarehouseOptions ()
void loadInbounds ()
})
watch ( tab , () => { page . value = 1 ; void loadInbounds () })
watch ([ page , pageSize ], () => { void loadInbounds () })
onMounted (() => { void loadCustomers (); void loadInbounds () })
onBeforeUnmount (() => { disposed = true ; loadRequestId += 1 ; detailRequestId += 1 ; customerRequestId += 1 ; warehouseRequestId += 1 })
</ script >
< template >
< div class = "business-page inbound-orders-page" >
< QueryToolbar v-model = "filters" class="surface-panel inbound-query-toolbar" ariaLabel="入库单筛选" :fields="queryFields" :search-on-reset="false" @search="applyFilters" @reset="resetFilters" >
< template # actions >
< el-button :aria-expanded = "showAdvancedFilters" @click ="toggleAdvancedFilters" >
< SlidersHorizontal :size = "16" />
{{ showAdvancedFilters ? '收起筛选' : '更多筛选' }}
< ChevronUp v-if = "showAdvancedFilters" :size="15" />
< ChevronDown v-else :size = "15" />
</ el-button >
</ template >
</ QueryToolbar >
< el-alert v-if = "customerLoadError" type="warning" :closable="false" show-icon role="alert" >
< template # title >< div class = "api-error-alert-title" >< span >{{ customerLoadError . message }}</ span >< ApiErrorMeta :details = "customerLoadError" /></ div ></ template >
< el-button link type = "warning" :loading = "customersLoading" @click ="loadCustomers" > 重新加载客户列表 </ el -button >
</ el-alert >
< LoadErrorState v-if = "loadError" class="inbound-load-error" :message="loadError.message" title="入库单暂时无法加载" @retry="loadInbounds"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
< section v-else class = "surface-panel table-panel" :aria-busy = "loading" >
< header class = "table-toolbar" >
2026-08-04 21:11:56 +08:00
< div class = "table-tabs-row" >
< nav class = "inbound-tabs" role = "tablist" aria -label =" 入库单处理状态 " >
< button v-for = "(item, index) in tabs" :key="item.value || 'all'" type="button" role="tab" class="inbound-tab" :class="{ 'is-active': tab === item.value }" :aria-selected="tab === item.value" :tabindex="tab === item.value ? 0 : -1" :title="item.description" @click="tab = item.value" @keydown="handleTabKeydown($event, index)" >{{ item.label }}</ button >
</ nav >
</ div >
< div class = "table-action-row" >
2026-08-04 21:01:39 +08:00
< span class = "result-summary" > 共 {{ result . total }} 条 · 所属企业 {{ selectedCustomerName }}</ span >
2026-08-04 21:11:56 +08:00
< div class = "table-action-buttons" >
< el-button :loading = "loading" @click ="loadInbounds" >< RefreshCw :size = "16" /> 刷新 </ el-button >
</ div >
2026-08-04 21:01:39 +08:00
</ div >
</ header >
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "result.records" :columns = "columns" :loading = "loading" :total = "result.total" : page -sizes ="[ 20 , 50 , 100 ]" row -key =" id " :empty-text = "listEmptyText" aria -label =" 入库单列表 " >
< template # actions = "{ row }" >< div class = "row-actions" >< el-button link type = "primary" @click ="openWorkspace(row as InboundOrderSummary)" >< ClipboardCheck :size = "15" /> 处理 </ el-button >< el-button link @click ="openDetail(row as InboundOrderSummary)" >< Eye :size = "15" /> 查看 </ el-button ></ div ></ template >
< template # inbound = "{ row }" >< div class = "primary-cell" >< strong >{{ text (( row as InboundOrderSummary ). inboundCode ) }}</ strong >< span > 订单 {{ text (( row as InboundOrderSummary ). orderNo ) }}</ span ></ div ></ template >
< template # orderStatus = "{ row }" >< div class = "status-stack" >< el-tag : type = "statusType((row as InboundOrderSummary).processStatus)" effect = "light" >{{ statusLabel (( row as InboundOrderSummary ). processStatus ) }}</ el-tag >< span > 订单 : {{ text (( row as InboundOrderSummary ). orderStatusSnapshot ) }}</ span ></ div ></ template >
< template # measureReviewStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). measureReviewStatus , measureReviewLabels ) }}</ template >
< template # expectedArrivalAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). expectedArrivalAt ) }}</ template >
< template # feeStatus = "{ row }" >< div class = "status-stack" >< span >{{ codeLabel (( row as InboundOrderSummary ). feeStatus , feeStatusLabels ) }}</ span >< small v-if = "(row as InboundOrderSummary).feeReviewStatus" > 审核 : {{ text (( row as InboundOrderSummary ) .feeReviewStatus ) }}</ small ></ div ></ template >
< template # customerName = "{ row }" >{{ text (( row as InboundOrderSummary ). customerName ) }}</ template >
< template # warehouseName = "{ row }" >{{ text (( row as InboundOrderSummary ). warehouseName ) }}</ template >
< template # deliveryWarehouseName = "{ row }" >{{ text (( row as InboundOrderSummary ). deliveryWarehouseName ) }}</ template >
< template # loadingStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). loadingStatus , loadingStatusLabels ) }}</ template >
< template # holdFlag = "{ row }" >< el-tag : type = "(row as InboundOrderSummary).holdFlag ? 'danger' : 'info'" effect = "plain" >{{ ( row as InboundOrderSummary ). holdFlag ? '是' : '否' }}</ el-tag ></ template >
< template # currentHoldReason = "{ row }" >{{ text (( row as InboundOrderSummary ). currentHoldReason ) }}</ template >
< template # containerNo = "{ row }" >{{ text (( row as InboundOrderSummary ). containerNo ) }}</ template >
< template # cabinetNo = "{ row }" >{{ text (( row as InboundOrderSummary ). cabinetNo ) }}</ template >
< template # companyName = "{ row }" >{{ text (( row as InboundOrderSummary ). companyName ) }}</ template >
< template # businessType = "{ row }" >{{ text (( row as InboundOrderSummary ). businessType ) }}</ template >
< template # channelName = "{ row }" >< div class = "primary-cell" >< strong >{{ text (( row as InboundOrderSummary ). channelName ) }}</ strong >< span >{{ text (( row as InboundOrderSummary ). channelCode ) }}</ span ></ div ></ template >
< template # taxMode = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). taxMode , taxModeLabels ) }}</ template >
< template # containerMode = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). containerMode , containerModeLabels ) }}</ template >
< template # customsInspection = "{ row }" >{{ ( row as InboundOrderSummary ). customsInspection === null ? '—' : ( row as InboundOrderSummary ). customsInspection ? '是' : '否' }}</ template >
< template # sailingAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). sailingAt ) }}</ template >
< template # portArrivalAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). portArrivalAt ) }}</ template >
< template # totalPackageCount = "{ row }" >{{ text (( row as InboundOrderSummary ). totalPackageCount ) }}</ template >
< template # actualGrossWeightKg = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). actualGrossWeightKg ) }}</ template >
< template # chargeableWeightKg = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). chargeableWeightKg ) }}</ template >
< template # actualVolumeCbm = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). actualVolumeCbm ) }}</ template >
< template # chargeableVolumeCbm = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). chargeableVolumeCbm ) }}</ template >
< template # palletCount = "{ row }" >{{ text (( row as InboundOrderSummary ). palletCount ) }}</ template >
< template # locationCodes = "{ row }" >{{ text (( row as InboundOrderSummary ). locationCodes ) }}</ template >
< template # volumeWeightKg = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). volumeWeightKg ) }}</ template >
< template # actualNetWeightKg = "{ row }" >{{ formatNumber (( row as InboundOrderSummary ). actualNetWeightKg ) }}</ template >
< template # destinationType = "{ row }" >{{ text (( row as InboundOrderSummary ). destinationType ) }}</ template >
< template # destinationName = "{ row }" >< div class = "primary-cell" >< strong >{{ text (( row as InboundOrderSummary ). destinationName ) }}</ strong >< span >{{ text (( row as InboundOrderSummary ). destinationCode ) }}</ span ></ div ></ template >
< template # measureCompletedAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). measureCompletedAt ) }}</ template >
< template # inboundedAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). inboundedAt ) }}</ template >
< template # expressCompanyName = "{ row }" >{{ text (( row as InboundOrderSummary ). expressCompanyName ) }}</ template >
< template # expressTrackingNo = "{ row }" >{{ text (( row as InboundOrderSummary ). expressTrackingNo ) }}</ template >
< template # createdAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). createdAt ) }}</ template >
< template # empty >< el-empty :description = "listEmptyText" /></ template >
</ DataTable >
</ section >
< AppDrawer v-model = "detailOpen" :title="detail ? `入库单详情 · ${detail.summary.inboundCode || detail.summary.id}` : '入库单详情'" size="min(720px, 94vw)" :loading="detailLoading" :show-footer="false" destroy-on-close @closed="closeDetail" >
< section v-if = "detailError" class="detail-error" role="alert"><strong>入库单详情暂时无法加载</strong><p>{{ detailError.message }}</p><ApiErrorMeta :details="detailError" /><el-button type="primary" :disabled="!detailTarget" @click="detailTarget && openDetail(detailTarget)" > 重新加载 </ el -button ></ section >
< div v-else-if = "detail" class="detail-content"><InboundSummaryPanel :summary="detail.summary" /><InboundHistoryPanel :pallet-locations="detail.palletLocations" :files="detail.files" :internal-notes="detail.internalNotes" :status-history="detail.statusHistory" :public-remark="detail.publicRemark" /></ div >
</ AppDrawer >
</ div >
</ template >
< style scoped >
. inbound - orders - page { min - width : 0 ; }
2026-08-04 21:11:56 +08:00
. inbound - tabs { display : flex ; width : max - content ; max - width : 100 % ; min - width : 0 ; overflow - x : auto ; padding : 4 px ; border : 1 px solid var ( -- cc - border ); border - radius : 8 px ; background : # f7f9fc ; scrollbar - width : none ; }
2026-08-04 21:01:39 +08:00
. inbound - tabs ::- webkit - scrollbar { display : none ; }
. inbound - tab { min - height : 44 px ; flex : 0 0 auto ; padding : 8 px 18 px ; border : 1 px solid transparent ; border - radius : 6 px ; background : transparent ; color : var ( -- cc - muted ); cursor : pointer ; font - size : 13 px ; line - height : 1.4 ; white - space : nowrap ; transition : background - color .16 s ease , border - color .16 s ease , color .16 s ease , box - shadow .16 s ease ; }
. inbound - tab : hover { background : # fff ; color : var ( -- cc - primary ); }
. inbound - tab : focus - visible { outline : 3 px solid rgba ( 102 , 126 , 234 , .2 ); outline - offset : 1 px ; }
. inbound - tab . is - active { border - color : # d8e0f0 ; background : # fff ; color : var ( -- cc - primary ); box - shadow : 0 1 px 3 px rgba ( 16 , 24 , 40 , .08 ); font - weight : 650 ; }
. inbound - load - error { margin - top : 12 px ; }
. inbound - query - toolbar { border - radius : 8 px ; }
. row - actions { display : flex ; align - items : center ; gap : 2 px ; white - space : nowrap ; }
. row - actions : deep (. el - button ) { margin - left : 0 ; }
2026-08-04 21:11:56 +08:00
. table - toolbar { display : grid ; min - width : 0 ; border - bottom : 1 px solid var ( -- cc - border ); }
. table - tabs - row { min - width : 0 ; padding : 10 px 14 px ; }
. table - action - row { display : flex ; min - width : 0 ; min - height : 56 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 8 px 14 px ; border - top : 1 px solid var ( -- cc - border ); background : # fbfcfe ; }
. table - action - buttons { display : flex ; min - width : 0 ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
2026-08-04 21:01:39 +08:00
. result - summary { color : var ( -- cc - muted ); font - size : 12 px ; white - space : nowrap ; }
2026-08-04 21:11:56 +08:00
. table - action - buttons : deep (. el - button ) { min - height : 40 px ; margin - left : 0 ; }
2026-08-04 21:01:39 +08:00
. primary - cell , . status - stack { display : grid ; min - width : 0 ; justify - items : start ; gap : 4 px ; }
. primary - cell strong , . primary - cell span , . status - stack span , . status - stack small { overflow - wrap : anywhere ; }
. primary - cell span , . status - stack span , . status - stack small { color : var ( -- cc - muted ); font - size : 12 px ; }
. detail - content { display : grid ; gap : 24 px ; min - width : 0 ; }
. detail - error { display : flex ; min - height : 240 px ; align - items : flex - start ; justify - content : center ; flex - direction : column ; }
. detail - error strong { color : var ( -- cc - danger ); font - size : 15 px ; }
. detail - error p { margin : 7 px 0 ; color : var ( -- cc - text ); font - size : 13 px ; }
. detail - error : deep (. el - button ) { margin - top : 14 px ; }
2026-08-04 21:11:56 +08:00
@ media ( max - width : 900 px ) { . inbound - tabs { width : 100 % ; } }
@ media ( max - width : 720 px ) { . inbound - tab { padding - inline : 14 px ; } . table - tabs - row { padding : 10 px ; } . table - action - row { min - height : 0 ; align - items : stretch ; flex - direction : column ; gap : 10 px ; padding : 10 px ; } . table - action - buttons { justify - content : flex - start ; } . result - summary { min - width : 0 ; overflow : hidden ; text - overflow : ellipsis ; } . table - action - buttons : deep (. el - button ) { min - height : 44 px ; } }
2026-08-04 21:01:39 +08:00
</ style >