diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts
new file mode 100644
index 00000000..2bce547d
--- /dev/null
+++ b/src/modules/overseas-warehouse/pages/OutboundsView.selection.test.ts
@@ -0,0 +1,123 @@
+import ElementPlus, { ElCheckbox, ElInput, ElPagination, ElSelect } from 'element-plus'
+import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import OutboundsView from './OutboundsView.vue'
+
+const api = vi.hoisted(() => ({ candidates: vi.fn(), create: vi.fn(), warehouses: vi.fn() }))
+vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
+vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) }))
+vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [] }) }))
+vi.mock('@/modules/warehouses', () => ({ listPlatformWarehouses: api.warehouses }))
+vi.mock('../api/outbounds', () => ({
+ listOutbounds: vi.fn().mockResolvedValue({ records: [], total: 0 }),
+ listOneClickOutboundCandidates: api.candidates,
+ createOneClickDropshipOutbound: api.create,
+ cancelOutbound: vi.fn(), createReturnOutbound: vi.fn(), listReturnOutboundCandidates: vi.fn(),
+}))
+
+const fixtures = Array.from({ length: 7 }, (_, index) => ({
+ inventoryPositionId: `position-${index}`, orderNo: `ORDER-${index}`, customerId: 'customer-a',
+ customerName: '客户甲', overseasWarehouseId: 'warehouse-a', palletNo: `PALLET-${index}`,
+ inboundCode: `IN-${index}`, skuCode: index < 6 ? 'MATCH' : 'OTHER', productName: '测试品名', availablePackages: index + 1,
+}))
+let wrapper: VueWrapper
+async function clickButton(text: string) {
+ const button = wrapper.findAll('button').find(item => item.text().includes(text))
+ expect(button, text).toBeDefined()
+ await button!.trigger('click')
+ await flushPromises()
+}
+async function openCreate() {
+ wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus], stubs: {
+ AppDialog: { props: ['modelValue'], emits: ['confirm'], template: '' },
+ } } })
+ await flushPromises()
+ await clickButton('新建出库单')
+}
+function candidatePagination() { return wrapper.findAllComponents(ElPagination).find(item => item.props('total') === 7)! }
+function pageCheckbox() { return wrapper.findAllComponents(ElCheckbox).find(item => item.props('ariaLabel') === '全选本页订单')! }
+
+describe('outbound candidate search and selection', () => {
+ beforeEach(() => {
+ vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
+ vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined })
+ api.candidates.mockReset().mockResolvedValue(fixtures.map(item => ({ ...item })))
+ api.create.mockReset().mockResolvedValue({})
+ api.warehouses.mockReset().mockResolvedValue({ records: [
+ { id: 'warehouse-a', warehouseName: '洛杉矶仓', warehouseCode: 'US-LAX-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' },
+ { id: 'warehouse-b', warehouseName: '纽约仓', warehouseCode: 'US-NY-01', warehouseType: 'OVERSEAS', status: 'ACTIVE' },
+ ] })
+ })
+ afterEach(() => { wrapper?.unmount(); vi.unstubAllGlobals() })
+
+ it('selects and deselects the current page while preserving other pages, then submits all selected quantities', async () => {
+ await openCreate()
+ pageCheckbox().vm.$emit('change', true)
+ await flushPromises()
+ expect(wrapper.text()).toContain('已选择 5 条')
+ candidatePagination().vm.$emit('update:current-page', 2)
+ await flushPromises()
+ expect(pageCheckbox().props('modelValue')).toBe(false)
+ pageCheckbox().vm.$emit('change', true)
+ await flushPromises()
+ expect(wrapper.text()).toContain('已选择 7 条')
+ pageCheckbox().vm.$emit('change', false)
+ await flushPromises()
+ expect(wrapper.text()).toContain('已选择 5 条')
+ await wrapper.get('[data-test="submit-outbound"]').trigger('click')
+ await flushPromises()
+ expect(api.create).toHaveBeenCalledWith('customer-a', expect.objectContaining({
+ items: fixtures.slice(0, 5).map(item => ({ inventoryPositionId: item.inventoryPositionId, packages: item.availablePackages, sku: item.skuCode, cartonMarks: [] })),
+ }))
+ })
+
+ it('searches on query, selects every filtered page, shows partial page selection and clears the selection', async () => {
+ await openCreate()
+ const query = wrapper.findAllComponents(ElInput).find(item => item.attributes('aria-label') === '搜索可出库订单')!
+ query.vm.$emit('update:modelValue', 'MATCH')
+ await flushPromises()
+ expect(wrapper.text()).toContain('全选筛选结果(7)')
+ await query.trigger('keyup', { key: 'Enter' })
+ await flushPromises()
+ expect(wrapper.text()).toContain('全选筛选结果(6)')
+ await clickButton('全选筛选结果')
+ expect(wrapper.text()).toContain('已选择 6 条')
+ await clickButton('重置')
+ candidatePagination().vm.$emit('update:current-page', 2)
+ await flushPromises()
+ expect(pageCheckbox().props('indeterminate')).toBe(true)
+ await clickButton('清空选择')
+ expect(wrapper.text()).toContain('已选择 0 条')
+ })
+
+ it('resets the page after page-size changes and clears selection when changing searchable warehouses', async () => {
+ await openCreate()
+ await clickButton('全选筛选结果')
+ const pagination = candidatePagination()
+ pagination.vm.$emit('update:current-page', 2)
+ await flushPromises()
+ pagination.vm.$emit('update:page-size', 20)
+ pagination.vm.$emit('size-change', 20)
+ await flushPromises()
+ expect(pagination.props('currentPage')).toBe(1)
+ expect(wrapper.text()).toContain('第 1 / 1 页')
+ expect(wrapper.text()).toContain('已选择 7 条')
+ const warehouse = wrapper.findAllComponents(ElSelect).find(item => item.attributes('aria-label') === '搜索海外仓')!
+ expect(warehouse.props('filterable')).toBe(true)
+ warehouse.vm.$emit('update:modelValue', 'warehouse-b')
+ warehouse.vm.$emit('change', 'warehouse-b')
+ await flushPromises()
+ expect(api.candidates).toHaveBeenLastCalledWith(undefined, { warehouseId: 'warehouse-b' })
+ expect(wrapper.text()).toContain('已选择 0 条')
+ })
+
+ it('warns about a multi-customer bulk selection and preserves the submit restriction', async () => {
+ api.candidates.mockResolvedValue([{ ...fixtures[0] }, { ...fixtures[1], customerId: 'customer-b', customerName: '客户乙' }])
+ await openCreate()
+ await clickButton('全选筛选结果')
+ expect(wrapper.text()).toContain('当前选择包含多个客户')
+ await wrapper.get('[data-test="submit-outbound"]').trigger('click')
+ await flushPromises()
+ expect(api.create).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/modules/overseas-warehouse/pages/OutboundsView.vue b/src/modules/overseas-warehouse/pages/OutboundsView.vue
index 347bf304..ee0fdc96 100644
--- a/src/modules/overseas-warehouse/pages/OutboundsView.vue
+++ b/src/modules/overseas-warehouse/pages/OutboundsView.vue
@@ -1,7 +1,7 @@