fix(dispatch): align dispatch actions and native return workflows
This commit is contained in:
@@ -3,14 +3,14 @@ import { physicalWeightLb, totalPhysicalWeightLb } from '../dispatchPhysicalWeig
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { GripVertical, Plus } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
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 { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, preArrangeDraftDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
import { dispatchVehicleSpecs } from '../dispatchVehicleSpecs'
|
||||
@@ -37,6 +37,10 @@ const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const draftError = ref<ApiErrorDetails | null>(null)
|
||||
const draftLoading = ref(false)
|
||||
const draftLoaded = ref(false)
|
||||
const draftBlocked = computed(() => draftLoading.value || !draftLoaded.value || Boolean(draftError.value))
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const candidates = ref<DispatchPalletCandidateDto[]>([])
|
||||
const palletSnapshots = ref<Record<string, DispatchPalletCandidateDto>>({})
|
||||
@@ -73,7 +77,7 @@ const visibleCandidates = computed(() =>
|
||||
const orderGroups = computed(() => {
|
||||
const groups = new Map<string, DispatchPalletCandidateDto[]>()
|
||||
for (const item of visibleCandidates.value) {
|
||||
const key = String(item.orderId || item.orderNo || item.planId)
|
||||
const key = item.sourceStage === 'RELABEL' ? `RELABEL:${String(item.orderRefs || '')}` : String(item.orderId || item.orderNo || item.planId)
|
||||
groups.set(key, [...(groups.get(key) || []), item])
|
||||
}
|
||||
return [...groups.entries()].map(([key, pallets]) => ({
|
||||
@@ -99,6 +103,7 @@ const containerOverweight = (container: LoadingContainer) => {
|
||||
}
|
||||
|
||||
function palletId(item: DispatchPalletCandidateDto) {
|
||||
if (item.sourceStage === 'RELABEL') return String(item.warehousePalletId || '')
|
||||
if (item.sourceType === 'RETURN_RECEIPT') return item.returnReceiptId ? `RETURN:${item.returnReceiptId}` : ''
|
||||
return String(item.planPalletId || item.warehousePalletId || '')
|
||||
}
|
||||
@@ -166,6 +171,7 @@ async function hydrateDraftPallets() {
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (draftBlocked.value) return
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
@@ -198,12 +204,12 @@ function reset() {
|
||||
}
|
||||
|
||||
function addContainer() {
|
||||
if (saving.value) return
|
||||
if (saving.value || draftBlocked.value) return
|
||||
selectedVehicle.value = ''
|
||||
vehicleDialogOpen.value = true
|
||||
}
|
||||
async function confirmAddContainer() {
|
||||
if (saving.value) return
|
||||
if (saving.value || draftBlocked.value) return
|
||||
const spec = selectedVehicleSpec.value
|
||||
if (!spec) {
|
||||
ElMessage.warning('请选择车型')
|
||||
@@ -226,7 +232,7 @@ async function confirmAddContainer() {
|
||||
|
||||
async function movePallet(item: DispatchPalletCandidateDto, containerId: string) {
|
||||
const id = palletId(item)
|
||||
if (saving.value || busyPalletIds.value.includes(id)) return
|
||||
if (saving.value || draftBlocked.value || busyPalletIds.value.includes(id)) return
|
||||
const target = containers.value.find((container) => container.id === containerId)
|
||||
if (!target || target.palletIds.includes(id)) return
|
||||
if (!String(item.overseasWarehouseId || '').trim()) {
|
||||
@@ -270,7 +276,7 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string)
|
||||
}
|
||||
|
||||
async function removePallet(id: string) {
|
||||
if (saving.value || busyPalletIds.value.includes(id)) return
|
||||
if (saving.value || draftBlocked.value || busyPalletIds.value.includes(id)) return
|
||||
const owner = containerFor(id)
|
||||
if (!owner) return
|
||||
busyPalletIds.value.push(id)
|
||||
@@ -286,13 +292,17 @@ async function removePallet(id: string) {
|
||||
}
|
||||
|
||||
async function deleteContainer(container: LoadingContainer) {
|
||||
if (saving.value) return
|
||||
if (saving.value || draftBlocked.value) return
|
||||
for (const id of [...container.palletIds]) {
|
||||
await removePallet(id)
|
||||
}
|
||||
if (!container.palletIds.length) {
|
||||
await deleteDispatchDraftContainer(ensureDraftToken(), container.id)
|
||||
containers.value = containers.value.filter((item) => item.id !== container.id)
|
||||
try {
|
||||
await deleteDispatchDraftContainer(ensureDraftToken(), container.id)
|
||||
containers.value = containers.value.filter((item) => item.id !== container.id)
|
||||
} catch (error) {
|
||||
mutationError.value = getApiErrorDetails(error, '删除装车车型失败,请重试')
|
||||
}
|
||||
}
|
||||
}
|
||||
function dragStart(event: DragEvent, item: DispatchPalletCandidateDto) {
|
||||
@@ -311,11 +321,15 @@ async function close(value = false) {
|
||||
|
||||
async function submit() {
|
||||
if (saving.value) return
|
||||
if (draftBlocked.value) {
|
||||
ElMessage.warning('排车草稿尚未恢复,请先重试恢复草稿;原选择已保留')
|
||||
return
|
||||
}
|
||||
if (busyPalletIds.value.length) {
|
||||
ElMessage.warning('托盘来源正在锁定,请稍后再预排车')
|
||||
return
|
||||
}
|
||||
if (loading.value || unresolvedAssignedIds.value.length) {
|
||||
if (loading.value || loadError.value || unresolvedAssignedIds.value.length) {
|
||||
ElMessage.warning('草稿中部分已选来源尚未恢复,请重试加载后再预排;原选择已保留')
|
||||
return
|
||||
}
|
||||
@@ -358,9 +372,16 @@ async function submit() {
|
||||
mutationError.value = null
|
||||
try {
|
||||
if (relabelOrders.length) {
|
||||
const latest = await listDispatchCandidates({ sourceStage: 'RELABEL', orderNo: relabelOrders[0], draftToken: ensureDraftToken() })
|
||||
const required = latest.filter(item => item.sourceStage === 'RELABEL' && String(item.orderRefs || '').trim() === relabelOrders[0])
|
||||
const selected = new Set(assignedIds.value)
|
||||
if (!required.length || required.length !== selected.size || required.some(item => !selected.has(palletId(item)))) {
|
||||
mutationError.value = { message: '换标必须一次选择本申请全部应排托盘,并放入同一装车容器;候选可能已变化,请重新查询核对,原选择已保留' }
|
||||
return
|
||||
}
|
||||
await preArrangeDispatch({
|
||||
planPalletIds: assigned.value.filter(item => item.sourceType !== 'RETURN_RECEIPT').map(palletId),
|
||||
returnReceiptIds: assigned.value.filter(item => item.sourceType === 'RETURN_RECEIPT').map(item => String(item.returnReceiptId)),
|
||||
planPalletIds: assigned.value.map(palletId),
|
||||
returnReceiptIds: [],
|
||||
relabelNo: relabelOrders[0],
|
||||
})
|
||||
} else {
|
||||
@@ -385,6 +406,10 @@ async function submit() {
|
||||
}
|
||||
emit('update:modelValue', false)
|
||||
emit('created')
|
||||
draftLoaded.value = false
|
||||
draftToken.value = ''
|
||||
containers.value = []
|
||||
palletSnapshots.value = {}
|
||||
ElMessage.success('排车明细已同步到预排列表')
|
||||
await router.push({ name: 'overseas-dispatches' })
|
||||
} catch (error) {
|
||||
@@ -394,37 +419,72 @@ async function submit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreDraft() {
|
||||
if (draftLoading.value) return
|
||||
draftLoading.value = true
|
||||
draftError.value = null
|
||||
try {
|
||||
const drafts = await listDispatchDrafts()
|
||||
const draft = draftToken.value ? drafts.find(item => item.draftToken === draftToken.value) : drafts[0]
|
||||
if (draftToken.value && containers.value.length && (!draft || containers.value.some(container =>
|
||||
container.palletIds.some(id => palletSnapshot(id)?.sourceStage === 'RELABEL') && !draft.containers?.some(item => item.id === container.id)))) {
|
||||
draftError.value = { message: '原草稿已失效或装车容器已变化,原选择已保留。请重试恢复;仍无法恢复时可释放草稿后重新选择。' }
|
||||
return
|
||||
}
|
||||
if (draft?.draftToken) {
|
||||
const restoredIds = new Set((draft.containers || []).flatMap(container => container.palletIds || []))
|
||||
if (assignedIds.value.some(id => palletSnapshot(id)?.sourceStage !== 'RELABEL' && !restoredIds.has(id))) {
|
||||
ElMessage.warning('原草稿部分来源锁已释放,已按服务端当前草稿恢复,请重新选择这些来源')
|
||||
}
|
||||
draftToken.value = draft.draftToken
|
||||
containers.value = (draft.containers || []).map((c) => {
|
||||
const spec = dispatchVehicleSpecs.find((item) => item.capacity === c.capacity) || dispatchVehicleSpecs[0]!
|
||||
const localRelabel = containers.value.find(item => item.id === c.id)?.palletIds.filter(id => palletSnapshot(id)?.sourceStage === 'RELABEL') || []
|
||||
return { id: c.id, capacity: c.capacity, maxWeightLb: spec.maxWeightLb, vehicleLabel: spec.label, palletIds: [...new Set([...(c.palletIds || []), ...localRelabel])] }
|
||||
})
|
||||
} else {
|
||||
ensureDraftToken()
|
||||
}
|
||||
draftLoaded.value = true
|
||||
} catch (error) {
|
||||
draftError.value = getApiErrorDetails(error, '排车草稿恢复失败,原选择已保留,请重试恢复后继续')
|
||||
} finally {
|
||||
draftLoading.value = false
|
||||
}
|
||||
if (draftLoaded.value && !draftError.value) await load()
|
||||
}
|
||||
|
||||
async function restartDraft() {
|
||||
if (draftLoading.value || saving.value || !draftToken.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('释放原草稿的普通及退回来源锁,并清空本页换标选择后重新开始?', '重新选择排车来源', { customClass: 'admin-density-confirm', confirmButtonText: '释放并重新选择', cancelButtonText: '保留草稿', type: 'warning' })
|
||||
} catch { return }
|
||||
draftLoading.value = true
|
||||
try {
|
||||
await releaseDispatchDraft(draftToken.value)
|
||||
draftToken.value = ''
|
||||
containers.value = []
|
||||
palletSnapshots.value = {}
|
||||
draftLoaded.value = false
|
||||
draftError.value = null
|
||||
} catch (error) {
|
||||
draftError.value = getApiErrorDetails(error, '释放草稿失败,原选择已保留,请重试')
|
||||
} finally { draftLoading.value = false }
|
||||
if (!draftError.value) await restoreDraft()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (!open) return
|
||||
draftToken.value = makeDraftToken()
|
||||
containers.value = []
|
||||
palletSnapshots.value = {}
|
||||
query.orderNo = ''
|
||||
query.orderType = ''
|
||||
query.sourceStage = 'RECEIVED'
|
||||
query.fbaWarehouseCode = ''
|
||||
warehouseTab.value = 'ALL'
|
||||
mutationError.value = null
|
||||
void (async () => {
|
||||
const drafts = await listDispatchDrafts().catch(() => [])
|
||||
const draft = drafts[0]
|
||||
if (draft?.draftToken) {
|
||||
draftToken.value = draft.draftToken
|
||||
containers.value = (draft.containers || []).map((c) => {
|
||||
const spec = dispatchVehicleSpecs.find((item) => item.capacity === c.capacity) || dispatchVehicleSpecs[0]!
|
||||
return {
|
||||
id: c.id,
|
||||
capacity: c.capacity,
|
||||
maxWeightLb: spec.maxWeightLb,
|
||||
vehicleLabel: spec.label,
|
||||
palletIds: c.palletIds || [],
|
||||
}
|
||||
})
|
||||
}
|
||||
await load()
|
||||
})()
|
||||
draftLoaded.value = false
|
||||
void restoreDraft()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -445,7 +505,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
<el-button type="primary" :loading="loading" @click="load">查询</el-button><el-button :disabled="loading" @click="reset">重置</el-button>
|
||||
</section>
|
||||
|
||||
<p v-if="query.sourceStage === 'RELABEL'" class="dispatch-source-hint">仅展示客户已确认、卡车派送且尚未排车的换标单。沿用原入库库存和托盘,按本次换标箱唛及新目的仓排车,无需重新生成入库单。</p>
|
||||
<p v-if="query.sourceStage === 'RELABEL'" class="dispatch-source-hint">仅展示客户已确认、卡车派送且尚未排车的换标单。必须一次选择本申请全部应排托盘并放入同一装车容器。换标选择仅保留在当前页面,尚未锁定;刷新页面后需重新选择,提交时由服务端重新校验。</p>
|
||||
|
||||
<nav class="warehouse-tabs density-tabs" aria-label="海外仓筛选">
|
||||
<button
|
||||
@@ -463,10 +523,12 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<LoadErrorState v-if="loadError" title="可排车入仓单加载失败" :message="loadError.message" :loading="loading" @retry="load"
|
||||
<LoadErrorState v-if="draftError" title="排车草稿恢复失败" :message="draftError.message" :loading="draftLoading" @retry="restoreDraft"><template #details><ApiErrorMeta :details="draftError" /></template></LoadErrorState>
|
||||
<p v-else-if="draftLoading" role="status">正在恢复排车草稿,请稍候…</p>
|
||||
<LoadErrorState v-else-if="loadError" title="可排车入仓单加载失败" :message="loadError.message" :loading="loading" @retry="load"
|
||||
><template #details><ApiErrorMeta :details="loadError" /></template
|
||||
></LoadErrorState>
|
||||
<div v-else class="dispatch-workbench">
|
||||
<div v-else v-loading="draftLoading" class="dispatch-workbench">
|
||||
<section class="order-pane" aria-label="可排车入仓单">
|
||||
<header>
|
||||
<div>
|
||||
@@ -549,7 +611,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag v-if="containerFor(palletId(item))" type="success" size="small">已锁定</el-tag>
|
||||
<el-tag v-if="containerFor(palletId(item))" :type="item.sourceStage === 'RELABEL' ? 'warning' : 'success'" size="small">{{ item.sourceStage === 'RELABEL' ? '已选择,尚未锁定' : '已锁定' }}</el-tag>
|
||||
<el-dropdown v-else trigger="click" :disabled="!containers.length || busyPalletIds.includes(palletId(item))" @command="movePallet(item, String($event))">
|
||||
<el-button class="density-action" data-tone="primary" :loading="busyPalletIds.includes(palletId(item))">移入 </el-button>
|
||||
<template #dropdown
|
||||
@@ -599,7 +661,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
><OrderReferenceText :value="text(palletSnapshot(id)?.orderNo)" /></span
|
||||
><el-button class="density-action" data-tone="danger" :disabled="saving" :loading="busyPalletIds.includes(id)" @click="removePallet(id)">移除</el-button>
|
||||
</div>
|
||||
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
|
||||
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘;换标选择提交时校验,其他来源移入后锁定</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!containers.length" :image-size="64" description="请先添加装车托盘" />
|
||||
@@ -608,8 +670,8 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
</div>
|
||||
|
||||
<template #footer><footer class="dialog-footer">
|
||||
<span>托盘移入即锁定,重量按 1kg = 2.2046lb 换算。</span>
|
||||
<div><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="!assignedIds.length || busyPalletIds.length > 0" @click="submit">预排车</el-button></div>
|
||||
<span>普通/退回来源移入后锁定;换标选择尚未锁定,仅保留在当前页面。重量按 1kg = 2.2046lb 换算。</span>
|
||||
<div><el-button v-if="draftError && draftToken" :disabled="draftLoading" @click="restartDraft">释放草稿并重新选择</el-button><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="draftBlocked || !assignedIds.length || busyPalletIds.length > 0" @click="submit">预排车</el-button></div>
|
||||
</footer></template>
|
||||
</AppDialog>
|
||||
<el-dialog class="admin-density-dialog" v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
|
||||
|
||||
Reference in New Issue
Block a user