fix: align audit and dispatch workflows and release failed requests

This commit is contained in:
2026-10-22 23:10:09 +08:00
parent 9cdcb9c077
commit 4a3456756f
124 changed files with 1296 additions and 2120 deletions
@@ -102,7 +102,7 @@ describe('MeasurementReviewPanel', () => {
expect(wrapper.text()).not.toContain('通过并进入')
await wrapper.findAll('button').find((button) => button.text() === '通过')!.trigger('click')
await flushPromises()
expect(confirmation).toHaveBeenCalledWith(expect.stringContaining('进入待一件代发组托'), '确认测量审核通过', expect.objectContaining({ confirmButtonText: '通过' }))
expect(confirmation).toHaveBeenCalledWith('审核通过后,入库单将进入待分柜', '确认测量', expect.objectContaining({ confirmButtonText: '通过' }))
expect(initialPricingSucceeded ? success : warning).toHaveBeenCalledWith(expect.stringContaining('入库单已进入待一件代发组托'))
expect(approveMeasurement).toHaveBeenCalledTimes(1)
expect(wrapper.emitted('changed')).toHaveLength(1)
@@ -140,8 +140,8 @@ async function confirmApprove() {
if (!detail.value || !isPending.value || !canReview.value || actionLoading.value || props.postApproveLoading || props.approvalCompleted) return
try {
await ElMessageBox.confirm(
`审核通过后,入库单将进入${approvalTargetLabel.value}。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?`,
'确认测量审核通过',
'审核通过后,入库单将进入待分柜',
'确认测量',
{ customClass: 'admin-density-confirm', confirmButtonText: approvalButtonLabel.value, cancelButtonText: '返回', type: 'warning' },
)
} catch {
@@ -285,6 +285,8 @@ defineExpose({ confirmApprove, enableWeightCorrection, approvalButtonLabel, isPe
</section>
<WeightOcrPanel
class="density-dialog-section"
data-tone="source"
review-mode
table-mode
:correction-mode="correctionMode"
@@ -310,7 +312,7 @@ defineExpose({ confirmApprove, enableWeightCorrection, approvalButtonLabel, isPe
</template>
<style scoped>
.review-panel { display: grid; gap: var(--admin-space-5, 12px); color: var(--workbench-text); }
.review-panel { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--admin-space-5, 12px); color: var(--workbench-text); }
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); position: sticky; top: 0; z-index: 3; flex-wrap: wrap; padding: var(--admin-space-3, 8px) 0; border-bottom: 1px solid var(--workbench-border); background: var(--admin-surface, #fff); }
.review-toolbar__status, .review-toolbar__actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3, 8px); }
.review-toolbar__status > span { color: var(--workbench-muted); font-size: var(--admin-font-caption, 12px); }
@@ -100,6 +100,17 @@ const workspace: WeightWorkspaceResource = {
},
}
const confirmedReviewWorkspace: WeightWorkspaceResource = {
...workspace,
processStatus: 'MEASURE_PENDING',
actualGrossWeightKg: 120,
actualNetWeightKg: 108,
weightResults: [{
...pendingWeight, recognitionStatus: 'CONFIRMED', adjustedWeightValue: 120,
adjustedWeightUnit: 'KG', grossWeightKg: 120, netWeightKg: 108, version: 1,
}],
}
function palletPage(records = [{
palletId: '50001', palletCode: 'TP-001', palletType: '木托盘', capacityCbm: 1.2, tareWeightKg: 12,
}]) {
@@ -496,7 +507,7 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(wrapper.text()).toContain('1747.500 KG')
})
it('opens a table image and atomically corrects and confirms an approved weight during measurement review', async () => {
it('opens the explicit table correction action and atomically confirms an approved weight during measurement review', async () => {
const confirmed = {
...pendingWeight,
recognitionStatus: 'CONFIRMED',
@@ -521,7 +532,10 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(wrapper.text()).not.toContain('删除图片')
expect(wrapper.find('.weight-data-table').exists()).toBe(true)
expect(wrapper.find('.weight-grid').exists()).toBe(false)
await openWeightDialog(wrapper)
expect(wrapper.get('.weight-data-table thead th').text()).toBe('操作')
expect(wrapper.find('.pallet-picker').exists()).toBe(false)
await wrapper.get('button[aria-label="修改重量 1"]').trigger('click')
await flushPromises()
await dialogWeightInput().setValue('125')
const reason = document.body.querySelector<HTMLTextAreaElement>('textarea[placeholder="请填写本次毛重修正原因"]')
expect(reason).not.toBeNull()
@@ -538,6 +552,57 @@ describe('WeightOcrPanel confirmation behavior', () => {
expect(correctWeightResult).not.toHaveBeenCalled()
})
it.each([
{ permissions: allWeightPermissions.filter(permission => !permission.endsWith('weight:edit')), status: 'MEASURE_PENDING', correctionMode: true },
{ permissions: allWeightPermissions.filter(permission => !permission.endsWith('weight:confirm')), status: 'MEASURE_PENDING', correctionMode: true },
{ permissions: allWeightPermissions, status: 'WAIT_SPLIT', correctionMode: true },
{ permissions: allWeightPermissions, status: 'MEASURE_PENDING', correctionMode: false },
])('keeps table correction unavailable for missing permissions or inactive review: %j', async ({ permissions, status, correctionMode }) => {
vi.mocked(getWeightWorkspace).mockResolvedValue({ ...confirmedReviewWorkspace, processStatus: status })
const wrapper = mountPanel(permissions, { reviewMode: true, correctionMode, tableMode: true })
await flushPromises()
expect(wrapper.find('button[aria-label="修改重量 1"]').exists()).toBe(false)
await openWeightDialog(wrapper)
expect(dialogWeightInput().attributes('disabled')).toBeDefined()
expect(wrapper.getComponent(AppDialog).props('showConfirm')).toBe(false)
expect(confirmWeightResult).not.toHaveBeenCalled()
})
it('allows cancelling and reopening correction, and preserves the draft for retry after a save fails', async () => {
vi.mocked(getWeightWorkspace).mockResolvedValue(confirmedReviewWorkspace)
vi.mocked(confirmWeightResult)
.mockRejectedValueOnce(new Error('重量写入失败'))
.mockResolvedValueOnce({ ...confirmedReviewWorkspace.weightResults[0], grossWeightKg: 125, netWeightKg: 113, version: 2 })
const wrapper = mountPanel(allWeightPermissions, { reviewMode: true, correctionMode: true, tableMode: true })
await flushPromises()
const correctionButton = wrapper.get('button[aria-label="修改重量 1"]')
await correctionButton.trigger('click')
await flushPromises()
await dialogWeightInput().setValue('125')
await new DOMWrapper(document.body.querySelector<HTMLTextAreaElement>('#weight-correction-reason')!).setValue('复核磅秤读数')
const cancelButton = [...document.body.querySelectorAll<HTMLButtonElement>('.el-dialog__footer button')].find(button => button.textContent?.trim() === '取消')!
await new DOMWrapper(cancelButton).trigger('click')
await flushPromises()
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
expect(confirmWeightResult).not.toHaveBeenCalled()
await correctionButton.trigger('click')
await flushPromises()
expect(Number(dialogWeightInput().element.value)).toBe(125)
await dialogConfirmButton('保存重量修正').trigger('click')
await flushPromises()
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true)
expect(Number(dialogWeightInput().element.value)).toBe(125)
expect(dialogConfirmButton('保存重量修正').attributes('disabled')).toBeUndefined()
expect(wrapper.emitted('changed')).toBeUndefined()
await dialogConfirmButton('保存重量修正').trigger('click')
await flushPromises()
expect(confirmWeightResult).toHaveBeenCalledTimes(2)
expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(false)
expect(wrapper.emitted('changed')).toHaveLength(1)
})
it('closes the confirmation dialog immediately after a successful confirmation', async () => {
const wrapper = mountPanel()
await flushPromises()
@@ -504,9 +504,8 @@ defineExpose({ reload: loadWorkspace })
<div class="weight-summary"><span>实测毛重</span><strong>{{ formatWeight(workspace.actualGrossWeightKg) }}</strong></div>
<div class="weight-summary"><span>货物重量</span><strong>{{ formatWeight(workspace.actualNetWeightKg) }}</strong></div>
</div>
<div class="pallet-picker">
<div v-if="canEdit && !reviewMode" class="pallet-picker">
<el-select-v2
v-if="canEdit && !reviewMode"
v-model="selectedPalletId"
:options="virtualPalletOptions"
aria-label="选择称重托盘"
@@ -582,9 +581,14 @@ defineExpose({ reload: loadWorkspace })
<div v-else-if="tableMode" class="weight-table-wrap">
<div class="workbench-evidence"><table class="weight-data-table">
<thead><tr><th>#</th><th>称重图片</th><th>托盘编码</th><th>托盘类型</th><th>库位</th><th>实测毛重(KG)</th><th>服务端确认毛重(KG)</th><th>托盘皮重(KG)</th><th>货物重量(KG)</th><th>操作</th></tr></thead>
<thead><tr><th>操作</th><th>#</th><th>称重图片</th><th>托盘编码</th><th>托盘类型</th><th>库位</th><th>实测毛重(KG)</th><th>服务端确认毛重(KG)</th><th>托盘皮重(KG)</th><th>货物重量(KG)</th></tr></thead>
<tbody>
<tr v-for="(row, index) in rows" :key="row.id">
<td>
<el-button v-if="reviewCorrectionEnabled && row.recognitionStatus === 'CONFIRMED' && canEdit && canConfirm" link type="primary" :aria-label="`修改重量 ${index + 1}`" :disabled="Boolean(activeAction)" @click="openPreview(row)">修改重量</el-button>
<el-tooltip v-else-if="canEdit && editable && !reviewMode" content="删除图片" placement="top"><el-button text type="danger" :aria-label="`删除称重图片 ${index + 1}`" :loading="isActionLoading('delete', row.id)" :disabled="Boolean(activeAction)" @click="removeResult(row)"><Trash2 :size="16" /></el-button></el-tooltip>
<span v-else>—</span>
</td>
<td>{{ index + 1 }}</td>
<td><WeightImageThumbnail :customer-id="customerId" :inbound-id="inboundId" :file-id="row.fileId" :alt="`称重图片 ${index + 1}`" @open="openPreview(row)" /></td>
<td>{{ row.palletCode || '—' }}</td>
@@ -594,7 +598,6 @@ defineExpose({ reload: loadWorkspace })
<td>{{ formatWeight(row.grossWeightKg) }}</td>
<td>{{ formatWeight(row.palletTareWeightKg) }}</td>
<td class="weight-net-value">{{ formatWeight(row.netWeightKg) }}</td>
<td><el-tooltip v-if="canEdit && editable && !reviewMode" content="删除图片" placement="top"><el-button text type="danger" :aria-label="`删除称重图片 ${index + 1}`" :loading="isActionLoading('delete', row.id)" :disabled="Boolean(activeAction)" @click="removeResult(row)"><Trash2 :size="16" /></el-button></el-tooltip><span v-else>—</span></td>
</tr>
</tbody>
</table></div>