style(admin): unify dialog layouts and dense readonly presentation
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { mount, flushPromises, enableAutoUnmount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { ApiResponseError, http } from '@/shared/api/http'
|
||||
import View from './PreloadPlanDetailView.vue'
|
||||
import { confirmPreloadPlan, getPreloadPlan, listDomesticPdaOperators, updateLoadingItemRemarks, updatePreloadTransport, type LoadingDetail, type PreloadSummary } from '../api/platformLoadingLists'
|
||||
|
||||
// Use the repository's supported CJS entry for the real async form validator.
|
||||
const elementPlus = createRequire(import.meta.url)('element-plus') as typeof import('element-plus')
|
||||
const ElementPlus = elementPlus.default
|
||||
const { ElInput, ElSelect } = elementPlus
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ params: { planId: '1900000000000000001' }, query: { warehouseId: '1900000000000000002' } }),
|
||||
useRouter: () => ({ replace: vi.fn(), push: vi.fn(), back: vi.fn() }),
|
||||
}))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('@/core/workspace-tabs/workspaceTabs', () => ({ useWorkspaceTabsStore: () => ({ removeByIds: vi.fn() }) }))
|
||||
vi.mock('../api/platformLoadingLists', () => ({
|
||||
confirmPreloadPlan: vi.fn(), getConfirmedLoadingList: vi.fn(), getPreloadPlan: vi.fn(),
|
||||
listDomesticPdaOperators: vi.fn(), updateLoadingItemRemarks: vi.fn(), updatePreloadTransport: vi.fn(),
|
||||
}))
|
||||
|
||||
function fixture(containerNo: string | null = 'MSCU-OLD', version = 7, itemVersion = 3): LoadingDetail<PreloadSummary> {
|
||||
return {
|
||||
summary: {
|
||||
planId: '1900000000000000001', containerBookingId: '2900000000000000001', preloadNo: 'PL-TEST-1', cabinetNo: 'CAB-TEST-1', containerNo,
|
||||
customers: [{ accountId: '11', customerName: '客户甲' }], warehouseId: '1900000000000000002', warehouseName: '国内仓', vesselName: 'SHIP', voyageNo: 'V1', containerType: '40HQ',
|
||||
status: 'EDITING', skuCount: '1', totalPackageCount: '10', totalVolumeCbm: 1, totalGrossWeightKg: 100, createdAt: '2026-10-19T10:00:00', version,
|
||||
readiness: { containerNoReady: Boolean(containerNo), preloadLinkCount: 1, confirmedPreloadCount: 0, oceanBookingId: '31', oceanBookingStatus: 'RELEASED', pdaTaskId: null, pdaTaskStatus: null, confirmationStatus: 'PENDING', canConfirm: Boolean(containerNo), readyForPda: false, blockingReasons: [] },
|
||||
},
|
||||
items: [{
|
||||
itemId: '41', itemVersion, accountId: '11', customerName: '客户甲', customsTaxMode: 'TAX_REFUND', taxRefundFlag: true,
|
||||
orderId: '51', orderNo: 'ORD-TEST-1', inboundId: '61', inboundDetailId: '62', inboundCode: 'IN-TEST-1', containerNo,
|
||||
skuCode: null, destinationWarehouse: 'LAX9', chineseName: '测试货物', englishName: 'Goods', goodsImageUrl: null,
|
||||
unitType: 'PACKAGE', palletId: null, palletNo: null, indivisible: false, packageCount: 10, palletCount: 0,
|
||||
grossWeightKg: 100, netWeightKg: 90, volumeCbm: 1, dimensionalWeightKg: 167, productQuantity: '10', unitsPerPackage: 1,
|
||||
fbaTrackingNo: null, shipmentNo: null, shippingMark: null, internalRemark: '原内部备注', customerRemark: '客户内容\n第二行', warehouseRemark: '原仓库备注', customerServiceRemark: '原客服备注', loadSequence: 1, frozenAt: null,
|
||||
}], histories: [],
|
||||
}
|
||||
}
|
||||
|
||||
async function render(initial = fixture()) {
|
||||
vi.mocked(getPreloadPlan).mockResolvedValue(initial)
|
||||
const wrapper = mount(View, { global: { plugins: [ElementPlus], stubs: {
|
||||
AdminPage: { template: '<main><slot /></main>' }, OperationErrorToast: true,
|
||||
LoadingListDetailDrawer: { name: 'LoadingListDetailDrawer', template: '<section><slot name="pda-operator" /></section>' },
|
||||
} } })
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
function dialog(wrapper: Awaited<ReturnType<typeof render>>, prefix: string) {
|
||||
return wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith(prefix))!
|
||||
}
|
||||
enableAutoUnmount(afterEach)
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', data: { planId: '1900000000000000001', detailResource: 'PRELOAD', planStatus: 'EDITING' } })
|
||||
vi.mocked(listDomesticPdaOperators).mockResolvedValue([{ userId: '71', displayName: '操作员甲' }])
|
||||
})
|
||||
afterEach(() => { vi.restoreAllMocks(); document.body.innerHTML = '' })
|
||||
|
||||
describe('预装清单统一弹窗', () => {
|
||||
it.each([null, 'MSCU-OLD'])('运输表保留规范化和纠错原因校验,原柜号=%s', async (original) => {
|
||||
const wrapper = await render(fixture(original))
|
||||
vi.mocked(updatePreloadTransport).mockResolvedValue(fixture('MSCU-NEW', 8))
|
||||
wrapper.getComponent({ name: 'LoadingListDetailDrawer' }).vm.$emit('edit')
|
||||
await flushPromises()
|
||||
const popup = dialog(wrapper, original ? '纠正' : '补录')
|
||||
expect(popup.props('title')).toContain('PL-TEST-1')
|
||||
const number = popup.findAllComponents(ElInput).find(input => input.props('placeholder') === '请输入提柜后取得的真实集装箱号')!
|
||||
number.vm.$emit('update:modelValue', ' mscu-new ')
|
||||
number.vm.$emit('blur', new FocusEvent('blur'))
|
||||
popup.findAllComponents(ElInput).find(input => input.props('placeholder') === '请输入航次')!.vm.$emit('update:modelValue', ' V2 ')
|
||||
if (original) {
|
||||
popup.vm.$emit('confirm'); await flushPromises()
|
||||
expect(updatePreloadTransport).not.toHaveBeenCalled()
|
||||
popup.findAllComponents(ElInput).find(input => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', ' 更正柜号 ')
|
||||
}
|
||||
popup.vm.$emit('confirm'); await flushPromises()
|
||||
expect(updatePreloadTransport).toHaveBeenCalledWith('1900000000000000001', '1900000000000000002', { containerNo: 'MSCU-NEW', voyageNo: 'V2', operationReason: original ? '更正柜号' : null, expectedVersion: 7 })
|
||||
expect(popup.props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('客户备注只读,全屏及版本冲突后保留输入并按最新两层版本重试', async () => {
|
||||
const initial = fixture()
|
||||
const wrapper = await render(initial)
|
||||
wrapper.getComponent({ name: 'LoadingListDetailDrawer' }).vm.$emit('edit-remarks', initial.items[0])
|
||||
await flushPromises()
|
||||
const popup = dialog(wrapper, '编辑备注')
|
||||
expect(popup.props('title')).toBe('编辑备注 · IN-TEST-1')
|
||||
const customer = popup.findAllComponents({ name: 'ElFormItem' }).find(field => field.props('label') === '客户备注(只读)')!
|
||||
expect(customer.text()).toContain('第二行')
|
||||
expect(customer.findComponent(ElInput).exists()).toBe(false)
|
||||
const fields = popup.findAllComponents(ElInput)
|
||||
fields[0]!.vm.$emit('update:modelValue', ' 新内部 ')
|
||||
fields[1]!.vm.$emit('update:modelValue', '')
|
||||
fields[2]!.vm.$emit('update:modelValue', '新客服')
|
||||
await popup.get('[aria-label="展开全屏"]').trigger('click')
|
||||
expect(fields[0]!.props('modelValue')).toBe(' 新内部 ')
|
||||
vi.mocked(getPreloadPlan).mockResolvedValue(fixture('MSCU-OLD', 8, 4))
|
||||
vi.mocked(updateLoadingItemRemarks).mockRejectedValueOnce(new ApiResponseError('版本变化', 'A0409')).mockResolvedValueOnce({ id: initial.summary.planId, version: 9, items: [{ id: '41', version: 5, internalRemark: '新内部', warehouseRemark: null, customerServiceRemark: '新客服' }] })
|
||||
popup.vm.$emit('confirm'); await flushPromises()
|
||||
expect(popup.props('modelValue')).toBe(true)
|
||||
expect(fields[0]!.props('modelValue')).toBe(' 新内部 ')
|
||||
popup.vm.$emit('confirm'); await flushPromises()
|
||||
const calls = vi.mocked(updateLoadingItemRemarks).mock.calls
|
||||
expect(calls[0]![2]).toMatchObject({ expectedPlanVersion: 7, expectedItemVersion: 3 })
|
||||
expect(calls[1]).toEqual([initial.summary.planId, '41', { internalRemark: '新内部', warehouseRemark: null, customerServiceRemark: '新客服', expectedPlanVersion: 8, expectedItemVersion: 4, operationId: calls[0]![2].operationId }])
|
||||
expect(popup.props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('确认资料显示已选作业员,保留必选门禁和原推送操作号', async () => {
|
||||
const initial = fixture()
|
||||
const wrapper = await render(initial)
|
||||
const drawer = wrapper.getComponent({ name: 'LoadingListDetailDrawer' })
|
||||
drawer.vm.$emit('confirm-preload'); await flushPromises()
|
||||
expect(dialog(wrapper, '确认预装').props('modelValue')).toBe(false)
|
||||
wrapper.getComponent(ElSelect).vm.$emit('update:modelValue', '71')
|
||||
await flushPromises()
|
||||
drawer.vm.$emit('confirm-preload'); await flushPromises()
|
||||
const popup = dialog(wrapper, '确认预装')
|
||||
expect(popup.get('table[aria-label="确认预装资料"]').text()).toContain('操作员甲')
|
||||
vi.mocked(confirmPreloadPlan).mockResolvedValue({ planId: initial.summary.planId, containerBookingId: initial.summary.containerBookingId, confirmationStatus: 'CONFIRMED', version: 8, readiness: { ...initial.summary.readiness, confirmationStatus: 'CONFIRMED', confirmedPreloadCount: 1, pdaTaskId: '91', pdaTaskStatus: 'PENDING', canConfirm: false, readyForPda: true } })
|
||||
popup.vm.$emit('confirm'); await flushPromises()
|
||||
expect(confirmPreloadPlan).toHaveBeenCalledWith(initial.summary.planId, 7, '71', expect.stringContaining('admin-preload-confirm-'))
|
||||
expect(popup.props('modelValue')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -119,7 +119,7 @@ describe('PreloadPlanDetailView prototype contract', () => {
|
||||
expect(source).toContain('<OperationErrorToast :details="remarkError || editError || confirmError" />')
|
||||
expect(source).toContain(':message="confirmError.message"')
|
||||
expect(source).toContain('width="min(520px, calc(100vw - 28px))"')
|
||||
expect(source).toContain("label=\"柜次号\"")
|
||||
expect(source).toContain("label=\"集装箱号\"")
|
||||
const confirmation = source.match(/<table[^>]*aria-label="确认预装资料"[\s\S]*?<\/table>/)?.[0] || ''
|
||||
for (const label of ['预装单号', '柜次号', '集装箱号', 'PDA 操作员']) expect(confirmation).toContain(`<th>${label}</th>`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,6 +64,10 @@ const canCorrectTransport = computed(() => {
|
||||
})
|
||||
const isContainerCorrection = computed(() => Boolean(detail.value?.summary.containerNo))
|
||||
const editDialogTitle = computed(() => isContainerCorrection.value ? '纠正集装箱号 / 航次' : '补录集装箱号 / 航次')
|
||||
function preloadDialogTitle(action: string, code?: string | null) {
|
||||
const businessNo = code?.trim() || detail.value?.summary.preloadNo?.trim() || detail.value?.summary.cabinetNo?.trim()
|
||||
return businessNo ? `${action} · ${businessNo}` : action
|
||||
}
|
||||
const editRules: FormRules = {
|
||||
containerNo: [
|
||||
{ required: true, message: '请输入集装箱号', trigger: 'blur' },
|
||||
@@ -291,34 +295,42 @@ watch([planId, warehouseId], () => {
|
||||
</LoadingListDetailDrawer>
|
||||
</div>
|
||||
|
||||
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
||||
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
|
||||
<el-form-item label="真实集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" placeholder="请输入提柜后取得的真实集装箱号" @blur="editForm.containerNo = normalizedContainerNo(editForm.containerNo)" /></el-form-item>
|
||||
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item>
|
||||
<el-form-item :label="isContainerCorrection ? '纠错原因' : '操作原因(可选)'" prop="operationReason"><el-input v-model="editForm.operationReason" type="textarea" :rows="3" maxlength="500" :placeholder="isContainerCorrection ? '必填,将记录在操作历史中' : '可填写补录说明'" /></el-form-item>
|
||||
</el-form>
|
||||
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="preloadDialogTitle(editDialogTitle)" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
||||
<section class="density-dialog-section" data-tone="work">
|
||||
<el-form ref="editFormRef" class="density-form" :model="editForm" :rules="editRules">
|
||||
<table class="density-table density-table--editable preload-editor-table" aria-label="运输信息填写"><tbody>
|
||||
<tr><td><el-form-item label-position="left" label-width="120px" label="真实集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" placeholder="请输入提柜后取得的真实集装箱号" @blur="editForm.containerNo = normalizedContainerNo(editForm.containerNo)" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label-position="left" label-width="120px" label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label-position="left" label-width="120px" :label="isContainerCorrection ? '纠错原因' : '操作原因(可选)'" prop="operationReason"><el-input v-model="editForm.operationReason" type="textarea" :rows="3" maxlength="500" :placeholder="isContainerCorrection ? '必填,将记录在操作历史中' : '可填写补录说明'" /></el-form-item></td></tr>
|
||||
</tbody></table>
|
||||
</el-form>
|
||||
</section>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="remarkOpen" class="remarks-dialog admin-density-dialog" title="编辑备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveRemarks">
|
||||
<AppDialog v-model="remarkOpen" class="remarks-dialog admin-density-dialog" :title="preloadDialogTitle('编辑备注', remarkTarget?.inboundCode || remarkTarget?.orderNo)" width="min(800px, calc(100vw - 28px))" fullscreenable confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveRemarks">
|
||||
<el-alert type="info" :closable="false" show-icon title="客户备注来自客户下单且不可修改;其余备注可维护" />
|
||||
<el-form label-position="top" class="remarks-form">
|
||||
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
|
||||
<el-form-item label="内部备注"><el-input v-model="remarkForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item>
|
||||
<el-form-item label="仓库备注"><el-input v-model="remarkForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item>
|
||||
<el-form-item label="客服备注"><el-input v-model="remarkForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></el-form-item>
|
||||
</el-form>
|
||||
<section class="density-dialog-section" data-tone="work">
|
||||
<el-form class="density-form">
|
||||
<table class="density-table density-table--editable preload-editor-table" aria-label="预装明细备注"><tbody>
|
||||
<tr><td><el-form-item label-position="left" label-width="128px" label="客户备注(只读)"><span class="customer-remark-text">{{ remarkTarget?.customerRemark || '—' }}</span></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label-position="left" label-width="128px" label="内部备注"><el-input v-model="remarkForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label-position="left" label-width="128px" label="仓库备注"><el-input v-model="remarkForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></el-form-item></td></tr>
|
||||
<tr><td><el-form-item label-position="left" label-width="128px" label="客服备注"><el-input v-model="remarkForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></el-form-item></td></tr>
|
||||
</tbody></table>
|
||||
</el-form>
|
||||
</section>
|
||||
<LoadErrorState v-if="remarkError" compact title="备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" title="确认预装并推送 PDA" width="min(520px, calc(100vw - 28px))" confirm-text="确认并推送" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm || !assigneeId || assigneeLoading" @confirm="submitConfirmPreload">
|
||||
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" :title="preloadDialogTitle('确认预装并推送 PDA')" width="min(520px, calc(100vw - 28px))" confirm-text="确认并推送" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm || !assigneeId || assigneeLoading" @confirm="submitConfirmPreload">
|
||||
<div class="confirm-content">
|
||||
<el-alert type="warning" :closable="false" show-icon title="本操作将确认整份预装计划" />
|
||||
<el-descriptions v-if="detail" :column="1" border>
|
||||
<el-descriptions-item label="预装单号">{{ detail.summary.preloadNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="柜次号">{{ detail.summary.cabinetNo || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="集装箱号">{{ detail.summary.containerNo || '待补录' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1" border><el-descriptions-item label="PDA 操作员">{{ selectedAssignee?.displayName || '未选择' }}</el-descriptions-item></el-descriptions>
|
||||
<section class="density-dialog-section" data-tone="summary">
|
||||
<table class="density-table preload-confirm-table" aria-label="确认预装资料"><tbody>
|
||||
<template v-if="detail"><tr><th>预装单号</th><td>{{ detail.summary.preloadNo }}</td></tr><tr><th>柜次号</th><td>{{ detail.summary.cabinetNo || '—' }}</td></tr><tr><th>集装箱号</th><td>{{ detail.summary.containerNo || '待补录' }}</td></tr></template>
|
||||
<tr><th>PDA 操作员</th><td>{{ selectedAssignee?.displayName || '未选择' }}</td></tr>
|
||||
</tbody></table>
|
||||
</section>
|
||||
<LoadErrorState v-if="confirmError" compact title="推送失败" :message="confirmError.message" :show-retry="false" />
|
||||
</div>
|
||||
</AppDialog>
|
||||
@@ -327,16 +339,13 @@ watch([planId, warehouseId], () => {
|
||||
|
||||
<style scoped>
|
||||
.preload-detail-page { display: grid; min-width: 0; width: 100%; gap: var(--admin-space-5, 12px); }
|
||||
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-6, 16px); }
|
||||
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.preload-editor-table { table-layout: fixed; }
|
||||
.preload-confirm-table th { width: 120px; }
|
||||
.customer-remark-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.confirm-content { display: grid; gap: var(--admin-space-5, 12px); }
|
||||
.pda-operator-picker { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 8px; }
|
||||
.pda-operator-picker label { flex: 0 0 auto; font-size: var(--admin-font-body, 13px); white-space: nowrap; }
|
||||
.pda-operator-picker label span { color: var(--el-color-warning-dark-2); font-size: var(--admin-font-caption, 12px); }
|
||||
.pda-operator-picker :deep(.el-select) { width: 180px; }
|
||||
.remarks-form { margin-top: 14px; }
|
||||
.remarks-form small { display: block; margin-top: var(--admin-space-1, 4px); color: var(--cc-muted); }
|
||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
||||
@media (min-width: 1920px) { .preload-detail-page { width: 100%; max-width: none; gap: var(--admin-space-5, 12px); } }
|
||||
@media (max-width: 720px) { .transport-form { grid-template-columns: 1fr; } .transport-form :deep(.el-form-item:last-child) { grid-column: auto; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user