fix(overseas): harden receiving warehouse change
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { createPinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import DataTable from '@/shared/components/DataTable.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import { listAllPlatformWarehouses } from '@/modules/warehouses'
|
||||
import {
|
||||
@@ -61,6 +64,10 @@ const receivingWarehouse = {
|
||||
...fbaWarehouse,
|
||||
id: '9007199254740996', warehouseCode: 'LAX02', warehouseName: '洛杉矶二号接柜仓', warehouseType: 'OVERSEAS',
|
||||
}
|
||||
const currentReceivingWarehouse = {
|
||||
...receivingWarehouse,
|
||||
id: summary.overseasWarehouseId, warehouseCode: summary.overseasWarehouseCode, warehouseName: summary.overseasWarehouseName,
|
||||
}
|
||||
function selectedRow(overrides: Record<string, unknown> = {}) {
|
||||
return { id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001', ...overrides }
|
||||
}
|
||||
@@ -309,24 +316,55 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
||||
})
|
||||
|
||||
it('shows the receiving warehouse and changes it from ACTIVE OVERSEAS master data with versioning', async () => {
|
||||
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([receivingWarehouse] as never)
|
||||
vi.mocked(listAllPlatformWarehouses).mockResolvedValueOnce([currentReceivingWarehouse, receivingWarehouse] as never)
|
||||
const wrapper = await mountView()
|
||||
await wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('海外接柜仓')
|
||||
expect(wrapper.text()).toContain('LAX01 · 洛杉矶接柜仓')
|
||||
expect(wrapper.get('.container-workbench-card__warehouse strong').attributes('title')).toBe('LAX01 · 洛杉矶接柜仓')
|
||||
expect(listAllPlatformWarehouses).not.toHaveBeenCalled()
|
||||
await button(wrapper, '修改接柜仓')!.trigger('click')
|
||||
await flushPromises()
|
||||
const receivingDialog = wrapper.findAllComponents(AppDialog).find((item) => item.props('title') === '修改海外接柜仓')!
|
||||
expect(receivingDialog.props('width')).toBe('min(600px, calc(100vw - 28px))')
|
||||
expect(listAllPlatformWarehouses).toHaveBeenLastCalledWith({ warehouseType: 'OVERSEAS', status: 'ACTIVE' })
|
||||
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
|
||||
const warehouseSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择 ACTIVE 海外接柜仓')!
|
||||
expect(warehouseSelect.props('modelValue')).toBe('')
|
||||
expect(wrapper.text()).not.toContain('9007199254740995')
|
||||
const warehouseOptions = wrapper.findAllComponents({ name: 'ElOption' })
|
||||
expect(warehouseOptions.find((item) => item.props('value') === summary.overseasWarehouseId)?.props('disabled')).toBe(true)
|
||||
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
|
||||
warehouseSelect.vm.$emit('update:modelValue', summary.overseasWarehouseId)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(confirm.disabled).toBe(true)
|
||||
expect(wrapper.text()).toContain('新的海外接柜仓必须与当前仓不同')
|
||||
warehouseSelect.vm.$emit('update:modelValue', '9007199254740996')
|
||||
await wrapper.vm.$nextTick()
|
||||
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
|
||||
expect(confirm.disabled).toBe(false)
|
||||
confirm.click()
|
||||
await flushPromises()
|
||||
expect(changeContainerReceivingWarehouse).toHaveBeenCalledWith(bookingId, {
|
||||
overseasWarehouseId: '9007199254740996', expectedVersion: 7, remark: undefined,
|
||||
}, expect.stringMatching(/^container-receiving-warehouse-/))
|
||||
expect(listContainerPlanProgress).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('keeps the receiving-warehouse confirmation disabled while candidates are loading', async () => {
|
||||
let resolveCandidates!: (value: typeof receivingWarehouse[]) => void
|
||||
vi.mocked(listAllPlatformWarehouses).mockReturnValueOnce(new Promise((resolve) => { resolveCandidates = resolve }) as never)
|
||||
const wrapper = await mountView()
|
||||
await wrapper.findAll('.el-radio-button').find((item) => item.text().startsWith('柜子'))!.trigger('click')
|
||||
await flushPromises()
|
||||
await button(wrapper, '修改接柜仓')!.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认修改')) as HTMLButtonElement
|
||||
expect(confirm.disabled).toBe(true)
|
||||
expect(listAllPlatformWarehouses).toHaveBeenCalledTimes(1)
|
||||
resolveCandidates([receivingWarehouse])
|
||||
await flushPromises()
|
||||
expect(confirm.disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('does not expose a receiving-warehouse edit when the server locks the container', async () => {
|
||||
@@ -390,6 +428,14 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
||||
expect(changeContainerReceivingWarehouse).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps dense cabinet fields and actions on one line with ellipsis at 1920px', () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/LastMileOrdersView.vue'), 'utf8')
|
||||
expect(source).toContain('.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }')
|
||||
expect(source).toContain('.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }')
|
||||
expect(source).toContain('.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; white-space: nowrap; }')
|
||||
expect(source).toContain("{ key: 'actions', label: '操作', width: 110, fixed: 'left'")
|
||||
})
|
||||
|
||||
it('loads only active FBA master data and submits exact string fields for a cross-customer transfer', async () => {
|
||||
const wrapper = await openPending()
|
||||
await selectRows(wrapper, [selectedRow(), selectedRow({ id: '2092873463568596995', customerId: '2092618717861048322' })])
|
||||
|
||||
@@ -90,6 +90,10 @@ const fields = computed<QueryField[]>(() => statusTab.value === '柜子'
|
||||
? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key))
|
||||
: allFields)
|
||||
const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value))
|
||||
const selectedReceivingWarehouse = computed(() => receivingWarehouseOptions.value.find((row) => (
|
||||
row.warehouseId === receivingWarehouseForm.warehouseId
|
||||
&& row.warehouseId !== receivingWarehouseTarget.value?.overseasWarehouseId
|
||||
)))
|
||||
|
||||
interface TransferWarehouseOption {
|
||||
warehouseId: string
|
||||
@@ -304,7 +308,7 @@ async function loadReceivingWarehouseOptions() {
|
||||
function openReceivingWarehouseChange(container: ContainerPlanProgressSummaryDto) {
|
||||
if (!container.canChangeReceivingWarehouse) return
|
||||
receivingWarehouseTarget.value = container
|
||||
Object.assign(receivingWarehouseForm, { warehouseId: container.overseasWarehouseId || '', remark: '' })
|
||||
Object.assign(receivingWarehouseForm, { warehouseId: '', remark: '' })
|
||||
receivingWarehouseIdempotencyKey = newReceivingWarehouseIdempotencyKey()
|
||||
receivingWarehouseOptions.value = []
|
||||
receivingWarehouseError.value = null
|
||||
@@ -315,13 +319,14 @@ function openReceivingWarehouseChange(container: ContainerPlanProgressSummaryDto
|
||||
}
|
||||
async function submitReceivingWarehouseChange() {
|
||||
const target = receivingWarehouseTarget.value
|
||||
const selected = receivingWarehouseOptions.value.find((warehouse) => warehouse.warehouseId === receivingWarehouseForm.warehouseId)
|
||||
const selected = selectedReceivingWarehouse.value
|
||||
if (!target || !selected) {
|
||||
ElMessage.warning('请选择有效的海外接柜仓')
|
||||
ElMessage.warning('请选择与当前仓不同的有效海外接柜仓')
|
||||
return
|
||||
}
|
||||
actionLoading.value = true
|
||||
actionError.value = null
|
||||
receivingWarehouseActionError.value = null
|
||||
try {
|
||||
await changeContainerReceivingWarehouse(target.sourceContainerBookingId, {
|
||||
overseasWarehouseId: selected.warehouseId,
|
||||
@@ -508,11 +513,11 @@ onMounted(() => { void load() })
|
||||
</header>
|
||||
<div v-if="containerRows.length" class="container-workbench__grid">
|
||||
<article v-for="container in containerRows" :key="container.sourceContainerBookingId" class="container-workbench-card">
|
||||
<span class="container-workbench-card__top"><strong>{{ container.containerNo || '柜号待补充' }}</strong></span>
|
||||
<span class="container-workbench-card__top"><strong :title="container.containerNo || '柜号待补充'">{{ container.containerNo || '柜号待补充' }}</strong></span>
|
||||
<span class="container-workbench-card__warehouse">
|
||||
<small>海外接柜仓</small><strong>{{ receivingWarehouseLabel(container) }}</strong>
|
||||
<small v-if="containerSnapshotBlockReason(container)" class="is-locked">{{ containerSnapshotBlockReason(container) }}</small>
|
||||
<small v-else-if="!container.canChangeReceivingWarehouse && container.receivingWarehouseLockReason" class="is-locked">{{ container.receivingWarehouseLockReason }}</small>
|
||||
<small>海外接柜仓</small><strong :title="receivingWarehouseLabel(container)">{{ receivingWarehouseLabel(container) }}</strong>
|
||||
<small v-if="containerSnapshotBlockReason(container)" class="is-locked" :title="containerSnapshotBlockReason(container)">{{ containerSnapshotBlockReason(container) }}</small>
|
||||
<small v-else-if="!container.canChangeReceivingWarehouse && container.receivingWarehouseLockReason" class="is-locked" :title="container.receivingWarehouseLockReason">{{ container.receivingWarehouseLockReason }}</small>
|
||||
</span>
|
||||
<span class="container-workbench-card__arrival"><small>计划到仓</small><strong>{{ formatDateTime(container.plannedArrivalAt) }}</strong></span>
|
||||
<span class="container-workbench-card__pending">
|
||||
@@ -576,7 +581,7 @@ onMounted(() => { void load() })
|
||||
<template v-else-if="containerDetail">
|
||||
<div class="container-detail-summary">
|
||||
<span>柜号:{{ containerDetail.summary.containerNo || '—' }}</span>
|
||||
<span>海外接柜仓:{{ receivingWarehouseLabel(containerDetail.summary) }}</span>
|
||||
<span :title="`海外接柜仓:${receivingWarehouseLabel(containerDetail.summary)}`">海外接柜仓:{{ receivingWarehouseLabel(containerDetail.summary) }}</span>
|
||||
<span>计划到仓:{{ formatDateTime(containerDetail.summary.plannedArrivalAt) }}</span>
|
||||
<span>订单:{{ containerDetail.summary.totalOrderCount }}</span>
|
||||
<span>入仓码:{{ containerDetail.summary.inboundCodeCount }}</span>
|
||||
@@ -588,7 +593,7 @@ onMounted(() => { void load() })
|
||||
</DataTable>
|
||||
</template>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="receivingWarehouseOpen" title="修改海外接柜仓" width="min(600px, calc(100vw - 28px))" confirm-text="确认修改" :loading="receivingWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!receivingWarehouseForm.warehouseId || Boolean(receivingWarehouseError)" @confirm="submitReceivingWarehouseChange">
|
||||
<AppDialog v-model="receivingWarehouseOpen" title="修改海外接柜仓" width="min(600px, calc(100vw - 28px))" confirm-text="确认修改" :loading="receivingWarehouseLoading" :confirm-loading="actionLoading" :confirm-disabled="!selectedReceivingWarehouse || Boolean(receivingWarehouseError)" @confirm="submitReceivingWarehouseChange">
|
||||
<LoadErrorState v-if="receivingWarehouseError" title="海外接柜仓候选加载失败" :message="receivingWarehouseError.message" compact @retry="loadReceivingWarehouseOptions">
|
||||
<template #details><ApiErrorMeta :details="receivingWarehouseError" /></template>
|
||||
</LoadErrorState>
|
||||
@@ -596,9 +601,10 @@ onMounted(() => { void load() })
|
||||
<el-form-item label="当前海外接柜仓"><el-input :model-value="receivingWarehouseLabel(receivingWarehouseTarget)" readonly /></el-form-item>
|
||||
<el-form-item label="新的海外接柜仓" required>
|
||||
<el-select v-model="receivingWarehouseForm.warehouseId" filterable clearable placeholder="请选择 ACTIVE 海外接柜仓">
|
||||
<el-option v-for="warehouse in receivingWarehouseOptions" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" />
|
||||
<el-option v-for="warehouse in receivingWarehouseOptions" :key="warehouse.warehouseId" :label="warehouse.label" :value="warehouse.warehouseId" :disabled="warehouse.warehouseId === receivingWarehouseTarget?.overseasWarehouseId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-alert v-if="receivingWarehouseForm.warehouseId === receivingWarehouseTarget?.overseasWarehouseId" title="新的海外接柜仓必须与当前仓不同" type="warning" :closable="false" show-icon />
|
||||
<el-alert v-if="!receivingWarehouseLoading && !receivingWarehouseOptions.length" title="暂无可用的 ACTIVE 海外接柜仓" type="info" :closable="false" show-icon />
|
||||
<el-form-item label="备注"><el-input v-model="receivingWarehouseForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入修改备注(选填)" /></el-form-item>
|
||||
<LoadErrorState v-if="receivingWarehouseActionError" title="海外接柜仓修改失败" :message="receivingWarehouseActionError.message" compact>
|
||||
@@ -613,6 +619,7 @@ onMounted(() => { void load() })
|
||||
.last-mile-toolbar :deep(.query-toolbar) { margin-top: 12px; padding: 12px 0 16px; border: 0; border-top: 1px solid var(--cc-border); border-radius: 0; }
|
||||
.container-convert-hint { margin: 0 0 16px; color: var(--cc-muted); line-height: 1.6; }
|
||||
.container-detail-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px 16px; margin-bottom: 12px; padding: 12px 14px; border: 1px solid var(--cc-border); border-radius: 6px; background: var(--cc-surface); font-size: 13px; }
|
||||
.container-detail-summary > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-workbench { padding: 18px; }
|
||||
.container-workbench__header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
|
||||
.container-workbench__header > div { display: grid; gap: 4px; }
|
||||
@@ -622,21 +629,22 @@ onMounted(() => { void load() })
|
||||
.container-workbench-card { display: grid; min-width: 0; gap: 14px; padding: 16px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; color: inherit; font: inherit; text-align: left; transition: border-color .16s ease, box-shadow .16s ease; }
|
||||
.container-workbench-card:hover { border-color: var(--cc-primary); box-shadow: 0 6px 18px rgb(15 23 42 / 8%); }
|
||||
.container-workbench-card__top { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.container-workbench-card__top strong { min-width: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 16px; }
|
||||
.container-workbench-card__top strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 16px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-workbench-card__top > span { flex: 0 0 auto; color: var(--cc-primary); font-size: 13px; }
|
||||
.container-workbench-card__arrival { display: grid; gap: 4px; }
|
||||
.container-workbench-card__arrival small { color: var(--cc-muted); font-size: 12px; }
|
||||
.container-workbench-card__arrival strong { color: var(--cc-text); font-size: 15px; }
|
||||
.container-workbench-card__warehouse { display: grid; gap: 4px; }
|
||||
.container-workbench-card__warehouse small { color: var(--cc-muted); font-size: 12px; }
|
||||
.container-workbench-card__warehouse strong { overflow-wrap: anywhere; color: var(--cc-text); font-size: 14px; }
|
||||
.container-workbench-card__warehouse .is-locked { color: #c2410c; }
|
||||
.container-workbench-card__warehouse strong { min-width: 0; overflow: hidden; color: var(--cc-text); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-workbench-card__warehouse .is-locked { min-width: 0; overflow: hidden; color: #c2410c; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.container-workbench-card__pending { display: flex; align-items: baseline; gap: 8px; padding: 12px 14px; border-left: 3px solid #f59e0b; background: #fffaf0; }
|
||||
.container-workbench-card__pending strong { color: #b45309; font-size: 28px; line-height: 1; }
|
||||
.container-workbench-card__pending span { color: #92400e; font-size: 13px; }
|
||||
.container-workbench-card__facts { display: flex; flex-wrap: wrap; gap: 6px 12px; color: var(--cc-muted); font-size: 12px; }
|
||||
.container-workbench-card__facts .is-warning { color: #c2410c; }
|
||||
.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
.container-workbench-card__actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; white-space: nowrap; }
|
||||
.container-workbench-card__actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.container-workbench__pagination { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--cc-border); color: var(--cc-muted); font-size: 13px; }
|
||||
@media (max-width: 1380px) { .container-workbench__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user