优化
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ArrowRight, GripVertical, Plus, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -11,7 +11,8 @@ import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||||
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[] }
|
||||
type VehicleSpec = { value: string; label: string; capacity: number; maxWeightLb: number }
|
||||
interface LoadingContainer { id: string; capacity: number; maxWeightLb: number; vehicleLabel: string; palletIds: string[] }
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; orders: InboundOrderListItem[] }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; created: [] }>()
|
||||
@@ -27,6 +28,8 @@ const containers = ref<LoadingContainer[]>([])
|
||||
const busyPalletIds = ref<string[]>([])
|
||||
const draftToken = ref('')
|
||||
const finalized = ref(false)
|
||||
const vehicleDialogOpen = ref(false)
|
||||
const selectedVehicle = ref('')
|
||||
const query = reactive({ orderNo: '', orderType: '', sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED', fbaWarehouseCode: '' })
|
||||
const warehouseTab = ref<'ALL' | 'LAX' | 'NYC'>('ALL')
|
||||
|
||||
@@ -61,6 +64,15 @@ const assignedIds = computed(() => containers.value.flatMap((item) => item.palle
|
||||
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))
|
||||
const vehicleSpecs: VehicleSpec[] = [
|
||||
{ value: 'LARGE_30', label: '大车(30托)', capacity: 30, maxWeightLb: 40000 },
|
||||
{ value: 'SMALL_15', label: '小车(15托)', capacity: 15, maxWeightLb: 10000 },
|
||||
{ value: 'SMALL_14', label: '小车(14托)', capacity: 14, maxWeightLb: 10000 },
|
||||
{ value: 'SMALL_12', label: '小车(12托)', capacity: 12, maxWeightLb: 10000 },
|
||||
]
|
||||
const selectedVehicleSpec = computed(() => vehicleSpecs.find((item) => item.value === selectedVehicle.value))
|
||||
const weightLb = (item?: DispatchPalletCandidateDto) => Number(item?.volumeWeightKg || 0) * 2.2046226218
|
||||
const containerWeightLb = (container: LoadingContainer) => container.palletIds.reduce((sum, id) => sum + weightLb(palletSnapshot(id)), 0)
|
||||
|
||||
function palletId(item: DispatchPalletCandidateDto) { return String(item.planPalletId) }
|
||||
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
|
||||
@@ -96,15 +108,15 @@ async function load() {
|
||||
}
|
||||
function reset() { query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; void load() }
|
||||
|
||||
async function addContainer() {
|
||||
function addContainer() { selectedVehicle.value = ''; vehicleDialogOpen.value = true }
|
||||
async function confirmAddContainer() {
|
||||
const spec = selectedVehicleSpec.value
|
||||
if (!spec) { ElMessage.warning('请选择车型'); return }
|
||||
try {
|
||||
const result = await ElMessageBox.prompt('请输入该装车托盘容器可装的托数', '添加装车托盘', {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', inputValue: '20', inputType: 'number',
|
||||
inputValidator: (value) => Number.isInteger(Number(value)) && Number(value) > 0 && Number(value) <= 500 ? true : '请输入 1 至 500 的整数',
|
||||
})
|
||||
const created = await createDispatchDraftContainer(ensureDraftToken(), Number(result.value))
|
||||
containers.value.push({ id: created.id, capacity: created.capacity, palletIds: created.palletIds || [] })
|
||||
} catch { /* user cancelled */ }
|
||||
const created = await createDispatchDraftContainer(ensureDraftToken(), spec.capacity)
|
||||
containers.value.push({ id: created.id, capacity: created.capacity, maxWeightLb: spec.maxWeightLb, vehicleLabel: spec.label, palletIds: created.palletIds || [] })
|
||||
vehicleDialogOpen.value = false
|
||||
} catch (error) { mutationError.value = getApiErrorDetails(error, '添加装车车型失败,请重试') }
|
||||
}
|
||||
|
||||
async function movePallet(item: DispatchPalletCandidateDto, containerId: string) {
|
||||
@@ -113,6 +125,9 @@ async function movePallet(item: DispatchPalletCandidateDto, containerId: string)
|
||||
if (!target || target.palletIds.includes(id)) return
|
||||
if (containerFor(id)) { ElMessage.warning('该托盘已在其他装车托盘中'); return }
|
||||
if (target.palletIds.length >= target.capacity) { ElMessage.warning('该装车托盘已达到设置的托数'); return }
|
||||
const nextWeight = containerWeightLb(target) + weightLb(item)
|
||||
if (nextWeight > target.maxWeightLb * 1.1) { ElMessage.warning(`${target.vehicleLabel}重量将达到 ${nextWeight.toFixed(0)}LB,超过限重10%,无法装入`); return }
|
||||
if (nextWeight > target.maxWeightLb) ElMessage.warning(`${target.vehicleLabel}当前重量 ${nextWeight.toFixed(0)}LB,已超过额定限重 ${target.maxWeightLb}LB`)
|
||||
const anchor = assigned.value[0]
|
||||
if (anchor?.customerId && anchor.customerId !== item.customerId) { ElMessage.warning('一次排车只能装同一客户的托盘'); return }
|
||||
if (anchor?.overseasWarehouseId && anchor.overseasWarehouseId !== item.overseasWarehouseId) { ElMessage.warning('一次排车只能装同一海外仓的托盘'); return }
|
||||
@@ -167,7 +182,7 @@ watch(() => props.modelValue, (open) => {
|
||||
if (!open) return
|
||||
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 (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() })()
|
||||
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 = vehicleSpecs.find((item) => item.capacity === c.capacity) || vehicleSpecs[0]!; return { id: c.id, capacity: c.capacity, maxWeightLb: spec.maxWeightLb, vehicleLabel: spec.label, palletIds: c.palletIds || [] } }); await hydrateDraftPallets().catch(() => undefined) } await load() })()
|
||||
}, { immediate: true })
|
||||
|
||||
watch([warehouseTab, () => query.orderType], () => {
|
||||
@@ -198,7 +213,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
<el-table :data="orderGroups" border height="520" highlight-current-row row-key="key" @current-change="activeOrderKey = $event?.key || ''">
|
||||
<el-table-column label="订单号" min-width="150"><template #default="{ row }">{{ text(row.first.orderNo) }}</template></el-table-column>
|
||||
<el-table-column label="订单类型" width="100"><template #default="{ row }">{{ orderType(row.first.orderType) }}</template></el-table-column>
|
||||
<el-table-column label="重量(kg)" width="100" align="right"><template #default="{ row }">{{ number(row.first.volumeWeightKg, 2) }}</template></el-table-column>
|
||||
<el-table-column label="重量(lb)" width="100" align="right"><template #default="{ row }">{{ number(Number(row.first.volumeWeightKg || 0) * 2.2046226218, 0) }}LB</template></el-table-column>
|
||||
<el-table-column label="托盘数量" width="90" align="right"><template #default="{ row }">{{ row.pallets.length }}</template></el-table-column>
|
||||
<el-table-column label="目的地FBA" width="110"><template #default="{ row }">{{ text(row.first.fbaWarehouseCode) }}</template></el-table-column>
|
||||
<el-table-column label="体积(m³)" width="100" align="right"><template #default="{ row }">{{ number(row.pallets.reduce((sum: number, item: DispatchPalletCandidateDto) => sum + Number(item.volumeCbm || 0), 0), 2) }}</template></el-table-column>
|
||||
@@ -214,7 +229,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
<div class="pallet-list">
|
||||
<div v-for="item in activePallets" :key="palletId(item)" class="pallet-row" :class="{ assigned: containerFor(palletId(item)) }" draggable="true" @dragstart="dragStart($event, item)">
|
||||
<GripVertical :size="16" class="drag-handle" />
|
||||
<div class="pallet-copy"><strong>{{ text(item.palletNo) }}</strong><span>{{ Number(item.packageCount || 0) }}箱 · {{ number(item.volumeCbm, 2) }}m³</span><div class="carton-marks"><el-tag v-for="mark in cartonMarks(item).slice(0, 4)" :key="mark" size="small" effect="plain">{{ mark }}</el-tag><span v-if="cartonMarks(item).length > 4">+{{ cartonMarks(item).length - 4 }}</span><span v-if="!cartonMarks(item).length">暂无箱号</span></div></div>
|
||||
<div class="pallet-copy"><strong>{{ text(item.palletNo) }}</strong><span>{{ Number(item.packageCount || 0) }}箱 · {{ number(weightLb(item), 0) }}LB</span><div class="carton-marks"><el-tag v-for="mark in cartonMarks(item).slice(0, 4)" :key="mark" size="small" effect="plain">{{ mark }}</el-tag><span v-if="cartonMarks(item).length > 4">+{{ cartonMarks(item).length - 4 }}</span><span v-if="!cartonMarks(item).length">暂无箱号</span></div></div>
|
||||
<el-tag v-if="containerFor(palletId(item))" type="success" size="small">已锁定</el-tag>
|
||||
<el-dropdown v-else trigger="click" :disabled="!containers.length || busyPalletIds.includes(palletId(item))" @command="movePallet(item, String($event))">
|
||||
<el-button text type="primary" :loading="busyPalletIds.includes(palletId(item))">移入 <ArrowRight :size="14" /></el-button>
|
||||
@@ -229,7 +244,7 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
<header><div><h3>装车托盘</h3><p>托盘 {{ assigned.length }} · 箱数 {{ assignedPackages }} · 立方 {{ assignedVolume.toFixed(2) }}</p></div><el-button type="primary" plain @click="addContainer"><Plus :size="15" />添加装车托盘</el-button></header>
|
||||
<div class="container-list">
|
||||
<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-head"><strong>{{ container.vehicleLabel }}</strong><span>托数 <b>{{ container.palletIds.length }}</b>/{{ container.capacity }} · 重量 <b>{{ number(containerWeightLb(container), 0) }}LB</b>/{{ container.maxWeightLb }}LB</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(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>
|
||||
@@ -240,8 +255,14 @@ watch([warehouseTab, () => query.orderType], () => {
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer"><span>托盘移入即锁定,移除或关闭弹窗即解锁。</span><div><el-button :disabled="saving" @click="close(false)">取消</el-button><el-button type="primary" :loading="saving" :disabled="!assignedIds.length" @click="submit">预排车</el-button></div></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" @click="submit">预排车</el-button></div></footer>
|
||||
</AppDialog>
|
||||
<el-dialog v-model="vehicleDialogOpen" title="选择车型" width="360px" append-to-body>
|
||||
<el-radio-group v-model="selectedVehicle" class="vehicle-options">
|
||||
<el-radio v-for="spec in vehicleSpecs" :key="spec.value" :value="spec.value">{{ spec.label }} · 限重 {{ spec.maxWeightLb }}LB</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer><el-button @click="vehicleDialogOpen = false">取消</el-button><el-button type="primary" @click="confirmAddContainer">确定</el-button></template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -275,5 +296,6 @@ h3, p { margin: 0; } h3 { font-size: 16px; } h3 span, header > span, p { color:
|
||||
.loaded-pallet > span { display: flex; min-width: 0; flex-direction: column; color: var(--cc-muted); font-size: 12px; }.loaded-pallet strong { color: var(--cc-text); font-size: 13px; }
|
||||
.dialog-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 16px -20px -20px; padding: 14px 20px; border-top: 1px solid var(--cc-border); background: #fafbfc; }
|
||||
.dialog-footer > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.vehicle-options { display: grid; gap: 14px; }
|
||||
@media (max-width: 1280px) { .dispatch-workbench { grid-template-columns: minmax(500px, 1.3fr) minmax(320px, 1fr); }.container-pane { grid-column: 1 / -1; border-left: 0; padding-left: 0; }.container-list { height: 300px; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user