优化
This commit is contained in:
@@ -1,129 +1,270 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { Plus, Trash2 } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowRight, GripVertical, Plus, Trash2 } from '@lucide/vue'
|
||||
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 { listDispatchCandidates, preArrangeDispatch, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import { listDispatchCandidates, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
|
||||
import type { InboundOrderListItem } from '../types'
|
||||
|
||||
interface LoadingContainer { id: string; capacity: number; palletIds: string[] }
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; orders: InboundOrderListItem[] }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; created: [] }>()
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const mutationError = ref<ApiErrorDetails | null>(null)
|
||||
const candidates = ref<DispatchPalletCandidateDto[]>([])
|
||||
const selectedIds = ref<string[]>([])
|
||||
const query = reactive({ orderNo: '', sourceStage: '' as '' | 'PLAN' | 'RECEIVED' })
|
||||
const route = ref('')
|
||||
const activeOrderKey = ref('')
|
||||
const containers = ref<LoadingContainer[]>([])
|
||||
const busyPalletIds = ref<string[]>([])
|
||||
const draftToken = ref('')
|
||||
const finalized = ref(false)
|
||||
const query = reactive({ orderNo: '', orderType: '', sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED', fbaWarehouseCode: '' })
|
||||
const warehouseTab = ref<'ALL' | 'LAX' | 'NYC'>('ALL')
|
||||
|
||||
const fbaWarehouseCode = computed(() => props.orders[0]?.fbaWarehouse || '')
|
||||
const selected = computed(() => candidates.value.filter((item) => selectedIds.value.includes(String(item.planPalletId))))
|
||||
const selectedPackages = computed(() => selected.value.reduce((sum, item) => sum + Number(item.packageCount || 0), 0))
|
||||
const selectedVolume = computed(() => selected.value.reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0))
|
||||
const orderTypeOptions = [
|
||||
{ value: 'FIRST_LEG', label: '头程订单' },
|
||||
{ value: 'RELABEL_APPLICATION', label: '换标订单' },
|
||||
{ value: 'RETURN_RELABEL', label: '退换标单' },
|
||||
{ value: 'LOCAL_LOOSEBOARD', label: '本土散板' },
|
||||
{ value: 'WAREHOUSE_TRANSFER', label: '仓储转运单' },
|
||||
]
|
||||
|
||||
const planIds = computed(() => [...new Set(props.orders.map((item) => item.planId).filter(Boolean))])
|
||||
const visibleCandidates = computed(() => candidates.value.filter((item) => {
|
||||
const typeMatches = !query.orderType || String(item.orderType || '') === query.orderType
|
||||
if (!typeMatches) return false
|
||||
if (warehouseTab.value === 'ALL') return true
|
||||
const warehouse = `${item.overseasWarehouseId || ''} ${item.overseasWarehouseName || ''}`.toLowerCase()
|
||||
return warehouseTab.value === 'LAX'
|
||||
? /lax|洛杉矶|los angeles/.test(warehouse)
|
||||
: /nyc|jfk|纽约|new york/.test(warehouse)
|
||||
}))
|
||||
const orderGroups = computed(() => {
|
||||
const groups = new Map<string, DispatchPalletCandidateDto[]>()
|
||||
for (const item of visibleCandidates.value) {
|
||||
const key = String(item.orderId || item.orderNo || item.planId)
|
||||
groups.set(key, [...(groups.get(key) || []), item])
|
||||
}
|
||||
return [...groups.entries()].map(([key, pallets]) => ({ key, first: pallets[0]!, pallets }))
|
||||
})
|
||||
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 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))
|
||||
|
||||
function palletId(item: DispatchPalletCandidateDto) { return String(item.planPalletId) }
|
||||
function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' }
|
||||
function toggle(candidate: DispatchPalletCandidateDto, checked: boolean) {
|
||||
const id = String(candidate.planPalletId)
|
||||
selectedIds.value = checked ? [...new Set([...selectedIds.value, id])] : selectedIds.value.filter((value) => value !== id)
|
||||
function number(value: unknown, digits = 0) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' }
|
||||
function dateTime(value: unknown) { const raw = String(value || '').trim(); return raw ? raw.replace('T', ' ').slice(0, 16) : '—' }
|
||||
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 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 remove(candidate: DispatchPalletCandidateDto) { toggle(candidate, false) }
|
||||
|
||||
async function load() {
|
||||
loading.value = true; loadError.value = null
|
||||
try {
|
||||
candidates.value = await listDispatchCandidates({
|
||||
orderNo: query.orderNo.trim() || undefined,
|
||||
fbaWarehouseCode: fbaWarehouseCode.value || undefined,
|
||||
sourceStage: query.sourceStage || undefined,
|
||||
})
|
||||
const validIds = new Set(candidates.value.map((item) => String(item.planPalletId)))
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id))
|
||||
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 })
|
||||
activeOrderKey.value = orderGroups.value[0]?.key || ''
|
||||
} catch (error) {
|
||||
candidates.value = []
|
||||
loadError.value = getApiErrorDetails(error, '可排车托盘加载失败,请重试')
|
||||
candidates.value = []; activeOrderKey.value = ''
|
||||
loadError.value = getApiErrorDetails(error, '可排车入仓单加载失败,请重试')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
function reset() { query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; void load() }
|
||||
|
||||
function reset() { query.orderNo = ''; query.sourceStage = ''; void load() }
|
||||
async function addContainer() {
|
||||
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 的整数',
|
||||
})
|
||||
containers.value.push({ id: `${Date.now()}-${containers.value.length + 1}`, capacity: Number(result.value), palletIds: [] })
|
||||
} catch { /* user cancelled */ }
|
||||
}
|
||||
|
||||
async function movePallet(item: DispatchPalletCandidateDto, containerId: string) {
|
||||
const id = palletId(item)
|
||||
const target = containers.value.find((container) => container.id === containerId)
|
||||
if (!target || target.palletIds.includes(id)) return
|
||||
if (containerFor(id)) { ElMessage.warning('该托盘已在其他装车托盘中'); return }
|
||||
if (target.palletIds.length >= target.capacity) { ElMessage.warning('该装车托盘已达到设置的托数'); return }
|
||||
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 }
|
||||
busyPalletIds.value.push(id); mutationError.value = null
|
||||
try { await lockDispatchDraftPallet(id, ensureDraftToken()); 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)
|
||||
const owner = containerFor(id)
|
||||
if (!owner) return
|
||||
busyPalletIds.value.push(id); mutationError.value = null
|
||||
try { await unlockDispatchDraftPallet(id, ensureDraftToken()); owner.palletIds = owner.palletIds.filter((value) => value !== id) }
|
||||
catch (error) { mutationError.value = getApiErrorDetails(error, '托盘解锁失败,请重试') }
|
||||
finally { busyPalletIds.value = busyPalletIds.value.filter((value) => value !== id) }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
if (!container.palletIds.length) 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) {
|
||||
const id = event.dataTransfer?.getData('text/plain') || ''
|
||||
const item = candidates.value.find((candidate) => palletId(candidate) === id)
|
||||
if (item) void movePallet(item, containerId)
|
||||
}
|
||||
|
||||
async function close(value = false) {
|
||||
const token = ensureDraftToken()
|
||||
if (!value && token && !finalized.value) {
|
||||
try { await releaseDispatchDraft(token) } catch { /* server-side expiry remains the fallback */ }
|
||||
}
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!selectedIds.value.length) { ElMessage.warning('请至少选择一个托盘'); return }
|
||||
if (!assignedIds.value.length) { ElMessage.warning('请至少移入一个托盘'); return }
|
||||
saving.value = true; mutationError.value = null
|
||||
try {
|
||||
await preArrangeDispatch({
|
||||
planPalletIds: selectedIds.value,
|
||||
route: route.value.trim() || undefined,
|
||||
fbaAppointmentNo: props.orders.find((item) => item.appointmentNo !== '—')?.appointmentNo,
|
||||
})
|
||||
emit('update:modelValue', false); emit('created'); ElMessage.success('预排车创建成功')
|
||||
await preArrangeDispatch({ planPalletIds: assignedIds.value, draftToken: ensureDraftToken() })
|
||||
finalized.value = true; emit('update:modelValue', false); emit('created'); ElMessage.success('预排车创建成功')
|
||||
await router.push({ name: 'overseas-dispatches' })
|
||||
} catch (error) { mutationError.value = getApiErrorDetails(error, '预排车创建失败,请重试') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
if (!open) return
|
||||
selectedIds.value = []; query.orderNo = ''; query.sourceStage = ''; route.value = ''; mutationError.value = null
|
||||
draftToken.value = makeDraftToken(); finalized.value = false; containers.value = []
|
||||
query.orderNo = ''; query.orderType = ''; query.sourceStage = 'RECEIVED'; query.fbaWarehouseCode = ''; warehouseTab.value = 'ALL'; mutationError.value = null
|
||||
void load()
|
||||
}, { immediate: true })
|
||||
|
||||
watch([warehouseTab, () => query.orderType], () => {
|
||||
activeOrderKey.value = orderGroups.value[0]?.key || ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OperationErrorToast :details="mutationError" />
|
||||
<AppDialog :model-value="modelValue" title="智能排车" width="min(1600px, calc(100vw - 48px))" confirm-text="预排车" :confirm-loading="saving" :confirm-disabled="!selectedIds.length" @update:model-value="emit('update:modelValue', $event)" @confirm="submit">
|
||||
<AppDialog :model-value="modelValue" title="智能排车" width="min(1800px, calc(100vw - 40px))" :show-footer="false" @update:model-value="close($event)">
|
||||
<section class="dispatch-query" aria-label="智能排车筛选">
|
||||
<el-input v-model="query.orderNo" clearable placeholder="订单号" @keyup.enter="load" />
|
||||
<el-select v-model="query.sourceStage" clearable placeholder="全部类型"><el-option label="计划托盘" value="PLAN" /><el-option label="已收货托盘" value="RECEIVED" /></el-select>
|
||||
<el-input :model-value="fbaWarehouseCode" readonly placeholder="FBA仓库" />
|
||||
<el-select v-model="query.orderType" clearable placeholder="全部订单类型"><el-option v-for="option in orderTypeOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select>
|
||||
<el-select v-model="query.sourceStage" clearable placeholder="全部状态"><el-option label="入库完成托盘" value="RECEIVED" /><el-option label="计划托盘" value="PLAN" /></el-select>
|
||||
<el-input v-model="query.fbaWarehouseCode" clearable placeholder="FBA仓库" @keyup.enter="load" />
|
||||
<el-button type="primary" :loading="loading" @click="load">查询</el-button><el-button :disabled="loading" @click="reset">重置</el-button>
|
||||
</section>
|
||||
<LoadErrorState v-if="loadError" title="可排车托盘加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
|
||||
<nav class="warehouse-tabs" aria-label="海外仓筛选">
|
||||
<button v-for="tab in [{ value: 'ALL', label: '全部' }, { value: 'LAX', label: '洛杉矶' }, { value: 'NYC', label: '纽约' }]" :key="tab.value" type="button" :class="['warehouse-tab', { active: warehouseTab === tab.value }]" @click="warehouseTab = tab.value as 'ALL' | 'LAX' | 'NYC'">{{ tab.label }}</button>
|
||||
</nav>
|
||||
|
||||
<LoadErrorState v-if="loadError" title="可排车入仓单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||||
<div v-else class="dispatch-workbench">
|
||||
<section class="candidate-pane" aria-label="可排车托盘列表">
|
||||
<header><h3>可排车托盘 <span>({{ candidates.length }}个)</span></h3></header>
|
||||
<el-table v-loading="loading" :data="candidates" border height="500" empty-text="暂无可排车托盘">
|
||||
<el-table-column width="48"><template #default="{ row }"><el-checkbox :model-value="selectedIds.includes(String((row as DispatchPalletCandidateDto).planPalletId))" @change="toggle(row as DispatchPalletCandidateDto, Boolean($event))" /></template></el-table-column>
|
||||
<el-table-column prop="orderNo" label="订单号" min-width="150" />
|
||||
<el-table-column prop="inboundCode" label="入仓码" min-width="130" />
|
||||
<el-table-column prop="palletNo" label="托盘号" min-width="130" />
|
||||
<el-table-column prop="fbaWarehouseCode" label="目的地FBA" width="115" />
|
||||
<el-table-column prop="packageCount" label="箱数" width="80" align="right" />
|
||||
<el-table-column prop="volumeCbm" label="体积(m³)" width="105" align="right" />
|
||||
</el-table>
|
||||
<section class="order-pane" aria-label="可排车入仓单">
|
||||
<header><div><h3>出库单列表 <span>({{ orderGroups.length }}单)</span></h3><div class="order-type-tabs"><button v-for="option in [{ value: '', label: '全部' }, ...orderTypeOptions]" :key="option.value || 'all'" type="button" :class="{ active: query.orderType === option.value }" @click="query.orderType = option.value">{{ option.label }}</button></div></div></header>
|
||||
<div v-loading="loading" class="order-table-wrap">
|
||||
<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="托盘数量" 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>
|
||||
<el-table-column label="货柜号" min-width="125"><template #default="{ row }">{{ text(row.first.containerNo) }}</template></el-table-column>
|
||||
<el-table-column label="预约送仓时间" min-width="145"><template #default="{ row }">{{ dateTime(row.first.appointmentAt) }}</template></el-table-column>
|
||||
<el-table-column label="预计装车时间" min-width="145"><template #default="{ row }">{{ dateTime(row.first.expectedLoadingAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="plan-pane" aria-label="装车托盘">
|
||||
<header><div><h3>装车托盘</h3><p>托盘 {{ selected.length }} · 箱数 {{ selectedPackages }} · 立方 {{ selectedVolume.toFixed(2) }}</p></div><el-button disabled><Plus :size="15" />添加装托盘</el-button></header>
|
||||
<el-input v-model="route" maxlength="255" placeholder="排车路线(选填)" />
|
||||
<div class="selected-list">
|
||||
<div v-for="item in selected" :key="String(item.planPalletId)" class="selected-pallet">
|
||||
<div><strong>{{ text(item.palletNo) }}</strong><span>{{ text(item.orderNo) }} · {{ text(item.fbaWarehouseCode) }}</span></div>
|
||||
<div><span>{{ Number(item.packageCount || 0) }}箱 / {{ Number(item.volumeCbm || 0).toFixed(2) }}m³</span><el-button text type="danger" :aria-label="`移除托盘${text(item.palletNo)}`" @click="remove(item)"><Trash2 :size="15" /></el-button></div>
|
||||
|
||||
<section class="detail-pane" aria-label="入仓单托盘和箱号详情">
|
||||
<header><h3>托盘明细 <span>{{ activePallets.length ? `(${activePallets.length}托)` : '' }}</span></h3><span>订单号:{{ text(activePallets[0]?.orderNo) }}</span></header>
|
||||
<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>
|
||||
<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>
|
||||
<template #dropdown><el-dropdown-menu><el-dropdown-item v-for="(container, index) in containers" :key="container.id" :command="container.id" :disabled="container.palletIds.length >= container.capacity">装车托盘 {{ index + 1 }}({{ container.palletIds.length }}/{{ container.capacity }})</el-dropdown-item></el-dropdown-menu></template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<el-empty v-if="!selected.length" :image-size="72" description="请选择左侧托盘" />
|
||||
<el-empty v-if="!activePallets.length" :image-size="64" description="点击左侧入仓单查看托盘和箱号" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="container-pane" aria-label="装车托盘容器">
|
||||
<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-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>
|
||||
<span v-if="!container.palletIds.length" class="drop-hint">拖入托盘,移入后立即锁定</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!containers.length" :image-size="64" description="请先添加装车托盘" />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<template #footer="{ close }"><div class="dialog-footer"><span>已选托盘会在预排车成功后锁定。</span><div><el-button :disabled="saving" @click="close">取消</el-button><el-button type="primary" :loading="saving" :disabled="!selectedIds.length" @click="submit">预排车</el-button></div></div></template>
|
||||
|
||||
<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>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dispatch-query { display: flex; align-items: center; gap: 12px; padding: 18px; border: 1px solid var(--cc-border); border-radius: 6px; background: var(--cc-bg); }
|
||||
.dispatch-query :deep(.el-input), .dispatch-query :deep(.el-select) { width: 210px; }
|
||||
.dispatch-workbench { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(420px, .95fr); gap: 20px; margin-top: 18px; }
|
||||
.candidate-pane, .plan-pane { min-width: 0; }
|
||||
.candidate-pane header, .plan-pane header { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
h3, p { margin: 0; } h3 { font-size: 17px; } h3 span, p { color: var(--cc-muted); font-size: 13px; font-weight: 400; }
|
||||
.plan-pane > .el-input { margin-bottom: 12px; }
|
||||
.selected-list { min-height: 444px; max-height: 444px; overflow-y: auto; border: 1px dashed #cfd5df; border-radius: 6px; background: #fafbfc; }
|
||||
.selected-pallet { display: flex; min-height: 72px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; border-bottom: 1px solid var(--cc-border); background: #fff; }
|
||||
.selected-pallet > div { display: flex; min-width: 0; align-items: center; gap: 12px; } .selected-pallet > div:first-child { align-items: flex-start; flex-direction: column; gap: 4px; }
|
||||
.selected-pallet span { color: var(--cc-muted); font-size: 13px; }
|
||||
.dialog-footer { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 16px; } .dialog-footer > span { color: var(--cc-muted); font-size: 13px; }
|
||||
@media (max-width: 1100px) { .dispatch-workbench { grid-template-columns: 1fr; } }
|
||||
.dispatch-query { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid #e6eaf2; border-radius: 8px; background: #f7f8fb; }
|
||||
.dispatch-query :deep(.el-input), .dispatch-query :deep(.el-select) { width: 190px; }
|
||||
.warehouse-tabs { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.warehouse-tab, .order-type-tabs button { border: 1px solid #dfe4ed; background: #fff; color: #667085; cursor: pointer; transition: all .18s ease; }
|
||||
.warehouse-tab { min-width: 92px; padding: 9px 20px; border-radius: 7px; font-size: 14px; }
|
||||
.warehouse-tab:hover, .order-type-tabs button:hover { border-color: #7654d6; color: #6941c6; }
|
||||
.warehouse-tab.active, .order-type-tabs button.active { border-color: #6f42d8; background: #6f42d8; color: #fff; }
|
||||
.dispatch-workbench { display: grid; grid-template-columns: minmax(550px, 1.5fr) minmax(330px, .85fr) minmax(390px, 1fr); gap: 16px; margin-top: 16px; }
|
||||
.order-pane, .detail-pane, .container-pane { min-width: 0; }
|
||||
.order-pane header, .detail-pane header, .container-pane header { display: flex; min-height: 74px; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
h3, p { margin: 0; } h3 { font-size: 16px; } h3 span, header > span, p { color: var(--cc-muted); font-size: 12px; font-weight: 400; }
|
||||
.order-type-tabs { display: flex; gap: 6px; margin-top: 10px; overflow-x: auto; }
|
||||
.order-type-tabs button { flex: 0 0 auto; padding: 5px 12px; border-radius: 16px; font-size: 12px; }
|
||||
.order-table-wrap { min-height: 520px; }
|
||||
.detail-pane, .container-pane { border-left: 1px solid var(--cc-border); padding-left: 16px; }
|
||||
.pallet-list, .container-list { height: 520px; overflow-y: auto; border: 1px solid var(--cc-border); border-radius: 6px; background: #fafbfc; }
|
||||
.pallet-row { display: grid; grid-template-columns: 20px minmax(0, 1fr) auto; min-height: 94px; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--cc-border); background: #fff; cursor: grab; transition: background .18s ease; }
|
||||
.pallet-row:hover { background: #f8f6ff; }
|
||||
.pallet-row.assigned { background: #f2fbf6; }.drag-handle { color: var(--cc-muted); }
|
||||
.pallet-copy { display: flex; min-width: 0; flex-direction: column; gap: 4px; }.pallet-copy > span, .carton-marks > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.carton-marks { display: flex; min-width: 0; flex-wrap: wrap; gap: 4px; }
|
||||
.container-pane > .el-input { margin-bottom: 10px; }.container-list { height: 460px; padding: 8px; }
|
||||
.loading-container { margin-bottom: 10px; border: 1px dashed #bcc6d5; border-radius: 6px; background: #fff; }
|
||||
.container-head { display: grid; grid-template-columns: 1fr auto 32px; min-height: 44px; align-items: center; gap: 8px; padding: 0 8px 0 12px; background: #f4f6fa; }
|
||||
.container-head span { color: var(--cc-muted); font-size: 12px; }.container-head b { color: var(--el-color-primary); }
|
||||
.container-body { min-height: 76px; padding: 8px; }.drop-hint { display: grid; min-height: 60px; place-items: center; color: var(--cc-muted); font-size: 12px; }
|
||||
.loaded-pallet { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 8px; border-bottom: 1px solid var(--cc-border); }
|
||||
.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; }
|
||||
@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