From 604e7fb2e138b745e3ca7ae9dafb9fc64cb85328 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Tue, 3 Nov 2026 14:23:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/CargoDiscardDialogs.test.ts | 37 ++++++++-------- .../pages/CargoDiscardsView.vue | 43 ++++++++++++------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/modules/overseas-warehouse/pages/CargoDiscardDialogs.test.ts b/src/modules/overseas-warehouse/pages/CargoDiscardDialogs.test.ts index de5268f8..b3514510 100644 --- a/src/modules/overseas-warehouse/pages/CargoDiscardDialogs.test.ts +++ b/src/modules/overseas-warehouse/pages/CargoDiscardDialogs.test.ts @@ -1,5 +1,5 @@ import ElementPlus, { ElMessageBox } from 'element-plus' -import { DOMWrapper, enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' +import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import AppDialog from '@/shared/components/AppDialog.vue' import LineClampText from '@/shared/components/LineClampText.vue' @@ -37,19 +37,22 @@ beforeEach(() => { async function page() { const wrapper = mount(CargoDiscardsView, { global: { plugins: [ElementPlus] } }); await flushPromises(); return wrapper } function row(wrapper: VueWrapper, no = 'CD-001') { return wrapper.findAll('.el-table__body tr').filter(item => item.text().includes(no)).at(-1)! } async function open() { const wrapper = await page(); await row(wrapper).findAll('button').find(item => item.text() === '详情')!.trigger('click'); await flushPromises(); return { wrapper, dialog: wrapper.findComponent(AppDialog) } } -function confirmation() { return new DOMWrapper(Array.from(document.querySelectorAll('.el-message-box')).at(-1)!) } -async function accept() { await confirmation().get('.el-message-box__btns .el-button--primary').trigger('click'); await flushPromises() } +function reviewDialog(wrapper: VueWrapper) { return wrapper.findAllComponents(AppDialog).find(item => item.props('title').startsWith('废弃单审核'))! } +async function accept(wrapper: VueWrapper) { await reviewDialog(wrapper).get('.app-dialog__footer .el-button--primary').trigger('click'); await flushPromises() } function errorState(wrapper: Pick, title: string) { return wrapper.findAllComponents(LoadErrorState).find(item => item.props('title') === title)! } async function retry(state: VueWrapper) { await state.get('.load-error-state__actions button').trigger('click'); await flushPromises() } describe('native cargo discard detail and approval', () => { it('rejects a pending application with a required trimmed reason and customer scope', async () => { - const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValueOnce({ value: ' 箱唛不符 ', action: 'confirm' }) const wrapper = await page() - await row(wrapper).findAll('button').find(item => item.text() === '驳回')!.trigger('click'); await flushPromises() - expect(prompt).toHaveBeenCalled() + expect(row(wrapper).findAll('button').map(item => item.text())).toEqual(['详情', '审核']) + await row(wrapper).findAll('button').find(item => item.text() === '审核')!.trigger('click'); await flushPromises() + const dialog = reviewDialog(wrapper) + dialog.getComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'REJECT'); await flushPromises() + await accept(wrapper); expect(api.review).not.toHaveBeenCalled() + await dialog.get('textarea').setValue(' 箱唛不符 ') + await accept(wrapper) expect(api.review).toHaveBeenCalledWith('CD-001', 'REJECT', '箱唛不符', '9007199254740994') - prompt.mockRestore() }) it('formats applicant names and seconds and uses the same reviewed status as the client', async () => { api.list.mockResolvedValueOnce({ records: [summary({ createdBy: '123456', createdByName: '客户姓名', createdAt: '2026-11-02T18:42:27.511', status: 'APPROVED' })], total: 1 }) @@ -95,11 +98,11 @@ describe('native cargo discard detail and approval', () => { it('confirms approval with the business number and sends the original APPROVE request only once', async () => { let resolve!: (value: CargoDiscardDetail) => void api.review.mockReturnValueOnce(new Promise(done => { resolve = done })) - const wrapper = await page(); const button = row(wrapper).findAll('button').find(item => item.text() === '审核并推送 PDA')! + const wrapper = await page(); const button = row(wrapper).findAll('button').find(item => item.text() === '审核')! await button.trigger('click'); await flushPromises() - expect(confirmation().text()).toContain('CD-001'); expect(confirmation().text()).toContain('推送 PDA') - expect(confirmation().classes()).toContain('admin-density-confirm'); expect(api.review).not.toHaveBeenCalled() - await accept(); await button.trigger('click'); await flushPromises() + expect(reviewDialog(wrapper).props('title')).toContain('CD-001'); expect(reviewDialog(wrapper).text()).toContain('审核通过') + expect(reviewDialog(wrapper).get('.el-dialog').classes()).toContain('admin-density-dialog'); expect(api.review).not.toHaveBeenCalled() + await accept(wrapper); await button.trigger('click'); await flushPromises() expect(api.review).toHaveBeenCalledExactlyOnceWith('CD-001', 'APPROVE', undefined, '9007199254740994') expect(row(wrapper).findAll('button').every(item => item.attributes('disabled') !== undefined)).toBe(true) resolve(detail()); await flushPromises(); expect(api.list).toHaveBeenCalledTimes(2) @@ -107,12 +110,12 @@ describe('native cargo discard detail and approval', () => { it('preserves the list and diagnostics on approval failure, and ignores native cancel and close', async () => { api.review.mockRejectedValueOnce(new ApiResponseError('审核未确认', 'A0503', 503, null, 'discard-review-request')) const wrapper = await page() - const approve = () => row(wrapper).findAll('button').find(item => item.text() === '审核并推送 PDA')! - await approve().trigger('click'); await flushPromises(); await accept() + const approve = () => row(wrapper).findAll('button').find(item => item.text() === '审核')! + await approve().trigger('click'); await flushPromises(); await accept(wrapper) expect(wrapper.text()).toContain('discard-review-request'); expect(row(wrapper).text()).toContain('CD-001') - await approve().trigger('click'); await flushPromises(); await confirmation().get('.el-message-box__btns .el-button').trigger('click'); await flushPromises() + await approve().trigger('click'); await flushPromises(); await reviewDialog(wrapper).get('.app-dialog__footer .el-button').trigger('click'); await flushPromises() expect(api.review).toHaveBeenCalledOnce() - await approve().trigger('click'); await flushPromises(); await confirmation().get('.el-message-box__headerbtn').trigger('click'); await flushPromises() + await approve().trigger('click'); await flushPromises(); reviewDialog(wrapper).vm.$emit('update:modelValue', false); await flushPromises() expect(api.review).toHaveBeenCalledOnce(); expect(errorState(wrapper, '审核失败')).toBeUndefined() }) it('keeps list access readonly, checks approval status and rechecks permission after confirmation', async () => { @@ -120,8 +123,8 @@ describe('native cargo discard detail and approval', () => { const readonly = await page(); expect(row(readonly).findAll('button').map(item => item.text())).toEqual(['详情']) can.mockReturnValue(true); api.list.mockResolvedValueOnce({ records: [summary({ status: 'APPROVED' })], total: 1 }) const approved = await page(); expect(row(approved).findAll('button').map(item => item.text())).toEqual(['详情']) - const wrapper = await page(); await row(wrapper).findAll('button').find(item => item.text() === '审核并推送 PDA')!.trigger('click'); await flushPromises() - can.mockImplementation(code => code !== permission.discardApprove); await accept(); expect(api.review).not.toHaveBeenCalled() + const wrapper = await page(); await row(wrapper).findAll('button').find(item => item.text() === '审核')!.trigger('click'); await flushPromises() + can.mockImplementation(code => code !== permission.discardApprove); await accept(wrapper); expect(api.review).not.toHaveBeenCalled() }) it('downloads each photo in its explicit scope and keeps native viewer order, size and cleanup', async () => { const result = detail(); result.photos = [photo(), photo('9007199254740997')] diff --git a/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue b/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue index c507e56d..b523eef4 100644 --- a/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue +++ b/src/modules/overseas-warehouse/pages/CargoDiscardsView.vue @@ -2,7 +2,7 @@ import '@/styles/adminWorkbench.css' import '@/styles/adminDensity.css' import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' -import { ElMessage, ElMessageBox } from 'element-plus' +import { ElMessage } from 'element-plus' import AdminPage from '@/shared/components/AdminPage.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import ListPageLayout from '@/shared/components/ListPageLayout.vue' @@ -73,30 +73,34 @@ async function readDetail() { finally { if (generation === detailGeneration) detailLoading.value = false } } async function openDetail(row: CargoDiscardSummary) { if (reviewing.value) return; clearPhotos(); detail.value = null; detailTarget.value = row; detailOpen.value = true; await readDetail() } -async function audit(row: CargoDiscardSummary) { +const reviewOpen = ref(false) +const reviewTarget = ref(null) +const reviewAction = ref<'APPROVE' | 'REJECT'>('APPROVE') +const reviewRemark = ref('') +/** 单一审核入口内选择通过或驳回。 */ +function openReview(row: CargoDiscardSummary) { if (reviewing.value || row.status !== 'PENDING_REVIEW' || !access.can(permission.discardApprove)) return - reviewing.value = row.discardNo; reviewError.value = null - try { await ElMessageBox.confirm('确认审核废弃单 ' + row.discardNo + ' 并推送 PDA 执行废弃?', '审核确认', { confirmButtonText: '通过并推送 PDA', cancelButtonText: '取消', customClass: 'admin-density-confirm admin-workbench is-dialog' }); if (!access.can(permission.discardApprove)) return; await reviewCargoDiscard(row.discardNo, 'APPROVE', undefined, row.customerId || undefined); ElMessage.success('已审核通过并推送 PDA'); await load() } - catch (error) { if (error !== 'cancel' && error !== 'close') reviewError.value = getApiErrorDetails(error, '审核失败') } - finally { reviewing.value = '' } + reviewTarget.value = row; reviewAction.value = 'APPROVE'; reviewRemark.value = ''; reviewError.value = null; reviewOpen.value = true } -/** 驳回须填写原因,成功后刷新当前列表和详情。 */ -async function reject(row: CargoDiscardSummary) { - if (reviewing.value || row.status !== 'PENDING_REVIEW' || !access.can(permission.discardApprove)) return +/** 驳回原因必填;请求期间防重复提交,失败保留审核输入。 */ +async function submitReview() { + const row = reviewTarget.value + if (!row || reviewing.value || row.status !== 'PENDING_REVIEW' || !access.can(permission.discardApprove)) return + const remark = reviewRemark.value.trim() + if (reviewAction.value === 'REJECT' && !remark) { ElMessage.warning('请填写驳回原因'); return } reviewing.value = row.discardNo; reviewError.value = null try { - const { value } = await ElMessageBox.prompt('请填写驳回原因', '驳回废弃单', { inputType: 'textarea', inputValidator: (value: string) => !!value?.trim() || '请填写驳回原因', confirmButtonText: '确认驳回', cancelButtonText: '取消', customClass: 'admin-density-confirm admin-workbench is-dialog' }) - if (!access.can(permission.discardApprove)) return - await reviewCargoDiscard(row.discardNo, 'REJECT', value.trim(), row.customerId || undefined) - ElMessage.success('已驳回'); await load() + await reviewCargoDiscard(row.discardNo, reviewAction.value, remark || undefined, row.customerId || undefined) + ElMessage.success(reviewAction.value === 'APPROVE' ? '已审核通过并推送 PDA' : '已驳回') + reviewOpen.value = false; await load() if (detailOpen.value && detailTarget.value?.discardNo === row.discardNo) await readDetail() - } catch (error) { if (error !== 'cancel' && error !== 'close') reviewError.value = getApiErrorDetails(error, '驳回失败') } + } catch (error) { reviewError.value = getApiErrorDetails(error, '审核失败') } finally { reviewing.value = '' } } onMounted(() => { void loadCustomers(); void load() }) + + + 审核通过驳回 + + + +