fix(admin): open measurement detail after review
This commit is contained in:
@@ -108,6 +108,26 @@ describe('InboundTaskList measurement scope', () => {
|
|||||||
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
|
expect(source).not.toContain("name: 'inbound-order-detail', params: { inboundId: row.id }")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('closes only after navigating an approved review to the measurement tab', () => {
|
||||||
|
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||||
|
const handler = source.slice(source.indexOf('async function handleTaskChanged'), source.indexOf('function handleReviewSelectionChange'))
|
||||||
|
|
||||||
|
expect(handler).toContain("name: 'unified-order-detail-by-inbound-code'")
|
||||||
|
expect(handler).toContain('params: { inboundCode }')
|
||||||
|
expect(handler).toContain("query: { tab: 'measurement', from: 'measurement-review' }")
|
||||||
|
expect(handler).toContain('await router.push')
|
||||||
|
expect(handler.indexOf('await router.push')).toBeLessThan(handler.lastIndexOf('reviewDialogOpen.value = false'))
|
||||||
|
expect(handler).toContain("getApiErrorDetails(error, '订单详情打开失败,请重试')")
|
||||||
|
expect(handler).toContain("name: 'unified-order-detail'")
|
||||||
|
expect(handler).toContain("query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' }")
|
||||||
|
expect(source).toContain(':post-approve-loading="reviewNavigationLoading"')
|
||||||
|
expect(source).toContain(':approval-completed="reviewApproved"')
|
||||||
|
expect(source).toContain(':close-on-click-modal="!reviewNavigationLoading"')
|
||||||
|
expect(source).toContain('reviewNavigationError')
|
||||||
|
expect(source).toContain('重新打开订单详情')
|
||||||
|
expect(source).not.toContain('params: { inboundId: target.id }')
|
||||||
|
})
|
||||||
|
|
||||||
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
|
it('shows authoritative measurement totals with operation first and no internal IDs as primary columns', () => {
|
||||||
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
const source = readFileSync(resolve('src/modules/inbounds/components/InboundTaskList.vue'), 'utf8')
|
||||||
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
|
const columnsSource = source.slice(source.indexOf('const columns'), source.indexOf('function text'))
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const warehouseOptions = ref<Array<{ label: string; value: string }>>([])
|
|||||||
const selectedInbound = ref<InboundOrderSummary | null>(null)
|
const selectedInbound = ref<InboundOrderSummary | null>(null)
|
||||||
const measurementDialogOpen = ref(false)
|
const measurementDialogOpen = ref(false)
|
||||||
const reviewDialogOpen = ref(false)
|
const reviewDialogOpen = ref(false)
|
||||||
|
const reviewNavigationLoading = ref(false)
|
||||||
|
const reviewApproved = ref(false)
|
||||||
|
const reviewNavigationError = ref<ApiErrorDetails | null>(null)
|
||||||
const relationshipDialogOpen = ref(false)
|
const relationshipDialogOpen = ref(false)
|
||||||
const relationshipDialogInboundCode = ref('')
|
const relationshipDialogInboundCode = ref('')
|
||||||
const batchReviewLoading = ref(false)
|
const batchReviewLoading = ref(false)
|
||||||
@@ -184,7 +187,11 @@ function resetFilters() {
|
|||||||
|
|
||||||
function openTask(row: InboundOrderSummary) {
|
function openTask(row: InboundOrderSummary) {
|
||||||
selectedInbound.value = row
|
selectedInbound.value = row
|
||||||
if (isReview.value) reviewDialogOpen.value = true
|
if (isReview.value) {
|
||||||
|
reviewApproved.value = false
|
||||||
|
reviewNavigationError.value = null
|
||||||
|
reviewDialogOpen.value = true
|
||||||
|
}
|
||||||
else measurementDialogOpen.value = true
|
else measurementDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,10 +207,47 @@ function openRelationship(row: InboundOrderSummary) {
|
|||||||
relationshipDialogOpen.value = true
|
relationshipDialogOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTaskChanged() {
|
async function handleTaskChanged() {
|
||||||
reviewDialogOpen.value = false
|
const target = selectedInbound.value
|
||||||
selectedInbound.value = null
|
if (!isReview.value) {
|
||||||
void loadTasks()
|
reviewDialogOpen.value = false
|
||||||
|
selectedInbound.value = null
|
||||||
|
void loadTasks()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!target || reviewNavigationLoading.value) return
|
||||||
|
reviewApproved.value = true
|
||||||
|
reviewNavigationError.value = null
|
||||||
|
|
||||||
|
const inboundCode = target.inboundCode?.trim()
|
||||||
|
const orderId = target.orderId?.trim()
|
||||||
|
if (!inboundCode && !orderId) {
|
||||||
|
reviewNavigationError.value = { message: '审核已通过,但未找到对应订单,请关闭弹框后刷新列表重试' }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewNavigationLoading.value = true
|
||||||
|
try {
|
||||||
|
if (inboundCode) {
|
||||||
|
await router.push({
|
||||||
|
name: 'unified-order-detail-by-inbound-code',
|
||||||
|
params: { inboundCode },
|
||||||
|
query: { tab: 'measurement', from: 'measurement-review' },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await router.push({
|
||||||
|
name: 'unified-order-detail',
|
||||||
|
params: { orderId },
|
||||||
|
query: { tenantId: target.tenantId, tab: 'measurement', from: 'measurement-review' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
reviewDialogOpen.value = false
|
||||||
|
selectedInbound.value = null
|
||||||
|
} catch (error) {
|
||||||
|
reviewNavigationError.value = getApiErrorDetails(error, '订单详情打开失败,请重试')
|
||||||
|
} finally {
|
||||||
|
reviewNavigationLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReviewSelectionChange(rows: InboundOrderSummary[]) {
|
function handleReviewSelectionChange(rows: InboundOrderSummary[]) {
|
||||||
@@ -266,6 +310,9 @@ function handleMeasurementSubmitted() {
|
|||||||
|
|
||||||
function clearSelectedInbound() {
|
function clearSelectedInbound() {
|
||||||
selectedInbound.value = null
|
selectedInbound.value = null
|
||||||
|
reviewApproved.value = false
|
||||||
|
reviewNavigationError.value = null
|
||||||
|
reviewNavigationLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([page, pageSize], () => { void loadTasks() })
|
watch([page, pageSize], () => { void loadTasks() })
|
||||||
@@ -367,9 +414,16 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
|||||||
top="4vh"
|
top="4vh"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
|
:close-on-click-modal="!reviewNavigationLoading"
|
||||||
|
:close-on-press-escape="!reviewNavigationLoading"
|
||||||
class="inbound-task-dialog"
|
class="inbound-task-dialog"
|
||||||
@closed="clearSelectedInbound"
|
@closed="clearSelectedInbound"
|
||||||
>
|
>
|
||||||
|
<el-alert v-if="reviewNavigationError" type="error" :closable="false" show-icon class="review-navigation-error">
|
||||||
|
<template #title>{{ reviewNavigationError.message }}</template>
|
||||||
|
<ApiErrorMeta :details="reviewNavigationError" />
|
||||||
|
<el-button v-if="selectedInbound?.inboundCode || selectedInbound?.orderId" link type="danger" :loading="reviewNavigationLoading" @click="handleTaskChanged">重新打开订单详情</el-button>
|
||||||
|
</el-alert>
|
||||||
<MeasurementReviewPanel
|
<MeasurementReviewPanel
|
||||||
v-if="selectedInbound"
|
v-if="selectedInbound"
|
||||||
:tenant-id="selectedInbound.tenantId"
|
:tenant-id="selectedInbound.tenantId"
|
||||||
@@ -377,6 +431,8 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
|||||||
:customer-name="selectedInbound.customerName || selectedInbound.companyName"
|
:customer-name="selectedInbound.customerName || selectedInbound.companyName"
|
||||||
:channel-name="selectedInbound.channelName"
|
:channel-name="selectedInbound.channelName"
|
||||||
:channel-code="selectedInbound.channelCode"
|
:channel-code="selectedInbound.channelCode"
|
||||||
|
:post-approve-loading="reviewNavigationLoading"
|
||||||
|
:approval-completed="reviewApproved"
|
||||||
@changed="handleTaskChanged"
|
@changed="handleTaskChanged"
|
||||||
/>
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -398,6 +454,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.inbound-task-page { min-width: 0; }
|
.inbound-task-page { min-width: 0; }
|
||||||
.state-stack { display: grid; gap: 12px; }
|
.state-stack { display: grid; gap: 12px; }
|
||||||
|
.review-navigation-error { margin-bottom: 14px; }
|
||||||
.row-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
.row-actions { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
||||||
.row-actions .el-button { margin: 0; }
|
.row-actions .el-button { margin: 0; }
|
||||||
.primary-cell { display: grid; gap: 3px; min-width: 0; }
|
.primary-cell { display: grid; gap: 3px; min-width: 0; }
|
||||||
|
|||||||
@@ -195,11 +195,73 @@ describe('MeasurementReviewPanel', () => {
|
|||||||
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
|
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))?.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(3)
|
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(2)
|
||||||
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 8 })
|
expect(approveMeasurement).toHaveBeenCalledWith('10001', '90001', { measurementId: '70001', expectedVersion: 8 })
|
||||||
expect(wrapper.emitted('changed')).toHaveLength(1)
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('emits only after approval succeeds and blocks duplicate submissions', async () => {
|
||||||
|
let finishApproval: (() => void) | undefined
|
||||||
|
vi.mocked(approveMeasurement).mockImplementation(() => new Promise((resolve) => {
|
||||||
|
finishApproval = () => resolve({ inboundId: '90001', measurementId: '70001', processStatus: 'WAIT_SPLIT', feeStatus: 'CALCULATED', initialPricingSucceeded: true, initialPricingMessage: null, initialFee: null })
|
||||||
|
}))
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||||
|
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const approveButton = wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!
|
||||||
|
await approveButton.trigger('click')
|
||||||
|
await approveButton.trigger('click')
|
||||||
|
|
||||||
|
expect(approveMeasurement).toHaveBeenCalledTimes(1)
|
||||||
|
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||||
|
finishApproval?.()
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.emitted('changed')).toHaveLength(1)
|
||||||
|
expect(getMeasurementReviewDetail).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the review panel open and does not emit when approval fails', async () => {
|
||||||
|
vi.mocked(approveMeasurement).mockRejectedValue(new Error('审核写入失败'))
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||||
|
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||||
|
expect(wrapper.find('.review-panel').exists()).toBe(true)
|
||||||
|
expect(wrapper.findAll('button').some((button) => button.text().includes('通过并进入待分柜'))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks approval while the order detail navigation is loading', async () => {
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||||
|
const wrapper = mount(MeasurementReviewPanel, {
|
||||||
|
props: { customerId: '10001', inboundId: '90001', postApproveLoading: true },
|
||||||
|
global: { plugins: [ElementPlus] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.findAll('button').find((button) => button.text().includes('通过并进入待分柜'))!.trigger('click')
|
||||||
|
|
||||||
|
expect(approveMeasurement).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not submit approval again after approval succeeded but navigation needs retrying', async () => {
|
||||||
|
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue(undefined as never)
|
||||||
|
const wrapper = mount(MeasurementReviewPanel, {
|
||||||
|
props: { customerId: '10001', inboundId: '90001', approvalCompleted: true },
|
||||||
|
global: { plugins: [ElementPlus] },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const approveButton = wrapper.findAll('button').find((button) => button.text().includes('审核已通过'))!
|
||||||
|
expect(approveButton.attributes('disabled')).toBeDefined()
|
||||||
|
await approveButton.trigger('click')
|
||||||
|
expect(approveMeasurement).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('shows historical rejected data read-only without new reject or release operations', async () => {
|
it('shows historical rejected data read-only without new reject or release operations', async () => {
|
||||||
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, processStatus: 'MEASURE_REJECTED', measureReviewStatus: 'REJECTED', reviewReason: '历史审核说明' })
|
vi.mocked(getMeasurementReviewDetail).mockResolvedValue({ ...detail, processStatus: 'MEASURE_REJECTED', measureReviewStatus: 'REJECTED', reviewReason: '历史审核说明' })
|
||||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ const props = defineProps<{
|
|||||||
customerName?: string | null
|
customerName?: string | null
|
||||||
channelName?: string | null
|
channelName?: string | null
|
||||||
channelCode?: string | null
|
channelCode?: string | null
|
||||||
|
postApproveLoading?: boolean
|
||||||
|
approvalCompleted?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{ changed: [] }>()
|
const emit = defineEmits<{ changed: [] }>()
|
||||||
@@ -129,7 +131,7 @@ async function handleWeightChanged() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmApprove() {
|
async function confirmApprove() {
|
||||||
if (!detail.value || !isPending.value || !canReview.value) return
|
if (!detail.value || !isPending.value || !canReview.value || actionLoading.value || props.postApproveLoading || props.approvalCompleted) return
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
'审核通过后,入库单将进入待分柜。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?',
|
'审核通过后,入库单将进入待分柜。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?',
|
||||||
@@ -147,7 +149,6 @@ async function confirmApprove() {
|
|||||||
})
|
})
|
||||||
if (result.initialPricingSucceeded) ElMessage.success('审核已通过,入库单已进入待分柜,费用已完成初算')
|
if (result.initialPricingSucceeded) ElMessage.success('审核已通过,入库单已进入待分柜,费用已完成初算')
|
||||||
else ElMessage.warning(`审核已通过,入库单已进入待分柜;费用初算未完成${result.initialPricingMessage ? `:${result.initialPricingMessage}` : ''}`)
|
else ElMessage.warning(`审核已通过,入库单已进入待分柜;费用初算未完成${result.initialPricingMessage ? `:${result.initialPricingMessage}` : ''}`)
|
||||||
await reload()
|
|
||||||
emit('changed')
|
emit('changed')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(isConflict(error) ? '数据已被更新,请刷新后重新审核' : getApiErrorDetails(error, '审核通过失败').message)
|
ElMessage.error(isConflict(error) ? '数据已被更新,请刷新后重新审核' : getApiErrorDetails(error, '审核通过失败').message)
|
||||||
@@ -183,7 +184,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
|||||||
</div>
|
</div>
|
||||||
<div class="review-toolbar__actions">
|
<div class="review-toolbar__actions">
|
||||||
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
|
<el-button v-if="isPending && canReview" type="primary" plain @click="correctionMode = true"><Pencil :size="16" />修改重量</el-button>
|
||||||
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve'" @click="confirmApprove"><Check :size="16" />通过并进入待分柜</el-button>
|
<el-button v-if="isPending && canReview" type="primary" :loading="actionLoading === 'approve' || postApproveLoading" :disabled="approvalCompleted" @click="confirmApprove"><Check :size="16" />{{ approvalCompleted ? '审核已通过' : '通过并进入待分柜' }}</el-button>
|
||||||
<el-button :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
|
<el-button :loading="loading" aria-label="刷新审核数据" @click="reload"><RotateCcw :size="16" />刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user