fix(admin): 完善弹窗滚动与表单交互布局
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import CarrierScheduleQueryPanel from './CarrierScheduleQueryPanel.vue'
|
||||
@@ -8,6 +8,10 @@ import { appendContainerShippingNode, getContainerShippingNodes } from '../api/p
|
||||
import { getPortSchedule } from '../api/platformPortSchedules'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { h } from 'vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
vi.mock('../api/platformCarrierScheduleQueries', () => ({
|
||||
listCarrierScheduleQueries: vi.fn(),
|
||||
@@ -91,6 +95,49 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each([false, true])('isolates nested manual entry from its parent and reopens in ordinary mode (dense=%s)', async (dense) => {
|
||||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
const host = mount(AppDialog, {
|
||||
props: { modelValue: true, title: '船期详情', showConfirm: false },
|
||||
attrs: { class: 'parent-schedule-dialog admin-density-dialog admin-workbench is-dialog' },
|
||||
slots: { default: () => h(CarrierScheduleQueryPanel, { oceanBookingId: '2', containerBookingId: '3', dense }) },
|
||||
global: { plugins: [ElementPlus, pinia] },
|
||||
attachTo: document.body,
|
||||
})
|
||||
try {
|
||||
await flushPromises()
|
||||
const panel = host.findComponent(CarrierScheduleQueryPanel)
|
||||
const open = async () => {
|
||||
await panel.findAll('button').find(button => button.text().includes('人工补录节点'))!.trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
await open()
|
||||
const child = panel.findComponent(AppDialog)
|
||||
const dialog = document.body.querySelector<HTMLElement>('.el-dialog:not(.parent-schedule-dialog)')!
|
||||
expect(dialog).not.toBeNull()
|
||||
expect(dialog.parentElement?.closest('.parent-schedule-dialog')).toBeNull()
|
||||
expect(document.body.contains(dialog)).toBe(true)
|
||||
expect(dialog.querySelector(':scope > .el-dialog__body .app-dialog__body')).not.toBeNull()
|
||||
expect(dialog.querySelector(':scope > .el-dialog__footer')).not.toBeNull()
|
||||
expect(child.props('width')).toContain(dense ? '780px' : '620px')
|
||||
if (dense) {
|
||||
dialog.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click()
|
||||
await flushPromises()
|
||||
expect(dialog.classList.contains('is-fullscreen')).toBe(true)
|
||||
}
|
||||
Array.from(dialog.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')).find(button => button.textContent?.trim() === '取消')!.click()
|
||||
await flushPromises()
|
||||
expect(child.props('modelValue')).toBe(false)
|
||||
expect(host.props('modelValue')).toBe(true)
|
||||
await open()
|
||||
expect(child.props('modelValue')).toBe(true)
|
||||
expect(dialog.classList.contains('is-fullscreen')).toBe(false)
|
||||
expect(appendContainerShippingNode).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
host.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows manual entry without creating a tracking task', async () => {
|
||||
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 })
|
||||
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2', containerBookingId: '3' }, global: { plugins: [ElementPlus, pinia] } })
|
||||
@@ -291,14 +338,15 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
inputs.find((input) => String(input.props('maxlength')) === '128')!.vm.$emit('update:modelValue', 'Pacific Ocean')
|
||||
inputs.find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '船司官网人工核验')
|
||||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('补录未成功,已保留当前填写内容')
|
||||
expect(wrapper.text()).toContain('req-409'); expect(wrapper.text()).toContain('trace-409')
|
||||
expect(document.body.querySelector('.el-dialog')?.textContent).toContain('补录未成功,已保留当前填写内容')
|
||||
expect(document.body.querySelector('.el-dialog')?.textContent).toContain('req-409'); expect(document.body.querySelector('.el-dialog')?.textContent).toContain('trace-409')
|
||||
const dialog = wrapper.findComponent({ name: 'AppDialog' })
|
||||
expect(dialog.props('title')).toBe('人工补录船节点 · CAB-3')
|
||||
if (dense) {
|
||||
await dialog.get('[aria-label="展开全屏"]').trigger('click')
|
||||
expect(dialog.find('.el-dialog.is-fullscreen').exists()).toBe(true)
|
||||
expect(dialog.find('textarea').element.value).toBe('船司官网人工核验')
|
||||
document.body.querySelector<HTMLButtonElement>('[aria-label="展开全屏"]')!.click()
|
||||
await flushPromises()
|
||||
expect(document.body.querySelector('.el-dialog.is-fullscreen')).not.toBeNull()
|
||||
expect(document.body.querySelector<HTMLTextAreaElement>('.el-dialog textarea')!.value).toBe('船司官网人工核验')
|
||||
}
|
||||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||||
|
||||
@@ -323,7 +371,7 @@ describe('CarrierScheduleQueryPanel loading states', () => {
|
||||
wrapper.findAllComponents({ name: 'ElInput' }).find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '人工核验可提柜')
|
||||
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('预计时间和实际时间至少填写一项')
|
||||
expect(document.body.querySelector('.el-dialog')?.textContent).toContain('预计时间和实际时间至少填写一项')
|
||||
expect(appendContainerShippingNode).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -282,7 +282,7 @@ watch(manualDialogOpen, (open) => { if (!open && !manualSaving.value) { manualId
|
||||
<details v-if="supersededActualNodes.length" class="technical-details duplicate-node-history"><summary>历史重复实际节点({{ supersededActualNodes.length }})</summary><ol class="technical-history"><li v-for="item in supersededActualNodes" :key="item.id"><span>{{ nodeTimeText(item) }}</span><strong>{{ nodeText(item.standardNode) }}</strong><small>{{ nodeLocation(item) }} · {{ item.providerCode }}<template v-if="item.sourceReference"> · {{ item.sourceReference }}</template></small></li></ol></details>
|
||||
<details v-if="query" class="technical-details"><summary>查询任务摘要、异常与变更历史</summary><dl class="query-facts" :class="{ 'density-fields': dense }"><div><dt>查询任务</dt><dd>{{ query.queryNo || '—' }}</dd></div><div><dt>跟踪标识</dt><dd>{{ trackingTypeText(query.trackingType) }} · {{ query.trackingNo || '—' }}</dd></div><div><dt>任务状态</dt><dd>{{ statusText }}</dd></div><div><dt>建立来源</dt><dd>{{ triggerSourceText(query.triggerSource) }}</dd></div><div><dt>最新实际节点</dt><dd>{{ nodeText(query.latestActualNode) }}</dd></div><div><dt>实际发生时间</dt><dd>{{ formatTime(query.latestActualAt) }}</dd></div><div><dt>最新来源</dt><dd>{{ latestSource }}</dd></div><div><dt>来源参考</dt><dd class="break-value">{{ sourceReference }}</dd></div><div><dt>观测时间</dt><dd>{{ formatTime(latestObservation) }}</dd></div><div><dt>最近成功</dt><dd>{{ formatTime(query.lastSuccessAt) }}</dd></div><div><dt>结果有效性</dt><dd>{{ queryView?.expired ? '结果已过期' : '结果有效' }}</dd></div><div v-if="query.lastErrorCode"><dt>最近错误码</dt><dd>{{ query.lastErrorCode }}</dd></div></dl><h3>任务变更历史</h3><p v-if="!histories.length" class="empty-line">暂无变更历史</p><ol v-else class="technical-history"><li v-for="item in histories" :key="item.id"><span>{{ formatTime(item.createdAt) }}</span><strong>{{ historyText(item.action) }}</strong><small>{{ item.operatorName || '系统' }}<template v-if="item.reason"> · {{ item.reason }}</template></small></li></ol></details>
|
||||
</template>
|
||||
<AppDialog class="admin-workbench is-dialog" :class="{ 'admin-density-dialog': dense }" v-model="manualDialogOpen" :title="manualDialogTitle" :width="dense ? 'min(780px, calc(100vw - 28px))' : 'min(620px, calc(100vw - 28px))'" :fullscreenable="dense" confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
|
||||
<AppDialog class="admin-workbench is-dialog" :class="{ 'admin-density-dialog': dense }" v-model="manualDialogOpen" :title="manualDialogTitle" :width="dense ? 'min(780px, calc(100vw - 28px))' : 'min(620px, calc(100vw - 28px))'" :fullscreenable="dense" append-to-body confirm-text="确认补录" :confirm-loading="manualSaving" @confirm="submitManualNode">
|
||||
<el-alert v-if="manualError" type="error" :closable="false" show-icon title="补录未成功,已保留当前填写内容"><template #default><ApiErrorMeta :details="manualError" /></template></el-alert>
|
||||
<el-alert v-if="manualValidationError" type="warning" :closable="false" show-icon :title="manualValidationError" />
|
||||
<el-alert type="info" :closable="false" title="观测时间按提交时刻记录;预计事实不推进实际进度,既有事实不可编辑或删除。" />
|
||||
|
||||
Reference in New Issue
Block a user