Merge release/test into dev
This commit is contained in:
@@ -3,6 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getOutbound } from '../api/outbounds'
|
||||
import OutboundDetailView from './OutboundDetailView.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
|
||||
const canPermission = vi.hoisted(() => vi.fn((_code: string) => false))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: canPermission }) }))
|
||||
@@ -10,6 +11,19 @@ vi.mock('vue-router', () => ({ useRoute: () => ({ params: { outboundNo: 'OUT/1'
|
||||
vi.mock('../api/outbounds', () => ({ getOutbound: vi.fn(), cancelOutbound: vi.fn() }))
|
||||
|
||||
describe('OutboundDetailView', () => {
|
||||
it.each([
|
||||
['WAIT_PICK', '待拣货', 'warning'], ['WAIT_OUTBOUND', '待出库', 'warning'], ['PICKING', '拣货中', 'info'],
|
||||
['PICKED', '已拣货', 'warning'], ['OUTBOUNDED', '已出库', 'success'], ['CANCELLED', '已取消', 'danger'],
|
||||
])('uses the same shared status tone as the list for %s', async (status, label, tone) => {
|
||||
vi.mocked(getOutbound).mockResolvedValueOnce({ outbound: { id: '1', outboundNo: 'OUT/1', status }, pallets: [], deliveries: [] })
|
||||
const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const badge = wrapper.findComponent(StatusBadge)
|
||||
expect(badge.text()).toBe(label)
|
||||
expect(badge.classes()).toContain(`is-${tone}`)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
canPermission.mockReset().mockReturnValue(false)
|
||||
vi.mocked(getOutbound).mockReset().mockResolvedValue({
|
||||
@@ -64,8 +78,9 @@ describe('OutboundDetailView', () => {
|
||||
emptyWrapper.unmount()
|
||||
})
|
||||
|
||||
it('shows cancellation only for PICKED with the independent permission', async () => {
|
||||
it('shows cancellation only before picking with the independent permission', async () => {
|
||||
canPermission.mockImplementation((code: string) => code === 'platform:overseas-warehouse:outbound:cancel')
|
||||
vi.mocked(getOutbound).mockResolvedValueOnce({ outbound: { id: '1', outboundNo: 'OUT/1', status: 'WAIT_PICK' }, pallets: [], deliveries: [] })
|
||||
const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('取消出库单')
|
||||
|
||||
@@ -21,7 +21,7 @@ const route = useRoute(); const router = useRouter(); const access = useAccessPr
|
||||
const outboundNo = computed(() => String(route.params.outboundNo || '').trim())
|
||||
const detail = ref<OutboundDetail | null>(null); const loading = ref(false); const cancelling = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null); const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const statusLabels: Record<string, string> = { WAIT_OUTBOUND: '待出库', PICKING: '拣货中', PICKED: '已拣货', OUTBOUNDED: '已出库', CANCELLED: '已取消' }
|
||||
const statusLabels: Record<string, string> = { WAIT_PICK: '待拣货', WAIT_OUTBOUND: '待出库', PICKING: '拣货中', PICKED: '已拣货', OUTBOUNDED: '已出库', CANCELLED: '已取消' }
|
||||
const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'palletNo', label: '托盘码', width: 220 },
|
||||
{ key: 'inboundCode', label: '入仓码', width: 190 },
|
||||
@@ -42,7 +42,7 @@ const warehouseLabel = computed(() => {
|
||||
})
|
||||
function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) }
|
||||
function outboundTypeLabel(value?: string | null) { return value === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : show(value) }
|
||||
const canCancel = computed(() => access.can(permission.outboundCancel) && ['WAIT_OUTBOUND', 'WAIT_PICK', 'PICKED'].includes(String(detail.value?.outbound.status || '')))
|
||||
const canCancel = computed(() => access.can(permission.outboundCancel) && ['WAIT_PICK', 'WAIT_OUTBOUND'].includes(String(detail.value?.outbound.status || '')))
|
||||
async function load() { if (!outboundNo.value) { loadError.value = { message: '详情缺少出库单号,请从出库单列表重新进入' }; return } loading.value = true; loadError.value = null; try { detail.value = await getOutbound(outboundNo.value) } catch (cause) { detail.value = null; loadError.value = getApiErrorDetails(cause, '出库单详情加载失败,请重试') } finally { loading.value = false } }
|
||||
async function cancel() { if (!canCancel.value || cancelling.value) return; let reason = ''; try { const result = await ElMessageBox.prompt(`确认取消出库单 ${outboundNo.value}?`, '取消出库单', { customClass: 'admin-density-confirm', inputType: 'textarea', inputPlaceholder: '请输入取消原因', inputPattern: /\S+/, inputErrorMessage: '请输入取消原因', confirmButtonText: '确认取消', cancelButtonText: '返回', type: 'warning' }); reason = result.value.trim() } catch { return } cancelling.value = true; mutationError.value = null; try { await cancelOutbound(outboundNo.value, reason); ElMessage.success('出库单已取消'); await load() } catch (cause) { mutationError.value = getApiErrorDetails(cause, '取消出库单失败,请重试') } finally { cancelling.value = false } }
|
||||
function back() { void router.push({ name: 'overseas-outbounds' }) }
|
||||
|
||||
@@ -36,6 +36,14 @@ async function openCreate() {
|
||||
}
|
||||
function candidatePagination() { return wrapper.findAllComponents(ElPagination).find(item => item.props('total') === 7)! }
|
||||
function pageCheckbox() { return wrapper.findAllComponents(ElCheckbox).find(item => item.props('ariaLabel') === '全选本页订单')! }
|
||||
async function selectBothPages() {
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
candidatePagination().vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('outbound candidate search and selection', () => {
|
||||
beforeEach(() => {
|
||||
@@ -71,29 +79,40 @@ describe('outbound candidate search and selection', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('searches on query, selects every filtered page, shows partial page selection and clears the selection', async () => {
|
||||
it('keeps searches on one row without bulk buttons and preserves filtered selection across pages', async () => {
|
||||
await openCreate()
|
||||
expect(wrapper.get('.dropship-search-row').findAll('.el-form-item')).toHaveLength(2)
|
||||
expect(wrapper.find('.candidate-selection').exists()).toBe(false)
|
||||
expect(wrapper.text()).not.toContain('全选筛选结果')
|
||||
expect(wrapper.text()).not.toContain('清空选择')
|
||||
const query = wrapper.findAllComponents(ElInput).find(item => item.props('ariaLabel') === '搜索可出库订单')!
|
||||
await query.get('input').setValue('MATCH')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('全选筛选结果(7)')
|
||||
expect(candidatePagination().props('total')).toBe(7)
|
||||
await query.get('input').trigger('keyup', { key: 'Enter' })
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('全选筛选结果(6)')
|
||||
await clickButton('全选筛选结果')
|
||||
const filteredPagination = wrapper.findAllComponents(ElPagination).find(item => item.props('total') === 6)!
|
||||
expect(filteredPagination).toBeDefined()
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
filteredPagination.vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('已选择 6 条')
|
||||
await wrapper.get('.candidate-query').findAll('button').find(item => item.text() === '重置')!.trigger('click')
|
||||
await flushPromises()
|
||||
candidatePagination().vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
expect(pageCheckbox().props('indeterminate')).toBe(true)
|
||||
await clickButton('清空选择')
|
||||
expect(wrapper.text()).toContain('已选择 0 条')
|
||||
pageCheckbox().vm.$emit('change', false)
|
||||
await flushPromises()
|
||||
expect(wrapper.get('.candidate-pagination').text()).toContain('已选择 5 条')
|
||||
})
|
||||
|
||||
it('resets the page after page-size changes and clears selection when changing searchable warehouses', async () => {
|
||||
await openCreate()
|
||||
await clickButton('全选筛选结果')
|
||||
await selectBothPages()
|
||||
const pagination = candidatePagination()
|
||||
pagination.vm.$emit('update:current-page', 2)
|
||||
await flushPromises()
|
||||
@@ -115,7 +134,8 @@ describe('outbound candidate search and selection', () => {
|
||||
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('全选筛选结果')
|
||||
pageCheckbox().vm.$emit('change', true)
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('当前选择包含多个客户')
|
||||
await wrapper.get('[data-test="submit-outbound"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listOutbounds } from '../api/outbounds'
|
||||
import OutboundsView from './OutboundsView.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
|
||||
const canPermission = vi.hoisted(() => vi.fn((_code: string) => true))
|
||||
const routerPush = vi.hoisted(() => vi.fn())
|
||||
@@ -16,10 +17,28 @@ vi.mock('../api/inventory', () => ({ listInventoryBalances: vi.fn(), listInvento
|
||||
class ResizeObserverStub { observe() {} unobserve() {} disconnect() {} }
|
||||
|
||||
describe('OutboundsView permissions and navigation', () => {
|
||||
it('distinguishes outbound stages and does not present unknown states as picking', async () => {
|
||||
const statuses = [
|
||||
['WAIT_PICK', '待拣货', 'warning'], ['WAIT_OUTBOUND', '待出库', 'warning'], ['PICKING', '拣货中', 'info'],
|
||||
['PICKED', '已拣货', 'warning'], ['OUTBOUNDED', '已出库', 'success'],
|
||||
['CANCELLED', '已取消', 'danger'], ['FUTURE_STATE', '未知状态', 'neutral'],
|
||||
]
|
||||
vi.mocked(listOutbounds).mockResolvedValueOnce({ records: statuses.map(([status], index) => ({ id: String(index), outboundNo: `OUT-${index}`, status })), page: 1, size: 20, total: statuses.length, pages: 1 })
|
||||
const wrapper = mount(OutboundsView, { global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
const badges = wrapper.findAllComponents(StatusBadge).filter(badge => badge.element.closest('tbody'))
|
||||
expect(badges).toHaveLength(statuses.length)
|
||||
statuses.forEach(([, label, tone], index) => {
|
||||
expect(badges[index].text()).toBe(label)
|
||||
expect(badges[index].classes()).toContain(`is-${tone}`)
|
||||
})
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined, removeItem: () => undefined })
|
||||
vi.mocked(listOutbounds).mockReset().mockResolvedValue({ records: [{ id: '1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', status: 'PICKED', palletCount: 1, packageCount: 2 }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
vi.mocked(listOutbounds).mockReset().mockResolvedValue({ records: [{ id: '1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', status: 'WAIT_PICK', palletCount: 1, packageCount: 2 }], page: 1, size: 20, total: 1, pages: 1 })
|
||||
canPermission.mockReset().mockReturnValue(true)
|
||||
routerPush.mockReset()
|
||||
})
|
||||
|
||||
@@ -82,11 +82,10 @@ 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 allFilteredSelected = computed(() => filteredDropshipCandidates.value.length > 0 && filteredDropshipCandidates.value.every(item => selectedDropshipKeys.value.has(dropshipCandidateKey(item))))
|
||||
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: 'WAREHOUSE_TRANSFER_OUTBOUND' }, { 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_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' }] }]
|
||||
|
||||
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 === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '仓储转运出库' : value === 'ONE_CLICK_DROPSHIP' ? '一件代发出库' : value === 'RETURN_OUTBOUND' ? '退货出库' : value === 'DISPATCH_DELIVERY' ? '派送出库' : value || '—' }
|
||||
@@ -188,7 +187,7 @@ async function submit() {
|
||||
} 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) && ['WAIT_OUTBOUND', 'WAIT_PICK', 'PICKED'].includes(String(row.status || '')) }
|
||||
function canCancel(row: OutboundRow) { return access.can(permission.outboundCancel) && ['WAIT_PICK', 'WAIT_OUTBOUND'].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) }
|
||||
@@ -203,7 +202,7 @@ onMounted(load)
|
||||
<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"><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></div></template>
|
||||
<template #outboundType="{ row }">{{ outboundTypeLabel(row.outboundType) }}</template>
|
||||
<template #status="{ row }"><StatusBadge :status="row.status === 'CANCELLED' ? '已取消' : row.status === 'OUTBOUNDED' ? '已出库' : row.status === 'PICKED' ? '已拣货' : '拣货中'" /></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(1320px, calc(100vw - 32px))" :confirm-text="outboundType === 'WAREHOUSE_TRANSFER_OUTBOUND' ? '绑定目的地并进入排车' : '生成出库单并推送PDA'" :confirm-loading="saving" @confirm="submit">
|
||||
@@ -221,22 +220,21 @@ onMounted(load)
|
||||
</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="已完成入库的一件代发订单">
|
||||
<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-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>
|
||||
<div class="candidate-selection" aria-label="订单批量选择">
|
||||
<el-button :disabled="inventoryLoading || !filteredDropshipCandidates.length || allFilteredSelected" @click="selectDropshipRows(filteredDropshipCandidates, true)">全选筛选结果({{ filteredDropshipCandidates.length }})</el-button>
|
||||
<el-button :disabled="!selectedDropshipIds.length" @click="selectedDropshipIds = []">清空选择</el-button>
|
||||
<span aria-live="polite">已选择 {{ selectedDropshipIds.length }} 条,翻页保留选择</span>
|
||||
</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>
|
||||
@@ -251,13 +249,13 @@ onMounted(load)
|
||||
<el-table-column prop="availablePackages" label="可出箱数" width="95" align="right" />
|
||||
</el-table>
|
||||
<div v-if="!inventoryLoading && !filteredDropshipCandidates.length" class="form-hint">暂无可直接出库的已完成入库订单</div>
|
||||
<div v-if="filteredDropshipCandidates.length" class="candidate-pagination">
|
||||
<span>第 {{ dropshipPage }} / {{ Math.max(1, Math.ceil(filteredDropshipCandidates.length / dropshipPageSize)) }} 页</span>
|
||||
<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" />
|
||||
<el-alert v-else title="一件代发按托盘与SKU选择;数量默认使用所选行全部可用箱数,同一张出库单只能选择同一客户、同一海外仓。" type="info" :closable="false" />
|
||||
</el-form-item>
|
||||
<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" />
|
||||
@@ -301,7 +299,7 @@ onMounted(load)
|
||||
</div>
|
||||
<el-alert title="退货出库支持按客户选择单个或多个箱子;SKU数量直接从所选箱唛明细扣减。" type="info" :closable="false" />
|
||||
</template>
|
||||
<el-form-item label="备注"><el-input v-model="remark" type="textarea" placeholder="请输入备注(选填)" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="remark" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" placeholder="请输入备注(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -317,9 +315,11 @@ onMounted(load)
|
||||
.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; }
|
||||
.candidate-selection { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); width: 100%; margin-top: var(--admin-space-3); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
.candidate-selection :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.dropship-candidate-table { margin-top: var(--admin-space-5); width: 100%; }
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user