2942 lines
193 KiB
Vue
2942 lines
193 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules, type UploadRequestOptions } from 'element-plus'
|
||
import { AlertTriangle, ChevronDown, ChevronUp, Download, FilePlus2, FileText, GripVertical, ImagePlus, MapPin, PackageCheck, Pencil, Plus, RotateCcw, Send, SlidersHorizontal, Trash2, Upload } from '@lucide/vue'
|
||
import { FIRST_LEG_ORDER_STATUSES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||
import type { TableAction } from '@/components/common/tableActions'
|
||
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 TransportFactsTable from '@/modules/first-leg/components/TransportFactsTable.vue'
|
||
import CustomsDocumentsDialog from '@/modules/first-leg/components/CustomsDocumentsDialog.vue'
|
||
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
|
||
import { getMerchantCustomsForOrder, isMerchantCustomsUnavailable, type MerchantCustomsContainer } from '@/modules/first-leg/api/customs'
|
||
import { isCustomsOrderCandidate, needsCustomsUpload } from '../utils/customsEligibility'
|
||
import {
|
||
estimateMerchantFirstLegPrice,
|
||
createIdempotencyKey,
|
||
createMerchantOrderAddress,
|
||
disableMerchantOrderAddress,
|
||
downloadMerchantFirstLegBatchImportErrorReport,
|
||
downloadMerchantFirstLegBatchImportTemplate,
|
||
downloadMerchantOrderCartonLabelsBatch,
|
||
downloadMerchantProductImage,
|
||
getMerchantInboundCodePolicy,
|
||
getMerchantOrderAddresses,
|
||
getMerchantPricingChannels,
|
||
importMerchantFirstLegOrders,
|
||
updateMerchantOrderAddress,
|
||
uploadMerchantProductImage,
|
||
type CustomerOrderAddress,
|
||
type CustomerOrderAddressSaveRequest,
|
||
type MerchantBatchImportMode,
|
||
type MerchantBatchImportResponse,
|
||
type MerchantPriceEstimate,
|
||
type MerchantPriceEstimateRequest,
|
||
type MerchantPricingChannel,
|
||
type InboundCodePolicy,
|
||
} from '@/modules/first-leg/api/firstLeg'
|
||
import { useFirstLegOrdersStore, type DraftEditSession } from '@/modules/first-leg/store/firstLegOrders'
|
||
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
|
||
import { useAuthStore } from '@/modules/auth/public'
|
||
import { useUnsavedWorkRegistration } from '@/navigation/workspace-tabs/unsavedWork'
|
||
import { useWorkspaceTabsStore } from '@/navigation/workspace-tabs/workspaceTabs'
|
||
import type { FirstLegPartyRole, FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
|
||
import type { FirstLegCargoItem, FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
|
||
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
|
||
import {
|
||
canClientEditFirstLegOrder,
|
||
createEmptyCargoItem,
|
||
getFirstLegStatusLabel,
|
||
getFirstLegPickingStatusLabel,
|
||
getFirstLegStatusTone,
|
||
getFirstLegTransportModeLabel,
|
||
summarizeFirstLegCargo,
|
||
validateFirstLegOrderDraft,
|
||
} from '@/modules/first-leg/utils/firstLegOrder'
|
||
import { ORDER_QUEUES, orderProgressLabel } from '../utils/orderProgress'
|
||
import type { OrderQueue } from '../api/unifiedOrders'
|
||
import { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
|
||
import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate'
|
||
|
||
const store = useFirstLegOrdersStore()
|
||
const tenantStore = useTenantAdministrationStore()
|
||
const workspace = useWorkspaceTabsStore()
|
||
const auth = useAuthStore()
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const props = withDefaults(defineProps<{ createPage?: boolean; detailPage?: boolean; showCreateAction?: boolean }>(), { createPage: false, detailPage: false, showCreateAction: true })
|
||
const isCreatePage = computed(() => props.createPage)
|
||
const isDetailPage = computed(() => props.detailPage)
|
||
const routeDraftId = computed(() => typeof route.params.draftId === 'string' ? route.params.draftId : '')
|
||
const routeDetailId = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
||
const routeCopySourceId = computed(() => typeof route.query.copyFrom === 'string' ? route.query.copyFrom : '')
|
||
const routeCopySourceIsDraft = computed(() => route.query.copyDraft === '1')
|
||
const canViewCompanyProfile = computed(() => auth.hasPermission('company:profile:view'))
|
||
const companyAuditLoaded = ref(false)
|
||
const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE')
|
||
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
|
||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||
const canCancelOrder = computed(() => auth.hasPermission('order:first-leg:cancel'))
|
||
const cancellingOrderId = ref('')
|
||
const pendingCancellation = shallowRef<{ id: string; expectedVersion: number; reason: string; businessNo: string; statusLabel: string }>()
|
||
|
||
function canOfferCancellation(order: FirstLegOrder) {
|
||
return canCancelOrder.value && ['DRAFT', 'PENDING_REVIEW', 'WAIT_MEASURE'].includes(order.status)
|
||
}
|
||
|
||
async function cancelOrder(order: FirstLegOrder) {
|
||
if (!canOfferCancellation(order) || cancellingOrderId.value) return
|
||
cancellingOrderId.value = order.id
|
||
try {
|
||
let command = pendingCancellation.value
|
||
if (command && command.id !== order.id) {
|
||
ElMessage.warning(`请先处理订单 ${command.businessNo} 未确认的取消结果`)
|
||
return
|
||
}
|
||
if (!command) {
|
||
const target = await store.prepareCancellation(order.id)
|
||
const { value } = await ElMessageBox.prompt(`订单:${target.businessNo};当前状态:${target.statusLabel}。请填写取消原因。`, '取消订单', {
|
||
type: 'warning', inputType: 'textarea', confirmButtonText: '确认取消订单', cancelButtonText: '返回',
|
||
inputValidator: value => Boolean(value?.trim()) && value.trim().length <= 500 || '请填写 1–500 字取消原因',
|
||
})
|
||
command = { ...target, reason: value.trim() }
|
||
pendingCancellation.value = command
|
||
} else {
|
||
await ElMessageBox.confirm(`订单:${command.businessNo};原状态:${command.statusLabel};原因:${command.reason}。将查询当前状态并重试原取消请求。`, '重试取消订单', {
|
||
type: 'warning', confirmButtonText: '重试原请求', cancelButtonText: '返回',
|
||
})
|
||
}
|
||
await store.cancelOrder(command.id, command.expectedVersion, command.reason)
|
||
pendingCancellation.value = undefined
|
||
ElMessage.success('订单已取消')
|
||
await loadOrderPage()
|
||
if (isDetailPage.value) await router.push({ name: 'first-leg-orders' })
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
if (error instanceof ApiError && (error.code === 'A0409' || error.status === 409)) {
|
||
pendingCancellation.value = undefined
|
||
ElMessage.error(`${error.message}。请刷新订单后重新确认取消。`)
|
||
} else {
|
||
ElMessage.error(error instanceof Error ? error.message : '取消失败,请重试原请求')
|
||
}
|
||
} finally {
|
||
cancellingOrderId.value = ''
|
||
}
|
||
}
|
||
const canSubmitOrder = computed(() => auth.hasPermission('order:first-leg:submit'))
|
||
const canConfirmInboundFee = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||
const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC'))
|
||
const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType))
|
||
const filters = ref<QueryModel>({ receivingWarehouseId: '', channelName: '', cargoKeyword: '', createdRange: [], status: '', destinationType: '', trackingNumber: '' })
|
||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||
const queryFields = computed<QueryField[]>(() => [
|
||
{ key: 'cargoKeyword', label: '入仓码 / SKU', type: 'input', placeholder: '请输入入仓码或 SKU', props: { 'aria-label': '入仓码或 SKU' } },
|
||
{ key: 'receivingWarehouseId', label: '收货仓', type: 'select', placeholder: '全部', options: availableWarehouses.value.map((item) => ({ label: item.name, value: item.id })), props: { 'aria-label': '收货仓' } },
|
||
{ key: 'channelName', label: '渠道名称', type: 'select', placeholder: '全部', options: store.queryChannelNames.map((name) => ({ label: name, value: name })), props: { 'aria-label': '渠道名称' } },
|
||
{ key: 'createdRange', label: '创建时间', type: 'daterange', placeholder: '请选择创建时间', props: { 'aria-label': '创建时间' } },
|
||
{ key: 'status', label: '当前状态', type: 'select', placeholder: '全部', options: FIRST_LEG_ORDER_STATUSES.filter(item => item.code !== 'CANCELLED').map((item) => ({ label: item.label, value: item.code })), props: { 'aria-label': '当前状态' } },
|
||
{ key: 'destinationType', label: '目的仓类型', type: 'select', placeholder: '全部', options: [{ label: 'FBA 仓', value: 'FBA' }, { label: '海外仓', value: 'OVERSEAS' }, { label: '私人地址', value: 'PRIVATE' }], props: { 'aria-label': '目的仓类型' } },
|
||
{ key: 'trackingNumber', label: '快递单号', type: 'input', placeholder: '请输入快递单号', props: { 'aria-label': '快递单号' } },
|
||
])
|
||
const channelOptions = ref<MerchantPricingChannel[]>([])
|
||
const inboundCodePolicy = ref<InboundCodePolicy | null>(null)
|
||
const manualInboundCodeRequired = computed(() => inboundCodePolicy.value?.mode === 'LEGACY_MANUAL')
|
||
const channelLoading = ref(false)
|
||
const channelError = ref('')
|
||
const priceEstimate = ref<MerchantPriceEstimate | null>(null)
|
||
const priceEstimateLoading = ref(false)
|
||
const priceEstimateError = ref('')
|
||
const selectedCargoIds = ref<string[]>([])
|
||
const batchImportInput = ref<HTMLInputElement>()
|
||
const batchImportOpen = ref(false)
|
||
const batchImportMode = ref<MerchantBatchImportMode>('PARTIAL')
|
||
const batchImportFile = ref<File | null>(null)
|
||
const batchImportIdempotencyKey = ref('')
|
||
const batchImportSubmitting = ref(false)
|
||
const batchTemplateDownloading = ref(false)
|
||
const batchErrorReportDownloading = ref(false)
|
||
const batchImportResult = ref<MerchantBatchImportResponse | null>(null)
|
||
const batchImportError = ref('')
|
||
const fixedOrderColumns: DataTableColumn[] = [
|
||
{ key: 'selection', type: 'selection', width: 48, fixed: 'left', reserveSelection: true },
|
||
{ key: 'actions', label: '操作', width: 284, fixed: 'left', align: 'left', headerAlign: 'left', slotName: 'actions' },
|
||
{ key: 'inboundCode', label: '入仓码', width: 230, fixed: 'left', align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'inboundCode' },
|
||
]
|
||
const selectableOrderColumns: DataTableColumn[] = [
|
||
{ key: 'status', label: '当前订单状态', width: 126, align: 'center', headerAlign: 'center', slotName: 'status' },
|
||
{ key: 'progress', label: '履约进度', width: 142, align: 'center', headerAlign: 'center', slotName: 'progress' },
|
||
{ key: 'channel', label: '渠道', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'channel' },
|
||
{ key: 'transportMode', label: '运输方式', width: 104, align: 'center', headerAlign: 'center', slotName: 'transportMode' },
|
||
{ key: 'pickupMethod', label: '交货方式', width: 112, align: 'center', headerAlign: 'center', slotName: 'pickupMethod' },
|
||
{ key: 'requestedPickupAt', label: '预约提货时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'requestedPickupAt' },
|
||
{ key: 'receivingWarehouse', label: '国内收货仓', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'receivingWarehouse' },
|
||
{ key: 'destinationWarehouse', label: '目的仓', width: 120, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'destinationWarehouse' },
|
||
{ key: 'destinationType', label: '目的仓类型', width: 112, align: 'center', headerAlign: 'center', slotName: 'destinationType' },
|
||
{ key: 'packageCount', label: '箱数', width: 82, align: 'center', headerAlign: 'center', slotName: 'packageCount' },
|
||
{ key: 'expressCompany', label: '快递公司', width: 128, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'expressCompany' },
|
||
{ key: 'trackingNumber', label: '快递单号', width: 158, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'trackingNumber' },
|
||
{ key: 'skus', label: 'SKUS', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'skus' },
|
||
{ key: 'grossWeight', label: '总毛重', width: 108, align: 'right', headerAlign: 'right', slotName: 'grossWeight' },
|
||
{ key: 'netWeight', label: '总净重', width: 108, align: 'right', headerAlign: 'right', slotName: 'netWeight' },
|
||
{ key: 'volume', label: '总体积', width: 106, align: 'right', headerAlign: 'right', slotName: 'volume' },
|
||
{ key: 'pieces', label: '总数量', width: 92, align: 'right', headerAlign: 'right', slotName: 'pieces' },
|
||
{ key: 'billingUnitPrice', label: '计费单价', width: 108, align: 'right', headerAlign: 'right', slotName: 'billingUnitPrice' },
|
||
{ key: 'chargeableVolume', label: '计费体积', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableVolume' },
|
||
{ key: 'chargeableWeight', label: '计费重量', width: 108, align: 'right', headerAlign: 'right', slotName: 'chargeableWeight' },
|
||
{ key: 'remark', label: '备注', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'remark' },
|
||
{ key: 'shipmentId', label: '货件编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'shipmentId' },
|
||
{ key: 'referenceId', label: '跟踪编号', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'referenceId' },
|
||
{ key: 'receivingTime', label: '国内仓入库时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'receivingTime' },
|
||
{ key: 'expectedShipDate', label: '期望出运', width: 132, align: 'center', headerAlign: 'center', slotName: 'expectedShipDate' },
|
||
{ key: 'cabinetNo', label: '柜号 / 柜次', width: 150, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'cabinetNo' },
|
||
{ key: 'vesselVoyage', label: '船名 / 航次', width: 160, align: 'left', headerAlign: 'left', showOverflowTooltip: true, slotName: 'vesselVoyage' },
|
||
{ key: 'estimatedSailingAt', label: '预计船期', width: 164, align: 'center', headerAlign: 'center', slotName: 'estimatedSailingAt' },
|
||
{ key: 'createdAt', label: '创建时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'createdAt' },
|
||
{ key: 'updatedAt', label: '更新时间', width: 164, align: 'center', headerAlign: 'center', slotName: 'updatedAt' },
|
||
]
|
||
const allSelectableColumnKeys = selectableOrderColumns.map((column) => String(column.key))
|
||
const defaultVisibleColumnKeys = ['status', 'progress', 'channel', 'transportMode', 'pickupMethod', 'receivingWarehouse', 'createdAt']
|
||
const visibleColumnKeys = ref<string[]>([...defaultVisibleColumnKeys])
|
||
const orderedColumnKeys = ref<string[]>([...allSelectableColumnKeys])
|
||
const draggedColumnKey = ref('')
|
||
const orderedSelectableColumns = computed(() => orderedColumnKeys.value
|
||
.map((key) => selectableOrderColumns.find((column) => String(column.key) === key))
|
||
.filter((column): column is DataTableColumn => Boolean(column)))
|
||
const visibleOrderColumns = computed(() => [
|
||
...fixedOrderColumns,
|
||
...orderedSelectableColumns.value.filter((column) => visibleColumnKeys.value.includes(String(column.key))),
|
||
])
|
||
|
||
function moveOrderColumn(key: string, offset: -1 | 1) {
|
||
const index = orderedColumnKeys.value.indexOf(key)
|
||
const targetIndex = index + offset
|
||
if (index < 0 || targetIndex < 0 || targetIndex >= orderedColumnKeys.value.length) return
|
||
const nextKeys = [...orderedColumnKeys.value]
|
||
;[nextKeys[index], nextKeys[targetIndex]] = [nextKeys[targetIndex], nextKeys[index]]
|
||
orderedColumnKeys.value = nextKeys
|
||
}
|
||
|
||
function dropOrderColumn(targetKey: string) {
|
||
const sourceKey = draggedColumnKey.value
|
||
draggedColumnKey.value = ''
|
||
if (!sourceKey || sourceKey === targetKey) return
|
||
const nextKeys = orderedColumnKeys.value.filter((key) => key !== sourceKey)
|
||
const targetIndex = nextKeys.indexOf(targetKey)
|
||
nextKeys.splice(targetIndex, 0, sourceKey)
|
||
orderedColumnKeys.value = nextKeys
|
||
}
|
||
|
||
function resetOrderColumns() {
|
||
orderedColumnKeys.value = [...allSelectableColumnKeys]
|
||
visibleColumnKeys.value = [...defaultVisibleColumnKeys]
|
||
}
|
||
const tablePage = ref(1)
|
||
const tablePageSize = ref(10)
|
||
const downloadingMarks = ref(false)
|
||
const exportingOrders = ref(false)
|
||
const batchSubmitting = ref(false)
|
||
const pendingBatchSubmissionIds = ref<string[]>([])
|
||
const selectedOrders = ref<FirstLegOrder[]>([])
|
||
const selectedOrderCount = computed(() => selectedOrders.value.length)
|
||
const selectedDraftOrders = computed(() => selectedOrders.value.filter((order) => order.status === 'DRAFT'))
|
||
const orderTableVersion = ref(0)
|
||
const activeOrderQueue = ref<OrderQueue>('all')
|
||
const editorOpen = ref(false)
|
||
const orderFormRef = ref<FormInstance>()
|
||
const editingId = ref<string>()
|
||
const editingSession = shallowRef<DraftEditSession>()
|
||
const openingEditor = ref(false)
|
||
const copyingOrderId = ref('')
|
||
const detailId = ref<string>()
|
||
const saving = ref(false)
|
||
const detailLoading = ref(false)
|
||
const detailError = ref('')
|
||
const detailCargoPage = ref(1)
|
||
const detailCargoPageSize = ref(5)
|
||
const inboundBilling = ref<MerchantInboundBilling | null>(null)
|
||
const inboundBillingLoading = ref(false)
|
||
const inboundBillingUnavailable = ref(false)
|
||
const inboundBillingUnavailableMessage = ref('还未测量推送')
|
||
const inboundBillingError = ref('')
|
||
const inboundBillingOrderId = ref('')
|
||
const feeConfirmationOpen = ref(false)
|
||
const customsByOrder = reactive<Record<string, MerchantCustomsContainer | null | undefined>>({})
|
||
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)
|
||
const pageDataLoading = computed(() => supportingDataLoading.value || (!isCreatePage.value && store.loading))
|
||
const editorBaseline = ref('')
|
||
let cargoSequence = 0
|
||
let packingGroupSequence = 0
|
||
let inboundBillingRequestSequence = 0
|
||
let channelRequestSequence = 0
|
||
let estimateRequestSequence = 0
|
||
let estimateTimer: ReturnType<typeof setTimeout> | undefined
|
||
|
||
const orderAddresses = ref<CustomerOrderAddress[]>([])
|
||
const orderAddressesLoading = ref(false)
|
||
const pickupAddresses = computed(() => orderAddresses.value.filter((item) => item.addressType === 'PICKUP'))
|
||
const privateAddresses = computed(() => orderAddresses.value.filter((item) => item.addressType === 'PRIVATE_DESTINATION'))
|
||
|
||
function addressLabel(address: CustomerOrderAddress) {
|
||
return `${address.addressName} · ${address.contactName} · ${address.countryCode} ${address.city} ${address.addressLine}`
|
||
}
|
||
|
||
const orderFormRules: FormRules = {
|
||
transportMode: [{ required: true, message: '请选择运输方式', trigger: 'change' }],
|
||
businessType: [{ required: true, message: '请选择业务类型', trigger: 'change' }],
|
||
receivingWarehouseId: [{ required: true, message: '请选择平台收货仓', trigger: 'change' }],
|
||
destinationCountryCode: [
|
||
{ required: true, message: '请输入目的国代码', trigger: 'blur' },
|
||
{ pattern: /^[A-Za-z]{2}$/, message: '请输入两位英文字母,例如 US', trigger: 'blur' },
|
||
],
|
||
destinationWarehouseName: [{ required: true, message: '请输入目的仓名称', trigger: 'blur' }],
|
||
channelCode: [{ required: true, message: '请选择运输渠道', trigger: 'change' }],
|
||
channelName: [{ required: true, message: '请输入渠道名称', trigger: 'blur' }],
|
||
pricingTaxMode: [{ required: true, message: '请选择报价包税方式', trigger: 'change' }],
|
||
pickupMethod: [{ required: true, message: '请选择提货方式', trigger: 'change' }],
|
||
taxMode: [{ required: true, message: '请选择报关模式', trigger: 'change' }],
|
||
destinationType: [{ required: true, message: '请选择目的仓类型', trigger: 'change' }],
|
||
}
|
||
|
||
function createEmptyPartySnapshot(role: FirstLegPartyRole): FirstLegPartySnapshot {
|
||
return {
|
||
id: '',
|
||
code: '',
|
||
name: '',
|
||
role,
|
||
contactName: '',
|
||
phone: '',
|
||
email: '',
|
||
countryRegion: role === 'SHIPPER' ? 'CN' : 'US',
|
||
province: '',
|
||
city: '',
|
||
postalCode: '',
|
||
address: '',
|
||
}
|
||
}
|
||
|
||
function createPickupPartySnapshot(): FirstLegPartySnapshot {
|
||
return {
|
||
...createEmptyPartySnapshot('SHIPPER'),
|
||
name: tenantStore.company.companyName,
|
||
contactName: tenantStore.company.contactName,
|
||
phone: tenantStore.company.contactPhone,
|
||
email: tenantStore.company.contactEmail,
|
||
}
|
||
}
|
||
|
||
function requiredTextRules(message: string): FormItemRule[] {
|
||
return [{ required: true, message, trigger: 'blur' }]
|
||
}
|
||
|
||
function requiredSelectRules(message: string): FormItemRule[] {
|
||
return [{ required: true, message, trigger: 'change' }]
|
||
}
|
||
|
||
function positiveNumberRules(message: string, integer = false): FormItemRule[] {
|
||
return [{ type: integer ? 'integer' : 'number', min: integer ? 1 : 0.001, message, trigger: 'change' }]
|
||
}
|
||
|
||
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 formatExportDateTime(value: string | null | undefined) {
|
||
if (!value) return '-'
|
||
const date = new Date(value)
|
||
if (Number.isNaN(date.getTime())) return value
|
||
const pad = (part: number) => String(part).padStart(2, '0')
|
||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||
}
|
||
|
||
const pickupMethodLabels: Record<FirstLegOrder['pickupMethod'], string> = {
|
||
SELF_EXPRESS: '自行快递到仓',
|
||
PICKUP: '平台上门提货',
|
||
SELF_TRUCK: '自行送货到仓',
|
||
}
|
||
const destinationTypeLabels: Record<FirstLegOrder['destinationType'], string> = {
|
||
FBA: 'FBA 仓',
|
||
OVERSEAS: '海外仓',
|
||
PRIVATE: '私人地址',
|
||
}
|
||
const taxModeLabels: Record<FirstLegOrder['taxMode'], string> = {
|
||
BUY_DECLARATION: '买单报关',
|
||
TAX_REFUND: '退税报关',
|
||
}
|
||
const packageTypeLabels: Readonly<Record<string, string>> = {
|
||
CARTON: '纸箱',
|
||
PALLET: '托盘',
|
||
WOODEN_CASE: '木箱',
|
||
BAG: '袋装',
|
||
OTHER: '其他',
|
||
}
|
||
|
||
function displayValue(value: string | null | undefined) {
|
||
return value?.trim() || '—'
|
||
}
|
||
|
||
function emptyDraft(): FirstLegOrderDraft {
|
||
return {
|
||
inboundCode: '',
|
||
referenceNo: '',
|
||
transportMode: 'FCL',
|
||
businessType: 'EXPORT',
|
||
fulfillmentType: 'STANDARD',
|
||
lxCode: '',
|
||
origin: '',
|
||
destination: '',
|
||
receivingWarehouseId: '',
|
||
expectedDepartureDate: '',
|
||
shipperPartyId: '',
|
||
senderAddressId: '',
|
||
shipperPartySnapshot: null,
|
||
shipper: '',
|
||
consigneePartyId: '',
|
||
privateDestinationAddressId: '',
|
||
consigneePartySnapshot: null,
|
||
consignee: '',
|
||
priority: 'NORMAL',
|
||
customerRemark: '',
|
||
tradeTerm: 'FOB',
|
||
destinationCountryCode: 'US',
|
||
destinationWarehouseId: '',
|
||
destinationWarehouseName: '',
|
||
pickupMethod: 'SELF_EXPRESS',
|
||
channelCode: '',
|
||
channelName: '',
|
||
pricingTaxMode: 'TAX_INCLUDED',
|
||
taxMode: 'BUY_DECLARATION',
|
||
customsInspection: null,
|
||
destinationType: 'FBA',
|
||
amazonWarehouseCode: '',
|
||
shipmentId: '',
|
||
items: [createEmptyCargoItem(`new-cargo-${++cargoSequence}`)],
|
||
}
|
||
}
|
||
|
||
function quickOrderDraft() {
|
||
const draft = emptyDraft()
|
||
if (!isCreatePage.value || route.query.quick !== '1') return draft
|
||
|
||
const destinationType = String(route.query.destinationType ?? '')
|
||
if (destinationType === 'FBA' || destinationType === 'OVERSEAS' || destinationType === 'PRIVATE') draft.destinationType = destinationType
|
||
draft.receivingWarehouseId = String(route.query.receivingWarehouseId ?? '')
|
||
draft.channelCode = String(route.query.channelCode ?? '')
|
||
draft.channelName = String(route.query.channelName ?? '')
|
||
draft.destinationWarehouseId = String(route.query.destinationWarehouseId ?? '')
|
||
draft.destinationWarehouseName = String(route.query.destinationWarehouseName ?? '')
|
||
draft.destinationCountryCode = String(route.query.destinationCountryCode ?? draft.destinationCountryCode).toUpperCase()
|
||
draft.amazonWarehouseCode = String(route.query.amazonWarehouseCode ?? '')
|
||
if (draft.destinationType === 'PRIVATE') {
|
||
try {
|
||
const consignee = JSON.parse(String(route.query.consignee ?? '{}')) as Record<string, unknown>
|
||
draft.consigneePartySnapshot = {
|
||
...createEmptyPartySnapshot('CONSIGNEE'),
|
||
name: draft.destinationWarehouseName,
|
||
countryRegion: draft.destinationCountryCode,
|
||
contactName: typeof consignee.contactName === 'string' ? consignee.contactName : '',
|
||
phone: typeof consignee.phone === 'string' ? consignee.phone : '',
|
||
city: typeof consignee.city === 'string' ? consignee.city : '',
|
||
address: typeof consignee.address === 'string' ? consignee.address : '',
|
||
postalCode: typeof consignee.postalCode === 'string' ? consignee.postalCode : '',
|
||
}
|
||
draft.consignee = draft.destinationWarehouseName
|
||
draft.destination = draft.destinationWarehouseName
|
||
} catch {
|
||
draft.consigneePartySnapshot = createEmptyPartySnapshot('CONSIGNEE')
|
||
}
|
||
} else {
|
||
draft.destination = draft.destinationWarehouseName
|
||
}
|
||
|
||
try {
|
||
const cargo = JSON.parse(String(route.query.cargo ?? '[]')) as Array<{ sku?: unknown; packageCount?: unknown }>
|
||
if (Array.isArray(cargo) && cargo.length) {
|
||
draft.items = cargo.map((item) => ({
|
||
...createEmptyCargoItem(`quick-cargo-${++cargoSequence}`),
|
||
sku: typeof item.sku === 'string' ? item.sku.slice(0, 64) : '',
|
||
packageCount: Number.isInteger(item.packageCount) && Number(item.packageCount) > 0 ? Number(item.packageCount) : 1,
|
||
}))
|
||
}
|
||
} catch {
|
||
// Keep the standard empty row when quick-order data is malformed.
|
||
}
|
||
const pricingTaxMode = String(route.query.pricingTaxMode ?? '')
|
||
if (pricingTaxMode === 'TAX_INCLUDED' || pricingTaxMode === 'TAX_EXCLUDED') draft.pricingTaxMode = pricingTaxMode
|
||
const grossWeightKg = Number(route.query.grossWeightKg)
|
||
const volumeCbm = Number(route.query.volumeCbm)
|
||
if (Number.isFinite(grossWeightKg) && grossWeightKg > 0) draft.items[0].grossWeightKg = grossWeightKg
|
||
if (Number.isFinite(volumeCbm) && volumeCbm > 0) draft.items[0].volumeCbm = volumeCbm
|
||
return draft
|
||
}
|
||
|
||
const form = reactive<FirstLegOrderDraft>(quickOrderDraft())
|
||
const serializedDraft = computed(() => JSON.stringify(form))
|
||
const editorDirty = computed(() => (editorOpen.value || isCreatePage.value) && serializedDraft.value !== editorBaseline.value)
|
||
const unsavedRouteKey = props.createPage ? 'first-leg-order-create' : 'first-leg-orders'
|
||
|
||
useUnsavedWorkRegistration({
|
||
key: `first-leg-order-editor:${unsavedRouteKey}`,
|
||
routeKey: unsavedRouteKey,
|
||
label: props.createPage ? '创建订单' : '订单编辑',
|
||
dirty: editorDirty,
|
||
})
|
||
|
||
const orderQueueCounts = computed(() => store.queueCounts)
|
||
const pagedOrders = computed(() => store.orders)
|
||
|
||
async function selectOrderQueue(queue: OrderQueue) {
|
||
activeOrderQueue.value = queue
|
||
tablePage.value = 1
|
||
await loadOrderPage()
|
||
}
|
||
|
||
async function changeOrderPage(page: number) {
|
||
tablePage.value = page
|
||
await loadOrderPage()
|
||
}
|
||
|
||
async function changeOrderPageSize(size: number) {
|
||
tablePageSize.value = size
|
||
tablePage.value = 1
|
||
await loadOrderPage()
|
||
}
|
||
|
||
let orderPageRequestVersion = 0
|
||
async function loadOrderPage() {
|
||
const requestVersion = ++orderPageRequestVersion
|
||
const text = (key: string) => typeof appliedFilters.value[key] === 'string' ? String(appliedFilters.value[key]).trim() || undefined : undefined
|
||
const range = Array.isArray(appliedFilters.value.createdRange) ? appliedFilters.value.createdRange : []
|
||
const requestedPage = tablePage.value
|
||
await store.load({
|
||
cargoKeyword: text('cargoKeyword'), receivingWarehouseId: text('receivingWarehouseId'),
|
||
channelName: text('channelName'), destinationType: text('destinationType'),
|
||
trackingNumber: text('trackingNumber'), mainStatus: text('status'),
|
||
createdFrom: range[0] ? `${range[0]}T00:00:00` : undefined,
|
||
createdTo: range[1] ? `${range[1]}T23:59:59.999` : undefined,
|
||
queue: activeOrderQueue.value, page: requestedPage, size: tablePageSize.value,
|
||
})
|
||
if (requestVersion !== orderPageRequestVersion) return
|
||
const lastPage = Math.max(1, Math.ceil(store.total / tablePageSize.value))
|
||
if (!store.loadError && requestedPage > lastPage) {
|
||
tablePage.value = lastPage
|
||
await loadOrderPage()
|
||
return
|
||
}
|
||
selectedOrders.value = []
|
||
orderTableVersion.value += 1
|
||
}
|
||
|
||
function eligibleCustomsDetail(order: FirstLegOrder) {
|
||
const value = customsByOrder[order.id]
|
||
if (!isCustomsOrderCandidate(order) || !value?.merchantStatus) return null
|
||
return value.inbounds.some((item) => item.orderId === order.id && item.taxMode === 'TAX_REFUND') ? value : null
|
||
}
|
||
|
||
async function loadCustomsEligibility(order: FirstLegOrder) {
|
||
if (!isCustomsOrderCandidate(order) || customsByOrder[order.id] !== undefined || customsEligibilityPending.has(order.id)) return
|
||
customsEligibilityPending.add(order.id)
|
||
try {
|
||
customsByOrder[order.id] = await getMerchantCustomsForOrder(order.id)
|
||
} catch (error) {
|
||
customsByOrder[order.id] = null
|
||
if (!isMerchantCustomsUnavailable(error)) {
|
||
// Keep the operation hidden when eligibility cannot be proven by the customer-scoped server response.
|
||
}
|
||
} finally {
|
||
customsEligibilityPending.delete(order.id)
|
||
}
|
||
}
|
||
|
||
watch(() => pagedOrders.value.map((order) => `${order.id}:${order.status}`).join('|'), () => {
|
||
pagedOrders.value.forEach((order) => void loadCustomsEligibility(order))
|
||
}, { immediate: true })
|
||
|
||
function openCustomsDocuments(order: FirstLegOrder) {
|
||
if (!eligibleCustomsDetail(order)) return
|
||
customsDialogOrderId.value = order.id
|
||
customsDialogOpen.value = true
|
||
}
|
||
|
||
function openInboundCodeAssociation(inboundCode: string) {
|
||
const code = inboundCode.trim()
|
||
if (!code) return
|
||
inboundAssociationCode.value = code
|
||
inboundAssociationOpen.value = true
|
||
}
|
||
|
||
function updateCustomsDetail(value: MerchantCustomsContainer) {
|
||
const previous = customsByOrder[customsDialogOrderId.value]
|
||
if (customsDialogOrderId.value) customsByOrder[customsDialogOrderId.value] = value
|
||
if (!isCreatePage.value && !isDetailPage.value && previous && previous.merchantStatus !== value.merchantStatus) void loadOrderPage()
|
||
}
|
||
|
||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||
const pagedDetailCargoItems = computed(() => {
|
||
const items = detailOrder.value?.items ?? []
|
||
const start = (detailCargoPage.value - 1) * detailCargoPageSize.value
|
||
return items.slice(start, start + detailCargoPageSize.value)
|
||
})
|
||
const progressSummaryItems = computed(() => {
|
||
const order = detailOrder.value
|
||
if (!order || order.status === 'CANCELLED') return []
|
||
const inbound = inboundBilling.value?.inbound.summary
|
||
const summary = order.businessSummary
|
||
const formatMetric = (value: number | null | undefined, unit = '') => value == null ? '待更新' : `${value.toLocaleString()}${unit ? ` ${unit}` : ''}`
|
||
const preloadStatus = ({ PENDING: '待确认', CONFIRMING: '确认中', CONFIRMED: '已确认' } as Record<string, string>)[summary.preloadStatus] ?? summary.preloadStatus
|
||
const transportReference = [summary.vesselName, summary.voyageNo].filter(Boolean).join(' · ')
|
||
const pickingProgress = summary.expectedPickCount == null
|
||
? '待更新'
|
||
: `${summary.pickedCount ?? 0} / ${summary.expectedPickCount}`
|
||
return [
|
||
{ label: '实际箱数', value: formatMetric(summary.totalPackageCount ?? inbound?.totalPackageCount, '箱') },
|
||
{ label: '实测毛重', value: formatMetric(summary.actualGrossWeightKg ?? inbound?.actualGrossWeightKg, 'kg') },
|
||
{ 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: transportReference || '待更新' },
|
||
{ label: '计费重量', value: formatMetric(summary.chargeableWeightKg, 'kg') },
|
||
{ label: '后续履约', value: summary.deliveryTrackingNo || (summary.deliveryCompletedAt ? `海外回传于 ${formatDateTime(summary.deliveryCompletedAt)}` : '待海外同步') },
|
||
]
|
||
})
|
||
const cabinetProgressItems = computed(() => {
|
||
const summary = detailOrder.value?.businessSummary
|
||
if (!summary || (!summary.cabinetNo && !summary.containerNo)) return []
|
||
return [{ cabinetId: summary.cabinetNo || summary.containerNo, cabinetNo: summary.cabinetNo || summary.containerNo, pickingStatusLabel: getFirstLegPickingStatusLabel(summary.loadingStatus), vesselName: summary.vesselName, voyageNo: summary.voyageNo, estimatedSailingAt: summary.estimatedSailingAt }]
|
||
})
|
||
const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId))
|
||
const selectedDestinationWarehouse = computed(() => store.warehouses.find((item) => item.id === form.destinationWarehouseId && item.warehouseType === form.destinationType))
|
||
const selectedPricingChannel = computed(() => channelOptions.value.find((item) => item.channelCode === form.channelCode))
|
||
const availablePricingTaxModes = computed(() => selectedPricingChannel.value?.pricingTaxModes ?? [])
|
||
const hasActiveFilters = computed(() => {
|
||
const values = appliedFilters.value
|
||
return Object.values(values).some((value) => Array.isArray(value) ? value.length > 0 : Boolean(typeof value === 'string' ? value.trim() : value))
|
||
})
|
||
const emptyDescription = computed(() => {
|
||
if (hasActiveFilters.value) return '没有符合筛选条件的订单'
|
||
if (activeOrderQueue.value === 'customs') return '暂无需要上传报关资料的订单'
|
||
if (activeOrderQueue.value === 'fee') return '暂无需要确认计费重的订单'
|
||
if (activeOrderQueue.value === 'draft') return '暂无草稿订单'
|
||
if (activeOrderQueue.value !== 'all') return `暂无${ORDER_QUEUES.find(q => q.key === activeOrderQueue.value)?.label ?? ''}的订单`
|
||
return '还没有订单'
|
||
})
|
||
|
||
async function loadPageData() {
|
||
supportingDataLoading.value = true
|
||
supportingDataErrors.value = []
|
||
companyAuditLoaded.value = false
|
||
const companyRequest = canViewCompanyProfile.value
|
||
? tenantStore.loadCompanyContext()
|
||
: Promise.resolve()
|
||
const destinationRequest = isCreatePage.value && form.destinationType !== 'PRIVATE'
|
||
? loadDestinationWarehouses(form.destinationType)
|
||
: Promise.resolve()
|
||
const orderPageRequest = isCreatePage.value || isDetailPage.value
|
||
? store.loadWarehouseType('DOMESTIC', true)
|
||
: loadOrderPage()
|
||
const addressRequest = isCreatePage.value ? loadOrderAddresses() : Promise.resolve()
|
||
const [ordersResult, companyResult, destinationResult, policyResult, addressResult] = await Promise.allSettled([
|
||
orderPageRequest,
|
||
companyRequest,
|
||
destinationRequest,
|
||
getMerchantInboundCodePolicy(),
|
||
addressRequest,
|
||
])
|
||
if (policyResult.status === 'fulfilled') inboundCodePolicy.value = policyResult.value
|
||
else {
|
||
inboundCodePolicy.value = null
|
||
supportingDataErrors.value.push(`入仓码配置加载失败:${errorMessage(policyResult.reason)}`)
|
||
}
|
||
if (canViewCompanyProfile.value && companyResult.status === 'fulfilled') {
|
||
companyAuditLoaded.value = true
|
||
} else if (companyResult.status === 'rejected') {
|
||
supportingDataErrors.value.push(`企业资料加载失败:${errorMessage(companyResult.reason)}`)
|
||
}
|
||
if (!isCreatePage.value && ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`)
|
||
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
|
||
if (destinationResult.status === 'rejected') supportingDataErrors.value.push(`目的仓加载失败:${errorMessage(destinationResult.reason)}`)
|
||
if (addressResult.status === 'rejected') supportingDataErrors.value.push(`企业地址簿加载失败:${errorMessage(addressResult.reason)}`)
|
||
await autofillWarehouseSelections()
|
||
if (isCreatePage.value && routeDraftId.value) await loadRouteDraft(routeDraftId.value)
|
||
else if (isCreatePage.value && routeCopySourceId.value) await loadCopiedOrder(routeCopySourceId.value, routeCopySourceIsDraft.value)
|
||
if (isDetailPage.value && routeDetailId.value) await loadDetailPage(routeDetailId.value)
|
||
supportingDataLoading.value = false
|
||
}
|
||
|
||
async function loadRouteDraft(id: string) {
|
||
const detailedOrder = await store.loadDraftDetail(id)
|
||
if (!canClientEditFirstLegOrder(detailedOrder.status)) throw new Error('当前订单已不是草稿状态,无法继续编辑')
|
||
const draft = draftFromOrder(detailedOrder)
|
||
if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType)
|
||
editingId.value = detailedOrder.id
|
||
editingSession.value = store.beginDraftEdit(detailedOrder)
|
||
replaceForm(draft)
|
||
await Promise.all(form.items.map(ensureProductImagePreview))
|
||
if (form.receivingWarehouseId) await loadSelectedWarehouse(form.receivingWarehouseId)
|
||
resetEditorValidation()
|
||
markEditorClean()
|
||
}
|
||
|
||
async function loadCopiedOrder(id: string, sourceIsDraft: boolean) {
|
||
try {
|
||
const detailedOrder = sourceIsDraft ? await store.loadDraftDetail(id) : await store.loadDetail(id)
|
||
const draft = copyDraftFromOrder(detailedOrder)
|
||
if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType)
|
||
|
||
const warehouseAvailable = availableWarehouses.value.some((item) => item.id === draft.receivingWarehouseId)
|
||
if (!warehouseAvailable) {
|
||
draft.receivingWarehouseId = ''
|
||
draft.channelCode = ''
|
||
draft.channelName = ''
|
||
}
|
||
if (draft.destinationType !== 'PRIVATE') {
|
||
const destinationAvailable = store.warehouses.some((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
|
||
if (!destinationAvailable) {
|
||
draft.destinationWarehouseId = ''
|
||
draft.destinationWarehouseName = ''
|
||
draft.amazonWarehouseCode = ''
|
||
}
|
||
}
|
||
|
||
editingId.value = undefined
|
||
replaceForm(draft)
|
||
await Promise.all(form.items.map(ensureProductImagePreview))
|
||
if (warehouseAvailable) await loadSelectedWarehouse(draft.receivingWarehouseId)
|
||
resetEditorValidation()
|
||
markEditorClean()
|
||
ElMessage.success('订单数据已复制,请核对后提交')
|
||
} catch (error) {
|
||
supportingDataErrors.value.push(`复制订单数据失败:${errorMessage(error)}`)
|
||
}
|
||
}
|
||
|
||
async function loadDestinationWarehouses(type: 'FBA' | 'OVERSEAS' | 'PRIVATE') {
|
||
if (type === 'PRIVATE') return
|
||
destinationWarehousesLoading.value = true
|
||
try {
|
||
await store.loadWarehouseType(type)
|
||
} finally {
|
||
destinationWarehousesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function errorMessage(error: unknown) {
|
||
return error instanceof Error ? error.message : '服务暂时不可用'
|
||
}
|
||
|
||
function batchImportErrorMessage(error: unknown) {
|
||
if (!(error instanceof ApiError)) return errorMessage(error)
|
||
return [
|
||
error.message,
|
||
error.code ? `错误码 ${error.code}` : '',
|
||
error.requestId ? `requestId ${error.requestId}` : '',
|
||
error.traceId ? `traceId ${error.traceId}` : '',
|
||
].filter(Boolean).join(' · ')
|
||
}
|
||
|
||
onMounted(() => {
|
||
void loadPageData()
|
||
})
|
||
|
||
watch(() => auth.isAuthenticated, (authenticated, wasAuthenticated) => {
|
||
if (authenticated && !wasAuthenticated && !isCreatePage.value) void loadPageData()
|
||
})
|
||
|
||
watch(isCreatePage, (isCreate, wasCreate) => {
|
||
if (!isCreate || wasCreate) return
|
||
if (routeDraftId.value) return
|
||
editingId.value = undefined
|
||
replaceForm(quickOrderDraft())
|
||
resetEditorValidation()
|
||
// The list and create routes reuse this component, so onMounted does not run again.
|
||
if (wasCreate === false) void loadPageData()
|
||
}, { immediate: true })
|
||
|
||
watch(routeDetailId, (orderId, previousOrderId) => {
|
||
if (!isDetailPage.value || !orderId || orderId === previousOrderId) return
|
||
void loadDetailPage(orderId)
|
||
})
|
||
|
||
watch(feeConfirmationOpen, (open) => {
|
||
if (!open && !isDetailPage.value) resetInboundBillingState()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
resetInboundBillingState()
|
||
channelRequestSequence += 1
|
||
estimateRequestSequence += 1
|
||
if (estimateTimer) clearTimeout(estimateTimer)
|
||
releaseProductImagePreviews(form.items)
|
||
if (detailOrder.value) releaseProductImagePreviews(detailOrder.value.items)
|
||
})
|
||
|
||
function markEditorClean() {
|
||
editorBaseline.value = serializedDraft.value
|
||
}
|
||
|
||
function resetEditorValidation() {
|
||
void nextTick(() => orderFormRef.value?.clearValidate())
|
||
}
|
||
|
||
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) {
|
||
releaseProductImagePreviews(form.items)
|
||
Object.assign(form, cloneDraft(draft))
|
||
markEditorClean()
|
||
}
|
||
|
||
function releaseProductImagePreviews(items: FirstLegCargoItem[]) {
|
||
items.forEach((item) => {
|
||
if (item.productImagePreviewUrl.startsWith('blob:')) URL.revokeObjectURL(item.productImagePreviewUrl)
|
||
item.productImagePreviewUrl = ''
|
||
})
|
||
}
|
||
|
||
async function ensureProductImagePreview(item: FirstLegCargoItem) {
|
||
if (!item.productImageFileId || item.productImagePreviewUrl || item.productImageUploading) return
|
||
item.productImageUploading = true
|
||
item.productImageError = ''
|
||
try {
|
||
const blob = await downloadMerchantProductImage(item.productImageFileId)
|
||
item.productImagePreviewUrl = URL.createObjectURL(blob)
|
||
} catch (error) {
|
||
item.productImageError = error instanceof Error ? error.message : '产品图片加载失败'
|
||
} finally {
|
||
item.productImageUploading = false
|
||
}
|
||
}
|
||
|
||
async function uploadCargoProductImage(item: FirstLegCargoItem, options: UploadRequestOptions) {
|
||
const file = options.file
|
||
const validationError = validateProductImageFile(file)
|
||
if (validationError) {
|
||
item.productImageError = validationError
|
||
ElMessage.warning(validationError)
|
||
throw new Error(validationError)
|
||
}
|
||
item.productImageUploading = true
|
||
item.productImageError = ''
|
||
try {
|
||
const result = await uploadMerchantProductImage(file)
|
||
if (item.productImagePreviewUrl.startsWith('blob:')) URL.revokeObjectURL(item.productImagePreviewUrl)
|
||
item.productImageFileId = String(result.fileId)
|
||
item.productImageName = result.fileName
|
||
item.productImagePreviewUrl = URL.createObjectURL(file)
|
||
} catch (error) {
|
||
item.productImageError = error instanceof Error ? error.message : '产品图片上传失败'
|
||
ElMessage.error(item.productImageError)
|
||
throw error
|
||
} finally {
|
||
item.productImageUploading = false
|
||
}
|
||
}
|
||
|
||
function removeCargoProductImage(item: FirstLegCargoItem) {
|
||
if (item.productImagePreviewUrl.startsWith('blob:')) URL.revokeObjectURL(item.productImagePreviewUrl)
|
||
item.productImageFileId = ''
|
||
item.productImageName = ''
|
||
item.productImagePreviewUrl = ''
|
||
item.productImageError = ''
|
||
}
|
||
|
||
function normalizedAddressValue(value: string | null | undefined) {
|
||
return value?.trim().toLocaleLowerCase() ?? ''
|
||
}
|
||
|
||
function findAddressBookEntryId(addressType: CustomerOrderAddress['addressType'], snapshot: FirstLegPartySnapshot | null) {
|
||
if (!snapshot) return ''
|
||
const address = orderAddresses.value.find((item) => item.addressType === addressType
|
||
&& normalizedAddressValue(item.companyName || item.addressName) === normalizedAddressValue(snapshot.name)
|
||
&& normalizedAddressValue(item.contactName) === normalizedAddressValue(snapshot.contactName)
|
||
&& normalizedAddressValue(item.phone) === normalizedAddressValue(snapshot.phone)
|
||
&& normalizedAddressValue(item.email) === normalizedAddressValue(snapshot.email)
|
||
&& normalizedAddressValue(item.countryCode) === normalizedAddressValue(snapshot.countryRegion)
|
||
&& normalizedAddressValue(item.province) === normalizedAddressValue(snapshot.province)
|
||
&& normalizedAddressValue(item.city) === normalizedAddressValue(snapshot.city)
|
||
&& normalizedAddressValue(item.postalCode) === normalizedAddressValue(snapshot.postalCode)
|
||
&& normalizedAddressValue(item.addressLine) === normalizedAddressValue(snapshot.address))
|
||
return address == null ? '' : String(address.id)
|
||
}
|
||
|
||
function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
|
||
return {
|
||
inboundCode: order.businessSummary.inboundCode,
|
||
referenceNo: order.referenceNo,
|
||
transportMode: order.transportMode ?? 'FCL',
|
||
businessType: order.businessType,
|
||
fulfillmentType: order.fulfillmentType ?? 'STANDARD',
|
||
lxCode: order.lxCode ?? '',
|
||
origin: order.origin,
|
||
destination: order.destination,
|
||
receivingWarehouseId: order.receivingWarehouseId,
|
||
expectedDepartureDate: order.expectedDepartureDate,
|
||
shipperPartyId: order.shipperPartyId,
|
||
senderAddressId: findAddressBookEntryId('PICKUP', order.shipperPartySnapshot),
|
||
shipperPartySnapshot: order.shipperPartySnapshot ? { ...order.shipperPartySnapshot } : null,
|
||
shipper: order.shipper,
|
||
consigneePartyId: order.consigneePartyId,
|
||
privateDestinationAddressId: findAddressBookEntryId('PRIVATE_DESTINATION', order.consigneePartySnapshot),
|
||
consigneePartySnapshot: order.consigneePartySnapshot ? { ...order.consigneePartySnapshot } : null,
|
||
consignee: order.consignee,
|
||
priority: order.priority,
|
||
customerRemark: order.customerRemark,
|
||
tradeTerm: order.tradeTerm,
|
||
destinationCountryCode: order.destinationCountryCode,
|
||
destinationWarehouseId: order.destinationWarehouseId,
|
||
destinationWarehouseName: order.destinationWarehouseName,
|
||
pickupMethod: order.pickupMethod,
|
||
channelCode: order.channelCode,
|
||
channelName: order.channelName,
|
||
pricingTaxMode: order.pricingTaxMode,
|
||
taxMode: order.taxMode,
|
||
customsInspection: order.customsInspection,
|
||
destinationType: order.destinationType,
|
||
amazonWarehouseCode: order.amazonWarehouseCode,
|
||
shipmentId: order.shipmentId,
|
||
items: order.items.map((item) => ({
|
||
...item,
|
||
productImagePreviewUrl: '',
|
||
productImageUploading: false,
|
||
productImageError: '',
|
||
})),
|
||
}
|
||
}
|
||
|
||
function copyDraftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
|
||
const draft = draftFromOrder(order)
|
||
return {
|
||
...draft,
|
||
inboundCode: '',
|
||
items: draft.items.map((item) => ({
|
||
...item,
|
||
id: `copied-cargo-${++cargoSequence}`,
|
||
productImagePreviewUrl: '',
|
||
productImageUploading: false,
|
||
productImageError: '',
|
||
})),
|
||
}
|
||
}
|
||
|
||
function selectChannel(code: string) {
|
||
const channel = channelOptions.value.find((item) => item.channelCode === code)
|
||
form.channelName = channel?.channelName ?? ''
|
||
if (channel && !channel.pricingTaxModes.includes(form.pricingTaxMode)) {
|
||
form.pricingTaxMode = channel.pricingTaxModes[0] ?? 'TAX_INCLUDED'
|
||
}
|
||
}
|
||
|
||
function pricingTaxModeLabel(mode: 'TAX_INCLUDED' | 'TAX_EXCLUDED') {
|
||
return mode === 'TAX_INCLUDED' ? '包税' : '不包税'
|
||
}
|
||
|
||
function formatEstimateMoney(value: number, currency: string) {
|
||
try {
|
||
return new Intl.NumberFormat('zh-CN', {
|
||
style: 'currency',
|
||
currency,
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}).format(value)
|
||
} catch {
|
||
return `${currency} ${value.toFixed(2)}`
|
||
}
|
||
}
|
||
|
||
function formatEstimateQuantity(value: number, unit: string) {
|
||
return `${value.toLocaleString('zh-CN', { maximumFractionDigits: 4 })} ${unit}`
|
||
}
|
||
|
||
const priceEstimateDisclaimer = computed(() => priceEstimate.value?.disclaimer || '仅供参考,最终以实际结算为准')
|
||
const priceEstimateBreakdown = computed(() => {
|
||
if (!priceEstimate.value) return null
|
||
const summary = summarizeFirstLegCargo(form.items)
|
||
return createPriceEstimateBreakdown(summary.grossWeightKg, summary.volumeCbm, priceEstimate.value.chargeUnit)
|
||
})
|
||
|
||
function resetPriceEstimate() {
|
||
estimateRequestSequence += 1
|
||
priceEstimate.value = null
|
||
priceEstimateLoading.value = false
|
||
priceEstimateError.value = ''
|
||
if (estimateTimer) clearTimeout(estimateTimer)
|
||
estimateTimer = undefined
|
||
}
|
||
|
||
async function loadChannelsForWarehouse(warehouseId: string) {
|
||
const sequence = ++channelRequestSequence
|
||
const selectedCode = form.channelCode
|
||
channelOptions.value = []
|
||
channelError.value = ''
|
||
channelLoading.value = true
|
||
form.channelCode = ''
|
||
form.channelName = ''
|
||
resetPriceEstimate()
|
||
try {
|
||
const channels = await getMerchantPricingChannels(warehouseId)
|
||
if (sequence !== channelRequestSequence || form.receivingWarehouseId !== warehouseId) return
|
||
channelOptions.value = channels
|
||
const selected = channels.find((item) => item.channelCode === selectedCode)
|
||
if (selected) {
|
||
form.channelCode = selected.channelCode
|
||
form.channelName = selected.channelName
|
||
if (!selected.pricingTaxModes.includes(form.pricingTaxMode)) {
|
||
form.pricingTaxMode = selected.pricingTaxModes[0] ?? 'TAX_INCLUDED'
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (sequence !== channelRequestSequence || form.receivingWarehouseId !== warehouseId) return
|
||
channelError.value = errorMessage(error)
|
||
} finally {
|
||
if (sequence === channelRequestSequence) channelLoading.value = false
|
||
}
|
||
}
|
||
|
||
function createEstimateRequest(): MerchantPriceEstimateRequest | null {
|
||
if (!form.receivingWarehouseId || !form.channelCode || form.destinationType === 'PRIVATE') return null
|
||
if (!availablePricingTaxModes.value.includes(form.pricingTaxMode)) return null
|
||
const summary = summarizeFirstLegCargo(form.items)
|
||
if (summary.grossWeightKg <= 0 || summary.volumeCbm <= 0) return null
|
||
if (form.destinationType === 'FBA' && !form.amazonWarehouseCode) return null
|
||
if (form.destinationType === 'OVERSEAS' && !form.destinationWarehouseId) return null
|
||
return {
|
||
originWarehouseId: form.receivingWarehouseId,
|
||
channelCode: form.channelCode,
|
||
destinationType: form.destinationType,
|
||
destinationCode: form.destinationType === 'FBA' ? form.amazonWarehouseCode : undefined,
|
||
destinationWarehouseId: form.destinationType === 'OVERSEAS' ? form.destinationWarehouseId : undefined,
|
||
destinationName: form.destinationWarehouseName || undefined,
|
||
pricingTaxMode: form.pricingTaxMode,
|
||
chargeMode: form.pricingTaxMode === 'TAX_INCLUDED' ? 'KG' : 'CBM',
|
||
actualWeightKg: summary.grossWeightKg,
|
||
actualVolumeCbm: summary.volumeCbm,
|
||
}
|
||
}
|
||
|
||
async function loadPriceEstimate() {
|
||
const request = createEstimateRequest()
|
||
const sequence = ++estimateRequestSequence
|
||
priceEstimate.value = null
|
||
priceEstimateError.value = ''
|
||
if (!request) {
|
||
priceEstimateLoading.value = false
|
||
return
|
||
}
|
||
priceEstimateLoading.value = true
|
||
try {
|
||
const estimate = await estimateMerchantFirstLegPrice(request)
|
||
if (sequence === estimateRequestSequence) priceEstimate.value = estimate
|
||
} catch (error) {
|
||
if (sequence === estimateRequestSequence) priceEstimateError.value = errorMessage(error)
|
||
} finally {
|
||
if (sequence === estimateRequestSequence) priceEstimateLoading.value = false
|
||
}
|
||
}
|
||
|
||
function schedulePriceEstimate() {
|
||
estimateRequestSequence += 1
|
||
priceEstimate.value = null
|
||
priceEstimateError.value = ''
|
||
priceEstimateLoading.value = false
|
||
if (estimateTimer) clearTimeout(estimateTimer)
|
||
estimateTimer = setTimeout(() => void loadPriceEstimate(), 300)
|
||
}
|
||
|
||
function retryPricingChannels() {
|
||
if (form.receivingWarehouseId) void loadChannelsForWarehouse(form.receivingWarehouseId)
|
||
}
|
||
|
||
watch(
|
||
() => JSON.stringify({
|
||
receivingWarehouseId: form.receivingWarehouseId,
|
||
channelCode: form.channelCode,
|
||
pricingTaxMode: form.pricingTaxMode,
|
||
destinationType: form.destinationType,
|
||
destinationWarehouseId: form.destinationWarehouseId,
|
||
amazonWarehouseCode: form.amazonWarehouseCode,
|
||
destinationWarehouseName: form.destinationWarehouseName,
|
||
items: form.items.map((item) => [item.grossWeightKg, item.volumeCbm]),
|
||
}),
|
||
schedulePriceEstimate,
|
||
)
|
||
|
||
async function selectDestinationWarehouse(id: string) {
|
||
const warehouse = availableDestinationWarehouses.value.find((item) => item.id === id)
|
||
if (!warehouse) return
|
||
form.destinationWarehouseId = warehouse.id
|
||
form.destinationWarehouseName = warehouse.name
|
||
form.destinationCountryCode = warehouse.countryCode.toUpperCase()
|
||
form.destination = warehouse.name
|
||
form.amazonWarehouseCode = form.destinationType === 'FBA' ? warehouse.code : ''
|
||
try {
|
||
const detail = await store.loadWarehouseDetail(warehouse.id)
|
||
form.destinationWarehouseName = detail.name
|
||
form.destinationCountryCode = detail.countryCode.toUpperCase()
|
||
form.destination = detail.name
|
||
form.amazonWarehouseCode = form.destinationType === 'FBA' ? detail.code : ''
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '目的仓详情加载失败')
|
||
}
|
||
}
|
||
|
||
async function changeDestinationType() {
|
||
form.destinationWarehouseId = ''
|
||
form.destinationWarehouseName = ''
|
||
form.destination = ''
|
||
form.amazonWarehouseCode = ''
|
||
form.privateDestinationAddressId = ''
|
||
form.consigneePartyId = ''
|
||
form.consigneePartySnapshot = form.destinationType === 'PRIVATE' ? createEmptyPartySnapshot('CONSIGNEE') : null
|
||
form.consignee = ''
|
||
try {
|
||
await loadDestinationWarehouses(form.destinationType)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '目的仓数据加载失败')
|
||
}
|
||
if (form.destinationType !== 'PRIVATE' && availableDestinationWarehouses.value.length === 1) {
|
||
await selectDestinationWarehouse(availableDestinationWarehouses.value[0].id)
|
||
}
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function changePickupMethod() {
|
||
if (form.pickupMethod === 'PICKUP') {
|
||
form.shipperPartySnapshot ??= createPickupPartySnapshot()
|
||
} else {
|
||
form.senderAddressId = ''
|
||
form.shipperPartyId = ''
|
||
form.shipperPartySnapshot = null
|
||
form.shipper = ''
|
||
form.origin = ''
|
||
}
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function syncPrivateDestinationName(value: string) {
|
||
if (form.destinationType !== 'PRIVATE') return
|
||
form.destinationWarehouseName = value
|
||
form.destination = value.trim()
|
||
if (form.consigneePartySnapshot) form.consigneePartySnapshot.name = value
|
||
}
|
||
|
||
function selectPrivateAddress(id: string | null | undefined) {
|
||
const address = privateAddresses.value.find((item) => String(item.id) === id)
|
||
if (!address) {
|
||
form.privateDestinationAddressId = ''
|
||
form.consigneePartyId = ''
|
||
form.consigneePartySnapshot = createEmptyPartySnapshot('CONSIGNEE')
|
||
form.consignee = ''
|
||
form.destination = ''
|
||
form.destinationCountryCode = ''
|
||
form.destinationWarehouseId = ''
|
||
form.destinationWarehouseName = ''
|
||
return
|
||
}
|
||
form.privateDestinationAddressId = String(address.id)
|
||
form.consigneePartyId = ''
|
||
form.consigneePartySnapshot = addressSnapshot(address, 'CONSIGNEE')
|
||
form.consignee = address.companyName || address.addressName
|
||
form.destination = address.addressName
|
||
form.destinationCountryCode = address.countryCode
|
||
form.destinationWarehouseId = ''
|
||
form.destinationWarehouseName = address.addressName
|
||
}
|
||
|
||
function addressSnapshot(address: CustomerOrderAddress, role: FirstLegPartyRole): FirstLegPartySnapshot {
|
||
return {
|
||
id: '',
|
||
code: '',
|
||
name: address.companyName || address.addressName,
|
||
role,
|
||
contactName: address.contactName,
|
||
phone: address.phone,
|
||
email: address.email ?? '',
|
||
countryRegion: address.countryCode,
|
||
province: address.province ?? '',
|
||
city: address.city,
|
||
postalCode: address.postalCode ?? '',
|
||
address: address.addressLine,
|
||
}
|
||
}
|
||
|
||
async function loadOrderAddresses() {
|
||
orderAddressesLoading.value = true
|
||
try {
|
||
orderAddresses.value = await getMerchantOrderAddresses()
|
||
} finally {
|
||
orderAddressesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function selectPickupAddress(id: string | null | undefined) {
|
||
const address = pickupAddresses.value.find((item) => String(item.id) === id)
|
||
if (!address) {
|
||
form.senderAddressId = ''
|
||
form.shipperPartyId = ''
|
||
form.shipperPartySnapshot = createEmptyPartySnapshot('SHIPPER')
|
||
form.shipper = ''
|
||
form.origin = ''
|
||
return
|
||
}
|
||
form.senderAddressId = String(address.id)
|
||
form.shipperPartyId = ''
|
||
form.shipperPartySnapshot = addressSnapshot(address, 'SHIPPER')
|
||
form.shipper = address.companyName || address.addressName
|
||
form.origin = address.city
|
||
}
|
||
|
||
function addressPayload(addressType: CustomerOrderAddress['addressType'], snapshot: FirstLegPartySnapshot, addressName: string): CustomerOrderAddressSaveRequest {
|
||
const payload: CustomerOrderAddressSaveRequest = {
|
||
addressType,
|
||
addressName: addressName.trim(),
|
||
companyName: snapshot.name.trim() || addressName.trim(),
|
||
contactName: snapshot.contactName.trim(),
|
||
phone: snapshot.phone.trim(),
|
||
countryCode: snapshot.countryRegion.trim().toUpperCase(),
|
||
city: snapshot.city.trim(),
|
||
addressLine: snapshot.address.trim(),
|
||
defaultAddress: false,
|
||
}
|
||
if (snapshot.email.trim()) payload.email = snapshot.email.trim()
|
||
if (snapshot.province.trim()) payload.province = snapshot.province.trim()
|
||
if (snapshot.postalCode.trim()) payload.postalCode = snapshot.postalCode.trim()
|
||
return payload
|
||
}
|
||
|
||
function addressComplete(snapshot: FirstLegPartySnapshot | null, addressName: string) {
|
||
return Boolean(snapshot && addressName.trim() && snapshot.contactName.trim() && snapshot.phone.trim()
|
||
&& /^[A-Za-z]{2}$/.test(snapshot.countryRegion.trim()) && snapshot.city.trim() && snapshot.address.trim())
|
||
}
|
||
|
||
async function savePickupAddress() {
|
||
const snapshot = form.shipperPartySnapshot
|
||
const addressName = snapshot?.name.trim() || `${snapshot?.city.trim() || '国内'}提货地址`
|
||
if (!addressComplete(snapshot, addressName)) {
|
||
ElMessage.warning('请先完整填写提货地址和联系人资料')
|
||
return
|
||
}
|
||
const existingAddressId = findAddressBookEntryId('PICKUP', snapshot)
|
||
if (existingAddressId && existingAddressId !== form.senderAddressId) {
|
||
selectPickupAddress(existingAddressId)
|
||
ElMessage.info('相同的企业提货地址已存在,已为您选中')
|
||
return
|
||
}
|
||
try {
|
||
const payload = addressPayload('PICKUP', snapshot!, addressName)
|
||
if (form.senderAddressId) await updateMerchantOrderAddress(form.senderAddressId, payload)
|
||
else form.senderAddressId = String(await createMerchantOrderAddress(payload))
|
||
await loadOrderAddresses()
|
||
selectPickupAddress(form.senderAddressId)
|
||
ElMessage.success('企业提货地址已保存')
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function savePrivateAddress() {
|
||
const snapshot = form.consigneePartySnapshot
|
||
const addressName = form.destinationWarehouseName.trim()
|
||
if (snapshot) snapshot.countryRegion = form.destinationCountryCode.trim().toUpperCase()
|
||
if (!addressComplete(snapshot, addressName)) {
|
||
ElMessage.warning('请先完整填写私人仓地址和收件资料')
|
||
return
|
||
}
|
||
snapshot!.name = addressName
|
||
const existingAddressId = findAddressBookEntryId('PRIVATE_DESTINATION', snapshot)
|
||
if (existingAddressId && existingAddressId !== form.privateDestinationAddressId) {
|
||
selectPrivateAddress(existingAddressId)
|
||
ElMessage.info('相同的企业私人仓地址已存在,已为您选中')
|
||
return
|
||
}
|
||
try {
|
||
const payload = addressPayload('PRIVATE_DESTINATION', snapshot!, addressName)
|
||
if (form.privateDestinationAddressId) await updateMerchantOrderAddress(form.privateDestinationAddressId, payload)
|
||
else form.privateDestinationAddressId = String(await createMerchantOrderAddress(payload))
|
||
await loadOrderAddresses()
|
||
selectPrivateAddress(form.privateDestinationAddressId)
|
||
ElMessage.success('企业私人仓地址已保存')
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function removeOrderAddress(type: CustomerOrderAddress['addressType']) {
|
||
const id = type === 'PICKUP' ? form.senderAddressId : form.privateDestinationAddressId
|
||
if (!id) return
|
||
try {
|
||
await disableMerchantOrderAddress(id)
|
||
if (type === 'PICKUP') selectPickupAddress(null)
|
||
else selectPrivateAddress(null)
|
||
await loadOrderAddresses()
|
||
ElMessage.success('地址已从企业地址簿停用')
|
||
} catch (error) {
|
||
ElMessage.error(errorMessage(error))
|
||
}
|
||
}
|
||
|
||
async function loadSelectedWarehouse(id: string | number | boolean | undefined) {
|
||
const warehouseId = typeof id === 'string' ? id : typeof id === 'number' ? String(id) : ''
|
||
if (!warehouseId) {
|
||
channelRequestSequence += 1
|
||
channelOptions.value = []
|
||
channelError.value = ''
|
||
form.channelCode = ''
|
||
form.channelName = ''
|
||
resetPriceEstimate()
|
||
return
|
||
}
|
||
const requests = [loadChannelsForWarehouse(warehouseId)]
|
||
try {
|
||
requests.push(store.loadWarehouseDetail(warehouseId).then(() => undefined))
|
||
await Promise.all(requests)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '仓库地址与收货时间加载失败')
|
||
}
|
||
}
|
||
|
||
async function autofillWarehouseSelections() {
|
||
if (!isCreatePage.value || editingId.value) return
|
||
if (!form.receivingWarehouseId) {
|
||
const yiwuWarehouse = availableWarehouses.value.find((warehouse) => warehouse.code.toUpperCase().includes('YIWU'))
|
||
?? availableWarehouses.value.find((warehouse) => warehouse.name.includes('义乌'))
|
||
if (yiwuWarehouse) form.receivingWarehouseId = yiwuWarehouse.id
|
||
}
|
||
if (form.receivingWarehouseId) {
|
||
await loadSelectedWarehouse(form.receivingWarehouseId)
|
||
}
|
||
if (form.destinationType !== 'PRIVATE' && !form.destinationWarehouseId && form.amazonWarehouseCode) {
|
||
const destinationCode = form.amazonWarehouseCode.trim().toUpperCase()
|
||
const matchedDestination = availableDestinationWarehouses.value.find((warehouse) => warehouse.code.trim().toUpperCase() === destinationCode)
|
||
if (matchedDestination) await selectDestinationWarehouse(matchedDestination.id)
|
||
}
|
||
if (form.destinationType !== 'PRIVATE' && !form.destinationWarehouseId && availableDestinationWarehouses.value.length === 1) {
|
||
await selectDestinationWarehouse(availableDestinationWarehouses.value[0].id)
|
||
}
|
||
markEditorClean()
|
||
}
|
||
|
||
function getOrderRowActions(row: object): TableAction[] {
|
||
const order = row as FirstLegOrder
|
||
const actions: TableAction[] = [
|
||
{ key: 'detail', label: '详情', run: () => openDetail(order) },
|
||
]
|
||
if (canCreateOrder.value) {
|
||
actions.push({ key: 'copy', label: '复制下单', loading: copyingOrderId.value === order.id, run: () => copyOrder(order) })
|
||
}
|
||
if (canConfirmInboundFee.value && order.businessSummary.customerFeeConfirmationRequired) {
|
||
actions.push({ key: 'fee', label: '确认计费重', run: () => openFeeConfirmation(order) })
|
||
}
|
||
if (canEditOrder.value && canClientEditFirstLegOrder(order.status)) {
|
||
actions.push({ key: 'edit', label: '编辑', loading: openingEditor.value, run: () => openEdit(order) })
|
||
}
|
||
if (canOfferCancellation(order)) {
|
||
actions.push({ key: 'cancel', label: pendingCancellation.value?.id === order.id ? '重试取消' : '取消订单', loading: cancellingOrderId.value === order.id, disabled: Boolean(cancellingOrderId.value), run: () => cancelOrder(order) })
|
||
}
|
||
if (eligibleCustomsDetail(order)) {
|
||
actions.push({ key: 'customs', label: '报关资料', run: () => openCustomsDocuments(order) })
|
||
}
|
||
return actions
|
||
}
|
||
|
||
async function copyOrder(order: FirstLegOrder) {
|
||
if (copyingOrderId.value || !canCreateOrder.value) return
|
||
copyingOrderId.value = order.id
|
||
try {
|
||
await router.push({
|
||
name: 'first-leg-order-create',
|
||
query: {
|
||
copyFrom: order.id,
|
||
...(order.status === 'DRAFT' ? { copyDraft: '1' } : {}),
|
||
},
|
||
})
|
||
} finally {
|
||
copyingOrderId.value = ''
|
||
}
|
||
}
|
||
|
||
async function openCreate() {
|
||
if (availableWarehouses.value.length === 0) {
|
||
ElMessage.warning(supportingDataLoading.value
|
||
? '平台收货仓正在加载,请稍候再试'
|
||
: '当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
|
||
return
|
||
}
|
||
editingId.value = undefined
|
||
replaceForm(emptyDraft())
|
||
resetEditorValidation()
|
||
await router.push({ name: 'first-leg-order-create' })
|
||
}
|
||
|
||
async function openEdit(order: FirstLegOrder) {
|
||
if (openingEditor.value || !canEditOrder.value || !canClientEditFirstLegOrder(order.status)) return
|
||
openingEditor.value = true
|
||
try {
|
||
const [detailedOrder] = await Promise.all([store.loadDetail(order.id), loadOrderAddresses()])
|
||
const draft = draftFromOrder(detailedOrder)
|
||
if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType)
|
||
const warehouseAvailable = availableWarehouses.value.some((item) => item.id === detailedOrder.receivingWarehouseId)
|
||
if (!warehouseAvailable) {
|
||
draft.receivingWarehouseId = ''
|
||
draft.channelCode = ''
|
||
draft.channelName = ''
|
||
channelOptions.value = []
|
||
channelError.value = ''
|
||
resetPriceEstimate()
|
||
ElMessage.warning('原平台收货仓当前不可用,请重新选择可用收货仓')
|
||
}
|
||
if (draft.destinationType !== 'PRIVATE') {
|
||
const destinationAvailable = store.warehouses.some((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
|
||
if (!destinationAvailable) {
|
||
draft.destinationWarehouseId = ''
|
||
draft.destinationWarehouseName = ''
|
||
draft.amazonWarehouseCode = ''
|
||
ElMessage.warning('原目的仓当前不可用,请重新选择平台维护的目的仓')
|
||
}
|
||
}
|
||
editingId.value = detailedOrder.id
|
||
editingSession.value = store.beginDraftEdit(detailedOrder)
|
||
replaceForm(draft)
|
||
await Promise.all(form.items.map(ensureProductImagePreview))
|
||
if (warehouseAvailable) void loadSelectedWarehouse(detailedOrder.receivingWarehouseId)
|
||
editorOpen.value = true
|
||
resetEditorValidation()
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '订单详情加载失败,暂时无法编辑')
|
||
} finally {
|
||
openingEditor.value = false
|
||
}
|
||
}
|
||
|
||
async function openDetail(order: FirstLegOrder) {
|
||
await router.push({ name: 'first-leg-order-detail', params: { orderId: order.id } })
|
||
}
|
||
|
||
async function loadDetailPage(orderId: string) {
|
||
feeConfirmationOpen.value = false
|
||
detailId.value = orderId
|
||
detailCargoPage.value = 1
|
||
detailLoading.value = true
|
||
detailError.value = ''
|
||
try {
|
||
const detailedOrder = await store.loadDetail(orderId)
|
||
if (detailedOrder.status === 'CANCELLED') resetInboundBillingState()
|
||
else void loadInboundBilling(detailedOrder)
|
||
// Load previews from the persisted file IDs after the store has replaced the detail row.
|
||
await Promise.all((detailOrder.value?.items ?? detailedOrder.items).map(ensureProductImagePreview))
|
||
} catch (error) {
|
||
detailError.value = error instanceof Error ? error.message : '订单详情加载失败'
|
||
ElMessage.error(detailError.value)
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function openFeeConfirmation(order: FirstLegOrder) {
|
||
resetInboundBillingState()
|
||
inboundBillingUnavailableMessage.value = '未推送计费重'
|
||
feeConfirmationOpen.value = true
|
||
void loadInboundBilling(order, '未推送计费重')
|
||
}
|
||
|
||
async function retryDetail() {
|
||
if (detailId.value) await loadDetailPage(detailId.value)
|
||
}
|
||
|
||
function resetInboundBillingState() {
|
||
inboundBillingRequestSequence += 1
|
||
inboundBillingOrderId.value = ''
|
||
inboundBilling.value = null
|
||
inboundBillingLoading.value = false
|
||
inboundBillingUnavailable.value = false
|
||
inboundBillingUnavailableMessage.value = '还未测量推送'
|
||
inboundBillingError.value = ''
|
||
}
|
||
|
||
function isCurrentInboundBillingRequest(sequence: number, orderId: string) {
|
||
return sequence === inboundBillingRequestSequence
|
||
&& inboundBillingOrderId.value === orderId
|
||
&& (isDetailPage.value || feeConfirmationOpen.value)
|
||
}
|
||
|
||
async function loadInboundBilling(order: FirstLegOrder, unavailableMessage = '还未测量推送') {
|
||
const sequence = ++inboundBillingRequestSequence
|
||
inboundBillingOrderId.value = order.id
|
||
inboundBilling.value = null
|
||
inboundBillingLoading.value = false
|
||
inboundBillingUnavailable.value = false
|
||
inboundBillingUnavailableMessage.value = unavailableMessage
|
||
inboundBillingError.value = ''
|
||
|
||
if (order.status === 'DRAFT') {
|
||
inboundBillingUnavailable.value = true
|
||
inboundBillingUnavailableMessage.value = feeConfirmationOpen.value
|
||
? '未推送计费重'
|
||
: '订单提交后可查看入库结果'
|
||
return
|
||
}
|
||
|
||
inboundBillingLoading.value = true
|
||
try {
|
||
const result = await getMerchantInboundBilling(order.id)
|
||
if (!isCurrentInboundBillingRequest(sequence, order.id)) return
|
||
inboundBilling.value = result
|
||
} catch (error) {
|
||
if (!isCurrentInboundBillingRequest(sequence, order.id)) return
|
||
if (error instanceof ApiError && error.code === 'A0404') {
|
||
inboundBillingUnavailable.value = true
|
||
inboundBillingUnavailableMessage.value = feeConfirmationOpen.value
|
||
? '未推送计费重'
|
||
: error.message || unavailableMessage
|
||
} else {
|
||
inboundBillingError.value = error instanceof Error ? error.message : '入库与费用加载失败'
|
||
}
|
||
} finally {
|
||
if (isCurrentInboundBillingRequest(sequence, order.id)) inboundBillingLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function retryInboundBilling() {
|
||
const order = inboundBillingOrderId.value ? store.getById(inboundBillingOrderId.value) : undefined
|
||
if (order) await loadInboundBilling(order, feeConfirmationOpen.value ? '未推送计费重' : '还未测量推送')
|
||
}
|
||
|
||
function handleInboundFeeConfirmed() {
|
||
if (!isCreatePage.value && !isDetailPage.value) void loadOrderPage()
|
||
if (!feeConfirmationOpen.value) void retryInboundBilling()
|
||
}
|
||
|
||
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
|
||
if (isCreatePage.value) {
|
||
markEditorClean()
|
||
void router.push({ name: 'first-leg-orders' })
|
||
}
|
||
})
|
||
}
|
||
|
||
async function resetCreateForm() {
|
||
if (saving.value) return
|
||
if (editorDirty.value) {
|
||
try {
|
||
await ElMessageBox.confirm('确认清空当前填写的订单内容并恢复默认值?', '重置订单', {
|
||
type: 'warning',
|
||
confirmButtonText: '确认重置',
|
||
cancelButtonText: '继续填写',
|
||
})
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
throw error
|
||
}
|
||
}
|
||
editingId.value = undefined
|
||
replaceForm(emptyDraft())
|
||
await autofillWarehouseSelections()
|
||
resetEditorValidation()
|
||
await router.replace({ name: 'first-leg-order-create' })
|
||
}
|
||
|
||
function addCargoItem() {
|
||
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
|
||
resetEditorValidation()
|
||
}
|
||
|
||
async function changeFulfillmentType() {
|
||
if (form.fulfillmentType !== 'ONE_CLICK_DROPSHIP' || form.destinationType === 'OVERSEAS') return
|
||
form.destinationType = 'OVERSEAS'
|
||
await changeDestinationType()
|
||
}
|
||
|
||
const sharedPackingFields = [
|
||
'packageType', 'packageCount', 'lengthCm', 'widthCm', 'heightCm',
|
||
'grossWeightKg', 'netWeightKg', 'volumeCbm',
|
||
] as const
|
||
|
||
function packingGroupFor(item: FirstLegCargoItem) {
|
||
return item.packingGroupNo.trim()
|
||
}
|
||
|
||
function mixedSkuCount(item: FirstLegCargoItem) {
|
||
const group = packingGroupFor(item)
|
||
return group ? form.items.filter((candidate) => packingGroupFor(candidate) === group).length : 1
|
||
}
|
||
|
||
function isAdditionalMixedSku(item: FirstLegCargoItem, index: number) {
|
||
const group = packingGroupFor(item)
|
||
return Boolean(group && form.items.slice(0, index).some((candidate) => packingGroupFor(candidate) === group))
|
||
}
|
||
|
||
function syncSkuQuantity(item: FirstLegCargoItem) {
|
||
if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0
|
||
&& Number.isInteger(item.packageCount) && item.packageCount > 0) {
|
||
item.pieces = item.unitsPerPackage * item.packageCount
|
||
}
|
||
}
|
||
|
||
function syncPackingGroupFields(source: FirstLegCargoItem) {
|
||
if (form.fulfillmentType !== 'ONE_CLICK_DROPSHIP') return
|
||
syncSkuQuantity(source)
|
||
const group = packingGroupFor(source)
|
||
if (!group) return
|
||
form.items.forEach((item) => {
|
||
if (item === source || packingGroupFor(item) !== group) return
|
||
sharedPackingFields.forEach((field) => { item[field] = source[field] as never })
|
||
syncSkuQuantity(item)
|
||
})
|
||
}
|
||
|
||
function normalizeSingleSkuGroups() {
|
||
const counts = new Map<string, number>()
|
||
form.items.forEach((item) => {
|
||
const group = packingGroupFor(item)
|
||
if (group) counts.set(group, (counts.get(group) ?? 0) + 1)
|
||
})
|
||
form.items.forEach((item) => {
|
||
const group = packingGroupFor(item)
|
||
if (group && counts.get(group) === 1) item.packingGroupNo = ''
|
||
})
|
||
}
|
||
|
||
function addSkuToPackingGroup(index: number) {
|
||
const source = form.items[index]
|
||
if (!source) return
|
||
const group = packingGroupFor(source) || `PG-${Date.now().toString(36)}-${++packingGroupSequence}`
|
||
source.packingGroupNo = group
|
||
const added = createEmptyCargoItem(`new-cargo-${++cargoSequence}`)
|
||
added.packingGroupNo = group
|
||
sharedPackingFields.forEach((field) => { added[field] = source[field] as never })
|
||
syncSkuQuantity(added)
|
||
let insertAt = index + 1
|
||
while (insertAt < form.items.length && packingGroupFor(form.items[insertAt]!) === group) insertAt++
|
||
form.items.splice(insertAt, 0, added)
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function removeCargoItem(index: number) {
|
||
if (form.items.length === 1) {
|
||
ElMessage.warning('订单至少需要保留一条货物明细')
|
||
return
|
||
}
|
||
const [removed] = form.items.splice(index, 1)
|
||
if (removed) releaseProductImagePreviews([removed])
|
||
normalizeSingleSkuGroups()
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function displayListValue(value: string | number | null | undefined) {
|
||
return value === '' || value == null ? '—' : String(value)
|
||
}
|
||
|
||
function orderMatchesCargoKeyword(order: FirstLegOrder, keyword: string) {
|
||
const normalized = keyword.toLowerCase()
|
||
return [
|
||
order.businessSummary.inboundCode,
|
||
...order.skuNos,
|
||
...order.items.map((item) => item.sku),
|
||
].some((value) => String(value ?? '').toLowerCase().includes(normalized))
|
||
}
|
||
|
||
function uniqueItemValues(order: FirstLegOrder, key: 'sku' | 'shipmentId' | 'referenceId') {
|
||
if (key === 'sku' && order.skuNos.length) return [...new Set(order.skuNos)].join('、')
|
||
const values = order.items.map((item) => item[key]?.trim()).filter((value): value is string => Boolean(value))
|
||
return [...new Set(values)].join('、') || '-'
|
||
}
|
||
|
||
function orderPackageCount(order: FirstLegOrder) {
|
||
return order.businessSummary.totalPackageCount ?? (order.items.length ? summarizeFirstLegCargo(order.items).packageCount : null)
|
||
}
|
||
|
||
function orderGrossWeight(order: FirstLegOrder) {
|
||
return order.businessSummary.actualGrossWeightKg ?? (order.items.length ? summarizeFirstLegCargo(order.items).grossWeightKg : null)
|
||
}
|
||
|
||
function orderVolume(order: FirstLegOrder) {
|
||
return order.businessSummary.actualVolumeCbm ?? (order.items.length ? summarizeFirstLegCargo(order.items).volumeCbm : null)
|
||
}
|
||
|
||
function orderItemTotal(order: FirstLegOrder, key: 'netWeightKg' | 'pieces') {
|
||
if (key === 'netWeightKg' && order.totalNetWeightKg != null) return order.totalNetWeightKg
|
||
if (key === 'pieces' && order.totalQuantity != null) return order.totalQuantity
|
||
if (!order.items.length) return null
|
||
return order.items.reduce((total, item) => total + (Number(item[key]) || 0), 0)
|
||
}
|
||
|
||
function formatListMetric(value: number | null | undefined, unit = '') {
|
||
if (value == null) return '-'
|
||
return `${value.toLocaleString('zh-CN', { maximumFractionDigits: 3 })}${unit ? ` ${unit}` : ''}`
|
||
}
|
||
|
||
function csvCell(value: unknown) {
|
||
return `"${String(value ?? '').replaceAll('"', '""')}"`
|
||
}
|
||
|
||
function downloadCsv(fileName: string, rows: unknown[][]) {
|
||
const content = `\uFEFF${rows.map((row) => row.map(csvCell).join(',')).join('\r\n')}`
|
||
const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }))
|
||
const anchor = document.createElement('a')
|
||
anchor.href = url
|
||
anchor.download = fileName
|
||
anchor.click()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
function saveBlob(blob: Blob, fileName: string) {
|
||
const url = URL.createObjectURL(blob)
|
||
const anchor = document.createElement('a')
|
||
anchor.href = url
|
||
anchor.download = fileName
|
||
anchor.click()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
function handleOrderSelectionChange(rows: FirstLegOrder[]) {
|
||
selectedOrders.value = rows
|
||
}
|
||
|
||
async function submitSelectedDraftOrders() {
|
||
const ids = [...new Set([...pendingBatchSubmissionIds.value, ...selectedDraftOrders.value.map(order => order.id)])]
|
||
if (batchSubmitting.value || !ids.length) return
|
||
if (companySubmissionBlocked.value) {
|
||
ElMessage.warning('企业资料审核通过后才能提交订单')
|
||
return
|
||
}
|
||
try {
|
||
await ElMessageBox.confirm(`确认提交 ${ids.length} 条草稿订单吗?${pendingBatchSubmissionIds.value.length ? '上次结果未确认的订单将使用原提交命令重试。' : ''}`, '批量提交订单', {
|
||
type: 'warning',
|
||
confirmButtonText: '确认提交',
|
||
cancelButtonText: '取消',
|
||
})
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
throw error
|
||
}
|
||
|
||
batchSubmitting.value = true
|
||
try {
|
||
const results = await Promise.allSettled(ids.map(id => store.submitSaved(id, companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined)))
|
||
pendingBatchSubmissionIds.value = ids.filter((id, index) => results[index]?.status === 'rejected' && store.hasPendingSubmission(id))
|
||
const successCount = results.filter((result) => result.status === 'fulfilled').length
|
||
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected')
|
||
await searchOrders(appliedFilters.value)
|
||
if (!failures.length) {
|
||
ElMessage.success(`已提交 ${successCount} 条草稿订单`)
|
||
return
|
||
}
|
||
const firstReason = failures[0]?.reason
|
||
const reason = firstReason instanceof Error ? firstReason.message : '提交失败'
|
||
if (successCount > 0) ElMessage.warning(`${successCount} 条提交成功,${failures.length} 条失败:${reason}`)
|
||
else ElMessage.error(`${failures.length} 条草稿订单提交失败:${reason}`)
|
||
} finally {
|
||
batchSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
async function downloadShippingMarks() {
|
||
if (downloadingMarks.value) return
|
||
const orders = selectedOrders.value
|
||
if (!orders.length) return
|
||
downloadingMarks.value = true
|
||
try {
|
||
const result = await downloadMerchantOrderCartonLabelsBatch(orders.map((order) => order.id))
|
||
saveBlob(result.blob, `批量箱唛-${orders.length}单.pdf`)
|
||
if (result.failureCount > 0) {
|
||
ElMessage.warning(`箱唛已下载,${result.failureCount} 份订单生成失败:${result.failures}`)
|
||
} else {
|
||
ElMessage.success(`已下载 ${orders.length} 份订单箱唛`)
|
||
}
|
||
} catch (error) {
|
||
const reason = error instanceof ApiError
|
||
? `${error.message}${error.code ? ` [${error.code}]` : ''}${error.traceId ? `,追踪号 ${error.traceId}` : ''}`
|
||
: (error instanceof Error ? error.message : '箱唛生成失败')
|
||
ElMessage.error(reason)
|
||
} finally {
|
||
downloadingMarks.value = false
|
||
}
|
||
}
|
||
|
||
function exportOrderInformation() {
|
||
exportingOrders.value = true
|
||
try {
|
||
const rows = selectedOrders.value.map((order) => [
|
||
displayListValue(order.businessSummary.inboundCode),
|
||
displayListValue(order.channelName),
|
||
order.transportMode ? getFirstLegTransportModeLabel(order.transportMode) : '-',
|
||
pickupMethodLabels[order.pickupMethod],
|
||
order.pickupMethod === 'PICKUP' ? formatExportDateTime(order.requestedPickupAt) : '-',
|
||
displayListValue(order.destinationWarehouseName),
|
||
destinationTypeLabels[order.destinationType],
|
||
getFirstLegStatusLabel(order.status),
|
||
orderProgressLabel(store.orderProgress[order.id]),
|
||
formatListMetric(orderPackageCount(order)),
|
||
displayListValue(order.expressCompanyName),
|
||
displayListValue(order.trackingNumber),
|
||
uniqueItemValues(order, 'sku'),
|
||
formatListMetric(orderGrossWeight(order), 'kg'),
|
||
formatListMetric(orderItemTotal(order, 'netWeightKg'), 'kg'),
|
||
formatListMetric(orderVolume(order), 'm³'),
|
||
formatListMetric(orderItemTotal(order, 'pieces')),
|
||
'-',
|
||
formatListMetric(order.chargeableVolumeCbm, 'm³'),
|
||
'-',
|
||
displayListValue(order.customerRemark),
|
||
displayListValue(order.shipmentId || uniqueItemValues(order, 'shipmentId')),
|
||
displayListValue(order.referenceNo || uniqueItemValues(order, 'referenceId')),
|
||
formatExportDateTime(order.inboundedAt),
|
||
formatExportDateTime(order.createdAt),
|
||
])
|
||
downloadCsv('订单信息.csv', [[
|
||
'入仓码', '渠道', '运输方式', '交货方式', '预约提货时间', '目的仓', '目的仓类型', '当前订单状态', '履约进度', '箱数', '快递公司', '快递单号', 'SKUS',
|
||
'总毛重', '总净重', '总体积', '总数量', '计费单价', '计费体积', '计费重量', '备注',
|
||
'货件编号', '跟踪编号', '国内仓入库时间', '创建时间',
|
||
], ...rows])
|
||
ElMessage.success(`已导出 ${rows.length} 条订单信息`)
|
||
} finally {
|
||
exportingOrders.value = false
|
||
}
|
||
}
|
||
|
||
const allCargoSelected = computed(() => form.items.length > 0 && selectedCargoIds.value.length === form.items.length)
|
||
const someCargoSelected = computed(() => selectedCargoIds.value.length > 0 && !allCargoSelected.value)
|
||
|
||
function toggleAllCargoItems(checked: boolean | string | number) {
|
||
selectedCargoIds.value = checked ? form.items.map((item) => item.id) : []
|
||
}
|
||
|
||
function toggleCargoItem(itemId: string, checked: boolean | string | number) {
|
||
if (checked) {
|
||
if (!selectedCargoIds.value.includes(itemId)) selectedCargoIds.value.push(itemId)
|
||
return
|
||
}
|
||
selectedCargoIds.value = selectedCargoIds.value.filter((id) => id !== itemId)
|
||
}
|
||
|
||
function removeSelectedCargoItems() {
|
||
if (!selectedCargoIds.value.length) {
|
||
ElMessage.warning('请先勾选要删除的货物')
|
||
return
|
||
}
|
||
if (selectedCargoIds.value.length >= form.items.length) {
|
||
ElMessage.warning('订单至少需要保留一条货物明细')
|
||
return
|
||
}
|
||
const selectedIds = new Set(selectedCargoIds.value)
|
||
const removedItems = form.items.filter((item) => selectedIds.has(item.id))
|
||
form.items.splice(0, form.items.length, ...form.items.filter((item) => !selectedIds.has(item.id)))
|
||
normalizeSingleSkuGroups()
|
||
releaseProductImagePreviews(removedItems)
|
||
selectedCargoIds.value = []
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function openBatchImport() {
|
||
batchImportMode.value = 'PARTIAL'
|
||
batchImportFile.value = null
|
||
batchImportIdempotencyKey.value = ''
|
||
batchImportResult.value = null
|
||
batchImportError.value = ''
|
||
batchImportOpen.value = true
|
||
}
|
||
|
||
function batchImportExcelRow(item: { index: number; clientRowId: string }) {
|
||
const matched = /^excel-row-(\d+)$/.exec(item.clientRowId)
|
||
return matched ? Number(matched[1]) : item.index + 3
|
||
}
|
||
|
||
function selectBatchImportFile(event: Event) {
|
||
const input = event.target as HTMLInputElement
|
||
const file = input.files?.[0]
|
||
input.value = ''
|
||
if (!file) return
|
||
if (!file.name.toLowerCase().endsWith('.xlsx')) {
|
||
batchImportError.value = '仅支持 .xlsx 格式的订单导入文件'
|
||
ElMessage.error(batchImportError.value)
|
||
return
|
||
}
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
batchImportError.value = '订单导入文件不能超过 5MB'
|
||
ElMessage.error(batchImportError.value)
|
||
return
|
||
}
|
||
batchImportFile.value = file
|
||
batchImportIdempotencyKey.value = createIdempotencyKey('first-leg-batch-import')
|
||
batchImportResult.value = null
|
||
batchImportError.value = ''
|
||
}
|
||
|
||
async function downloadBatchTemplate() {
|
||
try {
|
||
batchTemplateDownloading.value = true
|
||
batchImportError.value = ''
|
||
saveBlob(await downloadMerchantFirstLegBatchImportTemplate(), '头程订单批量导入模板.xlsx')
|
||
} catch (error) {
|
||
batchImportError.value = batchImportErrorMessage(error)
|
||
ElMessage.error(batchImportError.value)
|
||
} finally {
|
||
batchTemplateDownloading.value = false
|
||
}
|
||
}
|
||
|
||
async function downloadBatchErrorReport() {
|
||
const result = batchImportResult.value
|
||
if (!result?.errorReportFileId) return
|
||
try {
|
||
batchErrorReportDownloading.value = true
|
||
batchImportError.value = ''
|
||
saveBlob(
|
||
await downloadMerchantFirstLegBatchImportErrorReport(result.batchNo, result.errorReportFileId),
|
||
`${result.batchNo}-错误报告.xlsx`,
|
||
)
|
||
} catch (error) {
|
||
batchImportError.value = batchImportErrorMessage(error)
|
||
ElMessage.error(batchImportError.value)
|
||
} finally {
|
||
batchErrorReportDownloading.value = false
|
||
}
|
||
}
|
||
|
||
async function submitBatchImport() {
|
||
if (!batchImportFile.value) {
|
||
batchImportError.value = '请先选择 .xlsx 订单导入文件'
|
||
ElMessage.warning(batchImportError.value)
|
||
return
|
||
}
|
||
try {
|
||
batchImportSubmitting.value = true
|
||
batchImportError.value = ''
|
||
batchImportResult.value = await importMerchantFirstLegOrders(
|
||
batchImportFile.value,
|
||
batchImportMode.value,
|
||
batchImportIdempotencyKey.value,
|
||
)
|
||
const result = batchImportResult.value.result
|
||
if (result.succeeded && auth.hasPermission('order:first-leg:list')) await loadOrderPage()
|
||
if (result.failed) ElMessage.warning(`导入完成:成功 ${result.succeeded} 条,失败 ${result.failed} 条`)
|
||
else ElMessage.success(`已成功导入 ${result.succeeded} 条订单草稿`)
|
||
} catch (error) {
|
||
batchImportError.value = batchImportErrorMessage(error)
|
||
ElMessage.error(batchImportError.value)
|
||
} finally {
|
||
batchImportSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function viewImportedDrafts() {
|
||
batchImportOpen.value = false
|
||
void router.push({ name: 'first-leg-orders' })
|
||
}
|
||
|
||
async function validateEditorForm() {
|
||
form.customsInspection = form.items.some((item) => item.woodenProduct) ? true : null
|
||
try {
|
||
const policy = await getMerchantInboundCodePolicy()
|
||
inboundCodePolicy.value = policy
|
||
if (policy.mode === 'AUTO' && !editingId.value) form.inboundCode = ''
|
||
await nextTick()
|
||
} catch (error) {
|
||
inboundCodePolicy.value = null
|
||
ElMessage.warning(`入仓码配置加载失败:${errorMessage(error)}`)
|
||
return false
|
||
}
|
||
|
||
let fieldsValid = false
|
||
try {
|
||
fieldsValid = await orderFormRef.value?.validate() ?? false
|
||
} catch {
|
||
fieldsValid = false
|
||
}
|
||
if (!fieldsValid) {
|
||
ElMessage.warning('请检查并完善表单中标红的必填信息')
|
||
return false
|
||
}
|
||
|
||
if (manualInboundCodeRequired.value && !form.inboundCode?.trim()) {
|
||
ElMessage.warning('当前为旧系统并跑模式,请填写旧系统入仓码')
|
||
return false
|
||
}
|
||
|
||
const errors = validateFirstLegOrderDraft(form)
|
||
if (errors.length > 0) {
|
||
ElMessage.warning(errors[0])
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function saveDraft() {
|
||
if (saving.value) return
|
||
if (!await validateEditorForm()) return
|
||
saving.value = true
|
||
try {
|
||
if (!await confirmSubmissionEdit()) return
|
||
const order = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value, editingSession.value)
|
||
editingId.value = order.id
|
||
editingSession.value = store.beginDraftEdit(order)
|
||
markEditorClean()
|
||
editorOpen.value = false
|
||
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
|
||
ElMessage.success('订单草稿已保存')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function confirmSubmissionEdit() {
|
||
if (!editingId.value || !store.hasPendingSubmission(editingId.value)) return true
|
||
try {
|
||
await ElMessageBox.confirm('上次提交结果尚未确认。确认重新读取订单并按当前表单重新编辑吗?只有服务端仍为草稿时才能开始新的提交决定。', '重新编辑订单', {
|
||
type: 'warning', center: true, confirmButtonText: '确认重新编辑', cancelButtonText: '返回',
|
||
})
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return false
|
||
throw error
|
||
}
|
||
editingSession.value = await store.beginNewSubmissionEdit(editingId.value)
|
||
return true
|
||
}
|
||
|
||
async function submitOrder() {
|
||
if (saving.value) return
|
||
if (companySubmissionBlocked.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 retry = store.isSubmissionRetry(editingId.value, cloneDraft(form))
|
||
if (!retry && !await validateEditorForm()) return
|
||
|
||
try {
|
||
saving.value = true
|
||
if (!retry && !await confirmSubmissionEdit()) return
|
||
const knownCompanyAuditStatus = companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined
|
||
const savedOrder = await store.saveDraftForSubmission(cloneDraft(form), tenantStore.company.companyName, editingId.value, editingSession.value)
|
||
editingId.value = savedOrder.id
|
||
editingSession.value = store.beginDraftEdit(savedOrder)
|
||
await store.submitSaved(savedOrder.id, knownCompanyAuditStatus)
|
||
markEditorClean()
|
||
editorOpen.value = false
|
||
if (isCreatePage.value) {
|
||
workspace.close(['first-leg-order-create', 'first-leg-order-edit'])
|
||
await router.push({ name: 'first-leg-orders' })
|
||
ElMessage.success('订单已提交,等待平台审核')
|
||
return
|
||
}
|
||
await loadPageData()
|
||
ElMessage.success('订单已提交,等待平台审核')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '订单提交失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function searchOrders(model: QueryModel) {
|
||
appliedFilters.value = { ...model,
|
||
createdRange: Array.isArray(model.createdRange) ? model.createdRange.filter((value): value is string => typeof value === 'string') : [],
|
||
}
|
||
tablePage.value = 1
|
||
await loadOrderPage()
|
||
}
|
||
|
||
</script>
|
||
|
||
<template>
|
||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage, 'is-detail-route': isDetailPage, 'client-ui': !isCreatePage }">
|
||
<template v-if="!isCreatePage && !isDetailPage">
|
||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||
|
||
<div class="client-card order-table-card">
|
||
<DataTable :page="tablePage" :page-size="tablePageSize" @update:page="changeOrderPage" @update:page-size="changeOrderPageSize" :data="pagedOrders" :columns="visibleOrderColumns" :key="orderTableVersion" :loading="store.loading" :total="store.total" :page-sizes="[10, 20, 50]" row-key="id" empty-text="暂无订单" class="prototype-order-table" :row-actions="getOrderRowActions" @selection-change="handleOrderSelectionChange">
|
||
<template #toolbar>
|
||
<nav class="order-queue-tabs" aria-label="订单事项分类">
|
||
<button v-for="queue in ORDER_QUEUES" :key="queue.key" type="button"
|
||
:class="{ active: activeOrderQueue === queue.key }" :aria-pressed="activeOrderQueue === queue.key"
|
||
@click="selectOrderQueue(queue.key)">
|
||
{{ queue.label }}<span v-if="queue.key !== 'all' && orderQueueCounts[queue.key]"
|
||
:class="queue.todo ? 'queue-count' : 'queue-stage-count'">{{ queue.todo ? orderQueueCounts[queue.key] : `(${orderQueueCounts[queue.key]})` }}</span>
|
||
</button>
|
||
</nav>
|
||
<span v-if="selectedOrderCount">已选 {{ selectedOrderCount }} 条</span>
|
||
</template>
|
||
<template #toolbar-actions>
|
||
<el-popover placement="bottom-end" :width="320" trigger="click" popper-class="order-column-popper">
|
||
<template #reference>
|
||
<el-button class="column-settings-trigger" aria-label="设置订单列表显示列" title="设置显示列"><SlidersHorizontal :size="18" />显示字段筛选</el-button>
|
||
</template>
|
||
<div class="column-settings-panel">
|
||
<p>拖动调整顺序,也可使用上下移动按钮。</p>
|
||
<el-checkbox-group v-model="visibleColumnKeys" aria-label="订单列表显示列">
|
||
<div
|
||
v-for="(column, index) in orderedSelectableColumns"
|
||
:key="String(column.key)"
|
||
class="column-settings-item"
|
||
draggable="true"
|
||
@dragstart="draggedColumnKey = String(column.key)"
|
||
@dragend="draggedColumnKey = ''"
|
||
@dragover.prevent
|
||
@drop.prevent="dropOrderColumn(String(column.key))"
|
||
>
|
||
<GripVertical :size="17" aria-hidden="true" />
|
||
<el-checkbox :value="String(column.key)">{{ column.label }}</el-checkbox>
|
||
<el-button link :disabled="index === 0" :aria-label="`上移${column.label}`" :title="`上移${column.label}`" @click="moveOrderColumn(String(column.key), -1)"><ChevronUp :size="17" /></el-button>
|
||
<el-button link :disabled="index === orderedSelectableColumns.length - 1" :aria-label="`下移${column.label}`" :title="`下移${column.label}`" @click="moveOrderColumn(String(column.key), 1)"><ChevronDown :size="17" /></el-button>
|
||
</div>
|
||
</el-checkbox-group>
|
||
<el-button link class="column-settings-reset" @click="resetOrderColumns"><RotateCcw :size="17" />恢复默认</el-button>
|
||
</div>
|
||
</el-popover>
|
||
<el-button :loading="downloadingMarks" :disabled="selectedOrderCount === 0" @click="downloadShippingMarks"><Download :size="16" />下载唛头</el-button>
|
||
<el-button :loading="exportingOrders" :disabled="selectedOrderCount === 0" @click="exportOrderInformation"><Download :size="16" />导出订单信息</el-button>
|
||
<el-button v-if="canSubmitOrder" :loading="batchSubmitting" :disabled="(selectedDraftOrders.length === 0 && pendingBatchSubmissionIds.length === 0) || companySubmissionBlocked" @click="submitSelectedDraftOrders"><Send :size="17" />{{ pendingBatchSubmissionIds.length ? '重试批量提交' : '批量提交草稿' }}</el-button>
|
||
<el-button v-if="auth.hasPermission('order:first-leg:create')" @click="openBatchImport"><Upload :size="17" />批量导入订单</el-button>
|
||
<el-button v-if="props.showCreateAction && auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" />创建订单</el-button>
|
||
|
||
</template>
|
||
<template #before-table>
|
||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||
<el-button v-if="canViewCompanyProfile" link type="primary" @click="router.push('/company/profile')">查看企业资料</el-button>
|
||
</div>
|
||
<div v-if="supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>部分订单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
|
||
</div>
|
||
|
||
</template>
|
||
<template v-if="store.loadError" #error>
|
||
<div 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>
|
||
<template #inboundCode="{ row }">
|
||
<button
|
||
v-if="(row as FirstLegOrder).businessSummary.inboundCode"
|
||
class="inbound-code-primary table-ellipsis table-code"
|
||
type="button"
|
||
:title="`查看入仓码 ${(row as FirstLegOrder).businessSummary.inboundCode} 的订单详情`"
|
||
@click="openDetail(row as FirstLegOrder)"
|
||
>{{ (row as FirstLegOrder).businessSummary.inboundCode }}</button>
|
||
<span v-else class="pending-inbound-code">{{ (row as FirstLegOrder).status === 'DRAFT' ? '提交后生成' : '待生成' }}</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>
|
||
<template #requestedPickupAt="{ row }">{{ (row as FirstLegOrder).pickupMethod === 'PICKUP' && (row as FirstLegOrder).requestedPickupAt ? formatDateTime((row as FirstLegOrder).requestedPickupAt) : '-' }}</template>
|
||
<template #receivingWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).receivingWarehouseName)">{{ displayListValue((row as FirstLegOrder).receivingWarehouseName) }}</span></template>
|
||
<template #destinationWarehouse="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).destinationWarehouseName)">{{ displayListValue((row as FirstLegOrder).destinationWarehouseName) }}</span></template>
|
||
<template #destinationType="{ row }">{{ destinationTypeLabels[(row as FirstLegOrder).destinationType] }}</template>
|
||
<template #progress="{ row }">{{ orderProgressLabel(store.orderProgress[row.id]) }}</template>
|
||
<template #status="{ row }"><span class="client-status" :data-tone="getFirstLegStatusTone((row as FirstLegOrder).status)">{{ getFirstLegStatusLabel((row as FirstLegOrder).status) }}</span></template>
|
||
<template #packageCount="{ row }">{{ formatListMetric(orderPackageCount(row as FirstLegOrder)) }}</template>
|
||
<template #expressCompany="{ row }"><span class="table-ellipsis" :title="displayListValue((row as FirstLegOrder).expressCompanyName)">{{ displayListValue((row as FirstLegOrder).expressCompanyName) }}</span></template>
|
||
<template #trackingNumber="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).trackingNumber)">{{ displayListValue((row as FirstLegOrder).trackingNumber) }}</span></template>
|
||
<template #skus="{ row }"><span class="table-ellipsis table-code" :title="uniqueItemValues(row as FirstLegOrder, 'sku')">{{ uniqueItemValues(row as FirstLegOrder, 'sku') }}</span></template>
|
||
<template #grossWeight="{ row }">{{ formatListMetric(orderGrossWeight(row as FirstLegOrder), 'kg') }}</template>
|
||
<template #netWeight="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'netWeightKg'), 'kg') }}</template>
|
||
<template #volume="{ row }">{{ formatListMetric(orderVolume(row as FirstLegOrder), 'm³') }}</template>
|
||
<template #pieces="{ row }">{{ formatListMetric(orderItemTotal(row as FirstLegOrder, 'pieces')) }}</template>
|
||
<template #billingUnitPrice>-</template>
|
||
<template #chargeableVolume="{ row }">{{ formatListMetric((row as FirstLegOrder).chargeableVolumeCbm, 'm³') }}</template>
|
||
<template #chargeableWeight>-</template>
|
||
<template #remark="{ row }"><span class="table-ellipsis" :title="(row as FirstLegOrder).customerRemark">{{ displayListValue((row as FirstLegOrder).customerRemark) }}</span></template>
|
||
<template #shipmentId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId'))">{{ displayListValue((row as FirstLegOrder).shipmentId || uniqueItemValues(row as FirstLegOrder, 'shipmentId')) }}</span></template>
|
||
<template #referenceId="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId'))">{{ displayListValue((row as FirstLegOrder).referenceNo || uniqueItemValues(row as FirstLegOrder, 'referenceId')) }}</span></template>
|
||
<template #receivingTime="{ row }">{{ (row as FirstLegOrder).inboundedAt ? formatDateTime((row as FirstLegOrder).inboundedAt) : '-' }}</template>
|
||
<template #expectedShipDate="{ row }">{{ (row as FirstLegOrder).expectedDepartureDate ? formatDateTime((row as FirstLegOrder).expectedDepartureDate) : '-' }}</template>
|
||
<template #cabinetNo="{ row }"><span class="table-ellipsis table-code" :title="displayListValue((row as FirstLegOrder).businessSummary.cabinetNo || (row as FirstLegOrder).businessSummary.containerNo)">{{ displayListValue((row as FirstLegOrder).businessSummary.cabinetNo || (row as FirstLegOrder).businessSummary.containerNo) }}</span></template>
|
||
<template #vesselVoyage="{ row }"><span class="table-ellipsis" :title="displayListValue([(row as FirstLegOrder).businessSummary.vesselName, (row as FirstLegOrder).businessSummary.voyageNo].filter(Boolean).join(' · '))">{{ displayListValue([(row as FirstLegOrder).businessSummary.vesselName, (row as FirstLegOrder).businessSummary.voyageNo].filter(Boolean).join(' · ')) }}</span></template>
|
||
<template #estimatedSailingAt="{ row }">{{ (row as FirstLegOrder).businessSummary.estimatedSailingAt ? formatDateTime((row as FirstLegOrder).businessSummary.estimatedSailingAt) : '-' }}</template>
|
||
<template #createdAt="{ row }">{{ formatDateTime((row as FirstLegOrder).createdAt) }}</template>
|
||
<template #updatedAt="{ row }">{{ formatDateTime((row as FirstLegOrder).updatedAt) }}</template>
|
||
<template #empty>
|
||
<el-empty :description="emptyDescription">
|
||
<el-button v-if="props.showCreateAction && auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ pageDataLoading ? '加载收货仓中' : (availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓') }}</el-button>
|
||
</el-empty>
|
||
</template>
|
||
</DataTable>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-if="isCreatePage && supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>下单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||
<el-button :loading="pageDataLoading" @click="loadPageData">重新加载</el-button>
|
||
</div>
|
||
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
|
||
|
||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : (routeCopySourceId ? '复制订单' : '创建订单')" :before-close="requestCloseEditor" @back="closeEditor">
|
||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" :class="{ 'is-dialog-order-form': !isCreatePage }" scroll-to-error>
|
||
<section class="order-form-section prototype-section order-basic-section">
|
||
<div class="order-section-heading"><span class="order-step-badge">01</span><h3>客户下单</h3><span class="order-section-context">国内交货与运输信息</span></div>
|
||
<div class="prototype-form-flow shipping-form-flow">
|
||
<el-form-item label="履约类型" prop="fulfillmentType" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.fulfillmentType" class="segmented-options" @change="changeFulfillmentType"><el-radio-button value="STANDARD">普通头程</el-radio-button><el-radio-button value="ONE_CLICK_DROPSHIP">一件代发</el-radio-button></el-radio-group><small v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" class="field-guidance">测量完成后不办理国内库存入库,将直接推送 PDA 合托。</small></el-form-item>
|
||
<el-form-item v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.lxCode" maxlength="128" placeholder="请输入LX入库单号" /><small class="field-guidance">用于绑定领星入库单,提交后可在尾程订单、入库单和出库单中查询。</small></el-form-item>
|
||
<el-form-item label="取货方式" prop="pickupMethod" required class="prototype-form-row prototype-form-row--tabs"><el-radio-group v-model="form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_EXPRESS">快递自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK">卡车自送</el-radio-button></el-radio-group></el-form-item>
|
||
<template v-if="form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot">
|
||
<el-form-item label="企业提货地址簿" prop="senderAddressId" required :rules="requiredSelectRules('请选择企业提货地址')" class="prototype-form-row prototype-form-row--wide address-book-row">
|
||
<div class="address-book-control"><el-select v-model="form.senderAddressId" clearable filterable :loading="orderAddressesLoading" placeholder="选择企业保存的提货地址" @change="selectPickupAddress"><el-option v-for="address in pickupAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" /></el-select><el-button @click="savePickupAddress">保存当前地址</el-button><el-button v-if="form.senderAddressId" link type="danger" @click="removeOrderAddress('PICKUP')">停用</el-button></div>
|
||
</el-form-item>
|
||
<el-form-item label="提货联系人" prop="shipperPartySnapshot.contactName" required :rules="requiredTextRules('请输入提货联系人')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.contactName" maxlength="50" placeholder="请输入联系人姓名" /></el-form-item>
|
||
<el-form-item label="联系电话" prop="shipperPartySnapshot.phone" required :rules="requiredTextRules('请输入提货联系电话')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.phone" maxlength="30" placeholder="请输入联系电话" /></el-form-item>
|
||
<el-form-item label="州 / 省" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.province" maxlength="100" placeholder="请输入州或省" /></el-form-item>
|
||
<el-form-item label="城市" prop="shipperPartySnapshot.city" required :rules="requiredTextRules('请输入提货城市')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipperPartySnapshot.city" maxlength="100" placeholder="请输入城市" /></el-form-item>
|
||
<el-form-item label="提货地址" prop="shipperPartySnapshot.address" required :rules="requiredTextRules('请输入提货地址')" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.shipperPartySnapshot.address" maxlength="300" placeholder="请输入详细提货地址" /></el-form-item>
|
||
</template>
|
||
<el-form-item label="国内收货仓" prop="receivingWarehouseId" required class="prototype-form-row prototype-form-row--wide"><el-radio-group v-model="form.receivingWarehouseId" class="warehouse-options" @change="loadSelectedWarehouse"><el-radio v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id">{{ warehouse.name }}</el-radio></el-radio-group></el-form-item>
|
||
</div>
|
||
<div class="order-form-grid compact-transport-grid">
|
||
<el-form-item v-if="manualInboundCodeRequired" label="旧系统入仓码" prop="inboundCode" required :rules="requiredTextRules('请输入旧系统生成的入仓码')">
|
||
<el-input v-model="form.inboundCode" maxlength="40" placeholder="例如:NYEXX2608140001" @input="form.inboundCode = String($event).trim().toUpperCase()" />
|
||
<small class="field-guidance">当前全系统处于旧系统并跑模式,请粘贴旧系统生成的入仓码;订单提交后不可修改。</small>
|
||
</el-form-item>
|
||
<el-form-item label="渠道" prop="channelCode" required>
|
||
<el-select v-model="form.channelCode" :loading="channelLoading" :disabled="!form.receivingWarehouseId || channelLoading" :placeholder="form.receivingWarehouseId ? '请选择运输渠道' : '请先选择国内收货仓'" @change="selectChannel"><el-option v-for="channel in channelOptions" :key="channel.channelCode" :label="channel.channelName" :value="channel.channelCode" /></el-select>
|
||
<small v-if="channelError" class="field-guidance field-guidance--error">渠道加载失败:{{ channelError }} <el-button link type="primary" @click="retryPricingChannels">重试</el-button></small>
|
||
<small v-else-if="form.receivingWarehouseId && !channelLoading && channelOptions.length === 0" class="field-guidance">当前收货仓暂无可用渠道,请联系平台管理员。</small>
|
||
</el-form-item>
|
||
<el-form-item label="报价包税方式" prop="pricingTaxMode" required class="pricing-tax-mode-field">
|
||
<el-radio-group v-model="form.pricingTaxMode"><el-radio v-for="mode in availablePricingTaxModes" :key="mode" :value="mode">{{ pricingTaxModeLabel(mode) }}</el-radio></el-radio-group>
|
||
<small v-if="form.channelCode && availablePricingTaxModes.length === 0" class="field-guidance field-guidance--error">当前渠道没有可用报价方式,请重新选择渠道。</small>
|
||
</el-form-item>
|
||
<el-form-item label="买单 / 退税" prop="taxMode" required><el-radio-group v-model="form.taxMode"><el-radio value="BUY_DECLARATION">买单</el-radio><el-radio value="TAX_REFUND">退税</el-radio></el-radio-group></el-form-item>
|
||
<el-form-item label="是否整柜" prop="transportMode" required><el-radio-group v-model="form.transportMode"><el-radio value="FCL">整柜</el-radio><el-radio value="LCL">散货</el-radio></el-radio-group></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.customerRemark" type="textarea" :rows="3" resize="vertical" maxlength="300" show-word-limit placeholder="选填,例如:入仓前请电话联系" /></el-form-item>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="order-form-section prototype-section destination-section">
|
||
<div class="order-section-heading destination-section-heading"><span class="order-step-badge">02</span><h3>目的地信息</h3><span class="order-section-context">美国目的仓与收件信息</span></div>
|
||
<div class="prototype-form-flow destination-form-flow">
|
||
<el-form-item label="目的仓类型" prop="destinationType" required class="prototype-form-row prototype-form-row--tabs destination-type-row"><el-radio-group v-model="form.destinationType" class="segmented-options destination-type-options" @change="changeDestinationType"><el-radio-button v-if="form.fulfillmentType !== 'ONE_CLICK_DROPSHIP'" value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button v-if="form.fulfillmentType !== 'ONE_CLICK_DROPSHIP'" value="PRIVATE">私人仓</el-radio-button></el-radio-group></el-form-item>
|
||
|
||
<el-form-item v-if="form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row">
|
||
<div class="address-book-control"><el-select v-model="form.privateDestinationAddressId" clearable filterable :loading="orderAddressesLoading" placeholder="选择企业保存的私人仓地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="String(address.id)" :label="addressLabel(address)" :value="String(address.id)" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="form.privateDestinationAddressId" link type="danger" @click="removeOrderAddress('PRIVATE_DESTINATION')">停用</el-button></div>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="form.destinationType !== 'PRIVATE'" :label="form.destinationType === 'FBA' ? 'FBA 仓' : '海外仓'" prop="destinationWarehouseId" required :rules="requiredSelectRules('请选择平台维护的目的仓')" class="prototype-form-row"><el-select v-model="form.destinationWarehouseId" filterable :loading="destinationWarehousesLoading" placeholder="请选择当前可送达的目的仓" @change="selectDestinationWarehouse"><el-option v-for="warehouse in availableDestinationWarehouses" :key="warehouse.id" :label="form.destinationType === 'FBA' ? warehouse.code : `${warehouse.name} · ${warehouse.code}`" :value="warehouse.id" /></el-select><small v-if="!destinationWarehousesLoading && availableDestinationWarehouses.length === 0" class="field-guidance">平台暂未维护可用的{{ form.destinationType === 'FBA' ? ' FBA 仓' : '海外仓' }},请联系平台管理员。</small></el-form-item>
|
||
<div v-if="form.destinationType !== 'PRIVATE' && selectedDestinationWarehouse" class="selected-destination"><MapPin :size="17" /><div><span>{{ form.destinationType === 'FBA' ? selectedDestinationWarehouse.code : `${selectedDestinationWarehouse.name} · ${selectedDestinationWarehouse.code}` }} · {{ selectedDestinationWarehouse.countryCode }}</span><small v-if="selectedDestinationWarehouse.address">仓库地址:{{ selectedDestinationWarehouse.province }}{{ selectedDestinationWarehouse.city }} {{ selectedDestinationWarehouse.address }}</small><small v-if="selectedDestinationWarehouse.contactName || selectedDestinationWarehouse.contactPhone">联系人:{{ selectedDestinationWarehouse.contactName || '-' }} {{ selectedDestinationWarehouse.contactPhone }}</small></div></div>
|
||
<template v-if="form.destinationType === 'PRIVATE' && form.consigneePartySnapshot">
|
||
<el-form-item label="私人地址名称" prop="destinationWarehouseName" required class="prototype-form-row"><el-input v-model="form.destinationWarehouseName" maxlength="128" placeholder="例如:洛杉矶客户自有仓" @input="syncPrivateDestinationName" /></el-form-item>
|
||
<el-form-item label="收件人姓名" prop="consigneePartySnapshot.contactName" required :rules="requiredTextRules('请输入收货联系人姓名')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.contactName" maxlength="50" placeholder="请输入收件人姓名" /></el-form-item>
|
||
<el-form-item label="收件人电话" prop="consigneePartySnapshot.phone" required :rules="requiredTextRules('请输入收货联系电话')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.phone" maxlength="30" placeholder="请输入收件人电话" /></el-form-item>
|
||
<el-form-item label="收件国家代码" prop="destinationCountryCode" required class="prototype-form-row prototype-form-row--short"><el-input v-model="form.destinationCountryCode" maxlength="2" placeholder="例如:US" /></el-form-item>
|
||
<el-form-item label="州 / 省" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.province" maxlength="100" placeholder="请输入州或省" /></el-form-item>
|
||
<el-form-item label="城市" prop="consigneePartySnapshot.city" required :rules="requiredTextRules('请输入收件城市')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.consigneePartySnapshot.city" maxlength="100" placeholder="请输入城市" /></el-form-item>
|
||
<el-form-item label="收件地址" prop="consigneePartySnapshot.address" required :rules="requiredTextRules('请输入本单实际收货地址')" class="prototype-form-row prototype-form-row--wide"><el-input v-model="form.consigneePartySnapshot.address" maxlength="300" placeholder="请输入收件详细地址" /></el-form-item>
|
||
<el-form-item label="邮编" class="prototype-form-row prototype-form-row--short"><el-input v-model="form.consigneePartySnapshot.postalCode" maxlength="20" placeholder="请输入邮编" /></el-form-item>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="order-form-section prototype-section cargo-section">
|
||
<div class="order-section-heading cargo-table-heading"><div><h3>货物明细</h3></div></div>
|
||
<div class="cargo-table-toolbar">
|
||
<div v-if="isCreatePage && !routeDraftId && canCreateOrder" class="cargo-import-actions">
|
||
<el-button class="cargo-download-button" :loading="batchTemplateDownloading" @click="downloadBatchTemplate"><Download :size="16" />下载 Excel 模板</el-button>
|
||
<el-button class="cargo-upload-button" type="primary" plain @click="openBatchImport"><Upload :size="16" />Excel 批量导入</el-button>
|
||
<span class="cargo-table-hint">Excel 导入用于一次创建多条订单草稿。</span>
|
||
</div>
|
||
<span v-else class="cargo-table-hint">可逐行维护货物明细。</span>
|
||
<div class="cargo-table-actions"><el-button @click="addCargoItem"><Plus :size="16" />添加行</el-button><el-button type="danger" plain :disabled="!selectedCargoIds.length" @click="removeSelectedCargoItems"><Trash2 :size="15" />删除所选行</el-button></div>
|
||
</div>
|
||
<div class="cargo-table-wrap" :class="{ 'is-dropship': form.fulfillmentType === 'ONE_CLICK_DROPSHIP' }">
|
||
<table class="cargo-edit-table">
|
||
<thead><tr>
|
||
<th class="cargo-check-cell"><el-checkbox :model-value="allCargoSelected" :indeterminate="someCargoSelected" aria-label="全选货物" @change="toggleAllCargoItems" /></th><th class="cargo-index-cell">#</th>
|
||
<th>SKU NO.<small>{{ form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? '必填' : '选填' }}</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class="cargo-switch-column">木制品<small>勾选后自动商检</small></th>
|
||
<th class="cargo-number-column">箱规长 cm</th><th class="cargo-number-column">箱规宽 cm</th><th class="cargo-number-column">箱规高 cm</th><th class="cargo-number-column">每箱数量</th><th class="cargo-number-column">箱数</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总毛重</th><th class="cargo-number-column"><span class="required-field-mark" aria-hidden="true">*</span>总体积</th><th class="cargo-number-column">总数量</th><th class="cargo-number-column">总净重</th>
|
||
<th>清关申报单价 USD</th><th>清关申报金额 USD</th><th>用途</th><th>型号</th><th>品牌</th><th class="cargo-dimensions-column">产品规格 长×宽×高</th><th>货件编号<small>货件编号必填</small></th><th>跟踪编号<small>跟踪编号必填</small></th><th>UPC NO.</th><th>备注</th><th class="cargo-operation-column">操作</th>
|
||
</tr></thead>
|
||
<tbody><tr v-for="(item, index) in form.items" :key="item.id" :class="{ 'mixed-sku-detail-row': isAdditionalMixedSku(item, index) }">
|
||
<td class="cargo-check-cell"><el-checkbox :model-value="selectedCargoIds.includes(item.id)" :aria-label="`选择货物 ${index + 1}`" @change="toggleCargoItem(item.id, $event)" /></td><td class="cargo-index-cell">{{ index + 1 }}</td>
|
||
<td><el-form-item :prop="`items.${index}.sku`" :required="form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" :rules="form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? requiredTextRules('请输入 SKU NO.') : undefined"><div class="cargo-sku-input"><el-input v-model="item.sku" maxlength="64" placeholder="SKU编号" /><small v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP' && mixedSkuCount(item) > 1" class="mixed-sku-badge">同箱</small></div></el-form-item></td>
|
||
<td><el-form-item :prop="`items.${index}.cargoNameCn`" required :rules="requiredTextRules('请输入中文品名')"><el-input v-model="item.cargoNameCn" maxlength="128" placeholder="中文品名" /></el-form-item></td>
|
||
<td class="cargo-image-column"><div class="cargo-table-image"><img v-if="item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-upload v-else :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button text :loading="item.productImageUploading" aria-label="上传图片"><ImagePlus :size="17" /></el-button></el-upload><el-button v-if="item.productImagePreviewUrl" class="cargo-image-remove" text type="danger" aria-label="移除图片" @click="removeCargoProductImage(item)"><Trash2 :size="12" /></el-button></div></td>
|
||
<td><el-input v-model="item.cargoNameEn" maxlength="128" placeholder="English Name" /></td><td><el-input v-model="item.hsCode" maxlength="20" placeholder="HS Code" /></td><td><el-input v-model="item.customsHsCode" maxlength="20" placeholder="HS Code" /></td>
|
||
<td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-form-item v-else :prop="`items.${index}.packageType`" required :rules="requiredSelectRules('请选择包装类型')"><el-select v-model="item.packageType" @change="syncPackingGroupFields(item)"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" /></td>
|
||
<td class="cargo-switch-column"><el-switch v-model="item.dangerousGoods" :aria-label="`第 ${index + 1} 条货物是否危险品`" /></td><td class="cargo-switch-column"><div class="cargo-switch-cell"><el-switch v-model="item.woodenProduct" :aria-label="`第 ${index + 1} 条货物是否木制品`" /><small v-if="item.woodenProduct">自动商检</small></div></td>
|
||
<td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span><el-input-number v-else v-model="item.lengthCm" :min="0" :precision="2" :controls="false" placeholder="长" @change="syncPackingGroupFields(item)" /></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span><el-input-number v-else v-model="item.widthCm" :min="0" :precision="2" :controls="false" placeholder="宽" @change="syncPackingGroupFields(item)" /></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span><el-input-number v-else v-model="item.heightCm" :min="0" :precision="2" :controls="false" placeholder="高" @change="syncPackingGroupFields(item)" /></td>
|
||
<td><el-form-item :prop="`items.${index}.unitsPerPackage`" required :rules="positiveNumberRules('每箱数量必须大于 0', true)"><el-input-number v-model="item.unitsPerPackage" :min="1" :controls="false" placeholder="每箱数量" @change="syncSkuQuantity(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-form-item v-else :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" placeholder="箱数" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-form-item v-else :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" placeholder="总毛重" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-form-item v-else :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" placeholder="总体积" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="derived-sku-value">{{ item.pieces }}</span><el-form-item v-else :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" placeholder="总数量" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-input-number v-else v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" placeholder="净重" @change="syncPackingGroupFields(item)" /></td>
|
||
<td><el-input-number v-model="item.unitDeclaredValue" :min="0" :precision="2" :controls="false" placeholder="申报单价" /></td><td><el-input-number v-model="item.declaredValue" :min="0" :precision="2" :controls="false" placeholder="申报总价" /></td><td><el-input v-model="item.usage" maxlength="128" placeholder="用途" /></td><td><el-input v-model="item.model" maxlength="64" placeholder="型号" /></td><td><el-input v-model="item.brand" maxlength="64" placeholder="品牌" /></td>
|
||
<td class="cargo-dimensions-column"><div class="cargo-table-dimensions"><el-input-number v-model="item.productLengthCm" :min="0" :precision="2" :controls="false" placeholder="长" /><span>×</span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" placeholder="宽" /><span>×</span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" placeholder="高" /></div></td>
|
||
<td><el-form-item :prop="`items.${index}.shipmentId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入货件编号') : undefined"><el-input v-model="item.shipmentId" maxlength="64" placeholder="货件编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入跟踪编号') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="跟踪编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO.(选填)" /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" link type="primary" :aria-label="`为货物 ${index + 1} 添加同箱 SKU`" @click="addSkuToPackingGroup(index)"><Plus :size="15" />添加 SKU</el-button><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" />删除</el-button></td>
|
||
</tr></tbody>
|
||
</table>
|
||
</div>
|
||
<div class="cargo-table-hint"><span v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">一件代发必须填写 SKU;每个箱组默认装一个 SKU,同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。</span><span v-else-if="form.destinationType === 'FBA'">SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填</span><span v-else>SKU NO. 和 UPC NO. 均为选填项</span></div>
|
||
</section>
|
||
|
||
<section class="order-form-section prototype-section cost-estimate-section" aria-label="费用预估">
|
||
<div class="order-section-heading"><div><h3>参考运费</h3><p>根据当前价格表、渠道、目的仓以及货物重量和体积实时试算。</p></div></div>
|
||
<div class="price-estimate" :class="{ 'is-loading': priceEstimateLoading }" aria-live="polite">
|
||
<div v-if="priceEstimateLoading" class="price-estimate__state" v-loading="true">正在获取实时参考报价</div>
|
||
<div v-else-if="priceEstimateError" class="price-estimate__state is-error" role="alert">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>参考报价暂时不可用</strong><span>{{ priceEstimateError }},您仍可正常保存订单草稿。</span></div>
|
||
<el-button @click="loadPriceEstimate">重新试算</el-button>
|
||
</div>
|
||
<div v-else-if="priceEstimate" class="price-estimate__result">
|
||
<div class="price-estimate__channel">
|
||
<span>当前渠道</span>
|
||
<strong>{{ selectedPricingChannel?.channelName || priceEstimate.channelCode }}</strong>
|
||
<small>{{ pricingTaxModeLabel(priceEstimate.pricingTaxMode) }}<template v-if="selectedPricingChannel?.referenceTransit"> · 参考时效 {{ selectedPricingChannel.referenceTransit }}</template></small>
|
||
</div>
|
||
<div v-if="priceEstimateBreakdown" class="price-estimate__comparison">
|
||
<span>计费对比</span>
|
||
<strong>{{ priceEstimateBreakdown.actualLabel }} {{ formatEstimateQuantity(priceEstimateBreakdown.actualQuantity, priceEstimate.chargeUnit) }}</strong>
|
||
<strong>{{ priceEstimateBreakdown.convertedLabel }} {{ formatEstimateQuantity(priceEstimateBreakdown.convertedQuantity, priceEstimate.chargeUnit) }}</strong>
|
||
<small>{{ priceEstimateBreakdown.convertedFormula }}</small>
|
||
</div>
|
||
<div class="price-estimate__chargeable">
|
||
<span>{{ priceEstimateBreakdown?.chargeableLabel || '计费量' }}</span>
|
||
<strong>{{ formatEstimateQuantity(priceEstimate.chargeableQuantity, priceEstimate.chargeUnit) }}</strong>
|
||
<small v-if="priceEstimateBreakdown">{{ priceEstimateBreakdown.basisLabel }}(两者取大)</small>
|
||
</div>
|
||
<div><span>基础单价</span><strong>{{ formatEstimateMoney(priceEstimate.unitPrice, priceEstimate.currency) }} / {{ priceEstimate.chargeUnit }}</strong></div>
|
||
<div class="price-estimate__amount"><span>参考运费</span><strong>{{ formatEstimateMoney(priceEstimate.estimatedAmount, priceEstimate.currency) }}</strong></div>
|
||
<small>{{ priceEstimateDisclaimer }}</small>
|
||
</div>
|
||
<div v-else-if="form.destinationType === 'PRIVATE'" class="price-estimate__state">私人仓暂不提供在线参考报价,您仍可正常保存订单草稿。</div>
|
||
<div v-else class="price-estimate__state">请先选择国内收货仓、渠道、目的仓,并填写大于 0 的总毛重和总体积。</div>
|
||
</div>
|
||
</section>
|
||
</el-form>
|
||
<template #footer><div class="editor-actions"><el-button v-if="isCreatePage" :disabled="saving" @click="resetCreateForm"><RotateCcw :size="16" />重置</el-button><el-button v-else :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')" class="submit-order-button" type="primary" :loading="saving" @click="submitOrder"><Send :size="17" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||
</FirstLegOrderEditorShell>
|
||
|
||
<el-dialog v-model="batchImportOpen" title="Excel 批量创建订单草稿" width="680px" destroy-on-close append-to-body>
|
||
<div class="batch-import-dialog">
|
||
<el-alert title="使用中文模板创建订单草稿" description="仅支持 .xlsx,文件不超过 5MB、最多 50 行;请直接选择模板内的中文下拉项,不需要填写英文代码或内部编号。导入成功的数据只创建草稿,不会直接提交审核;选择木制品时,模板会要求同步选择需要商检。" type="info" :closable="false" show-icon />
|
||
<el-alert v-if="batchImportError" title="批量导入处理失败" :description="batchImportError" type="error" :closable="false" show-icon />
|
||
<div class="batch-import-actions">
|
||
<input ref="batchImportInput" class="cargo-import-input" type="file" accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @change="selectBatchImportFile" />
|
||
<el-button :loading="batchTemplateDownloading" @click="downloadBatchTemplate"><Download :size="16" />下载正式模板</el-button>
|
||
<el-button type="primary" plain @click="batchImportInput?.click()"><Upload :size="16" />选择 Excel</el-button>
|
||
<span>{{ batchImportFile?.name || '尚未选择文件' }}</span>
|
||
</div>
|
||
<el-radio-group v-model="batchImportMode" aria-label="导入模式">
|
||
<el-radio value="PARTIAL">部分成功:有效行创建草稿,失败行返回原因</el-radio>
|
||
<el-radio value="ATOMIC">原子导入:任一行失败则整批不创建</el-radio>
|
||
</el-radio-group>
|
||
<section v-if="batchImportResult" class="batch-import-result" aria-live="polite">
|
||
<strong>批次 {{ batchImportResult.batchNo }}</strong>
|
||
<span>共 {{ batchImportResult.result.total }} 条,已创建草稿 {{ batchImportResult.result.succeeded }} 条,失败 {{ batchImportResult.result.failed }} 条</span>
|
||
<div v-if="batchImportResult.errorReportFileId" class="batch-import-result-actions">
|
||
<el-button type="danger" plain :loading="batchErrorReportDownloading" @click="downloadBatchErrorReport"><Download :size="16" />下载 Excel 错误报告</el-button>
|
||
</div>
|
||
<ul v-if="batchImportResult.result.failed">
|
||
<li v-for="item in batchImportResult.result.items.filter((row) => !row.success)" :key="item.index">
|
||
Excel 第 {{ batchImportExcelRow(item) }} 行:{{ item.errorMessage || item.errorCode || '导入失败' }}
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
</div>
|
||
<template #footer><div class="editor-actions"><el-button v-if="batchImportResult?.result.succeeded && auth.hasPermission('order:first-leg:list')" :disabled="batchImportSubmitting" @click="viewImportedDrafts">查看订单草稿</el-button><el-button :disabled="batchImportSubmitting" @click="batchImportOpen = false">关闭</el-button><el-button type="primary" :loading="batchImportSubmitting" :disabled="!batchImportFile" @click="submitBatchImport">开始导入并创建草稿</el-button></div></template>
|
||
</el-dialog>
|
||
|
||
<section v-if="isDetailPage" class="first-leg-detail-page" aria-label="订单详情">
|
||
<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 class="detail-heading-main">
|
||
<el-button link type="primary" class="detail-back-action" @click="router.push({ name: 'first-leg-orders' })">← 返回列表</el-button>
|
||
<div><h2>{{ detailOrder.businessSummary.inboundCode || (detailOrder.status === 'CANCELLED' ? (detailOrder.orderNo || '—') : detailOrder.status === 'DRAFT' ? '入仓码提交后生成' : '入仓码待生成') }}</h2><p v-if="detailOrder.status === 'CANCELLED'">订单已取消。</p><p v-else>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }} · {{ detailOrder.origin || '起运地待更新' }} → {{ detailOrder.destination || '目的地待更新' }}</p></div>
|
||
</div>
|
||
<div class="detail-heading-actions"><el-tag :type="getFirstLegStatusTone(detailOrder.status)" effect="light">{{ getFirstLegStatusLabel(detailOrder.status) }}</el-tag><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" type="primary" plain :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button><el-button v-if="canOfferCancellation(detailOrder)" type="danger" plain :loading="cancellingOrderId === detailOrder.id" :disabled="!!cancellingOrderId" @click="cancelOrder(detailOrder)">{{ pendingCancellation?.id === detailOrder.id ? '重试取消' : '取消订单' }}</el-button></div>
|
||
</div>
|
||
<section class="detail-section">
|
||
<h3>订单与运输</h3>
|
||
<dl class="order-detail-grid">
|
||
<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>{{ detailOrder.status === 'CANCELLED' ? '—' : detailOrder.status === 'DRAFT' ? '提交后生成' : '待生成' }}</span></dd></div>
|
||
<div><dt>创建时间</dt><dd>{{ formatDateTime(detailOrder.createdAt) }}</dd></div>
|
||
<div><dt>运输方式</dt><dd>{{ detailOrder.status === 'CANCELLED' && !detailOrder.transportMode ? '—' : getFirstLegTransportModeLabel(detailOrder.transportMode) }}</dd></div>
|
||
<div><dt>贸易条款</dt><dd>{{ displayValue(detailOrder.tradeTerm) }}</dd></div>
|
||
<div><dt>发货方式</dt><dd>{{ pickupMethodLabels[detailOrder.pickupMethod] }}</dd></div>
|
||
<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>{{ displayValue(detailOrder.receivingTime) }}</dd></div>
|
||
<div><dt>目的仓类型</dt><dd>{{ destinationTypeLabels[detailOrder.destinationType] }}</dd></div>
|
||
<div><dt>目的国家 / 仓库</dt><dd>{{ detailOrder.destinationCountryCode || '-' }} · {{ detailOrder.destinationWarehouseName || '-' }}</dd></div>
|
||
<div v-if="detailOrder.destinationType === 'FBA'"><dt>Amazon 仓库代码</dt><dd>{{ displayValue(detailOrder.amazonWarehouseCode) }}</dd></div>
|
||
<div><dt>期望出运</dt><dd>{{ displayValue(detailOrder.expectedDepartureDate) }}</dd></div>
|
||
<div><dt>海关查验</dt><dd>{{ detailOrder.customsInspection == null ? '未选择' : (detailOrder.customsInspection ? '需要' : '不需要') }}</dd></div>
|
||
<div><dt>提交时间</dt><dd>{{ detailOrder.submittedAt ? formatDateTime(detailOrder.submittedAt) : '-' }}</dd></div>
|
||
<div><dt>完成时间</dt><dd>{{ detailOrder.completedAt ? formatDateTime(detailOrder.completedAt) : '-' }}</dd></div>
|
||
</dl>
|
||
</section>
|
||
<section class="detail-section">
|
||
<h3>收发货信息</h3>
|
||
<dl class="party-detail-grid">
|
||
<div><dt>发货方</dt><dd><strong>{{ detailOrder.shipperPartySnapshot?.name || detailOrder.shipper || '未提供' }}</strong><span v-if="detailOrder.shipperPartySnapshot">{{ detailOrder.shipperPartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.shipperPartySnapshot.phone || '未填写电话' }} · {{ detailOrder.shipperPartySnapshot.email || '未填写邮箱' }}<br />{{ detailOrder.shipperPartySnapshot.countryRegion }} {{ detailOrder.shipperPartySnapshot.province }} {{ detailOrder.shipperPartySnapshot.city }} {{ detailOrder.shipperPartySnapshot.address }} {{ detailOrder.shipperPartySnapshot.postalCode }}</span></dd></div>
|
||
<div><dt>收货方</dt><dd><strong>{{ detailOrder.consigneePartySnapshot?.name || detailOrder.consignee || detailOrder.destinationWarehouseName || '未提供' }}</strong><span v-if="detailOrder.consigneePartySnapshot">{{ detailOrder.consigneePartySnapshot.contactName || '未填写联系人' }} · {{ detailOrder.consigneePartySnapshot.phone || '未填写电话' }} · {{ detailOrder.consigneePartySnapshot.email || '未填写邮箱' }}<br />{{ detailOrder.consigneePartySnapshot.countryRegion }} {{ detailOrder.consigneePartySnapshot.province }} {{ detailOrder.consigneePartySnapshot.city }} {{ detailOrder.consigneePartySnapshot.address }} {{ detailOrder.consigneePartySnapshot.postalCode }}</span><span v-else>平台目的仓地址以最终履约安排为准</span></dd></div>
|
||
</dl>
|
||
<div v-if="detailOrder.customerRemark" class="customer-remark"><strong>客户备注</strong><p>{{ detailOrder.customerRemark }}</p></div>
|
||
</section>
|
||
<section v-if="detailOrder.status !== 'DRAFT' && detailOrder.status !== 'CANCELLED'" class="detail-section">
|
||
<h3>关键业务摘要</h3>
|
||
<dl class="fulfillment-summary">
|
||
<div v-for="item in progressSummaryItems" :key="item.label"><dt>{{ item.label }}</dt><dd>{{ item.value }}</dd></div>
|
||
</dl>
|
||
</section>
|
||
<OrderProgressPanel :order="detailOrder">
|
||
<template #cabinet>
|
||
<div class="cabinet-progress-detail">
|
||
<strong>柜次明细</strong>
|
||
<el-table v-if="cabinetProgressItems.length" :data="cabinetProgressItems" border size="small" class="cabinet-progress-table" row-key="cabinetId">
|
||
<el-table-column prop="cabinetNo" label="柜号 / 柜次" min-width="150"><template #default="{ row }"><span class="table-code">{{ row.cabinetNo || '—' }}</span></template></el-table-column>
|
||
<el-table-column label="船名 / 航次" min-width="180"><template #default="{ row }">{{ [row.vesselName, row.voyageNo].filter(Boolean).join(' · ') || '待更新' }}</template></el-table-column>
|
||
<el-table-column label="预计船期" width="170"><template #default="{ row }">{{ row.estimatedSailingAt ? formatDateTime(row.estimatedSailingAt) : '待更新' }}</template></el-table-column>
|
||
<el-table-column prop="pickingStatusLabel" label="拣货状态" width="120"><template #default="{ row }"><el-tag size="small" effect="plain">{{ row.pickingStatusLabel }}</el-tag></template></el-table-column>
|
||
</el-table>
|
||
<p v-else class="detail-inline-empty">尚未分配柜次,后续柜次节点将在这里同步。</p>
|
||
<TransportFactsTable :fragments="detailOrder.businessSummary.transportFragments" />
|
||
</div>
|
||
</template>
|
||
</OrderProgressPanel>
|
||
<section v-if="detailOrder.status !== 'CANCELLED' || detailOrder.expressCompanyName || detailOrder.trackingNumber" class="detail-section">
|
||
<h3>履约快递信息</h3>
|
||
<dl v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><div><dt>快递公司</dt><dd>{{ detailOrder.expressCompanyName || (detailOrder.status === 'CANCELLED' ? '—' : '待平台补充') }}</dd></div><div><dt>快递单号</dt><dd class="table-code">{{ detailOrder.trackingNumber || (detailOrder.status === 'CANCELLED' ? '—' : '待平台补充') }}</dd></div></dl>
|
||
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
|
||
</section>
|
||
<section class="detail-section">
|
||
<h3>货物明细</h3>
|
||
<dl v-if="detailSummary" class="cargo-summary"><div><dt>明细</dt><dd>{{ detailOrder.items.length }} 项</dd></div><div><dt>件数</dt><dd>{{ detailSummary.pieces }}</dd></div><div><dt>毛重</dt><dd>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</dd></div><div><dt>体积</dt><dd>{{ detailSummary.volumeCbm }} m³</dd></div></dl>
|
||
<div v-if="detailOrder.items.length" class="cargo-detail-table-wrap">
|
||
<el-table :data="pagedDetailCargoItems" border stripe class="cargo-detail-table" row-key="id">
|
||
<el-table-column type="index" label="#" width="54" align="center" :index="(index: number) => (detailCargoPage - 1) * detailCargoPageSize + index + 1" />
|
||
<el-table-column label="SKU NO." width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.sku || '-' }}</span></template></el-table-column>
|
||
<el-table-column v-if="detailOrder.fulfillmentType === 'ONE_CLICK_DROPSHIP'" prop="packingGroupNo" label="混箱组合" width="100" align="center"><template #default="{ row }">{{ row.packingGroupNo || '-' }}</template></el-table-column>
|
||
<el-table-column label="中文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameCn || '-' }}</template></el-table-column>
|
||
<el-table-column label="图片" width="66" align="center"><template #default="{ row }"><div class="cargo-detail-thumbnail"><img v-if="row.productImagePreviewUrl" :src="row.productImagePreviewUrl" :alt="`${row.cargoNameCn || '货物'}产品图片`" /><span v-else>-</span></div></template></el-table-column>
|
||
<el-table-column label="英文品名" min-width="150" show-overflow-tooltip><template #default="{ row }">{{ row.cargoNameEn || '-' }}</template></el-table-column>
|
||
<el-table-column label="包装类型" width="100" align="center"><template #default="{ row }">{{ packageTypeLabels[row.packageType] || row.packageType || '-' }}</template></el-table-column>
|
||
<el-table-column label="箱规 长×宽×高(cm)" width="170" align="center"><template #default="{ row }">{{ row.lengthCm || '-' }} × {{ row.widthCm || '-' }} × {{ row.heightCm || '-' }}</template></el-table-column>
|
||
<el-table-column prop="unitsPerPackage" label="每箱数量" width="88" align="right" />
|
||
<el-table-column prop="packageCount" label="箱数" width="72" align="right" />
|
||
<el-table-column prop="pieces" label="总数量" width="82" align="right" />
|
||
<el-table-column label="总毛重" width="94" align="right"><template #default="{ row }">{{ row.grossWeightKg }} kg</template></el-table-column>
|
||
<el-table-column label="总净重" width="94" align="right"><template #default="{ row }">{{ row.netWeightKg ? `${row.netWeightKg} kg` : '-' }}</template></el-table-column>
|
||
<el-table-column label="总体积" width="98" align="right"><template #default="{ row }">{{ row.volumeCbm }} m³</template></el-table-column>
|
||
<el-table-column label="报关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.hsCode || '-' }}</span></template></el-table-column>
|
||
<el-table-column label="清关 HS CODE" width="128" show-overflow-tooltip><template #default="{ row }"><span class="table-code">{{ row.customsHsCode || '-' }}</span></template></el-table-column>
|
||
<el-table-column label="申报单价" width="104" align="right"><template #default="{ row }">{{ row.unitDeclaredValue ? `USD ${row.unitDeclaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||
<el-table-column label="申报金额" width="104" align="right"><template #default="{ row }">{{ row.declaredValue ? `USD ${row.declaredValue.toLocaleString()}` : '-' }}</template></el-table-column>
|
||
<el-table-column label="货件编号" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.shipmentId || '-' }}</span></template></el-table-column>
|
||
<el-table-column label="跟踪编号" width="150" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.referenceId || '-' }}</span></template></el-table-column>
|
||
<el-table-column label="UPC NO." width="130" show-overflow-tooltip><template #default="{ row }"><span class="table-code table-ellipsis">{{ row.upc || '-' }}</span></template></el-table-column>
|
||
<el-table-column label="中英文材质" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.material || '-' }}</template></el-table-column>
|
||
<el-table-column label="中英文用途" width="120" show-overflow-tooltip><template #default="{ row }">{{ row.usage || '-' }}</template></el-table-column>
|
||
<el-table-column label="型号" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.model || '-' }}</template></el-table-column>
|
||
<el-table-column label="品牌" width="110" show-overflow-tooltip><template #default="{ row }">{{ row.brand || '-' }}</template></el-table-column>
|
||
<el-table-column label="产品规格 长×宽×高(cm)" width="180" align="center"><template #default="{ row }">{{ row.productLengthCm || '-' }} × {{ row.productWidthCm || '-' }} × {{ row.productHeightCm || '-' }}</template></el-table-column>
|
||
<el-table-column label="危险品" width="82" align="center"><template #default="{ row }">{{ row.dangerousGoods ? '是' : '否' }}</template></el-table-column>
|
||
<el-table-column label="木制品" width="82" align="center"><template #default="{ row }">{{ row.woodenProduct ? '是' : '否' }}</template></el-table-column>
|
||
<el-table-column label="备注" min-width="160" show-overflow-tooltip><template #default="{ row }">{{ row.remark || '-' }}</template></el-table-column>
|
||
</el-table>
|
||
<el-pagination
|
||
v-model:current-page="detailCargoPage"
|
||
v-model:page-size="detailCargoPageSize"
|
||
class="cargo-detail-pagination"
|
||
:page-sizes="[5, 10, 20]"
|
||
:total="detailOrder.items.length"
|
||
layout="total, sizes, prev, pager, next"
|
||
/>
|
||
</div>
|
||
<el-empty v-else description="暂无货物明细" :image-size="56" />
|
||
</section>
|
||
<InboundBillingPanel
|
||
v-if="detailOrder.status !== 'CANCELLED'"
|
||
:data="inboundBilling"
|
||
:loading="inboundBillingLoading"
|
||
:unavailable="inboundBillingUnavailable"
|
||
:unavailable-message="inboundBillingUnavailableMessage"
|
||
:error="inboundBillingError"
|
||
:can-confirm="canConfirmInboundFee"
|
||
@retry="retryInboundBilling"
|
||
@confirmed="handleInboundFeeConfirmed"
|
||
/>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
<InboundBillingPanel
|
||
v-if="feeConfirmationOpen"
|
||
dialog-only
|
||
auto-open
|
||
:data="inboundBilling"
|
||
:loading="inboundBillingLoading"
|
||
:unavailable="inboundBillingUnavailable"
|
||
unavailable-message="未推送计费重"
|
||
:error="inboundBillingError"
|
||
:can-confirm="canConfirmInboundFee"
|
||
@retry="retryInboundBilling"
|
||
@confirmed="handleInboundFeeConfirmed"
|
||
@closed="feeConfirmationOpen = false"
|
||
/>
|
||
<CustomsDocumentsDialog
|
||
v-model="customsDialogOpen"
|
||
:order-id="customsDialogOrderId"
|
||
:can-edit="auth.hasPermission('order:first-leg:edit')"
|
||
:initial-detail="customsByOrder[customsDialogOrderId]"
|
||
@updated="updateCustomsDetail"
|
||
/>
|
||
<InboundCodeAssociationDialog
|
||
v-model="inboundAssociationOpen"
|
||
:inbound-code="inboundAssociationCode"
|
||
/>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.first-leg-page { display: flex; min-width: 0; flex-direction: column; gap: var(--client-section-gap); }
|
||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 0; }
|
||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||
:global(.client-main:has(> .first-leg-page.is-detail-route) > .client-page-heading) { display: none; }
|
||
.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-pending { border-color: #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||
.supporting-data-error { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||
.supporting-data-error > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||
.supporting-data-error strong { color: var(--navy); font-size: 13px; }
|
||
.supporting-data-error span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||
.order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||
.order-table-card { padding: 0; overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); box-shadow: none; }
|
||
.prototype-order-table :deep(.data-table__toolbar) { flex-wrap: nowrap; }
|
||
.prototype-order-table :deep(.data-table__toolbar-heading) { min-width: 0; flex: 1; flex-wrap: nowrap; }
|
||
.prototype-order-table :deep(.data-table__toolbar-actions) { flex-shrink: 0; }
|
||
.order-queue-tabs { display: flex; min-width: 0; align-items: center; gap: 4px; overflow-x: auto; padding: 7px 0 10px; scrollbar-width: thin; }
|
||
.order-queue-tabs button { flex-shrink: 0; white-space: nowrap; }
|
||
.queue-stage-count { margin-left: 3px; }
|
||
.order-queue-tabs button { position: relative; min-height: 34px; padding: 0 12px; border: 1px solid transparent; border-radius: 7px; background: transparent; color: #667085; cursor: pointer; font: inherit; font-weight: 600; }
|
||
.order-queue-tabs button:hover { color: var(--client-action); }
|
||
.order-queue-tabs button.active { border-color: #cfe0ff; background: #f3f7ff; color: var(--client-action); }
|
||
.order-queue-tabs button:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
|
||
.queue-count { position: absolute; display: grid; min-width: 18px; height: 18px; place-items: center; padding: 0 4px; border: 2px solid #fff; border-radius: 999px; background: #ef5350; color: #fff; font-size: 10px; line-height: 1; right: -4px; top: -8px; }
|
||
.batch-import-dialog { display: flex; flex-direction: column; gap: 18px; }
|
||
.batch-import-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 10px; }
|
||
.batch-import-actions > span { min-width: 0; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
||
.batch-import-dialog :deep(.el-radio-group) { display: flex; align-items: flex-start; flex-direction: column; gap: 8px; }
|
||
.batch-import-result { display: flex; flex-direction: column; gap: 6px; padding: 14px; border: 1px solid var(--border); background: #f8fafc; }
|
||
.batch-import-result > span, .batch-import-result li { color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||
.batch-import-result-actions { display: flex; margin-top: 4px; }
|
||
.batch-import-result ul { max-height: 180px; margin: 4px 0 0; padding-left: 20px; overflow-y: auto; }
|
||
.column-settings-trigger { min-width: 116px; height: 36px; padding: 0 12px; color: #53627b; }
|
||
:global(.order-column-popper.el-popover) { padding: 0; border-color: #e3e8f0; border-radius: 4px; box-shadow: 0 8px 24px rgba(29, 45, 74, .12); }
|
||
:global(.order-column-popper .column-settings-panel) { padding: 14px 10px 10px; }
|
||
:global(.order-column-popper .column-settings-panel > p) { margin: 0 8px 10px; color: #778398; font-size: 12px; line-height: 1.5; }
|
||
:global(.order-column-popper .el-checkbox-group) { max-height: 430px; overflow-y: auto; padding: 0 2px; scrollbar-width: thin; }
|
||
:global(.order-column-popper .column-settings-item) { display: grid; min-height: 42px; grid-template-columns: 24px minmax(0, 1fr) 28px 28px; align-items: center; padding: 0 4px; color: #596982; }
|
||
:global(.order-column-popper .column-settings-item:hover) { background: #f7f8fb; }
|
||
:global(.order-column-popper .column-settings-item > svg) { cursor: grab; color: #71809a; }
|
||
:global(.order-column-popper .column-settings-item > svg:active) { cursor: grabbing; }
|
||
:global(.order-column-popper .column-settings-item .el-checkbox) { height: 42px; margin-right: 0; overflow: hidden; }
|
||
:global(.order-column-popper .column-settings-item .el-checkbox__label) { overflow: hidden; color: #4f5f78; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||
:global(.order-column-popper .column-settings-item .el-checkbox.is-checked .el-checkbox__label) { color: var(--primary); }
|
||
:global(.order-column-popper .column-settings-item .el-button) { width: 28px; height: 28px; padding: 0; margin: 0; color: #45556e; }
|
||
:global(.order-column-popper .column-settings-reset) { min-height: 36px; margin: 8px 6px 0; color: #40516b; }
|
||
.table-ellipsis, .table-multivalue { display: block; width: 100%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.table-code { font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||
.order-table-card > .order-eligibility, .order-table-card > .supporting-data-error { border-width: 0 0 1px; }
|
||
.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; }
|
||
.first-leg-detail-page { min-width: 0; padding: 10px 14px 20px; overflow: hidden; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: #fff; }
|
||
.order-detail-content { min-height: 360px; }
|
||
.inbound-code-primary { display: block; width: 100%; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: var(--client-action); cursor: pointer; font-size: var(--client-font-size); font-weight: 700; line-height: 18px; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||
.pending-inbound-code { color: var(--muted); font-size: 12px; }
|
||
.inbound-code-link { max-width: 100%; overflow: hidden; padding: 0; border: 0; background: transparent; color: var(--client-action); 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; }
|
||
.inbound-code-primary: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; }
|
||
.order-row-actions :deep(.el-button) { margin-left: 0; padding: 3px 1px; font-size: 11px; }
|
||
.is-create-route { gap: 0; }
|
||
.is-create-route .first-leg-editor-page { width: 100%; }
|
||
.is-create-route .supporting-data-error, .is-create-route > .el-alert { margin-bottom: 14px; }
|
||
.is-create-route .prototype-order-form { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 10px; margin: 0; padding: 0 0 10px; }
|
||
.is-create-route .prototype-section { position: relative; min-width: 0; margin: 0; padding: 0 16px 14px; overflow: hidden; border: 1px solid #d9e4ea; border-radius: 8px; background: #fff; box-shadow: 0 8px 26px rgba(24, 55, 75, .065); transition: border-color .22s ease, box-shadow .22s ease; }
|
||
.is-create-route .order-basic-section::before, .is-create-route .destination-section::before { position: absolute; z-index: 2; top: 0; right: 0; left: 0; height: 3px; background: linear-gradient(90deg, #16a394, #176b87 55%, transparent 92%); content: ''; }
|
||
.is-create-route .destination-section::before { background: linear-gradient(90deg, #2563eb, #4f46e5 55%, transparent 92%); }
|
||
.is-create-route .order-basic-section:focus-within { border-color: rgba(22, 163, 148, .45); box-shadow: 0 12px 34px rgba(22, 113, 113, .1); }
|
||
.is-create-route .destination-section:focus-within { border-color: rgba(37, 99, 235, .4); box-shadow: 0 12px 34px rgba(37, 99, 235, .1); }
|
||
.is-create-route .prototype-section + .prototype-section { margin-top: 0; }
|
||
.is-create-route .cargo-section, .is-create-route .cost-estimate-section { grid-column: 1 / -1; }
|
||
.is-create-route .order-basic-section > .order-section-heading, .is-create-route .destination-section > .order-section-heading { box-sizing: border-box; align-items: center; margin: 0 -16px 12px; padding: 10px 16px; border-bottom: 1px solid #e7ecf2; background: linear-gradient(90deg, #f8fbfc, #fff 58%); }
|
||
.is-create-route .destination-section > .order-section-heading { background: linear-gradient(90deg, #f7f9ff, #fff 58%); }
|
||
.is-create-route .order-basic-section > .order-section-heading h3, .is-create-route .destination-section > .order-section-heading h3 { margin: 0; color: var(--navy); font-size: 17px; font-weight: 700; line-height: 24px; }
|
||
.is-create-route .order-step-badge { display: grid; width: 30px; height: 30px; flex: 0 0 30px; color: #fff; border-radius: 8px; background: linear-gradient(135deg, #16a394, #176b87); box-shadow: 0 5px 12px rgba(23, 107, 135, .2); font-size: 11px; font-weight: 800; place-items: center; }
|
||
.is-create-route .destination-section .order-step-badge { background: linear-gradient(135deg, #367ae8, #4f46e5); box-shadow: 0 5px 12px rgba(37, 99, 235, .2); }
|
||
.is-create-route .order-section-context { margin-left: auto; color: #8a98a8; font-size: 11px; font-weight: 500; }
|
||
.is-create-route .cargo-section > .order-section-heading, .is-create-route .cost-estimate-section > .order-section-heading { align-items: center; margin: 0 -16px 10px; padding: 9px 16px; border-bottom: 1px solid #e7ecf2; }
|
||
.is-create-route .order-section-heading p { margin: 0; color: #8a98a8; }
|
||
.is-create-route .prototype-form-flow, .is-create-route .destination-form-flow { width: 100%; max-width: none; }
|
||
.is-create-route .prototype-form-row, .is-create-route .prototype-form-row--wide, .is-create-route .prototype-form-row--compact, .is-create-route .prototype-form-row--short, .is-create-route .prototype-form-row--tabs { grid-template-columns: 108px minmax(0, 1fr); }
|
||
.is-create-route .prototype-form-row :deep(.el-form-item__label) { display: flex; height: 36px; min-height: 36px; align-items: center; justify-content: flex-end; padding: 0 10px 0 0; font-size: 13px; line-height: 1.35; text-align: right; }
|
||
.is-create-route .segmented-options, .is-create-route .destination-type-options { width: 100%; max-width: 560px; background: #f3f6f9; }
|
||
.is-create-route .order-basic-section .segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { background: linear-gradient(105deg, #1596a6, #176b87); }
|
||
.is-create-route .destination-section .segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { background: linear-gradient(105deg, #367ae8, #4f46e5); box-shadow: 0 4px 10px rgba(37, 99, 235, .2); }
|
||
.is-create-route .compact-transport-grid { width: 100%; margin-top: 4px; }
|
||
.is-create-route .compact-transport-grid > :deep(.el-form-item), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(1)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(6)), .is-create-route .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 108px minmax(0, 1fr); }
|
||
.is-create-route .compact-transport-grid > :deep(.el-form-item__label) { display: flex; height: 36px; min-height: 36px; align-items: center; justify-content: flex-end; padding: 0 10px 0 0; line-height: 1.35; text-align: right; }
|
||
.is-create-route .selected-destination { width: auto; margin-left: 108px; }
|
||
.is-create-route .shipping-form-flow .prototype-form-row, .is-create-route .destination-form-flow .prototype-form-row, .is-create-route .compact-transport-grid > :deep(.el-form-item) { margin-bottom: 5px; }
|
||
.is-create-route .prototype-form-row :deep(.el-input__wrapper), .is-create-route .prototype-form-row :deep(.el-select__wrapper), .is-create-route .compact-transport-grid :deep(.el-input__wrapper), .is-create-route .compact-transport-grid :deep(.el-select__wrapper) { min-height: 36px; }
|
||
.is-create-route .segmented-options :deep(.el-radio-button__inner) { min-height: 36px; padding: 8px 12px; }
|
||
.is-create-route .warehouse-options { gap: 8px; padding-top: 1px; }
|
||
.is-create-route .warehouse-options :deep(.el-radio) { min-height: 32px; margin: 0; padding: 5px 11px; border: 1px solid #d7e0e7; border-radius: 7px; background: #f8fafb; color: #596779; transition: color .18s ease, border-color .18s ease, background .18s ease, box-shadow .18s ease, transform .18s ease; }
|
||
.is-create-route .warehouse-options :deep(.el-radio:hover) { transform: translateY(-1px); border-color: #91b8c3; background: #f3fafb; color: var(--primary); }
|
||
.is-create-route .warehouse-options :deep(.el-radio__input) { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
|
||
.is-create-route .warehouse-options :deep(.el-radio__label) { padding-left: 0; color: inherit; }
|
||
.is-create-route .warehouse-options :deep(.el-radio.is-checked) { border-color: var(--primary); background: linear-gradient(105deg, #1596a6, #176b87); box-shadow: 0 4px 10px rgba(23, 107, 135, .2); color: #fff; }
|
||
.is-create-route .cargo-table-heading, .is-create-route .cost-estimate-section .order-section-heading { margin-bottom: 10px; }
|
||
.order-form { width: min(100%, 1000px); margin: 0; padding: 0 0 12px; }
|
||
.order-form.is-dialog-order-form { width: 100%; max-width: none; }
|
||
.is-dialog-order-form > .cargo-section, .is-dialog-order-form > .cost-estimate-section { width: 100%; max-width: none; }
|
||
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
|
||
.order-form-section + .order-form-section { border-top: 1px solid #edf0f5; }
|
||
.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 > *, .cargo-form-grid > * { min-width: 0; }
|
||
.order-form :deep(.el-select), .order-form :deep(.el-date-editor), .order-form :deep(.el-input-number) { width: 100%; }
|
||
.form-span-full { grid-column: 1 / -1; }
|
||
.prototype-section { padding: 4px 0 0; }
|
||
.prototype-section:first-child > .order-section-heading { margin-top: -10px; }
|
||
.prototype-section:first-child > .order-section-heading h3 { margin-bottom: 6px; font-size: 20px; font-weight: 700; line-height: 1.35; }
|
||
.segmented-options { display: flex; width: min(100%, 560px); overflow: hidden; border: 1px solid #e3e8f0; border-radius: 5px; background: #fff; }
|
||
.segmented-options :deep(.el-radio-button) { flex: 1; }
|
||
.segmented-options :deep(.el-radio-button__inner) { display: flex; width: 100%; min-height: 42px; box-sizing: border-box; align-items: center; justify-content: center; padding: 10px 16px; border: 0; border-right: 1px solid #e3e8f0; border-radius: 0; box-shadow: none; color: #526070; font-size: 13px; }
|
||
.segmented-options :deep(.el-radio-button:last-child .el-radio-button__inner) { border-right: 0; }
|
||
.segmented-options :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) { border-color: var(--primary); background: var(--primary); box-shadow: none; color: #fff; }
|
||
.warehouse-options { display: flex; width: 100%; align-items: center; flex-wrap: wrap; gap: 10px 22px; padding-top: 9px; }
|
||
.warehouse-options :deep(.el-radio) { height: auto; margin-right: 0; color: #4d5a6c; }
|
||
.warehouse-options :deep(.el-radio__label) { padding-left: 7px; font-size: 13px; }
|
||
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
|
||
.order-section-heading h3 { margin-bottom: 5px; }
|
||
.order-section-heading p { margin: 0; color: #8b96a6; font-size: 12px; }
|
||
.prototype-form-flow { display: flex; max-width: 860px; flex-direction: column; padding-top: 2px; }
|
||
.prototype-form-row { display: grid; width: 100%; grid-template-columns: 114px minmax(0, 560px); align-items: start; margin-bottom: 16px; }
|
||
.prototype-form-row--wide { grid-template-columns: 114px minmax(0, 760px); }
|
||
.prototype-form-row--compact { grid-template-columns: 114px minmax(0, 420px); }
|
||
.prototype-form-row--short { grid-template-columns: 114px minmax(0, 280px); }
|
||
.prototype-form-row--tabs { grid-template-columns: 114px minmax(0, 560px); margin-bottom: 18px; }
|
||
.prototype-form-row :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 18px 0 0; line-height: 40px; color: #4d5a6c; font-size: 13px; }
|
||
.prototype-form-row :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
|
||
.prototype-form-row :deep(.el-input__wrapper), .prototype-form-row :deep(.el-select__wrapper) { min-height: 40px; box-shadow: 0 0 0 1px #dfe5ed inset; }
|
||
.address-book-control { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto; gap: 10px; align-items: center; }
|
||
.address-book-control :deep(.el-button) { min-height: 40px; margin-left: 0; }
|
||
.field-guidance { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; }
|
||
.field-guidance--error { color: #b54708; }
|
||
.cost-estimate-section { width: 100%; }
|
||
.cost-estimate-section .order-section-heading { margin-bottom: 12px; }
|
||
.price-estimate { min-height: 126px; overflow: hidden; border: 1px solid #dde4f2; border-radius: 8px; background: #fff; box-shadow: 0 2px 8px rgba(15, 31, 58, .025); }
|
||
.price-estimate__state { display: flex; min-height: 126px; align-items: center; justify-content: center; gap: 10px; padding: 24px; color: #7b8798; font-size: 12px; text-align: center; }
|
||
.price-estimate__state.is-error { justify-content: flex-start; background: #fffaf0; color: #b54708; text-align: left; }
|
||
.price-estimate__state.is-error > div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 4px; }
|
||
.price-estimate__state.is-error strong { color: #7a2e0e; font-size: 13px; }
|
||
.price-estimate__state.is-error span { overflow-wrap: anywhere; }
|
||
.price-estimate__result { display: grid; grid-template-columns: minmax(180px, 1.15fr) minmax(220px, 1.25fr) minmax(170px, .9fr) minmax(140px, .8fr) minmax(190px, 1fr); align-items: stretch; }
|
||
.price-estimate__result > div { display: flex; min-width: 0; min-height: 96px; justify-content: center; flex-direction: column; gap: 7px; padding: 18px 20px; border-right: 1px solid #edf0f5; }
|
||
.price-estimate__result > div > span { color: #8b96a6; font-size: 11px; }
|
||
.price-estimate__result > div > strong { overflow-wrap: anywhere; color: #303b50; font-size: 15px; }
|
||
.price-estimate__comparison strong { font-size: 13px !important; font-weight: 600; }
|
||
.price-estimate__comparison small, .price-estimate__chargeable small { color: #7b8798; font-size: 10px; line-height: 1.4; }
|
||
.price-estimate__chargeable strong { color: #526fd8 !important; font-size: 18px !important; }
|
||
.price-estimate__channel small { color: #7b8798; font-size: 10px; line-height: 1.5; }
|
||
.price-estimate__result .price-estimate__amount { border-right: 0; background: #f8f8ff; }
|
||
.price-estimate__result .price-estimate__amount strong { color: #637fec; font-size: 25px; }
|
||
.price-estimate__result > small { grid-column: 1 / -1; padding: 9px 14px; border-top: 1px solid #edf0f5; color: #98a0ad; font-size: 10px; text-align: right; }
|
||
.selected-destination { display: flex; width: min(100%, 720px); align-items: flex-start; gap: 9px; margin: -4px 0 18px 140px; padding: 10px 12px; background: #f2f7f6; color: var(--primary); }
|
||
.selected-destination svg { flex: 0 0 17px; margin-top: 2px; }
|
||
.selected-destination div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||
.selected-destination span { color: var(--navy); font-size: 12px; font-weight: 600; }
|
||
.selected-destination small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||
.destination-section { width: 100%; }
|
||
.destination-section-heading { margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #edf0f5; }
|
||
.destination-section-heading h3 { margin-bottom: 0; }
|
||
.destination-form-flow { width: min(100%, 900px); padding-left: 0; }
|
||
.destination-type-row { grid-template-columns: 114px minmax(0, 300px); margin-bottom: 16px; }
|
||
.destination-type-options { width: 300px; }
|
||
.destination-type-options :deep(.el-radio-button__inner) { min-height: 36px; padding: 8px 14px; font-size: 12px; }
|
||
.destination-form-flow > .prototype-form-row:not(.destination-type-row):not(.prototype-form-row--wide):not(.prototype-form-row--compact):not(.prototype-form-row--short) { grid-template-columns: 114px minmax(0, 360px); }
|
||
.flow-divider { width: min(100%, 860px); margin: 4px 0 22px; border-top: 1px solid var(--border); }
|
||
.compact-transport-grid { width: min(100%, 820px); grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 28px; margin: 20px 0 0 114px; }
|
||
.compact-transport-grid :deep(.el-form-item__label) { color: #4d5a6c; font-size: 13px; }
|
||
.shipping-form-flow { max-width: 900px; }
|
||
.prototype-order-form > .prototype-section:first-child { width: 100%; }
|
||
.is-create-route .prototype-order-form > .prototype-section:first-child > .order-section-heading { margin-top: 0; }
|
||
.shipping-form-flow .prototype-form-row { margin-bottom: 10px; }
|
||
.shipping-form-flow .prototype-form-row--tabs { margin-bottom: 12px; }
|
||
.compact-transport-grid { display: grid; width: 100%; grid-template-columns: 1fr; gap: 0; margin: 6px 0 0; }
|
||
.compact-transport-grid > :deep(.el-form-item) { display: grid; width: 100%; grid-template-columns: 114px minmax(0, 560px); align-items: start; margin-bottom: 10px; }
|
||
.compact-transport-grid > :deep(.pricing-tax-mode-field) { display: none; }
|
||
.compact-transport-grid > :deep(.el-form-item:nth-child(1)) { grid-template-columns: 114px minmax(0, 360px); }
|
||
.compact-transport-grid > :deep(.el-form-item:nth-child(6)) { grid-template-columns: 114px minmax(0, 360px); }
|
||
.compact-transport-grid > :deep(.el-form-item:nth-child(7)) { grid-template-columns: 114px minmax(0, 620px); margin-top: 2px; margin-bottom: 0; }
|
||
.compact-transport-grid > :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 18px 0 0; line-height: 40px; }
|
||
.compact-transport-grid > :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
|
||
.compact-transport-grid > :deep(.el-form-item .el-input__wrapper), .compact-transport-grid > :deep(.el-form-item .el-select__wrapper) { min-height: 40px; }
|
||
.cargo-section { min-width: 0; padding-bottom: 4px; }
|
||
.cargo-table-heading { margin-bottom: 8px; }
|
||
.cargo-table-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 10px; }
|
||
.cargo-import-actions { display: flex; align-items: center; gap: 10px; }
|
||
.cargo-import-input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
|
||
.cargo-import-actions :deep(.el-button) { min-height: 38px; margin-left: 0; padding: 8px 17px; }
|
||
.cargo-import-actions .cargo-upload-button { border-style: dashed; border-width: 2px; border-color: #b8c5ff; color: #637fec; }
|
||
.cargo-import-actions .cargo-upload-button:hover, .cargo-import-actions .cargo-upload-button:focus-visible { border-color: var(--primary); background: #f5f7ff; color: var(--primary); }
|
||
.cargo-import-actions .cargo-download-button { color: #4d5a6c; }
|
||
.cargo-import-actions .cargo-download-button :deep(svg) { color: #09a8d8; }
|
||
.cargo-table-actions { display: flex; align-items: center; gap: 8px; }
|
||
.cargo-table-actions :deep(.el-button) { margin-left: 0; }
|
||
.cargo-table-wrap { width: 100%; overflow-x: auto; border: 1px solid #e6eaf0; border-radius: 6px; background: #fff; scrollbar-color: #c8ced8 #f1f3f6; scrollbar-width: thin; }
|
||
.cargo-edit-table { width: max-content; min-width: 100%; border-collapse: collapse; table-layout: fixed; color: #4d5a6c; font-size: 12px; }
|
||
.cargo-edit-table th { height: 44px; padding: 7px 8px; border-bottom: 1px solid #e6eaf0; background: #f4f5fb; color: #637fec; font-size: 11px; font-weight: 650; text-align: left; white-space: nowrap; }
|
||
.cargo-edit-table th small { margin-left: 3px; color: #e8a20c; font-size: 9px; font-weight: 600; }
|
||
.cargo-edit-table td { width: 130px; height: 58px; padding: 7px 6px; border-bottom: 1px solid #edf0f5; background: #fff; vertical-align: top; }
|
||
.cargo-edit-table tbody tr:last-child td { border-bottom: 0; }
|
||
.cargo-edit-table :deep(.el-form-item) { margin-bottom: 0; }
|
||
.cargo-edit-table :deep(.el-form-item__content) { display: block; line-height: normal; }
|
||
.cargo-edit-table :deep(.el-form-item__error) { width: 100%; padding: 3px 2px 0; font-size: 9px; white-space: normal; }
|
||
.cargo-edit-table :deep(.el-input), .cargo-edit-table :deep(.el-select), .cargo-edit-table :deep(.el-input-number) { width: 100%; }
|
||
.cargo-edit-table :deep(.el-input__wrapper), .cargo-edit-table :deep(.el-select__wrapper) { min-height: 34px; padding: 1px 8px; box-shadow: none; }
|
||
.cargo-edit-table :deep(.el-input__wrapper:hover), .cargo-edit-table :deep(.el-select__wrapper:hover) { box-shadow: 0 0 0 1px #b9c8f5 inset; }
|
||
.cargo-edit-table :deep(.el-input__inner) { font-size: 11px; text-align: left; }
|
||
.cargo-check-cell, .cargo-index-cell { width: 38px !important; text-align: center !important; vertical-align: middle !important; }
|
||
.cargo-image-column { width: 62px !important; text-align: center !important; }
|
||
.cargo-number-column { width: 92px !important; }
|
||
.required-field-mark { margin-right: 2px; color: var(--el-color-danger); }
|
||
.cargo-dimensions-column { width: 240px !important; }
|
||
.cargo-operation-column { width: 76px !important; text-align: center !important; }
|
||
.cargo-table-wrap.is-dropship .cargo-operation-column { width: 148px !important; }
|
||
.cargo-sku-input { position: relative; width: 100%; }
|
||
.mixed-sku-badge { position: absolute; top: -10px; right: 2px; padding: 1px 5px; border-radius: 999px; background: #eaf6f2; color: var(--primary); font-size: 9px; font-weight: 650; line-height: 16px; white-space: nowrap; }
|
||
.mixed-sku-detail-row { background: #f8fbff; }
|
||
.shared-carton-value { display: inline-flex; min-height: 30px; align-items: center; color: #8a97a8; font-size: 12px; white-space: nowrap; }
|
||
.derived-sku-value { display: inline-flex; min-height: 30px; align-items: center; color: #172b4d; font-weight: 650; }
|
||
.cargo-switch-column { width: 112px !important; text-align: center !important; vertical-align: middle !important; }
|
||
.cargo-switch-cell { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 4px; }
|
||
.cargo-switch-cell small { color: #b54708; font-size: 9px; line-height: 1.25; white-space: normal; }
|
||
.cargo-table-image { position: relative; display: flex; width: 34px; height: 34px; align-items: center; justify-content: center; margin: 0 auto; border: 1px dashed #d6dce5; border-radius: 4px; color: #788493; }
|
||
.cargo-table-image img { width: 100%; height: 100%; border-radius: 3px; object-fit: contain; }
|
||
.cargo-table-image :deep(.el-button) { width: 30px; height: 30px; padding: 0; }
|
||
.cargo-table-image .cargo-image-remove { position: absolute; top: -9px; right: -9px; width: 19px; height: 19px; border: 1px solid #ffd7d7; border-radius: 50%; background: #fff; }
|
||
.cargo-table-dimensions { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); gap: 3px; align-items: center; }
|
||
.cargo-table-dimensions > span { color: #9aa4b2; }
|
||
.cargo-table-hint { display: flex; align-items: center; flex-wrap: wrap; gap: 9px; padding: 10px 2px 0; color: #8b96a6; font-size: 11px; }
|
||
.cargo-table-hint::before { color: #e5a512; content: '●'; font-size: 8px; }
|
||
.cargo-table-hint i { width: 3px; height: 3px; border-radius: 50%; background: #aab2bd; }
|
||
.cargo-entry { padding: 18px 20px; border: 1px solid #e2e7ee; background: #fff; box-shadow: 0 2px 8px rgba(15, 31, 58, .025); }
|
||
.cargo-entry + .cargo-entry { margin-top: 12px; }
|
||
.cargo-entry > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
|
||
.cargo-field-section + .cargo-field-section { margin-top: 18px; padding-top: 16px; border-top: 1px solid #edf0f5; }
|
||
.cargo-field-section h4 { margin: 0 0 14px; color: #516071; font-size: 12px; }
|
||
.cargo-form-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); column-gap: 12px; }
|
||
.cargo-form-grid--basic { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||
.product-image-field { grid-row: span 2; }
|
||
.product-image-control { display: grid; grid-template-columns: 78px minmax(0, 1fr); align-items: center; gap: 7px 10px; width: 100%; }
|
||
.product-image-preview, .product-image-placeholder { position: relative; display: flex; width: 78px; height: 78px; grid-row: 1 / span 3; align-items: center; justify-content: center; overflow: hidden; border: 1px dashed #cbd5da; background: #fff; color: var(--muted); }
|
||
.product-image-preview img { width: 100%; height: 100%; object-fit: contain; }
|
||
.product-image-preview :deep(.el-button) { position: absolute; top: 4px; right: 4px; width: 26px; height: 26px; }
|
||
.product-image-placeholder { flex-direction: column; gap: 5px; font-size: 10px; }
|
||
.product-image-control small { color: var(--muted); font-size: 10px; line-height: 1.4; }
|
||
.product-image-error { grid-column: 2; overflow-wrap: anywhere; color: #b42318; font-size: 10px; line-height: 1.4; }
|
||
.product-dimensions-field, .cargo-remark-field { grid-column: span 2; }
|
||
.dimension-inputs { display: grid; width: 100%; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 6px; }
|
||
.dimension-inputs > span { color: var(--muted); }
|
||
.cargo-detail-image { display: flex; width: 112px; height: 112px; align-items: center; justify-content: center; margin-top: 12px; overflow: hidden; border: 1px solid var(--border); background: #fff; color: var(--muted); font-size: 10px; text-align: center; }
|
||
.cargo-detail-image img { width: 100%; height: 100%; object-fit: contain; }
|
||
.order-detail-heading { display: flex; min-height: 50px; align-items: center; justify-content: space-between; gap: 16px; padding: 5px 0; border-bottom: 1px solid var(--border); }
|
||
.detail-heading-main, .detail-heading-actions { display: flex; min-width: 0; align-items: center; gap: 12px; }
|
||
.detail-heading-main > div { min-width: 0; }
|
||
.detail-back-action, .detail-heading-actions { flex: 0 0 auto; }
|
||
.order-detail-heading h2 { margin: 0; overflow: hidden; color: var(--navy); font-size: 18px; text-overflow: ellipsis; white-space: nowrap; }
|
||
.order-detail-heading p { margin: 3px 0 0; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||
.order-detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; margin: 0; border-top: 1px solid var(--border); border-left: 1px solid var(--border); }
|
||
.order-detail-grid div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 78px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.order-detail-grid dt { color: var(--muted); font-size: 10px; white-space: nowrap; }
|
||
.order-detail-grid dd { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: 12px; font-weight: 600; }
|
||
.detail-subline { display: block; margin-top: 2px; color: var(--muted); font-size: 9px; font-weight: 400; line-height: 1.35; }
|
||
.detail-section { margin-top: 10px; }
|
||
.detail-section h3 { margin: 0; padding: 0 0 5px; border-bottom: 1px solid #eaf0f7; color: var(--navy); font-size: 13px; }
|
||
.party-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-left: 1px solid var(--border); }
|
||
.party-detail-grid > div { display: grid; min-width: 0; grid-template-columns: 62px minmax(0, 1fr); gap: 7px; padding: 5px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.party-detail-grid dt { color: var(--muted); font-size: 10px; }
|
||
.party-detail-grid dd { min-width: 0; margin: 0; }
|
||
.party-detail-grid strong { display: block; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; }
|
||
.party-detail-grid span { display: block; margin-top: 3px; overflow-wrap: anywhere; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||
.customer-remark p { margin: 7px 0 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.65; }
|
||
.customer-remark { margin-top: 7px; padding: 8px 10px; background: #f2f7f6; }
|
||
.customer-remark strong { color: var(--navy); font-size: 11px; }
|
||
.fulfillment-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-left: 1px solid var(--border); }
|
||
.fulfillment-summary > div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 70px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.fulfillment-summary dt { color: var(--muted); font-size: 10px; }
|
||
.fulfillment-summary dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 650; }
|
||
.cabinet-progress-detail { border-top: 1px solid var(--border); }
|
||
.cabinet-progress-detail > strong { display: block; padding: 7px 0 5px; color: var(--navy); font-size: 11px; }
|
||
.cabinet-progress-table { margin-top: 0; }
|
||
.detail-inline-empty { margin: 0; padding: 7px 9px; background: #f7f9fb; color: var(--muted); font-size: 10px; }
|
||
.express-information { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-left: 1px solid var(--border); }
|
||
.express-information > div { display: grid; min-width: 0; min-height: 40px; grid-template-columns: 70px minmax(0, 1fr); align-items: center; gap: 8px; padding: 6px 10px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.express-information dt { color: var(--muted); font-size: 10px; }
|
||
.express-information dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 650; }
|
||
.express-empty { display: flex; align-items: center; gap: 9px; padding: 7px 9px; 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)); margin: 0; border-left: 1px solid var(--border); }
|
||
.cargo-summary div { display: grid; min-width: 0; min-height: 34px; grid-template-columns: 54px minmax(0, 1fr); align-items: center; gap: 7px; padding: 3px 8px; border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.cargo-summary dt { color: var(--muted); font-size: 10px; }
|
||
.cargo-summary dd { margin: 0; color: var(--navy); font-size: 11px; font-weight: 700; }
|
||
.cargo-detail-table-wrap { overflow: hidden; margin-top: 10px; border: 1px solid var(--border); border-radius: 5px; background: #fff; }
|
||
.cargo-detail-table { width: 100%; font-size: 11px; }
|
||
.cargo-detail-table :deep(.el-table__header th.el-table__cell) { height: 42px; padding: 0; background: #f7f8fb; color: #657187; font-size: 11px; font-weight: 650; white-space: nowrap; }
|
||
.cargo-detail-table :deep(.el-table__body td.el-table__cell) { height: 48px; padding: 0; color: #4f5b6d; vertical-align: middle; }
|
||
.cargo-detail-table :deep(.cell) { min-width: 0; padding: 0 10px; line-height: 18px; white-space: nowrap; }
|
||
.cargo-detail-thumbnail { display: flex; width: 34px; height: 34px; align-items: center; justify-content: center; margin: 0 auto; overflow: hidden; border: 1px solid #e1e6ee; border-radius: 3px; color: var(--muted); }
|
||
.cargo-detail-thumbnail img { width: 100%; height: 100%; object-fit: contain; }
|
||
.cargo-detail-pagination { min-height: 52px; justify-content: flex-end; box-sizing: border-box; padding: 9px 14px; border-top: 1px solid var(--border); }
|
||
: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: 1280px) {
|
||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.order-detail-grid, .fulfillment-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
}
|
||
@media (min-width: 721px) and (max-width: 1050px) {
|
||
.cargo-form-grid, .cargo-form-grid--basic { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.product-image-field { grid-row: auto; }
|
||
}
|
||
@media (max-width: 720px) {
|
||
.first-leg-query-toolbar { padding: 12px; }
|
||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: flex; }
|
||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 10px; }
|
||
.order-eligibility { flex-wrap: wrap; }
|
||
.order-eligibility .el-button { width: 100%; min-height: 44px; }
|
||
.order-form-grid, .order-detail-grid, .cargo-form-grid, .cargo-form-grid--basic, .party-detail-grid { grid-template-columns: 1fr; }
|
||
.product-image-field { grid-row: auto; }
|
||
.product-dimensions-field, .cargo-remark-field { grid-column: auto; }
|
||
.fulfillment-summary { grid-template-columns: 1fr; }
|
||
.fulfillment-summary > div { border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
|
||
.fulfillment-summary > div:last-child { border-bottom: 0; }
|
||
.prototype-form-row, .prototype-form-row--wide, .prototype-form-row--compact, .prototype-form-row--short, .prototype-form-row--tabs { display: block; margin-bottom: 18px; }
|
||
.prototype-form-row :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
|
||
.selected-destination { width: 100%; margin-left: 0; }
|
||
.address-book-control { grid-template-columns: 1fr auto; }
|
||
.address-book-control :deep(.el-select) { grid-column: 1 / -1; }
|
||
.segmented-options { width: 100%; align-items: stretch; flex-direction: column; }
|
||
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||
.warehouse-options { width: 100%; gap: 12px 18px; }
|
||
.compact-transport-grid > :deep(.el-form-item), .compact-transport-grid > :deep(.el-form-item:nth-child(7)) { display: block; margin-bottom: 14px; }
|
||
.compact-transport-grid > :deep(.pricing-tax-mode-field) { display: none; }
|
||
.compact-transport-grid > :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
|
||
.cargo-table-toolbar { align-items: stretch; flex-direction: column; }
|
||
.cargo-import-actions, .cargo-table-actions { flex-wrap: wrap; }
|
||
.price-estimate__result { grid-template-columns: 1fr; }
|
||
.price-estimate__result > div { min-height: auto; border-right: 0; border-bottom: 1px solid #edf0f5; }
|
||
.price-estimate__result > small { grid-column: auto; }
|
||
.price-estimate__result > small { text-align: left; }
|
||
.price-estimate__state.is-error { align-items: flex-start; flex-direction: column; }
|
||
.form-span-full { grid-column: auto; }
|
||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||
.order-load-state { min-height: 240px; grid-template-columns: 1fr; justify-items: start; }
|
||
.supporting-data-error { grid-template-columns: auto minmax(0, 1fr); }
|
||
.supporting-data-error .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
|
||
.order-load-state .el-button { width: 100%; min-height: 44px; }
|
||
.express-information { grid-template-columns: 1fr; }
|
||
.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); }
|
||
}
|
||
@media (max-width: 360px) {
|
||
.cargo-detail-pagination { justify-content: flex-start; overflow-x: auto; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.first-leg-detail-page { padding-bottom: 22px; }
|
||
.order-detail-heading { align-items: flex-start; flex-direction: column; }
|
||
.detail-heading-main { width: 100%; align-items: flex-start; }
|
||
.detail-heading-actions { width: 100%; justify-content: flex-end; }
|
||
.editor-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.editor-actions :deep(.el-button) { width: 100%; min-width: 0; margin-left: 0; }
|
||
.editor-actions :deep(.el-button:nth-child(3)) { grid-column: 1 / -1; }
|
||
}
|
||
</style>
|