This commit is contained in:
2026-09-17 16:34:36 +08:00
parent 9c9cb91593
commit b6a839a3c6
5 changed files with 43 additions and 4 deletions
@@ -20,7 +20,7 @@ export interface OutboundDetail { outbound: OutboundRow; pallets?: OutboundPalle
export interface OutboundQuery { page: number; size: number; documentNo?: string; lxCode?: string; warehouseId?: string; status?: string; outboundType?: string }
export interface ReturnOutboundCandidate { returnNo?: string | null; overseasWarehouseId?: string | null; cartonMark?: string | null; fbaCartonMark?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; totalQuantity?: number | null; status?: string | null }
export interface OneClickOutboundCandidate { inventoryPositionId: string; orderNo?: string | null; customerId?: string | null; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null; palletNo?: string | null; inboundCode?: string | null; skuCode?: string | null; productName?: string | null; availablePackages: number }
export interface WarehouseTransferOutboundCandidate { customerId: string; customerName?: string | null; transferNo: string; overseasWarehouseId: string; transferStatus: string; needRelabel: boolean; relabelNo?: string | null; packageCount: number; grossWeightKg?: number | null; volumeCbm?: number | null }
export interface WarehouseTransferOutboundCandidate { customerId: string; customerName?: string | null; transferNo: string; overseasWarehouseId: string; transferStatus: string; needRelabel: boolean; relabelNo?: string | null; destinationType?: 'FBA' | 'PRIVATE' | null; destinationWarehouseCode?: string | null; privateDestinationAddress?: string | null; privateDestinationPostalCode?: string | null; packageCount: number; grossWeightKg?: number | null; volumeCbm?: number | null }
export interface WarehouseTransferDestinationPayload { transfers: Array<{ customerId: string; transferNo: string }>; destinationType: 'FBA' | 'PRIVATE'; destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string; privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string; idempotencyKey: string }
export async function listOutbounds(query: OutboundQuery) {
const response = await overseasHttp.get<ApiEnvelope<OverseasPagePayload<OutboundRow>>, ApiEnvelope<OverseasPagePayload<OutboundRow>>>('/platform/v1/last-leg/outbound-orders', { params: query })
@@ -371,7 +371,7 @@ watch([warehouseTab, () => query.orderType], () => {
<OperationErrorToast :details="mutationError" />
<AppDialog class="admin-density-dialog admin-density" :model-value="modelValue" title="智能排车" width="min(1800px, calc(100vw - 40px))" @update:model-value="close($event)">
<section class="dispatch-query" aria-label="智能排车筛选">
<el-input v-model="query.orderNo" clearable :placeholder="query.sourceStage === 'RELABEL' ? '换标单号' : '订单号 / 换标单号'" @keyup.enter="load" />
<el-input v-model="query.orderNo" clearable :placeholder="query.sourceStage === 'RELABEL' ? '换标单号 / 原入仓码' : '订单号 / 换标单号 / 原入仓码'" @keyup.enter="load" />
<el-select v-model="query.orderType" clearable placeholder="全部订单类型"><el-option v-for="option in orderTypeOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select>
<el-select v-model="query.sourceStage" clearable placeholder="全部来源"><el-option label="已入库 / 待出库换标" value="RECEIVED" /><el-option label="待出库换标单" value="RELABEL" /><el-option label="计划托盘" value="PLAN" /></el-select>
<el-input v-model="query.fbaWarehouseCode" clearable placeholder="FBA仓库" @keyup.enter="load" />
@@ -87,4 +87,19 @@ describe('OutboundsView permissions and navigation', () => {
expect(api).toContain('/platform/v1/last-leg/warehouse-transfers/bind-destination')
expect(source).not.toContain('await createWarehouseTransferOutbound')
})
it('auto-selects the sole warehouse-transfer candidate and shows destination state', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/OutboundsView.vue'), 'utf8')
const api = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/api/outbounds.ts'), 'utf8')
expect(source).toContain("if (transferCandidates.value.length === 1) selectedTransferPalletIds.value = [transferSelectionKey(transferCandidates.value[0])]")
expect(source).toContain("transferCandidates.value.length === 1 ? [transferCandidates.value[0]] : []")
expect(source).toContain('label="主单目的去向"')
expect(source).toContain("['私人仓', row.privateDestinationAddress?.trim(), row.privateDestinationPostalCode?.trim()]")
expect(source).toContain('`FBA · ${row.destinationWarehouseCode.trim()}`')
expect(source).toContain("return '未设置'")
expect(api).toContain('destinationWarehouseCode?: string | null')
expect(api).toContain("destinationType?: 'FBA' | 'PRIVATE' | null")
expect(api).toContain('privateDestinationAddress?: string | null')
expect(api).toContain('privateDestinationPostalCode?: string | null')
})
})
@@ -116,9 +116,31 @@ async function loadDropshipCandidates() { if (!selectedWarehouseId.value) { drop
async function changeDropshipWarehouse() { selectedDropshipIds.value = []; await loadDropshipCandidates() }
async function selectCustomer() { selectedPositionId.value = ''; selectedDropshipIds.value = []; selectedReturnNo.value = ''; selectedReturnMarks.value = []; selectedReturnSkus.value = []; returnCandidates.value = []; returnStockRows.value = []; returnCartonRows.value = []; returnCartonDetails.value = {}; dropshipCandidates.value = []; dropshipPage.value = 1; if (outboundType.value === 'RETURN_OUTBOUND') { if (selectedCustomerId.value && selectedWarehouseId.value) await loadReturnCandidates(); return }; await loadDropshipCandidates() }
async function changeOutboundType() { selectedReturnNo.value = ''; selectedReturnMarks.value = []; selectedReturnSkus.value = []; returnCandidates.value = []; returnStockRows.value = []; returnCartonRows.value = []; returnCartonDetails.value = {}; selectedDropshipIds.value = []; selectedTransferPalletIds.value = []; dropshipCandidates.value = []; transferCandidates.value = []; if (outboundType.value === 'ONE_CLICK_DROPSHIP') await loadDropshipCandidates(); if (outboundType.value === 'WAREHOUSE_TRANSFER_OUTBOUND') await loadTransferCandidates() }
async function loadTransferCandidates() { inventoryLoading.value = true; try { transferCandidates.value = await listWarehouseTransferOutboundCandidates({ customerId: selectedCustomerId.value || undefined, warehouseId: selectedWarehouseId.value || undefined }) } catch (cause) { transferCandidates.value = []; ElMessage.error(getApiErrorDetails(cause, '无目的地仓储转运单加载失败').message) } finally { inventoryLoading.value = false } }
async function loadTransferCandidates() {
inventoryLoading.value = true
try {
transferCandidates.value = await listWarehouseTransferOutboundCandidates({ customerId: selectedCustomerId.value || undefined, warehouseId: selectedWarehouseId.value || undefined })
const candidateKeys = new Set(transferCandidates.value.map(transferSelectionKey))
selectedTransferPalletIds.value = selectedTransferPalletIds.value.filter(key => candidateKeys.has(key))
if (transferCandidates.value.length === 1) selectedTransferPalletIds.value = [transferSelectionKey(transferCandidates.value[0])]
} catch (cause) {
transferCandidates.value = []
selectedTransferPalletIds.value = []
ElMessage.error(getApiErrorDetails(cause, '无目的地仓储转运单加载失败').message)
} finally { inventoryLoading.value = false }
}
function transferSelectionKey(row: WarehouseTransferOutboundCandidate) { return `${row.customerId}:${row.transferNo}` }
function toggleTransferPallet(row: WarehouseTransferOutboundCandidate, checked: boolean | string | number) { const key = transferSelectionKey(row); selectedTransferPalletIds.value = checked ? [...new Set([...selectedTransferPalletIds.value, key])] : selectedTransferPalletIds.value.filter(id => id !== key) }
function transferDestinationLabel(row: WarehouseTransferOutboundCandidate) {
if (row.destinationType === 'PRIVATE' || row.privateDestinationAddress?.trim()) {
return ['私人仓', row.privateDestinationAddress?.trim(), row.privateDestinationPostalCode?.trim()].filter(Boolean).join(' · ')
}
if (row.destinationWarehouseCode?.trim()) return `FBA · ${row.destinationWarehouseCode.trim()}`
return '未设置'
}
function transferHasDestination(row: WarehouseTransferOutboundCandidate) {
return Boolean(row.destinationWarehouseCode?.trim() || row.privateDestinationAddress?.trim())
}
async function loadReturnCandidates() { if (!selectedCustomerId.value || !selectedWarehouseId.value) return; inventoryLoading.value = true; try { const [candidates, stock, cartons] = await Promise.all([listReturnOutboundCandidates(selectedCustomerId.value, selectedWarehouseId.value), listReturnStock({ page: 1, size: 200, customerId: selectedCustomerId.value, overseasWarehouseId: selectedWarehouseId.value, status: 'AVAILABLE' }), listReturnCartons({ page: 1, size: 200, customerId: selectedCustomerId.value, overseasWarehouseId: selectedWarehouseId.value, status: 'AVAILABLE' })]); returnCandidates.value = candidates; returnStockRows.value = stock.records; returnCartonRows.value = cartons.records; const details = await Promise.all(cartons.records.map(async (row) => [row.cartonMark, await getReturnCarton(row.cartonMark)] as const)); returnCartonDetails.value = Object.fromEntries(details); selectedReturnSkus.value = []; selectedReturnMarks.value = [] } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '客户库存加载失败').message) } finally { inventoryLoading.value = false } }
function dropshipCandidateKey(item: OneClickOutboundCandidate) { return `${item.inventoryPositionId}:${item.skuCode || ''}` }
function isDropshipSelected(item: OneClickOutboundCandidate) { return selectedDropshipIds.value.includes(dropshipCandidateKey(item)) }
@@ -142,7 +164,7 @@ async function submit() {
saving.value = true
try {
if (outboundType.value === 'WAREHOUSE_TRANSFER_OUTBOUND') {
const selected = selectedTransferRows.value
const selected = selectedTransferRows.value.length ? selectedTransferRows.value : transferCandidates.value.length === 1 ? [transferCandidates.value[0]] : []
if (!selected.length) { ElMessage.warning('请至少选择一个无目的地的仓储转运单'); return }
const destination = fbaWarehouseOptions.value.find(item => item.value === selectedDestinationWarehouseId.value)
if (destinationType.value === 'FBA' && !destination) { ElMessage.warning('请选择FBA目的仓'); return }
@@ -247,6 +269,7 @@ onMounted(load)
<el-table-column prop="packageCount" label="入库箱数" width="100" align="right" />
<el-table-column prop="volumeCbm" label="总体积(m³)" width="120" align="right" />
<el-table-column label="换标" width="110"><template #default="{ row }">{{ row.needRelabel ? `已换标${row.relabelNo ? ` · ${row.relabelNo}` : ''}` : '原箱出库' }}</template></el-table-column>
<el-table-column label="主单目的去向" min-width="240" show-overflow-tooltip><template #default="{ row }"><StatusBadge :status="transferHasDestination(row as WarehouseTransferOutboundCandidate) ? 'READY' : 'PENDING'" :label="transferDestinationLabel(row as WarehouseTransferOutboundCandidate)" /></template></el-table-column>
</el-table>
<div v-if="!inventoryLoading && !transferCandidates.length" class="form-hint">暂无已入库且尚无目的地的仓储转运单</div>
</el-form-item>
@@ -261,6 +261,7 @@ describe('overseas warehouse first-batch prototype contract', () => {
expect(dialog).not.toContain('customerId:')
expect(dialog).toContain("planIds: query.sourceStage !== 'RELABEL' && planIds.value.length ? planIds.value.join(',') : undefined")
expect(dialog).toContain('v-model="query.fbaWarehouseCode" clearable')
expect(dialog).toContain("订单号 / 换标单号 / 原入仓码")
expect(dialog).toContain('label="私人仓"')
expect(dialog).toContain('item.deliveryAddress, item.postalCode')
expect(dialog).not.toContain(':model-value="fbaWarehouseCode" readonly')