Merge branch 'release/test' of http://8.163.30.128:10082/html/admin-web into release/test
This commit is contained in:
@@ -24,7 +24,7 @@ describe('ContainersView', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.mocked(listAllPlatformWarehouses).mockResolvedValue([])
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'EDITING', version: 0, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null, departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩', destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000, 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(listContainers).mockResolvedValue({ records: [{ id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', 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: 68, maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
})
|
||||
|
||||
it('loads the real paged contract and renders core container fields', async () => {
|
||||
@@ -33,6 +33,7 @@ describe('ContainersView', () => {
|
||||
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
expect(listContainers).toHaveBeenCalledWith(expect.objectContaining({ page: 1, size: 20 }))
|
||||
expect(wrapper.text()).toContain('待装柜')
|
||||
expect(wrapper.text()).toContain('CB-1')
|
||||
expect(wrapper.text()).toContain('MSCU-1')
|
||||
expect(wrapper.text()).toContain('宁波 → 长滩')
|
||||
@@ -53,7 +54,7 @@ describe('ContainersView', () => {
|
||||
|
||||
it('uses stable view labels when cabinetNo is missing', async () => {
|
||||
const base = {
|
||||
id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'EDITING' as const, version: 0,
|
||||
id: '9001', planId: null, bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', containerType: '40HQ', planStatus: 'WAIT_LOADING' as const, version: 0,
|
||||
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: null, shippingCompanyName: null,
|
||||
departurePortCode: null, departurePortName: '宁波', destinationPortCode: null, destinationPortName: '长滩',
|
||||
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68, maxWeightKg: 28000,
|
||||
|
||||
@@ -73,7 +73,7 @@ const transportOnly = computed(() => Boolean(form.id) && detail.value?.planStatu
|
||||
const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'keyword', type: 'input', label: '关键词', width: 220, placeholder: '柜次号、柜号或提单号', props: { 'aria-label': '关键词', maxlength: 128 } },
|
||||
{ key: 'containerType', type: 'select', label: '柜型', width: 140, options: ['20GP', '40GP', '40HQ', '45HQ'].map((value) => ({ label: value, value })), props: { 'aria-label': '柜型' } },
|
||||
{ key: 'status', type: 'select', label: '排柜状态', width: 150, options: [{ label: '编辑中', value: 'EDITING' }, { label: '装柜中', value: 'LOADING' }, { label: '已完成', value: 'COMPLETED' }], props: { 'aria-label': '排柜状态' } },
|
||||
{ key: 'status', type: 'select', label: '排柜状态', width: 150, options: [{ label: '待装柜', value: 'WAIT_LOADING' }, { label: '预装柜中', value: 'EDITING' }, { label: '拣货中', value: 'LOADING' }, { label: '装柜完成', value: 'COMPLETED' }], props: { 'aria-label': '排柜状态' } },
|
||||
{ key: 'sailingRange', type: 'daterange', label: '预计开船', width: 260, props: { 'aria-label': '预计开船日期范围' } },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
@@ -90,7 +90,7 @@ const columns: DataTableColumn[] = [
|
||||
]
|
||||
|
||||
function optional(value: string | null) { return value?.trim() || null }
|
||||
function statusText(status: ContainerPlanStatus) { return ({ EDITING: '编辑中', LOADING: '装柜中', COMPLETED: '已完成' })[status] }
|
||||
function statusText(status: ContainerPlanStatus) { return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' })[status] }
|
||||
function formatTime(value: string | null | undefined) { return value?.replace('T', ' ').slice(0, 16) || '—' }
|
||||
function containerViewLabel(row: ContainerBookingResource) {
|
||||
const reference = [row.cabinetNo, row.containerNo, row.bookingNo]
|
||||
@@ -240,8 +240,8 @@ onMounted(() => { void Promise.all([loadWarehouses(), loadRows()]) })
|
||||
|
||||
<AppDrawer v-model="editorOpen" :title="editorTitle" size="min(860px, 96vw)" :confirm-loading="saving" :confirm-disabled="Boolean(warehouseError && !transportOnly)" confirm-text="保存" @confirm="save">
|
||||
<LoadErrorState v-if="editorError" title="货柜保存失败" :message="editorError.message" compact :retry-text="form.id ? '刷新最新数据' : '重试保存'" @retry="form.id ? openEdit({ id: form.id }) : save()"><template #details><ApiErrorMeta :details="editorError" /></template></LoadErrorState>
|
||||
<el-alert v-if="transportOnly" title="拣货中仅允许修改集装箱号和航次,其他字段已锁定。" type="warning" :closable="false" />
|
||||
<LoadErrorState v-if="warehouseError && !transportOnly" title="装柜仓库加载失败" :message="warehouseError.message" compact @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<el-alert v-if="transportOnly" title="装柜中仅允许修改集装箱号和航次,其他字段已锁定。" type="warning" :closable="false" />
|
||||
<el-form v-if="!warehouseError || transportOnly" ref="formRef" :model="form" :rules="rules" label-position="top" class="form-grid">
|
||||
<el-form-item label="柜次号" prop="cabinetNo"><el-input v-model="form.cabinetNo" maxlength="64" :disabled="transportOnly" /></el-form-item>
|
||||
<el-form-item label="集装箱号"><el-input v-model="form.containerNo" maxlength="64" placeholder="开始装柜前必须填写" /></el-form-item>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import ElementPlus, { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { AxiosError } from 'axios'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
|
||||
import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
|
||||
import {
|
||||
getSmartLoadingPlanByBooking,
|
||||
listContainers,
|
||||
listSmartLoadingCandidates,
|
||||
recommendSmartLoading,
|
||||
removeSmartLoadingItem,
|
||||
} from '../api/platformContainerLoading'
|
||||
|
||||
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
|
||||
@@ -73,6 +75,15 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
const statusField = wrapper.findComponent(QueryToolbar).props('fields')
|
||||
.find((field: { key: string }) => field.key === 'status')
|
||||
expect(statusField?.options).toEqual([
|
||||
{ label: '待装柜', value: 'WAIT_LOADING' },
|
||||
{ label: '预装柜中', value: 'EDITING' },
|
||||
{ label: '拣货中', value: 'LOADING' },
|
||||
{ label: '装柜完成', value: 'COMPLETED' },
|
||||
])
|
||||
|
||||
expect(wrapper.text()).not.toContain('税率')
|
||||
expect(wrapper.text()).not.toContain('是否退税')
|
||||
expect(wrapper.text()).not.toContain('美森正班')
|
||||
@@ -88,23 +99,21 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('CB-1 装柜方案')
|
||||
})
|
||||
|
||||
it('keeps pushed containers visible and routes them to the preload list without loading an editing draft', async () => {
|
||||
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 }
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [pushed], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(pickingPlan)
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
|
||||
expect(listSmartLoadingCandidates).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('该柜次正在预装流程中')
|
||||
expect(wrapper.text()).not.toContain('智能推荐')
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id)
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(pushed.warehouseId)
|
||||
expect(wrapper.text()).toContain('CB-2')
|
||||
expect(wrapper.text()).toContain('拣货中')
|
||||
expect(wrapper.text()).toContain('智能推荐')
|
||||
expect(wrapper.text()).toContain('加入')
|
||||
expect(wrapper.findAll('button').some((item) => item.text().trim() === '推送预装')).toBe(false)
|
||||
|
||||
await wrapper.findAll('button').find((item) => item.text().includes('查看预装清单'))!.trigger('click')
|
||||
expect(routerPush).toHaveBeenCalledWith({
|
||||
name: 'loading-plans',
|
||||
query: { planId: pushed.planId, warehouseId: pushed.warehouseId },
|
||||
})
|
||||
})
|
||||
|
||||
it('routes completed containers to the read-only loading list', async () => {
|
||||
@@ -145,4 +154,31 @@ describe('GlobalSmartLoadingView', () => {
|
||||
expect(wrapper.text()).toContain('已刷新最新版本')
|
||||
expect(wrapper.text()).toContain('request-1')
|
||||
})
|
||||
|
||||
it('shows a picking-specific message when a picked order cannot be removed', async () => {
|
||||
const loadingContainer = { ...container, planStatus: 'LOADING' as const }
|
||||
const loadingPlan = { ...plan, status: 'LOADING' as const }
|
||||
vi.mocked(listContainers).mockResolvedValue({ records: [loadingContainer], page: 1, size: 100, total: 1, pages: 1 })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(loadingPlan)
|
||||
vi.mocked(removeSmartLoadingItem).mockRejectedValue(new AxiosError(
|
||||
'conflict',
|
||||
'ERR_BAD_REQUEST',
|
||||
undefined,
|
||||
undefined,
|
||||
{ data: { code: 'A0409', message: '已有拣货记录的明细不能删除' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
|
||||
))
|
||||
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
|
||||
const errorMessage = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never)
|
||||
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const removeButton = wrapper.find('.row-actions .el-button--danger')
|
||||
expect(removeButton.exists()).toBe(true)
|
||||
await removeButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(removeSmartLoadingItem).toHaveBeenCalled()
|
||||
expect(errorMessage).toHaveBeenCalledWith('该订单在拣货中不许删除')
|
||||
expect(wrapper.text()).toContain('该订单在拣货中不许删除')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -56,7 +56,7 @@ const canAssign = computed(() => access.can('platform:firstleg:smart-loading:ass
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const selectedContainer = computed(() => containers.value.find((item) => item.id === selectedContainerId.value) || null)
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
|
||||
const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null)
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalPackages = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
@@ -71,9 +71,10 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
{ key: 'orderKeyword', type: 'input', label: '订单或货物', placeholder: '订单号、入库单、客户、货物', width: 250 },
|
||||
{ key: 'containerKeyword', type: 'input', label: '柜次', placeholder: '柜次号、集装箱号、目的仓', width: 230 },
|
||||
{ key: 'status', type: 'select', label: '柜次状态', width: 160, options: [
|
||||
{ label: '可编辑', value: 'EDITING' },
|
||||
{ label: '已推送预装', value: 'LOADING' },
|
||||
{ label: '已完成', value: 'COMPLETED' },
|
||||
{ label: '待装柜', value: 'WAIT_LOADING' },
|
||||
{ label: '预装柜中', value: 'EDITING' },
|
||||
{ label: '拣货中', value: 'LOADING' },
|
||||
{ label: '装柜完成', value: 'COMPLETED' },
|
||||
] },
|
||||
])
|
||||
|
||||
@@ -97,7 +98,8 @@ const filteredCandidates = computed(() => {
|
||||
function preferredContainerId() {
|
||||
const visible = filteredContainers.value
|
||||
if (String(appliedQuery.value.status || '').trim()) return visible[0]?.id || ''
|
||||
return visible.find((item) => item.planStatus === 'EDITING')?.id || visible[0]?.id || ''
|
||||
return visible.find((item) => item.planStatus === 'EDITING')?.id
|
||||
|| visible.find((item) => item.planStatus === 'WAIT_LOADING')?.id || visible[0]?.id || ''
|
||||
}
|
||||
|
||||
const candidateColumns: DataTableColumn[] = [
|
||||
@@ -126,10 +128,10 @@ function formatNumber(value: number, digits = 3) {
|
||||
function candidateRow(source: object) { return source as SmartLoadingCandidate }
|
||||
function itemRow(source: object) { return source as SmartLoadingItem }
|
||||
function statusLabel(status: ContainerPlanStatus) {
|
||||
return ({ EDITING: '可编辑', LOADING: '已推送预装', COMPLETED: '已完成' } as const)[status]
|
||||
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as const)[status]
|
||||
}
|
||||
function statusType(status: ContainerPlanStatus) {
|
||||
return status === 'EDITING' ? 'primary' : status === 'LOADING' ? 'warning' : 'success'
|
||||
return status === 'WAIT_LOADING' ? 'info' : status === 'EDITING' ? 'primary' : status === 'LOADING' ? 'warning' : 'success'
|
||||
}
|
||||
function routeMatches(source: object) {
|
||||
const candidate = candidateRow(source)
|
||||
@@ -163,9 +165,7 @@ async function loadSelectedData(preserved: Record<string, number> = {}) {
|
||||
const request = ++selectionRequest
|
||||
clearSelectionData()
|
||||
if (!container) return
|
||||
// Draft recovery is an EDITING-only contract. Later states remain visible and
|
||||
// continue through their dedicated read-only operational lists.
|
||||
if (container.planStatus !== 'EDITING') return
|
||||
if (container.planStatus === 'COMPLETED') return
|
||||
selectionLoading.value = true
|
||||
try {
|
||||
const [candidateResult, planResult] = await Promise.all([
|
||||
@@ -278,7 +278,7 @@ async function remove(source: object) {
|
||||
await mutate(async () => {
|
||||
acceptPlan(await removeSmartLoadingItem(plan.value!.id, item.id, plan.value!.version, item.version))
|
||||
ElMessage.success('装柜明细已移除')
|
||||
}, '移除装柜明细失败')
|
||||
}, '移除装柜明细失败', { pickingConflict: true })
|
||||
}
|
||||
async function pushPreload() {
|
||||
if (!plan.value) return
|
||||
@@ -295,7 +295,7 @@ async function pushPreload() {
|
||||
async function mutate(
|
||||
action: () => Promise<void>,
|
||||
fallback: string,
|
||||
recovery: { preserveDrafts?: Record<string, number>; preserveAssign?: boolean } = {},
|
||||
recovery: { preserveDrafts?: Record<string, number>; preserveAssign?: boolean; pickingConflict?: boolean } = {},
|
||||
) {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
@@ -305,9 +305,13 @@ async function mutate(
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
if (details.code === 'A0409') {
|
||||
await loadAll(recovery.preserveDrafts)
|
||||
mutationError.value = { ...details, message: '方案已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' }
|
||||
const pickingMessage = recovery.pickingConflict && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
mutationError.value = { ...details, message: pickingMessage || '方案已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' }
|
||||
if (recovery.preserveAssign) assignOpen.value = true
|
||||
ElMessage.warning(mutationError.value.message)
|
||||
if (pickingMessage) ElMessage.error(pickingMessage)
|
||||
else ElMessage.warning(mutationError.value.message)
|
||||
} else {
|
||||
mutationError.value = details
|
||||
ElMessage.error(details.message)
|
||||
@@ -378,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
|
||||
@@ -413,14 +417,8 @@ onMounted(load)
|
||||
<template #details><ApiErrorMeta :details="selectionError" /></template>
|
||||
</LoadErrorState>
|
||||
<el-empty v-else-if="!selectedContainer" description="请选择一个货柜" />
|
||||
<section v-else-if="selectedPlanStatus === 'LOADING'" class="readonly-status-card" aria-label="预装中柜次">
|
||||
<el-tag type="warning">已推送预装</el-tag>
|
||||
<strong>该柜次正在预装流程中</strong>
|
||||
<p>智能装柜方案已锁定,不能继续调整。预装运输信息和未拣货明细请在预装清单维护。</p>
|
||||
<el-button type="primary" @click="openList('LOADING')">查看预装清单</el-button>
|
||||
</section>
|
||||
<section v-else-if="selectedPlanStatus === 'COMPLETED'" class="readonly-status-card" aria-label="已完成柜次">
|
||||
<el-tag type="success">已完成</el-tag>
|
||||
<section v-else-if="selectedPlanStatus === 'COMPLETED'" class="readonly-status-card" aria-label="装柜完成柜次">
|
||||
<el-tag type="success">装柜完成</el-tag>
|
||||
<strong>该柜次已完成装柜</strong>
|
||||
<p>装柜计划、明细和运输信息已永久只读,请在装柜清单查看完整记录。</p>
|
||||
<el-button type="primary" @click="openList('COMPLETED')">查看装柜清单</el-button>
|
||||
|
||||
@@ -46,18 +46,20 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
expect(wrapper.text()).toContain('按 110% 上限')
|
||||
})
|
||||
|
||||
it('does not call the editing contract for an already pushed container', async () => {
|
||||
it('keeps picking containers editable while hiding the push action', async () => {
|
||||
vi.mocked(getContainer).mockResolvedValue({ ...container, planStatus: 'LOADING' })
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ id: '99', bookingId: container.id, planNo: 'LP-99', status: 'LOADING', containerNo: 'MSCU-1', voyageNo: null, version: 2, items: [] })
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] })
|
||||
await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady()
|
||||
const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
|
||||
expect(listSmartLoadingCandidates).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('该柜次正在预装流程中')
|
||||
expect(wrapper.text()).toContain('查看预装清单')
|
||||
expect(wrapper.text()).not.toContain('智能推荐')
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
|
||||
expect(listSmartLoadingCandidates).toHaveBeenCalledWith(container.warehouseId)
|
||||
expect(wrapper.text()).toContain('拣货中')
|
||||
expect(wrapper.text()).toContain('智能推荐')
|
||||
expect(wrapper.text()).toContain('ORD-1')
|
||||
expect(wrapper.findAll('button').some((button) => button.text().includes('推送预装'))).toBe(false)
|
||||
})
|
||||
|
||||
it('refreshes the latest plan after A0409', async () => {
|
||||
|
||||
@@ -38,9 +38,8 @@ const canCreate = computed(() => access.can('platform:firstleg:smart-loading:cre
|
||||
const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign'))
|
||||
const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete'))
|
||||
const canPush = computed(() => access.can('platform:firstleg:smart-loading:push'))
|
||||
const canViewPreload = computed(() => access.can('platform:firstleg:preload:list'))
|
||||
const canViewLoadingList = computed(() => access.can('platform:firstleg:loading-list:list'))
|
||||
const editable = computed(() => plan.value?.status === 'EDITING')
|
||||
const editable = computed(() => plan.value?.status === 'EDITING' || plan.value?.status === 'LOADING')
|
||||
const assignedDetailIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundDetailId)))
|
||||
const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0))
|
||||
@@ -95,7 +94,7 @@ async function load() {
|
||||
loading.value = true; loadError.value = null; noDraft.value = false
|
||||
try {
|
||||
container.value = await getContainer(containerId.value)
|
||||
if (container.value.planStatus !== 'EDITING') {
|
||||
if (container.value.planStatus === 'COMPLETED') {
|
||||
plan.value = null
|
||||
candidates.value = []
|
||||
return
|
||||
@@ -162,7 +161,7 @@ async function remove(source: object) {
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await removeSmartLoadingItem(plan.value.id, item.id, plan.value.version, item.version)); ElMessage.success('装柜明细已移除') }
|
||||
catch (error) { await handleMutationError(error, '移除失败') }
|
||||
catch (error) { await handleMutationError(error, '移除失败', true) }
|
||||
finally { mutating.value = false }
|
||||
})
|
||||
}
|
||||
@@ -176,7 +175,7 @@ async function pushPreload() {
|
||||
catch (error) { await handleMutationError(error, '推送失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
async function handleMutationError(error: unknown, fallback: string) {
|
||||
async function handleMutationError(error: unknown, fallback: string, pickingConflict = false) {
|
||||
const details = getApiErrorDetails(error, fallback)
|
||||
if (details.code === 'A0409') {
|
||||
const preserved = { ...quantityDrafts }
|
||||
@@ -185,7 +184,10 @@ async function handleMutationError(error: unknown, fallback: string) {
|
||||
if (preserved[item.id] != null) quantityDrafts[item.id] = preserved[item.id]
|
||||
}
|
||||
}
|
||||
mutationError.value = { ...details, message: details.code === 'A0409' ? '计划已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' : details.message }
|
||||
const pickingMessage = pickingConflict && details.code === 'A0409' && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
mutationError.value = { ...details, message: pickingMessage || (details.code === 'A0409' ? '计划已被其他人修改,已刷新最新版本并保留本次输入,请重新确认后提交' : details.message) }
|
||||
ElMessage.error(mutationError.value.message)
|
||||
}
|
||||
|
||||
@@ -205,20 +207,20 @@ onMounted(load)
|
||||
<el-button text aria-label="返回货柜列表" @click="router.back()"><ArrowLeft :size="18" />返回</el-button>
|
||||
<el-tooltip content="刷新" placement="top"><el-button :loading="loading" aria-label="刷新智能排柜" @click="load"><RefreshCw :size="16" /></el-button></el-tooltip>
|
||||
<el-button v-if="canAssign && editable" type="primary" plain :loading="mutating" @click="recommend"><Sparkles :size="16" />智能推荐</el-button>
|
||||
<el-button v-if="canPush && editable" type="primary" :disabled="!plan?.items.length" :loading="mutating" @click="pushPreload"><Upload :size="16" />推送预装</el-button>
|
||||
<el-button v-if="canPush && plan?.status === 'EDITING'" type="primary" :disabled="!plan.items.length" :loading="mutating" @click="pushPreload"><Upload :size="16" />推送预装</el-button>
|
||||
</div>
|
||||
</section>
|
||||
<LoadErrorState v-if="loadError" title="智能排柜加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<template v-else>
|
||||
<LoadErrorState v-if="mutationError" title="排柜操作失败" :message="mutationError.message" compact retry-text="刷新最新计划" @retry="load"><template #details><ApiErrorMeta :details="mutationError" /></template></LoadErrorState>
|
||||
<section v-if="container?.planStatus !== 'EDITING'" class="readonly-status-card" aria-label="只读装柜状态">
|
||||
<el-tag :type="container?.planStatus === 'COMPLETED' ? 'success' : 'warning'">{{ container?.planStatus === 'COMPLETED' ? '已完成' : '已推送预装' }}</el-tag>
|
||||
<strong>{{ container?.planStatus === 'COMPLETED' ? '该柜次已完成装柜' : '该柜次正在预装流程中' }}</strong>
|
||||
<p>智能装柜方案已锁定,不能继续调整。请进入对应清单查看后续信息。</p>
|
||||
<el-button v-if="container?.planStatus === 'LOADING' ? canViewPreload : canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />{{ container?.planStatus === 'COMPLETED' ? '查看装柜清单' : '查看预装清单' }}</el-button>
|
||||
<section v-if="container?.planStatus === 'COMPLETED'" class="readonly-status-card" aria-label="装柜完成状态">
|
||||
<el-tag type="success">装柜完成</el-tag>
|
||||
<strong>该柜次已完成装柜</strong>
|
||||
<p>智能装柜方案已锁定,不能继续调整。请进入装柜清单查看完整记录。</p>
|
||||
<el-button v-if="canViewLoadingList" type="primary" @click="openOperationalList"><ListChecks :size="16" />查看装柜清单</el-button>
|
||||
</section>
|
||||
<template v-else>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div><div><span>目的仓</span><strong>{{ container.destinationWarehouseName || container.destinationWarehouseCode || '不限' }}</strong></div><div><span>计划状态</span><strong>{{ plan?.status === 'LOADING' ? '已推送预装' : plan ? '编辑中' : '未创建' }}</strong></div></section>
|
||||
<section v-if="container" class="summary-band"><div><span>柜型</span><strong>{{ container.containerType }}</strong></div><div><span>装柜仓库</span><strong>{{ container.warehouseName || '—' }}</strong></div><div><span>目的仓</span><strong>{{ container.destinationWarehouseName || container.destinationWarehouseCode || '不限' }}</strong></div><div><span>计划状态</span><strong>{{ plan?.status === 'LOADING' ? '拣货中' : plan ? '预装柜中' : '待装柜' }}</strong></div></section>
|
||||
<section v-if="container" class="capacity-grid"><div><div class="meter-title"><span>体积占用(含 110% 上限)</span><strong>{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³</strong></div><el-progress :percentage="Math.min(volumePercent,100)" :status="volumePercent > 100 ? 'exception' : undefined" /></div><div><div class="meter-title"><span>重量占用(含 110% 上限)</span><strong>{{ formatNumber(totalWeight,1) }} / {{ formatNumber(weightLimit,1) }} kg</strong></div><el-progress :percentage="Math.min(weightPercent,100)" :status="weightPercent > 100 ? 'exception' : undefined" /></div></section>
|
||||
<el-empty v-if="noDraft" description="该货柜尚未创建排柜计划"><el-button v-if="canCreate" type="primary" :loading="mutating" @click="createPlan"><Plus :size="16" />创建排柜计划</el-button></el-empty>
|
||||
<template v-else-if="plan">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type ContainerPlanStatus = 'EDITING' | 'LOADING' | 'COMPLETED'
|
||||
export type ContainerPlanStatus = 'WAIT_LOADING' | 'EDITING' | 'LOADING' | 'COMPLETED'
|
||||
|
||||
export interface ContainerBookingResource {
|
||||
id: string
|
||||
|
||||
@@ -51,14 +51,14 @@ const title = computed(() => {
|
||||
})
|
||||
|
||||
function statusLabel(status?: string | null) {
|
||||
return ({ LOADING: '装柜中', COMPLETED: '装柜完成', EDITING: '编辑中' } as Record<string, string>)[status || ''] || status || '—'
|
||||
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as Record<string, string>)[status || ''] || status || '—'
|
||||
}
|
||||
|
||||
function actionLabel(action: string) {
|
||||
return ({
|
||||
CREATE: '创建计划', ASSIGN: '分配货物', ADJUST: '调整货物', DELETE: '移除货物',
|
||||
RECOMMEND: '智能排柜', PUSH_PRELOAD: '推送预装', UPDATE_TRANSPORT: '修改运输信息',
|
||||
START_LOADING: '开始装柜', COMPLETE: '完成装柜',
|
||||
START_LOADING: '开始拣货', COMPLETE: '完成装柜',
|
||||
} as Record<string, string>)[action] || action
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,16 @@ describe('LoadingListsView', () => {
|
||||
expect(source).toContain('expectedVersion: detail.value.summary.version')
|
||||
})
|
||||
|
||||
it('renders business labels instead of raw loading-plan status codes', () => {
|
||||
expect(source).toContain("LOADING: '拣货中'")
|
||||
expect(source).toContain("COMPLETED: '装柜完成'")
|
||||
expect(source).toContain(':status="statusLabel(row.status)"')
|
||||
expect(detailSource).toContain("EDITING: '预装柜中'")
|
||||
expect(detailSource).toContain(':status="statusLabel(summary.status)"')
|
||||
expect(detailSource).not.toContain(':status="summary.status"')
|
||||
expect(source).not.toContain('仅装柜中允许修改')
|
||||
})
|
||||
|
||||
it('exposes a row edit action only for loading preload plans', () => {
|
||||
expect(source).toContain("isPreload && canEdit && row.status === 'LOADING'")
|
||||
expect(source).toContain('openEditFromRow(row)')
|
||||
|
||||
@@ -111,7 +111,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
}
|
||||
|
||||
function statusLabel(status?: string | null) {
|
||||
return status === 'LOADING' ? '装柜中' : status === 'COMPLETED' ? '装柜完成' : status || '未知状态'
|
||||
return ({ WAIT_LOADING: '待装柜', EDITING: '预装柜中', LOADING: '拣货中', COMPLETED: '装柜完成' } as Record<string, string>)[status || ''] || status || '—'
|
||||
}
|
||||
|
||||
function queryId(value: unknown) {
|
||||
@@ -338,7 +338,7 @@ onMounted(() => {
|
||||
:confirm-disabled="!canEdit"
|
||||
@confirm="saveTransport"
|
||||
>
|
||||
<el-alert type="info" :closable="false" show-icon title="仅装柜中允许修改;保存时按详情版本执行乐观锁校验。" />
|
||||
<el-alert type="info" :closable="false" show-icon title="仅拣货中允许修改;保存时按详情版本执行乐观锁校验。" />
|
||||
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
|
||||
<el-form-item label="集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" show-word-limit placeholder="请输入集装箱号" /></el-form-item>
|
||||
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" show-word-limit placeholder="请输入航次" /></el-form-item>
|
||||
|
||||
@@ -7,7 +7,7 @@ export const loadingListRoutes = [
|
||||
component: () => import('./pages/PreloadPlansView.vue'),
|
||||
meta: {
|
||||
title: '预装清单',
|
||||
description: '查看全部仓库装柜中的预装计划、货物明细和操作历史',
|
||||
description: '查看全部仓库拣货中的预装计划、货物明细和操作历史',
|
||||
permission: 'platform:firstleg:preload:list',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user