Merge branch 'dev' of http://8.163.30.128:10082/html/admin-web into dev
This commit is contained in:
@@ -312,13 +312,13 @@ describe('GlobalSmartLoadingView', () => {
|
||||
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, version: 3 })
|
||||
vi.mocked(recommendSmartLoading).mockRejectedValue(new AxiosError(
|
||||
vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError(
|
||||
'conflict',
|
||||
'ERR_BAD_REQUEST',
|
||||
undefined,
|
||||
undefined,
|
||||
{ data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
|
||||
))
|
||||
)).mockResolvedValueOnce({ ...plan, version: 4 })
|
||||
|
||||
const button = wrapper.findAll('button').find((item) => item.text().includes('智能推荐'))
|
||||
expect(button).toBeDefined()
|
||||
@@ -326,8 +326,9 @@ describe('GlobalSmartLoadingView', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2)
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2)
|
||||
expect(warningMessage).toHaveBeenCalledWith('计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作')
|
||||
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3)
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(3)
|
||||
expect(warningMessage).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).not.toContain('装柜操作失败')
|
||||
expect(wrapper.text()).not.toContain('request-1')
|
||||
})
|
||||
|
||||
@@ -296,7 +296,15 @@ async function assign() {
|
||||
async function recommend() {
|
||||
if (!plan.value) return
|
||||
await mutate(async () => {
|
||||
acceptPlan(await recommendSmartLoading(plan.value!.id, plan.value!.version))
|
||||
try {
|
||||
acceptPlan(await recommendSmartLoading(plan.value!.id, plan.value!.version))
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '智能推荐失败')
|
||||
if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error
|
||||
await loadSelectedData()
|
||||
if (!plan.value) throw error
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
}
|
||||
await loadSelectedData()
|
||||
ElMessage.success('智能推荐已生成')
|
||||
}, '智能推荐失败')
|
||||
@@ -377,7 +385,7 @@ async function mutate(
|
||||
const pickingMessage = recovery.pickingConflict && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
const message = pickingMessage || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作'
|
||||
const message = pickingMessage || details.message || '计划或 PDA 明细已变化,已刷新最新版本并保留筛选条件,请重新操作'
|
||||
if (recovery.preserveAssign) assignOpen.value = true
|
||||
if (pickingMessage) ElMessage.error(pickingMessage)
|
||||
else ElMessage.warning(message)
|
||||
|
||||
@@ -75,10 +75,10 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
vi.mocked(getSmartLoadingPlanByBooking)
|
||||
.mockResolvedValueOnce(planResponse)
|
||||
.mockResolvedValueOnce({ ...planResponse, version: 3 })
|
||||
vi.mocked(recommendSmartLoading).mockRejectedValue(new AxiosError(
|
||||
vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError(
|
||||
'conflict', 'ERR_BAD_REQUEST', undefined, undefined,
|
||||
{ data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
|
||||
))
|
||||
)).mockResolvedValueOnce({ ...planResponse, version: 4 })
|
||||
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] } })
|
||||
@@ -87,8 +87,9 @@ describe('SmartLoadingWorkbenchView', () => {
|
||||
await wrapper.findAll('button').find((button) => button.text().includes('智能推荐'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.text()).toContain('已刷新最新版本')
|
||||
expect(wrapper.text()).toContain('request-1')
|
||||
expect(recommendSmartLoading).toHaveBeenCalledWith(planResponse.id, 2)
|
||||
expect(recommendSmartLoading).toHaveBeenCalledWith(planResponse.id, 3)
|
||||
expect(wrapper.text()).not.toContain('排柜操作失败')
|
||||
})
|
||||
|
||||
it('uses bordered row actions and keeps push independent from containerNo', () => {
|
||||
|
||||
@@ -149,7 +149,18 @@ async function recommend() {
|
||||
if (!plan.value) return
|
||||
mutating.value = true
|
||||
mutationError.value = null
|
||||
try { acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)); ElMessage.success('智能建议已生成,请人工核对后再推送') }
|
||||
try {
|
||||
try {
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
} catch (error) {
|
||||
const details = getApiErrorDetails(error, '智能推荐失败')
|
||||
if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error
|
||||
await load()
|
||||
if (!plan.value) throw error
|
||||
acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version))
|
||||
}
|
||||
ElMessage.success('智能建议已生成,请人工核对后再推送')
|
||||
}
|
||||
catch (error) { await handleMutationError(error, '智能推荐失败') }
|
||||
finally { mutating.value = false }
|
||||
}
|
||||
@@ -196,7 +207,7 @@ async function handleMutationError(error: unknown, fallback: string, pickingConf
|
||||
const pickingMessage = pickingConflict && details.code === 'A0409' && details.message.includes('拣货')
|
||||
? '该订单在拣货中不许删除'
|
||||
: null
|
||||
mutationError.value = { ...details, message: pickingMessage || (details.code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details.message) }
|
||||
mutationError.value = { ...details, message: pickingMessage || details.message || (details.code === 'A0409' ? '计划或 PDA 明细已变化,已刷新最新版本并保留本次输入,请重新操作' : details.message) }
|
||||
ElMessage.error(mutationError.value.message)
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,12 @@ describe('TruckAppointmentDetailView status contract', () => {
|
||||
expect(source).toContain("status === 'DISPATCHED' && dispatchBlockReason.value")
|
||||
expect(source).toContain('attachmentOpen.value = true')
|
||||
})
|
||||
|
||||
it('keeps the main detail available when attachments or history fail', () => {
|
||||
expect(source).toContain('Promise.allSettled([')
|
||||
expect(source).toContain("attachments.status === 'fulfilled'")
|
||||
expect(source).toContain("histories.status === 'fulfilled'")
|
||||
expect(source).toContain('主详情数据不受影响')
|
||||
expect(source).toContain('v-if="relatedError"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,9 +15,20 @@ import TruckAttachmentDialog from '../components/TruckAttachmentDialog.vue'
|
||||
import { truckAppointmentActionLabel, truckAppointmentStatusLabel, truckAppointmentStatusLabels, type SaveTruckAppointmentRequest, type TruckAppointmentResource, type TruckAppointmentStatus, type TruckAppointmentStatusHistory } from '../types'
|
||||
const route = useRoute(); const router = useRouter(); const loading = ref(false); const saving = ref(false); const statusSaving = ref(false); const attachmentOpen = ref(false); const resource = ref<TruckAppointmentResource | null>(null); const error = ref<ApiErrorDetails | null>(null); const form = reactive<SaveTruckAppointmentRequest>({ expectedVersion: 0, customerId: null, carrierId: null, plannedLoadingAt: null })
|
||||
const history = ref<TruckAppointmentStatusHistory[]>([])
|
||||
const relatedError = ref('')
|
||||
const access = useAccessPreviewStore(); const editing = computed(() => route.query.edit === '1'); const id = computed(() => String(route.params.id)); const canEdit = computed(() => access.can('platform:firstleg:truck-appointment:edit') && ['DRAFT', 'PENDING_DISPATCH', 'RESCHEDULED'].includes(resource.value?.status || '') && editing.value); const canChangeStatus = computed(() => access.can('platform:firstleg:truck-appointment:operate')); const canManageAttachments = computed(() => access.can('platform:firstleg:truck-appointment:edit'))
|
||||
const relationSnapshot = computed(() => resource.value ? [resource.value.customerName, resource.value.oceanBookingNo, resource.value.cabinetNo, resource.value.containerNo, resource.value.warehouseName].filter(Boolean).join(' / ') || '无历史关联' : '')
|
||||
async function loadRelated() { if (!resource.value) return; const [attachments, histories] = await Promise.all([listTruckAppointmentAttachments(id.value), listTruckAppointmentHistory(id.value)]); resource.value.attachments = attachments; history.value = histories }
|
||||
async function loadRelated() {
|
||||
if (!resource.value) return
|
||||
relatedError.value = ''
|
||||
const [attachments, histories] = await Promise.allSettled([
|
||||
listTruckAppointmentAttachments(id.value),
|
||||
listTruckAppointmentHistory(id.value),
|
||||
])
|
||||
if (attachments.status === 'fulfilled') resource.value.attachments = attachments.value
|
||||
if (histories.status === 'fulfilled') history.value = histories.value
|
||||
if (attachments.status === 'rejected' || histories.status === 'rejected') relatedError.value = '附件或状态历史暂时未能加载,主详情数据不受影响,可稍后刷新重试。'
|
||||
}
|
||||
async function load() { loading.value = true; error.value = null; try { const result = await getTruckAppointment(id.value); resource.value = result; Object.assign(form, { ...result, expectedVersion: result.version ?? 0 }); await loadRelated() } catch (err) { error.value = getApiErrorDetails(err, '详情加载失败,请稍后重试') } finally { loading.value = false } }
|
||||
async function submit() { saving.value = true; error.value = null; try { await updateTruckAppointment(id.value, form); ElMessage.success('拖车预约已保存'); await load() } catch (err) { error.value = getApiErrorDetails(err, '保存失败,请稍后重试') } finally { saving.value = false } }
|
||||
const actionByTarget: Partial<Record<TruckAppointmentStatus, TruckAppointmentAction>> = { PENDING_DISPATCH: 'SUBMIT', DISPATCHED: 'DISPATCH', PICKUP_IN_PROGRESS: 'PICKUP', ARRIVED_WAREHOUSE: 'ARRIVE', LOADING: 'LOAD', DEPARTED: 'DEPART', COMPLETED: 'COMPLETE', EXCEPTION: 'EXCEPTION', CANCELLED: 'CANCEL' }
|
||||
@@ -56,6 +67,7 @@ onMounted(load)
|
||||
<template v-if="resource">
|
||||
<p class="relation-snapshot"><strong>历史关联快照:</strong>{{ relationSnapshot }}</p>
|
||||
<el-alert v-if="dispatchBlockReason" :title="dispatchBlockReason" type="warning" :closable="false" show-icon />
|
||||
<el-alert v-if="relatedError" :title="relatedError" type="warning" :closable="false" show-icon />
|
||||
<ApiErrorMeta v-if="error" :details="error" />
|
||||
</template>
|
||||
</section>
|
||||
|
||||
@@ -298,9 +298,10 @@ describe('UnifiedOrdersView contract', () => {
|
||||
|
||||
it('keeps the basic information compact and maps available order fields', () => {
|
||||
expect(source).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))')
|
||||
for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '总净重', '商检验', '提货联系人', '目的地地址']) {
|
||||
for (const label of ['业务类型', '买单 / 退税', '提货方式', '配载状态', '目的仓类型', '客户申报净重', '客户申报总体积', '商检验', '提货联系人', '目的地地址']) {
|
||||
expect(source).toContain(`<dt>${label}</dt>`)
|
||||
}
|
||||
expect(source).toContain("['actualVolumeCbm', 'chargeableVolumeCbm'].includes(property)")
|
||||
expect(source).toContain("partyValue(detail.order.parties, 'SENDER', 'phone')")
|
||||
expect(source).toContain("partyValue(detail.order.parties, 'RECEIVER', 'phone')")
|
||||
expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'")
|
||||
|
||||
@@ -401,7 +401,10 @@ function displayField(row: UnifiedOrderResource, property: string | undefined) {
|
||||
if (value === null || value === undefined || value === '') return '—'
|
||||
if (typeof value === 'boolean') return value ? '是' : '否'
|
||||
if (Array.isArray(value)) return value.length ? (property === 'skuNos' ? value.join('、') : `${value.length} 个文件`) : '—'
|
||||
if (typeof value === 'number') return formatNumber(value)
|
||||
if (typeof value === 'number') {
|
||||
if (['actualVolumeCbm', 'chargeableVolumeCbm'].includes(property)) return formatNumber(value, 3)
|
||||
return formatNumber(value)
|
||||
}
|
||||
if (property.endsWith('At') || property.endsWith('Date')) return formatDate(String(value))
|
||||
if (property === 'measurementReviewStatus') return measurementStatusLabel(String(value))
|
||||
if (property === 'internalOrderStatus') return orderStatusLabel(String(value))
|
||||
@@ -961,9 +964,9 @@ onBeforeUnmount(() => {
|
||||
<div><dt>是否整柜</dt><dd>{{ firstLegContainerModeLabel(detail.order.order?.containerMode) }}</dd></div>
|
||||
<div><dt>商检验</dt><dd>{{ displayBoolean(detail.order.order?.customsInspection) }}</dd></div>
|
||||
<div><dt>总箱数</dt><dd>{{ detail.summary.totalPackageCount ?? '—' }} 箱</dd></div>
|
||||
<div><dt>总毛重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'grossWeightKg') ?? detail.summary.measurement?.actualGrossWeightKg) }} kg</dd></div>
|
||||
<div><dt>总净重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'netWeightKg')) }} kg</dd></div>
|
||||
<div><dt>总体积</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'volumeCbm') ?? detail.summary.measurement?.actualVolumeCbm, 3) }} m³</dd></div>
|
||||
<div><dt>客户申报毛重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'grossWeightKg')) }} kg</dd></div>
|
||||
<div><dt>客户申报净重</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'netWeightKg')) }} kg</dd></div>
|
||||
<div><dt>客户申报总体积</dt><dd>{{ formatNumber(cargoTotal(detail.order.cargoes, 'volumeCbm'), 3) }} m³</dd></div>
|
||||
<div><dt>计费毛重</dt><dd>{{ formatNumber(detail.summary.measurement?.chargeableWeightKg) }} kg</dd></div>
|
||||
<div><dt>计费体积</dt><dd>{{ formatNumber(detail.summary.chargeableVolumeCbm, 3) }} m³</dd></div>
|
||||
<div><dt>总材积重</dt><dd>{{ formatNumber(detail.summary.volumetricWeightKg) }} kg</dd></div>
|
||||
|
||||
Reference in New Issue
Block a user