This commit is contained in:
2026-10-20 17:24:50 +08:00
parent f155976f52
commit 3598310237
18 changed files with 203 additions and 120 deletions
@@ -19,7 +19,7 @@ import LineClampText from '@/shared/components/LineClampText.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { firstLegBusinessTypeLabel } from '@/shared/utils/firstLegBusinessLabels'
import { listPlatformInbounds } from '../core/api'
import type { InboundOrderSummary } from '../core/types'
import type { InboundOrderSummary, InboundPageResult } from '../core/types'
import InboundMeasurementDialog from '../measurement/InboundMeasurementDialog.vue'
import MeasurementReviewPanel from '../review/MeasurementReviewPanel.vue'
import { approveMeasurement, getMeasurementReviewDetail } from '../review/api'
@@ -33,7 +33,11 @@ type FilterModel = QueryModel & {
expectedArrivalRange: string[]
}
const props = defineProps<{ mode: InboundTaskMode; title?: string }>()
const props = defineProps<{
mode: InboundTaskMode
title?: string
taskLoader?: (pagination: { page: number; size: number }) => Promise<InboundPageResult>
}>()
const emit = defineEmits<{ changed: [] }>()
const router = useRouter()
const access = useAccessPreviewStore()
@@ -143,6 +147,7 @@ function dateParam(value: string | undefined, end = false) {
}
async function loadOptions() {
if (props.taskLoader) return
const serial = ++optionsRequestSerial
customerLoadError.value = null
try {
@@ -169,7 +174,7 @@ async function loadTasks() {
loadError.value = null
const applied = appliedFilters.value
try {
const response = await listPlatformInbounds(applied.customerId || undefined, buildInboundTaskQuery(props.mode, {
const response = props.taskLoader ? await props.taskLoader({ page: page.value, size: pageSize.value }) : await listPlatformInbounds(applied.customerId || undefined, buildInboundTaskQuery(props.mode, {
page: page.value,
size: pageSize.value,
warehouseId: applied.warehouseId || undefined,
@@ -328,6 +333,12 @@ function clearSelectedInbound() {
reviewNavigationLoading.value = false
}
function reload() {
if (page.value === 1) return loadTasks()
page.value = 1
}
defineExpose({ reload })
watch([page, pageSize], () => { void loadTasks() })
onMounted(() => { void loadOptions(); void loadTasks() })
onActivated(() => {
@@ -363,6 +374,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
>
<template #toolbar-start><h1>{{ title || (isReview ? '测量数据审核' : '订单测量') }}</h1></template>
<template #filters>
<slot name="filters" :loading="loading">
<slot name="status-tabs" />
<QueryToolbar
v-model="filters"
@@ -373,6 +385,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
@search="applyFilters"
@reset="resetFilters"
/>
</slot>
</template>
<template #notices><template v-if="customerLoadError || loadError">
<div class="state-stack">
+2
View File
@@ -20,3 +20,5 @@ export type { AdjustPublishedInboundFeeRequest, InboundFeeSnapshot } from './fee
export { downloadWeightImage } from './weight/api/weightOcr'
export type { InboundMeasurementDetail } from './measurement/types'
export type { InboundHoldCommand, InboundHoldDetail, InboundHoldEvidenceUpload, InboundHoldHistoryEntry } from './core/types'
export type { InboundOrderSummary, InboundPageResult } from './core/types'
@@ -1,28 +0,0 @@
import { mount, flushPromises } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import TransferQuantitySummary from './TransferQuantitySummary.vue'
import { getTransferQuantityTotals } from '../api/transferQuantityFacts'
vi.mock('../api/transferQuantityFacts', () => ({ getTransferQuantityTotals: vi.fn() }))
describe('转运完整筛选数量', () => {
it('keeps unknown pieces explicit and renders overlapping pallet facts after paging', async () => {
vi.mocked(getTransferQuantityTotals).mockResolvedValue({ orderCount: '2', declaredPieceCount: null, knownDeclaredPieceCount: '16', unknownDeclaredOrderCount: '1', receivedPieceCount: '20', knownReceivedPieceCount: '20', unknownReceivedOrderCount: '0', declaredCartonCount: '4', receivedCartonCount: '4', receivedPalletCount: '3', availableCartonCount: '2', shippedCartonCount: '2', remainingPalletCount: '2', shippedPalletCount: '2' })
const wrapper = mount(TransferQuantitySummary, { props: { query: { page: 1, size: 1, orderType: 'WAREHOUSE_TRANSFER' } } })
await flushPromises()
expect(wrapper.text()).toContain('未记录(已知 16 件,1 票未知)')
expect(wrapper.text()).toContain('20 件 / 4 箱 / 3 托')
expect(wrapper.text()).toContain('剩余 2 托')
expect(wrapper.text()).toContain('2 箱 / 2 托(托盘可重叠)')
await wrapper.setProps({ query: { page: 2, size: 1, orderType: 'WAREHOUSE_TRANSFER' } })
await flushPromises()
expect(wrapper.text()).toContain('全筛选 2 票')
expect(getTransferQuantityTotals).toHaveBeenLastCalledWith({ page: 2, size: 1, orderType: 'WAREHOUSE_TRANSFER' })
})
it('shows a recoverable error instead of zero totals', async () => {
vi.mocked(getTransferQuantityTotals).mockRejectedValue(new Error('offline'))
const wrapper = mount(TransferQuantitySummary, { props: { query: { page: 1, size: 1 } } })
await flushPromises()
expect(wrapper.get('[role="alert"]').text()).toContain('加载失败')
expect(wrapper.text()).not.toContain('全筛选 0')
})
})
@@ -1,24 +0,0 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getTransferQuantityTotals, type TransferQuantityTotals } from '../api/transferQuantityFacts'
import type { LastMileOrderQuery } from '../api/lastMileOrders'
const props = defineProps<{ query: LastMileOrderQuery }>()
const summary = ref<TransferQuantityTotals>()
const error = ref('')
let sequence = 0
watch(() => JSON.stringify(props.query), async () => {
const current = ++sequence
summary.value = undefined; error.value = ''
try { const value = await getTransferQuantityTotals(props.query); if (current === sequence) summary.value = value }
catch { if (current === sequence) error.value = '转运全筛选数量加载失败,请重新查询' }
}, { immediate: true })
</script>
<template>
<div v-if="summary" role="status" aria-label="仓储转运全筛选汇总">
<strong>仓储转运 · 全筛选 {{ summary.orderCount }} 票</strong> ·
预报 {{ summary.declaredPieceCount ?? `未记录(已知 ${summary.knownDeclaredPieceCount} 件,${summary.unknownDeclaredOrderCount} 票未知)` }}{{ summary.declaredPieceCount == null ? '' : ' 件' }} / {{ summary.declaredCartonCount }} 箱 ·
历史实收 {{ summary.receivedPieceCount ?? `未记录(已知 ${summary.knownReceivedPieceCount} 件,${summary.unknownReceivedOrderCount} 票未知)` }}{{ summary.receivedPieceCount == null ? '' : ' 件' }} / {{ summary.receivedCartonCount }} 箱 / {{ summary.receivedPalletCount }} 托 ·
当前已清点 {{ summary.availableCartonCount }} 箱 · 剩余 {{ summary.remainingPalletCount }} 托 · 已出库 {{ summary.shippedCartonCount }} 箱 / {{ summary.shippedPalletCount }} 托(托盘可重叠)
</div>
<p v-else-if="error" role="alert">{{ error }}</p>
</template>
@@ -41,7 +41,10 @@ async function retry(state: VueWrapper) { await state.get('.load-error-state__ac
describe('native cargo discard detail and approval', () => {
it('preserves all readonly facts, full item values and history through fullscreen', async () => {
const { dialog } = await open()
const { wrapper, dialog } = await open()
expect(api.statistics).not.toHaveBeenCalled()
expect(wrapper.text()).not.toContain('全筛选')
expect(wrapper.text()).not.toContain('统计未加载')
expect(dialog.props('title')).toBe('废弃单详情 · CD-001'); expect(api.detail).toHaveBeenCalledExactlyOnceWith('CD-001', '9007199254740994')
const facts = dialog.get('[aria-label="废弃单基础资料"]')
for (const value of ['CD-001', 'OUT-1', '洛杉矶仓', 'INBOUND-1', '待审核', '废弃原因与备注']) expect(facts.text()).toContain(value)
@@ -12,18 +12,17 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LineClampText from '@/shared/components/LineClampText.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { downloadCargoDiscardPhoto, listCargoDiscards, getCargoDiscard, reviewCargoDiscard, getCargoDiscardStatistics, type CargoDiscardStatistics, type CargoDiscardDetail, type CargoDiscardSummary } from '../api/cargoDiscards'
import { downloadCargoDiscardPhoto, listCargoDiscards, getCargoDiscard, reviewCargoDiscard, type CargoDiscardDetail, type CargoDiscardSummary } from '../api/cargoDiscards'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { overseasWarehousePermissions as permission } from '../permissions'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
const access = useAccessPreviewStore(); const rows = ref<CargoDiscardSummary[]>([]); const customers = ref<PlatformTenantResource[]>([]); const page = ref(1); const pageSize = ref(10); const total = ref(0); const loading = ref(false); const detailOpen = ref(false); const detail = ref<CargoDiscardDetail | null>(null); const filters = ref<QueryModel>({ customerId: '', discardNo: '', inboundCode: '', status: '' })
const statistics = ref<CargoDiscardStatistics | null>(null)
/** 数量事实缺失时明确提示,不用零代替未记录。 */
function quantity(value: number | null | undefined, unit: string) { return value == null ? '未记录 / 不适用' : `${value} ${unit}` }
const fields: QueryField[] = [{ key: 'customerId', type: 'select', label: '客户', placeholder: '请选择客户', options: [] }, { key: 'discardNo', type: 'input', label: '废弃单号', placeholder: '废弃单号', width: 220 }, { key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 180 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '已审核', value: 'APPROVED' }, { label: '待客户确认', value: 'PDA_COMPLETED' }, { label: '已完成', value: 'COMPLETED' }, { label: '已驳回', value: 'REJECTED' }, { label: '已取消', value: 'CANCELLED' }] }]
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 140, fixed: 'left', slotName: 'actions' }, { key: 'discardNo', label: '废弃单号', minWidth: 180 }, { key: 'inboundCodes', label: '入仓码', minWidth: 180 }, { key: 'overseasWarehouseName', label: '仓库', minWidth: 140 }, { key: 'requestedPackages', label: '申请箱数', width: 90 }, { key: 'totalFee', label: '废弃费用', width: 120, formatter: (row: CargoDiscardSummary) => `${row.currency || 'USD'} ${row.totalFee ?? 0}` }, { key: 'createdBy', label: '申请人', width: 110 }, { key: 'createdAt', label: '创建时间', minWidth: 170 }, { key: 'status', label: '状态', width: 150, slotName: 'status' }]
const statusLabels: Record<string, string> = { PENDING_REVIEW: '待审核', APPROVED: '已审核,待 PDA', PDA_COMPLETED: 'PDA 已完成,待客户确认', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' }
const customerError = ref<ApiErrorDetails | null>(null); const listError = ref<ApiErrorDetails | null>(null); const statisticsError = ref<ApiErrorDetails | null>(null)
const customerError = ref<ApiErrorDetails | null>(null); const listError = ref<ApiErrorDetails | null>(null)
const detailError = ref<ApiErrorDetails | null>(null); const reviewError = ref<ApiErrorDetails | null>(null)
const detailLoading = ref(false); const reviewing = ref(''); const detailTarget = ref<CargoDiscardSummary | null>(null)
const photoUrls = reactive<Record<string, string>>({}); const photoLoading = reactive<Record<string, boolean>>({}); const photoErrors = reactive<Record<string, ApiErrorDetails | null>>({})
@@ -42,17 +41,20 @@ async function loadPhoto(fileId: string) {
watch(detailOpen, value => { if (!value) { detailGeneration++; clearPhotos() } })
onBeforeUnmount(() => { disposed = true; loadGeneration++; detailGeneration++; clearPhotos() })
function text(key: string) { return String(filters.value[key] || '').trim() || undefined }
/** 分页与全筛选统计独立保留诊断,过期响应不得覆盖新的查询。 */
/** 过期响应不得覆盖新的查询。 */
async function load(model: QueryModel = filters.value) {
const generation = ++loadGeneration; Object.assign(filters.value, model); loading.value = true; listError.value = null; statisticsError.value = null; statistics.value = null
const generation = ++loadGeneration; Object.assign(filters.value, model); loading.value = true; listError.value = null
const query = { page: page.value, size: pageSize.value, customerId: text('customerId'), discardNo: text('discardNo'), inboundCode: text('inboundCode'), status: text('status') }
const [list, stat] = await Promise.allSettled([listCargoDiscards(query), getCargoDiscardStatistics(query)])
if (disposed || generation !== loadGeneration) return
if (list.status === 'fulfilled') { rows.value = list.value.records; total.value = list.value.total }
else { rows.value = []; total.value = 0; listError.value = getApiErrorDetails(list.reason, '废弃单列表加载失败') }
if (stat.status === 'fulfilled') statistics.value = stat.value
else statisticsError.value = getApiErrorDetails(stat.reason, '废弃统计加载失败')
loading.value = false
try {
const result = await listCargoDiscards(query)
if (disposed || generation !== loadGeneration) return
rows.value = result.records; total.value = result.total
} catch (error) {
if (disposed || generation !== loadGeneration) return
rows.value = []; total.value = 0; listError.value = getApiErrorDetails(error, '废弃单列表加载失败')
} finally {
if (!disposed && generation === loadGeneration) loading.value = false
}
}
async function loadCustomers() {
customerError.value = null
@@ -77,7 +79,7 @@ async function audit(row: CargoDiscardSummary) {
onMounted(() => { void loadCustomers(); void load() })
</script>
<template>
<AdminPage class="admin-density"><ListPageLayout aria-label="废弃单列表"><section><LoadErrorState v-if="customerError" title="客户列表加载失败" :message="customerError.message" @retry="loadCustomers"><template #details><ApiErrorMeta :details="customerError" /></template></LoadErrorState><LoadErrorState v-if="listError" title="废弃单列表加载失败" :message="listError.message" @retry="load()"><template #details><ApiErrorMeta :details="listError" /></template></LoadErrorState><LoadErrorState v-if="statisticsError" title="废弃统计加载失败" :message="statisticsError.message" @retry="load()"><template #details><ApiErrorMeta :details="statisticsError" /></template></LoadErrorState><LoadErrorState v-if="reviewError" title="审核失败" :message="reviewError.message" :show-retry="false"><template #details><ApiErrorMeta :details="reviewError" /></template></LoadErrorState><DataTable appearance="integrated" :stripe="false" :data="rows" :columns="columns" :loading="loading" :total="total" v-model:page="page" v-model:page-size="pageSize" preference-key="overseas-cargo-discards" row-key="id" empty-text="暂无废弃单" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-start><h1>废弃单</h1></template><template #filters><QueryToolbar ariaLabel="废弃单筛选" v-model="filters" :fields="fields" :loading="loading" preference-key="overseas-cargo-discards-query" @search="page = 1; load($event)" @reset="page = 1; load($event)" /></template><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="!!reviewing" @click="openDetail(row as CargoDiscardSummary)">详情</el-button><el-button class="density-action" v-if="access.can(permission.discardApprove) && (row as CargoDiscardSummary).status === 'PENDING_REVIEW'" size="small" type="primary" :disabled="!!reviewing" @click="audit(row as CargoDiscardSummary)">审核并推送 PDA</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabels[String((row as CargoDiscardSummary).status)] || (row as CargoDiscardSummary).status || '未记录'" /></template><template #pagination-left><span v-if="statistics">全筛选:{{ statistics.orderCount }} 票 · {{ statistics.skuCount }} 种 SKU · 申请 {{ statistics.requestedPackages }} 箱 · 已确认实际 {{ quantity(statistics.actualPackages, '箱') }} · 商品 {{ quantity(statistics.requestedPieces, '个') }} · 托盘 {{ quantity(statistics.palletCount, '托盘') }}</span><span v-else>统计未加载</span></template></DataTable></section></ListPageLayout></AdminPage>
<AdminPage class="admin-density"><ListPageLayout aria-label="废弃单列表"><section><LoadErrorState v-if="customerError" title="客户列表加载失败" :message="customerError.message" @retry="loadCustomers"><template #details><ApiErrorMeta :details="customerError" /></template></LoadErrorState><LoadErrorState v-if="listError" title="废弃单列表加载失败" :message="listError.message" @retry="load()"><template #details><ApiErrorMeta :details="listError" /></template></LoadErrorState><LoadErrorState v-if="reviewError" title="审核失败" :message="reviewError.message" :show-retry="false"><template #details><ApiErrorMeta :details="reviewError" /></template></LoadErrorState><DataTable appearance="integrated" :stripe="false" :data="rows" :columns="columns" :loading="loading" :total="total" v-model:page="page" v-model:page-size="pageSize" preference-key="overseas-cargo-discards" row-key="id" empty-text="暂无废弃单" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-start><h1>废弃单</h1></template><template #filters><QueryToolbar ariaLabel="废弃单筛选" v-model="filters" :fields="fields" :loading="loading" preference-key="overseas-cargo-discards-query" @search="page = 1; load($event)" @reset="page = 1; load($event)" /></template><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="!!reviewing" @click="openDetail(row as CargoDiscardSummary)">详情</el-button><el-button class="density-action" v-if="access.can(permission.discardApprove) && (row as CargoDiscardSummary).status === 'PENDING_REVIEW'" size="small" type="primary" :disabled="!!reviewing" @click="audit(row as CargoDiscardSummary)">审核并推送 PDA</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabels[String((row as CargoDiscardSummary).status)] || (row as CargoDiscardSummary).status || '未记录'" /></template></DataTable></section></ListPageLayout></AdminPage>
<AppDialog v-model="detailOpen" class="admin-density-dialog" :title="'废弃单详情 · ' + (detailTarget?.discardNo || '')" :confirm-loading="detailLoading" :show-confirm="false" cancel-text="关闭" width="min(920px, calc(100vw - 28px))" fullscreenable>
<p v-if="detailLoading" role="status">正在读取废弃单…</p>
<LoadErrorState v-else-if="detailError" title="废弃单详情加载失败" :message="detailError.message" @retry="readDetail"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
@@ -89,7 +89,8 @@ describe('inventory customer-name snapshots', () => {
await flushPromises()
expect(wrapper.text()).toContain('华远贸易')
expect(wrapper.text()).toContain('SKU-1 × 5 件;SKU-SECOND × 7 件')
expect(wrapper.text()).toContain('可用 12 件 / 2 箱 / 1 托')
expect(wrapper.find('[aria-label="可用库存件箱托汇总"]').exists()).toBe(false)
expect(wrapper.get('[aria-label="库存汇总"]').text()).toContain('可用库存(箱)10')
})
it('combines the available references and then shows an explicit empty business reference without exposing the inventory id', async () => {
@@ -12,7 +12,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { getInventorySummary, getInventoryPieceProjection, listInventoryBalances, type InventoryDashboardDto, type InventoryPieceProjection, type InventoryPieceGroup } from '../api/inventory'
import { getInventorySummary, getInventoryPieceProjection, listInventoryBalances, type InventoryDashboardDto, type InventoryPieceGroup } from '../api/inventory'
import { ElMessage } from 'element-plus'
import { listReceivingOverseasWarehouses, type ReceivingOverseasWarehouseDto } from '../api/pallets'
import { toInventoryListItem } from '../inventoryPresentation'
@@ -25,7 +25,6 @@ const access = useAccessPreviewStore()
const warehouses = ref<ReceivingOverseasWarehouseDto[]>([])
const rows = ref<InventoryListItem[]>([])
const summary = ref<InventoryDashboardDto | null>(null)
const pieces = ref<InventoryPieceProjection | null>(null)
const rowPieces = ref<Record<string, InventoryPieceGroup | undefined>>({})
const pieceError = ref('')
const loading = ref(false)
@@ -79,7 +78,7 @@ function query() {
async function load(model: QueryModel = filters.value) {
Object.assign(filters.value, model)
loading.value = true; loadError.value = null; pieces.value = null; rowPieces.value = {}; pieceError.value = ''
loading.value = true; loadError.value = null; rowPieces.value = {}; pieceError.value = ''
try {
const currentQuery = query()
const [pageResult, summaryResult] = await Promise.all([listInventoryBalances(currentQuery), getInventorySummary(currentQuery)])
@@ -87,10 +86,9 @@ async function load(model: QueryModel = filters.value) {
try {
const pieceResult = await getInventoryPieceProjection(currentQuery)
rowPieces.value = Object.fromEntries(pageResult.records.map(dto => [String(dto.id), pieceResult.groups.find(group => String(group.customerId) === String(dto.customerId) && String(group.warehouseId) === String(dto.overseasWarehouseId) && (group.destinationCode ?? '') === (dto.destinationWarehouseCode ?? '') && (group.anchorSkuCode ?? '') === (dto.skuCode ?? ''))]))
pieces.value = pieceResult
} catch { pieceError.value = '可用件数及箱内SKU加载失败,请重新查询;箱数列表仍可查看。' }
}
catch (error) { rows.value = []; total.value = 0; summary.value = null; pieces.value = null; rowPieces.value = {}; loadError.value = getApiErrorDetails(error, '库存数据加载失败,请重试') }
catch (error) { rows.value = []; total.value = 0; summary.value = null; rowPieces.value = {}; loadError.value = getApiErrorDetails(error, '库存数据加载失败,请重试') }
finally { loading.value = false }
}
@@ -109,7 +107,7 @@ onMounted(() => { void loadWarehouses(); void load() })
<template #toolbar-start><h1>库存管理</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading || contextLoading" ariaLabel="库存筛选" preference-key="overseas-inventory-query" @search="search" @reset="page = 1; rows = []; total = 0; summary = null" /></template>
<template #notices><template v-if="loadError"><LoadErrorState title="库存数据加载失败" :message="loadError.message" :loading="loading || contextLoading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template><p v-if="pieceError" role="alert">{{ pieceError }}</p></template>
<template #before-results><section class="inventory-metrics" aria-label="库存汇总"><div v-for="metric in metrics" :key="metric[0]" class="inventory-metric" :class="`is-${metric[2]}`"><span>{{ metric[0] }}(箱)</span><strong>{{ metric[1] }}</strong></div></section><p v-if="pieces" role="status" aria-label="可用库存件箱托汇总">全部筛选结果 · 可用 {{ pieces.availablePieceCount ?? `未知(已知 ${pieces.knownAvailablePieceCount} 件,${pieces.unknownCartonCount} 箱件数未记录)` }}{{ pieces.availablePieceCount == null ? '' : ' 件' }} / {{ pieces.availableCartonCount }} 箱 / {{ pieces.availablePalletCount }} 托</p></template>
<template #before-results><section class="inventory-metrics" aria-label="库存汇总"><div v-for="metric in metrics" :key="metric[0]" class="inventory-metric" :class="`is-${metric[2]}`"><span>{{ metric[0] }}(箱)</span><strong>{{ metric[1] }}</strong></div></section></template>
<template #toolbar-actions><el-button v-if="access.can(overseasWarehousePermissions.inventoryExport) && access.can('platform:download:list')" plain :loading="exportLoading" @click="exportBalances"><Download :size="16" />导出库存</el-button><el-button @click="router.push({ name: 'overseas-inventory-transactions' })"><BookOpen :size="16" />进销存记录</el-button></template>
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button></div></template>
@@ -194,23 +194,22 @@ async function chooseContainer(wrapper: Awaited<ReturnType<typeof mountView>>) {
}
describe('LastMileOrdersView pending order and container conversion', () => {
it('keeps server-wide return totals across pages and clears them on a statistics failure', async () => {
it('hides filter totals while retaining return row dimensions', async () => {
const totals = { orderCount: 3, skuTypes: 3, expectedQuantity: 19, receivedQuantity: 14, remainingQuantity: 5, receivedBoxCount: 3, receivedParcelCount: 2, receivedPalletCount: 1 }
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 30, pages: 3 })
vi.mocked(getReturnOrderStatistics).mockResolvedValue({ totals, pageDimensions: [] })
const wrapper = await mountView()
const summary = () => wrapper.find('[aria-label="退货全筛选汇总"]')
expect(summary().text()).toContain('预报 19 件')
expect(summary().text()).toContain('3 种 SKU')
expect(wrapper.find('[aria-label="退货全筛选汇总"]').exists()).toBe(false)
expect(wrapper.find('[aria-label="仓储转运全筛选汇总"]').exists()).toBe(false)
const table = wrapper.findComponent(DataTable)
table.vm.$emit('update:page', 2)
await flushPromises()
expect(getReturnOrderStatistics).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 }))
expect(summary().text()).toContain('实收 14 件')
expect(wrapper.text()).not.toContain('当前筛选中的退货')
vi.mocked(getReturnOrderStatistics).mockRejectedValueOnce(new Error('统计暂不可用'))
table.vm.$emit('update:page', 3)
await flushPromises()
expect(summary().exists()).toBe(false)
expect(wrapper.find('[aria-label="退货全筛选汇总"]').exists()).toBe(false)
expect(wrapper.text()).toContain('尾程订单加载失败')
expect(table.props('data')).toEqual([])
})
@@ -14,7 +14,6 @@ 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 TransferQuantitySummary from '../components/TransferQuantitySummary.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
@@ -1109,10 +1108,6 @@ onMounted(() => { void load() })
</el-button>
</template>
</QueryToolbar>
<TransferQuantitySummary v-if="!loading && statusTab !== '柜子'" :query="orderQuery()" />
<div v-if="returnStatistics && !loading" class="return-filter-summary" role="status" aria-label="退货全筛选汇总">
当前筛选中的退货:{{ returnStatistics.totals.orderCount }} 票 · {{ returnStatistics.totals.skuTypes }} 种 SKU · 预报 {{ returnStatistics.totals.expectedQuantity }} 件 · 实收 {{ returnStatistics.totals.receivedQuantity }} 件 · 待收 {{ returnStatistics.totals.remainingQuantity }} 件 · 签收 {{ returnStatistics.totals.receivedBoxCount }} 箱 / {{ returnStatistics.totals.receivedParcelCount }} 包裹 / {{ returnStatistics.totals.receivedPalletCount }} 托 · 预报载体数未提供
</div>
</section>
</template>
<template #notices><template v-if="loadError">
@@ -1311,8 +1306,6 @@ onMounted(() => { void load() })
</template>
<style scoped>
.return-filter-summary { display: flex; flex-wrap: wrap; gap: var(--admin-space-3); padding: var(--admin-space-3) var(--admin-space-5); border: 1px solid var(--cc-border); border-radius: var(--admin-radius); color: var(--cc-text); background: var(--cc-surface); font-size: var(--admin-font-body); }
.return-filter-summary > span { color: var(--cc-muted); }
.last-mile-dialog-facts, .last-mile-command-table, .manual-nodes-editor { table-layout: fixed; }
.last-mile-dialog-facts th { width: var(--admin-label-width); }
.last-mile-command-table td:not([colspan]) { width: 50%; }
@@ -46,23 +46,16 @@ enableAutoUnmount(afterEach)
afterAll(() => vi.unstubAllGlobals())
describe('RelabelOrdersView', () => {
it('uses full-filter totals instead of the visible page and preserves unknown boxes', async () => {
it('hides filter totals and preserves row quantities and unknown boxes', async () => {
vi.mocked(listRelabelOrders).mockResolvedValue({ records: [{ id: '1', relabelNo: 'RL-PIECE', skuCodes: 'A', totalPackageCount: 100, status: 'PROCESSING' }], page: 1, size: 10, total: 45, pages: 5 })
vi.mocked(getRelabelQuantities).mockResolvedValue({ orderCount: 45, skuCount: 12, knownLabelPieceCount: 300, unknownPieceOrders: 1, knownCartonCount: 8, unknownCartonOrders: 2, orders: { 'RL-PIECE': { labelPieceCount: 100, cartonCount: null, skuCount: 1, duplicateCarton: false, quantityUnit: 'PIECE', sourcePalletCount: null, targetPalletCount: null } } })
const wrapper = mount(RelabelOrdersView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const summary = wrapper.get('[aria-label="全部筛选结果数量汇总"]')
expect(summary.text()).toContain('45 票')
expect(summary.text()).toContain('300 件')
expect(summary.text()).toContain('8 箱')
expect(wrapper.findAll('tbody tr').some(row => row.text().includes('RL-PIECE') && row.text().includes('未提供') && row.text().includes('100'))).toBe(true)
})
it('does not display all-unknown carton quantities as zero boxes', async () => {
vi.mocked(getRelabelQuantities).mockResolvedValue({ orderCount: 1, skuCount: 1, knownLabelPieceCount: 100, unknownPieceOrders: 0, knownCartonCount: 0, unknownCartonOrders: 1, orders: {} })
const wrapper = mount(RelabelOrdersView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('[aria-label="全部筛选结果数量汇总"]').text()).toContain('箱数未提供(1票)')
expect(wrapper.get('[aria-label="全部筛选结果数量汇总"]').text()).not.toContain('0 箱')
expect(wrapper.find('[aria-label="全部筛选结果数量汇总"]').exists()).toBe(false)
const cells = wrapper.findAll('tbody tr').find(row => row.text().includes('RL-PIECE'))!.findAll('td')
expect(cells[5]!.text()).toBe('—')
expect(cells[5]!.text()).toBe(cells[8]!.text())
expect(cells[6]!.text()).toBe('100')
})
it('shows every SKU and quantity in the review dialog while keeping one box', async () => {
vi.mocked(getRelabelQuantities).mockResolvedValue({ orderCount: 1, skuCount: 2, knownLabelPieceCount: 15, unknownPieceOrders: 0, knownCartonCount: 1, unknownCartonOrders: 0, orders: { 'RL-MULTI': { labelPieceCount: 15, cartonCount: 1, skuCount: 2, duplicateCarton: false, quantityUnit: 'CARTON', sourcePalletCount: null, targetPalletCount: null } } })
@@ -37,9 +37,7 @@ const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const quantities = ref<RelabelQuantityTotals | null>(null)
const quantityError = ref('')
function dimension(value: number | string | null | undefined) { return value == null ? '未提供' : String(value) }
function knownDimension(value: number | string, unknown: number | string, unit: string) { return Number(value) === 0 && Number(unknown) > 0 ? `${unit === '箱' ? '箱数' : unit === '托' ? '托盘数' : '贴标件数'}未提供(${unknown}票)` : `已知 ${value} ${unit}${Number(unknown) > 0 ? `(${unknown}票未提供)` : ''}` }
function dimension(value: number | string | null | undefined) { return value == null ? '—' : String(value) }
const filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '' })
const fields = computed<QueryField[]>(() => [
{ key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 },
@@ -68,7 +66,7 @@ const dialogMode = ref<'create' | 'edit'>('create')
const submitting = ref(false)
const detailLoading = ref(false)
const editSourceStatus = ref('')
const auditLabelPieces = ref('未提供')
const auditLabelPieces = ref('—')
const form = reactive({ orderNo: '', customerId: '', relatedOrderNo: '', sku: '', productName: '', cartons: undefined as number | undefined, budgetTotal: undefined as number | undefined, status: 'PENDING_PROCESSING', labelRequirement: '', relabelCost: undefined as number | undefined, remark: '', warehouseId: '', labelDeadline: '', contactName: '', contactPhone: '', shipmentNo: '', newSkuCode: '' })
const createItems = ref<CreateRelabelItemPayload[]>([])
const createOperation = ref<CreateRelabelItemPayload['operationType']>('CARTON_LABEL')
@@ -253,12 +251,11 @@ async function load(model: QueryModel = filters.value) {
loading.value = true; loadError.value = null
try {
const query = { page: page.value, size: pageSize.value, relabelNo: String(filters.value.relabelNo || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined }
quantities.value = null; quantityError.value = ''
quantities.value = null
const result = await listRelabelOrders(query)
const measured = await getRelabelQuantities(query).catch(() => null)
if (current !== loadGeneration) return
quantities.value = measured
if (!measured) quantityError.value = '数量汇总加载失败,请重试;未将当前页作为全部合计'
rows.value = result.records.map((item) => ({
customerId: '', orderNo: String(item.relabelNo), relatedOrderNo: String(item.lastlegOrderNo || item.customerOrderNo || '—'), sku: String(item.skuCodes || '—'),
productName: item.productNameZh?.trim() || '—', cartons: dimension(quantities.value?.orders[item.relabelNo]?.cartonCount), labelPieces: dimension(quantities.value?.orders[item.relabelNo]?.labelPieceCount), skuCount: dimension(quantities.value?.orders[item.relabelNo]?.skuCount), budgetTotal: null, relabelCost: Number(item.chargeAmount || 0), walletStatus: walletStatus(item.walletStatus), walletErrorMessage: item.walletErrorMessage || '', labelRequirement: item.labelRequirement?.trim() || '—', fileName: '—', status: relabelStatus(item.status), rawStatus: String(item.status || ''), remark: '',
@@ -280,7 +277,6 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
<AdminPage class="admin-density"><ListPageLayout aria-label="换标单列表">
<section class="quantity-summary" aria-label="全部筛选结果数量汇总"><template v-if="quantities"><strong>全部筛选结果</strong><span>{{ quantities.orderCount }} 票</span><span>{{ quantities.skuCount }} 种 SKU</span><span>{{ knownDimension(quantities.knownLabelPieceCount, quantities.unknownPieceOrders, '件') }}</span><span>{{ knownDimension(quantities.knownCartonCount, quantities.unknownCartonOrders, '箱') }}</span><span>原托盘:{{ knownDimension(quantities.knownSourcePalletCount ?? 0, quantities.unknownSourcePalletOrders ?? quantities.orderCount, '托') }}</span><span>本轮目标托盘:{{ knownDimension(quantities.knownTargetPalletCount ?? 0, quantities.unknownTargetPalletOrders ?? quantities.orderCount, '托') }}</span></template><span v-else-if="quantityError" role="alert">{{ quantityError }}</span></section>
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-relabel-orders" row-key="orderNo" empty-text="暂无换标单" @selection-change="selectedRows = $event" @update:page="load()" @update:page-size="page = 1; load()">
<template #toolbar-start><h1>换标单</h1></template>
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="换标单筛选" preference-key="overseas-relabel-orders-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
@@ -326,7 +322,6 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
</template>
<style scoped>
.quantity-summary { display: flex; flex-wrap: wrap; gap: 8px 18px; padding: 12px 16px; border: 1px solid var(--cc-border); border-radius: 8px; margin-bottom: 12px; }
.el-form :deep(.el-row) { margin-left: 0 !important; margin-right: 0 !important; }
.el-form :deep(.el-input), .el-form :deep(.el-select), .el-form :deep(.el-input-number), .el-form :deep(.el-date-editor) { width: 100%; }
.relabel-editor-table { table-layout: fixed; }
@@ -21,7 +21,7 @@ export const unifiedOrderStatusOptions: Array<{ value: UnifiedOrderMainStatus; l
{ value: 'WEIGHT_CONFIRMED', label: '已确认数据' },
{ value: 'WAIT_CUSTOMS_DOCS', label: '待上传资料' },
{ value: 'WAIT_PALLETIZE', label: '一件代发打托' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'WAIT_SPLIT', label: '待装柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'EXPORT_CUSTOMS', label: '出口报关' },
@@ -302,7 +302,7 @@ export interface UnifiedOrderQuery {
tenantId?: string
keyword?: string
orderNo?: string
mainStatus?: UnifiedOrderMainStatus
mainStatus?: UnifiedOrderMainStatus | 'MEASUREMENT_REVIEW'
customerId?: string
receivingWarehouseId?: string
destinationWarehouseId?: string
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import type { UnifiedOrderResource } from './api/unifiedOrders'
import { toMeasurementReviewTask } from './measurementReviewTasks'
describe('measurement review task identity', () => {
it('keeps original tenant, inbound identity and optimistic version for approval', () => {
const task = toMeasurementReviewTask({
orderId: '9007199254740995', tenantId: '9007199254740993', inboundId: '9007199254740994', inboundVersion: 0,
inboundCode: 'IN-1', customerId: '9007199254740993', fulfillmentType: 'ONE_CLICK_DROPSHIP',
measurement: { reviewStatus: 'PENDING', actualGrossWeightKg: 12, actualVolumeCbm: 0.2, chargeableWeightKg: 13, completedAt: '2026-10-20T08:00:00' },
} as UnifiedOrderResource)
expect(task).toMatchObject({ id: '9007199254740994', tenantId: '9007199254740993', orderId: '9007199254740995', version: 0, inboundCode: 'IN-1', measureReviewStatus: 'PENDING', fulfillmentType: 'ONE_CLICK_DROPSHIP', actualGrossWeightKg: 12, actualVolumeCbm: 0.2 })
})
it('rejects an order without an inbound identity instead of approving a different resource', () => {
expect(() => toMeasurementReviewTask({ orderId: '1', inboundId: null } as UnifiedOrderResource)).toThrow('缺少入库单标识')
})
})
@@ -0,0 +1,66 @@
import type { InboundOrderSummary } from '@/modules/inbounds'
import type { UnifiedOrderResource } from './api/unifiedOrders'
/** 将统一订单查询的审核待办映射到现有测量审核操作,保留原始客户、入库单与版本。 */
export function toMeasurementReviewTask(row: UnifiedOrderResource): InboundOrderSummary {
if (row.inboundId == null) throw new Error('测量审核订单缺少入库单标识,请刷新后重试')
return {
id: String(row.inboundId),
tenantId: String(row.tenantId),
orderId: String(row.orderId),
orderNo: row.orderNo,
inboundCode: row.inboundCode,
processStatus: 'MEASURE_PENDING',
orderStatusSnapshot: row.internalOrderStatus,
measureReviewStatus: row.measurement?.reviewStatus ?? null,
feeStatus: null,
feeReviewStatus: row.feeReviewStatus,
notificationStatus: null,
version: row.inboundVersion,
expectedArrivalAt: row.expectedArrivalAt,
customerId: row.customerId == null ? null : String(row.customerId),
customerName: row.customerName,
companyId: null,
companyName: row.companyName ?? null,
warehouseId: row.receivingWarehouseId == null ? null : String(row.receivingWarehouseId),
warehouseName: row.receivingWarehouseName,
deliveryWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
deliveryWarehouseName: row.destinationWarehouseName,
businessType: row.businessType ?? null,
fulfillmentType: row.fulfillmentType ?? null,
channelCode: row.channelCode,
channelName: row.channelName,
taxMode: row.taxMode ?? null,
containerMode: null,
customsInspection: row.customsInspection ?? null,
destinationType: row.destinationType ?? null,
destinationWarehouseId: row.destinationWarehouseId == null ? null : String(row.destinationWarehouseId),
destinationCode: null,
destinationName: row.destinationWarehouseName,
holdFlag: row.holdFlag,
currentHoldReason: row.currentHoldReason,
totalPackageCount: row.totalPackageCount,
actualGrossWeightKg: row.measurement?.actualGrossWeightKg ?? row.actualGrossWeightKg,
actualNetWeightKg: row.totalNetWeightKg,
actualVolumeCbm: row.measurement?.actualVolumeCbm ?? row.actualVolumeCbm,
volumeWeightKg: row.volumetricWeightKg,
chargeableWeightKg: row.measurement?.chargeableWeightKg ?? null,
chargeableVolumeCbm: row.chargeableVolumeCbm,
palletCount: row.totalPalletCount,
locationCodes: row.locationCodes,
measureCompletedAt: row.measurement?.completedAt ?? row.measurementCompletedAt,
inboundedAt: row.inboundCompletedAt,
expressCompanyCode: null,
expressCompanyName: row.expressCompany,
expressTrackingNo: row.expressTrackingNo,
loadingStatus: row.transport?.loadingStatus ?? null,
containerNo: row.transport?.containerNo ?? null,
cabinetNo: row.transport?.cabinetNo ?? null,
vesselName: row.transport?.vesselName ?? null,
voyageNo: row.transport?.voyageNo ?? null,
sailingAt: row.transport?.effectiveSailingAt ?? null,
portArrivalAt: row.transport?.effectivePortArrivalAt ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
}
}
@@ -16,7 +16,12 @@ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
vi.mock('@/modules/inbounds', () => ({
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
InboundTaskList: { name: 'InboundTaskList', props: ['mode', 'title'], template: '<section><slot name="status-tabs" /></section>' },
InboundTaskList: {
name: 'InboundTaskList', props: ['mode', 'title', 'taskLoader'],
methods: { reload() { return this.taskLoader({ page: 1, size: 20 }) } },
mounted() { void this.reload() },
template: '<section><slot name="filters" :loading="false" /><slot name="status-tabs" /></section>',
},
}))
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
vi.mock('@/shared/components/exportTaskCreatedDialog', () => ({ showExportTaskCreatedDialog: vi.fn() }))
@@ -36,7 +41,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
unifiedOrderStatusOptions: [
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'WAIT_SPLIT', label: '待装柜' },
{ value: 'LOADING', label: '装柜中' },
{ value: 'LOADED', label: '装柜完成' },
{ value: 'DELIVERING', label: '派送中' },
@@ -76,6 +81,37 @@ describe('UnifiedOrdersView list request coordination', () => {
vi.mocked(showExportTaskCreatedDialog).mockReset().mockResolvedValue(false)
})
it('shares applied filters, advanced fields and reset across measurement review and other tabs', async () => {
const wrapper = mountView()
await flushPromises()
const model = { receivingWarehouseId: '9007199254740993', destinationWarehouseId: '200', inboundCode: ' IN-9 ', tenantId: '300', channelName: '海派', containerNo: 'CONT-1', cabinetNo: 'CAB-1', loaded: 'false', fullContainer: 'true', expectedArrivalDate: '2026-10-20', sailingDate: '2026-11-01', feeReviewStatus: 'APPROVED', measurementReviewStatus: 'PENDING', fbaCode: 'SKU-1', createdRange: ['2026-10-01', '2026-10-20'] }
const toolbar = wrapper.getComponent({ name: 'QueryToolbar' })
toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), ...model })
await flushPromises()
toolbar.vm.$emit('search')
await flushPromises()
const ordinaryQuery = vi.mocked(listPlatformUnifiedOrders).mock.lastCall![0]
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('测量数据审核'))!.trigger('click')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith({ ...ordinaryQuery, mainStatus: 'MEASUREMENT_REVIEW' })
const reviewToolbar = wrapper.getComponent({ name: 'QueryToolbar' })
expect(reviewToolbar.props('modelValue')).toMatchObject(model)
expect(reviewToolbar.attributes('preference-key')).toBe('unified-orders-query-v2')
expect(reviewToolbar.attributes('available-fields')).toBeDefined()
reviewToolbar.vm.$emit('reset')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', receivingWarehouseId: undefined, inboundCode: undefined, channelName: undefined, tenantId: undefined }))
reviewToolbar.vm.$emit('update:modelValue', { ...reviewToolbar.props('modelValue'), inboundCode: 'IN-REVIEW' })
await flushPromises()
reviewToolbar.vm.$emit('search')
await flushPromises()
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'MEASUREMENT_REVIEW', inboundCode: 'IN-REVIEW' }))
await wrapper.findAll('[role="tab"]').find(tab => tab.text().startsWith('待审核'))!.trigger('click')
await flushPromises()
expect(wrapper.getComponent({ name: 'QueryToolbar' }).props('modelValue')).toMatchObject({ inboundCode: 'IN-REVIEW' })
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'PENDING_REVIEW', inboundCode: 'IN-REVIEW' }))
})
it('shows only supported counts, keeps all text-only and never scopes counts by status or pagination', async () => {
vi.mocked(getPlatformUnifiedOrderStatusCounts).mockResolvedValue({ counts: { PENDING_REVIEW: 12, WAIT_MEASURE: 0, SIGNED: 3 } })
const wrapper = mountView()
@@ -166,13 +202,13 @@ describe('UnifiedOrdersView list request coordination', () => {
await flushPromises()
expect(wrapper.getComponent({ name: 'InboundTaskList' }).props()).toMatchObject({ mode: 'review', title: '订单列表' })
expect(wrapper.get('[aria-selected="true"]').text()).toBe('测量数据审核')
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
await wrapper.findAll('[role="tab"]').find((tab) => tab.text() === '待测量数据')!.trigger('click')
await flushPromises()
expect(wrapper.findComponent({ name: 'InboundTaskList' }).exists()).toBe(false)
expect(listPlatformUnifiedOrders).toHaveBeenLastCalledWith(expect.objectContaining({ mainStatus: 'WAIT_MEASURE' }))
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2)
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(3)
})
it('does not apply draft filters to pagination until query is submitted', async () => {
@@ -62,7 +62,7 @@ describe('UnifiedOrdersView contract', () => {
it('exposes the product main statuses including dropship palletizing', () => {
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待分柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '签收完成',
'待审核', '待测量数据', '待客户确认', '已确认数据', '待上传资料', '一件代发打托', '待装柜', '装柜中', '装柜完成', '出口报关', '已开船/待清关', '待拆柜', '待排车', '出库单', '派送中', '签收完成',
])
for (const legacyLabel of ['差异处理中', '进入分柜']) {
expect(source).not.toContain(legacyLabel)
@@ -27,6 +27,7 @@ import {
type LegacyOrderImportResult,
} from '@/modules/unified-orders/api/unifiedOrders'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
import { toMeasurementReviewTask } from '../measurementReviewTasks'
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
import OrderDeclarationEditorDialog from '@/modules/unified-orders/components/OrderDeclarationEditorDialog.vue'
@@ -330,6 +331,7 @@ const showAdvancedFilters = ref(false)
const filters = ref<FilterModel>(emptyFilters())
const appliedFilters = ref<FilterModel>(emptyFilters())
const selectedStatus = ref<UnifiedOrderMainStatus | ''>('')
const measurementReviewList = ref<InstanceType<typeof InboundTaskList> | null>(null)
const measurementReviewSelected = ref(false)
const selectedTab = computed(() => measurementReviewSelected.value ? 'MEASUREMENT_REVIEW' : selectedStatus.value)
const statusTabList = ref<HTMLElement | null>(null)
@@ -1641,9 +1643,14 @@ function tabCount(value: UnifiedOrderMainStatus | '' | 'MEASUREMENT_REVIEW') {
return value ? statusCounts.value[value] : undefined
}
async function loadMeasurementReviewTasks(pagination: { page: number; size: number }) {
const result = await listPlatformUnifiedOrders({ ...orderCountFilters(), mainStatus: 'MEASUREMENT_REVIEW', ...pagination })
return { ...result, records: result.records.map(toMeasurementReviewTask) }
}
async function loadOrders(refreshCounts = true) {
if (refreshCounts) void loadStatusCounts()
if (measurementReviewSelected.value) return
if (measurementReviewSelected.value) { await measurementReviewList.value?.reload(); return }
selectedApprovalOrders.value = []
selectedOrders.value = []
loading.value = true
@@ -2777,8 +2784,9 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<template v-else>
<div v-if="measurementReviewSelected" id="unified-orders-results" role="tabpanel">
<InboundTaskList mode="review" title="订单列表" @changed="loadStatusCounts">
<template #status-tabs>
<InboundTaskList ref="measurementReviewList" mode="review" title="订单列表" :task-loader="loadMeasurementReviewTasks" @changed="loadStatusCounts">
<template #filters="{ loading: reviewLoading }">
<div class="orders-query-panel integrated-query-group">
<div ref="statusTabList" class="status-tabs density-status-tabs" role="tablist" aria-label="订单主状态" :aria-busy="statusCountsLoading">
<button
v-for="(tab, index) in statusTabs"
@@ -2799,6 +2807,15 @@ function orderDialogTitle(action: string, inboundCode?: string | null) {
<el-button link type="primary" @click="loadStatusCounts">重试数量</el-button>
<ApiErrorMeta :details="statusCountsError" />
</div>
<QueryToolbar id="unified-orders-advanced-filters" v-model="filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :loading="reviewLoading" :search-on-reset="false" @search="search" @reset="reset">
<template #actions>
<el-button :aria-expanded="showAdvancedFilters" aria-controls="unified-orders-advanced-filters" @click="showAdvancedFilters = !showAdvancedFilters">
<SlidersHorizontal :size="16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
<ChevronUp v-if="showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
</el-button>
</template>
</QueryToolbar>
</div>
</template>
</InboundTaskList>
</div>