优化
This commit is contained in:
@@ -45,7 +45,10 @@ export interface DispatchCandidateQuery {
|
||||
orderNo?: string
|
||||
fbaWarehouseCode?: string
|
||||
sourceStage?: 'PLAN' | 'RECEIVED'
|
||||
draftToken?: string
|
||||
}
|
||||
export interface DispatchDraftContainerDto { id: string; capacity: number; palletIds: string[] }
|
||||
export interface DispatchDraftDto { draftToken: string; containers?: DispatchDraftContainerDto[] }
|
||||
|
||||
export interface PreArrangeDispatchPayload {
|
||||
planPalletIds: string[]
|
||||
@@ -107,13 +110,26 @@ export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) {
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string) {
|
||||
export async function lockDispatchDraftPallet(planPalletId: string, draftToken: string, draftContainerId?: string) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>(
|
||||
'/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken },
|
||||
'/platform/v1/last-leg/dispatches/draft-locks/lock', { planPalletId, draftToken, ...(draftContainerId ? { draftContainerId } : {}) },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function listDispatchDrafts() {
|
||||
const response = await overseasHttp.get<ApiEnvelope<DispatchDraftDto[]>, ApiEnvelope<DispatchDraftDto[]>>('/platform/v1/last-leg/dispatches/drafts')
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export async function createDispatchDraftContainer(draftToken: string, capacity: number) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<DispatchDraftContainerDto>, ApiEnvelope<DispatchDraftContainerDto>>('/platform/v1/last-leg/dispatches/drafts/containers', { draftToken, capacity })
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
export async function deleteDispatchDraftContainer(draftToken: string, containerId: string) {
|
||||
const response = await overseasHttp.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`/platform/v1/last-leg/dispatches/drafts/${encodeURIComponent(draftToken)}/containers/${encodeURIComponent(containerId)}`)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
export async function unlockDispatchDraftPallet(planPalletId: string, draftToken: string) {
|
||||
const response = await overseasHttp.post<ApiEnvelope<null>, ApiEnvelope<null>>(
|
||||
'/platform/v1/last-leg/dispatches/draft-locks/unlock', { planPalletId, draftToken },
|
||||
|
||||
@@ -8,7 +8,7 @@ import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
import { listDispatchCandidates, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
|
||||
interface LoadingContainer { id: string; capacity: number; palletIds: string[] }
|
||||
@@ -21,6 +21,7 @@ const saving = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const candidates = ref<DispatchPalletCandidateDto[]>([])
|
||||
const palletSnapshots = ref<Record<string, DispatchPalletCandidateDto>>({})
|
||||
const activeOrderKey = ref('')
|
||||
const containers = ref<LoadingContainer[]>([])
|
||||
const busyPalletIds = ref<string[]>([])
|
||||
@@ -57,7 +58,7 @@ const orderGroups = computed(() => {
|
||||
})
|
||||
const activePallets = computed(() => orderGroups.value.find((item) => item.key === activeOrderKey.value)?.pallets || [])
|
||||
const assignedIds = computed(() => containers.value.flatMap((item) => item.palletIds))
|
||||
const assigned = computed(() => assignedIds.value.map((id) => candidates.value.find((item) => palletId(item) === id)).filter((item): item is DispatchPalletCandidateDto => Boolean(item)))
|
||||
const assigned = computed(() => assignedIds.value.map((id) => palletSnapshot(id)).filter((item): item is DispatchPalletCandidateDto => Boolean(item)))
|
||||
const assignedPackages = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
const assignedVolume = computed(() => assigned.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
|
||||
@@ -68,16 +69,25 @@ function dateTime(value: unknown) { const raw = String(value || '').trim(); retu
|
||||
function orderType(value: unknown) { return ({ FIRST_LEG: '头程订单', RELABEL_APPLICATION: '换标订单', RETURN_RELABEL: '退换标单', LOCAL_LOOSEBOARD: '本土散板', WAREHOUSE_TRANSFER: '仓储转运单', ONE_CLICK_DROPSHIP: '一件代发', REJECTED: '拒收订单', EXPRESS: '快递订单' } as Record<string, string>)[String(value || '')] || text(value) }
|
||||
function cartonMarks(item: DispatchPalletCandidateDto) { return String(item.cartonMarks || '').split(',').map((value) => value.trim()).filter(Boolean) }
|
||||
function containerFor(id: string) { return containers.value.find((item) => item.palletIds.includes(id)) }
|
||||
function palletSnapshot(id: string) { return palletSnapshots.value[id] }
|
||||
function makeDraftToken() { return globalThis.crypto?.randomUUID?.() || `dispatch-${Date.now()}-${Math.random().toString(16).slice(2)}` }
|
||||
function ensureDraftToken() {
|
||||
if (!draftToken.value.trim()) draftToken.value = makeDraftToken()
|
||||
return draftToken.value
|
||||
}
|
||||
function rememberPallets(items: DispatchPalletCandidateDto[]) {
|
||||
palletSnapshots.value = { ...palletSnapshots.value, ...Object.fromEntries(items.map((item) => [palletId(item), item])) }
|
||||
}
|
||||
async function hydrateDraftPallets() {
|
||||
const restored = await listDispatchCandidates({ draftToken: ensureDraftToken() })
|
||||
rememberPallets(restored)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true; loadError.value = null
|
||||
try {
|
||||
candidates.value = await listDispatchCandidates({ planIds: planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined })
|
||||
candidates.value = await listDispatchCandidates({ planIds: planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined, draftToken: ensureDraftToken() })
|
||||
rememberPallets(candidates.value)
|
||||
activeOrderKey.value = orderGroups.value[0]?.key || ''
|
||||
} catch (error) {
|
||||
candidates.value = []; activeOrderKey.value = ''
|
||||
@@ -92,7 +102,8 @@ async function addContainer() {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', inputValue: '20', inputType: 'number',
|
||||
inputValidator: (value) => Number.isInteger(Number(value)) && Number(value) > 0 && Number(value) <= 500 ? true : '请输入 1 至 500 的整数',
|
||||
})
|
||||
containers.value.push({ id: `${Date.now()}-${containers.value.length + 1}`, capacity: Number(result.value), palletIds: [] })
|
||||
const created = await createDispatchDraftContainer(ensureDraftToken(), Number(result.value))
|
||||
containers.value.push({ id: created.id, capacity: created.capacity, palletIds: created.palletIds || [] })
|
||||
} catch { /* user cancelled */ }
|
||||
}
|
||||
|
||||
@@ -106,13 +117,12 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string)
|
||||
if (anchor?.customerId && anchor.customerId !== item.customerId) { ElMessage.warning('一次排车只能装同一客户的托盘'); return }
|
||||
if (anchor?.overseasWarehouseId && anchor.overseasWarehouseId !== item.overseasWarehouseId) { ElMessage.warning('一次排车只能装同一海外仓的托盘'); return }
|
||||
busyPalletIds.value.push(id); mutationError.value = null
|
||||
try { await lockDispatchDraftPallet(id, ensureDraftToken()); target.palletIds.push(id) }
|
||||
try { await lockDispatchDraftPallet(id, ensureDraftToken(), target.id); target.palletIds.push(id) }
|
||||
catch (error) { mutationError.value = getApiErrorDetails(error, '托盘锁定失败,请刷新后重试') }
|
||||
finally { busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) }
|
||||
}
|
||||
|
||||
async function removePallet(item: DispatchPalletCandidateDto) {
|
||||
const id = palletId(item)
|
||||
async function removePallet(id: string) {
|
||||
const owner = containerFor(id)
|
||||
if (!owner) return
|
||||
busyPalletIds.value.push(id); mutationError.value = null
|
||||
@@ -123,10 +133,9 @@ async function removePallet(item: DispatchPalletCandidateDto) {
|
||||
|
||||
async function deleteContainer(container: LoadingContainer) {
|
||||
for (const id of [...container.palletIds]) {
|
||||
const item = candidates.value.find((candidate) => palletId(candidate) === id)
|
||||
if (item) await removePallet(item)
|
||||
await removePallet(id)
|
||||
}
|
||||
if (!container.palletIds.length) containers.value = containers.value.filter((item) => item.id !== container.id)
|
||||
if (!container.palletIds.length) { await deleteDispatchDraftContainer(ensureDraftToken(), container.id); containers.value = containers.value.filter((item) => item.id !== container.id) }
|
||||
}
|
||||
function dragStart(event: DragEvent, item: DispatchPalletCandidateDto) { event.dataTransfer?.setData('text/plain', palletId(item)) }
|
||||
function dropInto(event: DragEvent, containerId: string) {
|
||||
@@ -156,9 +165,9 @@ async function submit() {
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (!open) return
|
||||
draftToken.value = makeDraftToken(); finalized.value = false; containers.value = []
|
||||
draftToken.value = makeDraftToken(); finalized.value = false; containers.value = []; palletSnapshots.value = {}
|
||||
query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; mutationError.value = null
|
||||
void load()
|
||||
void (async () => { const drafts = await listDispatchDrafts().catch(() => []); const draft = drafts[0]; if (draft?.draftToken) { draftToken.value = draft.draftToken; containers.value = (draft.containers || []).map((c) => ({ id: c.id, capacity: c.capacity, palletIds: c.palletIds || [] })); await hydrateDraftPallets().catch(() => undefined) } await load() })()
|
||||
}, { immediate: true })
|
||||
|
||||
watch([warehouseTab, () => query.orderType], () => {
|
||||
@@ -222,7 +231,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
<div v-for="(container, index) in containers" :key="container.id" class="loading-container" @dragover.prevent @drop.prevent="dropInto($event, container.id)">
|
||||
<div class="container-head"><strong>装车托盘 {{ index + 1 }}</strong><span>托数 <b>{{ container.palletIds.length }}</b>/{{ container.capacity }}</span><el-button text type="danger" :aria-label="`删除装车托盘${index + 1}`" @click="deleteContainer(container)"><Trash2 :size="15" /></el-button></div>
|
||||
<div class="container-body">
|
||||
<div v-for="id in container.palletIds" :key="id" class="loaded-pallet"><span><strong>{{ text(candidates.find((item) => palletId(item) === id)?.palletNo) }}</strong>{{ text(candidates.find((item) => palletId(item) === id)?.orderNo) }}</span><el-button text type="danger" :loading="busyPalletIds.includes(id)" @click="removePallet(candidates.find((item) => palletId(item) === id)!)">移除</el-button></div>
|
||||
<div v-for="id in container.palletIds" :key="id" class="loaded-pallet"><span><strong>{{ text(palletSnapshot(id)?.palletNo || id) }}</strong>{{ text(palletSnapshot(id)?.orderNo) }}</span><el-button text type="danger" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button></div>
|
||||
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,7 +227,10 @@ describe('overseas warehouse first-batch prototype contract', () => {
|
||||
expect(dialog).toContain("sourceStage: 'RECEIVED'")
|
||||
expect(dialog).toContain('一次排车只能装同一客户的托盘')
|
||||
expect(dialog).toContain('function ensureDraftToken()')
|
||||
expect(dialog).toContain('lockDispatchDraftPallet(id, ensureDraftToken())')
|
||||
expect(dialog).toContain('lockDispatchDraftPallet(id, ensureDraftToken(), target.id)')
|
||||
expect(dialog).toContain('function hydrateDraftPallets()')
|
||||
expect(dialog).toContain('@click="removePallet(id)"')
|
||||
expect(dialog).not.toContain('removePallet(candidates.find')
|
||||
expect(dialog).toContain('洛杉矶')
|
||||
expect(dialog).toContain('纽约')
|
||||
expect(dialog).toContain("value: 'ALL', label: '全部'")
|
||||
|
||||
Reference in New Issue
Block a user