Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
3 changed files with 44 additions and 16 deletions
Showing only changes of commit b77428b453 - Show all commits
@@ -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: '全部'")