优化
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import ElementPlus, { ElCheckbox, ElInput, ElPagination, ElSelect } from 'element-plus'
|
||||
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import OutboundsView from './OutboundsView.vue'
|
||||
|
||||
const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn() }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
|
||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [] }) }))
|
||||
vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: api.warehouses }))
|
||||
vi.mock('../api/outbounds', () => ({
|
||||
listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }),
|
||||
listOneClickOutboundCandidates: api.candidates,
|
||||
createOneClickDropshipOutbound: api.create,
|
||||
cancelOutbound: vi.fn(), createReturnOutbound: vi.fn(), listReturnOutboundCandidates: vi.fn(),
|
||||
}))
|
||||
|
||||
const fixtures = Array.from({ length: 7 }, (_, index) => ({
|
||||
inventoryPositionId: `position-${index}`, orderNo: `ORDER-${index}`, customerId: 'customer-a',
|
||||
customerName: '客户甲', overseasWarehouseId: 'warehouse-a', palletNo: `PALLET-${index}`,
|
||||
inboundCode: `IN-${index}`, skuCode: index < 6 ? 'MATCH' : 'OTHER', productName: '测试品名', availablePackages: index + 1,
|
||||
}))
|
||||
let wrapper: VueWrapper
|
||||
async function clickButton(text: string) {
|
||||
const button = wrapper.findAll('button').find(item => item.text().includes(text))
|
||||
expect(button, text).toBeDefined()
|
||||
await button!.trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
async function openCreate() {
|
||||
wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus], stubs: {
|
||||
AppDialog: { props: ['modelValue'], emits: ['confirm'], template: '<section v-if="modelValue"><slot /><button data-test="submit-outbound" @click="$emit(\'confirm\')">提交出库</button></section>' },
|
||||
} } })
|
||||
await flushPromises()
|
||||
await clickButton('新建出库单')
|
||||
}
|
||||
function candidatePagination() { return wrapper.findAllComponents(ElPagination).find(item => item.props('total') === 7)! }
|
||||
function pageCheckbox() { return wrapper.findAllComponents(ElCheckbox).find(item => item.props('ariaLabel') === '全选本页订单')! }
|
||||
|
||||
describe('outbound candidate search and selection', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
|
||||
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined })
|
||||
api.candidates.mockReset().mockResolvedValue(fixtures.map(item => ({ ...item })))
|
||||
api.create.mockReset().mockResolvedValue({})
|
||||
api.warehouses.mockReset().mockResolvedValue({ records: [
|
||||
{ id: 'warehouse-a', warehouseName: '洛杉矶仓', warehouseCode: 'US-LAX-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' },
|
||||
{ id: 'warehouse-b', warehouseName: '纽约仓', warehouseCode: 'US-NY-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' },
|
||||
] })
|
||||
})
|
||||
afterEach(() => { wrapper?.unmount(); vi.unstubAllGlobals() })
|
||||
|
||||
it('selects and deselects the current page while preserving other pages, then submits all selected quantities', async () => {
|
||||
await openCreate()
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('已选择 5 条')
|
||||
candidatePagination().vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
expect(pageCheckbox().props('modelValue')).toBe(false)
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('已选择 7 条')
|
||||
pageCheckbox().vm.$emit('change', false)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('已选择 5 条')
|
||||
await wrapper.get('[data-test="submit-outbound"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.create).toHaveBeenCalledWith('customer-a', expect.objectContaining({
|
||||
items: fixtures.slice(0, 5).map(item => ({ inventoryPositionId: item.inventoryPositionId, packages: item.availablePackages, sku: item.skuCode, cartonMarks: [] })),
|
||||
}))
|
||||
})
|
||||
|
||||
it('searches on query, selects every filtered page, shows partial page selection and clears the selection', async () => {
|
||||
await openCreate()
|
||||
const query = wrapper.findAllComponents(ElInput).find(item => item.attributes('aria-label') === '搜索可出库订单')!
|
||||
query.vm.$emit('update:modelValue', 'MATCH')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('全选筛选结果(7)')
|
||||
await query.trigger('keyup', { key: 'Enter' })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('全选筛选结果(6)')
|
||||
await clickButton('全选筛选结果')
|
||||
expect(wrapper.text()).toContain('已选择 6 条')
|
||||
await clickButton('重置')
|
||||
candidatePagination().vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
expect(pageCheckbox().props('indeterminate')).toBe(true)
|
||||
await clickButton('清空选择')
|
||||
expect(wrapper.text()).toContain('已选择 0 条')
|
||||
})
|
||||
|
||||
it('resets the page after page-size changes and clears selection when changing searchable warehouses', async () => {
|
||||
await openCreate()
|
||||
await clickButton('全选筛选结果')
|
||||
const pagination = candidatePagination()
|
||||
pagination.vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
pagination.vm.$emit('update:page-size', 20)
|
||||
pagination.vm.$emit('size-change', 20)
|
||||
await flushPromises()
|
||||
expect(pagination.props('currentPage')).toBe(1)
|
||||
expect(wrapper.text()).toContain('第 1 / 1 页')
|
||||
expect(wrapper.text()).toContain('已选择 7 条')
|
||||
const warehouse = wrapper.findAllComponents(ElSelect).find(item => item.attributes('aria-label') === '搜索海外仓')!
|
||||
expect(warehouse.props('filterable')).toBe(true)
|
||||
warehouse.vm.$emit('update:modelValue', 'warehouse-b')
|
||||
warehouse.vm.$emit('change', 'warehouse-b')
|
||||
await flushPromises()
|
||||
expect(api.candidates).toHaveBeenLastCalledWith(undefined, { warehouseId: 'warehouse-b' })
|
||||
expect(wrapper.text()).toContain('已选择 0 条')
|
||||
})
|
||||
|
||||
it('warns about a multi-customer bulk selection and preserves the submit restriction', async () => {
|
||||
api.candidates.mockResolvedValue([{ ...fixtures[0] }, { ...fixtures[1], customerId: 'customer-b', customerName: '客户乙' }])
|
||||
await openCreate()
|
||||
await clickButton('全选筛选结果')
|
||||
expect(wrapper.text()).toContain('当前选择包含多个客户')
|
||||
await wrapper.get('[data-test="submit-outbound"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Plus } from '@lucide/vue'
|
||||
import { Plus, Search } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
@@ -48,7 +48,7 @@ const customerOptions = computed(() => {
|
||||
return [...customers].map(([value, label]) => ({ value, label }))
|
||||
})
|
||||
const availablePositionOptions = computed(() => positionOptions.value.filter(({ balance }) => balance.customerId === selectedCustomerId.value))
|
||||
const warehouseOptions = computed(() => warehouseOptionsSource.value.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'OVERSEAS').map((item) => ({ value: String(item.id), label: item.warehouseName || String(item.id) })))
|
||||
const warehouseOptions = computed(() => warehouseOptionsSource.value.filter((item) => item.status === 'ACTIVE' && item.warehouseType === 'OVERSEAS').map((item) => ({ value: String(item.id), label: [item.warehouseName || '仓库名称待补充', item.warehouseCode].filter(Boolean).join(' · ') })))
|
||||
const selectedReturnStock = computed(() => returnStockRows.value.filter((item) => Number(item.availableQuantity || 0) > 0))
|
||||
const selectedReturnCartons = computed(() => returnCartonRows.value.filter((item) => item.status === 'AVAILABLE'))
|
||||
const queryTokens = (value: string) => value.split(/[,,\n]+/).map((item) => item.replace(/\s+/g, '').toLowerCase()).filter(Boolean)
|
||||
@@ -64,6 +64,12 @@ const effectiveReturnMarks = computed(() => [...new Set(selectedReturnMarks.valu
|
||||
const returnMarksModel = computed({ get: () => selectedReturnMarks.value, set: (value: string[]) => { selectedReturnMarks.value = value } })
|
||||
const filteredDropshipCandidates = computed(() => dropshipCandidates.value.filter(item => !dropshipQuery.value.trim() || [item.orderNo, item.inboundCode, item.customerName, item.palletNo, item.skuCode, item.productName].join(' ').toLowerCase().includes(dropshipQuery.value.trim().toLowerCase())))
|
||||
const pagedDropshipCandidates = computed(() => filteredDropshipCandidates.value.slice((dropshipPage.value - 1) * dropshipPageSize.value, dropshipPage.value * dropshipPageSize.value))
|
||||
const selectedDropshipKeys = computed(() => new Set(selectedDropshipIds.value))
|
||||
const selectedPageCount = computed(() => pagedDropshipCandidates.value.filter(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))).length)
|
||||
const allPageSelected = computed(() => pagedDropshipCandidates.value.length > 0 && selectedPageCount.value === pagedDropshipCandidates.value.length)
|
||||
const somePageSelected = computed(() => selectedPageCount.value > 0 && !allPageSelected.value)
|
||||
const allFilteredSelected = computed(() => filteredDropshipCandidates.value.length > 0 && filteredDropshipCandidates.value.every(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))))
|
||||
const selectedDropshipCustomerCount = computed(() => new Set(dropshipCandidates.value.filter(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))).map(item => item.customerId).filter(Boolean)).size)
|
||||
const oneClickMarks = computed(() => typeof cartonMarks.value === 'string' ? cartonMarks.value.split(/[,\n,]+/).map(value => value.trim()).filter(Boolean) : [])
|
||||
const columns: DataTableColumn[] = [{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' }, { key: 'outboundNo', prop: 'outboundNo', label: '出库单号', minWidth: 180 }, { key: 'outboundType', prop: 'outboundType', label: '出库类型', width: 150, slotName: 'outboundType' }, { key: 'lxCodes', prop: 'lxCodes', label: 'LX代码', minWidth: 180 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '关联排车单', minWidth: 160 }, { key: 'status', prop: 'status', label: '状态', width: 130, slotName: 'status' }, { key: 'palletCount', prop: 'palletCount', label: '托盘数', width: 100 }, { key: 'packageCount', prop: 'packageCount', label: '箱数', width: 100 }, { key: 'createdAt', prop: 'createdAt', label: '创建时间', minWidth: 180 }]
|
||||
const fields: QueryField[] = [{ key: 'documentNo', type: 'input', label: '出库单号', placeholder: '请输入出库单号', width: 220 }, { key: 'lxCode', type: 'input', label: 'LX代码', placeholder: '请输入领星入库单代码', width: 220 }, { key: 'warehouseId', type: 'input', label: '目的仓', placeholder: '请输入目的仓', width: 220 }, { key: 'status', type: 'select', label: '状态', placeholder: '全部状态', width: 160, options: [{ label: '全部', value: '' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }] }, { key: 'outboundType', type: 'select', label: '出库类型', placeholder: '全部出库类型', width: 170, options: [{ label: '全部', value: '' }, { label: '一件代发出库', value: 'ONE_CLICK_DROPSHIP' }, { label: '退货出库', value: 'RETURN_OUTBOUND' }, { label: '派送出库', value: 'DISPATCH_DELIVERY' }] }]
|
||||
@@ -97,6 +103,12 @@ async function loadReturnCandidates() { if (!selectedCustomerId.value || !select
|
||||
function dropshipCandidateKey(item: OneClickOutboundCandidate) { return `${item.inventoryPositionId}:${item.skuCode || ''}` }
|
||||
function isDropshipSelected(item: OneClickOutboundCandidate) { return selectedDropshipIds.value.includes(dropshipCandidateKey(item)) }
|
||||
function toggleDropshipCandidate(item: OneClickOutboundCandidate, checked: boolean | string | number) { const key = dropshipCandidateKey(item); selectedDropshipIds.value = checked ? [...new Set([...selectedDropshipIds.value, key])] : selectedDropshipIds.value.filter(value => value !== key) }
|
||||
function selectDropshipRows(items: OneClickOutboundCandidate[], checked: boolean | string | number) {
|
||||
const keys = new Set(items.map(dropshipCandidateKey))
|
||||
selectedDropshipIds.value = checked
|
||||
? [...new Set([...selectedDropshipIds.value, ...keys])]
|
||||
: selectedDropshipIds.value.filter(key => !keys.has(key))
|
||||
}
|
||||
function searchDropshipCandidates() { dropshipQuery.value = dropshipQueryInput.value.trim(); dropshipPage.value = 1 }
|
||||
function resetDropshipCandidates() { dropshipQueryInput.value = ''; dropshipQuery.value = ''; dropshipPage.value = 1 }
|
||||
function normalizeBatchQuery(value: string) { return value.split(/[,,\n]+/).map((item) => item.replace(/\s+/g, '')).filter(Boolean).join(',') }
|
||||
@@ -139,19 +151,25 @@ onMounted(load)
|
||||
</el-form-item>
|
||||
<template v-if="outboundType !== 'RETURN_OUTBOUND'">
|
||||
<el-form-item label="海外仓" required>
|
||||
<el-select v-model="selectedWarehouseId" filterable placeholder="请选择海外仓" @change="changeDropshipWarehouse">
|
||||
<el-select v-model="selectedWarehouseId" filterable clearable default-first-option aria-label="搜索海外仓" placeholder="输入仓库名称或编码搜索" @change="changeDropshipWarehouse">
|
||||
<el-option v-for="item in warehouseOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="已完成入库的一件代发订单">
|
||||
<div class="candidate-query">
|
||||
<el-input v-model="dropshipQueryInput" clearable placeholder="按订单号、入库单号、客户、托盘号、SKU或品名筛选" @keyup.enter="searchDropshipCandidates" />
|
||||
<el-input v-model="dropshipQueryInput" :prefix-icon="Search" clearable aria-label="搜索可出库订单" placeholder="按订单号、入库单号、客户、托盘号、SKU或品名筛选" @keyup.enter="searchDropshipCandidates" />
|
||||
<el-button type="primary" :loading="inventoryLoading" @click="searchDropshipCandidates">查询</el-button>
|
||||
<el-button @click="resetDropshipCandidates">重置</el-button>
|
||||
</div>
|
||||
<el-table v-loading="inventoryLoading" :data="pagedDropshipCandidates" border stripe class="dropship-candidate-table">
|
||||
<div class="candidate-selection" aria-label="订单批量选择">
|
||||
<el-button :disabled="inventoryLoading || !filteredDropshipCandidates.length || allFilteredSelected" @click="selectDropshipRows(filteredDropshipCandidates, true)">全选筛选结果({{ filteredDropshipCandidates.length }})</el-button>
|
||||
<el-button :disabled="!selectedDropshipIds.length" @click="selectedDropshipIds = []">清空选择</el-button>
|
||||
<span aria-live="polite">已选择 {{ selectedDropshipIds.length }} 条,翻页保留选择</span>
|
||||
</div>
|
||||
<el-table v-loading="inventoryLoading" :data="pagedDropshipCandidates" border stripe class="dropship-candidate-table" max-height="360">
|
||||
<el-table-column label="选择" width="68" align="center">
|
||||
<template #default="{ row }"><el-checkbox :model-value="isDropshipSelected(row as OneClickOutboundCandidate)" @change="toggleDropshipCandidate(row as OneClickOutboundCandidate, $event)" /></template>
|
||||
<template #header><el-checkbox aria-label="全选本页订单" :model-value="allPageSelected" :indeterminate="somePageSelected" :disabled="inventoryLoading || !pagedDropshipCandidates.length" @change="selectDropshipRows(pagedDropshipCandidates, $event)" /></template>
|
||||
<template #default="{ row }"><el-checkbox :aria-label="`选择 ${row.orderNo || '订单'} / ${row.palletNo || '托盘'} / ${row.skuCode || 'SKU'}`" :model-value="isDropshipSelected(row as OneClickOutboundCandidate)" @change="toggleDropshipCandidate(row as OneClickOutboundCandidate, $event)" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customerName" label="客户" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="orderNo" label="尾程订单号" min-width="170" show-overflow-tooltip />
|
||||
@@ -163,10 +181,11 @@ onMounted(load)
|
||||
</el-table>
|
||||
<div v-if="!inventoryLoading && !filteredDropshipCandidates.length" class="form-hint">暂无可直接出库的已完成入库订单</div>
|
||||
<div v-if="filteredDropshipCandidates.length" class="candidate-pagination">
|
||||
<span>已选择 {{ selectedDropshipIds.length }} 条</span>
|
||||
<el-pagination v-model:current-page="dropshipPage" v-model:page-size="dropshipPageSize" :page-sizes="[5, 10, 20]" :total="filteredDropshipCandidates.length" layout="total, sizes, prev, pager, next" />
|
||||
<span>第 {{ dropshipPage }} / {{ Math.max(1, Math.ceil(filteredDropshipCandidates.length / dropshipPageSize)) }} 页</span>
|
||||
<el-pagination v-model:current-page="dropshipPage" v-model:page-size="dropshipPageSize" :page-sizes="[5, 10, 20, 50]" :pager-count="5" :total="filteredDropshipCandidates.length" layout="total, sizes, prev, pager, next, jumper" @size-change="dropshipPage = 1" />
|
||||
</div>
|
||||
<el-alert title="一件代发按托盘与SKU选择;数量默认使用所选行全部可用箱数,同一张出库单只能选择同一海外仓。" type="info" :closable="false" />
|
||||
<el-alert v-if="selectedDropshipCustomerCount > 1" title="当前选择包含多个客户,请按客户搜索后选择;一张出库单只能属于同一客户。" type="warning" :closable="false" />
|
||||
<el-alert v-else title="一件代发按托盘与SKU选择;数量默认使用所选行全部可用箱数,同一张出库单只能选择同一客户、同一海外仓。" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -195,7 +214,7 @@ onMounted(load)
|
||||
|
||||
<style scoped>
|
||||
.create-outbound-form { padding: 0 var(--admin-space-1); }
|
||||
.create-outbound-form :deep(.el-input), .create-outbound-form :deep(.el-select), .create-outbound-form :deep(.el-input-number) { width: 100%; }
|
||||
.create-outbound-form :deep(.el-form-item__content > .el-input), .create-outbound-form :deep(.el-form-item__content > .el-select), .create-outbound-form :deep(.el-form-item__content > .el-input-number) { width: 100%; }
|
||||
.form-hint { margin-top: var(--admin-space-2); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.candidate-list { display: grid; gap: var(--admin-space-3); max-height: 300px; overflow: auto; padding: var(--admin-space-3) 0; }
|
||||
.candidate-list :deep(.el-checkbox) { margin-right: 0; }
|
||||
@@ -204,8 +223,16 @@ onMounted(load)
|
||||
.candidate-query { display: flex; width: 100%; gap: var(--admin-space-4); }
|
||||
.candidate-query :deep(.el-input) { flex: 1; }
|
||||
.candidate-query :deep(.el-button) { flex: 0 0 auto; min-width: 78px; margin-left: 0; }
|
||||
.candidate-selection { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); width: 100%; margin-top: var(--admin-space-3); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.candidate-selection :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.dropship-candidate-table { margin-top: var(--admin-space-5); width: 100%; }
|
||||
.candidate-pagination { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: var(--admin-space-5); padding: var(--admin-space-5) 0; color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.candidate-pagination, .return-pagination { flex-wrap: wrap; }
|
||||
.candidate-pagination > span, .return-pagination > span { flex-shrink: 0; white-space: nowrap; }
|
||||
.candidate-pagination :deep(.el-pagination), .return-pagination :deep(.el-pagination) { min-width: 0; max-width: 100%; flex-wrap: wrap; gap: var(--admin-space-2); justify-content: flex-end; }
|
||||
.candidate-pagination :deep(.el-pagination__sizes), .return-pagination :deep(.el-pagination__sizes) { flex: 0 0 128px; }
|
||||
.candidate-pagination :deep(.el-pagination__sizes .el-select), .return-pagination :deep(.el-pagination__sizes .el-select) { width: 128px; }
|
||||
.candidate-pagination :deep(.el-pagination__jump .el-input) { width: 56px; }
|
||||
.return-lists { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--admin-space-5); width: 100%; margin: var(--admin-space-1) 0 var(--admin-space-5); }
|
||||
.return-list-card { min-width: 0; padding: var(--admin-space-5); border: 1px solid var(--el-border-color-light); border-radius: var(--admin-radius); background: var(--el-fill-color-extra-light); }
|
||||
.list-title { display: flex; align-items: baseline; justify-content: space-between; gap: var(--admin-space-5); margin-bottom: var(--admin-space-5); }
|
||||
|
||||
@@ -28,6 +28,19 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)")
|
||||
})
|
||||
|
||||
it('prevents duplicate confirm-and-send chains and reuses one attempt idempotency key set', () => {
|
||||
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
|
||||
|
||||
expect(sendFeeSource).toContain('if (confirmFeeSending.value) return')
|
||||
expect(sendFeeSource).toContain('const attempt = currentFeeConfirmAttempt(selectedWeightKg, selectedVolumeCbm, selectedUnitPrice)')
|
||||
expect(sendFeeSource).toContain('attempt.calculateKey')
|
||||
expect(sendFeeSource).toContain('attempt.adjustKey')
|
||||
expect(sendFeeSource).toContain('attempt.confirmKey')
|
||||
expect(sendFeeSource).toContain('attempt.sendKey')
|
||||
expect(sendFeeSource).toContain("if (snapshot.feeStatus === 'SENT')")
|
||||
expect(source).toContain('feeConfirmAttempt = null\n feeConfirmOpen.value = true')
|
||||
})
|
||||
|
||||
it('exposes the product main statuses including dropship palletizing', () => {
|
||||
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
|
||||
'待审核', '待测量数据', '待一件代发组托', '待分柜', '装柜中', '装柜完成', '派送完成',
|
||||
|
||||
@@ -248,6 +248,14 @@ const feeConfirmReferenceLoading = ref(false)
|
||||
const feeConfirmReference = ref<InboundFeeSnapshot | null>(null)
|
||||
const feeConfirmUnitPrice = ref<number | null>(null)
|
||||
const feeConfirmPriceReason = ref('')
|
||||
type FeeConfirmAttempt = {
|
||||
fingerprint: string
|
||||
calculateKey: string
|
||||
adjustKey: string
|
||||
confirmKey: string
|
||||
sendKey: string
|
||||
}
|
||||
let feeConfirmAttempt: FeeConfirmAttempt | null = null
|
||||
const feeConfirmPriceChanged = computed(() => {
|
||||
const reference = Number(feeConfirmReference.value?.unitPrice)
|
||||
const current = Number(feeConfirmUnitPrice.value)
|
||||
@@ -1643,6 +1651,7 @@ async function openFeeConfirmDialog() {
|
||||
feeConfirmReference.value = null
|
||||
feeConfirmUnitPrice.value = null
|
||||
feeConfirmPriceReason.value = ''
|
||||
feeConfirmAttempt = null
|
||||
feeConfirmOpen.value = true
|
||||
feeConfirmReferenceLoading.value = true
|
||||
try {
|
||||
@@ -1671,7 +1680,31 @@ async function openFeeConfirmDialog() {
|
||||
}
|
||||
}
|
||||
|
||||
function currentFeeConfirmAttempt(
|
||||
selectedWeightKg: number,
|
||||
selectedVolumeCbm: number,
|
||||
selectedUnitPrice: number,
|
||||
) {
|
||||
const fingerprint = JSON.stringify([
|
||||
selectedWeightKg,
|
||||
selectedVolumeCbm,
|
||||
selectedUnitPrice,
|
||||
feeConfirmPriceReason.value.trim(),
|
||||
])
|
||||
if (!feeConfirmAttempt || feeConfirmAttempt.fingerprint !== fingerprint) {
|
||||
feeConfirmAttempt = {
|
||||
fingerprint,
|
||||
calculateKey: createFeeActionIdempotencyKey('calculate'),
|
||||
adjustKey: createFeeAdjustmentIdempotencyKey(),
|
||||
confirmKey: createFeeActionIdempotencyKey('confirm'),
|
||||
sendKey: createFeeActionIdempotencyKey('send'),
|
||||
}
|
||||
}
|
||||
return feeConfirmAttempt
|
||||
}
|
||||
|
||||
async function executeConfirmAndSendFee() {
|
||||
if (confirmFeeSending.value) return
|
||||
const summary = detail.value?.summary
|
||||
if (!summary?.inboundId) return
|
||||
const selectedWeightKg = Number(feeConfirmWeight.value)
|
||||
@@ -1689,6 +1722,7 @@ async function executeConfirmAndSendFee() {
|
||||
ElMessage.warning('人工修改参考单价后,请填写至少 2 个字符的调整原因')
|
||||
return
|
||||
}
|
||||
const attempt = currentFeeConfirmAttempt(selectedWeightKg, selectedVolumeCbm, selectedUnitPrice)
|
||||
confirmFeeSending.value = true
|
||||
try {
|
||||
const tenantId = String(summary.tenantId)
|
||||
@@ -1698,7 +1732,7 @@ async function executeConfirmAndSendFee() {
|
||||
selectedVolumeCbm,
|
||||
weightSource: 'ACTUAL',
|
||||
volumeSource: 'ACTUAL',
|
||||
}, createFeeActionIdempotencyKey('calculate'))
|
||||
}, attempt.calculateKey)
|
||||
const calculatedUnitPrice = Number(snapshot.unitPrice)
|
||||
if (Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) {
|
||||
snapshot = await adjustPublishedInboundFee(tenantId, inboundId, {
|
||||
@@ -1708,15 +1742,18 @@ async function executeConfirmAndSendFee() {
|
||||
additionalAmount: Number(snapshot.additionalAmount || 0),
|
||||
reason: feeConfirmPriceReason.value.trim() || '推送前按已展示参考单价人工确认',
|
||||
toleranceConfirmed: true,
|
||||
}, createFeeAdjustmentIdempotencyKey())
|
||||
}, attempt.adjustKey)
|
||||
}
|
||||
snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm'))
|
||||
snapshot = await confirmInboundFee(tenantId, inboundId, attempt.confirmKey)
|
||||
if (snapshot.feeStatus === 'CONFIRMED') {
|
||||
const sent = await sendInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('send'))
|
||||
automaticFee.value = { feeStatus: sent.feeStatus, totalAmount: Number(sent.totalAmount || 0) }
|
||||
snapshot = await sendInboundFee(tenantId, inboundId, attempt.sendKey)
|
||||
}
|
||||
if (snapshot.feeStatus === 'SENT') {
|
||||
automaticFee.value = { feeStatus: snapshot.feeStatus, totalAmount: Number(snapshot.totalAmount || 0) }
|
||||
feePanelRevision.value += 1
|
||||
ElMessage.success('计费量和单价已确认并发送给客户')
|
||||
feeConfirmOpen.value = false
|
||||
feeConfirmAttempt = null
|
||||
await loadDetail(true)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user