From ba072611dcf85eb0af8198b6a8419e6bb4072fe9 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Wed, 21 Oct 2026 18:05:02 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../overseas-master-data/PROTOTYPE_MATRIX.md | 9 +
.../components/CapacityDialogClosing.test.ts | 17 +-
.../pages/CapacityOfferDetailView.vue | 29 ++-
.../pages/CapacityOffersBehavior.test.ts | 189 ++++++++----------
.../pages/CapacityOffersView.vue | 38 ++--
5 files changed, 145 insertions(+), 137 deletions(-)
diff --git a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
index 9d310c78..8f28029d 100644
--- a/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
+++ b/src/modules/overseas-master-data/PROTOTYPE_MATRIX.md
@@ -1,5 +1,14 @@
# 海外通用资料原型核对矩阵
+## 散板需求只读展示(2026-10-21)
+
+依据本轮用户确认:仅保留内部余位需求池,人工联系下单;登记与后续拼车直接隐藏,不增加开关,不删除代码。当前范围唯一见[拼车主契约](../../../../../docs/海外仓业务/内部拼车需求池首版开发契约.md#当前页面范围2026-10-21),覆盖下文此前开放登记的页面结论。
+
+| 页面/操作 | 当前承载与调整 | 验收 |
+|---|---|---|
+| 散板需求列表/查询、分页、详情 | 保持独立列表与原余位来源,默认展示车次余位、线路、时间、联系信息;隐藏待匹配散板、我的登记、登记及待办入口,原代码保留 | 43项定向测试及页面、测试源码定向类型检查通过;待人工1920×1080、100%截图确认 |
+| 散板需求详情/查看、重新核对 | 保持独立详情,仅展示运输摘要、联系方式和分段余位;隐藏登记、意向列表、来货及履约窗口,旧登记参数不再打开窗口 | 同上 |
+
## 2026-10-20 列表顶部紧凑收尾
依据为本轮用户整柜业务截图及“所有列表顶部紧凑”的确认;列表公共工具栏和筛选内距由 `adminWorkbench.css` 统一维护。
diff --git a/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts b/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts
index 0f7ee28b..d12f529c 100644
--- a/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts
+++ b/src/modules/overseas-master-data/components/CapacityDialogClosing.test.ts
@@ -2,11 +2,10 @@ import ElementPlus, { ElMessageBox } from 'element-plus'
import { createPinia } from 'pinia'
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import { defineComponent } from 'vue'
+import { defineComponent, ref } from 'vue'
import { createMemoryHistory, createRouter, RouterView } from 'vue-router'
import CapacityInquiryDialog from './CapacityInquiryDialog.vue'
import CapacityOfferSettingsDialog from './CapacityOfferSettingsDialog.vue'
-import CapacityOfferDetailView from '../pages/CapacityOfferDetailView.vue'
import type { CapacityOffer } from '../capacityOfferTypes'
import type { FleetTrip } from '../tripTypes'
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
@@ -64,11 +63,13 @@ it.each(['inquiry', 'settings'] as const)('keeps the real %s dialog and edited i
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
})
-it('blocks browser back with the real router, cached detail and native dialog, without duplicating the global prompt', async () => {
+it('preserves route-leave protection in the retained native inquiry dialog without duplicating the global prompt', async () => {
const pinia = createPinia(), registry = useUnsavedWorkStore(pinia), confirms = vi.spyOn(ElMessageBox, 'confirm')
+ // 登记页面入口已隐藏;保留组件自身与真实路由的未保存保护回归。
+ const InquiryHost = defineComponent({ components: { CapacityInquiryDialog }, setup: () => ({ offer, open: ref(false) }), template: '' })
const router = createRouter({ history: createMemoryHistory(), routes: [
{ path: '/pool', name: 'overseas-master-data-capacity-offers', component: { template: '
运力列表
' }, meta: { requiresAuth: true } },
- { path: '/pool/:offerNo', name: 'overseas-master-data-capacity-offer-detail', component: CapacityOfferDetailView, meta: { requiresAuth: true } },
+ { path: '/pool/:offerNo', name: 'overseas-master-data-capacity-offer-detail', component: InquiryHost, meta: { requiresAuth: true } },
{ path: '/outside', name: 'outside', component: { template: '外部页面
' }, meta: { workspaceTab: false } },
] })
// Match the application's intentional policy: cross-workspace switching skips the global prompt.
@@ -81,7 +82,7 @@ it('blocks browser back with the real router, cached detail and native dialog, w
await router.push('/pool'); await router.isReady()
const wrapper = mount(Host, { attachTo: document.body, global: { plugins: [pinia, router, ElementPlus] } }); wrappers.push(wrapper)
await router.push('/pool/CO-CLOSE'); await flushPromises()
- await wrapper.findAll('button').find(button => button.text() === '登记客户货物')!.trigger('click'); await flushPromises()
+ await wrapper.findAll('button').find(button => button.text() === '打开保留的登记组件')!.trigger('click'); await flushPromises()
await wrapper.get('details summary').trigger('click')
const input = wrapper.get('input[placeholder="填写货物名称(确认承运前必填)"]')
await input.setValue('UI-修复后保留输入'); await flushPromises()
@@ -92,15 +93,15 @@ it('blocks browser back with the real router, cached detail and native dialog, w
expect(confirms).toHaveBeenCalledOnce(); expect(wrapper.get('.app-dialog').isVisible()).toBe(true)
await continueEditing()
expect(router.currentRoute.value.name).toBe('overseas-master-data-capacity-offer-detail')
- expect(input.element.value).toBe('UI-修复后保留输入'); expect(getCapacityOffer).toHaveBeenCalledExactlyOnceWith('CO-CLOSE')
+ expect(input.element.value).toBe('UI-修复后保留输入'); expect(getCapacityOffer).not.toHaveBeenCalled()
confirms.mockClear(); router.back(); await flushPromises()
const message = [...document.querySelectorAll('.el-message-box')].at(-1)!
const leave = [...message.querySelectorAll('button')].find(button => button.textContent?.trim() === '放弃并离开') as HTMLButtonElement
expect(leave).toBeTruthy(); leave.click(); await flushPromises()
expect(router.currentRoute.value.name).toBe('overseas-master-data-capacity-offers')
- expect(confirms).toHaveBeenCalledOnce(); expect(getCapacityOffer).toHaveBeenCalledExactlyOnceWith('CO-CLOSE')
+ expect(confirms).toHaveBeenCalledOnce(); expect(getCapacityOffer).not.toHaveBeenCalled()
await new Promise(resolve => setTimeout(resolve, 50)); await router.push('/pool/CO-CLOSE'); await flushPromises()
- await wrapper.findAll('button').find(button => button.text() === '登记客户货物')!.trigger('click'); await flushPromises()
+ await wrapper.findAll('button').find(button => button.text() === '打开保留的登记组件')!.trigger('click'); await flushPromises()
await wrapper.get('input[placeholder="填写货物名称(确认承运前必填)"]').setValue('另一次修改'); await flushPromises()
confirms.mockClear(); const navigation = router.push('/outside'); await flushPromises()
expect(confirms).toHaveBeenCalledOnce(); expect(document.body.textContent).toContain('全局离开确认')
diff --git a/src/modules/overseas-master-data/pages/CapacityOfferDetailView.vue b/src/modules/overseas-master-data/pages/CapacityOfferDetailView.vue
index 263fc536..a8738fbe 100644
--- a/src/modules/overseas-master-data/pages/CapacityOfferDetailView.vue
+++ b/src/modules/overseas-master-data/pages/CapacityOfferDetailView.vue
@@ -54,13 +54,13 @@ async function load() {
const value = await getCapacityOffer(String(route.params.offerNo))
let checked: CapacityInquiry | null = null
// 当前只读展示余位,旧登记深链不再打开登记窗口;原流程保留。
- if (false) {
+ /*
if (typeof route.query?.inquiry === 'string' && /^[A-Z][A-Z0-9-]{1,63}$/.test(route.query.inquiry)) {
const returned = await getCapacityInquiry(route.query.inquiry)
if (returned.offerNo !== value.offer.offerNo) throw new Error('登记不属于当前发布')
if (current === sequence) { checked = returned; readonly.value = true; confirmMode.value = false; editorOpen.value = true }
} else if (editing.value && editorOpen.value) checked = value.inquiries.records.find(row => row.bookingNo === editing.value?.bookingNo) || await getCapacityInquiry(editing.value.bookingNo)
- }
+ */
if (current === sequence) { resetting = true; detail.value = value; if (checked) editing.value = checked; page.value = value.inquiries.page; size.value = value.inquiries.size; queueMicrotask(() => { resetting = false }) }
}
catch (cause) { if (current === sequence) { detail.value = null; error.value = getApiErrorDetails(cause, '发布详情读取失败') } }
@@ -93,18 +93,29 @@ watch([page, size], loadInquiries)
onBeforeUnmount(() => { followupSequence++; sequence++; inquirySequence++ })
-
+
- 车次号
- {{ detail.offer.tripNo }}{{ detail.offer.tripNo }}
- 起运仓
- {{ detail.offer.warehouseName }}
- 计划发车
- {{ offerTime(detail.offer.plannedDepartureAt) }}
- 最晚送仓
- {{ offerTime(detail.offer.cutoffAt) }}
- 调度联系人
- {{ detail.offer.contactName || '—' }}
- 调度联系方式
- {{ detail.offer.contactPhone || '—' }}
- 无法登记客户货物:{{ detail.offer.blockReason || '当前发布不接受新意向' }}
- {{ detail.offer.blockReason || '按所选卸货站核对每段余位;登记客户货物不占位,调度确认后占位。' }}
{{ detail.offer.blockReason }}
{{ detail.offer.remark }}
+ {{ detail.offer.blockReason }}
{{ detail.offer.remark }}
分段运力
{{ loadText(row.planned) }}{{ loadText(row.reserved) }}{{ detail.offer.capacityKnown ? loadText(row.remaining) : '待调度核对' }}
- {{ access.can('platform:fleet:capacity-offer:manage') ? '本发布全部意向' : '我的登记' }}
- 重试原操作
+
-
-
+
+
diff --git a/src/modules/overseas-master-data/pages/CapacityOffersBehavior.test.ts b/src/modules/overseas-master-data/pages/CapacityOffersBehavior.test.ts
index c9a4a81b..377d033e 100644
--- a/src/modules/overseas-master-data/pages/CapacityOffersBehavior.test.ts
+++ b/src/modules/overseas-master-data/pages/CapacityOffersBehavior.test.ts
@@ -26,7 +26,7 @@ function mount(component: object, props: Record = {}): { wrappe
const pinia = createPinia()
const wrapper = shallowMount(component, { props, global: { plugins: [pinia], directives: { loading: () => undefined }, stubs: {
AdminPage: layout, ListPageLayout: layout, DetailPageLayout: layout,
- DataTable: { name: 'DataTable', props: ['data', 'page', 'pageSize', 'total'], emits: ['update:page'], template: '' },
+ DataTable: { name: 'DataTable', props: ['data', 'page', 'pageSize', 'total', 'columns'], emits: ['update:page'], template: '' },
QueryToolbar: { name: 'QueryToolbar', props: ['modelValue', 'fields'], emits: ['update:modelValue', 'search'], template: '' },
CapacityInquiryTable: { name: 'CapacityInquiryTable', props: ['rows', 'total', 'failed'], emits: ['action', 'update:page'], template: '
' },
CapacityInquiryDialog: component === CapacityInquiryDialog ? false : true,
@@ -59,35 +59,46 @@ it.each(['DRAFT', 'CONFIRMED', 'RECEIVED', 'BOUND', 'CANCELLED'] as const)('labe
expect(detail.find('[label="已核实实托数"]').exists()).toBe(false)
expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
})
-it('loads only the active tab and sends exactly one query after resetting from a later page', async () => {
+it('keeps the pool read-only and sends one query after resetting from a later page', async () => {
const { wrapper } = mount(CapacityOffersView); await flushPromises()
- expect(api.listCapacityOffers).toHaveBeenCalledOnce(); expect(api.listCapacityInquiries).not.toHaveBeenCalled()
+ expect(api.listCapacityOffers).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'AVAILABLE' }))
+ expect(api.listCapacityInquiries).not.toHaveBeenCalled()
+ expect(api.listLooseboardDemands).not.toHaveBeenCalled()
expect(wrapper.text()).toContain('待调度核对')
+ expect(wrapper.findComponent(CapacityInquiryDialog).exists()).toBe(false)
+ expect(wrapper.findComponent({ name: 'CapacityInquiryFulfillment' }).exists()).toBe(false)
+ for (const label of ['我的登记', '待匹配散板', '登记客户货物', '待我处理']) expect(button(wrapper, label)).toBeUndefined()
wrapper.findComponent({ name: 'DataTable' }).vm.$emit('update:page', 2); await flushPromises()
wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
- expect(api.listCapacityOffers).toHaveBeenCalledTimes(3); expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
- await button(wrapper, '我的登记').trigger('click'); await flushPromises()
- expect(api.listCapacityInquiries).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'MINE' })); expect(api.getCapacityOffer).not.toHaveBeenCalled()
- const query = wrapper.findComponent({ name: 'QueryToolbar' }); query.vm.$emit('update:modelValue', { scope: 'ALL' }); await flushPromises(); query.vm.$emit('search'); await flushPromises()
- expect(api.listCapacityInquiries).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'MINE' }))
+ expect(api.listCapacityOffers).toHaveBeenCalledTimes(3)
+ expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }))
})
-it('ignores an older page response after changing tabs and retains an explicit error state', async () => {
+
+it('ignores an older pool query response and retains errors with retry', async () => {
let resolve!: (value: ReturnType>) => void
- vi.mocked(api.listCapacityOffers).mockReturnValue(new Promise(done => { resolve = done }))
- const { wrapper } = mount(CapacityOffersView); await button(wrapper, '我的登记').trigger('click'); await flushPromises()
+ vi.mocked(api.listCapacityOffers).mockReturnValueOnce(new Promise(done => { resolve = done }))
+ const { wrapper } = mount(CapacityOffersView)
+ vi.mocked(api.listCapacityOffers).mockResolvedValue(pageOf([offer], 7))
+ wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
resolve(pageOf([offer], 900)); await flushPromises()
- expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).props('total')).toBe(1)
- vi.mocked(api.listCapacityInquiries).mockRejectedValue(new Error('读取失败')); wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
+ expect(wrapper.findComponent({ name: 'DataTable' }).props('total')).toBe(7)
+ vi.mocked(api.listCapacityOffers).mockRejectedValueOnce(new Error('读取失败'))
+ wrapper.findComponent({ name: 'QueryToolbar' }).vm.$emit('search'); await flushPromises()
expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true)
- expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).props('failed')).toBe(true)
+ wrapper.findComponent({ name: 'LoadErrorState' }).vm.$emit('retry'); await flushPromises()
+ expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(false)
})
-it('reads detail once, permits drafting while capacity is unknown, and never preloads customers or quotes', async () => {
+
+it('reads only the offer detail and hides registration even with write permission', async () => {
const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
- expect(api.getCapacityOffer).toHaveBeenCalledOnce(); expect(api.listCapacityInquiries).not.toHaveBeenCalled()
- await button(wrapper, '登记客户货物').trigger('click'); await flushPromises()
- expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('modelValue')).toBe(true)
+ expect(api.getCapacityOffer).toHaveBeenCalledOnce()
+ expect(api.listCapacityInquiries).not.toHaveBeenCalled()
+ expect(button(wrapper, '登记客户货物')).toBeUndefined()
+ expect(wrapper.findComponent(CapacityInquiryDialog).exists()).toBe(false)
+ expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).exists()).toBe(false)
expect(api.listOfferCustomers).not.toHaveBeenCalled(); expect(api.listOfferQuotes).not.toHaveBeenCalled()
})
+
it('preserves nullable estimates and retries the original immutable create request after an uncertain result', async () => {
vi.mocked(api.saveCapacityInquiry).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce({ offer, inquiry, replayed: true })
const { wrapper, pinia } = mount(CapacityInquiryDialog, { modelValue: true, offer, inquiry: null, readonly: false }); await flushPromises()
@@ -120,12 +131,13 @@ it('retains an uncertain request and route protection after closing and reopenin
wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(api.saveCapacityInquiry).toHaveBeenNthCalledWith(2, offer.offerNo, inquiry.bookingNo, first)
})
-it('keeps sales from confirming even if a stale row advertises the action', async () => {
- const row = { ...inquiry, actions: { ...inquiry.actions, canConfirm: true } }
- const { wrapper } = mount(CapacityOffersView); await flushPromises(); await button(wrapper, '我的登记').trigger('click'); await flushPromises()
- wrapper.findComponent({ name: 'CapacityInquiryTable' }).vm.$emit('action', 'confirm', row); await flushPromises()
- expect(api.commandCapacityInquiry).not.toHaveBeenCalled(); expect(ElMessageBox.confirm).not.toHaveBeenCalled()
+it('retains pool route details as the only cargo row action for sales', async () => {
+ const { wrapper } = mount(CapacityOffersView); await flushPromises()
+ expect(wrapper.findComponent({ name: 'CapacityInquiryTable' }).exists()).toBe(false)
+ expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
+ expect(ElMessageBox.confirm).not.toHaveBeenCalled()
})
+
it('loads publishing settings only when explicitly opened, not with the trip page', async () => {
state.manage = true
const { wrapper } = mount(CapacityOfferSettingsDialog, { modelValue: false, trip: { tripNo: 'TR-1', recordVersion: 5 } as FleetTrip }); await flushPromises()
@@ -148,24 +160,22 @@ it('retries an uncertain combined save and confirmation with the original payloa
expect(wrapper.emitted('saved')).toHaveLength(1)
})
-it('keeps the create button disabled with a reason after cutoff, while hiding it without write permission', async () => {
+it('keeps capacity blockers visible without registration prompts after cutoff', async () => {
vi.mocked(api.getCapacityOffer).mockResolvedValue({ offer: { ...offer, canCreateInquiry: false, blockReason: '交货截止已过或不符合当前发车时间' }, inquiries: pageOf([]) })
const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
- expect(button(wrapper, '登记客户货物').attributes('disabled')).toBeDefined()
- expect(wrapper.text()).toContain('无法登记客户货物:交货截止已过')
- expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('modelValue')).toBe(false)
- state.write = false
- const readonly = mount(CapacityOfferDetailView); await flushPromises()
- expect(readonly.wrapper.findAll('button').some(node => node.text() === '登记客户货物')).toBe(false)
+ expect(button(wrapper, '登记客户货物')).toBeUndefined()
+ expect(wrapper.text()).toContain('交货截止已过')
+ expect(wrapper.text()).not.toContain('无法登记客户货物')
+ expect(wrapper.findComponent(CapacityInquiryDialog).exists()).toBe(false)
})
-it('defaults minimal managers to their TODO inquiries without loading offers, and explicitly switches to available offers', async () => {
+
+it('defaults managers to the same read-only pool and preserves explicit historical queries', async () => {
state.manage = true
const { wrapper } = mount(CapacityOffersView); await flushPromises()
- expect(api.listCapacityInquiries).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'TODO' }))
- expect(api.listCapacityOffers).not.toHaveBeenCalled()
- expect(state.links.size).toBe(0)
- await button(wrapper, '可拼车次').trigger('click'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ scope: 'AVAILABLE' }))
+ expect(api.listCapacityInquiries).not.toHaveBeenCalled()
+ expect(api.listLooseboardDemands).not.toHaveBeenCalled()
+ for (const label of ['我的登记', '待匹配散板', '待我处理', '登记客户货物']) expect(button(wrapper, label)).toBeUndefined()
const query = wrapper.findComponent({ name: 'QueryToolbar' })
query.vm.$emit('update:modelValue', { scope: 'ALL' }); await flushPromises(); query.vm.$emit('search'); await flushPromises()
expect(api.listCapacityOffers).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'ALL' }))
@@ -188,21 +198,6 @@ it('requires an explicit choice for multiple destinations', async () => {
expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
})
-// Navigation must retain the exact bound business references and independent read permissions.
-it.each([CapacityOffersView, CapacityOfferDetailView])('opens the bound trip separately from read-only dispatch detail', async component => {
- const row = { ...inquiry, dispatchNo: 'DS-BOUND', status: 'BOUND' as const }
- state.links = new Set(['platform:fleet:trip:list', 'platform:overseas-warehouse:dispatch:list'])
- const { wrapper } = mount(component); await flushPromises()
- const dialog = wrapper.findComponent(CapacityInquiryDialog)
- dialog.vm.$emit('action', 'dispatch', row); await flushPromises()
- expect(state.push).toHaveBeenLastCalledWith({ name: 'overseas-dispatch-detail', params: { dispatchNo: 'DS-BOUND' }, query: { fleetSource: 'inquiry', fleetOfferNo: row.offerNo, fleetBookingNo: row.bookingNo } })
- dialog.vm.$emit('action', 'trip', row); await flushPromises()
- expect(state.push).toHaveBeenLastCalledWith({ name: 'overseas-master-data-trip-detail', params: { tripNo: row.tripNo }, query: { fleetSource: 'inquiry', fleetOfferNo: row.offerNo, fleetBookingNo: row.bookingNo } })
- expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
- state.links.clear(); state.push.mockClear()
- dialog.vm.$emit('action', 'trip', row); dialog.vm.$emit('action', 'dispatch', row); await flushPromises()
- expect(state.push).not.toHaveBeenCalled()
-})
it('shows follow-up destinations only with the matching read permission', async () => {
const row = { ...inquiry, dispatchNo: 'DS-BOUND', status: 'BOUND' as const }
state.links.add('platform:overseas-warehouse:dispatch:list')
@@ -318,69 +313,47 @@ it('shows the business identity and exposes the order link only with its list pe
await button(wrapper, 'ORDER-1').trigger('click')
expect(wrapper.emitted('action')?.[0]).toEqual(['business-order', row])
})
-it('reopens a returned inquiry through an authorized fetch and refuses a different offer', async () => {
- state.route.query = { inquiry: inquiry.bookingNo }
- vi.mocked(api.getCapacityInquiry).mockResolvedValue(inquiry)
- const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
- const editor = wrapper.findComponent({ name: 'CapacityInquiryDialog' })
- expect(editor.props('modelValue')).toBe(true); expect(editor.props('readonly')).toBe(true)
- expect(editor.props('inquiry').bookingNo).toBe(inquiry.bookingNo)
- wrapper.unmount()
- vi.mocked(api.getCapacityInquiry).mockResolvedValue({ ...inquiry, offerNo: 'FO-OTHER' })
- const rejected = mount(CapacityOfferDetailView).wrapper; await flushPromises()
- expect(rejected.findComponent({ name: 'CapacityInquiryDialog' }).props('modelValue')).toBe(false)
- expect(rejected.findComponent({ name: 'LoadErrorState' }).exists()).toBe(true)
-})
-it('opens refreshed server guidance after fulfillment succeeds without replaying a write', async () => {
+it('ignores old inquiry deep links and renders only the offer summary', async () => {
state.manage = true
- const { wrapper } = mount(CapacityOffersView); await flushPromises()
- const updated = { ...inquiry, status: 'BOUND' as const, progress: { ...inquiry.progress, primaryAction: 'VIEW', message: '服务端下一待办', owner: '审核 / 仓库' } }
- vi.mocked(api.getCapacityInquiry).mockResolvedValue(updated)
- wrapper.findComponent({ name: 'CapacityInquiryTable' }).vm.$emit('action', 'bind', inquiry); await flushPromises()
- wrapper.findComponent({ name: 'CapacityInquiryFulfillment' }).vm.$emit('saved'); await flushPromises()
- const editor = wrapper.findComponent({ name: 'CapacityInquiryDialog' })
- expect(editor.props('modelValue')).toBe(true); expect(editor.props('readonly')).toBe(true)
- expect(editor.props('inquiry').progress.message).toBe('服务端下一待办')
- expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
-})
-it('fetches an off-page returned inquiry once per refresh and retries without falling back to stale data', async () => {
state.route.query = { inquiry: inquiry.bookingNo }
- vi.mocked(api.getCapacityOffer).mockResolvedValue({ offer, inquiries: pageOf([], 30) })
- vi.mocked(api.getCapacityInquiry).mockResolvedValue(inquiry)
const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
- const editor = wrapper.findComponent({ name: 'CapacityInquiryDialog' })
- expect(editor.props('modelValue')).toBe(true)
- vi.mocked(api.getCapacityOffer).mockClear(); vi.mocked(api.getCapacityInquiry).mockClear()
- vi.mocked(api.getCapacityInquiry).mockResolvedValue({ ...inquiry, recordVersion: 7 })
- await button(wrapper, '重新核对').trigger('click'); await flushPromises()
- expect(api.getCapacityOffer).toHaveBeenCalledOnce()
- expect(api.getCapacityInquiry).toHaveBeenCalledExactlyOnceWith(inquiry.bookingNo)
- expect(editor.props('inquiry').recordVersion).toBe(7)
- vi.mocked(api.getCapacityInquiry).mockRejectedValueOnce(new Error('读取失败'))
- await button(wrapper, '重新核对').trigger('click'); await flushPromises()
- const error = wrapper.findComponent({ name: 'LoadErrorState' })
- expect(error.exists()).toBe(true)
- vi.mocked(api.getCapacityOffer).mockClear(); vi.mocked(api.getCapacityInquiry).mockClear()
- vi.mocked(api.getCapacityInquiry).mockResolvedValue({ ...inquiry, recordVersion: 8 })
- error.vm.$emit('retry'); await flushPromises()
- expect(api.getCapacityOffer).toHaveBeenCalledOnce()
- expect(api.getCapacityInquiry).toHaveBeenCalledExactlyOnceWith(inquiry.bookingNo)
- expect(editor.props('inquiry').recordVersion).toBe(8)
- expect(api.listCapacityInquiries).not.toHaveBeenCalled()
+ expect(api.getCapacityOffer).toHaveBeenCalledExactlyOnceWith('CO-1')
+ expect(api.getCapacityInquiry).not.toHaveBeenCalled()
+ expect(wrapper.findComponent(CapacityInquiryDialog).exists()).toBe(false)
+ expect(wrapper.findComponent({ name: 'CapacityInquiryFulfillment' }).exists()).toBe(false)
+ expect(wrapper.text()).toContain(offer.warehouseName)
+ expect(wrapper.text()).toContain(offer.contactPhone)
+})
+
+it('keeps transport, destination, remaining capacity and contact columns for manual follow-up', async () => {
+ const { wrapper } = mount(CapacityOffersView); await flushPromises()
+ const columns = wrapper.findComponent({ name: 'DataTable' }).props('columns') as { key: string }[]
+ for (const key of ['warehouseName', 'destinations', 'plannedDepartureAt', 'contactName']) expect(columns.some(column => column.key === key)).toBe(true)
+ expect(api.saveCapacityInquiry).not.toHaveBeenCalled()
expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
})
-it('shows customer looseboard demand in the same pool and passes the original to the selected trip inquiry', async () => {
- const source = { orderNo: 'LB-SOURCE', customerId: '19', version: 2, origin: 'LA', destination: 'ONT8', productName: '货物', estimatedPalletCount: 3, packageCount: 12, totalWeightKg: 100, totalVolumeCbm: 2, currency: 'USD', originContactName: 'Alice', originContactPhone: '555', remark: null }
- vi.mocked(api.listLooseboardDemands).mockResolvedValue(pageOf([source]))
+it('refreshes and retries read-only offer detail without loading an old inquiry', async () => {
+ state.route.query = { inquiry: inquiry.bookingNo }
+ const { wrapper } = mount(CapacityOfferDetailView); await flushPromises()
+ vi.mocked(api.getCapacityOffer).mockClear()
+ await button(wrapper, '重新核对').trigger('click'); await flushPromises()
+ expect(api.getCapacityOffer).toHaveBeenCalledExactlyOnceWith('CO-1')
+ expect(api.getCapacityInquiry).not.toHaveBeenCalled()
+ vi.mocked(api.getCapacityOffer).mockRejectedValueOnce(new Error('读取失败'))
+ await button(wrapper, '重新核对').trigger('click'); await flushPromises()
+ const error = wrapper.findComponent({ name: 'LoadErrorState' })
+ expect(error.exists()).toBe(true)
+ error.vm.$emit('retry'); await flushPromises()
+ expect(wrapper.findComponent({ name: 'LoadErrorState' }).exists()).toBe(false)
+ expect(api.getCapacityInquiry).not.toHaveBeenCalled()
+ expect(api.commandCapacityInquiry).not.toHaveBeenCalled()
+})
+
+it('does not load customer looseboard demands or expose matching from the pool', async () => {
+ state.manage = true
const { wrapper } = mount(CapacityOffersView); await flushPromises()
- await button(wrapper, '待匹配散板').trigger('click'); await flushPromises()
- expect(api.listLooseboardDemands).toHaveBeenCalledWith({ page: 1, size: 20, keyword: undefined })
- expect(wrapper.findComponent({ name: 'DataTable' }).props('data')).toEqual([source])
- expect(api.getCapacityOffer).not.toHaveBeenCalled()
- // 用页面实际暴露的匹配动作选择来源,再沿已有登记入口选车。
- const vm = wrapper.vm as unknown as { matchDemand: (row: typeof source) => void; create: (row: CapacityOffer) => Promise }
- vm.matchDemand(source); await flushPromises(); await vm.create(offer); await flushPromises()
- expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('sourceDemand')).toEqual(source)
- expect(wrapper.findComponent({ name: 'CapacityInquiryDialog' }).props('offer').offerNo).toBe(offer.offerNo)
+ expect(api.listLooseboardDemands).not.toHaveBeenCalled()
+ expect(button(wrapper, '匹配车次')).toBeUndefined()
+ expect(wrapper.findComponent(CapacityInquiryDialog).exists()).toBe(false)
})
diff --git a/src/modules/overseas-master-data/pages/CapacityOffersView.vue b/src/modules/overseas-master-data/pages/CapacityOffersView.vue
index c3c3439f..b53cbbc9 100644
--- a/src/modules/overseas-master-data/pages/CapacityOffersView.vue
+++ b/src/modules/overseas-master-data/pages/CapacityOffersView.vue
@@ -18,7 +18,6 @@ import { listLooseboardDemands, listCapacityOffers, listCapacityInquiries, getCa
import { offerStatusLabels, inquiryStatusLabels, offerTime, loadText, type LooseboardDemand, type CapacityOffer, type CapacityInquiry, type InquiryResult, type OfferQuery, type InquiryQuery } from '../capacityOfferTypes'
import { inquirySourceQuery, inquiryOrderTarget } from '../inquiryNavigation'
import { useCapacityInquiryCommands } from '../useCapacityInquiryCommands'
-import { false } from '../capacityOfferFeatures'
const router = useRouter(), access = useAccessPreviewStore()
const manage = computed(() => access.can('platform:fleet:capacity-offer:manage'))
// 当前只展示余位需求池;登记、原单匹配及履约模板直接隐藏,原实现保留。
@@ -106,7 +105,6 @@ function accept(value: InquiryResult) {
}
const commands = useCapacityInquiryCommands('capacity-pool-command', accept)
async function inquiryAction(key: string, row: CapacityInquiry) {
- if (!false) return
followupSequence++
if (openFulfillment(key, row)) return
if (key === 'cancel') { await commands.run(row, key); return }
@@ -132,30 +130,41 @@ watch([tab, page, size], load); onMounted(load); onBeforeUnmount(() => { followu
正在匹配 {{ selectedDemand.orderNo }}:{{ selectedDemand.origin }} → {{ selectedDemand.destination }}。请选择合适车次,保存登记后尚不保位。取消选择
-
-
+
- 散板需求
待我处理选择车次发布余位
+ 散板需求
选择车次发布余位
- 登记客户货物详情
+ 详情
{{ row.offerNo }}
{{ row.tripNo }}
{{ stop.name }} · {{ row.capacityKnown ? loadText(stop.remaining) : '待调度核对' }}
发车 {{ offerTime(row.plannedDepartureAt) }}
最晚送仓 {{ offerTime(row.cutoffAt) }}
{{ row.capacityKnown ? '—' : '待调度核对' }}{{ segment.fromName }} → {{ segment.toName }}
计划 {{ loadText(segment.planned) }}
预留 {{ loadText(segment.reserved) }}
- {{ offerStatusLabels[(row as CapacityOffer).status] }} · {{ row.canConfirm ? '可登记,确认后保位' : row.canCreateInquiry ? '可先登记,待调度核对,尚不保位' : '暂不可登记' }}{{ row.blockReason }}
+ {{ offerStatusLabels[(row as CapacityOffer).status] }}{{ row.blockReason }}
{{ row.contactName || '—' }}
{{ row.contactPhone || '—' }}
-
- {{ applied.scope === 'TODO' ? '待我处理' : '全部登记' }}
+
+
+