优化
This commit is contained in:
@@ -5,7 +5,7 @@ import { CheckCircle2, Eye, FileUp, Undo2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { confirmRelabelCompletion, getRelabelReviewTask, returnRelabelForRework } from '../api/relabelOrders'
|
||||
import { changeRelabelOrderStatus, confirmRelabelCompletion, getRelabelReviewTask, returnRelabelForRework } from '../api/relabelOrders'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -25,8 +25,20 @@ const loading = ref(false)
|
||||
const access = useAccessPreviewStore()
|
||||
const returning = ref(false)
|
||||
const confirming = ref(false)
|
||||
const voiding = ref(false)
|
||||
const canVoid = computed(() => !!detail.value && !['READY_FOR_OUTBOUND', 'COMPLETED', 'CANCELLED'].includes(detail.value.application.status || ''))
|
||||
async function voidOrder() {
|
||||
if (voiding.value || confirming.value || returning.value || !canVoid.value || !access.can(permission.relabelOrderStatus)) return
|
||||
voiding.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm('确认作废当前换标单?作废后停止换标作业,保留原单和操作记录。', '作废换标单', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' })
|
||||
await changeRelabelOrderStatus(orderId.value, 'CANCELLED', '平台作废换标单')
|
||||
ElMessage.success('换标单已作废'); await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标单作废失败,请重试').message) }
|
||||
finally { voiding.value = false }
|
||||
}
|
||||
async function confirmCompletion() {
|
||||
if (confirming.value) return
|
||||
if (confirming.value || voiding.value) return
|
||||
confirming.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm('确认仓库上传的换标结果无误并继续后续流程吗?', '换标完成确认', { customClass: 'admin-density-confirm', confirmButtonText: '确认并继续', cancelButtonText: '取消' })
|
||||
@@ -36,7 +48,7 @@ async function confirmCompletion() {
|
||||
finally { confirming.value = false }
|
||||
}
|
||||
async function returnForRework() {
|
||||
if (returning.value) return
|
||||
if (returning.value || voiding.value) return
|
||||
returning.value = true
|
||||
try {
|
||||
const task = await getRelabelReviewTask(orderId.value)
|
||||
@@ -67,7 +79,7 @@ function status(value: unknown) {
|
||||
READY_FOR_OUTBOUND: '换标完成',
|
||||
COMPLETED: '换标完成',
|
||||
REJECTED: '已驳回',
|
||||
CANCELLED: '已取消',
|
||||
CANCELLED: '已作废',
|
||||
} as Record<string, string>)[String(value || '')] || text(value)
|
||||
}
|
||||
function walletStatus(value: unknown) { return ({ FREEZE_PENDING: '冻结中', FROZEN: '已冻结', SETTLE_PENDING: '结算中', SETTLED: '已结算', RELEASE_PENDING: '释放中', RELEASED: '已释放', FREEZE_FAILED: '冻结失败', SETTLE_FAILED: '结算失败', RELEASE_FAILED: '释放失败' } as Record<string,string>)[String(value || '')] || text(value) }
|
||||
@@ -129,6 +141,7 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
<OverseasReadOnlyDetail class="admin-density" dense title="换标单详情" reference-label="换标单号" :reference-value="referenceValue" back-label="返回换标单" domain="换标单详情" :groups="groups" :field-values="fieldValues" :pending="false" :status-text="detail ? status(detail.application.status) : '加载中'" :loading="loading" :show-content="!loadError" @back="back">
|
||||
<template #state><LoadErrorState v-if="loadError" title="换标单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<template #content-after>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && canVoid" class="relabel-document-panel relabel-review-actions"><el-button type="danger" :loading="voiding" :disabled="confirming || returning" @click="voidOrder">作废</el-button></section>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(detail?.application.status || '') && detail?.completionEvidence?.photoFileIds?.length" class="relabel-document-panel relabel-review-actions"><el-button v-if="detail?.application.status === 'CUSTOMER_CONFIRM_PENDING'" type="primary" :loading="confirming" @click="confirmCompletion"><CheckCircle2 :size="15" />确认换标完成</el-button><el-button type="warning" :loading="returning" @click="returnForRework"><Undo2 :size="15" />退回重新贴标</el-button></section>
|
||||
<RelabelDocumentsPanel v-if="detail" :relabel-no="detail.application.relabelNo" :detail="detail" /><section class="relabel-document-panel" aria-label="PDA换标完工证据"><div class="relabel-mark-list__head"><h2>PDA 换标完工证据</h2><span>{{ detail?.completionEvidence?.reviewerName ? `复核人:${detail.completionEvidence.reviewerName}` : '等待仓库作业' }}</span></div><div v-if="detail?.completionEvidence?.photoFileIds?.length" class="document-row"><strong>共 {{ detail.completionEvidence.photoFileIds.length }} 张完工照片</strong><span class="document-actions density-actions"><el-button class="density-action" data-tone="neutral" v-for="(fileId, index) in detail.completionEvidence.photoFileIds" :key="fileId" size="small" @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></span></div><div v-else class="attachment-empty">暂未上传完工照片</div></section>
|
||||
<section class="relabel-document-panel" aria-label="总换标文件"><div class="relabel-mark-list__head"><h2>总换标 PDF</h2><label class="file-picker"><FileUp :size="15" />{{ uploading ? '上传中' : (detail?.application.totalFileName ? '更换PDF' : '上传PDF') }}<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'TOTAL')" /></label></div><div v-if="detail?.application.totalFileId" class="document-row"><strong>{{ detail.application.totalFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(detail.application.totalFileId, detail.application.totalFileName || '总换标PDF', 'TOTAL')"><Eye :size="14" />直接预览</el-button></div><div v-else class="attachment-empty">暂未上传总换标PDF</div></section>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import ElementPlus, { ElMessageBox } from 'element-plus'
|
||||
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listPlatformTenants } from '@/modules/iam'
|
||||
@@ -132,6 +132,29 @@ describe('RelabelOrdersView', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('offers void for unfinished states only and submits the confirmed real status command', async () => {
|
||||
const statuses = ['PENDING_REVIEW', 'PENDING_PROCESSING', 'PROCESSING', 'CUSTOMER_CONFIRM_PENDING', 'REJECTED', 'READY_FOR_OUTBOUND', 'COMPLETED', 'CANCELLED']
|
||||
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: statuses.map(status => ({ id: status, relabelNo: 'RL-' + status, status })), page: 1, size: 10, total: 8, pages: 1 })
|
||||
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const buttons = wrapper.findAll('tbody button').filter(button => button.text().trim() === '作废')
|
||||
expect(buttons).toHaveLength(5)
|
||||
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValueOnce('confirm' as never)
|
||||
await buttons[0]!.trigger('click'); await flushPromises()
|
||||
expect(changeRelabelOrderStatus).toHaveBeenCalledExactlyOnceWith('RL-PENDING_REVIEW', 'CANCELLED', '平台作废换标单')
|
||||
confirm.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps the order unchanged when void confirmation is cancelled', async () => {
|
||||
vi.mocked(listRelabelOrders).mockResolvedValueOnce({ records: [{ id: '1', relabelNo: 'RL-1', status: 'PROCESSING' }], page: 1, size: 10, total: 1, pages: 1 })
|
||||
const wrapper = mount(RelabelOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('cancel')
|
||||
await wrapper.findAll('tbody button').find(button => button.text().trim() === '作废')!.trigger('click'); await flushPromises()
|
||||
expect(changeRelabelOrderStatus).not.toHaveBeenCalled()
|
||||
confirm.mockRestore()
|
||||
})
|
||||
|
||||
it('hides the audit action after approval', async () => {
|
||||
vi.mocked(listRelabelOrders).mockResolvedValueOnce({
|
||||
records: [{ id: '1', relabelNo: 'RL-1', customerOrderNo: 'CO-1', skuCodes: 'SKU-1', totalPackageCount: 2, status: 'PENDING_PROCESSING' }],
|
||||
|
||||
@@ -3,7 +3,7 @@ import '@/styles/adminDensity.css'
|
||||
import { computed, onActivated, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||||
import { Plus, PackagePlus } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
@@ -24,6 +24,7 @@ const access = useAccessPreviewStore()
|
||||
const router = useRouter()
|
||||
const rows = ref<RelabelOrderListItem[]>([])
|
||||
const loading = ref(false)
|
||||
const voiding = ref('')
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const customers = ref<PlatformTenantResource[]>([])
|
||||
const customersLoading = ref(false)
|
||||
@@ -36,7 +37,7 @@ const filters = ref<QueryModel>({ relabelNo: '', sku: '', status: '' })
|
||||
const fields = computed<QueryField[]>(() => [
|
||||
{ key: 'relabelNo', type: 'input', label: '换标单号', placeholder: '请输入换标单号', width: 220 },
|
||||
{ key: 'sku', type: 'input', label: '商品SKU', placeholder: '请输入商品SKU', width: 180 },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }] },
|
||||
{ key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '待审核', value: 'PENDING_REVIEW' }, { label: '待处理', value: 'PENDING_PROCESSING' }, { label: '处理中', value: 'PROCESSING' }, { label: '换标完成', value: 'COMPLETED' }, { label: '已作废', value: 'CANCELLED' }] },
|
||||
])
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||||
@@ -177,8 +178,26 @@ function openDetail(source: object) {
|
||||
}
|
||||
|
||||
function relabelStatus(value: unknown) {
|
||||
if (value === 'CANCELLED') return '已作废'
|
||||
if (value === 'CUSTOMER_CONFIRM_PENDING') return '待客户确认'
|
||||
return relabelBusinessStatusLabel(value)
|
||||
}
|
||||
function canVoid(source: object) {
|
||||
const row = source as RelabelOrderListItem
|
||||
return !['READY_FOR_OUTBOUND', 'COMPLETED', 'CANCELLED'].includes(row.rawStatus || relabelStatusCode(row.status))
|
||||
}
|
||||
async function voidOrder(source: object) {
|
||||
const row = source as RelabelOrderListItem
|
||||
if (voiding.value || !canVoid(row) || !access.can(permission.relabelOrderStatus)) return
|
||||
voiding.value = String(row.orderNo)
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认作废换标单 ${row.orderNo}?作废后停止换标作业,保留原单和操作记录。`, '作废换标单', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' })
|
||||
await changeRelabelOrderStatus(String(row.orderNo), 'CANCELLED', '平台作废换标单')
|
||||
ElMessage.success('换标单已作废')
|
||||
await load()
|
||||
} catch (error) { if (error !== 'cancel' && error !== 'close') ElMessage.error(getApiErrorDetails(error, '换标单作废失败,请重试').message) }
|
||||
finally { voiding.value = '' }
|
||||
}
|
||||
function relabelAuditStatusLabel(value: string) {
|
||||
return ({
|
||||
PENDING_PROCESSING: '审核通过',
|
||||
@@ -231,7 +250,7 @@ watch(dialogOpen, (open) => { if (!open) createIdempotencyKey.value = '' })
|
||||
|
||||
|
||||
<template #toolbar-actions><el-button v-if="selectedRows.length" type="warning" @click="openPackingPlan"><PackagePlus :size="16" />创建合箱计划({{ selectedRows.length }})</el-button><el-button v-if="access.can(permission.relabelOrderCreate)" type="primary" @click="openForm('create')"><Plus :size="16" />新增换标单</el-button></template>
|
||||
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="warning" v-if="access.can(permission.relabelOrderStatus) && canAudit(row)" size="small" @click="openForm('edit', row)">审核</el-button></div></template>
|
||||
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row)">查看</el-button><el-button class="density-action" data-tone="warning" v-if="access.can(permission.relabelOrderStatus) && canAudit(row)" size="small" @click="openForm('edit', row)">审核</el-button><el-button v-if="access.can(permission.relabelOrderStatus) && canVoid(row)" class="density-action" data-tone="danger" size="small" :loading="voiding === row.orderNo" :disabled="!!voiding" @click="voidOrder(row)">作废</el-button></div></template>
|
||||
<template #pagination-left>共 {{ total }} 条换标单</template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout>
|
||||
|
||||
Reference in New Issue
Block a user