Files
client-web/src/views/FirstLegOrdersView.vue
T
2026-07-30 21:23:06 +08:00

756 lines
49 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, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { onBeforeRouteLeave, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AlertTriangle, Box, Building2, CheckCircle2, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Search, Send, Trash2 } from '@lucide/vue'
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, type FirstLegOrderStatus } from '@chongchong/shared'
import { useFirstLegOrdersStore } from '@/stores/firstLegOrders'
import { useTenantAdministrationStore } from '@/stores/tenantAdministration'
import { useAuthStore } from '@/stores/auth'
import { createFirstLegPartySnapshot, type FirstLegPartyRole } from '@/data/firstLegParties'
import { firstLegPartySource } from '@/data/firstLegPartySource'
import type { FirstLegOrder, FirstLegOrderDraft } from '@/types/firstLegOrder'
import {
canClientEditFirstLegOrder,
createEmptyCargoItem,
getFirstLegStatusLabel,
getFirstLegStatusTone,
getFirstLegTransportModeLabel,
summarizeFirstLegCargo,
validateFirstLegOrderDraft,
} from '@/utils/firstLegOrder'
const store = useFirstLegOrdersStore()
const tenantStore = useTenantAdministrationStore()
const auth = useAuthStore()
const router = useRouter()
const companyApproved = computed(() => tenantStore.company.auditStatus === 'ACTIVE')
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
const availableWarehouses = computed(() => store.warehouses)
const availableShippers = ref(firstLegPartySource.listAvailable('SHIPPER'))
const availableConsignees = ref(firstLegPartySource.listAvailable('CONSIGNEE'))
const filters = reactive({ keyword: '', status: '' as FirstLegOrderStatus | '', transportMode: '' })
const dateRange = ref<[string, string] | []>([])
const editorOpen = ref(false)
const detailOpen = ref(false)
const editingId = ref<string>()
const openingEditor = ref(false)
const detailId = ref<string>()
const saving = ref(false)
const detailLoading = ref(false)
const detailError = ref('')
const editorBaseline = ref('')
let cargoSequence = 0
function formatDateTime(value: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value || '-'
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
})
}
function emptyDraft(): FirstLegOrderDraft {
return {
referenceNo: '',
transportMode: 'FCL',
businessType: 'EXPORT',
origin: '',
destination: '',
receivingWarehouseId: '',
expectedDepartureDate: '',
shipperPartyId: '',
shipperPartySnapshot: null,
shipper: '',
consigneePartyId: '',
consigneePartySnapshot: null,
consignee: '',
priority: 'NORMAL',
customerRemark: '',
tradeTerm: 'FOB',
destinationCountryCode: 'US',
destinationWarehouseName: '',
pickupMethod: 'SELF_EXPRESS',
channelCode: 'SEA-US-FBA',
channelName: '美国头程渠道',
taxMode: 'BUY_DECLARATION',
customsInspection: false,
destinationType: 'FBA',
amazonWarehouseCode: '',
shipmentId: '',
items: [createEmptyCargoItem(`new-cargo-${++cargoSequence}`)],
}
}
const form = reactive<FirstLegOrderDraft>(emptyDraft())
const serializedDraft = computed(() => JSON.stringify(form))
const editorDirty = computed(() => editorOpen.value && serializedDraft.value !== editorBaseline.value)
const filteredOrders = computed(() => store.orders.filter((order) => {
const keyword = filters.keyword.trim().toLowerCase()
const matchesKeyword = !keyword || [order.orderNo, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(keyword))
const matchesStatus = !filters.status || order.status === filters.status
const matchesMode = !filters.transportMode || order.transportMode === filters.transportMode
const matchesDate = dateRange.value.length !== 2 || (order.createdAt.slice(0, 10) >= dateRange.value[0] && order.createdAt.slice(0, 10) <= dateRange.value[1])
return matchesKeyword && matchesStatus && matchesMode && matchesDate
}))
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId))
const hasActiveFilters = computed(() => Boolean(filters.keyword.trim() || filters.status || filters.transportMode || dateRange.value.length))
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的头程订单' : '还没有头程订单')
async function loadPageData() {
try {
const requests: Promise<unknown>[] = [store.load(), firstLegPartySource.load()]
if (auth.hasPermission('company:profile:view')) requests.push(tenantStore.loadCompanyContext())
await Promise.all(requests)
availableShippers.value = firstLegPartySource.listAvailable('SHIPPER')
availableConsignees.value = firstLegPartySource.listAvailable('CONSIGNEE')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单与往来单位数据加载失败')
}
}
function handleBeforeUnload(event: BeforeUnloadEvent) {
if (!editorDirty.value) return
event.preventDefault()
event.returnValue = ''
}
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
void loadPageData()
})
onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnload))
onBeforeRouteLeave(async () => {
if (!editorDirty.value) return true
return confirmDiscardChanges()
})
function markEditorClean() {
editorBaseline.value = serializedDraft.value
}
function cloneDraft(draft: FirstLegOrderDraft): FirstLegOrderDraft {
return {
...draft,
shipperPartySnapshot: draft.shipperPartySnapshot ? { ...draft.shipperPartySnapshot } : null,
consigneePartySnapshot: draft.consigneePartySnapshot ? { ...draft.consigneePartySnapshot } : null,
items: draft.items.map((item) => ({ ...item })),
}
}
function replaceForm(draft: FirstLegOrderDraft) {
Object.assign(form, cloneDraft(draft))
markEditorClean()
}
function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
return {
referenceNo: order.referenceNo,
transportMode: order.transportMode,
businessType: order.businessType,
origin: order.origin,
destination: order.destination,
receivingWarehouseId: order.receivingWarehouseId,
expectedDepartureDate: order.expectedDepartureDate,
shipperPartyId: order.shipperPartyId,
shipperPartySnapshot: order.shipperPartySnapshot ? { ...order.shipperPartySnapshot } : null,
shipper: order.shipper,
consigneePartyId: order.consigneePartyId,
consigneePartySnapshot: order.consigneePartySnapshot ? { ...order.consigneePartySnapshot } : null,
consignee: order.consignee,
priority: order.priority,
customerRemark: order.customerRemark,
tradeTerm: order.tradeTerm,
destinationCountryCode: order.destinationCountryCode,
destinationWarehouseName: order.destinationWarehouseName,
pickupMethod: order.pickupMethod,
channelCode: order.channelCode,
channelName: order.channelName,
taxMode: order.taxMode,
customsInspection: order.customsInspection,
destinationType: order.destinationType,
amazonWarehouseCode: order.amazonWarehouseCode,
shipmentId: order.shipmentId,
items: order.items.map((item) => ({ ...item })),
}
}
function selectParty(role: FirstLegPartyRole, id: string) {
const party = firstLegPartySource.findAvailable(id, role)
if (!party) return
const snapshot = createFirstLegPartySnapshot(party, role)
if (role === 'SHIPPER') {
form.shipperPartyId = party.id
form.shipperPartySnapshot = snapshot
form.shipper = party.name
} else {
form.consigneePartyId = party.id
form.consigneePartySnapshot = snapshot
form.consignee = party.name
}
}
function clearParty(role: FirstLegPartyRole) {
if (role === 'SHIPPER') {
form.shipperPartyId = ''
form.shipperPartySnapshot = null
form.shipper = ''
} else {
form.consigneePartyId = ''
form.consigneePartySnapshot = null
form.consignee = ''
}
}
function selectShipper(id: string) {
selectParty('SHIPPER', id)
}
function selectConsignee(id: string) {
selectParty('CONSIGNEE', id)
}
function clearShipper() {
clearParty('SHIPPER')
}
function clearConsignee() {
clearParty('CONSIGNEE')
}
async function loadSelectedWarehouse(id: string) {
if (!id) return
try {
await store.loadWarehouseDetail(id)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '仓库地址与收货时间加载失败')
}
}
function openCreate() {
if (availableWarehouses.value.length === 0) {
ElMessage.warning('当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
return
}
editingId.value = undefined
replaceForm(emptyDraft())
editorOpen.value = true
}
async function openEdit(order: FirstLegOrder) {
if (openingEditor.value || !canEditOrder.value || !canClientEditFirstLegOrder(order.status)) return
openingEditor.value = true
try {
const detailedOrder = await store.loadDetail(order.id)
const draft = draftFromOrder(detailedOrder)
const warehouseAvailable = availableWarehouses.value.some((item) => item.id === detailedOrder.receivingWarehouseId)
if (!warehouseAvailable) {
draft.receivingWarehouseId = ''
ElMessage.warning('原平台收货仓当前不可用,请重新选择可用收货仓')
}
editingId.value = detailedOrder.id
replaceForm(draft)
if (warehouseAvailable) void loadSelectedWarehouse(detailedOrder.receivingWarehouseId)
detailOpen.value = false
editorOpen.value = true
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单详情加载失败,暂时无法编辑')
} finally {
openingEditor.value = false
}
}
async function openDetail(order: FirstLegOrder) {
detailId.value = order.id
detailOpen.value = true
detailLoading.value = true
detailError.value = ''
try {
await store.loadDetail(order.id)
} catch (error) {
detailError.value = error instanceof Error ? error.message : '订单详情加载失败'
ElMessage.error(detailError.value)
} finally {
detailLoading.value = false
}
}
async function retryDetail() {
const order = detailId.value ? store.getById(detailId.value) : undefined
if (order) await openDetail(order)
}
async function confirmDiscardChanges() {
try {
await ElMessageBox.confirm('当前订单还有未保存的修改。离开后这些内容将丢失。', '放弃未保存的修改?', {
type: 'warning',
confirmButtonText: '放弃修改',
cancelButtonText: '继续编辑',
})
return true
} catch (error) {
if (error === 'cancel' || error === 'close') return false
throw error
}
}
async function requestCloseEditor(done: () => void) {
if (saving.value) return
if (!editorDirty.value || await confirmDiscardChanges()) done()
}
function closeEditor() {
void requestCloseEditor(() => {
editorOpen.value = false
})
}
function addCargoItem() {
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
}
function removeCargoItem(index: number) {
if (form.items.length === 1) {
ElMessage.warning('订单至少需要保留一条货物明细')
return
}
form.items.splice(index, 1)
}
async function saveDraft() {
if (saving.value) return
const errors = validateFirstLegOrderDraft(form)
if (errors.length > 0) {
ElMessage.warning(errors[0])
return
}
saving.value = true
try {
const order = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value)
editingId.value = order.id
markEditorClean()
editorOpen.value = false
ElMessage.success('订单草稿已保存')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
} finally {
saving.value = false
}
}
async function submitOrder() {
if (saving.value) return
if (!companyApproved.value) {
try {
await ElMessageBox.confirm('企业资料审核通过后才能提交订单。您可以继续保存草稿,或前往企业资料核对并补充信息。', '暂时无法提交订单', {
type: 'warning',
confirmButtonText: '前往企业资料',
cancelButtonText: '继续编辑草稿',
})
markEditorClean()
editorOpen.value = false
await router.push('/company/profile')
} catch (error) {
if (error === 'cancel' || error === 'close') return
throw error
}
return
}
const errors = validateFirstLegOrderDraft(form)
if (errors.length > 0) {
ElMessage.warning(errors[0])
return
}
try {
saving.value = true
const order = await store.submit(cloneDraft(form), tenantStore.company.companyName, editingId.value, tenantStore.company.auditStatus)
markEditorClean()
editorOpen.value = false
detailId.value = order.id
detailOpen.value = true
ElMessage.success('订单已提交,等待平台审核')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '订单提交失败')
} finally {
saving.value = false
}
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.transportMode = ''
dateRange.value = []
}
</script>
<template>
<section class="first-leg-page">
<div class="order-eligibility" :class="companyApproved ? 'is-approved' : 'is-pending'" role="status">
<component :is="companyApproved ? CheckCircle2 : AlertTriangle" :size="20" />
<div><strong>{{ companyApproved ? '企业资料已审核,可正常提交订单' : canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ companyApproved ? '提交后订单将进入平台审核。' : canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
<el-button v-if="!companyApproved && auth.hasPermission('company:profile:view')" link type="primary" @click="router.push('/company/profile')">查看企业资料</el-button>
</div>
<div class="order-metrics" aria-label="订单概览">
<article><span>全部订单</span><strong>{{ store.orders.length }}</strong></article>
<article><span>进行中</span><strong>{{ store.activeCount }}</strong></article>
<article><span>待平台审核</span><strong>{{ store.pendingCount }}</strong></article>
</div>
<div class="order-toolbar client-card">
<div class="order-filters">
<el-input v-model="filters.keyword" clearable placeholder="订单号、参考号、路线或品名" aria-label="订单关键词">
<template #prefix><Search :size="16" /></template>
</el-input>
<el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="订单状态">
<el-option v-for="item in FIRST_LEG_ORDER_STATUSES" :key="item.code" :label="item.label" :value="item.code" />
</el-select>
<el-select v-model="filters.transportMode" clearable placeholder="全部运输方式" aria-label="运输方式">
<el-option v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :label="item.label" :value="item.code" />
</el-select>
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="创建开始" end-placeholder="创建结束" aria-label="订单创建日期" />
</div>
<div class="order-toolbar-actions">
<el-button @click="resetFilters"><RotateCcw :size="16" />重置</el-button>
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :disabled="availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
</div>
</div>
<div v-loading="store.loading" class="client-card order-table-card">
<div v-if="store.loadError" class="order-load-state" role="alert">
<AlertTriangle :size="24" />
<div><strong>订单数据加载失败</strong><span>{{ store.loadError }}</span></div>
<el-button :loading="store.loading" @click="loadPageData">重新加载</el-button>
</div>
<template v-else>
<el-table v-if="filteredOrders.length" class="desktop-order-table" :data="filteredOrders" row-key="id">
<el-table-column label="操作" width="180" fixed="left">
<template #default="{ row }">
<div class="order-row-actions">
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
<el-button v-if="canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" />编辑</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="订单" min-width="190">
<template #default="{ row }">
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无客户参考号' }}</span>
</template>
</el-table-column>
<el-table-column label="运输路线" min-width="190">
<template #default="{ row }"><strong>{{ (row as FirstLegOrder).origin }} → {{ (row as FirstLegOrder).destination }}</strong><span class="order-reference">{{ getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) }}</span></template>
</el-table-column>
<el-table-column label="收货仓" min-width="120" prop="receivingWarehouseName" />
<el-table-column label="货物" min-width="150">
<template #default="{ row }">{{ (row as FirstLegOrder).items[0]?.cargoNameCn || '-' }}<span v-if="(row as FirstLegOrder).items.length > 1" class="order-reference">等 {{ (row as FirstLegOrder).items.length }} 项</span></template>
</el-table-column>
<el-table-column label="状态" width="120">
<template #default="{ row }"><el-tag :type="getFirstLegStatusTone((row as FirstLegOrder).status)" effect="light">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</el-tag></template>
</el-table-column>
<el-table-column label="负责人" prop="operatorName" width="110" />
<el-table-column label="更新时间" width="155"><template #default="{ row }">{{ formatDateTime((row as FirstLegOrder).updatedAt) }}</template></el-table-column>
</el-table>
<div v-if="filteredOrders.length" class="mobile-order-list">
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '未提交草稿' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
<span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span>
<dl><div><dt>运输路线</dt><dd>{{ order.origin }} → {{ order.destination }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName }}</dd></div><div><dt>货物</dt><dd>{{ order.items[0]?.cargoNameCn || '-' }}<span v-if="order.items.length > 1">等 {{ order.items.length }} 项</span></dd></div><div><dt>负责人</dt><dd>{{ order.operatorName }}</dd></div></dl>
<footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer>
</article>
</div>
<el-empty v-else :description="emptyDescription">
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :disabled="availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓' }}</el-button>
</el-empty>
</template>
</div>
<el-drawer v-model="editorOpen" class="first-leg-editor" :title="editingId ? '编辑订单草稿' : '创建头程订单'" size="760px" destroy-on-close :before-close="requestCloseEditor">
<el-form label-position="top" class="order-form">
<section class="order-form-section">
<h3>运输信息</h3>
<div class="order-form-grid">
<el-form-item label="运输方式" required><el-select v-model="form.transportMode"><el-option v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :label="item.label" :value="item.code" /></el-select></el-form-item>
<el-form-item label="业务类型" required><el-radio-group v-model="form.businessType"><el-radio-button value="EXPORT">出口</el-radio-button><el-radio-button value="IMPORT">进口</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="起运地" required><el-input v-model="form.origin" placeholder="例如:宁波" /></el-form-item>
<el-form-item label="目的地" required><el-input v-model="form.destination" placeholder="例如:洛杉矶" /></el-form-item>
<el-form-item label="平台收货仓" required><el-select v-model="form.receivingWarehouseId" filterable placeholder="请选择送货仓库" @change="loadSelectedWarehouse"><el-option v-for="warehouse in availableWarehouses" :key="warehouse.id" :label="`${warehouse.name} · ${warehouse.city}`" :value="warehouse.id" /></el-select></el-form-item>
<el-form-item label="期望出发日期"><el-date-picker v-model="form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" /></el-form-item>
<el-form-item label="客户参考号"><el-input v-model="form.referenceNo" maxlength="100" placeholder="贵司内部单号(选填)" /></el-form-item>
<el-form-item label="优先级"><el-select v-model="form.priority"><el-option label="普通" value="NORMAL" /><el-option label="紧急" value="URGENT" /><el-option label="加急" value="EXPEDITED" /></el-select></el-form-item>
<el-form-item label="目的国代码" required><el-input v-model="form.destinationCountryCode" maxlength="2" placeholder="例如:US" /></el-form-item>
<el-form-item label="目的仓名称" required><el-input v-model="form.destinationWarehouseName" maxlength="128" placeholder="例如:ONT8" /></el-form-item>
<el-form-item label="渠道编码" required><el-input v-model="form.channelCode" maxlength="64" /></el-form-item>
<el-form-item label="渠道名称" required><el-input v-model="form.channelName" maxlength="128" /></el-form-item>
<el-form-item label="提货方式" required><el-select v-model="form.pickupMethod"><el-option label="自送快递" value="SELF_EXPRESS" /><el-option label="上门提货" value="PICKUP" /><el-option label="自送卡车" value="SELF_TRUCK" /></el-select></el-form-item>
<el-form-item label="报关模式" required><el-select v-model="form.taxMode"><el-option label="买单报关" value="BUY_DECLARATION" /><el-option label="退税报关" value="TAX_REFUND" /></el-select></el-form-item>
<el-form-item label="目的仓类型" required><el-select v-model="form.destinationType"><el-option label="FBA" value="FBA" /><el-option label="海外仓" value="OVERSEAS" /><el-option label="私人地址" value="PRIVATE" /></el-select></el-form-item>
<el-form-item label="贸易术语"><el-input v-model="form.tradeTerm" maxlength="16" placeholder="例如:FOB" /></el-form-item>
<el-form-item v-if="form.destinationType === 'FBA'" label="Amazon 仓库编码"><el-input v-model="form.amazonWarehouseCode" maxlength="32" placeholder="例如:ONT8" /></el-form-item>
<el-form-item v-if="form.destinationType === 'FBA'" label="Shipment ID"><el-input v-model="form.shipmentId" maxlength="64" /></el-form-item>
</div>
<div v-if="selectedWarehouse" class="selected-warehouse"><MapPin :size="17" /><div><strong>{{ selectedWarehouse.name }}</strong><span>{{ selectedWarehouse.province }}{{ selectedWarehouse.city }} · {{ selectedWarehouse.address }}</span><small>收货时间:{{ selectedWarehouse.businessHours }}</small></div></div>
</section>
<section class="order-form-section">
<div class="order-section-heading"><div><h3>收发货单位</h3><p>仅可选择平台已启用的往来单位;下方联系人和地址调整只影响本订单快照。</p></div></div>
<div class="party-editor-grid">
<article class="party-editor">
<header><span><Building2 :size="17" /><strong>发货单位</strong></span><small>必填</small></header>
<el-form-item label="选择发货单位" required>
<el-select v-model="form.shipperPartyId" filterable clearable placeholder="请选择已启用的发货单位" @change="selectShipper" @clear="clearShipper">
<el-option v-if="form.shipperPartySnapshot && !availableShippers.some((party) => party.id === form.shipperPartyId)" :label="`${form.shipperPartySnapshot.name} · 历史快照`" :value="form.shipperPartyId" disabled />
<el-option v-for="party in availableShippers" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" />
</el-select>
</el-form-item>
<div v-if="form.shipperPartySnapshot" class="party-snapshot-fields">
<div class="party-name"><strong>{{ form.shipperPartySnapshot.name }}</strong><span>{{ form.shipperPartySnapshot.code }}</span></div>
<el-form-item label="联系人"><el-input v-model="form.shipperPartySnapshot.contactName" maxlength="50" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.shipperPartySnapshot.phone" maxlength="30" /></el-form-item>
<el-form-item label="联系邮箱"><el-input v-model="form.shipperPartySnapshot.email" maxlength="100" /></el-form-item>
<el-form-item label="本单发货地址"><el-input v-model="form.shipperPartySnapshot.address" maxlength="300" /></el-form-item>
</div>
<div v-else class="party-empty">选择后可核对本单联系人和发货地址</div>
</article>
<article class="party-editor">
<header><span><Building2 :size="17" /><strong>收货单位</strong></span><small>必填</small></header>
<el-form-item label="选择收货单位" required>
<el-select v-model="form.consigneePartyId" filterable clearable placeholder="请选择已启用的收货单位" @change="selectConsignee" @clear="clearConsignee">
<el-option v-if="form.consigneePartySnapshot && !availableConsignees.some((party) => party.id === form.consigneePartyId)" :label="`${form.consigneePartySnapshot.name} · 历史快照`" :value="form.consigneePartyId" disabled />
<el-option v-for="party in availableConsignees" :key="party.id" :label="`${party.name} · ${party.code}`" :value="party.id" />
</el-select>
</el-form-item>
<div v-if="form.consigneePartySnapshot" class="party-snapshot-fields">
<div class="party-name"><strong>{{ form.consigneePartySnapshot.name }}</strong><span>{{ form.consigneePartySnapshot.code }}</span></div>
<el-form-item label="联系人"><el-input v-model="form.consigneePartySnapshot.contactName" maxlength="50" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.consigneePartySnapshot.phone" maxlength="30" /></el-form-item>
<el-form-item label="联系邮箱"><el-input v-model="form.consigneePartySnapshot.email" maxlength="100" /></el-form-item>
<el-form-item label="本单收货地址"><el-input v-model="form.consigneePartySnapshot.address" maxlength="300" /></el-form-item>
</div>
<div v-else class="party-empty">选择后可核对本单联系人和收货地址</div>
</article>
</div>
</section>
<section class="order-form-section">
<div class="order-section-heading"><div><h3>货物明细</h3><p>总件数、重量和体积将在提交时由服务端汇总。</p></div><el-button @click="addCargoItem"><Plus :size="16" />添加货物</el-button></div>
<article v-for="(item, index) in form.items" :key="item.id" class="cargo-editor-card">
<header><strong>货物 {{ index + 1 }}</strong><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="16" />删除</el-button></header>
<div class="cargo-form-grid">
<el-form-item label="中文品名" required><el-input v-model="item.cargoNameCn" /></el-form-item>
<el-form-item label="英文品名"><el-input v-model="item.cargoNameEn" /></el-form-item>
<el-form-item label="客户 SKU"><el-input v-model="item.sku" /></el-form-item>
<el-form-item label="包装类型" required><el-select v-model="item.packageType"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item>
<el-form-item label="HS Code"><el-input v-model="item.hsCode" maxlength="20" /></el-form-item>
<el-form-item label="件数" required><el-input-number v-model="item.pieces" :min="1" controls-position="right" /></el-form-item>
<el-form-item label="箱数"><el-input-number v-model="item.packageCount" :min="0" controls-position="right" /></el-form-item>
<el-form-item label="毛重 (kg)" required><el-input-number v-model="item.grossWeightKg" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="体积 (m³)" required><el-input-number v-model="item.volumeCbm" :min="0" :precision="3" controls-position="right" /></el-form-item>
<el-form-item label="申报货值"><el-input-number v-model="item.declaredValue" :min="0" :precision="2" controls-position="right" /></el-form-item>
<el-form-item label="币种"><el-input v-model="item.currency" maxlength="3" /></el-form-item>
<el-form-item label="危险品"><el-switch v-model="item.dangerousGoods" /></el-form-item>
<template v-if="form.transportMode === 'FCL'">
<el-form-item label="箱型" required><el-select v-model="item.containerType"><el-option v-for="type in ['20GP', '40GP', '40HQ']" :key="type" :label="type" :value="type" /></el-select></el-form-item>
<el-form-item label="柜量" required><el-input-number v-model="item.containerCount" :min="0" controls-position="right" /></el-form-item>
</template>
<el-form-item label="货物备注" class="cargo-remark"><el-input v-model="item.remark" maxlength="300" /></el-form-item>
</div>
</article>
</section>
<section class="order-form-section">
<h3>订单备注</h3>
<el-form-item label="客户备注"><el-input v-model="form.customerRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="平台工作人员可以看到该备注" /></el-form-item>
</section>
</el-form>
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companyApproved ? '提交审核' : '提交前需完成资料审核' }}</el-button></div></template>
</el-drawer>
<el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="680px">
<div v-loading="detailLoading" class="order-detail-content">
<div v-if="detailError" class="order-load-state is-compact" role="alert">
<AlertTriangle :size="24" />
<div><strong>订单详情加载失败</strong><span>{{ detailError }}</span></div>
<el-button :loading="detailLoading" @click="retryDetail">重新加载</el-button>
</div>
<template v-else-if="detailOrder">
<div class="order-detail-heading">
<div><span>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</span><h2>{{ detailOrder.orderNo || '未提交草稿' }}</h2><p>{{ detailOrder.origin }} → {{ detailOrder.destination }}</p></div>
<el-tag :type="getFirstLegStatusTone(detailOrder.status)" effect="light">{{ getFirstLegStatusLabel(detailOrder.status) }}</el-tag>
</div>
<dl class="order-detail-grid">
<div><dt>客户参考号</dt><dd>{{ detailOrder.referenceNo || '-' }}</dd></div><div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
<div><dt>发货方</dt><dd>{{ detailOrder.shipperPartySnapshot?.name || detailOrder.shipper }}<span v-if="detailOrder.shipperPartySnapshot" class="detail-subline">{{ detailOrder.shipperPartySnapshot.code }} · {{ detailOrder.shipperPartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.shipperPartySnapshot.phone || '未填写电话' }}<br />{{ detailOrder.shipperPartySnapshot.countryRegion }} {{ detailOrder.shipperPartySnapshot.province }} {{ detailOrder.shipperPartySnapshot.city }} {{ detailOrder.shipperPartySnapshot.address }}</span></dd></div>
<div><dt>收货方</dt><dd>{{ detailOrder.consigneePartySnapshot?.name || detailOrder.consignee }}<span v-if="detailOrder.consigneePartySnapshot" class="detail-subline">{{ detailOrder.consigneePartySnapshot.code }} · {{ detailOrder.consigneePartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.consigneePartySnapshot.phone || '未填写电话' }}<br />{{ detailOrder.consigneePartySnapshot.countryRegion }} {{ detailOrder.consigneePartySnapshot.province }} {{ detailOrder.consigneePartySnapshot.city }} {{ detailOrder.consigneePartySnapshot.address }}</span></dd></div>
<div><dt>平台收货仓</dt><dd>{{ detailOrder.receivingWarehouseSnapshot?.name || detailOrder.receivingWarehouseName }}<span v-if="detailOrder.receivingWarehouseSnapshot" class="detail-subline">{{ detailOrder.receivingWarehouseSnapshot.code }} · {{ detailOrder.receivingWarehouseSnapshot.province }}{{ detailOrder.receivingWarehouseSnapshot.city }}{{ detailOrder.receivingWarehouseSnapshot.address }}</span></dd></div><div><dt>负责人</dt><dd>{{ detailOrder.operatorName }}</dd></div>
<div><dt>期望出发</dt><dd>{{ detailOrder.expectedDepartureDate || '-' }}</dd></div><div><dt>ETD / ETA</dt><dd>{{ detailOrder.etd || '-' }} / {{ detailOrder.eta || '-' }}</dd></div>
</dl>
<section class="detail-section">
<h3>履约快递信息</h3>
<div v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><PackageCheck :size="20" /><div><span>快递公司</span><strong>{{ detailOrder.expressCompanyName || '待平台补充' }}</strong></div><div><span>快递单号</span><strong>{{ detailOrder.trackingNumber || '待平台补充' }}</strong></div></div>
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
</section>
<section class="detail-section"><h3>货物汇总</h3><div v-if="detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><ul class="cargo-detail-list"><li v-for="item in detailOrder.items" :key="item.id"><strong>{{ item.cargoNameCn }}</strong><span>{{ item.sku || '无 SKU' }} · {{ item.pieces }} 件 · {{ item.grossWeightKg }} kg</span></li></ul></section>
<section class="detail-section"><h3>处理时间线</h3><el-timeline><el-timeline-item v-for="item in [...detailOrder.timeline].reverse()" :key="item.id" :timestamp="item.occurredAt" placement="top"><strong>{{ item.title }}</strong><p>{{ item.description }}</p><span>{{ item.operatorName }}</span></el-timeline-item></el-timeline></section>
</template>
</div>
<template #footer><div v-if="detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button></div></template>
</el-drawer>
</section>
</template>
<style scoped>
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
.order-eligibility svg { flex: 0 0 20px; margin-top: 1px; }
.order-eligibility div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
.order-eligibility strong { color: var(--navy); font-size: 13px; }
.order-eligibility span { color: var(--muted); font-size: 11px; line-height: 1.55; }
.order-eligibility.is-approved { border-color: #bcd9d2; background: #eff8f5; color: #20745f; }
.order-eligibility.is-pending { border-color: #e5d2a5; background: #fff9eb; color: #9a6a12; }
.order-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.order-metrics article { display: flex; align-items: center; justify-content: space-between; min-height: 76px; padding: 16px 20px; border: 1px solid var(--border); background: #fff; }
.order-metrics span { color: var(--muted); font-size: 12px; }
.order-metrics strong { color: var(--navy); font-size: 25px; }
.order-toolbar { display: flex; align-items: stretch; flex-direction: column; gap: 12px; padding: 16px; }
.order-filters { display: grid; width: 100%; grid-template-columns: minmax(190px, 1.35fr) minmax(130px, .65fr) minmax(150px, .75fr) minmax(230px, 1fr); gap: 10px; }
.order-toolbar-actions, .order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; }
.order-table-card { padding: 0; overflow: hidden; }
.order-load-state { display: grid; min-height: 220px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 28px; color: #9a5a09; }
.order-load-state > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
.order-load-state strong { color: var(--navy); font-size: 14px; }
.order-load-state span { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.55; }
.order-load-state.is-compact { min-height: 180px; }
.order-detail-content { min-height: 180px; }
.mobile-order-list { display: none; }
.order-number-link { display: block; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-weight: 700; text-align: left; }
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
.order-reference { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
.order-form { padding: 0 2px 28px; }
.order-form-section + .order-form-section { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border); }
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
.order-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; }
.order-form-grid :deep(.el-select), .order-form-grid :deep(.el-date-editor), .cargo-form-grid :deep(.el-select), .cargo-form-grid :deep(.el-input-number) { width: 100%; }
.selected-warehouse { display: flex; gap: 10px; margin-top: -2px; padding: 12px 14px; border: 1px solid #cfe1dd; background: #f3f8f7; color: var(--primary); }
.selected-warehouse svg { flex: 0 0 17px; margin-top: 2px; }
.selected-warehouse div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
.selected-warehouse span, .selected-warehouse small { color: var(--muted); font-size: 11px; line-height: 1.5; }
.party-editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.party-editor { min-width: 0; padding: 16px; border: 1px solid var(--border); background: #f9fbfb; }
.party-editor > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.party-editor > header span { display: flex; min-width: 0; align-items: center; gap: 8px; color: var(--navy); }
.party-editor > header svg { flex: 0 0 17px; color: var(--primary); }
.party-editor > header small { color: var(--muted); font-size: 11px; }
.party-editor :deep(.el-select) { width: 100%; }
.party-snapshot-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.party-name { display: flex; min-width: 0; grid-column: 1 / -1; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.party-name strong { min-width: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
.party-name span { flex: 0 0 auto; color: var(--muted); font-size: 10px; }
.party-snapshot-fields .el-form-item:last-child { grid-column: 1 / -1; }
.party-empty { display: grid; min-height: 150px; place-items: center; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; text-align: center; }
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.order-section-heading h3 { margin-bottom: 5px; }
.order-section-heading p { margin: 0; color: var(--muted); font-size: 11px; }
.cargo-editor-card { padding: 16px; border: 1px solid var(--border); background: #f9fbfb; }
.cargo-editor-card + .cargo-editor-card { margin-top: 12px; }
.cargo-editor-card header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.cargo-form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 14px; }
.cargo-remark { grid-column: 1 / -1; }
.order-detail-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
.order-detail-heading span { color: var(--muted); font-size: 11px; }
.order-detail-heading h2 { margin: 6px 0; font-size: 21px; }
.order-detail-heading p { margin: 0; color: var(--muted); }
.order-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; margin: 18px 0; border: 1px solid var(--border); }
.order-detail-grid div { padding: 13px; border-bottom: 1px solid var(--border); }
.order-detail-grid div:nth-child(odd) { border-right: 1px solid var(--border); }
.order-detail-grid dt { color: var(--muted); font-size: 11px; }
.order-detail-grid dd { margin: 5px 0 0; font-size: 13px; font-weight: 600; }
.detail-subline { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; line-height: 1.5; }
.detail-section { margin-top: 22px; }
.detail-section h3 { margin: 0 0 12px; font-size: 15px; }
.express-information { display: grid; grid-template-columns: auto repeat(2, minmax(0, 1fr)); align-items: center; gap: 16px; padding: 15px 16px; background: #f2f7f6; color: var(--primary); }
.express-information > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
.express-information span { color: var(--muted); font-size: 11px; }
.express-information strong { overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
.express-empty { display: flex; align-items: flex-start; gap: 12px; padding: 15px 16px; border: 1px dashed #cbd5da; color: var(--muted); }
.express-empty svg { flex: 0 0 20px; color: var(--primary); }
.express-empty div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
.express-empty strong { color: var(--navy); font-size: 13px; }
.express-empty span { font-size: 11px; line-height: 1.5; }
.cargo-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--border); }
.cargo-summary div { display: flex; min-height: 74px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border-right: 1px solid var(--border); }
.cargo-summary div:last-child { border-right: 0; }
.cargo-summary span { color: var(--muted); font-size: 11px; }
.cargo-detail-list { margin: 10px 0 0; padding: 0; list-style: none; }
.cargo-detail-list li { display: flex; justify-content: space-between; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
.cargo-detail-list span, :deep(.el-timeline-item__content p), :deep(.el-timeline-item__content > span) { color: var(--muted); }
:deep(.el-timeline-item__content p) { margin: 5px 0; font-size: 12px; }
:deep(.el-timeline-item__content > span) { font-size: 11px; }
@media (max-width: 1100px) { .order-toolbar-actions { align-self: flex-end; } .order-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 1280px) {
.desktop-order-table { display: none; }
.mobile-order-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 10px; }
.mobile-order-card { min-width: 0; padding: 14px; border: 1px solid var(--border); background: #fff; }
.mobile-order-card header, .mobile-order-card footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mobile-order-card header button { min-width: 0; overflow: hidden; padding: 0; border: 0; background: none; color: var(--primary); font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
.mobile-order-card header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
.mobile-order-card dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
.mobile-order-card dl div { min-width: 0; }
.mobile-order-card dt, .mobile-order-card footer > span { color: var(--muted); font-size: 10px; }
.mobile-order-card dd { overflow: hidden; margin: 4px 0 0; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.mobile-order-card dd span { margin-left: 4px; color: var(--muted); font-weight: 400; }
.mobile-order-card footer { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
.mobile-order-card footer > div { display: flex; }
}
@media (max-width: 720px) {
.order-eligibility { flex-wrap: wrap; }
.order-eligibility .el-button { width: 100%; min-height: 44px; }
.order-metrics { grid-template-columns: 1fr; }
.order-metrics article { min-height: 60px; }
.order-filters, .order-form-grid, .cargo-form-grid, .order-detail-grid, .party-editor-grid, .party-snapshot-fields { grid-template-columns: 1fr; }
.order-toolbar-actions { width: 100%; align-self: stretch; }
.order-toolbar-actions :deep(.el-button) { min-height: 44px; flex: 1; }
.editor-actions :deep(.el-button) { min-height: 44px; }
.desktop-order-table { display: none; }
.order-load-state { min-height: 240px; grid-template-columns: 1fr; justify-items: start; }
.order-load-state .el-button { width: 100%; min-height: 44px; }
.mobile-order-list { grid-template-columns: 1fr; }
.mobile-order-card { padding: 14px; border: 1px solid var(--border); background: #fff; }
.mobile-order-card header, .mobile-order-card footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mobile-order-card header button { padding: 0; border: 0; background: none; color: var(--primary); font-weight: 750; }
.mobile-order-card header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
.mobile-order-card dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
.mobile-order-card dl div { min-width: 0; }
.mobile-order-card dt, .mobile-order-card footer > span { color: var(--muted); font-size: 10px; }
.mobile-order-card dd { overflow: hidden; margin: 4px 0 0; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
.mobile-order-card dd span { margin-left: 4px; color: var(--muted); font-weight: 400; }
.mobile-order-card footer { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
.mobile-order-card footer > div { display: flex; }
.order-detail-grid div:nth-child(odd) { border-right: 0; }
.party-name, .party-snapshot-fields .el-form-item:last-child { grid-column: auto; }
.express-information { grid-template-columns: auto 1fr; }
.express-information > div:last-child { grid-column: 2; }
.cargo-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.cargo-summary div:nth-child(2) { border-right: 0; }
.cargo-summary div:nth-child(-n+2) { border-bottom: 1px solid var(--border); }
.cargo-detail-list li { flex-direction: column; }
:global(.first-leg-editor), :global(.first-leg-detail) { width: 100% !important; }
}
</style>