优化
This commit is contained in:
@@ -15,7 +15,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { cancelOutbound, createOneClickDropshipOutbound, createReturnOutbound, listOneClickOutboundCandidates, listReturnOutboundCandidates, listOutbounds, type OneClickOutboundCandidate, type OutboundRow, type ReturnOutboundCandidate } from '../api/outbounds'
|
||||
import { bindWarehouseTransferDestination, cancelOutbound, createOneClickDropshipOutbound, createReturnOutbound, listOneClickOutboundCandidates, listReturnOutboundCandidates, listWarehouseTransferOutboundCandidates, listOutbounds, type OneClickOutboundCandidate, type OutboundRow, type ReturnOutboundCandidate, type WarehouseTransferOutboundCandidate } from '../api/outbounds'
|
||||
import { type InventoryBalanceDto, type InventoryPositionDto } from '../api/inventory'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
@@ -25,9 +25,15 @@ import { getReturnCarton, listReturnCartons, listReturnStock, type ReturnCartonD
|
||||
|
||||
const router = useRouter(); const access = useAccessPreviewStore()
|
||||
const rows = ref<OutboundRow[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const filters = ref<QueryModel>({ documentNo: '', lxCode: '', warehouseId: '', status: '', outboundType: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref<number | undefined>(); const cartonMarks = ref(''); const selectedReturnMarks = ref<string[]>([]); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref<Array<{ balance: InventoryBalanceDto; position: InventoryPositionDto }>>([]); const dropshipCandidates = ref<OneClickOutboundCandidate[]>([]); const selectedDropshipIds = ref<string[]>([]); const returnCandidates = ref<ReturnOutboundCandidate[]>([]); const selectedWarehouseId = ref(''); const selectedReturnNo = ref(''); const outboundType = ref<'ONE_CLICK_DROPSHIP' | 'RETURN_OUTBOUND'>('ONE_CLICK_DROPSHIP'); const inventoryLoading = ref(false); const dropshipQueryInput = ref(''); const dropshipQuery = ref(''); const dropshipPage = ref(1); const dropshipPageSize = ref(5)
|
||||
const filters = ref<QueryModel>({ documentNo: '', lxCode: '', warehouseId: '', status: '', outboundType: '' }); const createOpen = ref(false); const saving = ref(false); const recipient = ref(''); const remark = ref(''); const quantity = ref<number | undefined>(); const cartonMarks = ref(''); const selectedReturnMarks = ref<string[]>([]); const selectedCustomerId = ref(''); const selectedPositionId = ref(''); const positionOptions = ref<Array<{ balance: InventoryBalanceDto; position: InventoryPositionDto }>>([]); const dropshipCandidates = ref<OneClickOutboundCandidate[]>([]); const selectedDropshipIds = ref<string[]>([]); const returnCandidates = ref<ReturnOutboundCandidate[]>([]); const selectedWarehouseId = ref(''); const selectedReturnNo = ref(''); const outboundType = ref<'ONE_CLICK_DROPSHIP' | 'RETURN_OUTBOUND' | 'WAREHOUSE_TRANSFER_OUTBOUND'>('ONE_CLICK_DROPSHIP'); const inventoryLoading = ref(false); const dropshipQueryInput = ref(''); const dropshipQuery = ref(''); const dropshipPage = ref(1); const dropshipPageSize = ref(5)
|
||||
const tenantOptions = ref<PlatformTenantResource[]>([])
|
||||
const warehouseOptionsSource = ref<PlatformWarehouseSummaryResource[]>([])
|
||||
const fbaWarehouseOptionsSource = ref<PlatformWarehouseSummaryResource[]>([])
|
||||
const transferCandidates = ref<WarehouseTransferOutboundCandidate[]>([])
|
||||
const selectedTransferPalletIds = ref<string[]>([])
|
||||
const destinationType = ref<'FBA' | 'PRIVATE'>('FBA')
|
||||
const selectedDestinationWarehouseId = ref('')
|
||||
const deliveryAddress = ref(''); const postalCode = ref(''); const contactName = ref(''); const contactPhone = ref('')
|
||||
const returnStockRows = ref<ReturnStockRow[]>([])
|
||||
const returnCartonRows = ref<ReturnCartonRow[]>([])
|
||||
const selectedReturnSkus = ref<string[]>([])
|
||||
@@ -49,6 +55,14 @@ const customerOptions = computed(() => {
|
||||
})
|
||||
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 || '仓库名称待补充', item.warehouseCode].filter(Boolean).join(' · ') })))
|
||||
const fbaWarehouseOptions = computed(() => fbaWarehouseOptionsSource.value.filter(item => item.status === 'ACTIVE').map(item => ({ value: String(item.id), code: item.warehouseCode || '', name: item.warehouseName || '', label: [item.warehouseName || 'FBA仓名称待补充', item.warehouseCode].filter(Boolean).join(' · ') })))
|
||||
const selectedTransferRows = computed(() => transferCandidates.value.filter(item => selectedTransferPalletIds.value.includes(`${item.customerId}:${item.transferNo}`)))
|
||||
function transferCustomerName(row: WarehouseTransferOutboundCandidate) {
|
||||
const snapshotName = row.customerName?.trim()
|
||||
if (snapshotName) return snapshotName
|
||||
const customer = tenantOptions.value.find(item => String(item.id) === String(row.customerId))
|
||||
return customer?.legalName?.trim() || customer?.tenantName?.trim() || `客户 ${row.customerId}`
|
||||
}
|
||||
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)
|
||||
@@ -72,16 +86,17 @@ const allFilteredSelected = computed(() => filteredDropshipCandidates.value.leng
|
||||
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' }] }]
|
||||
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: 'WAREHOUSE_TRANSFER_OUTBOUND' }, { label: '一件代发出库', value: 'ONE_CLICK_DROPSHIP' }, { label: '退货出库', value: 'RETURN_OUTBOUND' }, { label: '派送出库', value: 'DISPATCH_DELIVERY' }] }]
|
||||
|
||||
async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const result = await listOutbounds({ page: page.value, size: pageSize.value, documentNo: String(filters.value.documentNo || '').trim() || undefined, lxCode: String(filters.value.lxCode || '').trim() || undefined, warehouseId: String(filters.value.warehouseId || '').trim() || undefined, status: String(filters.value.status || '').trim() || undefined, outboundType: String(filters.value.outboundType || '').trim() || undefined }); rows.value = result.records.map((row) => ({ ...row, createdAt: formatOverseasDateTime(row.createdAt) })); total.value = result.total } catch (cause) { error.value = getApiErrorDetails(cause, '出库单列表加载失败') } finally { loading.value = false } }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||||
async function loadInventoryOptions() {
|
||||
inventoryLoading.value = true
|
||||
try {
|
||||
const [tenants, warehouses] = await Promise.allSettled([
|
||||
const [tenants, warehouses, fbaWarehouses] = await Promise.allSettled([
|
||||
listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' }),
|
||||
listPlatformWarehouses({ page: 1, size: 200, warehouseType: 'OVERSEAS', status: 'ACTIVE' }),
|
||||
listPlatformWarehouses({ page: 1, size: 200, warehouseType: 'FBA', status: 'ACTIVE' }),
|
||||
])
|
||||
if (tenants.status === 'fulfilled') tenantOptions.value = tenants.value.records
|
||||
else ElMessage.error(getApiErrorDetails(tenants.reason, '客户列表加载失败').message)
|
||||
@@ -92,13 +107,18 @@ async function loadInventoryOptions() {
|
||||
selectedWarehouseId.value = String(lax?.id || warehouses.value.records[0]?.id || '')
|
||||
}
|
||||
} else ElMessage.error(getApiErrorDetails(warehouses.reason, '海外仓列表加载失败').message)
|
||||
if (fbaWarehouses.status === 'fulfilled') fbaWarehouseOptionsSource.value = fbaWarehouses.value.records
|
||||
else ElMessage.error(getApiErrorDetails(fbaWarehouses.reason, 'FBA目的仓加载失败').message)
|
||||
} finally { inventoryLoading.value = false }
|
||||
}
|
||||
async function openCreate() { createOpen.value = true; outboundType.value = 'ONE_CLICK_DROPSHIP'; selectedCustomerId.value = ''; selectedWarehouseId.value = ''; selectedDropshipIds.value = []; dropshipCandidates.value = []; dropshipQueryInput.value = ''; dropshipQuery.value = ''; dropshipPage.value = 1; returnSkuQueryInput.value = ''; returnSkuQuery.value = ''; returnCartonQueryInput.value = ''; returnCartonQuery.value = ''; returnSkuPage.value = 1; returnCartonPage.value = 1; await loadInventoryOptions(); await loadDropshipCandidates() }
|
||||
async function openCreate() { createOpen.value = true; outboundType.value = 'ONE_CLICK_DROPSHIP'; selectedCustomerId.value = ''; selectedWarehouseId.value = ''; selectedDropshipIds.value = []; selectedTransferPalletIds.value = []; transferCandidates.value = []; destinationType.value = 'FBA'; selectedDestinationWarehouseId.value = ''; deliveryAddress.value = ''; postalCode.value = ''; contactName.value = ''; contactPhone.value = ''; dropshipCandidates.value = []; dropshipQueryInput.value = ''; dropshipQuery.value = ''; dropshipPage.value = 1; returnSkuQueryInput.value = ''; returnSkuQuery.value = ''; returnCartonQueryInput.value = ''; returnCartonQuery.value = ''; returnSkuPage.value = 1; returnCartonPage.value = 1; await loadInventoryOptions(); await loadDropshipCandidates() }
|
||||
async function loadDropshipCandidates() { if (!selectedWarehouseId.value) { dropshipCandidates.value = []; return }; dropshipPage.value = 1; inventoryLoading.value = true; try { dropshipCandidates.value = await listOneClickOutboundCandidates(selectedCustomerId.value || undefined, { warehouseId: selectedWarehouseId.value }) } catch (cause) { dropshipCandidates.value = []; ElMessage.error(getApiErrorDetails(cause, '一件代发订单加载失败').message) } finally { inventoryLoading.value = false } }
|
||||
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 = []; dropshipCandidates.value = []; if (outboundType.value === 'ONE_CLICK_DROPSHIP') 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 } }
|
||||
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) }
|
||||
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)) }
|
||||
@@ -116,9 +136,37 @@ function searchReturnSkus() { returnSkuQueryInput.value = normalizeBatchQuery(re
|
||||
function searchReturnCartons() { returnCartonQueryInput.value = normalizeBatchQuery(returnCartonQueryInput.value); returnCartonQuery.value = returnCartonQueryInput.value; returnCartonPage.value = 1 }
|
||||
function toggleReturnSku(sku: string, checked: boolean | string | number) { selectedReturnSkus.value = checked ? [...new Set([...selectedReturnSkus.value, sku])] : selectedReturnSkus.value.filter((value) => value !== sku) }
|
||||
function toggleReturnCarton(cartonMark: string, checked: boolean | string | number) { selectedReturnMarks.value = checked ? [...new Set([...selectedReturnMarks.value, cartonMark])] : selectedReturnMarks.value.filter((value) => value !== cartonMark) }
|
||||
async function submit() { const marks = String(cartonMarks.value).split(/[,\n,]+/).map(value => value.trim()).filter(Boolean); if (!selectedWarehouseId.value) { ElMessage.warning('请选择海外仓'); return }; if (outboundType.value === 'RETURN_OUTBOUND' && !selectedCustomerId.value) { ElMessage.warning('请选择客户'); return }; saving.value = true; try { if (outboundType.value === 'RETURN_OUTBOUND') { if (!recipient.value.trim() || !selectedReturnNo.value || !selectedReturnMarks.value.length) { ElMessage.warning('请填写收件信息并选择退货单和箱唛'); return }; await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, remark: remark.value.trim() || undefined, idempotencyKey: `web-return-${Date.now()}` }); ElMessage.success('退货出库单已创建并推送PDA箱唛拣货任务') } else { const selected = dropshipCandidates.value.filter(item => selectedDropshipIds.value.includes(dropshipCandidateKey(item))); if (!selected.length) { ElMessage.warning('请至少选择一个已完成入库的一件代发订单'); return }; const customerIds = new Set(selected.map(item => item.customerId).filter(Boolean)); if (customerIds.size !== 1) { ElMessage.warning('一张出库单只能选择同一个客户的订单'); return }; const warehouses = new Set(selected.map(item => item.overseasWarehouseId).filter(Boolean)); if (warehouses.size > 1 || (warehouses.size === 1 && !warehouses.has(selectedWarehouseId.value))) { ElMessage.warning('所选订单必须属于当前海外仓'); return }; const items = selected.map(item => ({ inventoryPositionId: item.inventoryPositionId, packages: item.availablePackages, sku: item.skuCode || '', cartonMarks: [] as string[] })); await createOneClickDropshipOutbound([...customerIds][0] || '', { recipient: '一件代发订单直出', items, remark: remark.value.trim() || undefined, idempotencyKey: `web-dropship-${Date.now()}` }); ElMessage.success(`已按订单生成 ${selected.length} 个托盘出库明细并推送PDA拣货任务`) } createOpen.value = false; await load() } catch (cause) { ElMessage.error(getApiErrorDetails(cause, '出库单创建失败').message) } finally { saving.value = false } }
|
||||
async function submit() {
|
||||
if (outboundType.value !== 'WAREHOUSE_TRANSFER_OUTBOUND' && !selectedWarehouseId.value) { ElMessage.warning('请选择海外仓'); return }
|
||||
if (outboundType.value === 'RETURN_OUTBOUND' && !selectedCustomerId.value) { ElMessage.warning('请选择客户'); return }
|
||||
saving.value = true
|
||||
try {
|
||||
if (outboundType.value === 'WAREHOUSE_TRANSFER_OUTBOUND') {
|
||||
const selected = selectedTransferRows.value
|
||||
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 }
|
||||
if (destinationType.value === 'PRIVATE' && (!deliveryAddress.value.trim() || !postalCode.value.trim() || !contactName.value.trim() || !contactPhone.value.trim())) { ElMessage.warning('请完整填写私人仓地址、邮编、联系人和电话'); return }
|
||||
await bindWarehouseTransferDestination({ transfers: selected.map(item => ({ customerId: item.customerId, transferNo: item.transferNo })), destinationType: destinationType.value, destinationWarehouseId: destination?.value, destinationWarehouseCode: destination?.code, destinationWarehouseName: destination?.name, privateDestinationAddress: deliveryAddress.value.trim() || undefined, privateDestinationPostalCode: postalCode.value.trim() || undefined, privateDestinationContactName: contactName.value.trim() || undefined, privateDestinationContactPhone: contactPhone.value.trim() || undefined, idempotencyKey: `web-transfer-destination-${Date.now()}` })
|
||||
ElMessage.success(`已为 ${selected.length} 个转运单绑定目的地,可进入智能排车`)
|
||||
} else if (outboundType.value === 'RETURN_OUTBOUND') {
|
||||
if (!recipient.value.trim() || !selectedReturnNo.value || !selectedReturnMarks.value.length) { ElMessage.warning('请填写收件信息并选择退货单和箱唛'); return }
|
||||
await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, remark: remark.value.trim() || undefined, idempotencyKey: `web-return-${Date.now()}` })
|
||||
ElMessage.success('退货出库单已创建并推送PDA箱唛拣货任务')
|
||||
} else {
|
||||
const selected = dropshipCandidates.value.filter(item => selectedDropshipIds.value.includes(dropshipCandidateKey(item)))
|
||||
if (!selected.length) { ElMessage.warning('请至少选择一个已完成入库的一件代发订单'); return }
|
||||
const customerIds = new Set(selected.map(item => item.customerId).filter(Boolean)); if (customerIds.size !== 1) { ElMessage.warning('一张出库单只能选择同一个客户的订单'); return }
|
||||
const warehouses = new Set(selected.map(item => item.overseasWarehouseId).filter(Boolean)); if (warehouses.size > 1 || (warehouses.size === 1 && !warehouses.has(selectedWarehouseId.value))) { ElMessage.warning('所选订单必须属于当前海外仓'); return }
|
||||
const items = selected.map(item => ({ inventoryPositionId: item.inventoryPositionId, packages: item.availablePackages, sku: item.skuCode || '', cartonMarks: [] as string[] }))
|
||||
await createOneClickDropshipOutbound([...customerIds][0] || '', { recipient: '一件代发订单直出', items, remark: remark.value.trim() || undefined, idempotencyKey: `web-dropship-${Date.now()}` })
|
||||
ElMessage.success(`已按订单生成 ${selected.length} 个托盘出库明细并推送PDA拣货任务`)
|
||||
}
|
||||
createOpen.value = false; await load()
|
||||
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '出库单创建失败').message) } finally { saving.value = false }
|
||||
}
|
||||
function openDetail(row: OutboundRow) { void router.push({ name: 'overseas-outbound-detail', params: { outboundNo: row.outboundNo } }) }
|
||||
function canCancel(row: OutboundRow) { return access.can(permission.outboundCancel) && row.status === 'PICKED' }
|
||||
function canCancel(row: OutboundRow) { return access.can(permission.outboundCancel) && ['WAIT_OUTBOUND', 'WAIT_PICK', 'PICKED'].includes(String(row.status || '')) }
|
||||
async function cancel(row: OutboundRow) { if (!canCancel(row)) return; let reason = ''; try { const result = await ElMessageBox.prompt(`出库单 ${row.outboundNo} · 出库数量 ${row.packageCount ?? 0}`, '取消出库单', { customClass: 'admin-density-confirm', inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return }; mutationError.value = null; try { await cancelOutbound(row.outboundNo, reason); ElMessage.success('出库单已取消,库存及关联单据已回滚'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败') } }
|
||||
function search(model: QueryModel) { page.value = 1; void load(model) }
|
||||
function reset(model: QueryModel) { page.value = 1; void load(model) }
|
||||
@@ -136,12 +184,13 @@ onMounted(load)
|
||||
<template #status="{ row }"><StatusBadge :status="row.status === 'CANCELLED' ? '已取消' : row.status === 'OUTBOUNDED' ? '已出库' : row.status === 'PICKED' ? '已拣货' : '拣货中'" /></template>
|
||||
</DataTable></section>
|
||||
</ListPageLayout></AdminPage>
|
||||
<AppDialog class="admin-density-dialog" v-model="createOpen" title="新建出库单" width="min(1320px, calc(100vw - 32px))" confirm-text="生成出库单并推送PDA" :confirm-loading="saving" @confirm="submit">
|
||||
<AppDialog class="admin-density-dialog" v-model="createOpen" title="新建出库单" width="min(1320px, calc(100vw - 32px))" :confirm-text="outboundType === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '绑定目的地并进入排车' : '生成出库单并推送PDA'" :confirm-loading="saving" @confirm="submit">
|
||||
<el-form class="create-outbound-form density-form" label-position="top">
|
||||
<el-form-item label="出库类型" required>
|
||||
<el-radio-group v-model="outboundType" @change="changeOutboundType">
|
||||
<el-radio-button label="ONE_CLICK_DROPSHIP">一件代发出库</el-radio-button>
|
||||
<el-radio-button label="RETURN_OUTBOUND">退货出库</el-radio-button>
|
||||
<el-radio-button label="WAREHOUSE_TRANSFER_OUTBOUND">仓储转运出库</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="outboundType === 'RETURN_OUTBOUND'" label="客户" required>
|
||||
@@ -149,7 +198,7 @@ onMounted(load)
|
||||
<el-option v-for="item in customerOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="outboundType !== 'RETURN_OUTBOUND'">
|
||||
<template v-if="outboundType === 'ONE_CLICK_DROPSHIP'">
|
||||
<el-form-item label="海外仓" required>
|
||||
<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" />
|
||||
@@ -188,6 +237,28 @@ onMounted(load)
|
||||
<el-alert v-else title="一件代发按托盘与SKU选择;数量默认使用所选行全部可用箱数,同一张出库单只能选择同一客户、同一海外仓。" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-else-if="outboundType === 'WAREHOUSE_TRANSFER_OUTBOUND'">
|
||||
<el-alert title="仅展示已完成入库、换标已完成或无需换标,并且尚未设置目的地的仓储转运单。绑定后将从此列表移除并进入智能排车候选。" type="info" :closable="false" />
|
||||
<el-form-item label="无目的地转运单" required>
|
||||
<el-table v-loading="inventoryLoading" :data="transferCandidates" border stripe class="dropship-candidate-table" max-height="380">
|
||||
<el-table-column label="选择" width="68" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedTransferPalletIds.includes(transferSelectionKey(row as WarehouseTransferOutboundCandidate))" @change="toggleTransferPallet(row as WarehouseTransferOutboundCandidate, $event)" /></template></el-table-column>
|
||||
<el-table-column label="客户" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ transferCustomerName(row as WarehouseTransferOutboundCandidate) }}</template></el-table-column>
|
||||
<el-table-column prop="transferNo" label="转运单号" min-width="190" show-overflow-tooltip />
|
||||
<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>
|
||||
<div v-if="!inventoryLoading && !transferCandidates.length" class="form-hint">暂无已入库且尚无目的地的仓储转运单</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="目的地类型" required><el-radio-group v-model="destinationType"><el-radio-button label="FBA">FBA目的仓</el-radio-button><el-radio-button label="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item v-if="destinationType === 'FBA'" label="FBA目的仓" required><el-select v-model="selectedDestinationWarehouseId" filterable clearable placeholder="请选择FBA目的仓"><el-option v-for="item in fbaWarehouseOptions" :key="item.value" :value="item.value" :label="item.label" /></el-select></el-form-item>
|
||||
<template v-else>
|
||||
<el-form-item label="私人仓地址" required><el-input v-model="deliveryAddress" placeholder="请输入完整派送地址" /></el-form-item>
|
||||
<el-form-item label="邮编" required><el-input v-model="postalCode" maxlength="32" placeholder="请输入邮编" /></el-form-item>
|
||||
<el-form-item label="联系人" required><el-input v-model="contactName" placeholder="请输入联系人" /></el-form-item>
|
||||
<el-form-item label="联系电话" required><el-input v-model="contactPhone" placeholder="请输入联系电话" /></el-form-item>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label="收件信息" required><el-input v-model="recipient" placeholder="请输入收件人、电话和地址" /></el-form-item>
|
||||
<el-form-item label="海外仓" required><el-select v-model="selectedWarehouseId" :disabled="!selectedCustomerId" filterable placeholder="请选择海外仓" @change="loadReturnCandidates"><el-option v-for="item in warehouseOptions" :key="item.value" :value="item.value" :label="item.label" /></el-select></el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user