feat(admin): use smart-loading groups for simulations
This commit is contained in:
@@ -22,6 +22,7 @@ import ContainerCustomsPanel from '../components/ContainerCustomsPanel.vue'
|
||||
import ContainerLastLegFeesPanel from '../components/ContainerLastLegFeesPanel.vue'
|
||||
import ContainerOverviewPanel from '../components/ContainerOverviewPanel.vue'
|
||||
import OceanBookingLinkDialog from '../components/OceanBookingLinkDialog.vue'
|
||||
import { getSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
|
||||
import {
|
||||
cancelContainerException,
|
||||
completeContainerClearance,
|
||||
@@ -164,6 +165,7 @@ const loadingListDescription = computed(() => loadingListMode.value === 'confirm
|
||||
? '装柜明细、PDA 证据与报关关联均以当前柜次为边界。'
|
||||
: '核对预装明细与 PDA 作业准备情况,装柜完成后再处理整柜报关。')
|
||||
const overseasDestinationText = computed(() => [detail.value?.destinationWarehouseCode, detail.value?.destinationWarehouseName].filter(Boolean).join(' / ') || '—')
|
||||
const plannedChannelName = computed(() => getSmartLoadingChannelGroup(detail.value?.channelGroupCode)?.name || '—')
|
||||
const emptyReturnWarningText = computed(() => {
|
||||
const warning = workbenchDetail.value?.terminalDrayage?.emptyReturnWarning
|
||||
if (!warning || warning.status === 'NOT_APPLICABLE') return '当前暂无还空预警'
|
||||
@@ -504,7 +506,7 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action
|
||||
<LoadErrorState v-if="overviewError" compact title="履约摘要加载失败" :message="overviewError.message" :show-retry="false" />
|
||||
<section v-show="activeTab === 'overview'" class="detail-section surface-panel" aria-labelledby="container-information-heading">
|
||||
<header class="detail-section__heading"><h2 id="container-information-heading">货柜信息</h2><PalletLabelPrintDialog v-if="overview?.loadingPlanId && detail.warehouseId" :plan-id="overview.loadingPlanId" :warehouse-id="detail.warehouseId" :disabled="!detail.containerNo || detail.planStatus === 'EDITING'" /></header>
|
||||
<div class="detail-fields"><dl><div><dt>真实订舱</dt><dd><span>{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}</span><el-button v-if="canLinkOceanBooking" link type="primary" @click="openOceanBookingLink"><Link2 :size="14" />关联已放舱订舱</el-button></dd></div><div><dt>异常状态</dt><dd>{{ detail.exceptionActive ? '异常' : '正常' }}</dd></div><div><dt>目的仓</dt><dd>{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}</dd></div><div><dt>拖车公司</dt><dd>{{ detail.terminalTruckingCompany || '—' }}</dd></div></dl></div>
|
||||
<div class="detail-fields"><dl><div><dt>真实订舱</dt><dd><span>{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}</span><el-button v-if="canLinkOceanBooking" link type="primary" @click="openOceanBookingLink"><Link2 :size="14" />关联已放舱订舱</el-button></dd></div><div><dt>计划渠道</dt><dd>{{ plannedChannelName }}</dd></div><div><dt>实际渠道</dt><dd>{{ detail.channelName || '待关联订舱' }}</dd></div><div><dt>异常状态</dt><dd>{{ detail.exceptionActive ? '异常' : '正常' }}</dd></div><div><dt>目的仓</dt><dd>{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}</dd></div><div><dt>拖车公司</dt><dd>{{ detail.terminalTruckingCompany || '—' }}</dd></div></dl></div>
|
||||
<ContainerOverviewPanel v-if="isDomestic && overview" :overview="overview" :sections="['exception']" :can-manage-documents="canEdit" :can-manage-customs="canManageCustoms" :can-manage-exception="canEdit" @manage-exception="openException" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ describe('ContainersView', () => {
|
||||
vi.mocked(getTerminalDrayageHistory).mockResolvedValue([])
|
||||
vi.mocked(selectTerminalDrayageFleet).mockResolvedValue({ ...drayage, fleetSupplierId: '502', fleetSupplierName: '新车队', version: 4, allowedActions: [...drayage.allowedActions] })
|
||||
vi.mocked(recordTerminalDrayageEmptyReturn).mockReset().mockResolvedValue({ ...drayage, status: 'EMPTY_RETURNED', actualEmptyReturnAt: '2026-09-11T13:00:00', version: 4, emptyReturnWarning: { status: 'RESOLVED', deadlineAt: '2026-09-10T10:00:00', overdueHours: 0, overdueDays: 0 }, allowedActions: ['SELECT_FLEET'] })
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER', 'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN'], terminalDrayage: drayage, oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER'], oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null })
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER', 'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN'], terminalDrayage: drayage, oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: null, channelName: null, containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
vi.mocked(getContainer).mockResolvedValue({ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER'], oceanBookingId: null, warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: null, channelName: null, containerType: '40HC', planStatus: 'WAIT_LOADING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null })
|
||||
vi.mocked(getContainerOverview).mockResolvedValue({ container: { id: '9001', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'WAIT_LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: null, vesselName: null, version: 0 }, loadingPlanId: null, pda: null, documents: [], exception: { active: false, reason: null, markedBy: null, markedByName: null, markedAt: null }, exceptionHistory: [], timeline: [] })
|
||||
})
|
||||
|
||||
@@ -320,7 +320,7 @@ describe('ContainersView', () => {
|
||||
|
||||
it('routes each plan status to its owning workflow without reopening pushed drafts', async () => {
|
||||
const base = {
|
||||
id: '9001', planId: '9101', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', version: 0,
|
||||
id: '9001', planId: '9101', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: null, channelName: null, containerType: '40HC', version: 0,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
||||
departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩',
|
||||
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 76, maxWeightKg: 28000,
|
||||
@@ -560,7 +560,7 @@ describe('ContainersView', () => {
|
||||
link.click()
|
||||
await flushPromises()
|
||||
expect(wrapper.findAllComponents(AppDialog).find((dialog) => dialog.props('title') === '关联已放舱订舱')?.props('modelValue')).toBe(true)
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '8', channelCode: 'ZBYHK', containerType: '40HC', page: 1, size: 200 })
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '8', channelGroupCode: 'MATSON_CLX', containerType: '40HC', page: 1, size: 200 })
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
}
|
||||
@@ -585,7 +585,7 @@ describe('ContainersView', () => {
|
||||
expect(source).not.toContain('新增货柜')
|
||||
expect(simulationSource).toContain('新增模拟柜')
|
||||
expect(simulationSource).toContain('createContainerSimulation({ ...simulationForm })')
|
||||
expect(simulationSource).toContain("channelCode: '', warehouseId: '', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000")
|
||||
expect(simulationSource).toContain("channelGroupCode: '', warehouseId: '', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000")
|
||||
expect(source).toContain('CONTAINER_SPECIFICATIONS')
|
||||
expect(simulationSource).toContain('selectSimulationContainerType')
|
||||
expect(simulationSource).toContain('服务端会生成稳定的柜次号')
|
||||
@@ -602,7 +602,7 @@ describe('ContainersView', () => {
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{
|
||||
id: '215', planId: null, bookingNo: 'CB-260825-0215', cabinetNo: 'CAB-260825-0215', containerNo: null,
|
||||
sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER'], oceanBookingId: null, warehouseId: '193000000000200002', warehouseName: '东莞仓',
|
||||
channelCode: 'EXX_RAIL_NY', channelName: '纽约铁路', containerType: '45HC', planStatus: 'EDITING',
|
||||
channelGroupCode: 'EXX', channelCode: null, channelName: null, containerType: '45HC', planStatus: 'EDITING',
|
||||
capacityCbm: 86, maxWeightKg: 29000, version: 7,
|
||||
}], page: 1, size: 20, total: 1, pages: 1 } as never)
|
||||
vi.mocked(listOceanBookingLinkCandidates).mockResolvedValueOnce({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
@@ -616,7 +616,7 @@ describe('ContainersView', () => {
|
||||
.filter((item) => item.textContent?.includes('关联已放舱订舱')).at(-1)!.click()
|
||||
await flushPromises()
|
||||
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelCode: 'EXX_RAIL_NY', containerType: '45HC', page: 1, size: 200 })
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelGroupCode: 'EXX', containerType: '45HC', page: 1, size: 200 })
|
||||
expect((wrapper.findComponent(OceanBookingLinkDialog).vm as unknown as { candidates: unknown[] }).candidates).toEqual([])
|
||||
expect(readFileSync(resolve(process.cwd(), 'src/modules/container-loading/components/OceanBookingLinkDialog.vue'), 'utf8'))
|
||||
.toContain('暂无符合当前装柜仓库、物流渠道和柜型的已放舱需求')
|
||||
@@ -629,7 +629,7 @@ describe('ContainersView', () => {
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{
|
||||
id: '315', planId: null, bookingNo: 'CB-CAPACITY-SNAPSHOT', cabinetNo: 'CAB-CAPACITY-SNAPSHOT', containerNo: null,
|
||||
sourceType: 'SIMULATION', allowedActions: ['EDIT_CONTAINER'], oceanBookingId: null, warehouseId: '193000000000200002', warehouseName: '东莞仓',
|
||||
channelCode: 'EXX_RAIL_NY', channelName: '纽约铁路', containerType: '45HC', planStatus: 'WAIT_LOADING',
|
||||
channelGroupCode: 'EXX', channelCode: null, channelName: null, containerType: '45HC', planStatus: 'WAIT_LOADING',
|
||||
capacityCbm: 86, maxWeightKg: 29000, version: 7,
|
||||
}], page: 1, size: 20, total: 1, pages: 1 } as never)
|
||||
vi.mocked(listOceanBookingLinkCandidates).mockReturnValueOnce(new Promise((resolve) => { resolveCandidates = resolve }))
|
||||
@@ -647,7 +647,7 @@ describe('ContainersView', () => {
|
||||
linkAction.click()
|
||||
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledTimes(1)
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelCode: 'EXX_RAIL_NY', containerType: '45HC', page: 1, size: 200 })
|
||||
expect(listOceanBookingLinkCandidates).toHaveBeenCalledWith({ warehouseId: '193000000000200002', channelGroupCode: 'EXX', containerType: '45HC', page: 1, size: 200 })
|
||||
expect(getOceanBooking).not.toHaveBeenCalled()
|
||||
resolveCandidates({ records: [{ oceanBookingId: '315', bookingNo: 'OB-CAPACITY-SNAPSHOT', requirementId: '45315', containerType: '45HC', capacityCbm: 85, maxWeightKg: 28500, requiredQuantity: 1, allocatedQuantity: 0, remainingQuantity: 1, customerName: '客户', warehouseName: '东莞仓', channelName: '纽约铁路' }], page: 1, size: 200, total: 1, pages: 1 })
|
||||
await flushPromises()
|
||||
|
||||
@@ -21,6 +21,7 @@ import { CONTAINER_SPECIFICATIONS, applyContainerSpecification, containerSpecifi
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getContainer, listContainers, updateContainer } from '../api/platformContainerLoading'
|
||||
import OceanBookingLinkDialog from '../components/OceanBookingLinkDialog.vue'
|
||||
import { getSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
|
||||
import {
|
||||
getTerminalDrayageEmails,
|
||||
sendTerminalDrayageEmail,
|
||||
@@ -239,6 +240,8 @@ const allColumns: DataTableColumn[] = [
|
||||
{ key: 'pickupCode', label: 'PU码', minWidth: 140, formatter: (row: ContainerBookingResource) => row.pickupCode || '—' },
|
||||
{ key: 'oceanBooking', label: '真实订舱', minWidth: 170, slotName: 'oceanBooking' },
|
||||
{ key: 'warehouseName', prop: 'warehouseName', label: '装柜仓库', minWidth: 160 },
|
||||
{ key: 'channelGroupCode', label: '计划渠道', minWidth: 140, formatter: (row: ContainerBookingResource) => getSmartLoadingChannelGroup(row.channelGroupCode)?.name || '—' },
|
||||
{ key: 'channelName', label: '实际渠道', minWidth: 160, formatter: (row: ContainerBookingResource) => row.channelName || '待关联订舱' },
|
||||
{ key: 'terminalTruckingCompany', label: '码头车队', width: 190, slotName: 'terminalTruckingCompany' },
|
||||
{ key: 'pickupAppointmentAt', label: '预约提柜', width: 165, slotName: 'pickupAppointmentAt' },
|
||||
{ key: 'actualPickupAt', label: '实际提柜', width: 165, slotName: 'actualPickupAt' },
|
||||
@@ -256,7 +259,7 @@ const allColumns: DataTableColumn[] = [
|
||||
{ key: 'estimatedPortArrivalAt', label: '预计到港', minWidth: 160, slotName: 'estimatedPortArrivalAt' },
|
||||
{ key: 'actualPortArrivalAt', label: '实际到港', minWidth: 160, slotName: 'actualPortArrivalAt' },
|
||||
]
|
||||
const domesticColumnKeys = new Set(['actions', 'containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'oceanBooking', 'warehouseName', 'billOfLadingNo', 'route', 'containerType', 'capacity', 'vessel', 'shippingCompanyName', 'loadingAt', 'estimatedSailingAt'])
|
||||
const domesticColumnKeys = new Set(['actions', 'containerNo', 'planNo', 'planStatus', 'exceptionActive', 'cabinetNo', 'oceanBooking', 'warehouseName', 'channelGroupCode', 'channelName', 'billOfLadingNo', 'route', 'containerType', 'capacity', 'vessel', 'shippingCompanyName', 'loadingAt', 'estimatedSailingAt'])
|
||||
const overseasColumnKeys = new Set(['actions', 'emptyReturnWarning', 'containerNo', 'cabinetNo', 'destinationClearanceDocumentStatus', 'deliveryOrderDocumentStatus', 'pickupCode', 'terminalTruckingCompany', 'pickupAppointmentAt', 'actualPickupAt', 'estimatedEmptyReturnAt', 'actualEmptyReturnAt', 'billOfLadingNo', 'route', 'vessel', 'actualSailingAt', 'estimatedPortArrivalAt', 'actualPortArrivalAt'])
|
||||
const columns = computed(() => allColumns
|
||||
.filter((column) => (isDomestic.value ? domesticColumnKeys : overseasColumnKeys).has(String(column.key)))
|
||||
|
||||
@@ -7,7 +7,6 @@ import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
|
||||
import { resolveSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
|
||||
import type { ContainerBookingResource } from '../types'
|
||||
import {
|
||||
assignPendingReviewSmartLoadingOrder,
|
||||
@@ -62,7 +61,7 @@ vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups:
|
||||
|
||||
const container = {
|
||||
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
|
||||
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
|
||||
warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船',
|
||||
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
|
||||
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 76, maxWeightKg: 28000,
|
||||
@@ -98,7 +97,8 @@ function containerPage(records: ContainerBookingResource[] = [container]) {
|
||||
return {
|
||||
records: records.map((item) => ({
|
||||
bookingId: item.id, bookingNo: item.bookingNo, cabinetNo: item.cabinetNo, containerNo: item.containerNo,
|
||||
warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', channelCode: item.channelCode || '', channelName: item.channelName || '',
|
||||
warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', channelGroupCode: item.channelGroupCode,
|
||||
channelCode: item.channelCode, channelName: item.channelName,
|
||||
containerType: item.containerType, capacityCbm: item.capacityCbm, maxWeightKg: item.maxWeightKg,
|
||||
planId: item.planId, planNo: null, planStatus: item.planStatus, version: item.version,
|
||||
})),
|
||||
@@ -108,7 +108,7 @@ function containerPage(records: ContainerBookingResource[] = [container]) {
|
||||
function mockContainerSearch(records: ContainerBookingResource[]) {
|
||||
vi.mocked(listSmartLoadingWorkbenchContainers).mockImplementation(async (query) => containerPage(records.filter((item) => (
|
||||
item.planStatus !== 'COMPLETED'
|
||||
&& query.channelGroupCodes.includes(resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code || '')
|
||||
&& query.channelGroupCodes.includes(item.channelGroupCode)
|
||||
))))
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
planId: '9007199254740999',
|
||||
cabinetNo: 'CB-2',
|
||||
containerNo: 'MSCU-2',
|
||||
channelGroupCode: 'HEDE',
|
||||
channelCode: 'HDHK',
|
||||
channelName: '合德海卡包税',
|
||||
}
|
||||
@@ -264,6 +265,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
planId: '9007199254740999',
|
||||
cabinetNo: 'CB-2',
|
||||
containerNo: 'MSCU-2',
|
||||
channelGroupCode: 'HEDE',
|
||||
channelCode: 'HDHK',
|
||||
channelName: '合德海卡包税',
|
||||
}
|
||||
@@ -348,6 +350,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
containerNo: 'MSCU-DG-MAX',
|
||||
warehouseId: '18',
|
||||
warehouseName: '东莞仓',
|
||||
channelGroupCode: 'MATSON_MAX',
|
||||
channelCode: 'MATSON_MAX_TRUCK',
|
||||
channelName: '美森加班海卡',
|
||||
}
|
||||
@@ -490,7 +493,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('不能部分输入、单独分配或单独移除')
|
||||
expect(document.body.textContent).toContain('目标柜次:CB-1 · 集装箱号:MSCU-1')
|
||||
expect(document.body.textContent).toContain('发货仓:义乌仓 · 柜次渠道:美森正班海卡包税')
|
||||
expect(document.body.textContent).toContain('发货仓:义乌仓 · 柜次渠道:美森正班')
|
||||
expect(document.body.textContent).not.toContain('本次分配箱数')
|
||||
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click()
|
||||
await flushPromises()
|
||||
@@ -507,8 +510,8 @@ describe('GlobalSmartLoadingView', () => {
|
||||
inboundId: null,
|
||||
orderStatusSnapshot: 'PENDING_REVIEW',
|
||||
inboundCode: 'EXXHK2609020361',
|
||||
channelCode: 'SLOW',
|
||||
channelName: '慢船',
|
||||
channelCode: 'HEDE_TRUCK',
|
||||
channelName: '合德海卡',
|
||||
unitGrossWeightKg: null,
|
||||
averageUnitVolumeCbm: null,
|
||||
availableVolumeCbm: null,
|
||||
@@ -527,7 +530,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(document.body.textContent).toContain('平均单箱毛重 待测量')
|
||||
expect(document.body.textContent).toContain('平均单箱体积 待测量')
|
||||
expect(document.body.textContent).not.toContain('平均单箱毛重 0 kg')
|
||||
expect(document.body.textContent).toContain('订单渠道:慢船')
|
||||
expect(document.body.textContent).toContain('订单渠道:合德海卡')
|
||||
expect(document.body.textContent).toContain('这是人工装柜分配,确认后仍会加入')
|
||||
expect(document.body.textContent).toContain('智能推荐不会跨渠道')
|
||||
;([...document.body.querySelectorAll('button')]
|
||||
@@ -708,7 +711,7 @@ describe('GlobalSmartLoadingView', () => {
|
||||
})
|
||||
|
||||
it('isolates a missing-channel row without hiding another valid container', async () => {
|
||||
const missingChannel = { ...container, id: '9007199254740997', cabinetNo: 'CB-MISSING', channelCode: null, channelName: null }
|
||||
const missingChannel = { ...container, id: '9007199254740997', cabinetNo: 'CB-MISSING', channelGroupCode: '', channelCode: null, channelName: null }
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [missingChannel, container], page: 1, size: 100, total: 2, pages: 1 })
|
||||
mockContainerSearch([missingChannel, container])
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
|
||||
@@ -76,19 +76,23 @@ let containerRequest = 0
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const selectedContainer = computed(() => containers.value.find((item) => item.bookingId === selectedContainerId.value) || null)
|
||||
const crossChannelAssignment = computed(() => Boolean(
|
||||
selectedCandidate.value?.channelCode
|
||||
&& selectedContainer.value?.channelCode
|
||||
&& selectedCandidate.value.channelCode !== selectedContainer.value.channelCode,
|
||||
))
|
||||
function containerChannelGroup(container: SmartLoadingWorkbenchContainer | null | undefined) {
|
||||
if (!container) return null
|
||||
return SMART_LOADING_CHANNEL_GROUPS.find((group) => group.code === container.channelGroupCode) || null
|
||||
}
|
||||
const crossChannelAssignment = computed(() => {
|
||||
const candidateGroup = resolveSmartLoadingChannelGroup(selectedCandidate.value?.channelCode, selectedCandidate.value?.channelName)
|
||||
const containerGroup = containerChannelGroup(selectedContainer.value)
|
||||
return Boolean(candidateGroup && containerGroup && candidateGroup.code !== containerGroup.code)
|
||||
})
|
||||
const orchestratableContainers = computed(() => containers.value)
|
||||
const completedContainerCount = computed(() => 0)
|
||||
const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length)
|
||||
const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelGroupCode).length)
|
||||
const unmappedChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
|
||||
item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
item.channelGroupCode && !containerChannelGroup(item)
|
||||
)).length)
|
||||
const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.value.filter((item) => (
|
||||
resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)
|
||||
containerChannelGroup(item)
|
||||
)).length)
|
||||
const channelOptions = computed(() => SMART_LOADING_CHANNEL_GROUPS)
|
||||
const warehouseOptions = computed(() => {
|
||||
@@ -661,8 +665,8 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div><dt>总体积</dt><dd><strong>{{ formatNumber(totalVolume) }}</strong> m³</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
<el-alert v-if="missingChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少渠道快照,已隔离;请补齐后再参与智能装柜`" />
|
||||
<el-alert v-if="unmappedChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedChannelContainerCount} 个可编排柜次的渠道未纳入固定 9 类,已隔离`" />
|
||||
<el-alert v-if="missingChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${missingChannelContainerCount} 个可编排柜次缺少计划渠道,已隔离;请重新创建模拟柜`" />
|
||||
<el-alert v-if="unmappedChannelContainerCount && !mappedWorkbenchContainerCount" type="warning" :closable="false" show-icon :title="`${unmappedChannelContainerCount} 个可编排柜次的计划渠道未纳入固定 9 类,已隔离`" />
|
||||
<el-alert v-if="completedContainerCount && !orchestratableContainers.length" type="success" :closable="false" show-icon title="当前柜次已完成装柜,不可继续编辑" />
|
||||
<QueryToolbar
|
||||
v-model="query"
|
||||
@@ -746,7 +750,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
</span>
|
||||
<span class="container-option__identity">柜次号:{{ displayCabinetNo(item) }}</span>
|
||||
<span>集装箱号:{{ displayActualContainerNo(item) }}</span>
|
||||
<span>渠道:{{ resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.name || '渠道待补充' }}</span>
|
||||
<span>渠道:{{ containerChannelGroup(item)?.name || '渠道待补充' }}</span>
|
||||
<span>仓库:{{ item.warehouseName || '仓库待补充' }} · 柜型:{{ item.containerType || '柜型待补充' }}</span>
|
||||
<!-- 空柜的操作时机由状态和操作按钮表达,不在每张柜次卡片重复提示。 -->
|
||||
</button>
|
||||
@@ -802,7 +806,7 @@ onMounted(() => { void load(); void loadCustomers() })
|
||||
<div v-if="selectedCandidate" class="assign-dialog">
|
||||
<p><strong>{{ selectedCandidate.groupNo || selectedCandidate.customerName || '客户待补充' }}</strong></p>
|
||||
<p>目标柜次:{{ displayCabinetNo(selectedContainer) }} · 集装箱号:{{ displayActualContainerNo(selectedContainer) }}</p>
|
||||
<p>发货仓:{{ selectedContainer?.warehouseName || '—' }} · 柜次渠道:{{ selectedContainer?.channelName || '—' }}</p>
|
||||
<p>发货仓:{{ selectedContainer?.warehouseName || '—' }} · 柜次渠道:{{ containerChannelGroup(selectedContainer)?.name || '—' }}</p>
|
||||
<p>订单渠道:{{ selectedCandidate.channelName || '—' }}</p>
|
||||
<el-alert v-if="crossChannelAssignment" title="订单渠道与柜次渠道不同;这是人工装柜分配,确认后仍会加入。智能推荐不会跨渠道。" type="warning" :closable="false" />
|
||||
<template v-if="selectedCandidate.coLoadGroupId">
|
||||
|
||||
@@ -19,7 +19,7 @@ vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({
|
||||
|
||||
const container = {
|
||||
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
|
||||
warehouseId: '8', warehouseName: '义乌仓', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
|
||||
warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: null, channelName: null, containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
||||
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
|
||||
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 76, maxWeightKg: 28000,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types'
|
||||
import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems'
|
||||
import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups'
|
||||
import { resolveSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
|
||||
import { getSmartLoadingChannelGroup } from '../smartLoadingChannelGroups'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -206,7 +206,7 @@ async function load() {
|
||||
if (detail.code === 'A0404') { plan.value = null; noDraft.value = true }
|
||||
else throw error
|
||||
}
|
||||
const channelGroup = resolveSmartLoadingChannelGroup(container.value.channelCode, container.value.channelName)
|
||||
const channelGroup = getSmartLoadingChannelGroup(container.value.channelGroupCode)
|
||||
candidates.value = plan.value?.canAssign && channelGroup
|
||||
? (await listSmartLoadingCandidates({ channelGroupCodes: [channelGroup.code], warehouseId: container.value.warehouseId, page: 1, size: 200 })).records
|
||||
: []
|
||||
|
||||
Reference in New Issue
Block a user