Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test

This commit is contained in:
2026-08-11 20:11:01 +08:00
103 changed files with 3854 additions and 622 deletions
@@ -0,0 +1,118 @@
import { flushPromises, shallowMount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
import UnifiedOrdersView from './UnifiedOrdersView.vue'
vi.mock('vue-router', () => ({
useRoute: () => ({ params: {}, query: {} }),
useRouter: () => ({ push: vi.fn() }),
}))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
addPlatformUnifiedOrderNote: vi.fn(),
approvePlatformUnifiedOrder: vi.fn(),
getPlatformUnifiedOrder: vi.fn(),
listPlatformUnifiedOrderFeeDetails: vi.fn(),
listPlatformUnifiedOrders: vi.fn(),
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'INBOUNDED', label: '已入库' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERED', label: '派送完成' },
],
}))
function emptyPage() {
return { records: [], page: 1, size: 20, total: 0, pages: 0 }
}
function mountView() {
return shallowMount(UnifiedOrdersView, {
global: {
directives: { loading: () => undefined },
stubs: {
AdminPage: { template: '<main><slot /></main>' },
ListPageLayout: { template: '<section><slot name="toolbar" /><slot name="state" /><slot /></section>' },
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue'], emits: ['update:modelValue', 'search', 'reset'], template: '<div data-testid="query-toolbar" />' },
DataTable: { name: 'DataTable', props: ['page', 'pageSize'], emits: ['update:page', 'update:pageSize'], template: '<div data-testid="data-table" />' },
'el-button': { template: '<button><slot /></button>' },
},
},
})
}
describe('UnifiedOrdersView list request coordination', () => {
beforeEach(() => {
vi.mocked(listPlatformUnifiedOrders).mockReset().mockResolvedValue(emptyPage())
})
it('loads once initially and once for query, reset, status tab and pagination', async () => {
const wrapper = mountView()
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
await wrapper.findAll('.status-tab')[1]!.trigger('click')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(5)
})
it('does not apply draft filters to pagination until query is submitted', async () => {
const wrapper = mountView()
await flushPromises()
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
const draft = { ...toolbar.props('modelValue'), inboundCode: ' DRAFT-CODE ' }
toolbar.vm.$emit('update:modelValue', draft)
await wrapper.vm.$nextTick()
wrapper.getComponent({ name: 'DataTable' }).vm.$emit('update:page', 2)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, inboundCode: undefined }))
toolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, inboundCode: 'DRAFT-CODE' }))
})
it('resets a later page and changes page size with one request per interaction', async () => {
const wrapper = mountView()
await flushPromises()
const table = wrapper.getComponent({ name: 'DataTable' })
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
table.vm.$emit('update:page', 3)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
toolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
table.vm.$emit('update:pageSize', 50)
table.vm.$emit('update:page', 1)
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(4)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 50 }))
})
})
@@ -19,7 +19,7 @@ describe('UnifiedOrdersView contract', () => {
it('renders accessible current-status tabs with keyboard navigation', () => {
expect(source).toContain('role="tablist"')
expect(source).toContain('role="tab"')
expect(source).toContain(':aria-selected="filters.mainStatus === tab.value"')
expect(source).toContain(':aria-selected="selectedStatus === tab.value"')
expect(source).toContain("event.key === 'ArrowRight'")
expect(source).toContain("event.key === 'ArrowLeft'")
expect(source).toContain("event.key === 'Home'")
@@ -38,8 +38,8 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('listAllPlatformWarehouses')
expect(source).toContain("{ status: 'ACTIVE' }")
expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'")
expect(source).toContain("loaded: optionalBoolean(filters.value.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(filters.value.fullContainer)")
expect(source).toContain("loaded: optionalBoolean(activeFilters.loaded)")
expect(source).toContain("fullContainer: optionalBoolean(activeFilters.fullContainer)")
expect(source).toContain("{ value: 'UNRECORDED', label: '未录入' }")
expect(source).not.toContain("{ value: 'DRAFT', label: '草稿' }")
expect(source).not.toContain("{ value: 'CANCELLED', label: '已取消' }")
@@ -59,7 +59,7 @@ describe('UnifiedOrdersView contract', () => {
})
it('uses tabs as the only order-status filter and keeps advanced controls inside QueryToolbar', () => {
expect(source).toContain('filters.value.mainStatus || undefined')
expect(source).toContain('mainStatus: selectedStatus.value || undefined')
expect(source).toContain('<template #actions>')
expect(source).toContain(':aria-expanded="showAdvancedFilters"')
expect(source).not.toContain('class="filter-mode-actions"')
@@ -69,9 +69,9 @@ describe('UnifiedOrdersView contract', () => {
it('covers real list states and keeps operation column first and left-fixed', () => {
expect(source).toContain("{ key: 'actions', label: '操作', width: 300, fixed: 'left'")
expect(source).toContain("{ key: 'selection', type: 'selection', width: 52, fixed: 'left'")
expect(source).toContain('批量审核接口待接入')
expect(source).toContain('@selection-change="handleSelectionChange"')
expect(source).not.toContain("{ key: 'selection', type: 'selection'")
expect(source).not.toContain('批量审核接口待接入')
expect(source).not.toContain('@selection-change="handleSelectionChange"')
expect(source).toContain('title="订单列表暂时无法加载"')
expect(source).toContain('empty-text="暂无符合条件的订单"')
expect(source).toContain(':loading="loading"')
@@ -79,7 +79,7 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-model:page-size="pageSize"')
expect(source).toContain('slotName: \'orderStatus\'')
expect(source).toContain('slotName: \'measurementReviewStatus\'')
expect(source).toContain("NOT_SUBMITTED: '未提交'")
expect(source).toContain('unifiedOrderReviewStatusLabel(value, \'未录入\')')
expect(source).toContain("{ key: 'customerRemark', prop: 'customerRemark', label: '备注'")
expect(source).toContain("{ key: 'latestInternalNote', prop: 'latestInternalNote', label: '客服内部备注'")
expect(source.match(/<LineClampText v-if="column\.key === 'remark'"/g)).toHaveLength(3)
@@ -102,6 +102,14 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('v-if="moreUnifiedOrderActions(row as UnifiedOrderResource).length"')
})
it('keeps editable query values separate from the applied list request', () => {
expect(source).toContain('const appliedFilters = ref<FilterModel>(emptyFilters())')
expect(source).toContain('const activeFilters = appliedFilters.value')
expect(source).toContain('appliedFilters.value = cloneFilters(filters.value)')
expect(source).toContain('appliedFilters.value = emptyFilters()')
expect(source).not.toContain('filters.value.mainStatus')
})
it('keeps only confirmed business actions as explicit boundaries', () => {
expect(source).toContain('nextActionBoundary')
expect(source).not.toContain('class="action-boundary"')
@@ -148,6 +156,10 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain(':title="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)"')
expect(source).toContain("can('platform:order:note')")
expect(source).toContain('await loadOrders()')
expect(source).toContain('orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus)')
expect(source).toContain('feeReviewLabel(detail.summary.feeReviewStatus)')
expect(source).toContain('unifiedOrderLoadingStatusLabel(detail.summary.transport?.loadingStatus)')
expect(source).not.toContain("value.replaceAll('_', ' ')")
})
it('exposes the single-remainder fulfillment aggregate in row expansion and detail', () => {
@@ -188,6 +200,9 @@ describe('UnifiedOrdersView contract', () => {
expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'")
expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')")
expect(source).toContain('暂无费用数据')
expect(source).toContain('firstLegTransportModeLabel(detail.order.order?.transportMode)')
expect(source).toContain('firstLegPickupMethodLabel(detail.order.order?.pickupMethod)')
expect(source).toContain('firstLegDestinationTypeLabel(detail.order.order?.destinationType)')
})
it('mounts downstream progress inside the existing order detail without opening unsafe writes', () => {
@@ -30,7 +30,22 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import {
firstLegBusinessTypeLabel,
firstLegContainerModeLabel,
firstLegDestinationTypeLabel,
firstLegPickupMethodLabel,
firstLegTaxModeLabel,
firstLegTradeTermLabel,
firstLegTransportModeLabel,
} from '@/shared/utils/firstLegBusinessLabels'
import { UnifiedOrderWorkflowPanel } from '@/modules/unified-orders/workflows'
import {
unifiedOrderHistoryActionLabel,
unifiedOrderLoadingStatusLabel,
unifiedOrderReviewStatusLabel,
unifiedOrderStatusLabel,
} from '@/modules/unified-orders/status'
import {
downloadWeightImage,
getInboundMeasurement,
@@ -39,7 +54,6 @@ import {
type FilterModel = QueryModel & {
tenantId: string
mainStatus: UnifiedOrderMainStatus | ''
receivingWarehouseId: string
destinationWarehouseId: string
inboundCode: string
@@ -60,7 +74,6 @@ const route = useRoute()
const router = useRouter()
const access = useAccessPreviewStore()
const records = ref<UnifiedOrderResource[]>([])
const selectedOrders = ref<UnifiedOrderResource[]>([])
const customers = ref<PlatformTenantResource[]>([])
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
const page = ref(1)
@@ -96,6 +109,8 @@ let detailLoadingKey = ''
let detailRequestSerial = 0
const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const appliedFilters = ref<FilterModel>(emptyFilters())
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
const orderId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
const detailTenantId = computed(() => typeof route.query.tenantId === 'string' ? route.query.tenantId : '')
const detailRouteKey = computed(() => orderId.value ? `${detailTenantId.value}:${orderId.value}` : '')
@@ -182,7 +197,6 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 300, fixed: 'left', slotName: 'actions' },
{ key: 'selection', type: 'selection', width: 52, fixed: 'left', align: 'center', selectable: canBulkReview },
{ key: 'fulfillment', type: 'expand', width: 52, fixed: 'left', slotName: 'fulfillment', hideable: false, movable: false },
{ key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, slotName: 'field' },
{ key: 'internalOrderStatus', prop: 'internalOrderStatus', label: '订单状态', minWidth: 130, slotName: 'orderStatus' },
@@ -240,13 +254,17 @@ const columns: DataTableColumn[] = [
function emptyFilters(): FilterModel {
return {
tenantId: '', mainStatus: '', receivingWarehouseId: '', destinationWarehouseId: '',
tenantId: '', receivingWarehouseId: '', destinationWarehouseId: '',
inboundCode: '', expectedArrivalDate: '', feeReviewStatus: '', channelName: '',
containerNo: '', cabinetNo: '', sailingDate: '', loaded: '', fullContainer: '', measurementReviewStatus: '',
fbaCode: '', createdRange: [],
}
}
function cloneFilters(source: FilterModel): FilterModel {
return { ...source, createdRange: [...source.createdRange] }
}
function trimmed(value: string) {
return value.trim() || undefined
}
@@ -256,8 +274,10 @@ function optionalBoolean(value: string) {
}
function selectStatus(status: UnifiedOrderMainStatus | '') {
filters.value = { ...filters.value, mainStatus: status }
search()
if (selectedStatus.value === status) return
selectedStatus.value = status
if (page.value === 1) void loadOrders()
else page.value = 1
}
function handleTabKeydown(event: KeyboardEvent, index: number) {
@@ -338,34 +358,29 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
if (property.endsWith('At') || property.endsWith('Date')) return formatDate(String(value))
if (property === 'measurementReviewStatus') return measurementStatusLabel(String(value))
if (property === 'internalOrderStatus') return orderStatusLabel(String(value))
if (property === 'feeReviewStatus') return feeReviewLabel(String(value))
if (property === 'loadingStatus') return unifiedOrderLoadingStatusLabel(String(value))
if (property === 'businessType') return firstLegBusinessTypeLabel(String(value))
if (property === 'pickupMethod') return firstLegPickupMethodLabel(String(value))
if (property === 'taxMode') return firstLegTaxModeLabel(String(value))
if (property === 'destinationType') return firstLegDestinationTypeLabel(String(value))
return String(value)
}
function statusLabel(value: UnifiedOrderMainStatus | string | null) {
return unifiedOrderStatusOptions.find((item) => item.value === value)?.label || orderStatusLabel(value) || '状态更新'
return unifiedOrderStatusLabel(value, '状态更新')
}
function historyActionLabel(value: string | null | undefined) {
return ({
CREATE: '创建订单', CREATED: '创建订单', SUBMITTED: '提交订单', APPROVED: '审核通过',
ORDER_APPROVED: '订单审核通过', REJECTED: '审核驳回', MEASURE_APPROVE: '测量审核通过',
MEASURE_REJECT: '测量审核驳回', INBOUND_BEGIN: '开始入库', INBOUND_COMPLETE: '入库完成',
UI_FIXTURE_MEASURE_APPROVE: '测量数据审核通过', UI_FIXTURE_INBOUND_BEGIN: '开始入库',
STATUS_UPDATE: '状态更新',
} as Record<string, string>)[value || ''] || (value ? value.replaceAll('_', ' ') : '状态更新')
return unifiedOrderHistoryActionLabel(value)
}
function measurementStatusLabel(value: string | null | undefined) {
return ({ NOT_SUBMITTED: '未提交', UNRECORDED: '未录入', PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' } as Record<string, string>)[value || ''] || value || '未录入'
return unifiedOrderReviewStatusLabel(value, '未录入')
}
function orderStatusLabel(value: string | null | undefined) {
return ({
DRAFT: '草稿', SUBMITTED: '待审核', APPROVED: '已审核', ACCEPTED: '已受理', REJECTED: '已驳回',
PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消', PENDING_REVIEW: '待审核',
WAIT_MEASURE: '待测量数据', INBOUNDED: '已入库', WAIT_SPLIT: '待分柜', LOADING: '装柜中',
LOADED: '装柜完成', DELIVERED: '派送完成',
} as Record<string, string>)[value || ''] || value || '—'
return unifiedOrderStatusLabel(value)
}
function cargoValue(cargo: Record<string, unknown>, key: string) {
@@ -398,11 +413,12 @@ function feeNumber(value: number | null | undefined) {
}
function feeReviewLabel(value: string | null | undefined) {
return ({ PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回', NOT_SUBMITTED: '未提交' } as Record<string, string>)[value || ''] || value || '—'
return unifiedOrderReviewStatusLabel(value)
}
function feeDirectionLabel(value: string | null | undefined) {
return ({ RECEIVABLE: '应收', PAYABLE: '应付' } as Record<string, string>)[value || ''] || value || '—'
if (!value) return '—'
return ({ RECEIVABLE: '应收', PAYABLE: '应付' } as Record<string, string>)[value] || '未知方向'
}
async function loadFeeDetails() {
@@ -508,19 +524,6 @@ function canApprove(row: UnifiedOrderResource) {
return row.mainStatus === 'PENDING_REVIEW' && row.nextAction === 'APPROVE_ORDER' && can('platform:order:approve')
}
function canBulkReview(row: UnifiedOrderResource) {
return canApprove(row)
}
function handleSelectionChange(rows: UnifiedOrderResource[]) {
selectedOrders.value = rows
}
function openBulkReviewPlaceholder() {
if (selectedOrders.value.length === 0) return
ElMessage.info('批量审核接口待接入,请先逐笔审核')
}
function openApproval(row: UnifiedOrderResource) {
if (!canApprove(row)) return
approvalOrder.value = row
@@ -625,30 +628,30 @@ async function loadOrders() {
loading.value = true
loadError.value = null
try {
const range = filters.value.createdRange
const activeFilters = appliedFilters.value
const range = activeFilters.createdRange
const result = await listPlatformUnifiedOrders({
tenantId: filters.value.tenantId || undefined,
mainStatus: filters.value.mainStatus || undefined,
receivingWarehouseId: filters.value.receivingWarehouseId || undefined,
destinationWarehouseId: filters.value.destinationWarehouseId || undefined,
inboundCode: trimmed(filters.value.inboundCode),
expectedArrivalDate: filters.value.expectedArrivalDate || undefined,
feeReviewStatus: filters.value.feeReviewStatus || undefined,
channelName: trimmed(filters.value.channelName),
containerNo: trimmed(filters.value.containerNo),
cabinetNo: trimmed(filters.value.cabinetNo),
sailingDate: filters.value.sailingDate || undefined,
loaded: optionalBoolean(filters.value.loaded),
fullContainer: optionalBoolean(filters.value.fullContainer),
measurementReviewStatus: filters.value.measurementReviewStatus || undefined,
fbaCode: trimmed(filters.value.fbaCode),
tenantId: activeFilters.tenantId || undefined,
mainStatus: selectedStatus.value || undefined,
receivingWarehouseId: activeFilters.receivingWarehouseId || undefined,
destinationWarehouseId: activeFilters.destinationWarehouseId || undefined,
inboundCode: trimmed(activeFilters.inboundCode),
expectedArrivalDate: activeFilters.expectedArrivalDate || undefined,
feeReviewStatus: activeFilters.feeReviewStatus || undefined,
channelName: trimmed(activeFilters.channelName),
containerNo: trimmed(activeFilters.containerNo),
cabinetNo: trimmed(activeFilters.cabinetNo),
sailingDate: activeFilters.sailingDate || undefined,
loaded: optionalBoolean(activeFilters.loaded),
fullContainer: optionalBoolean(activeFilters.fullContainer),
measurementReviewStatus: activeFilters.measurementReviewStatus || undefined,
fbaCode: trimmed(activeFilters.fbaCode),
createdFrom: range.length === 2 ? `${range[0]}T00:00:00` : undefined,
createdTo: range.length === 2 ? `${range[1]}T23:59:59` : undefined,
page: page.value,
size: pageSize.value,
})
records.value = result.records
selectedOrders.value = []
total.value = result.total
} catch (error) {
records.value = []
@@ -660,6 +663,7 @@ async function loadOrders() {
}
function search() {
appliedFilters.value = cloneFilters(filters.value)
if (page.value !== 1) {
page.value = 1
return
@@ -669,6 +673,7 @@ function search() {
function reset() {
filters.value = emptyFilters()
appliedFilters.value = emptyFilters()
if (page.value === 1) void loadOrders()
else page.value = 1
}
@@ -838,9 +843,9 @@ onMounted(() => {
<div class="panel-title"><div><h2>订单基本信息</h2><p>{{ detail.summary.orderNo || `订单 ${detail.summary.orderId}` }}</p></div><Box :size="19" /></div>
<dl class="order-info-grid">
<div><dt>公司</dt><dd>{{ customerName(detail.summary) }}</dd></div>
<div><dt>运输方式</dt><dd>{{ detail.order.order?.transportMode || '—' }}</dd></div>
<div><dt>运输方式</dt><dd>{{ firstLegTransportModeLabel(detail.order.order?.transportMode) }}</dd></div>
<div><dt>当前状态</dt><dd><StatusBadge :status="statusLabel(detail.summary.mainStatus)" /></dd></div>
<div><dt>内部订单状态</dt><dd>{{ detail.summary.internalOrderStatus || '—' }}</dd></div>
<div><dt>内部订单状态</dt><dd>{{ orderStatusLabel(detail.summary.internalOrderStatus || detail.summary.mainStatus) }}</dd></div>
<div><dt>平台收货仓</dt><dd>{{ detail.summary.receivingWarehouseName || '—' }}</dd></div>
<div><dt>收货仓地址</dt><dd>{{ detail.order.order?.receivingWarehouseAddress || '—' }}</dd></div>
<div><dt>目的仓</dt><dd>{{ detail.summary.destinationWarehouseName || '—' }}</dd></div>
@@ -850,13 +855,13 @@ onMounted(() => {
<div><dt>目的仓联系人</dt><dd>{{ detail.summary.destinationContact || '—' }}</dd></div>
<div><dt>目的仓联系人电话</dt><dd>{{ detail.summary.destinationContactPhone || '—' }}</dd></div>
<div><dt>渠道</dt><dd>{{ detail.summary.channelName || '—' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ detail.summary.businessType || '—' }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ detail.order.order?.tradeTerm || '—' }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ detail.order.order?.taxMode || '—' }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ detail.summary.feeReviewStatus || '—' }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(detail.summary.businessType) }}</dd></div>
<div><dt>贸易条款</dt><dd>{{ firstLegTradeTermLabel(detail.order.order?.tradeTerm) }}</dd></div>
<div><dt>买单 / 退税</dt><dd>{{ firstLegTaxModeLabel(detail.order.order?.taxMode) }}</dd></div>
<div><dt>费用审核状态</dt><dd>{{ feeReviewLabel(detail.summary.feeReviewStatus) }}</dd></div>
<div><dt>是否扣货</dt><dd>{{ displayBoolean(detail.summary.holdCargo) }}</dd></div>
<div><dt>扣货理由</dt><dd>{{ detail.summary.holdReason || '—' }}</dd></div>
<div><dt>提货方式</dt><dd>{{ detail.order.order?.pickupMethod || '—' }}</dd></div>
<div><dt>提货方式</dt><dd>{{ firstLegPickupMethodLabel(detail.order.order?.pickupMethod) }}</dd></div>
<div><dt>提货时间</dt><dd>{{ formatDate(detail.summary.pickupAt) }}</dd></div>
<div><dt>到仓时间</dt><dd>{{ detail.order.order?.receivingTime || '—' }}</dd></div>
<div><dt>货物到仓时间</dt><dd>{{ formatDate(detail.summary.expectedArrivalAt || detail.summary.inboundCompletedAt) }}</dd></div>
@@ -867,9 +872,9 @@ onMounted(() => {
<div><dt>船名 / 航次</dt><dd>{{ detail.summary.transport?.vesselName || '—' }} / {{ detail.summary.transport?.voyageNo || '—' }}</dd></div>
<div><dt>开船时间</dt><dd>{{ formatDate(detail.summary.transport?.sailingAt) }}</dd></div>
<div><dt>到港时间</dt><dd>{{ formatDate(detail.summary.transport?.portArrivalAt) }}</dd></div>
<div><dt>配载状态</dt><dd>{{ detail.summary.transport?.loadingStatus || '—' }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ detail.order.order?.destinationType || '—' }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ detail.order.order?.containerMode || '—' }}</dd></div>
<div><dt>配载状态</dt><dd>{{ unifiedOrderLoadingStatusLabel(detail.summary.transport?.loadingStatus) }}</dd></div>
<div><dt>目的仓类型</dt><dd>{{ firstLegDestinationTypeLabel(detail.order.order?.destinationType) }}</dd></div>
<div><dt>是否整柜</dt><dd>{{ firstLegContainerModeLabel(detail.order.order?.containerMode) }}</dd></div>
<div><dt>商检验</dt><dd>{{ displayBoolean(detail.order.order?.customsInspection) }}</dd></div>
<div><dt>总箱数</dt><dd>{{ detail.summary.totalPackageCount ?? '—' }} 箱</dd></div>
<div><dt>推荐托盘类型</dt><dd>{{ detail.summary.recommendedPalletType || '—' }}</dd></div>
@@ -1026,10 +1031,10 @@ onMounted(() => {
:key="tab.value || 'ALL'"
type="button"
role="tab"
:aria-selected="filters.mainStatus === tab.value"
:aria-selected="selectedStatus === tab.value"
aria-controls="unified-orders-results"
:tabindex="filters.mainStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': filters.mainStatus === tab.value }]"
:tabindex="selectedStatus === tab.value ? 0 : -1"
:class="['status-tab', { 'is-active': selectedStatus === tab.value }]"
@click="selectStatus(tab.value)"
@keydown="handleTabKeydown($event, index)"
>{{ tab.label }}</button>
@@ -1054,10 +1059,7 @@ onMounted(() => {
<template #default>
<div v-if="!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表" @selection-change="handleSelectionChange">
<template #toolbar-actions>
<el-button type="primary" plain :disabled="selectedOrders.length === 0" title="批量审核接口待接入" @click="openBulkReviewPlaceholder"><ListChecks :size="16" />批量审核<span v-if="selectedOrders.length">({{ selectedOrders.length }})</span></el-button>
</template>
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="records" :columns="columns" preference-key="unified-orders-table" :loading="loading" :total="total" :page-sizes="[20, 50, 100]" row-key="orderId" empty-text="暂无符合条件的订单" aria-label="统一订单列表">
<template #actions="{ row }">
<div class="row-actions">
<el-button link type="primary" @click="openDetail(row as UnifiedOrderResource)"><Eye :size="15" />详情</el-button>
@@ -1087,7 +1089,7 @@ onMounted(() => {
<template #customer="{ row }"><div class="primary-cell"><strong>{{ customerName(row as UnifiedOrderResource) }}</strong><span>客户 ID {{ (row as UnifiedOrderResource).tenantId }}</span></div></template>
<template #receivingWarehouseName="{ row }">{{ (row as UnifiedOrderResource).receivingWarehouseName || '—' }}</template>
<template #destinationWarehouseName="{ row }">{{ (row as UnifiedOrderResource).destinationWarehouseName || '—' }}</template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).transport?.containerNo || (row as UnifiedOrderResource).transport?.loadingStatus || '—' }}</strong><span v-if="(row as UnifiedOrderResource).transport?.vesselName">{{ (row as UnifiedOrderResource).transport?.vesselName }} · {{ (row as UnifiedOrderResource).transport?.voyageNo || '—' }}</span></div></template>
<template #transport="{ row }"><div class="primary-cell"><strong>{{ (row as UnifiedOrderResource).transport?.containerNo || unifiedOrderLoadingStatusLabel((row as UnifiedOrderResource).transport?.loadingStatus) }}</strong><span v-if="(row as UnifiedOrderResource).transport?.vesselName">{{ (row as UnifiedOrderResource).transport?.vesselName }} · {{ (row as UnifiedOrderResource).transport?.voyageNo || '—' }}</span></div></template>
<template #cargo="{ row }"><span>{{ (row as UnifiedOrderResource).totalPackageCount ?? '—' }} 箱</span><span v-if="(row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== null && (row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualGrossWeightKg) }} kg</span><span v-if="(row as UnifiedOrderResource).measurement?.actualVolumeCbm !== null && (row as UnifiedOrderResource).measurement?.actualVolumeCbm !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualVolumeCbm, 3) }} m³</span></template>
<template #expectedShipDate="{ row }">{{ formatDate((row as UnifiedOrderResource).expectedShipDate) }}</template>
<template #updatedAt="{ row }">{{ formatDate((row as UnifiedOrderResource).updatedAt) }}</template>