Files
admin-web/src/modules/inbounds/pages/InboundOrdersView.vue
T

497 lines
34 KiB
Vue

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { ChevronDown, ChevronUp, ClipboardCheck, Eye, Link2, Printer, RefreshCw, SlidersHorizontal } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import InboundCodeRelationshipDialog from '@/shared/components/InboundCodeRelationshipDialog.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { firstLegBusinessTypeLabel, firstLegContainerModeLabel, firstLegDestinationTypeLabel, firstLegTaxModeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listPlatformWarehouses } from '@/modules/warehouses'
import { downloadInboundCartonLabels, listPlatformInbounds } from '../core/api'
import type { InboundListTab, InboundOrderSummary } from '../core/types'
import { inboundFeeStatusLabels, inboundLoadingStatusLabels, inboundOrderStatusLabels, inboundProcessStatusLabels, inboundReviewStatusLabels, inboundStatusLabel } from '../status'
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()
const access = useAccessPreviewStore()
const tab = ref<InboundListTab | ''>('')
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const printingInboundId = ref('')
const relationshipDialogOpen = ref(false)
const relationshipDialogInboundCode = ref('')
const result = ref({ records: [] as InboundOrderSummary[], total: 0 })
const filters = ref<FilterModel>({ customerId: '', inboundCode: '', warehouseId: '', deliveryWarehouseId: '', processStatus: '', orderStatus: '', measureReviewStatus: '', customerName: '', destinationCode: '', feeStatus: '', feeReviewStatus: '', holdFlag: '', expectedArrivalRange: [] })
const appliedFilters = ref<FilterModel>({ ...filters.value, expectedArrivalRange: [] })
const selectedCustomerId = ref('')
const appliedCustomerId = 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 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: 'WAIT_SPLIT', label: '待分柜', description: '测量审核完成,等待分柜;费用可继续维护' },
]
const advancedFilterKeys = new Set(['orderStatus', 'feeReviewStatus', 'customerName', 'destinationCode', 'holdFlag'])
const allQueryFields = computed<QueryField[]>(() => [
{
key: 'customerId',
type: 'select',
label: '所属企业',
width: 260,
placeholder: customerLoadError.value ? '所属企业加载失败' : customersLoading.value ? '正在加载客户' : '全部客户',
options: customers.value.map((customer) => ({
value: String(customer.id),
label: customer.legalName?.trim() || customer.tenantName,
})),
props: { 'aria-label': '所属企业', loading: customersLoading.value, filterable: true, disabled: Boolean(customerLoadError.value) || customers.value.length === 0 },
},
{ 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: [
{ 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: 'WAIT_SPLIT' }, { label: '已作废', value: 'VOIDED' },
] },
{ key: 'orderStatus', type: 'select', width: 180, label: '订单状态', placeholder: '全部订单状态', options: Object.entries(inboundOrderStatusLabels).map(([value, label]) => ({ value, label })) },
{ key: 'measureReviewStatus', type: 'select', width: 160, label: '测量审核状态', placeholder: '测量审核状态', options: [
{ label: '待审核', value: 'PENDING' }, { label: '审核通过', value: 'APPROVED' }, { label: '审核驳回', value: 'REJECTED' },
] },
{ key: 'feeStatus', type: 'select', width: 160, label: '费用状态', placeholder: '费用状态', options: [
{ label: '未计费', value: 'UNPRICED' }, { label: '已试算', value: 'CALCULATED' },
{ label: '已确认', value: 'CONFIRMED' }, { label: '已发送', value: 'SENT' }, { label: '已作废', value: 'VOIDED' },
] },
{ key: 'feeReviewStatus', type: 'select', width: 180, label: '费用审核状态', placeholder: '全部审核状态', options: Object.entries(inboundReviewStatusLabels).map(([value, label]) => ({ value, label })) },
{ 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: '预计到达日期' },
])
const queryFields = computed(() => showAdvancedFilters.value
? allQueryFields.value
: allQueryFields.value.filter((field) => !advancedFilterKeys.has(field.key)))
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 228, fixed: 'left', slotName: 'actions' },
{ key: 'inboundCode', label: '入仓码', minWidth: 166, slotName: 'inbound' },
{ key: 'processStatus', label: '入库流程状态', minWidth: 132, slotName: 'processStatus', afterColumn: 'inboundCode' },
{ key: 'orderStatus', label: '订单状态', minWidth: 120, slotName: 'orderStatus', afterColumn: 'inboundCode' },
{ key: 'measureReviewStatus', label: '测量数据审核状态', minWidth: 142, slotName: 'measureReviewStatus', afterColumn: 'inboundCode' },
{ key: 'feeStatus', label: '费用状态', minWidth: 112, slotName: 'feeStatus', afterColumn: 'inboundCode' },
{ key: 'feeReviewStatus', label: '费用审核状态', minWidth: 132, slotName: 'feeReviewStatus', afterColumn: 'inboundCode' },
{ key: 'loadingStatus', label: '配载状态', minWidth: 110, slotName: 'loadingStatus', defaultVisible: false, afterColumn: 'inboundCode' },
{ key: 'holdFlag', label: '是否扣货', width: 96, slotName: 'holdFlag', defaultVisible: false, afterColumn: 'inboundCode' },
{ key: 'expectedArrivalAt', label: '预计到达时间', minWidth: 164, slotName: 'expectedArrivalAt' },
{ key: 'customerName', label: '客户', minWidth: 142, slotName: 'customerName' },
{ key: 'warehouseName', label: '仓库', minWidth: 140, slotName: 'warehouseName' },
{ key: 'deliveryWarehouseName', label: '交货仓', minWidth: 140, slotName: 'deliveryWarehouseName' },
{ 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 },
]
const selectedCustomerName = computed(() => {
const customer = customers.value.find((item) => String(item.id) === appliedCustomerId.value)
return customer?.legalName?.trim() || customer?.tenantName?.trim() || '全部客户'
})
const listEmptyText = '暂无符合条件的入库单'
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),
}
})
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(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, status: 'ACTIVE' })
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 applied = appliedFilters.value
const range = applied.expectedArrivalRange
try {
const response = await listPlatformInbounds(appliedCustomerId.value || undefined, {
page: page.value,
size: pageSize.value,
tab: tab.value || undefined,
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',
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() {
appliedFilters.value = { ...filters.value, expectedArrivalRange: [...filters.value.expectedArrivalRange] }
appliedCustomerId.value = selectedCustomerId.value
if (page.value === 1) void loadInbounds()
else page.value = 1
}
function openDetail(row: InboundOrderSummary) {
if (!row.orderId) {
ElMessage.warning('该入库单尚未关联订单,暂时无法查看订单详情')
return
}
void router.push({ name: 'unified-order-detail', params: { orderId: row.orderId }, query: { tenantId: row.tenantId } })
}
function openRelationship(row: InboundOrderSummary) { const code = row.inboundCode?.trim(); if (!code) return; relationshipDialogInboundCode.value = code; relationshipDialogOpen.value = true }
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
anchor.download = `${row.inboundCode || '入仓码待补充'}-${row.totalPackageCount || '整单'}件-箱唛.pdf`
anchor.click()
URL.revokeObjectURL(url)
ElMessage.success('箱唛已生成,重复打印将继续使用原箱号')
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '箱唛生成失败').message)
} finally {
printingInboundId.value = ''
}
}
function formatNumber(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
function formatAggregate(value: number, digits = 3) {
if (!Number.isFinite(value)) return '0'
return value.toLocaleString('zh-CN', { maximumFractionDigits: 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 inboundStatusLabel(status, inboundProcessStatusLabels, '未知状态')
}
function statusType(status: string) {
if (status === 'WAIT_SPLIT') 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 inboundStatusLabel(status, labels) }
const measureReviewLabels = inboundReviewStatusLabels
const feeStatusLabels = inboundFeeStatusLabels
const loadingStatusLabels = inboundLoadingStatusLabels
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()
})
watch(tab, () => { if (page.value === 1) void loadInbounds(); else page.value = 1 })
watch([page, pageSize], () => { void loadInbounds() })
onMounted(() => { void loadCustomers(); void loadInbounds() })
onBeforeUnmount(() => { disposed = true; loadRequestId += 1; customerRequestId += 1; warehouseRequestId += 1 })
</script>
<template>
<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>
</template>
<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>
<section v-if="!loadError" class="surface-panel table-panel" :aria-busy="loading">
<header class="table-toolbar">
<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="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>
</header>
<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="入库单列表">
<template #pagination-left>当前显示 {{ currentPageCount }} 条 · 共 {{ result.total }} 条</template>
<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>
<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>
<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>
<template #measureReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).measureReviewStatus, measureReviewLabels) }}</template>
<template #expectedArrivalAt="{ row }">{{ formatDate((row as InboundOrderSummary).expectedArrivalAt) }}</template>
<template #feeStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).feeStatus, feeStatusLabels) }}</template>
<template #feeReviewStatus="{ row }">{{ codeLabel((row as InboundOrderSummary).feeReviewStatus, inboundReviewStatusLabels) }}</template>
<template #customerName="{ row }"><LineClampText :content="(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 }">{{ firstLegBusinessTypeLabel((row as InboundOrderSummary).businessType) }}</template>
<template #channelName="{ row }"><div class="primary-cell"><LineClampText :content="(row as InboundOrderSummary).channelName" /><span>{{ text((row as InboundOrderSummary).channelCode) }}</span></div></template>
<template #taxMode="{ row }">{{ firstLegTaxModeLabel((row as InboundOrderSummary).taxMode) }}</template>
<template #containerMode="{ row }">{{ firstLegContainerModeLabel((row as InboundOrderSummary).containerMode) }}</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 }">{{ firstLegDestinationTypeLabel((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>
</ListPageLayout>
<InboundCodeRelationshipDialog dense 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')" />
</AdminPage>
</template>
<style scoped>
.inbound-orders-page { min-width: 0; }
.inbound-code-cell { display: grid; gap: 3px; min-width: 0; }.inbound-code-cell .el-button { width: fit-content; margin: 0; padding: 0; }
.inbound-query-toolbar { align-items: stretch; border-radius: 8px; background: #f8fafc; }
.inbound-tabs { display: flex; width: max-content; max-width: 100%; min-width: 0; overflow-x: auto; border-bottom: 2px solid var(--cc-border); scrollbar-width: none; }
.inbound-tabs::-webkit-scrollbar { display: none; }
.inbound-tab { min-height: 42px; flex: 0 0 auto; padding: 10px 22px; border: 0; border-bottom: 2px solid transparent; margin-bottom: -2px; background: transparent; color: #475467; cursor: pointer; font-size: 13px; line-height: 1.4; white-space: nowrap; transition: border-color .16s ease, color .16s ease, background-color .16s ease; }
.inbound-tab:hover { color: var(--cc-primary); background: #f8fbff; }
.inbound-tab:focus-visible { outline: 3px solid rgba(102, 126, 234, .2); outline-offset: 1px; }
.inbound-tab.is-active { border-bottom-color: var(--cc-primary); background: #fff; color: var(--cc-primary); font-weight: 650; }
.inbound-state-stack { display: grid; min-width: 0; gap: 12px; }
.row-actions { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.table-toolbar { display: grid; min-width: 0; border-bottom: 1px solid var(--cc-border); }
.table-tabs-row { min-width: 0; padding: 0 14px; background: #fff; }
.inbound-stats-row { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 14px; background: var(--cc-primary); color: #fff; }
.inbound-stats-row .selected-info, .inbound-stats-row .total-info { min-width: 0; font-size: 12px; 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: 8px; }
.inbound-stats-row b { font-weight: 650; }
.inbound-stats-row i { width: 1px; height: 12px; background: rgba(255, 255, 255, .38); }
.table-panel :deep(.el-table th.el-table__cell) { border-right: 1px solid #edf0f4; }
.table-panel :deep(.el-table td.el-table__cell) { border-right: 1px solid #edf0f4; }
.primary-cell, .status-stack { display: grid; min-width: 0; justify-items: start; gap: 4px; }
.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: 12px; }
@media (max-width: 900px) { .inbound-tabs { width: 100%; } }
@media (max-width: 720px) { .inbound-tab { padding-inline: 14px; } .table-tabs-row { padding: 0 10px; } .inbound-stats-row { min-height: 0; align-items: stretch; flex-direction: column; gap: 10px; padding: 10px; } .inbound-stats-row .total-info { justify-content: flex-start; } }
@media (max-width: 360px) { .inbound-tab { padding-inline: 12px; } .row-actions { align-items: stretch; flex-direction: column; } .row-actions :deep(.el-button) { width: 100%; min-height: 40px; } }
</style>