Files
admin-web/src/modules/inbounds/pages/InboundOrdersView.vue
T
2026-08-04 21:01:39 +08:00

475 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 {
const response = await getPlatformInbound(recordCustomerId(row), row.id)
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 recordCustomerId(row: InboundOrderSummary) { return row.customerId || row.companyId || selectedCustomerId.value }
function openWorkspace(row: InboundOrderSummary) {
const customerId = recordCustomerId(row)
void router.push({ name: 'inbound-order-detail', params: { inboundId: row.id }, query: customerId ? { tenantId: customerId } : undefined })
}
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">
<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 class="table-toolbar-actions">
<span class="result-summary">共 {{ result.total }} 条 · 所属企业 {{ selectedCustomerName }}</span>
<el-button :loading="loading" @click="loadInbounds"><RefreshCw :size="16" />刷新</el-button>
</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; }
.inbound-tabs { display: flex; max-width: 100%; min-width: 0; overflow-x: auto; padding: 4px; border: 1px solid var(--cc-border); border-radius: 8px; background: #f7f9fc; scrollbar-width: none; }
.inbound-tabs::-webkit-scrollbar { display: none; }
.inbound-tab { min-height: 44px; flex: 0 0 auto; padding: 8px 18px; border: 1px solid transparent; border-radius: 6px; background: transparent; color: var(--cc-muted); cursor: pointer; font-size: 13px; line-height: 1.4; white-space: nowrap; transition: background-color .16s ease, border-color .16s ease, color .16s ease, box-shadow .16s ease; }
.inbound-tab:hover { background: #fff; color: var(--cc-primary); }
.inbound-tab:focus-visible { outline: 3px solid rgba(102, 126, 234, .2); outline-offset: 1px; }
.inbound-tab.is-active { border-color: #d8e0f0; background: #fff; color: var(--cc-primary); box-shadow: 0 1px 3px rgba(16, 24, 40, .08); font-weight: 650; }
.inbound-load-error { margin-top: 12px; }
.inbound-query-toolbar { border-radius: 8px; }
.row-actions { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
.row-actions :deep(.el-button) { margin-left: 0; }
.table-toolbar { display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 16px; min-height: 68px; padding: 10px 14px; border-bottom: 1px solid var(--cc-border); }
.table-toolbar-actions { display: flex; flex: 0 0 auto; align-items: center; gap: 14px; }
.result-summary { color: var(--cc-muted); font-size: 12px; white-space: nowrap; }
.table-toolbar :deep(.el-button) { min-height: 40px; margin-left: 0; }
.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; }
.detail-content { display: grid; gap: 24px; min-width: 0; }
.detail-error { display: flex; min-height: 240px; align-items: flex-start; justify-content: center; flex-direction: column; }
.detail-error strong { color: var(--cc-danger); font-size: 15px; }
.detail-error p { margin: 7px 0; color: var(--cc-text); font-size: 13px; }
.detail-error :deep(.el-button) { margin-top: 14px; }
@media (max-width: 900px) { .table-toolbar { align-items: stretch; flex-direction: column; } .inbound-tabs { width: 100%; } .table-toolbar-actions { justify-content: space-between; } }
@media (max-width: 720px) { .inbound-tab { padding-inline: 14px; } .table-toolbar { padding: 10px; } .table-toolbar-actions { gap: 10px; } .result-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; } .table-toolbar :deep(.el-button) { min-height: 44px; } }
</style>