fix(admin): consume booking link actions when cached pages reactivate
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import { createPinia, setActivePinia } from 'pinia'
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
@@ -34,7 +35,7 @@ const drayage = {
|
|||||||
|
|
||||||
function createTestRouter() {
|
function createTestRouter() {
|
||||||
return createRouter({ history: createMemoryHistory(), routes: [
|
return createRouter({ history: createMemoryHistory(), routes: [
|
||||||
{ path: '/', component: ContainersView },
|
{ path: '/', name: 'domestic-container-workbench', component: ContainersView },
|
||||||
{ path: '/container-loading/domestic/:containerId/smart-loading', name: 'smart-loading-workbench', component: { template: '<div />' } },
|
{ path: '/container-loading/domestic/:containerId/smart-loading', name: 'smart-loading-workbench', component: { template: '<div />' } },
|
||||||
{ path: '/loading-plans', name: 'loading-plans', component: { template: '<div />' } },
|
{ path: '/loading-plans', name: 'loading-plans', component: { template: '<div />' } },
|
||||||
{ path: '/container-loading/domestic/:containerId', name: 'domestic-container-detail', component: { template: '<div />' } },
|
{ path: '/container-loading/domestic/:containerId', name: 'domestic-container-detail', component: { template: '<div />' } },
|
||||||
@@ -417,12 +418,57 @@ describe('ContainersView', () => {
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the existing released-booking workflow when entered from container detail', () => {
|
it('consumes the booking-link action once when returning to a cached list and preserves other query state', async () => {
|
||||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/ContainersView.vue'), 'utf8')
|
vi.mocked(getContainer).mockClear()
|
||||||
expect(source).toContain('route.query.linkContainerId')
|
vi.mocked(listContainers).mockClear()
|
||||||
expect(source).toContain("const target = await getContainer(linkContainerId, 'DOMESTIC')")
|
vi.mocked(listOceanBookingLinkCandidates).mockClear().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||||
expect(source).toContain('await openLink(target)')
|
const router = createTestRouter()
|
||||||
expect(source).toContain("void router.replace({ name: 'domestic-container-workbench' })")
|
await router.push('/'); await router.isReady()
|
||||||
|
const shell = defineComponent({ template: '<router-view v-slot="{ Component }"><keep-alive><component :is="Component" /></keep-alive></router-view>' })
|
||||||
|
const wrapper = mount(shell, { global: { plugins: [ElementPlus, router] } })
|
||||||
|
await flushPromises()
|
||||||
|
const initial = wrapper.findComponent(ContainersView).vm.$.uid
|
||||||
|
await router.push('/container-loading/domestic/9001')
|
||||||
|
await flushPromises()
|
||||||
|
await router.push({ name: 'domestic-container-workbench', query: { linkContainerId: '9001', keyword: 'CB-1' }, hash: '#table' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.findComponent(ContainersView).vm.$.uid).toBe(initial)
|
||||||
|
expect(getContainer).toHaveBeenCalledTimes(1)
|
||||||
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledTimes(1)
|
||||||
|
expect(listContainers).toHaveBeenCalledTimes(1)
|
||||||
|
expect(router.currentRoute.value.query).toEqual({ keyword: 'CB-1' })
|
||||||
|
expect(router.currentRoute.value.hash).toBe('#table')
|
||||||
|
const dialog = wrapper.findAllComponents(AppDialog).find((item) => item.props('title') === '关联已放舱订舱')!
|
||||||
|
expect(dialog.props('modelValue')).toBe(true)
|
||||||
|
dialog.vm.$emit('update:modelValue', false)
|
||||||
|
await flushPromises()
|
||||||
|
await router.push('/container-loading/domestic/9001')
|
||||||
|
await router.push({ name: 'domestic-container-workbench' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(dialog.props('modelValue')).toBe(false)
|
||||||
|
expect(getContainer).toHaveBeenCalledTimes(1)
|
||||||
|
expect(listOceanBookingLinkCandidates).toHaveBeenCalledTimes(1)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores a stale linking response after leaving the list', async () => {
|
||||||
|
const target = await vi.mocked(getContainer).getMockImplementation()!('9001', 'DOMESTIC')
|
||||||
|
let finish!: (value: typeof target) => void
|
||||||
|
vi.mocked(getContainer).mockClear().mockImplementationOnce(() => new Promise((resolve) => { finish = resolve }))
|
||||||
|
vi.mocked(listOceanBookingLinkCandidates).mockClear()
|
||||||
|
const router = createTestRouter()
|
||||||
|
await router.push({ name: 'domestic-container-workbench', query: { linkContainerId: '9001' } }); await router.isReady()
|
||||||
|
const wrapper = mount(ContainersView, { global: { plugins: [ElementPlus, router] } })
|
||||||
|
await flushPromises()
|
||||||
|
await router.replace({ query: { linkContainerId: '9001', keyword: 'updated' } })
|
||||||
|
await flushPromises()
|
||||||
|
expect(getContainer).toHaveBeenCalledTimes(1)
|
||||||
|
await router.push('/container-loading/domestic/9001')
|
||||||
|
finish(target)
|
||||||
|
await flushPromises()
|
||||||
|
expect(listOceanBookingLinkCandidates).not.toHaveBeenCalled()
|
||||||
|
expect(router.currentRoute.value.name).toBe('domestic-container-detail')
|
||||||
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('normalizes an optional booking-link container number', async () => {
|
it('normalizes an optional booking-link container number', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Boxes, Check, Eye, FileText, FileUp, Link2, ListChecks, MoreHorizontal, Pencil, Plus, Sparkles } from '@lucide/vue'
|
import { Boxes, Check, Eye, FileText, FileUp, Link2, ListChecks, MoreHorizontal, Pencil, Plus, Sparkles } from '@lucide/vue'
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||||
@@ -686,22 +686,31 @@ watch(linkOpen, (open) => {
|
|||||||
linkSequence += 1
|
linkSequence += 1
|
||||||
linkLoading.value = false
|
linkLoading.value = false
|
||||||
})
|
})
|
||||||
onMounted(async () => {
|
let routeLinkSequence = 0
|
||||||
await Promise.all([loadCustomers(), loadRows()])
|
watch([() => route.name, () => route.query.linkContainerId, isDomestic, canEdit], async ([name, requestedId, domestic, editable]) => {
|
||||||
const linkContainerId = typeof route.query.linkContainerId === 'string' ? route.query.linkContainerId : ''
|
const sequence = ++routeLinkSequence
|
||||||
if (!isDomestic.value || !linkContainerId || !canEdit.value) return
|
const linkContainerId = typeof requestedId === 'string' ? requestedId : ''
|
||||||
|
if (name !== 'domestic-container-workbench' || !domestic || !linkContainerId || !editable) return
|
||||||
try {
|
try {
|
||||||
const target = await getContainer(linkContainerId, 'DOMESTIC')
|
const target = await getContainer(linkContainerId, 'DOMESTIC')
|
||||||
|
if (sequence !== routeLinkSequence) return
|
||||||
if (target.sourceType !== 'SIMULATION' || target.oceanBookingId || !hasAction(target, 'EDIT_CONTAINER')) {
|
if (target.sourceType !== 'SIMULATION' || target.oceanBookingId || !hasAction(target, 'EDIT_CONTAINER')) {
|
||||||
ElMessage.warning('当前货柜已关联订舱或所处阶段不允许关联')
|
ElMessage.warning('当前货柜已关联订舱或所处阶段不允许关联')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await openLink(target)
|
await openLink(target)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(getApiErrorDetails(error, '关联货柜加载失败').message)
|
if (sequence === routeLinkSequence) ElMessage.error(getApiErrorDetails(error, '关联货柜加载失败').message)
|
||||||
} finally {
|
} finally {
|
||||||
void router.replace({ name: 'domestic-container-workbench' })
|
if (sequence === routeLinkSequence && route.query.linkContainerId === linkContainerId) {
|
||||||
|
const { linkContainerId: consumed, ...query } = route.query
|
||||||
|
void router.replace({ path: route.path, query, hash: route.hash })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
onBeforeUnmount(() => { routeLinkSequence += 1 })
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadCustomers(), loadRows()])
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user