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,14 +1,15 @@
import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus'
import { AxiosError } from 'axios'
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import SmartLoadingDialog from '../components/SmartLoadingDialog.vue'
import type { ContainerBookingResource } from '../types'
import {
assignPendingReviewSmartLoadingOrder,
@@ -27,6 +28,8 @@ import {
removeSmartLoadingPallet,
} from '../api/platformContainerLoading'
enableAutoUnmount(afterEach)
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
vi.mock('vue-router', () => ({
@@ -120,7 +123,50 @@ function mockContainerSearch(records: ContainerBookingResource[]) {
))))
}
function assignmentDialog() {
const root = document.body.querySelector<HTMLElement>('.smart-loading-assignment-dialog')
if (!root) throw new Error('assignment dialog is missing')
return new DOMWrapper(root)
}
describe('GlobalSmartLoadingView', () => {
it('mounts assignment outside the parent workbench and keeps fullscreen and reopen states independent', async () => {
const wrapper = mount(SmartLoadingDialog, { attachTo: document.body, props: { modelValue: true }, global: { plugins: [ElementPlus] } })
try {
await flushPromises()
const workbench = wrapper.getComponent(GlobalSmartLoadingView)
const openAssignment = async () => {
await workbench.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click')
await flushPromises()
}
await openAssignment()
const popup = workbench.getComponent(AppDialog)
const outer = document.querySelector('.smart-loading-dialog')!
const child = assignmentDialog()
expect(document.body.contains(child.element)).toBe(true)
expect(outer.contains(child.element)).toBe(false)
expect(child.element.closest('.global-loading-page')).toBeNull()
expect(child.classes()).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog']))
popup.getComponent(ElInputNumber).vm.$emit('update:modelValue', 4)
await child.get('[aria-label="展开全屏"]').trigger('click')
expect(child.classes()).toContain('is-fullscreen')
expect(outer.classList.contains('is-fullscreen')).toBe(true)
await child.findAll('button').find(button => button.text() === '取消')!.trigger('click')
await flushPromises()
expect(popup.props('modelValue')).toBe(false)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(workbench.get('.container-option.selected').attributes('aria-pressed')).toBe('true')
await openAssignment()
expect(popup.props('modelValue')).toBe(true)
expect(assignmentDialog().classes()).not.toContain('is-fullscreen')
expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(candidate.availablePackageCount)
expect(outer.classList.contains('is-fullscreen')).toBe(true)
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
} finally {
wrapper.unmount()
}
})
it.each(['PACKAGE', 'SEALED_PALLET'] as const)('分配窗口全屏保留来源及输入,按%s原接口提交', async (unitType) => {
if (unitType === 'SEALED_PALLET') {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
@@ -138,7 +184,7 @@ describe('GlobalSmartLoadingView', () => {
await wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click')
await flushPromises()
const popup = wrapper.getComponent(AppDialog)
expect(popup.get('table[aria-label="全局装柜分配来源"]').text()).toContain('CB-1')
expect(assignmentDialog().get('table[aria-label="全局装柜分配来源"]').text()).toContain('CB-1')
if (unitType === 'PACKAGE') {
const input = popup.getComponent(ElInputNumber)
expect(input.props('modelValue')).toBe(candidate.availablePackageCount)
@@ -146,11 +192,11 @@ describe('GlobalSmartLoadingView', () => {
input.vm.$emit('update:modelValue', 4)
} else {
expect(popup.findComponent(ElInputNumber).exists()).toBe(false)
expect(popup.get('details').text()).toContain('I181:9 箱')
expect(assignmentDialog().get('details').text()).toContain('I181:9 箱')
expect(popup.props('title')).toBe('整托加入当前柜次 · DP-TEST-1')
}
await popup.get('[aria-label="展开全屏"]').trigger('click')
expect(popup.find('.el-dialog.is-fullscreen').exists()).toBe(true)
await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click')
expect(assignmentDialog().classes()).toContain('is-fullscreen')
if (unitType === 'PACKAGE') expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(4)
popup.vm.$emit('confirm'); await flushPromises()
if (unitType === 'PACKAGE') {
@@ -572,11 +618,11 @@ describe('GlobalSmartLoadingView', () => {
expect(document.body.textContent).toContain('不能部分输入、单独分配或单独移除')
const popup = wrapper.getComponent(AppDialog)
expect(popup.props('title')).toBe('整组加入当前柜次 · CLG-2093539779795795969')
const contextRows = popup.findAll('table[aria-label="全局装柜分配来源"] tr')
const contextRows = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr')
expect(contextRows[0]!.findAll('td').map(cell => cell.text())).toEqual(['CB-1', 'MSCU-1'])
expect(contextRows[1]!.findAll('td').map(cell => cell.text())).toEqual(['义乌仓', '美森正班'])
await popup.get('[aria-label="展开全屏"]').trigger('click')
expect(popup.get('table[aria-label="全局装柜同柜组成员"]').text()).toContain('NYEXX2608290051-B')
await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click')
expect(assignmentDialog().get('table[aria-label="全局装柜同柜组成员"]').text()).toContain('NYEXX2608290051-B')
expect(document.body.textContent).not.toContain('本次分配箱数')
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click()
await flushPromises()
@@ -612,9 +658,9 @@ describe('GlobalSmartLoadingView', () => {
await flushPromises()
const popup = wrapper.getComponent(AppDialog)
expect(popup.props('title')).toBe('加入当前柜次 · EXXHK2609020361')
const measurements = popup.findAll('table[aria-label="全局装柜分配填写"] tr').find(row => row.text().includes('平均单箱毛重'))!
const measurements = assignmentDialog().findAll('table[aria-label="全局装柜分配填写"] tr').find(row => row.text().includes('平均单箱毛重'))!
expect(measurements.findAll('td').map(cell => cell.text())).toEqual(['待测量', '待测量'])
const channel = popup.findAll('table[aria-label="全局装柜分配来源"] tr').find(row => row.text().includes('订单渠道'))!
const channel = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr').find(row => row.text().includes('订单渠道'))!
expect(channel.findAll('td')[0]!.text()).toBe('合德海卡')
expect(document.body.textContent).toContain('这是人工装柜分配,确认后仍会加入')
expect(document.body.textContent).toContain('智能推荐不会跨渠道')
@@ -907,10 +953,10 @@ describe('GlobalSmartLoadingView', () => {
await flushPromises()
// 箱数与上方资料使用同一标签列,保留输入名称与原范围。
const label = popup.get('label[for="global-loading-assignment-count"]')
const label = assignmentDialog().get('label[for="global-loading-assignment-count"]')
expect(label.element.parentElement?.tagName).toBe('TH')
expect(label.element.parentElement?.nextElementSibling?.getAttribute('colspan')).toBe('3')
expect(popup.find('input#global-loading-assignment-count').exists()).toBe(true)
expect(assignmentDialog().find('input#global-loading-assignment-count').exists()).toBe(true)
expect(input.props('max')).toBe(candidate.availablePackageCount)
popup.vm.$emit('confirm')
await flushPromises()
@@ -918,11 +964,12 @@ describe('GlobalSmartLoadingView', () => {
expect(assignSmartLoadingItem).toHaveBeenCalledWith(plan.id, { inboundId: candidate.inboundId, packageCount: 7, expectedVersion: plan.version })
expect(popup.props('modelValue')).toBe(true)
expect(input.props('modelValue')).toBe(7)
expect(popup.text()).toContain('加入当前柜次失败')
expect(popup.text()).toContain('当前账号无权访问该数据')
expect(popup.text()).toContain('assign-request')
expect(popup.text()).toContain('assign-trace')
expect(wrapper.get('.global-loading-page').findAll('.load-error-state')).toHaveLength(1)
expect(assignmentDialog().text()).toContain('加入当前柜次失败')
expect(assignmentDialog().text()).toContain('当前账号无权访问该数据')
expect(assignmentDialog().text()).toContain('assign-request')
expect(assignmentDialog().text()).toContain('assign-trace')
expect(assignmentDialog().findAll('.load-error-state')).toHaveLength(1)
expect(wrapper.get('.global-loading-page').findAll('.load-error-state')).toHaveLength(0)
wrapper.unmount()
})
@@ -839,7 +839,7 @@ onMounted(() => { void load(); void loadCustomers() })
</section>
</section>
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="assignOpen" :title="assignmentTitle" width="min(920px, calc(100vw - 28px))" fullscreenable :confirm-loading="mutating" :confirm-disabled="Boolean(selectedCandidate?.blockReason || selectedCandidate?.locked)" :confirm-text="selectedCandidate?.unitType === 'SEALED_PALLET' ? '确认整托分配' : selectedCandidate?.coLoadGroupId ? '确认整组分配' : '确认加入'" @confirm="assign">
<AppDialog class="smart-loading-assignment-dialog admin-density-dialog admin-workbench is-dialog" v-model="assignOpen" :title="assignmentTitle" width="min(920px, calc(100vw - 28px))" append-to-body fullscreenable :confirm-loading="mutating" :confirm-disabled="Boolean(selectedCandidate?.blockReason || selectedCandidate?.locked)" :confirm-text="selectedCandidate?.unitType === 'SEALED_PALLET' ? '确认整托分配' : selectedCandidate?.coLoadGroupId ? '确认整组分配' : '确认加入'" @confirm="assign">
<LoadErrorState v-if="mutationError" title="加入当前柜次失败" :message="mutationError.message" compact :show-retry="false">
<template #details><ApiErrorMeta :details="mutationError" /></template>
</LoadErrorState>
@@ -3,13 +3,15 @@ import { AxiosError } from 'axios'
import AppDialog from '@/shared/components/AppDialog.vue'
import { createPinia, setActivePinia } from 'pinia'
import { createMemoryHistory, createRouter } from 'vue-router'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import SmartLoadingWorkbenchView from './SmartLoadingWorkbenchView.vue'
import { assignSmartLoadingGroup, assignSmartLoadingItem, assignSmartLoadingPallet, getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading, removeSmartLoadingGroup } from '../api/platformContainerLoading'
enableAutoUnmount(afterEach)
vi.mock('../api/platformContainerLoading', () => ({
getContainer: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(),
createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingPallet: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(),
@@ -233,6 +235,11 @@ describe('SmartLoadingWorkbenchView', () => {
const dialog = wrapper.getComponent(AppDialog)
expect(dialog.props('title')).toBe('按入仓码分配箱数 · IN-1')
const count = dialog.getComponent({ name: 'ElInputNumber' })
const label = dialog.get('label[for="cabinet-loading-assignment-count"]')
expect(label.element.parentElement?.tagName).toBe('TH')
expect(label.element.parentElement?.nextElementSibling?.getAttribute('colspan')).toBe('3')
expect(dialog.get('input#cabinet-loading-assignment-count').attributes('aria-label')).toBe('本次计划装柜箱数')
expect(count.attributes('aria-required')).toBe('true')
expect(count.props('max')).toBe(12)
count.vm.$emit('update:modelValue', 5)
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.trim() === '确认分配') as HTMLButtonElement).click()
@@ -427,7 +427,8 @@ onMounted(load)
<tr v-else-if="selectedCandidate.unitType === 'SEALED_PALLET'"><th>整托箱数</th><td colspan="3">整托 {{ selectedCandidate.availablePackageCount }} 箱</td></tr>
<template v-else>
<tr><th>货物摘要</th><td colspan="3"><LineClampText :content="selectedCandidate.goodsSummary || '货物摘要暂缺'" /></td></tr>
<tr><th>可用箱数</th><td>{{ selectedCandidate.availablePackageCount }} 箱</td><td colspan="2"><el-form-item label="本次计划装柜箱数" label-position="left" label-width="140px" required><el-input-number v-model="assignForm.packageCount" :min="1" :max="selectedCandidate.availablePackageCount" :step="1" controls-position="right" placeholder="请输入本次分配箱数" /></el-form-item></td></tr>
<tr><th>可用箱数</th><td colspan="3">{{ selectedCandidate.availablePackageCount }} 箱</td></tr>
<tr><th><label for="cabinet-loading-assignment-count"><span class="assignment-required" aria-hidden="true">*</span>本次计划装柜箱数</label></th><td colspan="3"><el-form-item><el-input-number id="cabinet-loading-assignment-count" v-model="assignForm.packageCount" aria-label="本次计划装柜箱数" aria-required="true" :min="1" :max="selectedCandidate.availablePackageCount" :step="1" controls-position="right" placeholder="请输入本次分配箱数" /></el-form-item></td></tr>
</template>
</tbody></table></div>
</el-form>
@@ -451,6 +452,9 @@ onMounted(load)
</template>
<style scoped>
.assignment-table { table-layout: fixed; }
.assignment-table th { width: 128px; }
.assignment-required { margin-right: var(--admin-space-1); color: var(--el-color-danger); }
.workbench { display: grid; gap: var(--admin-space-5); min-width: 0; }
.workbench-command-bar,.header-actions,.row-actions,.meter-title { display: flex; align-items: center; }
.workbench-command-bar { justify-content: space-between; gap: var(--admin-space-5); }