|
|
|
@@ -9,6 +9,7 @@ import DataTable, { type DataTableColumn } from '@/components/common/DataTable.v
|
|
|
|
|
import { ApiError } from '@/api/http'
|
|
|
|
|
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
|
|
|
|
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
|
|
|
|
import InboundCodeAssociationDialog from '@/modules/first-leg/components/InboundCodeAssociationDialog.vue'
|
|
|
|
|
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
|
|
|
|
|
import CustomsDocumentsDialog from '@/modules/first-leg/components/CustomsDocumentsDialog.vue'
|
|
|
|
|
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
|
|
|
|
@@ -193,6 +194,8 @@ const customsByOrder = reactive<Record<string, MerchantCustomsContainer | null |
|
|
|
|
|
const customsEligibilityPending = new Set<string>()
|
|
|
|
|
const customsDialogOpen = ref(false)
|
|
|
|
|
const customsDialogOrderId = ref('')
|
|
|
|
|
const inboundAssociationOpen = ref(false)
|
|
|
|
|
const inboundAssociationCode = ref('')
|
|
|
|
|
const supportingDataLoading = ref(false)
|
|
|
|
|
const supportingDataErrors = ref<string[]>([])
|
|
|
|
|
const destinationWarehousesLoading = ref(false)
|
|
|
|
@@ -335,7 +338,7 @@ const packageTypeLabels: Readonly<Record<string, string>> = {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayValue(value: string | null | undefined) {
|
|
|
|
|
return value?.trim() || '-'
|
|
|
|
|
return value?.trim() || '—'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function emptyDraft(): FirstLegOrderDraft {
|
|
|
|
@@ -494,6 +497,13 @@ function openCustomsDocuments(order: FirstLegOrder) {
|
|
|
|
|
customsDialogOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openInboundCodeAssociation(inboundCode: string) {
|
|
|
|
|
const code = inboundCode.trim()
|
|
|
|
|
if (!code) return
|
|
|
|
|
inboundAssociationCode.value = code
|
|
|
|
|
inboundAssociationOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateCustomsDetail(value: MerchantCustomsContainer) {
|
|
|
|
|
if (customsDialogOrderId.value) customsByOrder[customsDialogOrderId.value] = value
|
|
|
|
|
}
|
|
|
|
@@ -522,7 +532,7 @@ const progressSummaryItems = computed(() => {
|
|
|
|
|
{ label: '实测体积', value: formatMetric(summary.actualVolumeCbm ?? inbound?.actualVolumeCbm, 'm³') },
|
|
|
|
|
{ label: '预装清单', value: preloadStatus ? `${preloadStatus}${summary.preloadItemCount == null ? '' : ` · ${summary.preloadItemCount} 项`}` : '待更新' },
|
|
|
|
|
{ label: '拣货进度', value: pickingProgress },
|
|
|
|
|
{ label: '柜次进度', value: summary.cabinetNo || summary.containerNo || '待更新' },
|
|
|
|
|
{ label: '柜次进度', value: summary.cabinetNo || summary.containerNo || '—' },
|
|
|
|
|
{ label: '船期摘要', value: transportReference || '待更新' },
|
|
|
|
|
{ label: '计费重量', value: formatMetric(summary.chargeableWeightKg, 'kg') },
|
|
|
|
|
{ label: '后续履约', value: summary.deliveryTrackingNo || (summary.deliveryCompletedAt ? `海外回传于 ${formatDateTime(summary.deliveryCompletedAt)}` : '待海外同步') },
|
|
|
|
@@ -1220,7 +1230,7 @@ function removeCargoItem(index: number) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayListValue(value: string | number | null | undefined) {
|
|
|
|
|
return value === '' || value == null ? '-' : String(value)
|
|
|
|
|
return value === '' || value == null ? '—' : String(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function orderMatchesCargoKeyword(order: FirstLegOrder, keyword: string) {
|
|
|
|
@@ -1653,9 +1663,18 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<template #order="{ row }">
|
|
|
|
|
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '未提交草稿'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
|
|
|
|
<button class="order-number-link table-code" type="button" :title="(row as FirstLegOrder).orderNo || '—'" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '—' }}</button>
|
|
|
|
|
</template>
|
|
|
|
|
<template #inboundCode="{ row }">
|
|
|
|
|
<button
|
|
|
|
|
v-if="(row as FirstLegOrder).businessSummary.inboundCode"
|
|
|
|
|
class="inbound-code-link table-ellipsis table-code"
|
|
|
|
|
type="button"
|
|
|
|
|
:title="`查看入仓码 ${(row as FirstLegOrder).businessSummary.inboundCode} 的关联摘要`"
|
|
|
|
|
@click="openInboundCodeAssociation((row as FirstLegOrder).businessSummary.inboundCode)"
|
|
|
|
|
>{{ (row as FirstLegOrder).businessSummary.inboundCode }}</button>
|
|
|
|
|
<span v-else>—</span>
|
|
|
|
|
</template>
|
|
|
|
|
<template #inboundCode="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).businessSummary.inboundCode)">{{ displayListValue((row as FirstLegOrder).businessSummary.inboundCode) }}</span></template>
|
|
|
|
|
<template #channel="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).channelName)">{{ displayListValue((row as FirstLegOrder).channelName) }}</span></template>
|
|
|
|
|
<template #transportMode="{ row }">{{ (row as FirstLegOrder).transportMode ? getFirstLegTransportModeLabel((row as FirstLegOrder).transportMode) : '-' }}</template>
|
|
|
|
|
<template #pickupMethod="{ row }">{{ pickupMethodLabels[(row as FirstLegOrder).pickupMethod] }}</template>
|
|
|
|
@@ -1695,10 +1714,11 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
<div v-loading="store.loading" class="mobile-order-surface">
|
|
|
|
|
<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 v-if="order.businessSummary.inboundCode" class="order-reference">入仓码 {{ order.businessSummary.inboundCode }}</span>
|
|
|
|
|
<span class="order-reference">{{ order.referenceNo || '无 Reference ID' }}</span>
|
|
|
|
|
<dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[order.destinationType] }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '待更新' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输方式</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : '待更新' }}</dd></div><div><dt>交货方式</dt><dd>{{ pickupMethodLabels[order.pickupMethod] }}</dd></div><div><dt>预约提货时间</dt><dd>{{ order.pickupMethod === 'PICKUP' && order.requestedPickupAt ? formatDateTime(order.requestedPickupAt) : '-' }}</dd></div><div v-if="order.businessSummary.cabinetNo || order.businessSummary.containerNo"><dt>柜号 / 柜次</dt><dd>{{ order.businessSummary.cabinetNo || order.businessSummary.containerNo }}</dd></div><div v-if="order.businessSummary.vesselName || order.businessSummary.voyageNo"><dt>船名 / 航次</dt><dd>{{ [order.businessSummary.vesselName, order.businessSummary.voyageNo].filter(Boolean).join(' · ') }}</dd></div><div v-if="order.businessSummary.sailingAt"><dt>预计船期</dt><dd>{{ formatDateTime(order.businessSummary.sailingAt) }}</dd></div></dl>
|
|
|
|
|
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '—' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
|
|
|
|
|
<button v-if="order.businessSummary.inboundCode" class="order-reference inbound-code-link" type="button" @click="openInboundCodeAssociation(order.businessSummary.inboundCode)">入仓码 {{ order.businessSummary.inboundCode }}</button>
|
|
|
|
|
<span v-else class="order-reference">入仓码 —</span>
|
|
|
|
|
<span class="order-reference">{{ order.referenceNo || '跟踪编号 —' }}</span>
|
|
|
|
|
<dl><div><dt>目的地</dt><dd>{{ order.destination || '待更新' }}</dd></div><div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[order.destinationType] }}</dd></div><div><dt>收货仓</dt><dd>{{ order.receivingWarehouseName || '—' }}</dd></div><div><dt>货物摘要</dt><dd>{{ getOrderCargoSummary(order) }}</dd></div><div><dt>运输方式</dt><dd>{{ order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : '待更新' }}</dd></div><div><dt>交货方式</dt><dd>{{ pickupMethodLabels[order.pickupMethod] }}</dd></div><div><dt>预约提货时间</dt><dd>{{ order.pickupMethod === 'PICKUP' && order.requestedPickupAt ? formatDateTime(order.requestedPickupAt) : '—' }}</dd></div><div><dt>柜号 / 柜次</dt><dd>{{ order.businessSummary.cabinetNo || order.businessSummary.containerNo || '—' }}</dd></div><div v-if="order.businessSummary.vesselName || order.businessSummary.voyageNo"><dt>船名 / 航次</dt><dd>{{ [order.businessSummary.vesselName, order.businessSummary.voyageNo].filter(Boolean).join(' · ') }}</dd></div><div v-if="order.businessSummary.sailingAt"><dt>预计船期</dt><dd>{{ formatDateTime(order.businessSummary.sailingAt) }}</dd></div></dl>
|
|
|
|
|
<footer>
|
|
|
|
|
<span>更新于 {{ formatDateTime(order.updatedAt) }}</span>
|
|
|
|
|
<div>
|
|
|
|
@@ -1864,13 +1884,13 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
</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>
|
|
|
|
|
<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>
|
|
|
|
|
<section class="detail-section">
|
|
|
|
|
<h3>订单与运输</h3>
|
|
|
|
|
<dl class="order-detail-grid">
|
|
|
|
|
<div v-if="detailOrder.businessSummary.inboundCode"><dt>入仓码</dt><dd>{{ detailOrder.businessSummary.inboundCode }}</dd></div>
|
|
|
|
|
<div><dt>入仓码</dt><dd><button v-if="detailOrder.businessSummary.inboundCode" class="inbound-code-link table-code" type="button" @click="openInboundCodeAssociation(detailOrder.businessSummary.inboundCode)">{{ detailOrder.businessSummary.inboundCode }}</button><span v-else>—</span></dd></div>
|
|
|
|
|
<div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
|
|
|
|
|
<div><dt>运输方式</dt><dd>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
|
|
|
|
<div><dt>贸易条款</dt><dd>{{ displayValue(detailOrder.tradeTerm) }}</dd></div>
|
|
|
|
@@ -1878,7 +1898,7 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
<div><dt>预约提货时间</dt><dd>{{ detailOrder.pickupMethod === 'PICKUP' && detailOrder.requestedPickupAt ? formatDateTime(detailOrder.requestedPickupAt) : '-' }}</dd></div>
|
|
|
|
|
<div><dt>报关方式</dt><dd>{{ taxModeLabels[detailOrder.taxMode] }}</dd></div>
|
|
|
|
|
<div><dt>运输渠道</dt><dd>{{ detailOrder.channelName || detailOrder.channelCode || '-' }}<span v-if="detailOrder.channelName && detailOrder.channelCode" class="detail-subline">{{ detailOrder.channelCode }}</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.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>{{ displayValue(detailOrder.receivingTime) }}</dd></div>
|
|
|
|
|
<div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[detailOrder.destinationType] }}</dd></div>
|
|
|
|
|
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</dd></div>
|
|
|
|
@@ -1907,9 +1927,9 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
<h3>柜次进度</h3>
|
|
|
|
|
<div v-if="cabinetProgressItems.length" class="cabinet-progress-list">
|
|
|
|
|
<article v-for="cabinet in cabinetProgressItems" :key="cabinet.cabinetId" class="cabinet-progress-card">
|
|
|
|
|
<header><strong>{{ cabinet.cabinetNo || '柜次待分配' }}</strong><el-tag size="small" effect="plain">{{ cabinet.status || '待更新' }}</el-tag></header>
|
|
|
|
|
<header><strong>{{ cabinet.cabinetNo || '—' }}</strong><el-tag size="small" effect="plain">{{ cabinet.status || '待更新' }}</el-tag></header>
|
|
|
|
|
<dl>
|
|
|
|
|
<div><dt>柜号</dt><dd>{{ cabinet.cabinetNo || '待分配' }}</dd></div>
|
|
|
|
|
<div><dt>柜号</dt><dd>{{ cabinet.cabinetNo || '—' }}</dd></div>
|
|
|
|
|
<div><dt>船名 / 航次</dt><dd>{{ [cabinet.vesselName, cabinet.voyageNo].filter(Boolean).join(' · ') || '待更新' }}</dd></div>
|
|
|
|
|
<div><dt>预计船期</dt><dd>{{ cabinet.sailingAt ? formatDateTime(cabinet.sailingAt) : '待更新' }}</dd></div>
|
|
|
|
|
</dl>
|
|
|
|
@@ -1986,6 +2006,10 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
:initial-detail="customsByOrder[customsDialogOrderId]"
|
|
|
|
|
@updated="updateCustomsDetail"
|
|
|
|
|
/>
|
|
|
|
|
<InboundCodeAssociationDialog
|
|
|
|
|
v-model="inboundAssociationOpen"
|
|
|
|
|
:inbound-code="inboundAssociationCode"
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
@@ -2062,6 +2086,9 @@ async function searchOrders(model: QueryModel) {
|
|
|
|
|
.mobile-order-surface { display: none; min-height: 220px; }
|
|
|
|
|
.mobile-order-list { display: none; }
|
|
|
|
|
.order-number-link { display: block; width: 100%; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-size: 11px; font-weight: 700; line-height: 18px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
.inbound-code-link { max-width: 100%; overflow: hidden; padding: 0; border: 0; background: transparent; color: var(--primary); cursor: pointer; text-align: left; text-decoration: underline; text-decoration-color: transparent; text-underline-offset: 3px; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
.inbound-code-link:hover, .inbound-code-link:focus-visible { text-decoration-color: currentColor; }
|
|
|
|
|
.inbound-code-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
|
|
|
|
.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-row-actions { align-items: center; justify-content: center; gap: 6px; white-space: nowrap; }
|
|
|
|
|