fix(admin): 完善弹窗滚动与表单交互布局

This commit is contained in:
2026-10-21 07:40:08 +08:00
parent 2f9a3330e5
commit 0a1968a134
41 changed files with 614 additions and 131 deletions
@@ -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="观测时间按提交时刻记录;预计事实不推进实际进度,既有事实不可编辑或删除。" />