优化
This commit is contained in:
@@ -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<HTMLElement>('.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<VueWrapper, 'findAllComponents'>, 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')]
|
||||
|
||||
@@ -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<CargoDiscardSummary | null>(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() })
|
||||
</script>
|
||||
<template>
|
||||
<AdminPage class="admin-density admin-workbench"><ListPageLayout aria-label="废弃单列表"><section><LoadErrorState v-if="customerError" title="客户列表加载失败" :message="customerError.message" @retry="loadCustomers"><template #details><ApiErrorMeta :details="customerError" /></template></LoadErrorState><LoadErrorState v-if="listError" title="废弃单列表加载失败" :message="listError.message" @retry="load()"><template #details><ApiErrorMeta :details="listError" /></template></LoadErrorState><LoadErrorState v-if="reviewError" title="审核失败" :message="reviewError.message" :show-retry="false"><template #details><ApiErrorMeta :details="reviewError" /></template></LoadErrorState><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :data="rows" :columns="columns" :loading="loading" :total="total" v-model:page="page" v-model:page-size="pageSize" preference-key="overseas-cargo-discards" row-key="id" empty-text="暂无废弃单" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-start><h1>废弃单</h1></template><template #filters><QueryToolbar ariaLabel="废弃单筛选" v-model="filters" :fields="fields" :loading="loading" preference-key="overseas-cargo-discards-query" @search="page = 1; load($event)" @reset="page = 1; load($event)" /></template><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="!!reviewing" @click="openDetail(row as CargoDiscardSummary)">详情</el-button><el-button class="density-action" v-if="access.can(permission.discardApprove) && (row as CargoDiscardSummary).status === 'PENDING_REVIEW'" size="small" type="primary" :disabled="!!reviewing" @click="audit(row as CargoDiscardSummary)">审核并推送 PDA</el-button><el-button v-if="access.can(permission.discardApprove) && (row as CargoDiscardSummary).status === 'PENDING_REVIEW'" class="density-action" size="small" type="danger" :disabled="!!reviewing" @click="reject(row as CargoDiscardSummary)">驳回</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabels[String((row as CargoDiscardSummary).status)] || (row as CargoDiscardSummary).status || '未记录'" /></template></DataTable></div></section></ListPageLayout></AdminPage>
|
||||
<AdminPage class="admin-density admin-workbench"><ListPageLayout aria-label="废弃单列表"><section><LoadErrorState v-if="customerError" title="客户列表加载失败" :message="customerError.message" @retry="loadCustomers"><template #details><ApiErrorMeta :details="customerError" /></template></LoadErrorState><LoadErrorState v-if="listError" title="废弃单列表加载失败" :message="listError.message" @retry="load()"><template #details><ApiErrorMeta :details="listError" /></template></LoadErrorState><LoadErrorState v-if="reviewError" title="审核失败" :message="reviewError.message" :show-retry="false"><template #details><ApiErrorMeta :details="reviewError" /></template></LoadErrorState><div class="workbench-table is-page-surface"><DataTable appearance="integrated" :data="rows" :columns="columns" :loading="loading" :total="total" v-model:page="page" v-model:page-size="pageSize" preference-key="overseas-cargo-discards" row-key="id" empty-text="暂无废弃单" @update:page="load()" @update:page-size="page = 1; load()"><template #toolbar-start><h1>废弃单</h1></template><template #filters><QueryToolbar ariaLabel="废弃单筛选" v-model="filters" :fields="fields" :loading="loading" preference-key="overseas-cargo-discards-query" @search="page = 1; load($event)" @reset="page = 1; load($event)" /></template><template #actions="{ row }"><el-button class="density-action" size="small" :disabled="!!reviewing" @click="openDetail(row as CargoDiscardSummary)">详情</el-button><el-button class="density-action" v-if="access.can(permission.discardApprove) && (row as CargoDiscardSummary).status === 'PENDING_REVIEW'" size="small" type="primary" :disabled="!!reviewing" @click="openReview(row as CargoDiscardSummary)">审核</el-button></template><template #status="{ row }"><StatusBadge :status="statusLabels[String((row as CargoDiscardSummary).status)] || (row as CargoDiscardSummary).status || '未记录'" /></template></DataTable></div></section></ListPageLayout></AdminPage>
|
||||
<AppDialog v-model="detailOpen" class="admin-density-dialog admin-workbench is-dialog" :title="'废弃单详情 · ' + (detailTarget?.discardNo || '')" :confirm-loading="detailLoading" :show-confirm="false" cancel-text="关闭" width="min(920px, calc(100vw - 28px))" fullscreenable>
|
||||
<p v-if="detailLoading" role="status">正在读取废弃单…</p>
|
||||
<LoadErrorState v-else-if="detailError" title="废弃单详情加载失败" :message="detailError.message" @retry="readDetail"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
@@ -126,10 +130,17 @@ onMounted(() => { void loadCustomers(); void load() })
|
||||
<LoadErrorState v-else-if="photoErrors[photo.fileId]" title="废弃照片加载失败" :message="photoErrors[photo.fileId]?.message" @retry="loadPhoto(photo.fileId)"><template #details><ApiErrorMeta :details="photoErrors[photo.fileId]!" /></template></LoadErrorState>
|
||||
<el-button v-else @click="loadPhoto(photo.fileId)">加载照片</el-button><small>托盘:{{ photo.palletNo }} · {{ photo.submittedAt }}</small>
|
||||
</div></div><p v-else>暂无已提交的废弃现场照片</p>
|
||||
<div v-if="detail.summary.status === 'PENDING_REVIEW' && access.can(permission.discardApprove)"><el-button type="danger" :disabled="!!reviewing" @click="reject(detail.summary)">驳回</el-button></div>
|
||||
<div v-if="detail.summary.status === 'PENDING_REVIEW' && access.can(permission.discardApprove)"><el-button type="primary" :disabled="!!reviewing" @click="openReview(detail.summary)">审核</el-button></div>
|
||||
<h3>状态流转</h3><table v-if="detail.histories.length" class="density-table" aria-label="废弃状态流转"><tbody><tr v-for="(item, index) in detail.histories" :key="index"><td>{{ item.createdAt || '—' }}</td><td>{{ statusLabels[item.toStatus || ''] || item.toStatus || '未记录' }}</td><td>{{ item.remark || '—' }}</td></tr></tbody></table><p v-else>暂无状态流转记录</p>
|
||||
</template>
|
||||
</AppDialog>
|
||||
<AppDialog v-model="reviewOpen" class="admin-density-dialog admin-workbench is-dialog" :title="'废弃单审核 · ' + (reviewTarget?.discardNo || '')" :confirm-loading="!!reviewing" confirm-text="提交审核" width="min(560px, calc(100vw - 28px))" @confirm="submitReview">
|
||||
<el-form label-position="top" :disabled="!!reviewing">
|
||||
<el-form-item label="审核结果"><el-radio-group v-model="reviewAction"><el-radio value="APPROVE">审核通过</el-radio><el-radio value="REJECT">驳回</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item :label="reviewAction === 'REJECT' ? '驳回原因' : '审核意见'" :required="reviewAction === 'REJECT'"><el-input v-model="reviewRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit :placeholder="reviewAction === 'REJECT' ? '请填写驳回原因' : '请输入审核意见(选填)'" /></el-form-item>
|
||||
</el-form>
|
||||
<LoadErrorState v-if="reviewError" title="审核失败" :message="reviewError.message" :show-retry="false"><template #details><ApiErrorMeta :details="reviewError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user