发布采购、拼车、预约及匹配端侧修复至灰度 #19

Merged
admin merged 55 commits from release/test into main 2026-10-12 10:20:54 +08:00
3 changed files with 120 additions and 12 deletions
Showing only changes of commit f047dce10c - Show all commits
@@ -52,7 +52,7 @@ export async function listReturnOutboundCandidates(customerId: string, warehouse
const response = await http.get<ApiEnvelope<ReturnOutboundCandidate[]>, ApiEnvelope<ReturnOutboundCandidate[]>>('/platform/v1/last-leg/outbound-orders/return/candidates', { params: { customerId, warehouseId } })
return unwrapApiResponse(response)
}
export async function createReturnOutbound(customerId: string, payload: { recipient: string; overseasWarehouseId: string; returnNo: string; cartonMarks: string[]; skuCode?: string; unitType?: 'BOX' | 'PALLET'; remark?: string; idempotencyKey: string }) {
export async function createReturnOutbound(customerId: string, payload: { recipient: string; overseasWarehouseId: string; returnNo: string; cartonMarks: string[]; skuCode?: string; quantity?: number; unitType?: 'BOX' | 'PALLET'; remark?: string; idempotencyKey: string }) {
const response = await http.post<ApiEnvelope<OutboundRow>, ApiEnvelope<OutboundRow>>('/platform/v1/last-leg/outbound-orders/return', payload, { params: { customerId } })
return unwrapApiResponse(response)
}
@@ -0,0 +1,91 @@
import ElementPlus, { ElCheckbox, ElInputNumber, ElSelect } from 'element-plus'
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
import OutboundsView from './OutboundsView.vue'
const api = vi.hoisted(() => ({ create: vi.fn(), stock: vi.fn(), cartons: vi.fn(), detail: 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: [{ id: 'C1', tenantName: '客户一' }], total: 1, pages: 1 }) }))
vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: vi.fn().mockResolvedValue({ records: [{ id: 'W1', warehouseName: '洛杉矶仓', warehouseCode: 'US-LAX-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] }) }))
vi.mock('../api/outbounds', () => ({ listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }), listOneClickOutboundCandidates: vi.fn().mockResolvedValue([]), listReturnOutboundCandidates: vi.fn().mockResolvedValue([]), createReturnOutbound: api.create }))
vi.mock('../api/returnInventory', () => ({ listReturnStock: api.stock, listReturnCartons: api.cartons, getReturnCarton: api.detail }))
const sources = [
{ cartonMark: 'BOX-A', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] },
{ cartonMark: 'BOX-B', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 5, availableQuantity: 5 }] },
{ cartonMark: 'PARCEL', sourceUnitType: 'PARCEL', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-B', quantity: 6, availableQuantity: 6 }] },
{ cartonMark: 'MIXED', sourceUnitType: 'BOX', sourceReturnNo: 'RT-1', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 3 }, { skuCode: 'SKU-B', quantity: 2 }] },
{ cartonMark: 'OTHER-ORDER', sourceUnitType: 'BOX', sourceReturnNo: 'RT-2', status: 'AVAILABLE', items: [{ skuCode: 'SKU-A', quantity: 100 }] },
]
let wrapper: VueWrapper
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined })
api.stock.mockResolvedValue({ records: [{ skuCode: 'SKU-A', availableQuantity: 113 }, { skuCode: 'SKU-B', availableQuantity: 8 }], total: 2 })
api.cartons.mockResolvedValue({ records: sources, total: sources.length })
api.detail.mockImplementation(async mark => sources.find(row => row.cartonMark === mark))
api.create.mockResolvedValue({})
})
afterEach(() => { wrapper?.unmount(); vi.unstubAllGlobals() })
async function open(sku: string) {
wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus], stubs: { AppDialog: { props: ['modelValue'], emits: ['confirm'], template: '<section v-if="modelValue"><slot /><button data-test="submit" @click="$emit(\'confirm\')">提交</button></section>' } } } })
await flushPromises()
await wrapper.findAll('button').find(button => button.text().includes('新建出库单'))!.trigger('click'); await flushPromises()
const type = wrapper.findComponent({ name: 'ElRadioGroup' }); type.vm.$emit('update:modelValue', 'RETURN_OUTBOUND'); type.vm.$emit('change', 'RETURN_OUTBOUND'); await flushPromises()
const customer = wrapper.findAllComponents(ElSelect).find(select => select.props('placeholder') === '请选择客户')!
customer.vm.$emit('update:modelValue', 'C1'); customer.vm.$emit('change', 'C1'); await flushPromises()
const order = wrapper.findAllComponents(ElSelect).find(select => select.props('placeholder') === '选择已签收入库的退货单')!
order.vm.$emit('update:modelValue', 'RT-1'); order.vm.$emit('change', 'RT-1'); await flushPromises()
await wrapper.get('input[placeholder="请输入收件人、电话和地址"]').setValue('本地测试收件信息')
const mode = wrapper.findAllComponents({ name: 'ElRadioGroup' }).find(group => group.text().includes('单 SKU'))!
mode.vm.$emit('update:modelValue', 'SKU'); mode.vm.$emit('change', 'SKU'); await flushPromises()
const left = wrapper.findAllComponents({ name: 'ElTable' }).filter(table => table.classes().includes('return-result-table'))[0]!
left.findAllComponents(ElCheckbox).find(check => check.element.closest('tr')?.textContent?.includes(sku))!.vm.$emit('change', true); await flushPromises()
}
function right() { return wrapper.findAllComponents({ name: 'ElTable' }).filter(table => table.classes().includes('return-result-table'))[1]! }
async function selectMarks(marks: string[]) {
for (const mark of marks) right().findAllComponents(ElCheckbox).find(check => check.element.closest('tr')?.textContent?.includes(mark === 'PARCEL' ? '包裹散件(按 SKU 拣货)' : mark))!.vm.$emit('change', true)
await flushPromises()
}
describe('single SKU outbound source and piece quantity', () => {
it('selects packed single-SKU boxes, scopes quantities to the return order and excludes mixed boxes', async () => {
await open('SKU-A')
expect(right().props('data').map((row: { cartonMark: string }) => row.cartonMark)).toEqual(['BOX-A', 'BOX-B'])
expect(wrapper.text()).not.toContain('OTHER-ORDER')
await selectMarks(['BOX-A', 'BOX-B'])
expect(wrapper.findComponent(ElInputNumber).props('modelValue')).toBe(10)
await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises()
expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ skuCode: 'SKU-A', quantity: 10, cartonMarks: ['BOX-A', 'BOX-B'] }))
})
it('submits six pieces from two five-piece boxes', async () => {
await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B'])
wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', 6); await flushPromises()
await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises()
expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ quantity: 6 }))
})
it('allows loose PARCEL sources even when the box/pallet candidate endpoint excludes them', async () => {
await open('SKU-B'); expect(right().props('data').map((row: { cartonMark: string }) => row.cartonMark)).toEqual(['PARCEL'])
await selectMarks(['PARCEL']); wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', 2); await flushPromises()
await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises()
expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({ skuCode: 'SKU-B', quantity: 2, cartonMarks: ['PARCEL'] }))
})
it('never offers a parcel as a physical box in box/pallet mode', async () => {
await open('SKU-B')
const mode = wrapper.findAllComponents({ name: 'ElRadioGroup' }).find(group => group.text().includes('单 SKU'))!
mode.vm.$emit('update:modelValue', 'BOX'); mode.vm.$emit('change', 'BOX'); await flushPromises()
expect(right().props('data').some((row: { sourceUnitType: string }) => row.sourceUnitType === 'PARCEL')).toBe(false)
expect(right().text()).not.toContain('包裹散件')
})
it.each([[0,1],[11,10]])('clamps input %s to the allowed bound %s', async (quantity, expected) => {
await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B'])
wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', quantity); await flushPromises()
await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises()
expect(api.create).toHaveBeenCalledWith('C1', expect.objectContaining({quantity: expected}))
})
it.each([1.5])('blocks invalid quantity %s before creating an order', async quantity => {
await open('SKU-A'); await selectMarks(['BOX-A', 'BOX-B'])
wrapper.findComponent(ElInputNumber).vm.$emit('update:modelValue', quantity); await flushPromises()
await wrapper.get('[data-test="submit"]').trigger('click'); await flushPromises()
expect(api.create).not.toHaveBeenCalled()
})
})
@@ -1,6 +1,6 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { Plus, Search } from '@lucide/vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
@@ -38,6 +38,7 @@ const returnStockRows = ref<ReturnStockRow[]>([])
const returnCartonRows = ref<ReturnCartonRow[]>([])
const selectedReturnSkus = ref<string[]>([])
const returnPickMode = ref<'BOX' | 'SKU'>('BOX')
const returnQuantity = ref<number | undefined>()
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')
@@ -64,7 +65,15 @@ function transferCustomerName(row: WarehouseTransferOutboundCandidate) {
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(returnPickMode.value === 'SKU' ? item.looseAvailableQuantity || 0 : item.availableQuantity || 0) > 0))
const selectedReturnStock = computed(() => returnStockRows.value.map((item) => {
if (returnPickMode.value !== 'SKU') return item
const quantity = selectedReturnCartons.value.reduce((sum, carton) => {
if (selectedReturnNo.value && !(carton.sourceReturnNo || '').split(',').includes(selectedReturnNo.value)) return sum
const items = returnCartonDetails.value[carton.cartonMark]?.items
return items?.length === 1 && items[0]?.skuCode === item.skuCode ? sum + Number(items[0].availableQuantity ?? items[0].quantity) : sum
}, 0)
return { ...item, availableQuantity: quantity }
}).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(',')),
@@ -76,11 +85,11 @@ const queryTokens = (value: string) => value.split(/[,,\n]+/).map((item) => it
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 (returnPickMode.value === 'BOX' && item.sourceUnitType === 'PARCEL') return false
if (selectedReturnNo.value && !(item.sourceReturnNo || returnCandidateByMark.value[item.cartonMark]?.returnNo || '').split(',').includes(selectedReturnNo.value)) return false
if (returnPickMode.value === 'SKU') {
const detail = returnCartonDetails.value[item.cartonMark]
if (!selectedReturnSkus.value.length || item.sourceUnitType !== 'PARCEL' || !returnCandidateByMark.value[item.cartonMark]
|| detail?.items?.length !== 1 || detail.items[0]?.skuCode !== selectedReturnSkus.value[0]) return false
if (!selectedReturnSkus.value.length || detail?.items?.length !== 1 || detail.items[0]?.skuCode !== selectedReturnSkus.value[0]) return false
}
const detailSkus = returnCartonDetails.value[item.cartonMark]?.items?.map((detail) => detail.skuCode).join(',') || ''
return matchesAnyToken(`${item.cartonMark},${item.fbaCartonMark || ''},${detailSkus}`, returnCartonQuery.value)
@@ -88,6 +97,11 @@ const filteredReturnCartons = computed(() => selectedReturnCartons.value.filter(
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[])
const selectedReturnQuantity = computed(() => effectiveReturnMarks.value.reduce((sum, mark) => {
const line = returnCartonDetails.value[mark]?.items.find(item => item.skuCode === selectedReturnSkus.value[0])
return sum + Number(line?.availableQuantity ?? line?.quantity ?? 0)
}, 0))
watch(selectedReturnQuantity, quantity => { returnQuantity.value = quantity || undefined })
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))
@@ -203,13 +217,16 @@ async function submit() {
if (!recipient.value.trim()) { ElMessage.warning('请填写收件信息'); return }
if (!selectedReturnNo.value) { ElMessage.warning('请选择退货单'); return }
if (returnPickMode.value === 'SKU' && selectedReturnSkus.value.length !== 1) { ElMessage.warning('请选择一个 SKU'); return }
if (returnPickMode.value === 'SKU' && (!Number.isSafeInteger(returnQuantity.value) || Number(returnQuantity.value) <= 0 || Number(returnQuantity.value) > selectedReturnQuantity.value)) {
ElMessage.warning(`请填写 1-${selectedReturnQuantity.value} 件的出库数量`); return
}
if (!selectedReturnMarks.value.length) {
ElMessage.warning(returnPickMode.value === 'SKU'
? filteredReturnCartons.value.length ? '请在右侧选择该 SKU 的散件来源记录' : '该退货单没有此 SKU 可出库的散件库存;已装箱货物请切换箱/托出库'
? filteredReturnCartons.value.length ? '请在右侧选择该 SKU 的来源箱或散件记录' : '该退货单没有此 SKU 的可用单 SKU 箱或散件库存;混合箱请先拆箱'
: '请选择出库箱号或托盘')
return
}
await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, skuCode: returnPickMode.value === 'SKU' ? selectedReturnSkus.value[0] : undefined, unitType: returnPickMode.value === 'SKU' ? 'BOX' : returnUnitType.value, remark: remark.value.trim() || undefined, idempotencyKey: `web-return-${Date.now()}` })
await createReturnOutbound(selectedCustomerId.value, { recipient: recipient.value.trim(), overseasWarehouseId: selectedWarehouseId.value, returnNo: selectedReturnNo.value, cartonMarks: selectedReturnMarks.value, skuCode: returnPickMode.value === 'SKU' ? selectedReturnSkus.value[0] : undefined, quantity: returnPickMode.value === 'SKU' ? returnQuantity.value : undefined, unitType: returnPickMode.value === 'SKU' ? 'BOX' : 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)))
@@ -329,21 +346,21 @@ onMounted(load)
<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>
<el-form-item label="出库维度"><el-radio-group v-model="returnPickMode" @change="changeReturnPickMode"><el-radio-button value="BOX">箱 / 托</el-radio-button><el-radio-button value="SKU">单 SKU</el-radio-button></el-radio-group></el-form-item>
<p v-if="returnPickMode === 'SKU'" class="candidate-hint">单 SKU 模式可填写全部或部分件数;已装箱货物由 PDA 扫箱唛按计划件数拣货,剩余数量保留在原箱。</p><el-form-item v-if="returnPickMode === 'SKU'" label="出库数量(件)" required><el-input-number v-model="returnQuantity" :min="1" :max="selectedReturnQuantity || 1" :precision="0" placeholder="选择来源后填写件数" /><span class="candidate-hint">所选来源可用 {{ selectedReturnQuantity }} 件;按来源箱号顺序分配</span></el-form-item><el-form-item label="出库维度"><el-radio-group v-model="returnPickMode" @change="changeReturnPickMode"><el-radio-button value="BOX">箱 / 托</el-radio-button><el-radio-button value="SKU">单 SKU</el-radio-button></el-radio-group></el-form-item>
<el-form-item v-if="returnPickMode === 'BOX'" 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="returnPickMode === 'BOX' && 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 v-if="returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><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="returnPickMode === 'SKU' ? '散件可用数量' : '可用数量'" width="110" align="right"><template #default="{ row }">{{ returnPickMode === 'SKU' ? row.looseAvailableQuantity : row.availableQuantity }}</template></el-table-column></el-table>
<el-table :data="pagedReturnStocks" border stripe class="return-result-table" max-height="400"><el-table-column v-if="returnPickMode === 'SKU'" label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnSkus.includes(row.skuCode)" @change="toggleReturnSku(row.skuCode, $event)" /></template></el-table-column><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>{{ returnPickMode === 'SKU' ? `已选择 ${selectedReturnSkus.length} 个 SKU` : '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="list-title"><span>{{ returnPickMode === 'SKU' ? 'SKU 来源明细' : '箱号列表' }}</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"><template #default="{ row }"><el-checkbox :model-value="selectedReturnMarks.includes(row.cartonMark)" @change="toggleReturnCarton(row.cartonMark, $event)" /></template></el-table-column><el-table-column prop="cartonMark" label="箱号" min-width="190" show-overflow-tooltip /><el-table-column label="托盘" min-width="150"><template #default="{ row }">{{ 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>
<el-table :data="pagedReturnCartons" border stripe class="return-result-table" max-height="400"><el-table-column label="选择" width="60" align="center"><template #default="{ row }"><el-checkbox :model-value="selectedReturnMarks.includes(row.cartonMark)" @change="toggleReturnCarton(row.cartonMark, $event)" /></template></el-table-column><el-table-column :label="returnPickMode === 'SKU' ? '库存来源' : '箱号'" min-width="190" show-overflow-tooltip><template #default="{ row }">{{ row.sourceUnitType === 'PARCEL' ? '包裹散件(按 SKU 拣货)' : 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 }} 个{{ returnPickMode === 'SKU' ? '来源' : '箱号' }}</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>