修复财务页面竞态并整理采购与运输展示
This commit is contained in:
@@ -238,6 +238,27 @@ describe('overseas business mounted pages', () => {
|
||||
})
|
||||
|
||||
it('mounts procurement list with customer candidates and real data', async () => { const wrapper = mountPage(PurchaseOrdersView); await flushPromises(); expect(listOverseasPurchaseCustomerOptions).toHaveBeenCalled(); expect(listOverseasPurchases).toHaveBeenCalledWith({ page: 1, size: 20, customerId: '', orderNo: '', purchaseCategory: '', productName: '', status: '' }); expect(wrapper.text()).toContain('OP-100'); expect(wrapper.text()).toContain('收货中'); expect(wrapper.text()).toContain('2026-08-01 10:00') })
|
||||
it.each([
|
||||
{ status: 'REJECTED' as const, label: '已拒绝' },
|
||||
{ status: 'CANCELLED' as const, label: '已取消' },
|
||||
])('filters procurement orders by $status and resets to all statuses', async ({ status, label }) => {
|
||||
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [{ ...purchase, status }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
const wrapper = mountPage(PurchaseOrdersView)
|
||||
await flushPromises()
|
||||
const statusSelect = wrapper.findAllComponents({ name: 'ElSelect' }).find(select => select.props('placeholder') === '全部状态')!
|
||||
expect(statusSelect.findAllComponents({ name: 'ElOption' }).map(option => ({ value: option.props('value'), label: option.props('label') }))).toEqual(expect.arrayContaining([
|
||||
{ value: 'REJECTED', label: '已拒绝' }, { value: 'CANCELLED', label: '已取消' },
|
||||
]))
|
||||
statusSelect.vm.$emit('update:modelValue', status)
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.findAll('button').find(button => button.text() === '查询')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ status }))
|
||||
expect(wrapper.find('.el-table__body .el-tag').text()).toBe(label)
|
||||
await wrapper.findAll('button').find(button => button.text() === '重置')!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(listOverseasPurchases).toHaveBeenLastCalledWith(expect.objectContaining({ status: '' }))
|
||||
})
|
||||
it('reviews pending procurement from the list, submits rejection as remark, and omits edit', async () => {
|
||||
vi.mocked(listOverseasPurchases).mockResolvedValue({ records: [{ ...purchase, status: 'PENDING_REVIEW' }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
const wrapper = mountPage(PurchaseOrdersView); await flushPromises()
|
||||
@@ -402,12 +423,15 @@ describe('overseas business mounted pages', () => {
|
||||
})
|
||||
|
||||
it('mounts truck list with enabled create and supported types', async () => { const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises(); expect(listUsTruckOrders).toHaveBeenCalled(); expect(wrapper.text()).toContain('TK-200'); expect(wrapper.text()).toContain('15拖'); expect(wrapper.text()).toContain('2026-08-01 10:00'); expect(wrapper.findAll('button').find((button) => button.text().includes('新增订单'))?.attributes('disabled')).toBeUndefined() })
|
||||
it('opens the requested truck review from the list using its customer scope', async () => {
|
||||
it('shows a truck detail entry instead of review and edit and keeps its customer scope', async () => {
|
||||
const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises()
|
||||
const auth = useAuthStore(); auth.permissions.push('platform:overseas-business:truck:status')
|
||||
await wrapper.vm.$nextTick()
|
||||
const review = wrapper.findAll('button').find(button => button.text() === '审核')
|
||||
expect(review).toBeDefined(); await review!.trigger('click'); await flushPromises()
|
||||
const actions = wrapper.findAll('.el-table__body-wrapper button')
|
||||
expect(actions.map(button => button.text())).toContain('详情')
|
||||
expect(actions.map(button => button.text())).not.toContain('审核')
|
||||
expect(actions.map(button => button.text())).not.toContain('编辑')
|
||||
await actions.find(button => button.text() === '详情')!.trigger('click'); await flushPromises()
|
||||
expect(routing.push).toHaveBeenCalledWith({ name: 'overseas-business-truck-detail', params: { orderId: 'TK-200' }, query: { customerId: '10002' } })
|
||||
})
|
||||
it('renders truck load failure without samples', async () => { vi.mocked(listUsTruckOrders).mockRejectedValue(new Error('hidden')); const wrapper = mountPage(TransportOrdersView, { domain: 'truck' }); await flushPromises(); expect(wrapper.text()).toContain('本土卡车订单加载失败'); expect(wrapper.text()).not.toContain('TK-200') })
|
||||
|
||||
@@ -5,9 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import RowActions from '@/shared/components/RowActions.vue'
|
||||
import type { DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import { listLocalLooseboardOrders, listUsTruckOrders } from '../api/platformOverseasBusiness'
|
||||
import { overseasBusinessPermissions as permissions } from '../permissions'
|
||||
import type { LocalLooseboardOrder } from '../types'
|
||||
import type { LocalLooseboardOrder, LocalTransportOrder } from '../types'
|
||||
import TransportOrdersView from './TransportOrdersView.vue'
|
||||
|
||||
const routing = vi.hoisted(() => ({ push: vi.fn(), route: { query: {} } }))
|
||||
@@ -30,7 +31,7 @@ function mountList(domain: 'truck' | 'looseboard', edit = false) {
|
||||
auth.permissions = [permissions.truckList, permissions.looseboardList, ...(edit ? [permissions.looseboardEdit] : [])]
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
return mount(TransportOrdersView, { props: { domain }, global: { plugins: [pinia, ElementPlus], stubs: {
|
||||
DataTable: { props: ['data'], template: '<div><div v-for="row in data" :key="row.id"><slot name="actions" :row="row" /></div></div>' },
|
||||
DataTable: { name: 'DataTable', props: ['data', 'columns'], template: '<div><div v-for="row in data" :key="row.id"><span v-for="column in columns" :key="column.key" :data-column="column.key">{{ column.prop ? row[column.prop] : \'\' }}</span><slot name="actions" :row="row" /></div></div>' },
|
||||
AppDialog: true,
|
||||
} } })
|
||||
}
|
||||
@@ -63,4 +64,34 @@ describe('transport document entry', () => {
|
||||
expect(listUsTruckOrders).toHaveBeenCalledOnce()
|
||||
expect(listLocalLooseboardOrders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('displays truck pallet counts instead of the unused package count', async () => {
|
||||
const truck: LocalTransportOrder = {
|
||||
id: '9007199254741001', customerId: '9007199254741999', orderNo: 'TR-TEST', fleetArrangement: null,
|
||||
vehicleType: 'PALLET_15', origin: 'LA', destination: 'ONT', palletCount: 3, packageCount: 0,
|
||||
receivedPalletCount: 0, allocatedPalletCount: 0, shippedPalletCount: 0,
|
||||
totalWeightKg: '100', totalVolumeCbm: '2', budgetAmount: '500', currency: 'USD',
|
||||
originContactName: null, originContactPhone: null, destinationContactName: null, destinationContactPhone: null,
|
||||
pickupAt: null, deliveryAt: null, status: 'PENDING_REVIEW', version: 1, createdAt: '', updatedAt: '',
|
||||
}
|
||||
vi.mocked(listUsTruckOrders).mockResolvedValue({ records: [truck], total: 1, page: 1, size: 20, pages: 1 })
|
||||
const wrapper = mountList('truck'); await flushPromises()
|
||||
const columns = wrapper.findComponent({ name: 'DataTable' }).props('columns') as DataTableColumn[]
|
||||
expect(columns.find(column => column.key === 'palletCount')).toMatchObject({ label: '托数', prop: 'palletCount' })
|
||||
expect(columns.some(column => column.key === 'packageCount')).toBe(false)
|
||||
expect(wrapper.get('[data-column="palletCount"]').text()).toBe('3')
|
||||
const actions = wrapper.findComponent(RowActions)
|
||||
expect(actions.text()).toContain('详情')
|
||||
expect(actions.text()).not.toContain('审核')
|
||||
expect(actions.text()).not.toContain('编辑')
|
||||
})
|
||||
|
||||
it('preserves looseboard package counts and forecast pallet counts', async () => {
|
||||
const wrapper = mountList('looseboard'); await flushPromises()
|
||||
const columns = wrapper.findComponent({ name: 'DataTable' }).props('columns') as DataTableColumn[]
|
||||
expect(columns.find(column => column.key === 'packageCount')).toMatchObject({ label: '件数', prop: 'packageCount' })
|
||||
expect(columns.find(column => column.key === 'estimatedPalletCount')).toMatchObject({ label: '预计托数', prop: 'estimatedPalletCount' })
|
||||
expect(wrapper.get('[data-column="packageCount"]').text()).toBe('20')
|
||||
expect(wrapper.get('[data-column="estimatedPalletCount"]').text()).toBe('2')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,7 +60,7 @@ const truckColumns: DataTableColumn[] = [
|
||||
{ key: 'origin', prop: 'origin', label: '起运地', minWidth: 145 }, { key: 'destination', prop: 'destination', label: '目的地', minWidth: 145 },
|
||||
{ key: 'originContactName', prop: 'originContactName', label: '起运地联系人', minWidth: 125 }, { key: 'originContactPhone', prop: 'originContactPhone', label: '起运地联系电话', minWidth: 155 },
|
||||
{ key: 'destinationContactName', prop: 'destinationContactName', label: '目的地联系人', minWidth: 125 }, { key: 'destinationContactPhone', prop: 'destinationContactPhone', label: '目的地联系电话', minWidth: 155 },
|
||||
{ key: 'packageCount', prop: 'packageCount', label: '件数', width: 80, align: 'right' }, { key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' },
|
||||
{ key: 'palletCount', prop: 'palletCount', label: '托数', width: 80, align: 'right' }, { key: 'totalWeightKg', prop: 'totalWeightKg', label: '总重量(kg)', width: 115, align: 'right' },
|
||||
{ key: 'budgetAmount', label: '预算总价', width: 120, align: 'right', formatter: (row: LocalTransportOrder) => row.budgetAmount == null ? '—' : `${row.budgetAmount} ${row.currency}` },
|
||||
{ key: 'pickupAt', label: '取货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.pickupAt) },
|
||||
{ key: 'deliveryAt', label: '送货时间', width: 165, formatter: (row: LocalTransportOrder) => formatDateTime(row.deliveryAt) },
|
||||
@@ -119,8 +119,7 @@ function looseboardActions(row: LocalLooseboardOrder): RowActionItem[] {
|
||||
}
|
||||
function truckActions(row: LocalTransportOrder): RowActionItem[] {
|
||||
return [
|
||||
{ key: 'detail', label: row.status === 'PENDING_REVIEW' && !row.fleetArrangement && access.can(overseasBusinessPermissions.truckStatus) ? '审核' : '详情', priority: 10 },
|
||||
{ key: 'edit', label: '编辑', visible: !row.fleetArrangement && access.can(editPermission.value), disabled: !canEdit(row) },
|
||||
{ key: 'detail', label: '详情', priority: 10 },
|
||||
{ key: 'arrangement', label: '承运安排', visible: !!row.fleetArrangement },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -68,6 +68,14 @@ describe('truck review and fulfillment', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('displays saved pallet counts instead of the unused package count', async () => {
|
||||
vi.mocked(getUsTruckOrder).mockResolvedValue(order({ palletCount: 3, packageCount: 0 }))
|
||||
const wrapper = mountDetail(); await flushPromises()
|
||||
const facts = new Map(wrapper.findAll('.truck-detail-fact').map(fact => [fact.get('dt').text(), fact.get('dd').text()]))
|
||||
expect(facts.get('托数')).toBe('3')
|
||||
expect(facts.has('件数')).toBe(false)
|
||||
})
|
||||
|
||||
it('groups the business number, single status and actions in the shared detail header', async () => {
|
||||
const wrapper = mountDetail(); await flushPromises()
|
||||
const header = wrapper.find('.detail-page-layout__header')
|
||||
|
||||
@@ -42,7 +42,7 @@ const fields = computed(() => detail.value ? [
|
||||
['取货联系人', detail.value.originContactName], ['取货联系电话', detail.value.originContactPhone],
|
||||
['送达地', detail.value.destination], ['送达地址', detail.value.destinationAddress],
|
||||
['送达联系人', detail.value.destinationContactName], ['送达联系电话', detail.value.destinationContactPhone],
|
||||
['件数', detail.value.packageCount], ['重量(kg)', detail.value.totalWeightKg], ['体积(m³)', detail.value.totalVolumeCbm],
|
||||
['托数', detail.value.palletCount], ['重量(kg)', detail.value.totalWeightKg], ['体积(m³)', detail.value.totalVolumeCbm],
|
||||
['预算总价', detail.value.budgetAmount == null ? '—' : `${detail.value.budgetAmount} ${detail.value.currency}`],
|
||||
['取货时间', dateTime(detail.value.pickupAt)], ['送货时间', dateTime(detail.value.deliveryAt)],
|
||||
['创建时间', dateTime(detail.value.createdAt)], ['备注', detail.value.remark],
|
||||
@@ -97,7 +97,7 @@ watch([orderNo, customerId], load, { immediate: true })
|
||||
<section v-if="detail.fleetArrangement" class="truck-detail-section"><FleetArrangementSummary :arrangement="detail.fleetArrangement" /><p class="truck-detail-note">取货和送达照片请在排车任务中上传。</p></section>
|
||||
|
||||
<section class="truck-detail-section truck-flow-section" aria-label="运输流程">
|
||||
<div class="truck-section-heading"><h2>运输流程</h2><span class="truck-flow-caption">审核通过后由排车单推进取货、派送和送达</span></div>
|
||||
<div class="truck-section-heading"><h2>运输流程</h2></div>
|
||||
<ol class="truck-flow">
|
||||
<li v-for="(step, index) in flowSteps" :key="step" :class="{ active: index === flowIndex, done: index < flowIndex || detail.status === 'COMPLETED' }">
|
||||
<span class="truck-flow-marker"><Check v-if="index < flowIndex || detail.status === 'COMPLETED'" :size="14" /><MapPin v-else-if="index === 2 || index === 4" :size="14" /><span v-else>{{ index + 1 }}</span></span>
|
||||
@@ -106,9 +106,9 @@ watch([orderNo, customerId], load, { immediate: true })
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>订单资料</h2><span class="truck-flow-caption">基础信息与货量</span></div><dl class="truck-detail-grid"><div v-for="[label, value] in fields" :key="String(label)" class="truck-detail-fact"><dt>{{ label }}</dt><dd>{{ value ?? '—' }}</dd></div></dl></section>
|
||||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>订单资料</h2></div><dl class="truck-detail-grid"><div v-for="[label, value] in fields" :key="String(label)" class="truck-detail-fact"><dt>{{ label }}</dt><dd>{{ value ?? '—' }}</dd></div></dl></section>
|
||||
<section class="truck-detail-section"><BusinessDocumentFees business-type="US_TRUCK" :business-no="detail.orderNo" :customer-id="detail.customerId" :business-id="detail.id" /></section>
|
||||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>审核与操作记录</h2><span class="truck-flow-caption">保留关键节点</span></div><div class="workbench-table"><DataTable :data="detail.histories" :columns="historyColumns" empty-text="暂无操作记录" /></div></section>
|
||||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>审核与操作记录</h2></div><div class="workbench-table"><DataTable :data="detail.histories" :columns="historyColumns" empty-text="暂无操作记录" /></div></section>
|
||||
</template>
|
||||
</DetailPageLayout></AdminPage>
|
||||
</template>
|
||||
@@ -121,7 +121,7 @@ watch([orderNo, customerId], load, { immediate: true })
|
||||
.truck-detail-section { margin-bottom: var(--admin-space-6); }
|
||||
.truck-section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.truck-section-heading h2 { margin: 0; font-size: var(--admin-font-section); color: var(--cc-text); }
|
||||
.truck-flow-caption, .truck-detail-note { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-detail-note { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.truck-detail-note { margin: 10px 0 0; }
|
||||
.truck-flow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.truck-flow li { position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; color: var(--cc-muted); text-align: center; }
|
||||
|
||||
Reference in New Issue
Block a user