This commit is contained in:
2026-11-06 10:42:39 +08:00
parent 8d80074266
commit f5f62af4a7
16 changed files with 184 additions and 30 deletions
@@ -152,10 +152,10 @@ describe('platform overseas business API adapter', () => {
})
it('supports looseboard list/detail/create and PUT update', async () => {
const detail = { summary: looseboardSummary, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [] }
const detail = { summary: looseboardSummary, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [], dispatchNos: ['PC-1', 'PC-2'] }
overseas.get.mockResolvedValueOnce(envelope({ records: [looseboardSummary], page: 1, size: 20, total: 1, pages: 1 })).mockResolvedValueOnce(envelope(detail))
expect((await listLocalLooseboardOrders({ status: 'PENDING_ACCEPTANCE' })).records[0]?.id).toBe('9007199254741005')
expect((await getLocalLooseboardOrder('LB-1', '10003')).status).toBe('PENDING_ACCEPTANCE')
expect(await getLocalLooseboardOrder('LB-1', '10003')).toMatchObject({ status: 'PENDING_ACCEPTANCE', dispatchNos: ['PC-1', 'PC-2'] })
const create = { productName: '家居用品', estimatedPalletCount: 2, originContactName: 'A', originContactPhone: '1', destinationContactName: 'B', destinationContactPhone: '2', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00', origin: 'LA', destination: 'ONT', packageCount: 8, totalWeightKg: 80.5, totalVolumeCbm: 3.2, idempotencyKey: 'HTTP-20260824-MARKER-looseboard' }
overseas.post.mockResolvedValue(envelope(detail)); await createLocalLooseboardOrder('9007199254742001', create)
expect(overseas.post).toHaveBeenCalledWith('/platform/v1/last-leg/local-looseboard-orders', create, { params: { customerId: '9007199254742001' } })
@@ -180,7 +180,7 @@ describe('platform overseas business API adapter', () => {
it('preserves unknown pool pallet service and product name in list and detail responses', async () => {
const summary = { ...looseboardSummary, palletRequired: null, productName: null }
overseas.get.mockResolvedValueOnce(envelope({ records: [summary], total: 1, page: 1, size: 20, pages: 1 })).mockResolvedValueOnce(envelope({ summary, histories: [] }))
overseas.get.mockResolvedValueOnce(envelope({ records: [summary], total: 1, page: 1, size: 20, pages: 1 })).mockResolvedValueOnce(envelope({ summary, histories: [], dispatchNos: [] }))
expect((await listLocalLooseboardOrders({})).records[0]).toMatchObject({ palletRequired: null, productName: null })
expect(await getLocalLooseboardOrder('LB-1', '10003')).toMatchObject({ palletRequired: null, productName: null })
expect(overseas.get).toHaveBeenCalledTimes(2)
@@ -27,7 +27,7 @@ interface TruckDetailPayload {
interface LooseboardSummaryPayload extends Omit<LocalLooseboardOrder, 'id' | 'budgetAmount' | 'totalWeightKg' | 'totalVolumeCbm'> { id: Numeric; budgetAmount?: Numeric | null; totalWeightKg: Numeric; totalVolumeCbm: Numeric }
interface LooseboardDetailPayload {
summary: LooseboardSummaryPayload; originAddress?: string | null; destinationAddress?: string | null
remark?: string | null; createdBy?: string | null; histories?: OverseasBusinessHistory[]
remark?: string | null; createdBy?: string | null; histories?: OverseasBusinessHistory[]; dispatchNos: string[]
}
function normalizePage<TPayload, TResult>(payload: PagePayload<TPayload>, normalize: (value: TPayload) => TResult): OverseasBusinessPage<TResult> {
@@ -49,8 +49,9 @@ function truckDetail(value: TruckDetailPayload): LocalTransportDetail {
function looseboardSummary(value: LooseboardSummaryPayload): LocalLooseboardOrder {
return { ...value, id: String(value.id), totalWeightKg: String(value.totalWeightKg), totalVolumeCbm: String(value.totalVolumeCbm), budgetAmount: value.budgetAmount == null ? null : String(value.budgetAmount) }
}
/** 详情保留按原单查询的全部排车,不依赖车队预订摘要。 */
function looseboardDetail(value: LooseboardDetailPayload): LocalLooseboardDetail {
return { ...looseboardSummary(value.summary), originAddress: value.originAddress || null, destinationAddress: value.destinationAddress || null, remark: value.remark || null, createdBy: value.createdBy || null, histories: value.histories || [] }
return { ...looseboardSummary(value.summary), originAddress: value.originAddress || null, destinationAddress: value.destinationAddress || null, remark: value.remark || null, createdBy: value.createdBy || null, histories: value.histories || [], dispatchNos: value.dispatchNos }
}
export async function listOverseasPurchaseCustomerOptions(keyword = '', limit = 200) {
@@ -9,6 +9,8 @@ import type { LocalLooseboardDetail } from '../types'
import LooseboardOrderDetailView from './LooseboardOrderDetailView.vue'
const routing = vi.hoisted(() => ({ push: vi.fn(), route: { params: { orderId: 'LB-TEST' }, query: { customerId: '9007199254741999' } as Record<string, string> } }))
const permissions = vi.hoisted(() => ({ can: vi.fn(() => false) }))
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => permissions }))
vi.mock('vue-router', () => ({ useRoute: () => routing.route, useRouter: () => ({ push: routing.push }) }))
vi.mock('../api/platformOverseasBusiness', () => ({ getLocalLooseboardOrder: vi.fn() }))
@@ -21,7 +23,7 @@ function order(overrides: Partial<LocalLooseboardDetail> = {}): LocalLooseboardD
pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00', packageCount: 20,
totalWeightKg: '100', totalVolumeCbm: '2', budgetAmount: '500', currency: 'USD',
status: 'PENDING_ACCEPTANCE', version: 1, remark: '运输备注', createdBy: '7',
createdAt: '2026-10-20T08:00:00', updatedAt: '2026-10-20T08:00:00', histories: [], ...overrides,
createdAt: '2026-10-20T08:00:00', updatedAt: '2026-10-20T08:00:00', histories: [], dispatchNos: [], ...overrides,
}
}
function mountDetail() {
@@ -31,11 +33,33 @@ function mountDetail() {
enableAutoUnmount(afterEach)
beforeEach(() => {
vi.clearAllMocks()
permissions.can.mockReturnValue(false)
routing.route = reactive({ params: { orderId: 'LB-TEST' }, query: { customerId: '9007199254741999' } })
vi.mocked(getLocalLooseboardOrder).mockResolvedValue(order())
})
describe('looseboard document fees', () => {
it('shows all actual dispatches without a fleet booking and links only with dispatch permission', async () => {
vi.mocked(getLocalLooseboardOrder).mockResolvedValue(order({ dispatchNos: ['PC-1', 'PC-2'] }))
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.find('.looseboard-dispatches').text()).toContain('PC-1')
expect(wrapper.find('.looseboard-dispatches').text()).toContain('PC-2')
expect(wrapper.find('.looseboard-dispatches button').exists()).toBe(false)
permissions.can.mockReturnValue(true)
await wrapper.findAll('button').find(button => button.text() === '刷新订单')!.trigger('click')
await flushPromises()
const links = wrapper.findAll('.looseboard-dispatches button')
expect(links).toHaveLength(2)
await links[1]!.trigger('click')
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'PC-2' } })
})
it('identifies a looseboard order that has not been dispatched', async () => {
const wrapper = mountDetail(); await flushPromises()
expect(wrapper.text()).toContain('排车单号')
expect(wrapper.text()).toContain('暂未排车')
})
it('refreshes the current status when returning to a cached document tab', async () => {
const visible = ref(true)
vi.mocked(getLocalLooseboardOrder).mockResolvedValueOnce(order({ status: 'WAITING_INBOUND' }))
@@ -3,6 +3,7 @@ import '@/styles/adminWorkbench.css'
import '@/styles/adminDensity.css'
import { computed, onActivated, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { ArrowLeft } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
@@ -19,6 +20,7 @@ import { poolOrderStatusLabels } from '../fleetArrangement'
import type { LocalLooseboardDetail, OverseasBusinessHistory } from '../types'
const route = useRoute(), router = useRouter()
const access = useAccessPreviewStore()
const orderNo = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
const customerId = computed(() => typeof route.query.customerId === 'string' ? route.query.customerId : '')
const detail = ref<LocalLooseboardDetail | null>(null), loading = ref(false)
@@ -81,7 +83,10 @@ onActivated(() => {
<template v-else-if="detail">
<section v-if="detail.fleetArrangement" class="looseboard-detail-section"><FleetArrangementSummary :arrangement="detail.fleetArrangement" /></section>
<section v-if="detail.deliveryMode === 'WAREHOUSE_PICKUP' && detail.transferNo" class="looseboard-detail-section"><el-alert title="上门取货需在对应取货排车的作业节点上传取货及到仓资料。" type="info" :closable="false" /><TransferPickupProgressPanel :transfer-no="detail.transferNo" :customer-id="detail.customerId" /></section>
<section class="looseboard-detail-section"><h2>订单资料</h2><el-descriptions class="density-descriptions" :column="3" border><el-descriptions-item v-for="[label, value] in fields" :key="String(label)" :label="String(label)">{{ value ?? '—' }}</el-descriptions-item></el-descriptions></section>
<section class="looseboard-detail-section"><h2>订单资料</h2><el-descriptions class="density-descriptions" :column="3" border>
<el-descriptions-item label="排车单号" :span="3"><div v-if="detail.dispatchNos.length" class="looseboard-dispatches"><template v-for="dispatchNo in detail.dispatchNos" :key="dispatchNo"><el-button v-if="access.can('platform:overseas-warehouse:dispatch:list')" link type="primary" @click="router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo } })">{{ dispatchNo }}</el-button><span v-else>{{ dispatchNo }}</span></template></div><span v-else>暂未排车</span></el-descriptions-item>
<el-descriptions-item v-for="[label, value] in fields" :key="String(label)" :label="String(label)">{{ value ?? '—' }}</el-descriptions-item>
</el-descriptions></section>
<section class="looseboard-detail-section"><BusinessDocumentFees business-type="LOCAL_LOOSEBOARD" :business-no="detail.orderNo" :customer-id="detail.customerId" :business-id="detail.id" /></section>
<section class="looseboard-detail-section"><h2>操作记录</h2><div class="workbench-table"><DataTable :data="detail.histories" :columns="historyColumns" empty-text="暂无操作记录" /></div></section>
</template>
@@ -92,4 +97,5 @@ onActivated(() => {
.looseboard-detail-header { flex-wrap: wrap; }
.looseboard-detail-header .density-detail-heading { flex: 1; }
.looseboard-detail-section { margin-bottom: var(--admin-space-6); }
.looseboard-dispatches { display: flex; flex-wrap: wrap; gap: var(--admin-space-2); overflow-wrap: anywhere; }
</style>
@@ -61,7 +61,7 @@ const procurementDetail = (): PlatformProcurement => ({
const truck: LocalTransportOrder = { fleetArrangement: null, id: '9007199254740995', customerId: '10002', orderNo: 'TK-200', vehicleType: 'PALLET_15', origin: 'Long Beach', destination: 'Ontario', palletCount: 2, packageCount: 20, receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0, totalWeightKg: '1200.000', totalVolumeCbm: '16.500', budgetAmount: '850.00', currency: 'USD', originContactName: 'Bob', originContactPhone: '10010', destinationContactName: 'Alice', destinationContactPhone: '10020', pickupAt: '2026-08-28T09:30:00', deliveryAt: '2026-08-28T15:45:00', status: 'PENDING_REVIEW', version: 1, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-02T10:00:00' }
const truckDetail: LocalTransportDetail = { ...truck, originAddress: 'Pier A', destinationAddress: 'Warehouse B', remark: null, createdBy: '7', pallets: [], histories: [] }
const looseboard: LocalLooseboardOrder = { fleetArrangement: null, id: '9007199254740997', customerId: '10003', orderNo: 'LB-300', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE', origin: 'LA', destination: 'ONT', productName: '家居用品', estimatedPalletCount: 2, packageCount: 8, totalWeightKg: '80.5', totalVolumeCbm: '3.2', budgetAmount: '50.00', currency: 'USD', originContactName: '发货人', originContactPhone: '1', destinationContactName: '收货人', destinationContactPhone: '2', pickupAt: '2026-10-20T09:30:00', deliveryAt: '2026-10-20T15:45:00', status: 'PENDING_ACCEPTANCE', version: 0, createdAt: '2026-08-01T10:00:00', updatedAt: '2026-08-01T10:00:00' }
const looseboardDetail: LocalLooseboardDetail = { ...looseboard, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [] }
const looseboardDetail: LocalLooseboardDetail = { ...looseboard, originAddress: null, destinationAddress: null, remark: null, createdBy: '7', histories: [], dispatchNos: [] }
const arrangement: FleetArrangement = { sourceType: 'FLEET_CAPACITY_OFFER', businessType: 'US_TRUCK', businessOrderNo: 'TR-200', bookingNo: 'FB-TEST', tripNo: 'TRIP-TEST', plannedDepartureAt: null, originWarehouseName: '我仓', destinationLabel: 'ONT8', deliveryMode: 'SELF_DELIVERY', deliveryInstructions: null, fulfillmentStage: 'WAITING_INBOUND', stageOccurredAt: null, agreedFreightAmount: null, currency: 'USD', incomingNo: null, dispatchNo: null, deliveryNo: null, signedPackages: null, unsignedPackages: null, returnedPackages: null, exceptionSummary: null, readOnlyReason: '池来源订单按实际运输履约推进' }
function activatePermissions() { const pinia = createPinia(); setActivePinia(pinia); const auth = useAuthStore(); auth.token = 'test-token'; auth.user = { id: '7', name: '测试操作员', account: 'tester', roles: [] }; auth.permissions = ['platform:overseas-business:purchase:list', 'platform:overseas-business:purchase:edit', 'platform:overseas-business:purchase:review', 'platform:overseas-business:truck:list', 'platform:overseas-business:truck:create', 'platform:overseas-business:truck:edit', 'platform:overseas-business:looseboard:list', 'platform:overseas-business:looseboard:create', 'platform:overseas-business:looseboard:edit']; useAccessPreviewStore().initializeAfterLogin(); return pinia }
+1
View File
@@ -92,6 +92,7 @@ export interface LocalLooseboardOrder {
export interface LocalLooseboardDetail extends LocalLooseboardOrder {
originAddress: string | null; destinationAddress: string | null; remark: string | null; createdBy: string | null
histories: OverseasBusinessHistory[]
dispatchNos: string[]
}
export interface CreateLocalLooseboardOrderRequest {
deliveryMode?: 'DELIVERY_TO_WAREHOUSE' | 'WAREHOUSE_PICKUP'