425 lines
49 KiB
Vue
425 lines
49 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, onMounted, ref, watch } from 'vue'
|
||
import { Plus, Search } from '@lucide/vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
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 { 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'
|
||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||
import { listPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||
import { getReturnCarton, listReturnCartons, listReturnStock, type ReturnCartonDetail, type ReturnCartonRow, type ReturnStockRow } from '../api/returnInventory'
|
||
|
||
const router = useRouter(); const access = useAccessPreviewStore()
|
||
const route = useRoute()
|
||
function openDispatch(row: OutboundRow) {
|
||
if (!row.dispatchNo || !access.can(permission.dispatchList)) return
|
||
void router.push({ name: 'overseas-dispatches', query: { dispatchNo: row.dispatchNo } })
|
||
}
|
||
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: typeof route.query.documentNo === 'string' ? route.query.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 returnSkuQueryInput = ref(''); const returnSkuQuery = ref('')
|
||
const returnCartonQueryInput = ref(''); const returnCartonQuery = ref('')
|
||
const returnSkuPage = ref(1); const returnCartonPage = ref(1); const returnUnitType = ref<'BOX' | 'PALLET'>('BOX')
|
||
const returnSkuPageSize = ref(5); const returnCartonPageSize = ref(5)
|
||
const returnCartonDetails = ref<Record<string, ReturnCartonDetail>>({})
|
||
const customerOptions = computed(() => {
|
||
const customers = new Map<string, string>()
|
||
tenantOptions.value.forEach((item) => {
|
||
if (item.status !== 'ACTIVE') return
|
||
const name = item.tenantName?.trim() || '客户名称待补充'
|
||
const phone = item.contactPhone?.trim()
|
||
customers.set(String(item.id), phone ? `${name}-${phone}` : name)
|
||
})
|
||
positionOptions.value.forEach(({ balance }) => customers.set(balance.customerId, balance.customerName?.trim() || '客户名称待补充'))
|
||
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 || '仓库名称待补充', 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 returnOrderOptions = computed(() => [...new Set([
|
||
...returnCandidates.value.flatMap(item => (item.returnNo || '').split(',')),
|
||
...returnCartonRows.value.flatMap(item => (item.sourceReturnNo || '').split(',')),
|
||
].map(no => no.trim()).filter(Boolean))])
|
||
const returnCandidateByMark = computed(() => Object.fromEntries(returnCandidates.value.map(item => [item.cartonMark || '', item])))
|
||
const returnPallets = computed(() => [...new Set(returnCandidates.value.filter(item => !selectedReturnNo.value || (item.returnNo || '').split(',').includes(selectedReturnNo.value)).map(item => item.palletCode).filter((code): code is string => Boolean(code)))])
|
||
const queryTokens = (value: string) => value.split(/[,,\n]+/).map((item) => item.replace(/\s+/g, '').toLowerCase()).filter(Boolean)
|
||
const matchesAnyToken = (value: string, query: string) => { const tokens = queryTokens(query); const normalized = value.replace(/\s+/g, '').toLowerCase(); return !tokens.length || tokens.some((token) => normalized.includes(token)) }
|
||
const filteredReturnStocks = computed(() => selectedReturnStock.value.filter((item) => matchesAnyToken(`${item.skuCode},${item.productNameZh || ''}`, returnSkuQuery.value)))
|
||
const filteredReturnCartons = computed(() => selectedReturnCartons.value.filter((item) => {
|
||
if (selectedReturnNo.value && !(item.sourceReturnNo || returnCandidateByMark.value[item.cartonMark]?.returnNo || '').split(',').includes(selectedReturnNo.value)) return false
|
||
const detailSkus = returnCartonDetails.value[item.cartonMark]?.items?.map((detail) => detail.skuCode).join(',') || ''
|
||
return matchesAnyToken(`${item.cartonMark},${item.fbaCartonMark || ''},${detailSkus}`, returnCartonQuery.value)
|
||
}))
|
||
const pagedReturnStocks = computed(() => filteredReturnStocks.value.slice((returnSkuPage.value - 1) * returnSkuPageSize.value, returnSkuPage.value * returnSkuPageSize.value))
|
||
const pagedReturnCartons = computed(() => filteredReturnCartons.value.slice((returnCartonPage.value - 1) * returnCartonPageSize.value, returnCartonPage.value * returnCartonPageSize.value))
|
||
const effectiveReturnMarks = computed(() => [...new Set(selectedReturnMarks.value)].filter(Boolean) as string[])
|
||
|
||
watch([selectedReturnNo, selectedCustomerId, selectedWarehouseId], () => {
|
||
selectedReturnMarks.value = []
|
||
})
|
||
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 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: 'grossWeightKg', prop: 'grossWeightKg', label: '毛重(kg)', width: 120, align: 'right' }, { key: 'volumeWeightKg', prop: 'volumeWeightKg', label: '材积重(kg)', width: 130, align: 'right' }, { key: 'volumeCbm', prop: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right' }, { 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_PICK' }, { label: '拣货中', value: 'PICKING' }, { label: '已拣货', value: 'PICKED' }, { label: '待出库', value: 'WAIT_OUTBOUND' }, { label: '已出库', value: 'OUTBOUNDED' }, { label: '已取消', value: 'CANCELLED' }] }, { 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' }, { label: '废弃出库', value: 'DISCARD_OUTBOUND' }, { label: '贴标出库', value: 'RELABEL_OUTBOUND' }] }]
|
||
|
||
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 === 'RELABEL_OUTBOUND' ? '贴标出库' : value === 'DISCARD_OUTBOUND' ? '废弃出库' : value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||
async function loadCustomerOptions() {
|
||
const records: PlatformTenantResource[] = []
|
||
let page = 1
|
||
while (true) {
|
||
const result = await listPlatformTenants({ page, size: 200, status: 'ACTIVE' })
|
||
records.push(...result.records)
|
||
if (page >= result.pages || !result.records.length) return records
|
||
page += 1
|
||
}
|
||
}
|
||
async function loadInventoryOptions() {
|
||
inventoryLoading.value = true
|
||
try {
|
||
const [tenants, warehouses, fbaWarehouses] = await Promise.allSettled([
|
||
loadCustomerOptions(),
|
||
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
|
||
else ElMessage.error(getApiErrorDetails(tenants.reason, '客户列表加载失败').message)
|
||
if (warehouses.status === 'fulfilled') {
|
||
warehouseOptionsSource.value = warehouses.value.records
|
||
if (!selectedWarehouseId.value) {
|
||
const lax = warehouses.value.records.find((item) => item.warehouseCode === 'US-LAX-01' || item.warehouseName?.includes('洛杉矶'))
|
||
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 = []; 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 = []; 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 = []; 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 {
|
||
const pallets = await listWarehouseTransferOutboundCandidates({ customerId: selectedCustomerId.value || undefined, warehouseId: selectedWarehouseId.value || undefined })
|
||
// The API returns pallet rows, but destination binding applies to the whole transfer.
|
||
const transfers = new Map<string, WarehouseTransferOutboundCandidate>()
|
||
for (const pallet of pallets) {
|
||
const key = transferSelectionKey(pallet)
|
||
const transfer = transfers.get(key)
|
||
if (transfer) {
|
||
transfer.packageCount += pallet.packageCount
|
||
transfer.grossWeightKg = sumTransferMeasurement(transfer.grossWeightKg, pallet.grossWeightKg)
|
||
transfer.volumeCbm = sumTransferMeasurement(transfer.volumeCbm, pallet.volumeCbm)
|
||
} else transfers.set(key, { ...pallet })
|
||
}
|
||
transferCandidates.value = [...transfers.values()]
|
||
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 sumTransferMeasurement(left?: number | null, right?: number | null) {
|
||
return left == null && right == null ? null : Number(((left ?? 0) + (right ?? 0)).toFixed(6))
|
||
}
|
||
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); 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)) }
|
||
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(',') }
|
||
function searchReturnSkus() { returnSkuQueryInput.value = normalizeBatchQuery(returnSkuQueryInput.value); returnSkuQuery.value = returnSkuQueryInput.value; returnSkuPage.value = 1 }
|
||
function searchReturnCartons() { returnCartonQueryInput.value = normalizeBatchQuery(returnCartonQueryInput.value); returnCartonQuery.value = returnCartonQueryInput.value; returnCartonPage.value = 1 }
|
||
function changeReturnUnitType() { selectedReturnMarks.value = [] }
|
||
function toggleReturnCarton(cartonMark: string, checked: boolean | string | number) {
|
||
selectedReturnMarks.value = checked ? [...new Set([...selectedReturnMarks.value, cartonMark])] : selectedReturnMarks.value.filter(value => value !== cartonMark)
|
||
}
|
||
function toggleReturnPallet(palletCode: string, checked: boolean | string | number) {
|
||
const marks = returnCandidates.value.filter(item => item.palletCode === palletCode && item.cartonMark).map(item => item.cartonMark!)
|
||
selectedReturnMarks.value = checked ? [...new Set([...selectedReturnMarks.value, ...marks])] : selectedReturnMarks.value.filter(mark => !marks.includes(mark))
|
||
}
|
||
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.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 }
|
||
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()) { ElMessage.warning('请填写收件信息'); return }
|
||
if (!selectedReturnNo.value) { ElMessage.warning('请选择退货单'); return }
|
||
// Return outbound is physical-unit based. Partial SKU quantities must go
|
||
// through the PDA packing flow and produce a new labelled carton first.
|
||
if (!selectedReturnMarks.value.length) {
|
||
ElMessage.warning('请选择出库包裹/箱号或托盘')
|
||
return
|
||
}
|
||
await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, unitType: returnUnitType.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.outboundType !== 'DISCARD_OUTBOUND' && ['WAIT_PICK', 'WAIT_OUTBOUND'].includes(String(row.status || '')) }
|
||
async function cancel(row: OutboundRow) {
|
||
if (!canCancel(row)) return
|
||
let reason = ''
|
||
const dispatchHint = row.dispatchNo ? '关联排车须在出库取消后再取消原排车,释放占用后才能重新预排。' : ''
|
||
try {
|
||
const result = await ElMessageBox.prompt(`出库单 ${row.outboundNo} · 出库数量 ${row.packageCount ?? 0}。已关联车次请先解除关联;取消将申请撤销未履行的派送应收,装车、买约及其他费用保留待核对。${dispatchHint}`, '取消出库单', {
|
||
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)
|
||
const nextStep = !row.dispatchNo ? '' : access.can(permission.dispatchList)
|
||
? ',关联排车请点击“处理原排车”取消并释放占用,再从入仓单或换标单重新预排'
|
||
: ',请由有排车权限的人员取消原排车并释放占用,再从入仓单或换标单重新预排'
|
||
ElMessage.success(`出库单已取消;派送应收撤销结果请查看费用明细${nextStep}`)
|
||
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) }
|
||
onMounted(load)
|
||
</script>
|
||
<template>
|
||
<AdminPage class="admin-density"><ListPageLayout aria-label="出库单">
|
||
<OperationErrorToast :details="mutationError" />
|
||
|
||
|
||
<section><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!error" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-outbounds" row-key="id" empty-text="暂无出库单" @update:page="load()" @update:page-size="page = 1; load()">
|
||
<template #toolbar-start><h1>出库单</h1></template>
|
||
<template #filters><QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="出库单筛选" preference-key="overseas-outbounds-query" :search-on-reset="false" @search="search" @reset="reset" /></template>
|
||
<template #notices><template v-if="error"><LoadErrorState title="出库单列表加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template></template>
|
||
|
||
|
||
<template #toolbar-actions><el-button v-if="access.can(permission.outboundCreate)" type="primary" @click="openCreate"><Plus :size="16" />新建出库单</el-button></template>
|
||
<template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button class="density-action" data-tone="neutral" size="small" @click="openDetail(row as OutboundRow)">详情</el-button><el-button class="density-action" data-tone="danger" v-if="canCancel(row as OutboundRow)" size="small" @click="cancel(row as OutboundRow)">取消</el-button><el-button v-if="row.dispatchNo && access.can(permission.dispatchList)" class="density-action" data-tone="primary" size="small" @click="openDispatch(row as OutboundRow)">{{ row.status === 'CANCELLED' ? '处理原排车' : '查看排车' }}</el-button></div></template>
|
||
<template #outboundType="{ row }">{{ outboundTypeLabel(row.outboundType) }}</template>
|
||
<template #status="{ row }"><StatusBadge :status="row.status === 'CANCELLED' ? '已取消' : row.status === 'OUTBOUNDED' ? '已出库' : row.status === 'PICKED' ? '已拣货' : row.status === 'WAIT_OUTBOUND' ? '待出库' : row.status === 'PICKING' ? '拣货中' : row.status === 'WAIT_PICK' ? '待拣货' : '未知状态'" /></template>
|
||
</DataTable></section>
|
||
</ListPageLayout></AdminPage>
|
||
<AppDialog class="admin-density-dialog" v-model="createOpen" title="新建出库单" width="min(1760px, calc(100vw - 32px))" fullscreenable :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>
|
||
<el-select v-model="selectedCustomerId" :loading="inventoryLoading" clearable filterable placeholder="请选择客户" @change="selectCustomer">
|
||
<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 === 'ONE_CLICK_DROPSHIP'">
|
||
<div class="dropship-search-row">
|
||
<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" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="一件代发订单">
|
||
<div class="candidate-query">
|
||
<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-form-item>
|
||
</div>
|
||
<section class="candidate-results" aria-label="已完成入库的一件代发订单">
|
||
<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 #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 />
|
||
<el-table-column prop="inboundCode" label="入库单号" min-width="160" show-overflow-tooltip />
|
||
<el-table-column prop="palletNo" label="托盘号" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="skuCode" label="SKU" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="productName" label="品名" min-width="140" show-overflow-tooltip />
|
||
<el-table-column prop="availablePackages" label="可出箱数" width="95" align="right" />
|
||
<el-table-column prop="grossWeightKg" label="可出毛重(kg)" width="125" align="right" />
|
||
<el-table-column prop="volumeWeightKg" label="可出材积重(kg)" width="135" align="right" />
|
||
<el-table-column prop="volumeCbm" label="可出体积(m³)" width="125" align="right" />
|
||
</el-table>
|
||
<div v-if="!inventoryLoading && !filteredDropshipCandidates.length" class="form-hint">暂无可直接出库的已完成入库订单</div>
|
||
<div class="candidate-pagination">
|
||
<span aria-live="polite" title="翻页保留选择">已选择 {{ selectedDropshipIds.length }} 条 · 第 {{ 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 v-if="selectedDropshipCustomerCount > 1" title="当前选择包含多个客户,请按客户搜索后选择;一张出库单只能属于同一客户。" type="warning" :closable="false" />
|
||
<p v-else class="candidate-hint">按托盘与 SKU 选择,默认出库全部可用箱数;仅限同一客户、同一海外仓,翻页保留选择。</p>
|
||
</section>
|
||
</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-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>
|
||
<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>
|
||
<el-form-item label="退货单" required><el-select v-model="selectedReturnNo" filterable placeholder="选择已签收入库的退货单" @change="selectedReturnMarks = []"><el-option v-for="no in returnOrderOptions" :key="no" :label="no" :value="no" /></el-select></el-form-item>
|
||
<p class="candidate-hint">整包裹、整箱或整托出库。包裹或箱子只出部分商品时,请先在退货合箱作业生成新箱并贴标,再选择新箱出库。</p>
|
||
<el-form-item label="箱托方式"><el-radio-group v-model="returnUnitType" @change="changeReturnUnitType"><el-radio-button value="BOX">按包裹 / 箱号出库</el-radio-button><el-radio-button value="PALLET">按托盘出库</el-radio-button></el-radio-group></el-form-item>
|
||
<el-form-item v-if="returnUnitType === 'PALLET' && returnPallets.length" label="整托选择"><div class="return-pallet-options"><el-checkbox v-for="code in returnPallets" :key="code" :model-value="returnCandidates.filter(item => item.palletCode === code && item.cartonMark).every(item => selectedReturnMarks.includes(item.cartonMark!))" @change="toggleReturnPallet(code, $event)">{{ code }}</el-checkbox></div></el-form-item>
|
||
<div class="return-lists">
|
||
<section class="return-list-card">
|
||
<div class="list-title"><span>SKU库存</span><small>按 SKU 查询</small></div>
|
||
<div class="candidate-query"><el-input v-model="returnSkuQueryInput" clearable placeholder="如:SKU001,SKU002" /><el-button type="primary" @click="searchReturnSkus">查询</el-button></div>
|
||
<el-table :data="pagedReturnStocks" border stripe class="return-result-table" max-height="400"><el-table-column prop="skuCode" label="SKU" min-width="150" show-overflow-tooltip /><el-table-column prop="productNameZh" label="品名" min-width="150" show-overflow-tooltip /><el-table-column label="可用数量" width="110" align="right"><template #default="{ row }">{{ row.availableQuantity }}</template></el-table-column></el-table>
|
||
<div class="return-pagination"><span>SKU 库存</span><el-pagination v-model:current-page="returnSkuPage" v-model:page-size="returnSkuPageSize" :page-sizes="[5, 10, 20, 50]" :total="filteredReturnStocks.length" layout="total, sizes, prev, pager, next" @size-change="returnSkuPage = 1" /></div>
|
||
</section>
|
||
<section class="return-list-card">
|
||
<div class="list-title"><span>包裹 / 箱号列表</span><small>支持多个箱号或 SKU,用英文逗号分隔</small></div>
|
||
<div class="candidate-query"><el-input v-model="returnCartonQueryInput" clearable placeholder="如:RC001,RC002 或 SKU001,SKU002" /><el-button type="primary" @click="searchReturnCartons">查询</el-button></div>
|
||
<el-table :data="pagedReturnCartons" border stripe class="return-result-table" max-height="400"><el-table-column label="选择" width="60" align="center" fixed="left"><template #default="{ row }"><el-checkbox :model-value="selectedReturnMarks.includes(row.cartonMark)" @change="toggleReturnCarton(row.cartonMark, $event)" /></template></el-table-column><el-table-column label="包裹 / 箱号" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.cartonMark }}</template></el-table-column><el-table-column label="托盘" min-width="150"><template #default="{ row }">{{ row.sourceUnitType === 'PARCEL' ? '无需托盘' : returnCandidateByMark[row.cartonMark]?.palletCode || '散箱' }}</template></el-table-column><el-table-column label="包裹 / 箱内 SKU 及数量" min-width="230"><template #default="{ row }">{{ returnCartonDetails[row.cartonMark]?.items?.map((item) => `${item.skuCode} × ${item.quantity}`).join(',') || '加载中' }}</template></el-table-column></el-table>
|
||
<div class="return-pagination"><span>已选择 {{ selectedReturnMarks.length }} 个包裹 / 箱</span><el-pagination v-model:current-page="returnCartonPage" v-model:page-size="returnCartonPageSize" :page-sizes="[5, 10, 20, 50]" :total="filteredReturnCartons.length" layout="total, sizes, prev, pager, next" @size-change="returnCartonPage = 1" /></div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
<el-form-item label="备注"><el-input v-model="remark" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" placeholder="请输入备注(选填)" /></el-form-item>
|
||
</el-form>
|
||
</AppDialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.create-outbound-form { padding: 0 var(--admin-space-1); }
|
||
.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; }
|
||
.candidate-row { display: grid; grid-template-columns: minmax(120px, 1fr) minmax(150px, 1fr) auto; gap: var(--admin-space-5); align-items: center; width: min(700px, 100%); }
|
||
.candidate-row span, .candidate-row em { color: var(--cc-muted); font-style: normal; font-size: var(--admin-font-caption); }
|
||
.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; }
|
||
.dropship-search-row { display: grid; grid-template-columns: minmax(0, 280px) minmax(0, 640px); align-items: end; gap: var(--admin-space-5); }
|
||
.dropship-search-row :deep(.el-form-item) { min-width: 0; margin-bottom: var(--admin-space-3); }
|
||
.candidate-results { min-width: 0; }
|
||
.candidate-hint { margin: 0 0 var(--admin-space-3); color: var(--cc-muted); font-size: var(--admin-font-caption); line-height: 1.5; }
|
||
.dropship-candidate-table { 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, 0.85fr) minmax(0, 1.15fr); gap: var(--admin-space-5); width: 100%; margin: var(--admin-space-1) 0 var(--admin-space-5); }
|
||
.return-pallet-options { display: flex; flex-wrap: wrap; gap: 8px 16px; }
|
||
.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); }
|
||
.list-title span { color: var(--el-text-color-primary); font-size: var(--admin-font-section); font-weight: 650; }
|
||
.list-title small { color: var(--cc-muted); font-size: var(--admin-font-caption); font-weight: 400; }
|
||
.return-result-table { width: 100%; margin-top: var(--admin-space-5); background: var(--el-bg-color); }
|
||
.return-pagination { display: flex; min-height: 44px; align-items: center; justify-content: space-between; gap: var(--admin-space-5); padding-top: var(--admin-space-5); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||
.return-pagination :deep(.el-pagination) { flex-wrap: wrap; justify-content: flex-end; }
|
||
@media (max-width: 980px) { .return-lists { grid-template-columns: 1fr; }.return-pagination { align-items: flex-start; flex-direction: column; } }
|
||
</style>
|