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'
2026-08-07 17:16:23 +08:00
import { ElMessage } from 'element-plus'
2026-08-30 17:43:03 +08:00
import { ChevronDown , ChevronUp , ClipboardCheck , Eye , Link2 , Printer , RefreshCw , SlidersHorizontal } from '@lucide/vue'
2026-08-12 12:35:32 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
2026-08-04 21:01:39 +08:00
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
2026-08-10 16:24:54 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-04 21:01:39 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 16:24:54 +08:00
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-04 21:01:39 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-30 18:21:12 +08:00
import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
2026-08-04 21:01:39 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
2026-08-11 19:35:28 +08:00
import { firstLegBusinessTypeLabel , firstLegContainerModeLabel , firstLegDestinationTypeLabel , firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
2026-08-04 21:01:39 +08:00
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
2026-08-12 16:31:46 +08:00
import { downloadInboundCartonLabels , listPlatformInbounds } from '../core/api'
import type { InboundListTab , InboundOrderSummary } from '../core/types'
2026-08-11 19:35:28 +08:00
import { inboundFeeStatusLabels , inboundLoadingStatusLabels , inboundOrderStatusLabels , inboundProcessStatusLabels , inboundReviewStatusLabels , inboundStatusLabel } from '../status'
2026-08-04 21:01:39 +08:00
type FilterModel = QueryModel & {
customerId : string
inboundCode : 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 ()
2026-08-12 12:35:32 +08:00
const access = useAccessPreviewStore ()
2026-08-04 21:01:39 +08:00
const tab = ref < InboundListTab | '' >( '' )
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
2026-08-07 17:16:23 +08:00
const printingInboundId = ref ( '' )
2026-08-30 18:21:12 +08:00
const relationshipDialogOpen = ref ( false )
const relationshipDialogInboundCode = ref ( '' )
2026-08-04 21:01:39 +08:00
const result = ref ({ records : [] as InboundOrderSummary [], total : 0 })
2026-08-14 19:59:32 +08:00
const filters = ref < FilterModel >({ customerId : '' , inboundCode : '' , warehouseId : '' , deliveryWarehouseId : '' , processStatus : '' , orderStatus : '' , measureReviewStatus : '' , customerName : '' , destinationCode : '' , feeStatus : '' , feeReviewStatus : '' , holdFlag : '' , expectedArrivalRange : [] })
2026-08-11 18:06:35 +08:00
const appliedFilters = ref < FilterModel >({ ... filters . value , expectedArrivalRange : [] })
2026-08-04 21:01:39 +08:00
const selectedCustomerId = ref ( '' )
2026-08-11 18:06:35 +08:00
const appliedCustomerId = ref ( '' )
2026-08-04 21:01:39 +08:00
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 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 : '等待费用编排完成' },
2026-08-14 14:27:03 +08:00
{ value : 'WAIT_SPLIT' , label : '待分柜' , description : '测量审核完成,等待分柜;费用可继续维护' },
2026-08-04 21:01:39 +08:00
]
const advancedFilterKeys = new Set ([ 'orderStatus' , 'feeReviewStatus' , 'customerName' , 'destinationCode' , 'holdFlag' ])
const allQueryFields = computed < QueryField [] >(() => [
{
key : 'customerId' ,
type : 'select' ,
2026-08-06 19:03:13 +08:00
label : '所属企业' ,
2026-08-04 21:01:39 +08:00
width : 260 ,
placeholder : customerLoadError . value ? '所属企业加载失败' : customersLoading . value ? '正在加载客户' : '全部客户' ,
options : customers . value . map (( customer ) => ({
value : String ( customer . id ),
2026-08-14 12:50:42 +08:00
label : customer . legalName ? . trim () || customer . tenantName ,
2026-08-04 21:01:39 +08:00
})),
props : { 'aria-label' : '所属企业' , loading : customersLoading . value , filterable : true , disabled : Boolean ( customerLoadError . value ) || customers . value . length === 0 },
},
2026-08-06 19:03:13 +08:00
{ key : 'warehouseId' , type : 'select' , width : 180 , label : '定位仓库' , 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 , label : '交货仓' , 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 , label : '入仓码' , placeholder : '入仓码' },
{ key : 'processStatus' , type : 'select' , width : 160 , label : '流程状态' , placeholder : '流程状态' , options : [
2026-08-04 21:01:39 +08:00
{ label : '待测量' , value : 'WAIT_MEASURE' }, { label : '测量中' , value : 'MEASURING' },
{ label : '待测量审核' , value : 'MEASURE_PENDING' }, { label : '测量已驳回' , value : 'MEASURE_REJECTED' },
{ label : '待计费' , value : 'WAIT_PRICING' }, { label : '费用处理中' , value : 'FEE_CONFIRMING' },
2026-08-13 14:55:59 +08:00
{ label : '入库处理中' , value : 'INBOUNDING' }, { label : '待分柜' , value : 'WAIT_SPLIT' }, { label : '已作废' , value : 'VOIDED' },
2026-08-04 21:01:39 +08:00
] },
2026-08-11 19:35:28 +08:00
{ key : 'orderStatus' , type : 'select' , width : 180 , label : '订单状态' , placeholder : '全部订单状态' , options : Object . entries ( inboundOrderStatusLabels ). map (([ value , label ]) => ({ value , label })) },
2026-08-06 19:03:13 +08:00
{ key : 'measureReviewStatus' , type : 'select' , width : 160 , label : '测量审核状态' , placeholder : '测量审核状态' , options : [
2026-08-04 21:01:39 +08:00
{ label : '待审核' , value : 'PENDING' }, { label : '审核通过' , value : 'APPROVED' }, { label : '审核驳回' , value : 'REJECTED' },
] },
2026-08-06 19:03:13 +08:00
{ key : 'feeStatus' , type : 'select' , width : 160 , label : '费用状态' , placeholder : '费用状态' , options : [
2026-08-04 21:01:39 +08:00
{ label : '未计费' , value : 'UNPRICED' }, { label : '已试算' , value : 'CALCULATED' },
{ label : '已确认' , value : 'CONFIRMED' }, { label : '已发送' , value : 'SENT' }, { label : '已作废' , value : 'VOIDED' },
] },
2026-08-11 19:35:28 +08:00
{ key : 'feeReviewStatus' , type : 'select' , width : 180 , label : '费用审核状态' , placeholder : '全部审核状态' , options : Object . entries ( inboundReviewStatusLabels ). map (([ value , label ]) => ({ value , label })) },
2026-08-06 19:03:13 +08:00
{ key : 'customerName' , type : 'input' , width : 260 , label : '客户名称' , placeholder : '客户名称' },
{ key : 'destinationCode' , type : 'input' , width : 220 , label : '目的仓编码' , placeholder : '目的仓编码' },
{ key : 'holdFlag' , type : 'select' , width : 160 , label : '挂起状态' , placeholder : '挂起状态' , options : [{ label : '已挂起' , value : 'true' }, { label : '未挂起' , value : 'false' }] },
{ key : 'expectedArrivalRange' , type : 'daterange' , width : 320 , label : '预计到达日期' , placeholder : '预计到达日期' },
2026-08-04 21:01:39 +08:00
])
const queryFields = computed (() => showAdvancedFilters . value
? allQueryFields . value
: allQueryFields . value . filter (( field ) => ! advancedFilterKeys . has ( field . key )))
const columns : DataTableColumn [] = [
2026-08-07 17:16:23 +08:00
{ key : 'actions' , label : '操作' , width : 228 , fixed : 'left' , slotName : 'actions' },
2026-08-04 21:01:39 +08:00
{ key : 'inboundCode' , label : '入仓码' , minWidth : 166 , slotName : 'inbound' },
2026-08-12 10:59:49 +08:00
{ key : 'processStatus' , label : '入库流程状态' , minWidth : 132 , slotName : 'processStatus' },
{ key : 'orderStatus' , label : '订单状态' , minWidth : 120 , slotName : 'orderStatus' },
2026-08-04 21:01:39 +08:00
{ key : 'measureReviewStatus' , label : '测量数据审核状态' , minWidth : 142 , slotName : 'measureReviewStatus' },
{ key : 'expectedArrivalAt' , label : '预计到达时间' , minWidth : 164 , slotName : 'expectedArrivalAt' },
2026-08-12 10:59:49 +08:00
{ key : 'feeStatus' , label : '费用状态' , minWidth : 112 , slotName : 'feeStatus' },
{ key : 'feeReviewStatus' , label : '费用审核状态' , minWidth : 132 , slotName : 'feeReviewStatus' },
2026-08-04 21:01:39 +08:00
{ key : 'customerName' , label : '客户' , minWidth : 142 , slotName : 'customerName' },
{ key : 'warehouseName' , label : '仓库' , minWidth : 140 , slotName : 'warehouseName' },
{ key : 'deliveryWarehouseName' , label : '交货仓' , minWidth : 140 , slotName : 'deliveryWarehouseName' },
2026-08-12 12:35:32 +08:00
{ key : 'loadingStatus' , label : '配载状态' , minWidth : 110 , slotName : 'loadingStatus' , defaultVisible : false },
{ key : 'holdFlag' , label : '是否扣货' , width : 96 , slotName : 'holdFlag' , defaultVisible : false },
{ key : 'currentHoldReason' , label : '扣货理由' , minWidth : 150 , slotName : 'currentHoldReason' , defaultVisible : false },
{ key : 'containerNo' , label : '集装箱号' , minWidth : 132 , slotName : 'containerNo' , defaultVisible : false },
{ key : 'cabinetNo' , label : '柜号' , minWidth : 118 , slotName : 'cabinetNo' , defaultVisible : false },
{ key : 'companyName' , label : '公司' , minWidth : 160 , slotName : 'companyName' , defaultVisible : false },
{ key : 'businessType' , label : '业务类型' , minWidth : 112 , slotName : 'businessType' , defaultVisible : false },
{ key : 'channelName' , label : '渠道' , minWidth : 138 , slotName : 'channelName' , defaultVisible : false },
{ key : 'taxMode' , label : '买单/退税' , minWidth : 108 , slotName : 'taxMode' , defaultVisible : false },
{ key : 'containerMode' , label : '是否整柜' , minWidth : 108 , slotName : 'containerMode' , defaultVisible : false },
{ key : 'customsInspection' , label : '商检验' , width : 92 , slotName : 'customsInspection' , defaultVisible : false },
{ key : 'sailingAt' , label : '开船时间' , minWidth : 164 , slotName : 'sailingAt' , defaultVisible : false },
{ key : 'portArrivalAt' , label : '到港时间' , minWidth : 164 , slotName : 'portArrivalAt' , defaultVisible : false },
{ key : 'totalPackageCount' , label : '总箱数' , width : 90 , slotName : 'totalPackageCount' , defaultVisible : false },
{ key : 'actualGrossWeightKg' , label : '总毛重(T.K.GW)' , minWidth : 136 , slotName : 'actualGrossWeightKg' , defaultVisible : false },
{ key : 'chargeableWeightKg' , label : '计费毛重' , minWidth : 112 , slotName : 'chargeableWeightKg' , defaultVisible : false },
{ key : 'actualVolumeCbm' , label : '总体积(m³)' , minWidth : 118 , slotName : 'actualVolumeCbm' , defaultVisible : false },
{ key : 'chargeableVolumeCbm' , label : '计费体积(m³)' , minWidth : 128 , slotName : 'chargeableVolumeCbm' , defaultVisible : false },
{ key : 'palletCount' , label : '总托盘数' , width : 100 , slotName : 'palletCount' , defaultVisible : false },
{ key : 'locationCodes' , label : '库位位置' , minWidth : 124 , slotName : 'locationCodes' , defaultVisible : false },
{ key : 'volumeWeightKg' , label : '总材积重' , minWidth : 110 , slotName : 'volumeWeightKg' , defaultVisible : false },
{ key : 'actualNetWeightKg' , label : '总净重(T.K.NW)' , minWidth : 136 , slotName : 'actualNetWeightKg' , defaultVisible : false },
{ key : 'destinationType' , label : '目的仓类型' , minWidth : 112 , slotName : 'destinationType' , defaultVisible : false },
{ key : 'destinationName' , label : '目的仓' , minWidth : 132 , slotName : 'destinationName' , defaultVisible : false },
{ key : 'measureCompletedAt' , label : '测量完成时间' , minWidth : 164 , slotName : 'measureCompletedAt' , defaultVisible : false },
{ key : 'inboundedAt' , label : '入库完成时间' , minWidth : 164 , slotName : 'inboundedAt' , defaultVisible : false },
{ key : 'expressCompanyName' , label : '快递公司' , minWidth : 120 , slotName : 'expressCompanyName' , defaultVisible : false },
{ key : 'expressTrackingNo' , label : '快递单号' , minWidth : 150 , slotName : 'expressTrackingNo' , defaultVisible : false },
{ key : 'createdAt' , label : '创建日期' , minWidth : 164 , slotName : 'createdAt' , defaultVisible : false },
2026-08-04 21:01:39 +08:00
]
const selectedCustomerName = computed (() => {
2026-08-11 18:06:35 +08:00
const customer = customers . value . find (( item ) => String ( item . id ) === appliedCustomerId . value )
2026-08-04 21:01:39 +08:00
return customer ? . legalName ? . trim () || customer ? . tenantName ? . trim () || '全部客户'
})
const listEmptyText = '暂无符合条件的入库单'
2026-08-06 13:56:04 +08:00
const currentPageCount = computed (() => result . value . records . length )
const aggregateStats = computed (() => {
const records = result . value . records
return {
packages : records . reduce (( total , item ) => total + ( item . totalPackageCount || 0 ), 0 ),
grossWeight : records . reduce (( total , item ) => total + ( item . actualGrossWeightKg || 0 ), 0 ),
chargeableWeight : records . reduce (( total , item ) => total + ( item . chargeableWeightKg || 0 ), 0 ),
volume : records . reduce (( total , item ) => total + ( item . actualVolumeCbm || 0 ), 0 ),
}
})
2026-08-04 21:01:39 +08:00
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 {
2026-08-06 13:56:04 +08:00
const response = await listPlatformWarehouses ( 1 , 200 )
2026-08-04 21:01:39 +08:00
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 {
2026-08-14 12:50:42 +08:00
const response = await listPlatformTenants ({ page : currentPage , size : 200 , status : 'ACTIVE' })
2026-08-04 21:01:39 +08:00
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
2026-08-11 18:06:35 +08:00
const applied = appliedFilters . value
const range = applied . expectedArrivalRange
2026-08-04 21:01:39 +08:00
try {
2026-08-11 18:06:35 +08:00
const response = await listPlatformInbounds ( appliedCustomerId . value || undefined , {
2026-08-04 21:01:39 +08:00
page : page . value ,
size : pageSize . value ,
tab : tab . value || undefined ,
2026-08-11 18:06:35 +08:00
inboundCode : queryValue ( applied . inboundCode ),
warehouseId : queryValue ( applied . warehouseId ),
deliveryWarehouseId : queryValue ( applied . deliveryWarehouseId ),
processStatus : queryValue ( applied . processStatus ),
orderStatus : queryValue ( applied . orderStatus ),
measureReviewStatus : queryValue ( applied . measureReviewStatus ),
customerName : queryValue ( applied . customerName ),
destinationCode : queryValue ( applied . destinationCode ),
feeStatus : queryValue ( applied . feeStatus ),
feeReviewStatus : queryValue ( applied . feeReviewStatus ),
holdFlag : applied . holdFlag === '' ? undefined : applied . holdFlag === 'true' ,
2026-08-04 21:01:39 +08:00
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 () {
2026-08-11 18:06:35 +08:00
appliedFilters . value = { ... filters . value , expectedArrivalRange : [... filters . value . expectedArrivalRange ] }
appliedCustomerId . value = selectedCustomerId . value
2026-08-04 21:01:39 +08:00
if ( page . value === 1 ) void loadInbounds ()
else page . value = 1
}
2026-08-12 16:31:46 +08:00
function openDetail ( row : InboundOrderSummary ) {
2026-08-13 20:00:38 +08:00
if ( ! row . orderId ) {
ElMessage . warning ( '该入库单尚未关联订单,暂时无法查看订单详情' )
return
}
void router . push ({ name : 'unified-order-detail' , params : { orderId : row . orderId }, query : { tenantId : row . tenantId } })
2026-08-04 21:01:39 +08:00
}
2026-08-30 18:21:12 +08:00
function openRelationship ( row : InboundOrderSummary ) { const code = row . inboundCode ? . trim (); if ( ! code ) return ; relationshipDialogInboundCode . value = code ; relationshipDialogOpen . value = true }
2026-08-07 17:16:23 +08:00
async function printCartonLabels ( row : InboundOrderSummary ) {
if ( printingInboundId . value ) return
printingInboundId . value = row . id
try {
const blob = await downloadInboundCartonLabels ( row . tenantId , row . id )
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url
2026-08-30 17:43:03 +08:00
anchor . download = ` ${ row . inboundCode || '入仓码待补充' } - ${ row . totalPackageCount || '整单' } 件-箱唛.pdf`
2026-08-07 17:16:23 +08:00
anchor . click ()
URL . revokeObjectURL ( url )
ElMessage . success ( '箱唛已生成,重复打印将继续使用原箱号' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '箱唛生成失败' ). message )
} finally {
printingInboundId . value = ''
}
}
2026-08-04 21:01:39 +08:00
function formatNumber ( value : number | null , digits = 3 ) { return value === null ? '—' : value . toFixed ( digits ) }
2026-08-06 13:56:04 +08:00
function formatAggregate ( value : number , digits = 3 ) {
if ( ! Number . isFinite ( value )) return '0'
return value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits })
}
2026-08-04 21:01:39 +08:00
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 ) {
2026-08-11 19:35:28 +08:00
return inboundStatusLabel ( status , inboundProcessStatusLabels , '未知状态' )
2026-08-04 21:01:39 +08:00
}
function statusType ( status : string ) {
2026-08-13 14:55:59 +08:00
if ( status === 'WAIT_SPLIT' ) return 'success'
2026-08-04 21:01:39 +08:00
if ( status === 'MEASURE_REJECTED' || status === 'VOIDED' ) return 'danger'
if ( status === 'MEASURING' || status === 'INBOUNDING' ) return 'info'
return 'warning'
}
2026-08-11 19:35:28 +08:00
function codeLabel ( status : string | null | undefined , labels : Record < string , string >) { return inboundStatusLabel ( status , labels ) }
const measureReviewLabels = inboundReviewStatusLabels
const feeStatusLabels = inboundFeeStatusLabels
const loadingStatusLabels = inboundLoadingStatusLabels
2026-08-04 21:01:39 +08:00
function resetFilters () {
selectedCustomerId . value = ''
showAdvancedFilters . value = false
filters . value = { ... filters . value , customerId : '' , warehouseId : '' , deliveryWarehouseId : '' }
warehouseOptions . value = []
warehousesUnavailable . value = false
applyFilters ()
}
function toggleAdvancedFilters () {
if ( ! showAdvancedFilters . value ) {
showAdvancedFilters . value = true
return
}
showAdvancedFilters . value = false
filters . value = {
... filters . value ,
orderStatus : '' ,
feeReviewStatus : '' ,
customerName : '' ,
destinationCode : '' ,
holdFlag : '' ,
}
}
watch (() => filters . value . customerId , ( customerId ) => {
if ( customerId === selectedCustomerId . value ) return
selectedCustomerId . value = customerId
filters . value = { ... filters . value , warehouseId : '' , deliveryWarehouseId : '' }
void loadWarehouseOptions ()
})
2026-08-10 18:12:23 +08:00
watch ( tab , () => { if ( page . value === 1 ) void loadInbounds (); else page . value = 1 })
2026-08-04 21:01:39 +08:00
watch ([ page , pageSize ], () => { void loadInbounds () })
onMounted (() => { void loadCustomers (); void loadInbounds () })
2026-08-12 16:31:46 +08:00
onBeforeUnmount (() => { disposed = true ; loadRequestId += 1 ; customerRequestId += 1 ; warehouseRequestId += 1 })
2026-08-04 21:01:39 +08:00
</ script >
< template >
2026-08-10 16:24:54 +08:00
< AdminPage as = "div" class = "business-page inbound-orders-page" >
< ListPageLayout ariaLabel = "入库单管理" >
< template # toolbar >
< QueryToolbar v-model = "filters" class="surface-panel inbound-query-toolbar" ariaLabel="入库单筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="inbound-orders-query" :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 >
2026-08-04 21:01:39 +08:00
</ template >
2026-08-10 16:24:54 +08:00
< template v-if = "customerLoadError || loadError" # state >
< div class = "inbound-state-stack" >
< 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" >< RefreshCw :size = "15" /> 重新加载客户列表 </ el-button >
</ el-alert >
< LoadErrorState v-if = "loadError" :message="loadError.message" title="入库单暂时无法加载" @retry="loadInbounds"><template #details><ApiErrorMeta :details="loadError" /></ template ></ LoadErrorState >
</ div >
</ template >
2026-08-04 21:01:39 +08:00
2026-08-10 16:24:54 +08:00
< section v-if = "!loadError" class="surface-panel table-panel" :aria-busy="loading" >
2026-08-04 21:01:39 +08:00
< 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 >
2026-08-06 13:56:04 +08:00
< div class = "inbound-stats-row" aria -label =" 入库单统计 " >
< span class = "total-info" >
< b > 总箱数 {{ formatAggregate ( aggregateStats . packages , 0 ) }}</ b >
< i aria -hidden =" true " />
< b > 总毛重 {{ formatAggregate ( aggregateStats . grossWeight ) }} kg </ b >
< i aria -hidden =" true " />
< b > 计费重量 {{ formatAggregate ( aggregateStats . chargeableWeight ) }} kg </ b >
< i aria -hidden =" true " />
< b > 总体积 {{ formatAggregate ( aggregateStats . volume ) }} m³ </ b >
</ span >
</ div >
2026-08-04 21:01:39 +08:00
</ header >
2026-08-06 19:03:13 +08:00
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "result.records" :columns = "columns" preference -key =" inbound -orders-table " :loading = "loading" :total = "result.total" : page -sizes ="[ 20 , 50 , 100 ]" row -key =" id " :empty-text = "listEmptyText" aria -label =" 入库单列表 " >
2026-08-10 16:24:54 +08:00
< template # pagination -left > 当前显示 {{ currentPageCount }} 条 · 共 {{ result . total }} 条 </ template >
2026-08-13 20:00:38 +08:00
< template # actions = "{ row }" >< div v-if = "access.can('platform:firstleg:inbound:view')" class="row-actions"><el-button size="small" type="primary" :disabled="!(row as InboundOrderSummary).orderId" @click="openDetail(row as InboundOrderSummary)"><ClipboardCheck :size="15" />处理</el-button><el-button size="small" plain type="primary" :loading="printingInboundId === (row as InboundOrderSummary).id" :disabled="Boolean(printingInboundId)" @click="printCartonLabels(row as InboundOrderSummary)"><Printer :size="15" />打印箱唛</el-button><el-button size="small" plain type="primary" :disabled="!(row as InboundOrderSummary).orderId" @click="openDetail(row as InboundOrderSummary)"><Eye :size="15" />查看</el-button></div><span v-else class="muted-action" > 无查看权限 </ span ></ template >
2026-08-30 17:43:03 +08:00
< template # inbound = "{ row }" >< div class = "inbound-code-cell" >< strong >{{ text (( row as InboundOrderSummary ). inboundCode ) }}</ strong >< el-button v-if = "(row as InboundOrderSummary).inboundCode" link type="primary" @click="openRelationship(row as InboundOrderSummary)"><Link2 :size="14" /> 关联数据 </ el-button ></ div ></ template >
2026-08-12 10:59:49 +08:00
< template # processStatus = "{ row }" >< el-tag : type = "statusType((row as InboundOrderSummary).processStatus)" effect = "light" >{{ statusLabel (( row as InboundOrderSummary ). processStatus ) }}</ el-tag ></ template >
< template # orderStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). orderStatusSnapshot , inboundOrderStatusLabels ) }}</ template >
2026-08-04 21:01:39 +08:00
< template # measureReviewStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). measureReviewStatus , measureReviewLabels ) }}</ template >
< template # expectedArrivalAt = "{ row }" >{{ formatDate (( row as InboundOrderSummary ). expectedArrivalAt ) }}</ template >
2026-08-12 10:59:49 +08:00
< template # feeStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). feeStatus , feeStatusLabels ) }}</ template >
< template # feeReviewStatus = "{ row }" >{{ codeLabel (( row as InboundOrderSummary ). feeReviewStatus , inboundReviewStatusLabels ) }}</ template >
2026-08-04 21:01:39 +08:00
< 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 >
2026-08-11 19:35:28 +08:00
< template # businessType = "{ row }" >{{ firstLegBusinessTypeLabel (( row as InboundOrderSummary ). businessType ) }}</ template >
2026-08-04 21:01:39 +08:00
< template # channelName = "{ row }" >< div class = "primary-cell" >< strong >{{ text (( row as InboundOrderSummary ). channelName ) }}</ strong >< span >{{ text (( row as InboundOrderSummary ). channelCode ) }}</ span ></ div ></ template >
2026-08-11 19:35:28 +08:00
< template # taxMode = "{ row }" >{{ firstLegTaxModeLabel (( row as InboundOrderSummary ). taxMode ) }}</ template >
< template # containerMode = "{ row }" >{{ firstLegContainerModeLabel (( row as InboundOrderSummary ). containerMode ) }}</ template >
2026-08-04 21:01:39 +08:00
< 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 >
2026-08-11 19:35:28 +08:00
< template # destinationType = "{ row }" >{{ firstLegDestinationTypeLabel (( row as InboundOrderSummary ). destinationType ) }}</ template >
2026-08-04 21:01:39 +08:00
< 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 >
2026-08-10 16:24:54 +08:00
</ section >
</ ListPageLayout >
2026-08-30 18:21:12 +08:00
< InboundCodeRelationshipDialog v-if = "relationshipDialogOpen" v-model="relationshipDialogOpen" :inbound-code="relationshipDialogInboundCode" :can-read-base="access.can('platform:order:list')" :can-read-loading="access.can('platform:firstleg:smart-loading:list')" :can-read-overseas="access.can('platform:overseas-warehouse:inventory:list')" />
2026-08-10 16:24:54 +08:00
</ AdminPage >
2026-08-04 21:01:39 +08:00
</ template >
< style scoped >
. inbound - orders - page { min - width : 0 ; }
2026-08-30 17:43:03 +08:00
. inbound - code - cell { display : grid ; gap : 3 px ; min - width : 0 ; }. inbound - code - cell . el - button { width : fit - content ; margin : 0 ; padding : 0 ; }
2026-08-06 13:56:04 +08:00
. inbound - query - toolbar { align - items : stretch ; border - radius : 8 px ; background : # f8fafc ; }
. inbound - tabs { display : flex ; width : max - content ; max - width : 100 % ; min - width : 0 ; overflow - x : auto ; border - bottom : 2 px solid var ( -- cc - border ); scrollbar - width : none ; }
2026-08-04 21:01:39 +08:00
. inbound - tabs ::- webkit - scrollbar { display : none ; }
2026-08-06 13:56:04 +08:00
. inbound - tab { min - height : 42 px ; flex : 0 0 auto ; padding : 10 px 22 px ; border : 0 ; border - bottom : 2 px solid transparent ; margin - bottom : - 2 px ; background : transparent ; color : # 475467 ; cursor : pointer ; font - size : 13 px ; line - height : 1.4 ; white - space : nowrap ; transition : border - color .16 s ease , color .16 s ease , background - color .16 s ease ; }
. inbound - tab : hover { color : var ( -- cc - primary ); background : # f8fbff ; }
2026-08-04 21:01:39 +08:00
. inbound - tab : focus - visible { outline : 3 px solid rgba ( 102 , 126 , 234 , .2 ); outline - offset : 1 px ; }
2026-08-06 13:56:04 +08:00
. inbound - tab . is - active { border - bottom - color : var ( -- cc - primary ); background : # fff ; color : var ( -- cc - primary ); font - weight : 650 ; }
2026-08-10 16:24:54 +08:00
. inbound - state - stack { display : grid ; min - width : 0 ; gap : 12 px ; }
2026-08-04 21:01:39 +08:00
. 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 ); }
2026-08-06 13:56:04 +08:00
. table - tabs - row { min - width : 0 ; padding : 0 14 px ; background : # fff ; }
. inbound - stats - row { display : flex ; min - width : 0 ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 10 px 14 px ; background : var ( -- cc - primary ); color : # fff ; }
. inbound - stats - row . selected - info , . inbound - stats - row . total - info { min - width : 0 ; font - size : 12 px ; font - weight : 650 ; line - height : 1.5 ; }
. inbound - stats - row . total - info { display : flex ; align - items : center ; flex - wrap : wrap ; justify - content : flex - end ; gap : 8 px ; }
. inbound - stats - row b { font - weight : 650 ; }
. inbound - stats - row i { width : 1 px ; height : 12 px ; background : rgba ( 255 , 255 , 255 , .38 ); }
. table - panel : deep (. el - table th . el - table__cell ) { border - right : 1 px solid # edf0f4 ; }
. table - panel : deep (. el - table td . el - table__cell ) { border - right : 1 px solid # edf0f4 ; }
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 ; }
2026-08-10 15:36:53 +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 : 0 10 px ; } . inbound - stats - row { min - height : 0 ; align - items : stretch ; flex - direction : column ; gap : 10 px ; padding : 10 px ; } . inbound - stats - row . total - info { justify - content : flex - start ; } }
2026-08-12 18:00:26 +08:00
@ media ( max - width : 360 px ) { . inbound - tab { padding - inline : 12 px ; } . row - actions { align - items : stretch ; flex - direction : column ; } . row - actions : deep (. el - button ) { width : 100 % ; min - height : 40 px ; } }
2026-08-04 21:01:39 +08:00
</ style >