feat(lastleg): expose commercial source and booking references

This commit is contained in:
2026-10-09 16:11:59 +08:00
parent 3a917324f1
commit 00a186b37c
13 changed files with 134 additions and 14 deletions
@@ -67,6 +67,14 @@ describe('overseas business mounted pages', () => {
})
it('opens the associated commercial order using a server-side order number filter', async () => {
routing.route.query = { orderNo: 'LB-LINKED' }
const wrapper = mount(TransportOrdersView, { props: { domain: 'looseboard' }, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(listLocalLooseboardOrders).toHaveBeenCalledWith(expect.objectContaining({ orderNo: 'LB-LINKED' }))
expect(wrapper.find('input[placeholder="请输入商业单号"]').element).toHaveProperty('value', 'LB-LINKED')
})
it('opens pool arrangements from the list without another request or exposing edit actions', async () => {
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [{ ...truck, fleetArrangement: arrangement, status: 'WAITING_INBOUND' }], page: 1, size: 20, total: 1, pages: 1 })
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
@@ -162,7 +170,7 @@ describe('overseas business mounted pages', () => {
it('keeps truck edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'TK-200' }; routing.route.query = { customerId: '10002' }; vi.mocked(updateUsTruckOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('Ontario-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateUsTruckOrder).toHaveBeenCalledWith('TK-200', '10002', expect.objectContaining({ version: 1, destination: 'Ontario-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('Ontario-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() })
it('mounts truck create with explicit customer selector and enabled submit', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); expect(listPlatformTenants).toHaveBeenCalledWith({ page: 1, size: 200, status: 'ACTIVE' }); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() })
it('reuses one idempotency key and preserves truck create input after retries fail', async () => { vi.mocked(createUsTruckOrder).mockRejectedValue(new ApiResponseError('temporary failure', 'S0500', 500)); const wrapper = mountPage(TransportOrderFormView, { domain: 'truck' }); await flushPromises(); const selects = wrapper.findAllComponents({ name: 'ElSelect' }); selects[0].vm.$emit('update:modelValue', '10001'); wrapper.findComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'PORT_PICKUP'); const origin = wrapper.find('input[placeholder="城市 / 港口 / 州"]'); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await origin.setValue('LA-MARKER'); await destination.setValue('ONT-MARKER'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('联系人'))!.find('input').setValue('联系人'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('联系方式'))!.find('input').setValue('13800000000'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总重量'))!.find('input').setValue('100'); await wrapper.findAll('.el-form-item').find((item) => item.text().startsWith('总体积'))!.find('input').setValue('2.5'); wrapper.findComponent({ name: 'ElDatePicker' }).vm.$emit('update:modelValue', '2026-08-31'); await wrapper.vm.$nextTick(); const submit = wrapper.findAll('button').find((button) => button.text().includes('提交订单')); await submit?.trigger('click'); await flushPromises(); await submit?.trigger('click'); await flushPromises(); expect(createUsTruckOrder).toHaveBeenCalledTimes(2); const keys = vi.mocked(createUsTruckOrder).mock.calls.map((call) => call[1].idempotencyKey); expect(keys[0]).toBeTruthy(); expect(keys[1]).toBe(keys[0]); expect((origin.element as HTMLInputElement).value).toBe('LA-MARKER'); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER'); expect(document.body.textContent).toContain('服务暂时不可用'); expect(routing.push).not.toHaveBeenCalled() })
it('loads the real looseboard list', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises(); expect(listLocalLooseboardOrders).toHaveBeenCalledWith({ page: 1, size: 20, routeKeyword: '', status: '' }); expect(wrapper.text()).toContain('LB-300'); expect(wrapper.text()).toContain('待接单'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(listUsTruckOrders).not.toHaveBeenCalled() })
it('loads the real looseboard list', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'looseboard' }); await flushPromises(); expect(listLocalLooseboardOrders).toHaveBeenCalledWith({ page: 1, size: 20, orderNo: '', routeKeyword: '', status: '' }); expect(wrapper.text()).toContain('LB-300'); expect(wrapper.text()).toContain('待接单'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(listUsTruckOrders).not.toHaveBeenCalled() })
it('loads and submits pending-acceptance looseboard edit with PUT adapter', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(getLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003'); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, transportMode: 'SHARED_TRUCK', palletRequired: 'YES' })) })
it('keeps looseboard edit form and version unchanged after a 409', async () => { routing.route.params = { orderId: 'LB-300' }; routing.route.query = { customerId: '10003' }; vi.mocked(updateLocalLooseboardOrder).mockRejectedValue(new ApiResponseError('订单版本冲突', 'B0409', 409)); const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); const destination = wrapper.find('input[placeholder="城市 / 州"]'); await destination.setValue('ONT-MARKER-EDIT'); await wrapper.findAll('button').find((button) => button.text().includes('保存修改'))?.trigger('click'); await flushPromises(); expect(updateLocalLooseboardOrder).toHaveBeenCalledWith('LB-300', '10003', expect.objectContaining({ version: 0, destination: 'ONT-MARKER-EDIT' })); expect((destination.element as HTMLInputElement).value).toBe('ONT-MARKER-EDIT'); expect(document.body.textContent).toContain('订单版本冲突'); expect(routing.push).not.toHaveBeenCalled() })
it('mounts looseboard create without a detail route or pending notice', async () => { const wrapper = mountPage(TransportOrderFormView, { domain: 'looseboard' }); await flushPromises(); expect(wrapper.text()).toContain('客户信息'); expect(wrapper.text()).toContain('请选择客户'); expect(wrapper.text()).not.toContain('接口能力待后端补齐'); expect(wrapper.findAll('button').find((button) => button.text().includes('提交订单'))?.attributes('disabled')).toBeUndefined() })
@@ -1,7 +1,7 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { Plus } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
@@ -19,7 +19,7 @@ import { overseasBusinessPermissions } from '../permissions'
import type { LocalLooseboardOrder, LocalLooseboardQuery, LocalLooseboardStatus, LocalTransportOrder, LocalTransportQuery, LocalTransportStatus, LocalTransportType, LooseboardTransportMode } from '../types'
const props = defineProps<{ domain: 'truck' | 'looseboard' }>()
const router = useRouter(); const access = useAccessPreviewStore()
const route = useRoute(); const router = useRouter(); const access = useAccessPreviewStore()
const isLooseboard = computed(() => props.domain === 'looseboard'); const domainLabel = computed(() => isLooseboard.value ? '本土散板' : '本土卡车')
const queryPreferenceKey = computed(() => isLooseboard.value ? 'overseas-business-looseboard-query' : 'overseas-business-truck-query')
const createPermission = computed(() => isLooseboard.value ? overseasBusinessPermissions.looseboardCreate : overseasBusinessPermissions.truckCreate)
@@ -28,12 +28,13 @@ const rows = ref<Array<LocalTransportOrder | LocalLooseboardOrder>>([]); const l
const page = ref(1); const pageSize = ref(20); const total = ref(0)
const arrangementOpen = ref(false), arrangement = ref<FleetArrangement | null>(null)
function showArrangement(row: LocalTransportOrder | LocalLooseboardOrder) { arrangement.value = row.fleetArrangement; arrangementOpen.value = true }
const filters = ref<QueryModel>({ status: '', routeKeyword: '' }); const applied = ref({ ...filters.value })
const filters = ref<QueryModel>({ orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }); const applied = ref({ ...filters.value })
const truckStatusLabels: Record<LocalTransportStatus, string> = { PENDING_REVIEW: '待审核', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PALLET_OVERAGE_REVIEW: '超托待审', PARTIALLY_ALLOCATED: '部分分配', ALLOCATED: '已分配', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', REJECTED: '已拒绝', CANCELLED: '已取消' }
const looseboardStatusLabels: Record<LocalLooseboardStatus, string> = { PENDING_ACCEPTANCE: '待接单', WAITING_INBOUND: '待入仓', RECEIVING: '收货中', WAITING_CONSOLIDATION: '待合托', PENDING_DISPATCH: '待派车', DISPATCHED: '已派车', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消', ...poolOrderStatusLabels }
const transportTypeLabels: Record<LocalTransportType, string> = { PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' }
const transportModeLabels: Record<LooseboardTransportMode, string> = { SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' }
const fields = computed<QueryField[]>(() => [
{ key: 'orderNo', type: 'input', label: '商业单号', placeholder: '请输入商业单号', width: 230 },
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 170, options: Object.entries(isLooseboard.value ? looseboardStatusLabels : truckStatusLabels).map(([value, label]) => ({ value, label })) },
{ key: 'routeKeyword', type: 'input', label: '运输路线', placeholder: '起运地/目的地', width: 240 },
])
@@ -64,10 +65,10 @@ async function load() {
rows.value = []; total.value = 0; loadError.value = null; loading.value = true
try {
if (isLooseboard.value) {
const result = await listLocalLooseboardOrders({ page: page.value, size: pageSize.value, routeKeyword: String(applied.value.routeKeyword || ''), status: String(applied.value.status || '') as LocalLooseboardStatus | '' } satisfies LocalLooseboardQuery)
const result = await listLocalLooseboardOrders({ page: page.value, size: pageSize.value, orderNo: String(applied.value.orderNo || ''), routeKeyword: String(applied.value.routeKeyword || ''), status: String(applied.value.status || '') as LocalLooseboardStatus | '' } satisfies LocalLooseboardQuery)
rows.value = result.records; total.value = result.total
} else {
const result = await listUsTruckOrders({ page: page.value, size: pageSize.value, routeKeyword: String(applied.value.routeKeyword || ''), status: String(applied.value.status || '') as LocalTransportStatus | '' } satisfies LocalTransportQuery)
const result = await listUsTruckOrders({ page: page.value, size: pageSize.value, orderNo: String(applied.value.orderNo || ''), routeKeyword: String(applied.value.routeKeyword || ''), status: String(applied.value.status || '') as LocalTransportStatus | '' } satisfies LocalTransportQuery)
rows.value = result.records; total.value = result.total
}
} catch (error) { loadError.value = getApiErrorDetails(error, `${domainLabel.value}订单加载失败,请稍后重试`) } finally { loading.value = false }
@@ -76,7 +77,7 @@ function search(model: QueryModel) { applied.value = { ...model }; if (page.valu
function openCreate() { void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-new' : 'overseas-business-truck-new' }) }
function canEdit(row: LocalTransportOrder | LocalLooseboardOrder) { return !row.fleetArrangement && (isLooseboard.value ? row.status === 'PENDING_ACCEPTANCE' : row.status === 'PENDING_REVIEW') }
function openEdit(row: LocalTransportOrder | LocalLooseboardOrder) { if (!canEdit(row)) return; void router.push({ name: isLooseboard.value ? 'overseas-business-looseboard-edit' : 'overseas-business-truck-edit', params: { orderId: row.orderNo }, query: { customerId: row.customerId } }) }
watch([page, pageSize], () => { void load() }); watch(isLooseboard, () => { page.value = 1; applied.value = { status: '', routeKeyword: '' }; filters.value = { ...applied.value }; void load() }); onMounted(() => { void load() })
watch([page, pageSize], () => { void load() }); watch([isLooseboard, () => route.query.orderNo], () => { page.value = 1; applied.value = { orderNo: String(route.query.orderNo || ''), status: '', routeKeyword: '' }; filters.value = { ...applied.value }; void load() }); onMounted(() => { void load() })
</script>
<template>