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 18:06:37 +08:00
7 changed files with 43 additions and 26 deletions
@@ -81,7 +81,6 @@ describe('GlobalSmartLoadingView', () => {
{ label: '待装柜', value: 'WAIT_LOADING' },
{ label: '预装柜中', value: 'EDITING' },
{ label: '拣货中', value: 'LOADING' },
{ label: '装柜完成', value: 'COMPLETED' },
])
expect(wrapper.text()).not.toContain('税率')
@@ -99,6 +98,28 @@ describe('GlobalSmartLoadingView', () => {
expect(wrapper.text()).toContain('CB-1 装柜方案')
})
it('does not overwrite the container status when selecting it', async () => {
const waiting = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-WAIT', planStatus: 'WAIT_LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [container, waiting], page: 1, size: 100, total: 2, pages: 1 })
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => (
bookingId === waiting.id
? { ...plan, bookingId: waiting.id, status: 'LOADING' as const }
: plan
))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const waitingOption = wrapper.findAll('.container-option').find((item) => item.text().includes('CB-WAIT'))!
expect(waitingOption.text()).toContain('待装柜')
await waitingOption.trigger('click')
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(waiting.id)
expect(waitingOption.text()).toContain('待装柜')
expect(waitingOption.text()).not.toContain('拣货中')
})
it('keeps picking containers editable while hiding the push action', async () => {
const pushed = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-2', planStatus: 'LOADING' as const }
const pickingPlan = { ...plan, bookingId: pushed.id, status: 'LOADING' as const }
@@ -116,19 +137,15 @@ describe('GlobalSmartLoadingView', () => {
expect(wrapper.findAll('button').some((item) => item.text().trim() === '推送预装')).toBe(false)
})
it('routes completed containers to the read-only loading list', async () => {
it('hides completed containers from the smart loading workbench', async () => {
const completed = { ...container, id: '9007199254740997', planId: '9007199254740998', cabinetNo: 'CB-3', planStatus: 'COMPLETED' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [completed], page: 1, size: 100, total: 1, pages: 1 })
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('该柜次已完成装柜')
await wrapper.findAll('button').find((item) => item.text().includes('查看装柜清单'))!.trigger('click')
expect(routerPush).toHaveBeenCalledWith({
name: 'loading-lists',
query: { planId: completed.planId, warehouseId: completed.warehouseId },
})
expect(wrapper.text()).not.toContain('CB-3')
expect(wrapper.text()).not.toContain('装柜完成')
})
it('refreshes the latest plan after A0409 without losing the selected container', async () => {
@@ -74,7 +74,6 @@ const queryFields = computed<QueryField[]>(() => [
{ label: '待装柜', value: 'WAIT_LOADING' },
{ label: '预装柜中', value: 'EDITING' },
{ label: '拣货中', value: 'LOADING' },
{ label: '装柜完成', value: 'COMPLETED' },
] },
])
@@ -82,6 +81,7 @@ const filteredContainers = computed(() => {
const keyword = String(appliedQuery.value.containerKeyword || '').trim().toLowerCase()
const status = String(appliedQuery.value.status || '')
return containers.value.filter((item) => {
if (item.planStatus === 'COMPLETED') return false
if (status && item.planStatus !== status) return false
if (!keyword) return true
return [item.cabinetNo, item.containerNo, item.bookingNo, item.destinationWarehouseCode, item.destinationWarehouseName]
@@ -142,14 +142,14 @@ function syncDrafts(next: SmartLoadingPlan, preserved: Record<string, number> =
for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key]
for (const item of next.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount
}
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}) {
function acceptPlan(next: SmartLoadingPlan, preserved: Record<string, number> = {}, syncContainerStatus = true) {
plan.value = next
noDraft.value = false
syncDrafts(next, preserved)
const container = selectedContainer.value
if (container) {
container.planId = next.id
container.planStatus = next.status
if (syncContainerStatus) container.planStatus = next.status
}
}
function clearSelectionData() {
@@ -178,7 +178,7 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
])
if (request !== selectionRequest) return
candidates.value = candidateResult
if (planResult.value) acceptPlan(planResult.value, preserved)
if (planResult.value) acceptPlan(planResult.value, preserved, false)
else noDraft.value = planResult.missing
} catch (error) {
if (request === selectionRequest) selectionError.value = getApiErrorDetails(error, '柜次装柜数据加载失败')
@@ -382,7 +382,7 @@ onMounted(load)
</section>
<aside class="dispatch-pane container-pane" aria-label="货柜列表">
<header class="pane-header"><div><strong>货柜</strong><small>待装柜、预装柜中、拣货中与装柜完成</small></div></header>
<header class="pane-header"><div><strong>货柜</strong><small>待装柜、预装柜中与拣货中</small></div></header>
<div v-loading="loading" class="container-list">
<el-empty v-if="!filteredContainers.length" description="暂无符合条件的货柜" :image-size="72" />
<button
@@ -43,11 +43,11 @@ const detail = {
describe('platform loading lists API', () => {
beforeEach(() => vi.restoreAllMocks())
it('queries preload plans across all warehouses with fixed LOADING status', async () => {
it('queries preload plans without forcing a status so EDITING and LOADING are returned', async () => {
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: { records: [preloadSummary], page: '1', size: '20', total: '1', pages: '1' } })
const result = await listPreloadPlans({ page: 1, size: 20, keyword: 'PL' })
expect(get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-plans', { params: { page: 1, size: 20, keyword: 'PL', status: 'LOADING' } })
expect(get).toHaveBeenCalledWith('/platform/v1/first-leg/loading-plans', { params: { page: 1, size: 20, keyword: 'PL' } })
expect(result.total).toBe(1)
expect(result.records[0]?.planId).toBe('1900000000000000001')
expect(result.records[0]?.customers[0]?.accountId).toBe('1900000000000000002')
@@ -203,7 +203,7 @@ function idempotencyKey(planId: string) {
export async function listPreloadPlans(query: LoadingListQuery) {
const response = await http.get<ApiEnvelope<RawPage<PreloadSummary>>, ApiEnvelope<RawPage<PreloadSummary>>>(
`${basePath}/loading-plans`,
{ params: { ...query, status: 'LOADING' } },
{ params: query },
)
return normalizePage(unwrapApiResponse(response), normalizePreloadSummary)
}
@@ -102,7 +102,7 @@ function formatNumber(value?: number | null, digits = 3) {
</section>
</template>
<div v-if="mode === 'preload' && canEdit && summary.status === 'LOADING'" class="detail-actions">
<div v-if="mode === 'preload' && canEdit && ['EDITING', 'LOADING'].includes(summary.status)" class="detail-actions">
<el-button type="primary" @click="emit('edit')"><Pencil :size="16" />编辑运输信息</el-button>
</div>
<el-alert v-else-if="mode === 'confirmed'" type="info" :closable="false" show-icon title="装柜完成后清单、明细和运输信息永久只读" />
@@ -34,7 +34,7 @@ describe('LoadingListsView', () => {
})
it('only exposes transport editing for LOADING preload details', () => {
expect(detailSource).toContain("mode === 'preload' && canEdit && summary.status === 'LOADING'")
expect(detailSource).toContain("mode === 'preload' && canEdit && ['EDITING', 'LOADING'].includes(summary.status)")
expect(detailSource).toContain('装柜完成后清单、明细和运输信息永久只读')
expect(source).toContain('expectedVersion: detail.value.summary.version')
})
@@ -50,7 +50,7 @@ describe('LoadingListsView', () => {
})
it('exposes a row edit action only for loading preload plans', () => {
expect(source).toContain("isPreload && canEdit && row.status === 'LOADING'")
expect(source).toContain("isPreload && canEdit && ['EDITING', 'LOADING'].includes(row.status)")
expect(source).toContain('openEditFromRow(row)')
expect(source).toContain('await loadDetail()')
})
@@ -65,7 +65,7 @@ describe('LoadingListsView', () => {
expect(source).toContain('useRoute()')
expect(source).toContain('route.query.planId')
expect(source).toContain('route.query.warehouseId')
expect(source).toContain("const expectedStatus = isPreload.value ? 'LOADING' : 'COMPLETED'")
expect(source).toContain("const expectedStatuses = isPreload.value ? ['EDITING', 'LOADING'] : ['COMPLETED']")
expect(source).toContain('MODE_STATUS_MISMATCH')
})
@@ -159,9 +159,9 @@ async function openDeepLinkedDetail() {
detailError.value = null
detailOpen.value = true
await loadDetail()
const expectedStatus = isPreload.value ? 'LOADING' : 'COMPLETED'
const expectedStatuses = isPreload.value ? ['EDITING', 'LOADING'] : ['COMPLETED']
const loadedDetail = detail.value as LoadingDetail | null
if (loadedDetail && loadedDetail.summary.status !== expectedStatus) {
if (loadedDetail && !expectedStatuses.includes(loadedDetail.summary.status)) {
detail.value = null
detailError.value = {
code: 'MODE_STATUS_MISMATCH',
@@ -211,7 +211,7 @@ function openDetail(rowValue: object) {
async function openEditFromRow(rowValue: object) {
const row = rowValue as LoadingListSummary
if (!isPreload.value || !canEdit.value || row.status !== 'LOADING') return
if (!isPreload.value || !canEdit.value || !['EDITING', 'LOADING'].includes(row.status)) return
selectedPlanId.value = String(row.planId)
selectedWarehouseId.value = String(row.warehouseId || '')
detail.value = null
@@ -219,11 +219,11 @@ async function openEditFromRow(rowValue: object) {
detailOpen.value = true
await loadDetail()
const loadedDetail = detail.value as LoadingDetail | null
if (loadedDetail && loadedDetail.summary.status === 'LOADING') openEdit()
if (loadedDetail && ['EDITING', 'LOADING'].includes(loadedDetail.summary.status)) openEdit()
}
function openEdit() {
if (!detail.value || !('preloadNo' in detail.value.summary) || detail.value.summary.status !== 'LOADING') return
if (!detail.value || !('preloadNo' in detail.value.summary) || !['EDITING', 'LOADING'].includes(detail.value.summary.status)) return
editForm.containerNo = detail.value.summary.containerNo || ''
editForm.voyageNo = detail.value.summary.voyageNo || ''
editForm.operationReason = ''
@@ -306,7 +306,7 @@ onMounted(() => {
>
<template #actions="{ row }">
<el-button v-if="canView" link type="primary" @click="openDetail(row)"><Eye :size="15" />查看</el-button>
<el-button v-if="isPreload && canEdit && row.status === 'LOADING'" link type="primary" @click="openEditFromRow(row)"><Pencil :size="15" />编辑</el-button>
<el-button v-if="isPreload && canEdit && ['EDITING', 'LOADING'].includes(row.status)" link type="primary" @click="openEditFromRow(row)"><Pencil :size="15" />编辑</el-button>
</template>
<template #customers="{ row }">{{ (row as PreloadSummary).customers.map((item) => item.customerName || item.accountId).join('、') || '—' }}</template>
<template #vesselVoyage="{ row }">{{ (row as PreloadSummary).vesselName || '—' }} / {{ (row as PreloadSummary).voyageNo || '—' }}</template>