feat(admin): 实现排车候选分页并修正报关批次参数
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# 预约工作日历逐操作矩阵
|
||||
|
||||
## 2026-10-21 排车候选分页
|
||||
|
||||
| 页面/组件 | 操作与承载依据 | 本次调整 | 验收 |
|
||||
|---|---|---|---|
|
||||
| WorkDispatchForm | 保留既有预约事项深链内的表单、批次范围与整车草稿流程;现行排车契约第17项候选分页 | 表下分页仅请求当前页;说明总数为关联计划候选数,表格仅显示本批货物;跨页保留选择和原批次版本,完整组/来源版本变化阻止建单 | 定向自动检查单独记录;1920×1080、100%待人工截图确认 |
|
||||
|
||||
## 2026-10-20 日期时间组件统一(当前确认)
|
||||
|
||||
依据:用户截图 `codex-clipboard-ed442cca-1bd2-43d3-87e8-6671e5bcc508.png`,以及随后明确要求排查并优化同类原生日期输入。保留现有业务入口与承载方式,使用 Element Plus 日期/时间控件及已有公共表单样式。
|
||||
|
||||
@@ -48,7 +48,7 @@ beforeEach(() => {
|
||||
natives = [candidate()]; draft = { draftToken: 'server-owned-token', recordVersion: 0, status: 'ACTIVE', warehouseId: 'origin-1', containers: [], sources: [], expiresAt: '2026-10-20T00:00:00Z' }
|
||||
vi.mocked(getReceivingContext).mockImplementation(async key => receiving(key))
|
||||
vi.mocked(getBatchDispatchCandidates).mockImplementation(async key => ({ batchKey: key, version: 4, candidates: [row('9007199254740993', 'customer-a', key)], blockReasons: [] }))
|
||||
vi.mocked(listDispatchCandidates).mockImplementation(async () => ({ candidates: clone(natives), complete: true, snapshotAt: '2026-10-19T00:00:00Z' }))
|
||||
vi.mocked(listDispatchCandidates).mockImplementation(async () => ({ total: 1, page: 1, size: 20, pages: 1, candidates: clone(natives), complete: true, snapshotAt: '2026-10-19T00:00:00Z' }))
|
||||
vi.mocked(createDispatchDraft).mockImplementation(async () => clone(draft))
|
||||
vi.mocked(createDispatchDraftContainer).mockImplementation(async body => { draft.recordVersion = 1; draft.containers = [{ draftContainerId: 'container-1', sequenceNo: 1, capacity: body.capacity, sourceKeys: [] }]; return clone(draft) })
|
||||
vi.mocked(lockDispatchDraftPallet).mockImplementation(async body => { const source = natives.find(value => value.selection.planPalletId === body.source.planPalletId)!; draft.recordVersion = Number(draft.recordVersion) + 1; draft.sources.push(clone(source)); draft.containers[0]!.sourceKeys.push(source.sourceKey); return clone(draft) })
|
||||
@@ -58,7 +58,7 @@ beforeEach(() => {
|
||||
})
|
||||
it('creates the whole dispatch when native Long candidate quantities arrive as strings and batch quantities are Integers', async () => {
|
||||
const actual = await vi.importActual<typeof import('../overseas-warehouse/api/dispatches')>('../overseas-warehouse/api/dispatches')
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue(parseApiJsonResponse(JSON.stringify({ code: '200', data: { candidates: [{ ...candidate(), availablePackages: '12' }], complete: true, snapshotAt: '' } })))
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue(parseApiJsonResponse(JSON.stringify({ code: '200', data: { total: 1, page: 1, size: 20, pages: 1, candidates: [{ ...candidate(), availablePackages: '12' }], complete: true, snapshotAt: '' } })))
|
||||
vi.mocked(listDispatchCandidates).mockImplementation(actual.listDispatchCandidates)
|
||||
const wrapper = render()
|
||||
try {
|
||||
@@ -98,7 +98,7 @@ it('uses a server-owned draft, explicit zero versions, native source hashes and
|
||||
})
|
||||
it('combines same-warehouse real customers in one container and keeps each batch source owner', async () => {
|
||||
const second = row('9007199254740994', 'customer-b'); natives.push(candidate(second)); vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: batch, version: 4, candidates: [row(), second], blockReasons: [] })
|
||||
const wrapper = render(); await flushPromises(); expect(wrapper.findAll('input').map(input => (input.element as HTMLInputElement).checked)).toEqual([true, true])
|
||||
const wrapper = render(); await flushPromises(); expect(wrapper.findAll('input[type=checkbox]').map(input => (input.element as HTMLInputElement).checked)).toEqual([true, true])
|
||||
await button(wrapper, '创建整车排车单').trigger('click'); await flushPromises(); const groups = vi.mocked(convertDispatchDraft).mock.calls[0]![0].groups
|
||||
expect(groups).toHaveLength(1); expect(groups[0]!.dispatch.cargo).toEqual(natives.map(value => value.selection)); expect(groups[0]!.dispatch.batchAllocations.map(value => value.customerId)).toEqual(['customer-a', 'customer-b']); expect(wrapper.text()).not.toContain('共车'); wrapper.unmount()
|
||||
})
|
||||
@@ -112,12 +112,12 @@ it('keeps blocked, mismatched-customer, wrong-source and unknown-quantity cargo
|
||||
const blocked = { ...row('2'), blockReasons: ['本托已被其他排车占用'] }, wrong = row('3'), unknown = { ...row('4'), packageCount: null }, wrongSource = row('5')
|
||||
wrong.allocations[0]!.customerId = 'customer-b'; wrongSource.allocations[0]!.sourceKey = 'ORDER:unrelated'; natives.push(...[blocked, wrong, unknown, wrongSource].map(value => candidate(value)))
|
||||
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: batch, version: 4, candidates: [row(), blocked, wrong, unknown, wrongSource], blockReasons: [] })
|
||||
const wrapper = render(); await flushPromises(); expect(wrapper.findAll('input').map(input => (input.element as HTMLInputElement).checked)).toEqual([true, false, false, false, false])
|
||||
const wrapper = render(); await flushPromises(); expect(wrapper.findAll('input[type=checkbox]').map(input => (input.element as HTMLInputElement).checked)).toEqual([true, false, false, false, false])
|
||||
expect(wrapper.text()).toContain('本托已被其他排车占用'); await button(wrapper, '创建整车排车单').trigger('click'); await flushPromises(); expect(vi.mocked(convertDispatchDraft).mock.calls[0]![0].groups[0]!.dispatch.batchAllocations).toEqual(row().allocations); wrapper.unmount()
|
||||
})
|
||||
it('fails closed on incomplete native candidates, missing versions, and cross-warehouse batches', async () => {
|
||||
vi.mocked(listDispatchCandidates).mockResolvedValueOnce({ candidates: natives, complete: false, snapshotAt: '' }); const incomplete = render(); await flushPromises(); expect(incomplete.text()).toContain('未完整返回'); expect(createDispatchDraft).not.toHaveBeenCalled(); incomplete.unmount()
|
||||
natives[0]!.selection.sourceVersion = ''; const missing = render(); await flushPromises(); expect(missing.get('input').attributes('disabled')).toBeDefined(); missing.unmount()
|
||||
it('accepts partial pages but fails closed on missing versions and cross-warehouse batches', async () => {
|
||||
vi.mocked(listDispatchCandidates).mockResolvedValueOnce({ total: 1, page: 1, size: 20, pages: 1, candidates: natives, complete: false, snapshotAt: '' }); const incomplete = render(); await flushPromises(); expect(incomplete.get('input[type=checkbox]').attributes('disabled')).toBeUndefined(); expect(createDispatchDraft).not.toHaveBeenCalled(); incomplete.unmount()
|
||||
natives[0]!.selection.sourceVersion = ''; const missing = render(); await flushPromises(); expect(missing.text()).toContain('来源版本不完整'); missing.unmount()
|
||||
vi.mocked(getReceivingContext).mockImplementation(async key => { const value = receiving(key); if (key !== batch) value.batch.originWarehouseId = 'another-warehouse'; return value })
|
||||
const cross = render({ batchKeys: [batch, 'FBA_APPOINTMENT:2'] }); await flushPromises(); expect(cross.text()).toContain('相同的起运仓'); expect(convertDispatchDraft).not.toHaveBeenCalled(); cross.unmount()
|
||||
})
|
||||
@@ -160,3 +160,38 @@ it('shows batch-level blockers even without rows and refuses duplicate physical
|
||||
const blocked = render(); await flushPromises(); expect(blocked.get('[role="alert"]').text()).toContain('原批次已替代'); expect(createDispatchDraft).not.toHaveBeenCalled(); blocked.unmount()
|
||||
const duplicate = render({ batchKeys: [batch, 'FBA_APPOINTMENT:2'] }); await flushPromises(); expect(duplicate.findAll('input').every(input => input.attributes('disabled') !== undefined)).toBe(true); expect(duplicate.text()).toContain('同一实物'); expect(convertDispatchDraft).not.toHaveBeenCalled(); duplicate.unmount()
|
||||
})
|
||||
|
||||
it('keeps original batch allocations and selected cargo while paging through associated plan candidates', async () => {
|
||||
const first = row(), second = row('9007199254740994', 'customer-b')
|
||||
natives = [candidate(first), candidate(second)]
|
||||
vi.mocked(getBatchDispatchCandidates).mockResolvedValue({ batchKey: batch, version: 4, candidates: [first, second], blockReasons: [] })
|
||||
vi.mocked(listDispatchCandidates).mockImplementation(async query => ({ total: 21, page: query.page!, size: query.size!, pages: 2, candidates: [clone(natives[query.page === 2 ? 1 : 0]!)], complete: false, snapshotAt: '' }))
|
||||
const wrapper = render()
|
||||
try {
|
||||
await flushPromises(); expect(wrapper.findAll('input[type=checkbox]')).toHaveLength(1)
|
||||
await wrapper.get('input[type=checkbox]').setValue(true)
|
||||
const state = wrapper.vm as unknown as { changeCandidatePage(page: number): void }
|
||||
state.changeCandidatePage(2); await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
|
||||
expect(listDispatchCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2, size: 20, warehouseTab: 'ALL', warehouseId: 'origin-1' }))
|
||||
expect(getBatchDispatchCandidates).toHaveBeenCalledTimes(1)
|
||||
expect(listDispatchCandidates).toHaveBeenCalledTimes(2)
|
||||
await button(wrapper, '创建整车排车单').trigger('click'); await flushPromises()
|
||||
expect(convertDispatchDraft).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(convertDispatchDraft).mock.calls[0]![0].groups[0]!.dispatch.batchAllocations).toEqual([...first.allocations, ...second.allocations])
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
it('rejects a selected batch source whose group version changed on another candidate page', async () => {
|
||||
vi.mocked(listDispatchCandidates).mockImplementation(async query => {
|
||||
const value = clone(natives[0]!)
|
||||
if (query.page === 2) value.selectionGroup.groupVersion = 'new-group-version'
|
||||
return { total: 21, page: query.page!, size: query.size!, pages: 2, candidates: [value], complete: false, snapshotAt: '' }
|
||||
})
|
||||
const wrapper = render()
|
||||
try {
|
||||
await flushPromises(); await wrapper.get('input[type=checkbox]').setValue(true)
|
||||
;(wrapper.vm as unknown as { changeCandidatePage(page: number): void }).changeCandidatePage(2)
|
||||
await flushPromises(); await button(wrapper, '创建整车排车单').trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('选货组组成已变化')
|
||||
expect(createDispatchDraft).not.toHaveBeenCalled()
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import { getDispatch, getBatchDispatchCandidates, listDispatchCandidates, createDispatchDraft, createDispatchDraftContainer, listDispatchDrafts, lockDispatchDraftPallet, convertDispatchDraft, releaseDispatchDraft, dispatchVehicleSpecs, DispatchInputError, requireVersion, requireBatchVersions, requireSelectionGroup, expandSelectionGroups, assertSelectionGroupVersions, selectionGroupBlockReason, validateCommandResult, validateCurrentDispatch, isDefiniteDispatchRejection, type BatchDispatchCandidate, type BatchDispatchCandidates, type Candidate, type DispatchDetail, type DispatchDraft, type ConvertDraft, type ConvertedDraft, type DraftSelection, type VersionCommand } from '@/modules/overseas-warehouse'
|
||||
import { getDispatch, getBatchDispatchCandidates, listDispatchCandidates, useDispatchCandidates, createDispatchDraft, createDispatchDraftContainer, listDispatchDrafts, lockDispatchDraftPallet, convertDispatchDraft, releaseDispatchDraft, dispatchVehicleSpecs, DispatchInputError, requireVersion, requireBatchVersions, requireSelectionGroup, expandSelectionGroups, assertSelectionGroupVersions, selectionGroupBlockReason, validateCommandResult, validateCurrentDispatch, isDefiniteDispatchRejection, type BatchDispatchCandidate, type BatchDispatchCandidates, type Candidate, type DispatchDetail, type DispatchDraft, type ConvertDraft, type ConvertedDraft, type DraftSelection, type VersionCommand } from '@/modules/overseas-warehouse'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
@@ -21,14 +21,20 @@ type Preparation = { warehouseId: string; capacity: number; rows: ReadyRow[]; di
|
||||
const access = useAccessPreviewStore(), auth = useAuthStore(), owner = auth.user?.id || ''
|
||||
const scope = JSON.stringify([...new Set(props.batchKeys)].sort()), storageKey = `appointment-dispatch:${owner}:${scope}`
|
||||
const contexts = ref<BatchDispatchCandidates[]>([]), rows = ref<BatchRow[]>([]), selected = ref<string[]>([]), warehouseId = ref('')
|
||||
const loading = ref(false), saving = ref(false), error = ref<ApiErrorDetails | null>(null), saveError = ref<ApiErrorDetails | null>(null)
|
||||
const working = ref(false), saving = ref(false), error = ref<ApiErrorDetails | null>(null), saveError = ref<ApiErrorDetails | null>(null)
|
||||
const candidatePage = useDispatchCandidates(error, listDispatchCandidates)
|
||||
const { page: candidatePageNumber, size: candidatePageSize, total: candidateTotal } = candidatePage
|
||||
const loading = computed(() => working.value || candidatePage.loading.value)
|
||||
const selectedRows = ref<Record<string, BatchRow>>({})
|
||||
let receivingContexts: Awaited<ReturnType<typeof getReceivingContext>>[] = []
|
||||
let nativeGeneration = 0
|
||||
const capacity = ref(30), result = ref<DispatchDetail | null>(null), pending = ref<Preparation | null>(null), corrupted = ref(false)
|
||||
const adding = ref(false), addingBusy = ref(false), arrangingRemaining = ref(false), existingReady = ref(false)
|
||||
const existing = ref<Array<{ batchKey: string; dispatchNo: string; customerId: string }>>([]), baseline = ref('[]')
|
||||
const readable = computed(() => ['platform:appointment:list', 'platform:overseas-warehouse:last-mile-order:list', 'platform:overseas-warehouse:inbound-order:list', 'platform:overseas-warehouse:dispatch:list'].every(permission => access.can(permission)))
|
||||
const canCreate = computed(() => readable.value && access.can('platform:overseas-warehouse:dispatch:create'))
|
||||
const writable = computed(() => props.canWrite && canCreate.value && !corrupted.value && (!existing.value.length || existingReady.value))
|
||||
const chosen = computed(() => rows.value.filter(row => selected.value.includes(row.key)))
|
||||
const chosen = computed(() => selected.value.map(key => selectedRows.value[key]).filter((row): row is BatchRow => Boolean(row)))
|
||||
const unresolved = computed(() => saving.value || Boolean(pending.value) || corrupted.value)
|
||||
useWorkDraft(computed(() => JSON.stringify(selected.value) !== baseline.value && !result.value), unresolved)
|
||||
const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value)) as T
|
||||
@@ -43,7 +49,7 @@ function detailPath(no: string) { return `/overseas-warehouse/dispatches/${encod
|
||||
async function load() {
|
||||
if (!readable.value || pending.value || corrupted.value) return
|
||||
const current = ++generation, batches = [...new Set(props.batchKeys)]
|
||||
loading.value = true; error.value = null; existingReady.value = false; rows.value = []; selected.value = []; baseline.value = '[]'; contexts.value = []
|
||||
working.value = true; error.value = null; existingReady.value = false; rows.value = []; selected.value = []; baseline.value = '[]'; contexts.value = []; selectedRows.value = {}; candidatePage.reset(); nativeGeneration++
|
||||
try {
|
||||
if (!batches.length || batches.some(key => !/^FBA_APPOINTMENT:\d+$/.test(key))) throw new DispatchInputError('请从真实预约批次进入排车,不能重组其他业务批次')
|
||||
const receiving = await Promise.all(batches.map(key => getReceivingContext(key)))
|
||||
@@ -66,16 +72,24 @@ async function load() {
|
||||
if (current !== generation) return
|
||||
if (batchCandidates.some((context, index) => context.batchKey !== batches[index])) throw new DispatchInputError('实物候选与当前预约批次不一致')
|
||||
requireBatchVersions(batchCandidates.flatMap(context => context.candidates.flatMap(row => row.allocations)))
|
||||
contexts.value = batchCandidates
|
||||
const planIds = [...new Set(batchCandidates.flatMap(context => context.candidates.map(row => row.planId)))]
|
||||
const native = planIds.length ? await listDispatchCandidates({ warehouseId: warehouseId.value, planIds }) : { candidates: [], complete: true }
|
||||
if (current !== generation) return
|
||||
requireBatchVersions([...batchCandidates.flatMap(context => context.candidates.flatMap(row => row.allocations)), ...native.candidates.flatMap(row => row.appointmentBatches)])
|
||||
if (native.complete !== true) throw new DispatchInputError('本仓原生候选未完整返回,不能按部分结果建单')
|
||||
rows.value = batchCandidates.flatMap((context, index) => context.candidates.map(row => {
|
||||
const matches = native.candidates.filter(value => value.selection.sourceType === 'INBOUND_PLAN' && value.selection.planPalletId === row.planPalletId && value.customerId === row.customerId)
|
||||
contexts.value = batchCandidates; receivingContexts = receiving
|
||||
await loadCandidatePage(true)
|
||||
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : '批次可排实物读取失败') }
|
||||
finally { if (current === generation) working.value = false }
|
||||
}
|
||||
async function loadCandidatePage(autoSelect = false) {
|
||||
const sequence = ++nativeGeneration, current = generation
|
||||
const planIds = [...new Set(contexts.value.flatMap(context => context.candidates.map(row => row.planId)))]
|
||||
if (!planIds.length) { candidatePage.reset(); rows.value = []; return }
|
||||
await candidatePage.load({ warehouseId: warehouseId.value, planIds, page: candidatePageNumber.value, size: candidatePageSize.value, warehouseTab: 'ALL' })
|
||||
if (sequence !== nativeGeneration || current !== generation) return
|
||||
if (error.value) { rows.value = []; return }
|
||||
try {
|
||||
requireBatchVersions(candidatePage.candidates.value.flatMap(row => row.appointmentBatches))
|
||||
rows.value = contexts.value.flatMap((context, index) => context.candidates.map(row => {
|
||||
const matches = candidatePage.candidates.value.filter(value => value.selection.sourceType === 'INBOUND_PLAN' && value.selection.planPalletId === row.planPalletId && value.customerId === row.customerId)
|
||||
const candidate = matches.length === 1 ? matches[0]! : null
|
||||
const sources = receiving[index]!.batch.items
|
||||
const sources = receivingContexts[index]!.batch.items
|
||||
const blocks = [...context.blockReasons, ...row.blockReasons]
|
||||
if (!candidate || candidate.warehouseId !== warehouseId.value || !candidate.sourceVersion || candidate.selection.sourceVersion !== candidate.sourceVersion) blocks.push('原生实物来源、起运仓或版本待核实')
|
||||
if (candidate) { const groupBlock = selectionGroupBlockReason(candidate); if (groupBlock) blocks.push(groupBlock) }
|
||||
@@ -84,20 +98,25 @@ async function load() {
|
||||
if (row.allocations.reduce((sum, allocation) => sum + allocation.packageCount, 0) !== row.packageCount) blocks.push('逐来源批次分配与实物箱数不一致')
|
||||
return { ...row, key: `${context.batchKey}:${row.customerId}:${row.planPalletId}`, batchKey: context.batchKey, native: candidate, blockReasons: [...new Set(blocks)] }
|
||||
}))
|
||||
if (candidateTotal.value > candidatePageSize.value) rows.value = rows.value.filter(row => row.native)
|
||||
for (const row of rows.value) if (row.native && rows.value.filter(other => other.native?.sourceKey === row.native!.sourceKey).length > 1) row.blockReasons.push('同一实物出现在多条批次候选,请先核对分配范围')
|
||||
const eligible = rows.value.filter(row => !reasons(row).length)
|
||||
selected.value = batches.length === 1 && physicalCount(eligible) <= capacity.value ? eligible.map(row => row.key) : []
|
||||
baseline.value = JSON.stringify(selected.value)
|
||||
} catch (cause) { if (current === generation) error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : '批次可排实物读取失败') }
|
||||
finally { if (current === generation) loading.value = false }
|
||||
if (autoSelect && candidateTotal.value <= candidatePageSize.value) {
|
||||
const eligible = rows.value.filter(row => !reasons(row).length)
|
||||
selected.value = props.batchKeys.length === 1 && physicalCount(eligible) <= capacity.value ? eligible.map(row => row.key) : []
|
||||
selectedRows.value = Object.fromEntries(eligible.filter(row => selected.value.includes(row.key)).map(row => [row.key, clone(row)]))
|
||||
baseline.value = JSON.stringify(selected.value)
|
||||
}
|
||||
} catch (cause) { error.value = getApiErrorDetails(cause, cause instanceof DispatchInputError ? cause.message : '批次可排实物读取失败') }
|
||||
}
|
||||
function changeCandidatePage(page: number) { candidatePageNumber.value = page; void loadCandidatePage() }
|
||||
function changeCandidateSize(size: number) { candidatePageSize.value = size; candidatePageNumber.value = 1; void loadCandidatePage() }
|
||||
function select(row: BatchRow, event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!writable.value || unresolved.value || loading.value || error.value || reasons(row).length) { input.checked = selected.value.includes(row.key); return }
|
||||
if (!input.checked) selected.value = selected.value.filter(key => key !== row.key)
|
||||
else if (!selected.value.includes(row.key)) {
|
||||
if (physicalCount([...chosen.value, row]) > capacity.value) ElMessage.warning('已达到本组计划托位上限,请核对实际托盘')
|
||||
else selected.value.push(row.key)
|
||||
else { selectedRows.value[row.key] = clone(row); selected.value.push(row.key) }
|
||||
}
|
||||
input.checked = selected.value.includes(row.key)
|
||||
}
|
||||
@@ -153,6 +172,7 @@ async function save() {
|
||||
ensureOwner()
|
||||
if (!pending.value) {
|
||||
if (!writable.value || error.value || result.value || !chosen.value.length || chosen.value.some(row => reasons(row).length || !row.native) || physicalCount(chosen.value) > capacity.value) throw new DispatchInputError('请核对本仓实物资格、来源批次与计划托位')
|
||||
candidatePage.assertCurrent(chosen.value.map(row => row.native!))
|
||||
pending.value = { warehouseId: warehouseId.value, capacity: capacity.value, rows: clone(chosen.value) as ReadyRow[], dispatchKey: crypto.randomUUID(), draft: null, containerId: null, lockedKeys: [], step: null, converted: null, releasing: false }; persist()
|
||||
}
|
||||
saving.value = true
|
||||
@@ -200,7 +220,7 @@ async function releaseRejected() {
|
||||
}
|
||||
async function remaining() { if (!unresolved.value && !addingBusy.value) { result.value = null; adding.value = false; arrangingRemaining.value = true; await load() } }
|
||||
onMounted(load)
|
||||
onBeforeUnmount(() => { generation++ })
|
||||
onBeforeUnmount(() => { generation++; nativeGeneration++; candidatePage.reset() })
|
||||
</script>
|
||||
<template>
|
||||
<section class="work-form" aria-label="按预约批次创建整车排车">
|
||||
@@ -218,6 +238,8 @@ onBeforeUnmount(() => { generation++ })
|
||||
<el-button v-if="batchKeys.length === 1" :disabled="!writable || unresolved" @click="emit('batch', batchKeys[0]!)">按当前批次箱数分批</el-button><p>部分箱数先按本批逐来源数量分批;不对应完整实物时,先在原库存作业拆托。</p>
|
||||
<label>本组计划托位<select v-model.number="capacity" :disabled="!writable || unresolved"><option v-for="spec in dispatchVehicleSpecs" :key="spec.value" :value="spec.capacity">{{ spec.label }}</option></select></label>
|
||||
<table class="density-table"><thead><tr><th>选择</th><th>客户</th><th>托盘码</th><th>入库单号</th><th>来源</th><th>箱数</th><th>不可选择原因</th></tr></thead><tbody><tr v-for="(row, index) in rows" :key="`${row.key}:${index}`"><td><input type="checkbox" :checked="selected.includes(row.key)" :disabled="!writable || unresolved || reasons(row).length > 0" @change="select(row, $event)"></td><td>{{ row.native?.customerName || '客户待核实' }}</td><td>{{ row.palletNo || '—' }}</td><td>{{ row.planNo || '—' }}</td><td>{{ row.sourceStage === 'RECEIVED' ? '已收货实物' : '计划托盘' }}</td><td>{{ row.packageCount ?? '待核实' }}</td><td>{{ reasons(row).join(';') || '可选' }}</td></tr></tbody></table><p v-if="!rows.length">当前批次尚无可排实物。</p>
|
||||
<el-pagination aria-label="批次关联候选分页" :current-page="candidatePageNumber" :page-size="candidatePageSize" :page-sizes="[10, 20, 50]" :total="candidateTotal" :disabled="unresolved || loading" layout="total, sizes, prev, pager, next" @update:current-page="changeCandidatePage" @update:page-size="changeCandidateSize" />
|
||||
<p>上方总数为关联计划的候选数;当前页展示其中属于本批次的实物。已选 {{ chosen.length }} 条(含其他页)。</p>
|
||||
<el-button type="primary" :disabled="!writable || !chosen.length" :loading="saving" @click="save">创建整车排车单</el-button>
|
||||
</template>
|
||||
<p v-if="pending" role="status">本次草稿与原请求已保留,请勿改变来源、版本或另建排车单。<el-button :disabled="!canCreate || saving" :loading="saving" @click="save">核对并重试原请求</el-button></p>
|
||||
|
||||
Reference in New Issue
Block a user