优化
This commit is contained in:
@@ -66,9 +66,9 @@ describe('platform container loading API', () => {
|
||||
it('normalizes every long id in candidates and plans', async () => {
|
||||
vi.mocked(http.get)
|
||||
.mockResolvedValueOnce({ code: '200', data: container })
|
||||
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] })
|
||||
.mockResolvedValueOnce({ code: '200', data: [{ accountId: 1, inboundId: 2, orderId: 4, inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null }] })
|
||||
expect((await getContainer('9')).id).toBe(String(container.id))
|
||||
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }])
|
||||
expect(await listSmartLoadingCandidates('7', '5')).toMatchObject([{ accountId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }])
|
||||
expect(http.get).toHaveBeenLastCalledWith('/platform/v1/first-leg/smart-loading/inbound-candidates', { params: { bookingId: '7', warehouseId: '5' } })
|
||||
})
|
||||
|
||||
|
||||
@@ -60,6 +60,15 @@ const plan = {
|
||||
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
|
||||
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
|
||||
}
|
||||
const candidate = {
|
||||
accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2',
|
||||
orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00',
|
||||
warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9',
|
||||
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类',
|
||||
availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2,
|
||||
availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null,
|
||||
locked: null, blockReason: null,
|
||||
}
|
||||
|
||||
describe('GlobalSmartLoadingView', () => {
|
||||
beforeEach(() => {
|
||||
@@ -67,7 +76,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }])
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([candidate])
|
||||
})
|
||||
|
||||
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
|
||||
@@ -199,6 +208,47 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).toContain(': candidate.availablePackageCount')
|
||||
})
|
||||
|
||||
it('filters candidates by expected arrival range and order status', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
||||
{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' },
|
||||
{ ...candidate, inboundId: '52', inboundCode: 'IN-WRONG-STATUS', orderStatusSnapshot: 'LOADING', expectedArrivalAt: '2026-09-11T08:00:00' },
|
||||
{ ...candidate, inboundId: '53', inboundCode: 'IN-WRONG-DATE', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-20T08:00:00' },
|
||||
])
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
wrapper.getComponent(QueryToolbar).vm.$emit('search', {
|
||||
customerId: '', inboundCode: '', orderStatus: 'WAIT_SPLIT', expectedArrivalRange: ['2026-09-10', '2026-09-12'],
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const candidates = wrapper.get('.candidate-pane').text()
|
||||
expect(candidates).toContain('IN-MATCH')
|
||||
expect(candidates).not.toContain('IN-WRONG-STATUS')
|
||||
expect(candidates).not.toContain('IN-WRONG-DATE')
|
||||
})
|
||||
|
||||
it('paginates large inbound candidate results with twenty rows by default', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(Array.from({ length: 25 }, (_, index) => ({
|
||||
...candidate,
|
||||
inboundId: String(100 + index),
|
||||
orderId: String(200 + index),
|
||||
inboundCode: `IN-${String(index + 1).padStart(3, '0')}`,
|
||||
})))
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const pane = wrapper.get('.candidate-pane')
|
||||
expect(pane.text()).toContain('IN-020')
|
||||
expect(pane.text()).not.toContain('IN-021')
|
||||
expect(pane.text()).toContain('共 25 条 · 第 1 / 2 页')
|
||||
|
||||
await pane.findAll('button').find((button) => button.attributes('aria-label') === '下一页')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(pane.text()).toContain('IN-021')
|
||||
expect(pane.text()).not.toContain('IN-001')
|
||||
})
|
||||
|
||||
it('aggregates a two-order candidate group and calls only whole-group assignment', async () => {
|
||||
vi.mocked(listSmartLoadingCandidates).mockResolvedValue([
|
||||
{ accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '客户一', goodsSummary: '货物一', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 3, groupTotalBoxes: 12, locked: false, blockReason: null },
|
||||
@@ -260,13 +310,13 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps only customer and inbound code filters', async () => {
|
||||
it('keeps the confirmed candidate filters without unrelated customs fields', async () => {
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }> }>
|
||||
expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode'])
|
||||
expect(fields.map((field) => field.key)).toEqual(['customerId', 'inboundCode', 'expectedArrivalRange', 'orderStatus'])
|
||||
expect(source).not.toContain('目的仓')
|
||||
|
||||
expect(wrapper.text()).not.toContain('税率')
|
||||
@@ -529,6 +579,9 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
|
||||
expect(source).toContain('grid-template-columns: minmax(390px, 1.1fr) minmax(320px, .9fr) minmax(460px, 1.35fr)')
|
||||
expect(source).toContain('min-height: 112px')
|
||||
expect(source).toContain("label: '预计到货时间'")
|
||||
expect(source).toContain("label: '订单状态'")
|
||||
expect(source).toContain(':page-sizes="[20, 50, 100]"')
|
||||
})
|
||||
|
||||
it('uses overflow-aware channel tabs with a compact right-aligned summary', () => {
|
||||
|
||||
@@ -54,8 +54,10 @@ const selectedCandidate = ref<SmartLoadingCandidateRow | null>(null)
|
||||
const assignForm = reactive({ packageCount: 1 })
|
||||
const quantityDrafts = reactive<Record<string, number>>({})
|
||||
const planGroupsByInboundId = ref(new Map<string, SmartLoadingPlanCoLoadGroup>())
|
||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '' })
|
||||
const query = ref<QueryModel>({ customerId: '', inboundCode: '', expectedArrivalRange: [], orderStatus: '' })
|
||||
const appliedQuery = ref<QueryModel>({ ...query.value })
|
||||
const candidatePage = ref(1)
|
||||
const candidatePageSize = ref(20)
|
||||
let selectionRequest = 0
|
||||
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
@@ -171,9 +173,23 @@ const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / vol
|
||||
const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0)
|
||||
const displayedVolumePercent = computed(() => percentage(volumePercent.value))
|
||||
const displayedWeightPercent = computed(() => percentage(weightPercent.value))
|
||||
const candidateOrderStatusLabels: Record<string, string> = {
|
||||
DRAFT: '草稿', SUBMITTED: '待审核', PENDING_REVIEW: '待审核', APPROVED: '已审核', ACCEPTED: '已受理',
|
||||
REJECTED: '已驳回', PROCESSING: '处理中', WAIT_MEASURE: '待测量数据', WAIT_SPLIT: '待分柜',
|
||||
LOADING: '装柜中', LOADED: '装柜完成', COMPLETED: '已完成', DELIVERED: '派送完成', CANCELLED: '已取消',
|
||||
}
|
||||
function candidateOrderStatusLabel(status: string) {
|
||||
return candidateOrderStatusLabels[status] || '未知状态'
|
||||
}
|
||||
const candidateOrderStatusOptions = computed(() => [...new Set(candidates.value
|
||||
.map((candidate) => candidate.orderStatusSnapshot?.trim().toUpperCase() || '')
|
||||
.filter(Boolean))]
|
||||
.map((status) => ({ value: status, label: candidateOrderStatusLabel(status) })))
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'customerId', type: 'select', label: '客户', placeholder: '全部客户', width: 220, options: customers.value.filter((customer) => customer.status === 'ACTIVE').map((customer) => ({ label: customer.tenantName, value: String(customer.id) })), props: { filterable: true, loading: customersLoading.value } },
|
||||
{ key: 'inboundCode', type: 'input', label: '入仓码', placeholder: '请输入入仓码', width: 240 },
|
||||
{ key: 'expectedArrivalRange', type: 'daterange', label: '预计到货时间', placeholder: '请选择预计到货时间范围', width: 300 },
|
||||
{ key: 'orderStatus', type: 'select', label: '订单状态', placeholder: '全部订单状态', width: 200, options: candidateOrderStatusOptions.value },
|
||||
])
|
||||
|
||||
const filteredContainers = computed(() => containers.value.filter((item) => (
|
||||
@@ -184,13 +200,27 @@ const filteredContainers = computed(() => containers.value.filter((item) => (
|
||||
const filteredCandidates = computed(() => {
|
||||
const customerId = String(appliedQuery.value.customerId || '')
|
||||
const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase()
|
||||
const orderStatus = String(appliedQuery.value.orderStatus || '').trim().toUpperCase()
|
||||
const arrivalRange = Array.isArray(appliedQuery.value.expectedArrivalRange)
|
||||
? appliedQuery.value.expectedArrivalRange.map((value) => String(value || '').slice(0, 10))
|
||||
: []
|
||||
const channelCode = selectedContainer.value?.channelCode || activeChannelCode.value
|
||||
return displayedCandidates.value.filter((item) => {
|
||||
if (!channelCode || item.channelCode !== channelCode) return false
|
||||
if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false
|
||||
return !inboundCode || item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode))
|
||||
if (inboundCode && !item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode))) return false
|
||||
if (orderStatus && !item.sourceCandidates.some((member) => member.orderStatusSnapshot?.trim().toUpperCase() === orderStatus)) return false
|
||||
if (arrivalRange.length === 2 && !item.sourceCandidates.some((member) => {
|
||||
const arrivalDate = member.expectedArrivalAt?.slice(0, 10) || ''
|
||||
return arrivalDate && arrivalDate >= arrivalRange[0] && arrivalDate <= arrivalRange[1]
|
||||
})) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
const pagedCandidates = computed(() => {
|
||||
const start = (candidatePage.value - 1) * candidatePageSize.value
|
||||
return filteredCandidates.value.slice(start, start + candidatePageSize.value)
|
||||
})
|
||||
|
||||
function preferredContainerId() {
|
||||
const visible = filteredContainers.value
|
||||
@@ -273,6 +303,7 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
clearSelectionData()
|
||||
if (!container) return
|
||||
if (container.planStatus === 'COMPLETED') return
|
||||
candidatePage.value = 1
|
||||
selectionLoading.value = true
|
||||
try {
|
||||
const [candidateResult, planResult] = await Promise.all([
|
||||
@@ -351,6 +382,7 @@ async function selectChannel(channelCode: string) {
|
||||
}
|
||||
async function applyFilters(model: QueryModel) {
|
||||
appliedQuery.value = { ...model }
|
||||
candidatePage.value = 1
|
||||
if (!filteredContainers.value.some((item) => item.id === selectedContainerId.value)) {
|
||||
selectedContainerId.value = preferredContainerId()
|
||||
await loadSelectedData()
|
||||
@@ -548,9 +580,13 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<section class="dispatch-pane candidate-pane">
|
||||
<header class="pane-header"><div><strong>候选装柜明细</strong><small>{{ activeChannel?.name || '未选择渠道' }} · {{ selectedContainer?.warehouseName || '请先选择柜次' }}</small></div></header>
|
||||
<DataTable
|
||||
:data="filteredCandidates"
|
||||
v-model:page="candidatePage"
|
||||
v-model:page-size="candidatePageSize"
|
||||
:data="pagedCandidates"
|
||||
:columns="candidateColumns"
|
||||
:loading="loading || selectionLoading || mutating"
|
||||
:total="filteredCandidates.length"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
stripe
|
||||
preference-key="global-smart-loading-candidates"
|
||||
column-settings-mode="hide"
|
||||
|
||||
@@ -235,6 +235,8 @@ export interface SmartLoadingCandidate {
|
||||
orderId: string
|
||||
orderNo: string | null
|
||||
inboundCode: string | null
|
||||
orderStatusSnapshot?: string | null
|
||||
expectedArrivalAt?: string | null
|
||||
warehouseId: string
|
||||
channelCode: string | null
|
||||
channelName: string | null
|
||||
|
||||
Reference in New Issue
Block a user