优化
This commit is contained in:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user