Files
admin-web/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue
T

309 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ArrowRight, GripVertical, Plus, Trash2 } from '@lucide/vue'
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'
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, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches'
import { formatOverseasDateTime } from '../lastMilePresentation'
import type { InboundOrderListItem } from '../types'
import OrderReferenceText from './OrderReferenceText.vue'
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: [] }>()
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 palletSnapshots = ref<Record<string, DispatchPalletCandidateDto>>({})
const activeOrderKey = ref('')
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')
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) => 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 || '—' }
function number(value: unknown, digits = 0) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' }
function dateTime(value: unknown) { return formatOverseasDateTime(value) }
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, draftToken: ensureDraftToken() })
rememberPallets(candidates.value)
activeOrderKey.value = orderGroups.value[0]?.key || ''
} catch (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 addContainer() { selectedVehicle.value = ''; vehicleDialogOpen.value = true }
async function confirmAddContainer() {
const spec = selectedVehicleSpec.value
if (!spec) { ElMessage.warning('请选择车型'); return }
try {
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) {
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 nextWeight = containerWeightLb(target) + weightLb(item)
if (nextWeight > target.maxWeightLb * 1.1) ElMessage.error(`${target.vehicleLabel}重量将达到 ${nextWeight.toFixed(0)}LB,已超过限重10%,请注意调整`)
else 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 }
busyPalletIds.value.push(id); mutationError.value = null
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(id: string) {
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]) {
await removePallet(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) {
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 (!assignedIds.value.length) { ElMessage.warning('请至少移入一个托盘'); return }
saving.value = true; mutationError.value = null
try {
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
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) => { 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], () => {
activeOrderKey.value = orderGroups.value[0]?.key || ''
})
</script>
<template>
<OperationErrorToast :details="mutationError" />
<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.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>
<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="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="170"><template #default="{ row }"><OrderReferenceText :value="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="重量(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>
<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>
<section class="detail-pane" aria-label="入仓单托盘和箱号详情">
<header><h3>托盘明细 <span>{{ activePallets.length ? `(${activePallets.length}托)` : '' }}</span></h3><span class="detail-order-reference">订单号:<OrderReferenceText :value="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(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>
<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="!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" :class="{ 'loading-container--overweight': containerWeightLb(container) > container.maxWeightLb * 1.1 }" @dragover.prevent @drop.prevent="dropInto($event, container.id)">
<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 <em v-if="containerWeightLb(container) > container.maxWeightLb * 1.1">超限10%</em></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 || '托盘号待补充') }}</strong><OrderReferenceText :value="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>
<el-empty v-if="!containers.length" :image-size="64" description="请先添加装车托盘" />
</div>
</aside>
</div>
<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>
.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; }
.detail-order-reference { display: flex; min-width: 0; max-width: 240px; align-items: baseline; gap: 3px; }
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; }
.loading-container--overweight { border-color: var(--el-color-danger); }
.loading-container--overweight .container-head { background: var(--el-color-danger-light-9); }
.loading-container--overweight .container-head span,.loading-container--overweight .container-head b { color: var(--el-color-danger); }
.container-head em { margin-left: 4px; color: var(--el-color-danger); font-style: normal; font-weight: 700; }
.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: 1; 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>