2026-07-30 17:59:42 +08:00
< script setup lang = "ts" >
2026-08-03 17:44:42 +08:00
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
2026-08-13 09:48:51 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-08-07 18:44:58 +08:00
import { ElMessage , ElMessageBox , type FormInstance , type FormItemRule , type FormRules , type UploadRequestOptions } from 'element-plus'
2026-09-18 16:48:35 +08:00
import { AlertTriangle , ChevronDown , ChevronUp , Download , FilePlus2 , FileText , GripVertical , ImagePlus , MapPin , PackageCheck , Pencil , Plus , RotateCcw , Send , SlidersHorizontal , Trash2 , Upload } from '@lucide/vue'
2026-08-12 17:46:46 +08:00
import { FIRST_LEG_ORDER_STATUSES , type FirstLegOrderStatus } from '@chongchong/shared'
2026-08-01 12:17:33 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/components/common/QueryToolbar.vue'
import DataTable , { type DataTableColumn } from '@/components/common/DataTable.vue'
2026-09-15 18:17:21 +08:00
import type { TableAction } from '@/components/common/tableActions'
2026-08-04 21:01:34 +08:00
import { ApiError } from '@/api/http'
2026-08-03 18:52:30 +08:00
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
2026-08-04 21:01:34 +08:00
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
2026-08-30 17:35:52 +08:00
import InboundCodeAssociationDialog from '@/modules/first-leg/components/InboundCodeAssociationDialog.vue'
2026-08-06 13:56:13 +08:00
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
2026-08-28 16:54:16 +08:00
import CustomsDocumentsDialog from '@/modules/first-leg/components/CustomsDocumentsDialog.vue'
2026-08-04 21:01:34 +08:00
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
2026-08-28 16:54:16 +08:00
import { getMerchantCustomsForOrder , isMerchantCustomsUnavailable , type MerchantCustomsContainer } from '@/modules/first-leg/api/customs'
2026-08-07 14:47:00 +08:00
import {
estimateMerchantFirstLegPrice ,
2026-09-01 19:34:37 +08:00
createIdempotencyKey ,
createMerchantOrderAddress ,
disableMerchantOrderAddress ,
2026-09-01 20:38:12 +08:00
downloadMerchantFirstLegBatchImportErrorReport ,
2026-09-01 19:34:37 +08:00
downloadMerchantFirstLegBatchImportTemplate ,
2026-08-13 17:12:05 +08:00
downloadMerchantOrderCartonLabelsBatch ,
2026-08-07 18:44:58 +08:00
downloadMerchantProductImage ,
2026-08-18 11:32:58 +08:00
getMerchantInboundCodePolicy ,
2026-09-01 19:34:37 +08:00
getMerchantOrderAddresses ,
2026-08-07 14:47:00 +08:00
getMerchantPricingChannels ,
2026-09-01 19:34:37 +08:00
importMerchantFirstLegOrders ,
2026-09-14 17:18:56 +08:00
submitMerchantFirstLegOrder ,
2026-09-01 19:34:37 +08:00
updateMerchantOrderAddress ,
2026-08-07 18:44:58 +08:00
uploadMerchantProductImage ,
2026-09-01 19:34:37 +08:00
type CustomerOrderAddress ,
type CustomerOrderAddressSaveRequest ,
type MerchantBatchImportMode ,
type MerchantBatchImportResponse ,
2026-08-07 14:47:00 +08:00
type MerchantPriceEstimate ,
type MerchantPriceEstimateRequest ,
type MerchantPricingChannel ,
2026-08-18 11:32:58 +08:00
type InboundCodePolicy ,
2026-08-07 14:47:00 +08:00
} from '@/modules/first-leg/api/firstLeg'
2026-08-03 18:52:30 +08:00
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
import { useAuthStore } from '@/modules/auth/public'
2026-08-05 15:55:02 +08:00
import { useUnsavedWorkRegistration } from '@/navigation/workspace-tabs/unsavedWork'
2026-08-25 16:11:00 +08:00
import { useWorkspaceTabsStore } from '@/navigation/workspace-tabs/workspaceTabs'
2026-08-04 12:01:05 +08:00
import type { FirstLegPartyRole , FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
2026-08-07 18:44:58 +08:00
import type { FirstLegCargoItem , FirstLegOrder , FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
2026-08-04 21:01:34 +08:00
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
2026-07-30 17:59:42 +08:00
import {
canClientEditFirstLegOrder ,
createEmptyCargoItem ,
getFirstLegStatusLabel ,
getFirstLegStatusTone ,
getFirstLegTransportModeLabel ,
summarizeFirstLegCargo ,
validateFirstLegOrderDraft ,
2026-08-03 18:52:30 +08:00
} from '@/modules/first-leg/utils/firstLegOrder'
2026-08-07 18:44:58 +08:00
import { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
2026-08-21 09:59:58 +08:00
import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate'
2026-07-30 17:59:42 +08:00
const store = useFirstLegOrdersStore ()
const tenantStore = useTenantAdministrationStore ()
2026-08-25 16:11:00 +08:00
const workspace = useWorkspaceTabsStore ()
2026-07-30 17:59:42 +08:00
const auth = useAuthStore ()
2026-08-13 09:48:51 +08:00
const route = useRoute ()
2026-07-30 17:59:42 +08:00
const router = useRouter ()
2026-09-18 16:48:35 +08:00
const props = withDefaults ( defineProps < { createPage ?: boolean ; detailPage ?: boolean ; showCreateAction ?: boolean } > (), { createPage : false , detailPage : false , showCreateAction : true })
2026-08-05 15:55:02 +08:00
const isCreatePage = computed (() => props . createPage )
2026-09-18 16:48:35 +08:00
const isDetailPage = computed (() => props . detailPage )
2026-08-13 09:48:51 +08:00
const routeDraftId = computed (() => typeof route . params . draftId === 'string' ? route . params . draftId : '' )
2026-09-18 16:48:35 +08:00
const routeDetailId = computed (() => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
2026-09-20 10:45:31 +08:00
const routeCopySourceId = computed (() => typeof route . query . copyFrom === 'string' ? route . query . copyFrom : '' )
const routeCopySourceIsDraft = computed (() => route . query . copyDraft === '1' )
2026-08-04 10:31:53 +08:00
const canViewCompanyProfile = computed (() => auth . hasPermission ( 'company:profile:view' ))
const companyAuditLoaded = ref ( false )
const companySubmissionBlocked = computed (() => companyAuditLoaded . value && tenantStore . company . auditStatus !== 'ACTIVE' )
2026-07-30 17:59:42 +08:00
const canCreateOrder = computed (() => auth . hasPermission ( 'order:first-leg:create' ))
const canEditOrder = computed (() => auth . hasPermission ( 'order:first-leg:edit' ))
2026-09-14 17:18:56 +08:00
const canSubmitOrder = computed (() => auth . hasPermission ( 'order:first-leg:submit' ))
2026-09-03 17:49:09 +08:00
const canConfirmInboundFee = computed (() => auth . hasPermission ( 'order:first-leg:edit' ))
2026-08-04 11:14:29 +08:00
const availableWarehouses = computed (() => store . warehouses . filter (( warehouse ) => warehouse . warehouseType === 'DOMESTIC' ))
const availableDestinationWarehouses = computed (() => store . warehouses . filter (( warehouse ) => warehouse . warehouseType === form . destinationType ))
2026-09-18 16:48:35 +08:00
const filters = ref < QueryModel >({ receivingWarehouseId : '' , channelName : '' , cargoKeyword : '' , createdRange : [], status : '' , destinationType : '' , trackingNumber : '' })
2026-08-01 12:17:33 +08:00
const appliedFilters = ref < QueryModel >({ ... filters . value })
2026-08-13 17:12:05 +08:00
const queryFields = computed < QueryField [] >(() => [
2026-09-18 16:48:35 +08:00
{ key : 'cargoKeyword' , label : '入仓码 / SKU' , type : 'input' , placeholder : '请输入入仓码或 SKU' , props : { 'aria-label' : '入仓码或 SKU' } },
2026-08-13 17:12:05 +08:00
{ 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 : [... new Set ( store . orders . map (( item ) => item . channelName ). filter ( Boolean ))]. 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 . 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' : '快递单号' } },
])
2026-08-07 14:47:00 +08:00
const channelOptions = ref < MerchantPricingChannel [] >([])
2026-08-18 11:32:58 +08:00
const inboundCodePolicy = ref < InboundCodePolicy | null >( null )
const manualInboundCodeRequired = computed (() => inboundCodePolicy . value ? . mode === 'LEGACY_MANUAL' )
2026-08-07 14:47:00 +08:00
const channelLoading = ref ( false )
const channelError = ref ( '' )
const priceEstimate = ref < MerchantPriceEstimate | null >( null )
const priceEstimateLoading = ref ( false )
const priceEstimateError = ref ( '' )
2026-08-12 17:46:46 +08:00
const selectedCargoIds = ref < string [] >([])
2026-09-01 19:34:37 +08:00
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 )
2026-09-01 20:38:12 +08:00
const batchErrorReportDownloading = ref ( false )
2026-09-01 19:34:37 +08:00
const batchImportResult = ref < MerchantBatchImportResponse | null >( null )
2026-09-01 20:38:12 +08:00
const batchImportError = ref ( '' )
2026-08-13 17:12:05 +08:00
const fixedOrderColumns : DataTableColumn [] = [
{ key : 'selection' , type : 'selection' , width : 48 , fixed : 'left' , reserveSelection : true },
2026-09-15 18:17:21 +08:00
{ key : 'actions' , label : '操作' , width : 284 , fixed : 'left' , align : 'left' , headerAlign : 'left' , slotName : 'actions' },
2026-09-18 16:48:35 +08:00
{ key : 'inboundCode' , label : '入仓码' , width : 230 , fixed : 'left' , align : 'left' , headerAlign : 'left' , showOverflowTooltip : true , slotName : 'inboundCode' },
2026-08-01 12:17:33 +08:00
]
2026-08-13 17:12:05 +08:00
const selectableOrderColumns : DataTableColumn [] = [
2026-09-15 18:28:58 +08:00
{ key : 'status' , label : '当前订单状态' , width : 126 , align : 'center' , headerAlign : 'center' , slotName : 'status' },
2026-08-13 17:12:05 +08:00
{ key : 'channel' , label : '渠道' , width : 150 , align : 'left' , headerAlign : 'left' , showOverflowTooltip : true , slotName : 'channel' },
2026-08-17 19:57:35 +08:00
{ 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' },
2026-08-17 19:13:41 +08:00
{ key : 'receivingWarehouse' , label : '国内收货仓' , width : 150 , align : 'left' , headerAlign : 'left' , showOverflowTooltip : true , slotName : 'receivingWarehouse' },
2026-08-13 17:12:05 +08:00
{ key : 'destinationWarehouse' , label : '目的仓' , width : 120 , align : 'left' , headerAlign : 'left' , showOverflowTooltip : true , slotName : 'destinationWarehouse' },
2026-08-17 19:57:35 +08:00
{ key : 'destinationType' , label : '目的仓类型' , width : 112 , align : 'center' , headerAlign : 'center' , slotName : 'destinationType' },
2026-08-13 17:12:05 +08:00
{ 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' },
2026-08-17 19:13:41 +08:00
{ 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 : 'sailingAt' , label : '预计船期' , width : 164 , align : 'center' , headerAlign : 'center' , slotName : 'sailingAt' },
2026-08-13 17:12:05 +08:00
{ key : 'createdAt' , label : '创建时间' , width : 164 , align : 'center' , headerAlign : 'center' , slotName : 'createdAt' },
2026-08-17 19:13:41 +08:00
{ key : 'updatedAt' , label : '更新时间' , width : 164 , align : 'center' , headerAlign : 'center' , slotName : 'updatedAt' },
2026-08-13 17:12:05 +08:00
]
const allSelectableColumnKeys = selectableOrderColumns . map (( column ) => String ( column . key ))
2026-09-18 16:48:35 +08:00
const defaultVisibleColumnKeys = [ 'status' , 'channel' , 'transportMode' , 'pickupMethod' , 'receivingWarehouse' , 'createdAt' ]
const visibleColumnKeys = ref < string [] >([... defaultVisibleColumnKeys ])
2026-08-13 17:12:05 +08:00
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 ]
2026-09-18 16:48:35 +08:00
visibleColumnKeys . value = [... defaultVisibleColumnKeys ]
2026-08-13 17:12:05 +08:00
}
2026-08-01 12:17:33 +08:00
const tablePage = ref ( 1 )
const tablePageSize = ref ( 10 )
2026-08-13 17:12:05 +08:00
const downloadingMarks = ref ( false )
const exportingOrders = ref ( false )
2026-09-14 17:18:56 +08:00
const batchSubmitting = ref ( false )
2026-08-13 17:12:05 +08:00
const selectedOrders = ref < FirstLegOrder [] >([])
const selectedOrderCount = computed (() => selectedOrders . value . length )
2026-09-14 17:18:56 +08:00
const selectedDraftOrders = computed (() => selectedOrders . value . filter (( order ) => order . status === 'DRAFT' ))
2026-08-13 17:12:05 +08:00
const orderTableVersion = ref ( 0 )
2026-09-19 17:02:11 +08:00
type OrderQueue = 'all' | 'fee' | 'customs' | 'draft'
2026-09-18 16:48:35 +08:00
const activeOrderQueue = ref < OrderQueue >( 'all' )
2026-07-30 17:59:42 +08:00
const editorOpen = ref ( false )
2026-08-01 12:17:33 +08:00
const orderFormRef = ref < FormInstance >()
2026-07-30 17:59:42 +08:00
const editingId = ref < string >()
2026-07-30 21:23:06 +08:00
const openingEditor = ref ( false )
2026-09-20 10:45:31 +08:00
const copyingOrderId = ref ( '' )
2026-07-30 17:59:42 +08:00
const detailId = ref < string >()
const saving = ref ( false )
const detailLoading = ref ( false )
const detailError = ref ( '' )
2026-08-13 17:12:05 +08:00
const detailCargoPage = ref ( 1 )
const detailCargoPageSize = ref ( 5 )
2026-08-04 21:01:34 +08:00
const inboundBilling = ref < MerchantInboundBilling | null >( null )
const inboundBillingLoading = ref ( false )
const inboundBillingUnavailable = ref ( false )
2026-09-03 10:46:14 +08:00
const inboundBillingUnavailableMessage = ref ( '还未测量推送' )
2026-08-04 21:01:34 +08:00
const inboundBillingError = ref ( '' )
2026-09-04 12:00:33 +08:00
const inboundBillingOrderId = ref ( '' )
const feeConfirmationOpen = ref ( false )
2026-08-28 16:54:16 +08:00
const customsByOrder = reactive < Record < string , MerchantCustomsContainer | null | undefined > > ({})
const customsEligibilityPending = new Set < string >()
const customsDialogOpen = ref ( false )
const customsDialogOrderId = ref ( '' )
2026-08-30 17:35:52 +08:00
const inboundAssociationOpen = ref ( false )
const inboundAssociationCode = ref ( '' )
2026-07-31 15:34:22 +08:00
const supportingDataLoading = ref ( false )
const supportingDataErrors = ref < string [] >([])
2026-08-06 14:55:13 +08:00
const destinationWarehousesLoading = ref ( false )
2026-08-06 15:32:07 +08:00
const pageDataLoading = computed (() => supportingDataLoading . value || ( ! isCreatePage . value && store . loading ))
2026-07-30 17:59:42 +08:00
const editorBaseline = ref ( '' )
let cargoSequence = 0
2026-09-11 16:50:23 +08:00
let packingGroupSequence = 0
2026-08-04 21:01:34 +08:00
let inboundBillingRequestSequence = 0
2026-08-07 14:47:00 +08:00
let channelRequestSequence = 0
let estimateRequestSequence = 0
let estimateTimer : ReturnType < typeof setTimeout > | undefined
2026-07-30 17:59:42 +08:00
2026-09-01 19:34:37 +08:00
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' ))
2026-08-04 11:14:29 +08:00
2026-09-01 19:34:37 +08:00
function addressLabel ( address : CustomerOrderAddress ) {
return ` ${ address . addressName } · ${ address . contactName } · ${ address . countryCode } ${ address . city } ${ address . addressLine } `
2026-08-04 11:14:29 +08:00
}
2026-08-01 12:17:33 +08:00
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' }],
2026-08-07 14:47:00 +08:00
channelCode : [{ required : true , message : '请选择运输渠道' , trigger : 'change' }],
2026-08-01 12:17:33 +08:00
channelName : [{ required : true , message : '请输入渠道名称' , trigger : 'blur' }],
2026-08-07 14:47:00 +08:00
pricingTaxMode : [{ required : true , message : '请选择报价包税方式' , trigger : 'change' }],
2026-08-01 12:17:33 +08:00
pickupMethod : [{ required : true , message : '请选择提货方式' , trigger : 'change' }],
taxMode : [{ required : true , message : '请选择报关模式' , trigger : 'change' }],
destinationType : [{ required : true , message : '请选择目的仓类型' , trigger : 'change' }],
2026-08-04 12:01:05 +08:00
}
function createEmptyPartySnapshot ( role : FirstLegPartyRole ) : FirstLegPartySnapshot {
return {
id : '' ,
code : '' ,
name : '' ,
role ,
contactName : '' ,
phone : '' ,
email : '' ,
countryRegion : role === 'SHIPPER' ? 'CN' : 'US' ,
province : '' ,
city : '' ,
postalCode : '' ,
address : '' ,
}
2026-08-01 12:17:33 +08:00
}
2026-08-04 12:58:04 +08:00
function createPickupPartySnapshot () : FirstLegPartySnapshot {
return {
... createEmptyPartySnapshot ( 'SHIPPER' ),
name : tenantStore . company . companyName ,
contactName : tenantStore . company . contactName ,
phone : tenantStore . company . contactPhone ,
email : tenantStore . company . contactEmail ,
}
}
2026-08-01 12:17:33 +08:00
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' }]
}
2026-07-30 21:23:06 +08:00
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 ,
})
}
2026-08-13 17:12:05 +08:00
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 ()) } `
}
2026-08-06 20:14:54 +08:00
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 ) {
2026-08-30 17:35:52 +08:00
return value ? . trim () || '—'
2026-08-06 20:14:54 +08:00
}
2026-07-30 17:59:42 +08:00
function emptyDraft () : FirstLegOrderDraft {
return {
2026-08-18 11:32:58 +08:00
inboundCode : '' ,
2026-07-30 17:59:42 +08:00
referenceNo : '' ,
transportMode : 'FCL' ,
businessType : 'EXPORT' ,
2026-09-02 10:43:51 +08:00
fulfillmentType : 'STANDARD' ,
2026-09-06 15:53:56 +08:00
lxCode : '' ,
2026-07-30 17:59:42 +08:00
origin : '' ,
destination : '' ,
receivingWarehouseId : '' ,
expectedDepartureDate : '' ,
shipperPartyId : '' ,
2026-09-01 19:34:37 +08:00
senderAddressId : '' ,
2026-07-30 17:59:42 +08:00
shipperPartySnapshot : null ,
shipper : '' ,
consigneePartyId : '' ,
2026-09-01 19:34:37 +08:00
privateDestinationAddressId : '' ,
2026-07-30 17:59:42 +08:00
consigneePartySnapshot : null ,
consignee : '' ,
priority : 'NORMAL' ,
customerRemark : '' ,
tradeTerm : 'FOB' ,
destinationCountryCode : 'US' ,
2026-08-04 11:14:29 +08:00
destinationWarehouseId : '' ,
2026-07-30 17:59:42 +08:00
destinationWarehouseName : '' ,
pickupMethod : 'SELF_EXPRESS' ,
2026-08-07 14:47:00 +08:00
channelCode : '' ,
channelName : '' ,
pricingTaxMode : 'TAX_INCLUDED' ,
2026-07-30 17:59:42 +08:00
taxMode : 'BUY_DECLARATION' ,
2026-09-01 19:34:37 +08:00
customsInspection : null ,
2026-07-30 17:59:42 +08:00
destinationType : 'FBA' ,
amazonWarehouseCode : '' ,
shipmentId : '' ,
items : [ createEmptyCargoItem ( `new-cargo- ${ ++ cargoSequence } ` )],
}
}
2026-08-13 09:48:51 +08:00
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.
}
return draft
}
const form = reactive < FirstLegOrderDraft >( quickOrderDraft ())
2026-07-30 18:08:40 +08:00
const serializedDraft = computed (() => JSON . stringify ( form ))
2026-08-03 17:44:42 +08:00
const editorDirty = computed (() => ( editorOpen . value || isCreatePage . value ) && serializedDraft . value !== editorBaseline . value )
2026-09-01 20:38:12 +08:00
const woodenCargoLines = computed (() => form . items
. map (( item , index ) => item . woodenProduct ? index + 1 : null )
. filter (( index ) : index is number => index != null ))
2026-08-05 15:55:02 +08:00
const unsavedRouteKey = props . createPage ? 'first-leg-order-create' : 'first-leg-orders'
useUnsavedWorkRegistration ({
key : `first-leg-order-editor: ${ unsavedRouteKey } ` ,
routeKey : unsavedRouteKey ,
2026-08-06 13:56:13 +08:00
label : props . createPage ? '创建订单' : '订单编辑' ,
2026-08-05 15:55:02 +08:00
dirty : editorDirty ,
})
2026-07-30 17:59:42 +08:00
2026-09-18 16:48:35 +08:00
function matchesAppliedFilters ( order : FirstLegOrder ) {
2026-08-01 12:17:33 +08:00
const statusValue = appliedFilters . value . status
const createdRangeValue = appliedFilters . value . createdRange
2026-08-13 17:12:05 +08:00
const textValue = ( key : string ) => typeof appliedFilters . value [ key ] === 'string' ? String ( appliedFilters . value [ key ]). trim (). toLowerCase () : ''
const receivingWarehouseId = textValue ( 'receivingWarehouseId' )
const channelName = textValue ( 'channelName' )
const cargoKeyword = textValue ( 'cargoKeyword' )
const destinationType = textValue ( 'destinationType' )
const trackingNumber = textValue ( 'trackingNumber' )
2026-08-01 12:17:33 +08:00
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
const createdRange = Array . isArray ( createdRangeValue ) ? createdRangeValue . filter (( value ) : value is string => typeof value === 'string' ) : []
2026-08-13 17:12:05 +08:00
const matchesWarehouse = ! receivingWarehouseId || order . receivingWarehouseId . toLowerCase () === receivingWarehouseId
const matchesChannel = ! channelName || order . channelName . toLowerCase () === channelName
const matchesCargo = ! cargoKeyword || [ order . businessSummary . inboundCode , ... order . skuNos , ... order . items . map (( item ) => item . sku )]
. filter ( Boolean ). some (( value ) => String ( value ). toLowerCase (). includes ( cargoKeyword ))
const matchesDestinationType = ! destinationType || order . destinationType . toLowerCase () === destinationType
const matchesTrackingNumber = ! trackingNumber || order . trackingNumber . toLowerCase (). includes ( trackingNumber )
2026-08-01 12:17:33 +08:00
const matchesStatus = ! status || order . status === status
const matchesDate = createdRange . length !== 2 || ( order . createdAt . slice ( 0 , 10 ) >= createdRange [ 0 ] && order . createdAt . slice ( 0 , 10 ) <= createdRange [ 1 ])
2026-09-18 16:48:35 +08:00
return matchesWarehouse && matchesChannel && matchesCargo && matchesDestinationType && matchesTrackingNumber && matchesStatus && matchesDate
}
const queryMatchedOrders = computed (() => store . orders . filter ( matchesAppliedFilters ))
const orderQueueCounts = computed (() => ({
all : queryMatchedOrders . value . length ,
fee : queryMatchedOrders . value . filter (( order ) => order . businessSummary . customerFeeConfirmationRequired ). length ,
customs : queryMatchedOrders . value . filter (( order ) => Boolean ( eligibleCustomsDetail ( order ))). length ,
2026-09-19 17:02:11 +08:00
draft : queryMatchedOrders . value . filter (( order ) => order . status === 'DRAFT' ). length ,
2026-09-18 16:48:35 +08:00
}))
const filteredOrders = computed (() => queryMatchedOrders . value . filter (( order ) => {
if ( activeOrderQueue . value === 'fee' ) return order . businessSummary . customerFeeConfirmationRequired
if ( activeOrderQueue . value === 'customs' ) return Boolean ( eligibleCustomsDetail ( order ))
2026-09-19 17:02:11 +08:00
if ( activeOrderQueue . value === 'draft' ) return order . status === 'DRAFT'
2026-09-18 16:48:35 +08:00
return true
2026-07-30 17:59:42 +08:00
}))
2026-08-01 12:17:33 +08:00
const pagedOrders = computed (() => {
const start = ( tablePage . value - 1 ) * tablePageSize . value
return filteredOrders . value . slice ( start , start + tablePageSize . value )
})
2026-07-30 17:59:42 +08:00
2026-09-18 16:48:35 +08:00
function selectOrderQueue ( queue : OrderQueue ) {
activeOrderQueue . value = queue
tablePage . value = 1
selectedOrders . value = []
orderTableVersion . value += 1
}
2026-08-28 16:54:16 +08:00
function eligibleCustomsDetail ( order : FirstLegOrder ) {
const value = customsByOrder [ order . id ]
if ( order . status !== 'LOADED' || ! 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 ( order . status !== 'LOADED' || 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
}
2026-08-30 17:35:52 +08:00
function openInboundCodeAssociation ( inboundCode : string ) {
const code = inboundCode . trim ()
if ( ! code ) return
inboundAssociationCode . value = code
inboundAssociationOpen . value = true
}
2026-08-28 16:54:16 +08:00
function updateCustomsDetail ( value : MerchantCustomsContainer ) {
if ( customsDialogOrderId . value ) customsByOrder [ customsDialogOrderId . value ] = value
}
2026-07-30 17:59:42 +08:00
const detailOrder = computed (() => detailId . value ? store . getById ( detailId . value ) : undefined )
const detailSummary = computed (() => detailOrder . value ? summarizeFirstLegCargo ( detailOrder . value . items ) : null )
2026-08-13 17:12:05 +08:00
const pagedDetailCargoItems = computed (() => {
const items = detailOrder . value ? . items ?? []
const start = ( detailCargoPage . value - 1 ) * detailCargoPageSize . value
return items . slice ( start , start + detailCargoPageSize . value )
})
2026-08-06 13:56:13 +08:00
const progressSummaryItems = computed (() => {
const order = detailOrder . value
if ( ! order ) 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
2026-08-10 12:05:43 +08:00
const transportReference = [ summary . vesselName , summary . voyageNo ]. filter ( Boolean ). join ( ' · ' )
2026-08-06 13:56:13 +08:00
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 },
2026-09-18 16:48:35 +08:00
{ label : '关联柜次' , value : summary . cabinetNo || summary . containerNo || '—' },
2026-08-10 12:05:43 +08:00
{ label : '船期摘要' , value : transportReference || '待更新' },
2026-08-06 13:56:13 +08:00
{ label : '计费重量' , value : formatMetric ( summary . chargeableWeightKg , 'kg' ) },
2026-08-10 12:05:43 +08:00
{ label : '后续履约' , value : summary . deliveryTrackingNo || ( summary . deliveryCompletedAt ? `海外回传于 ${ formatDateTime ( summary . deliveryCompletedAt ) } ` : '待海外同步' ) },
2026-08-06 13:56:13 +08:00
]
})
2026-08-10 12:05:43 +08:00
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 , status : summary . loadingStatus || '待更新' , vesselName : summary . vesselName , voyageNo : summary . voyageNo , sailingAt : summary . sailingAt }]
})
2026-07-30 17:59:42 +08:00
const selectedWarehouse = computed (() => availableWarehouses . value . find (( item ) => item . id === form . receivingWarehouseId ))
2026-08-04 12:58:04 +08:00
const selectedDestinationWarehouse = computed (() => store . warehouses . find (( item ) => item . id === form . destinationWarehouseId && item . warehouseType === form . destinationType ))
2026-08-07 14:47:00 +08:00
const selectedPricingChannel = computed (() => channelOptions . value . find (( item ) => item . channelCode === form . channelCode ))
const availablePricingTaxModes = computed (() => selectedPricingChannel . value ? . pricingTaxModes ?? [])
2026-08-01 12:17:33 +08:00
const hasActiveFilters = computed (() => {
const values = appliedFilters . value
2026-08-13 17:12:05 +08:00
return Object . values ( values ). some (( value ) => Array . isArray ( value ) ? value . length > 0 : Boolean ( typeof value === 'string' ? value . trim () : value ))
2026-08-01 12:17:33 +08:00
})
2026-08-06 13:56:13 +08:00
const emptyDescription = computed (() => hasActiveFilters . value ? '没有符合筛选条件的订单' : '还没有订单' )
2026-07-30 17:59:42 +08:00
2026-07-30 18:08:40 +08:00
async function loadPageData () {
2026-07-31 15:34:22 +08:00
supportingDataLoading . value = true
supportingDataErrors . value = []
2026-08-04 10:31:53 +08:00
companyAuditLoaded . value = false
const companyRequest = canViewCompanyProfile . value
2026-07-31 15:34:22 +08:00
? tenantStore . loadCompanyContext ()
: Promise . resolve ()
2026-08-06 14:55:13 +08:00
const destinationRequest = isCreatePage . value && form . destinationType !== 'PRIVATE'
? loadDestinationWarehouses ( form . destinationType )
: Promise . resolve ()
2026-09-18 16:48:35 +08:00
const orderPageRequest = isCreatePage . value || isDetailPage . value
2026-08-06 15:32:07 +08:00
? store . loadWarehouseType ( 'DOMESTIC' , true )
2026-08-25 16:15:38 +08:00
: store . load ({ includeDrafts : true })
2026-09-01 19:34:37 +08:00
const addressRequest = isCreatePage . value ? loadOrderAddresses () : Promise . resolve ()
const [ ordersResult , companyResult , destinationResult , policyResult , addressResult ] = await Promise . allSettled ([
2026-08-06 15:32:07 +08:00
orderPageRequest ,
2026-07-31 15:34:22 +08:00
companyRequest ,
2026-08-06 14:55:13 +08:00
destinationRequest ,
2026-08-18 11:32:58 +08:00
getMerchantInboundCodePolicy (),
2026-09-01 19:34:37 +08:00
addressRequest ,
2026-07-31 15:34:22 +08:00
])
2026-08-18 11:32:58 +08:00
if ( policyResult . status === 'fulfilled' ) inboundCodePolicy . value = policyResult . value
else {
inboundCodePolicy . value = null
supportingDataErrors . value . push ( `入仓码配置加载失败: ${ errorMessage ( policyResult . reason ) } ` )
}
2026-08-04 10:31:53 +08:00
if ( canViewCompanyProfile . value && companyResult . status === 'fulfilled' ) {
companyAuditLoaded . value = true
} else if ( companyResult . status === 'rejected' ) {
2026-07-31 15:34:22 +08:00
supportingDataErrors . value . push ( `企业资料加载失败: ${ errorMessage ( companyResult . reason ) } ` )
}
2026-08-06 15:32:07 +08:00
if ( ! isCreatePage . value && ordersResult . status === 'rejected' && ! store . loadError ) supportingDataErrors . value . push ( `订单加载失败: ${ errorMessage ( ordersResult . reason ) } ` )
2026-08-04 11:14:29 +08:00
if ( store . warehouseLoadError ) supportingDataErrors . value . push ( `公共仓库加载失败: ${ store . warehouseLoadError } ` )
2026-08-06 18:02:02 +08:00
if ( ! isCreatePage . value && store . draftLoadError ) supportingDataErrors . value . push ( `订单草稿加载失败: ${ store . draftLoadError } ` )
2026-08-06 14:55:13 +08:00
if ( destinationResult . status === 'rejected' ) supportingDataErrors . value . push ( `目的仓加载失败: ${ errorMessage ( destinationResult . reason ) } ` )
2026-09-01 19:34:37 +08:00
if ( addressResult . status === 'rejected' ) supportingDataErrors . value . push ( `企业地址簿加载失败: ${ errorMessage ( addressResult . reason ) } ` )
2026-08-04 12:58:04 +08:00
await autofillWarehouseSelections ()
2026-08-13 09:48:51 +08:00
if ( isCreatePage . value && routeDraftId . value ) await loadRouteDraft ( routeDraftId . value )
2026-09-20 10:45:31 +08:00
else if ( isCreatePage . value && routeCopySourceId . value ) await loadCopiedOrder ( routeCopySourceId . value , routeCopySourceIsDraft . value )
2026-09-18 16:48:35 +08:00
if ( isDetailPage . value && routeDetailId . value ) await loadDetailPage ( routeDetailId . value )
2026-07-31 15:34:22 +08:00
supportingDataLoading . value = false
}
2026-08-13 09:48:51 +08:00
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
replaceForm ( draft )
await Promise . all ( form . items . map ( ensureProductImagePreview ))
if ( form . receivingWarehouseId ) await loadSelectedWarehouse ( form . receivingWarehouseId )
resetEditorValidation ()
markEditorClean ()
}
2026-09-20 10:45:31 +08:00
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 ) } ` )
}
}
2026-08-06 14:55:13 +08:00
async function loadDestinationWarehouses ( type : 'FBA' | 'OVERSEAS' | 'PRIVATE' ) {
if ( type === 'PRIVATE' ) return
destinationWarehousesLoading . value = true
try {
await store . loadWarehouseType ( type )
} finally {
destinationWarehousesLoading . value = false
}
}
2026-07-31 15:34:22 +08:00
function errorMessage ( error : unknown ) {
return error instanceof Error ? error . message : '服务暂时不可用'
2026-07-30 18:08:40 +08:00
}
2026-09-01 20:38:12 +08:00
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 ( ' · ' )
}
2026-07-30 18:08:40 +08:00
onMounted (() => {
void loadPageData ()
2026-07-30 17:59:42 +08:00
})
2026-08-25 16:11:00 +08:00
watch (() => auth . isAuthenticated , ( authenticated , wasAuthenticated ) => {
if ( authenticated && ! wasAuthenticated && ! isCreatePage . value ) void loadPageData ()
})
2026-08-03 17:44:42 +08:00
watch ( isCreatePage , ( isCreate , wasCreate ) => {
if ( ! isCreate || wasCreate ) return
2026-08-13 09:48:51 +08:00
if ( routeDraftId . value ) return
2026-08-03 17:44:42 +08:00
editingId . value = undefined
2026-08-13 09:48:51 +08:00
replaceForm ( quickOrderDraft ())
2026-08-03 17:44:42 +08:00
resetEditorValidation ()
2026-09-17 14:26:41 +08:00
// The list and create routes reuse this component, so onMounted does not run again.
if ( wasCreate === false ) void loadPageData ()
2026-08-03 17:44:42 +08:00
}, { immediate : true })
2026-09-18 16:48:35 +08:00
watch ( routeDetailId , ( orderId , previousOrderId ) => {
if ( ! isDetailPage . value || ! orderId || orderId === previousOrderId ) return
void loadDetailPage ( orderId )
2026-08-04 21:01:34 +08:00
})
2026-09-04 12:00:33 +08:00
watch ( feeConfirmationOpen , ( open ) => {
2026-09-18 16:48:35 +08:00
if ( ! open && ! isDetailPage . value ) resetInboundBillingState ()
2026-09-04 12:00:33 +08:00
})
2026-08-04 21:01:34 +08:00
onBeforeUnmount (() => {
resetInboundBillingState ()
2026-08-07 14:47:00 +08:00
channelRequestSequence += 1
estimateRequestSequence += 1
if ( estimateTimer ) clearTimeout ( estimateTimer )
2026-08-07 18:44:58 +08:00
releaseProductImagePreviews ( form . items )
if ( detailOrder . value ) releaseProductImagePreviews ( detailOrder . value . items )
2026-08-04 21:01:34 +08:00
})
2026-07-30 18:08:40 +08:00
function markEditorClean () {
editorBaseline . value = serializedDraft . value
}
2026-08-01 12:17:33 +08:00
function resetEditorValidation () {
void nextTick (() => orderFormRef . value ? . clearValidate ())
}
2026-07-30 21:23:06 +08:00
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 })),
}
}
2026-07-30 17:59:42 +08:00
function replaceForm ( draft : FirstLegOrderDraft ) {
2026-08-07 18:44:58 +08:00
releaseProductImagePreviews ( form . items )
2026-07-30 21:23:06 +08:00
Object . assign ( form , cloneDraft ( draft ))
2026-07-30 18:08:40 +08:00
markEditorClean ()
2026-07-30 17:59:42 +08:00
}
2026-08-07 18:44:58 +08:00
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 = ''
}
2026-09-18 09:15:15 +08:00
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 )
}
2026-07-30 17:59:42 +08:00
function draftFromOrder ( order : FirstLegOrder ) : FirstLegOrderDraft {
return {
2026-08-18 11:32:58 +08:00
inboundCode : order . businessSummary . inboundCode ,
2026-07-30 17:59:42 +08:00
referenceNo : order . referenceNo ,
2026-08-06 13:56:13 +08:00
transportMode : order . transportMode ?? 'FCL' ,
2026-07-30 17:59:42 +08:00
businessType : order . businessType ,
2026-09-06 15:53:56 +08:00
fulfillmentType : order . fulfillmentType ?? 'STANDARD' ,
lxCode : order . lxCode ?? '' ,
2026-07-30 17:59:42 +08:00
origin : order . origin ,
destination : order . destination ,
receivingWarehouseId : order . receivingWarehouseId ,
expectedDepartureDate : order . expectedDepartureDate ,
shipperPartyId : order . shipperPartyId ,
2026-09-18 09:15:15 +08:00
senderAddressId : findAddressBookEntryId ( 'PICKUP' , order . shipperPartySnapshot ),
2026-07-30 21:23:06 +08:00
shipperPartySnapshot : order . shipperPartySnapshot ? { ... order . shipperPartySnapshot } : null ,
2026-07-30 17:59:42 +08:00
shipper : order . shipper ,
consigneePartyId : order . consigneePartyId ,
2026-09-18 09:15:15 +08:00
privateDestinationAddressId : findAddressBookEntryId ( 'PRIVATE_DESTINATION' , order . consigneePartySnapshot ),
2026-07-30 21:23:06 +08:00
consigneePartySnapshot : order . consigneePartySnapshot ? { ... order . consigneePartySnapshot } : null ,
2026-07-30 17:59:42 +08:00
consignee : order . consignee ,
priority : order . priority ,
customerRemark : order . customerRemark ,
tradeTerm : order . tradeTerm ,
destinationCountryCode : order . destinationCountryCode ,
2026-08-04 11:14:29 +08:00
destinationWarehouseId : order . destinationWarehouseId ,
2026-07-30 17:59:42 +08:00
destinationWarehouseName : order . destinationWarehouseName ,
pickupMethod : order . pickupMethod ,
channelCode : order . channelCode ,
channelName : order . channelName ,
2026-08-07 14:47:00 +08:00
pricingTaxMode : order . pricingTaxMode ,
2026-07-30 17:59:42 +08:00
taxMode : order . taxMode ,
customsInspection : order . customsInspection ,
destinationType : order . destinationType ,
amazonWarehouseCode : order . amazonWarehouseCode ,
shipmentId : order . shipmentId ,
2026-08-07 18:44:58 +08:00
items : order . items . map (( item ) => ({
... item ,
productImagePreviewUrl : '' ,
productImageUploading : false ,
productImageError : '' ,
})),
2026-07-30 17:59:42 +08:00
}
}
2026-09-20 10:45:31 +08:00
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 : '' ,
})),
}
}
2026-08-03 15:50:24 +08:00
function selectChannel ( code : string ) {
2026-08-07 14:47:00 +08:00
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'
}
2026-08-03 15:50:24 +08:00
}
2026-08-07 14:47:00 +08:00
function pricingTaxModeLabel ( mode : 'TAX_INCLUDED' | 'TAX_EXCLUDED' ) {
return mode === 'TAX_INCLUDED' ? '包税' : '不包税'
}
2026-08-19 09:14:38 +08:00
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 || '仅供参考,最终以实际结算为准' )
2026-08-21 09:59:58 +08:00
const priceEstimateBreakdown = computed (() => {
if ( ! priceEstimate . value ) return null
const summary = summarizeFirstLegCargo ( form . items )
return createPriceEstimateBreakdown ( summary . grossWeightKg , summary . volumeCbm , priceEstimate . value . chargeUnit )
})
2026-08-19 09:14:38 +08:00
2026-08-07 14:47:00 +08:00
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 ,
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 () {
2026-08-19 09:14:38 +08:00
estimateRequestSequence += 1
priceEstimate . value = null
priceEstimateError . value = ''
priceEstimateLoading . value = false
2026-08-07 14:47:00 +08:00
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 ,
)
2026-08-04 12:58:04 +08:00
async function selectDestinationWarehouse ( id : string ) {
2026-08-04 11:14:29 +08:00
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 : ''
2026-08-04 12:58:04 +08:00
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 : '目的仓详情加载失败' )
}
2026-08-04 11:14:29 +08:00
}
2026-08-04 12:58:04 +08:00
async function changeDestinationType () {
2026-08-04 11:14:29 +08:00
form . destinationWarehouseId = ''
form . destinationWarehouseName = ''
2026-08-04 12:01:05 +08:00
form . destination = ''
2026-08-04 11:14:29 +08:00
form . amazonWarehouseCode = ''
2026-09-01 19:34:37 +08:00
form . privateDestinationAddressId = ''
2026-08-04 12:01:05 +08:00
form . consigneePartyId = ''
form . consigneePartySnapshot = form . destinationType === 'PRIVATE' ? createEmptyPartySnapshot ( 'CONSIGNEE' ) : null
form . consignee = ''
2026-08-06 14:55:13 +08:00
try {
await loadDestinationWarehouses ( form . destinationType )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '目的仓数据加载失败' )
}
2026-08-04 12:58:04 +08:00
if ( form . destinationType !== 'PRIVATE' && availableDestinationWarehouses . value . length === 1 ) {
await selectDestinationWarehouse ( availableDestinationWarehouses . value [ 0 ]. id )
}
2026-08-04 11:14:29 +08:00
resetEditorValidation ()
}
2026-08-04 12:01:05 +08:00
function changePickupMethod () {
if ( form . pickupMethod === 'PICKUP' ) {
2026-08-04 12:58:04 +08:00
form . shipperPartySnapshot ??= createPickupPartySnapshot ()
2026-08-04 12:01:05 +08:00
} else {
2026-09-01 19:34:37 +08:00
form . senderAddressId = ''
2026-08-04 12:01:05 +08:00
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
}
2026-09-18 09:15:15 +08:00
function selectPrivateAddress ( id : string | null | undefined ) {
2026-09-01 19:34:37 +08:00
const address = privateAddresses . value . find (( item ) => String ( item . id ) === id )
2026-09-18 09:15:15 +08:00
if ( ! address ) {
form . privateDestinationAddressId = ''
form . consigneePartyId = ''
form . consigneePartySnapshot = createEmptyPartySnapshot ( 'CONSIGNEE' )
form . consignee = ''
form . destination = ''
form . destinationCountryCode = ''
form . destinationWarehouseId = ''
form . destinationWarehouseName = ''
return
}
2026-09-01 19:34:37 +08:00
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
2026-08-04 11:14:29 +08:00
form . destinationWarehouseId = ''
2026-09-01 19:34:37 +08:00
form . destinationWarehouseName = address . addressName
2026-08-04 11:14:29 +08:00
}
2026-09-01 19:34:37 +08:00
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
}
}
2026-09-18 09:15:15 +08:00
function selectPickupAddress ( id : string | null | undefined ) {
2026-09-01 19:34:37 +08:00
const address = pickupAddresses . value . find (( item ) => String ( item . id ) === id )
2026-09-18 09:15:15 +08:00
if ( ! address ) {
form . senderAddressId = ''
form . shipperPartyId = ''
form . shipperPartySnapshot = createEmptyPartySnapshot ( 'SHIPPER' )
form . shipper = ''
form . origin = ''
return
}
2026-09-01 19:34:37 +08:00
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 ( '请先完整填写提货地址和联系人资料' )
2026-08-04 11:14:29 +08:00
return
}
2026-09-18 09:15:15 +08:00
const existingAddressId = findAddressBookEntryId ( 'PICKUP' , snapshot )
if ( existingAddressId && existingAddressId !== form . senderAddressId ) {
selectPickupAddress ( existingAddressId )
ElMessage . info ( '相同的企业提货地址已存在,已为您选中' )
return
}
2026-09-01 19:34:37 +08:00
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 ))
2026-08-04 11:14:29 +08:00
}
}
2026-09-01 19:34:37 +08:00
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
2026-09-18 09:15:15 +08:00
const existingAddressId = findAddressBookEntryId ( 'PRIVATE_DESTINATION' , snapshot )
if ( existingAddressId && existingAddressId !== form . privateDestinationAddressId ) {
selectPrivateAddress ( existingAddressId )
ElMessage . info ( '相同的企业私人仓地址已存在,已为您选中' )
return
}
2026-09-01 19:34:37 +08:00
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 )
2026-09-18 09:15:15 +08:00
if ( type === 'PICKUP' ) selectPickupAddress ( null )
else selectPrivateAddress ( null )
2026-09-01 19:34:37 +08:00
await loadOrderAddresses ()
ElMessage . success ( '地址已从企业地址簿停用' )
} catch ( error ) {
ElMessage . error ( errorMessage ( error ))
}
2026-08-03 15:50:24 +08:00
}
async function loadSelectedWarehouse ( id : string | number | boolean | undefined ) {
const warehouseId = typeof id === 'string' ? id : typeof id === 'number' ? String ( id ) : ''
2026-08-07 14:47:00 +08:00
if ( ! warehouseId ) {
channelRequestSequence += 1
channelOptions . value = []
channelError . value = ''
form . channelCode = ''
form . channelName = ''
resetPriceEstimate ()
return
}
const requests = [ loadChannelsForWarehouse ( warehouseId )]
2026-07-30 18:08:40 +08:00
try {
2026-08-07 14:47:00 +08:00
requests . push ( store . loadWarehouseDetail ( warehouseId ). then (() => undefined ))
await Promise . all ( requests )
2026-07-30 18:08:40 +08:00
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '仓库地址与收货时间加载失败' )
}
}
2026-08-04 12:58:04 +08:00
async function autofillWarehouseSelections () {
if ( ! isCreatePage . value || editingId . value ) return
if ( ! form . receivingWarehouseId && availableWarehouses . value . length === 1 ) {
form . receivingWarehouseId = availableWarehouses . value [ 0 ]. id
await loadSelectedWarehouse ( form . receivingWarehouseId )
}
if ( form . destinationType !== 'PRIVATE' && ! form . destinationWarehouseId && availableDestinationWarehouses . value . length === 1 ) {
await selectDestinationWarehouse ( availableDestinationWarehouses . value [ 0 ]. id )
}
markEditorClean ()
}
2026-09-15 18:20:34 +08:00
function getOrderRowActions ( row : object ) : TableAction [] {
const order = row as FirstLegOrder
const actions : TableAction [] = [
2026-09-15 19:46:06 +08:00
{ key : 'detail' , label : '详情' , run : () => openDetail ( order ) },
2026-09-15 18:20:34 +08:00
]
2026-09-20 10:45:31 +08:00
if ( canCreateOrder . value ) {
2026-09-20 11:23:35 +08:00
actions . push ({ key : 'copy' , label : '复制下单' , loading : copyingOrderId . value === order . id , run : () => copyOrder ( order ) })
2026-09-20 10:45:31 +08:00
}
2026-09-15 18:20:34 +08:00
if ( canConfirmInboundFee . value && order . businessSummary . customerFeeConfirmationRequired ) {
actions . push ({ key : 'fee' , label : '确认计费重' , run : () => openFeeConfirmation ( order ) })
}
if ( canEditOrder . value && canClientEditFirstLegOrder ( order . status )) {
2026-09-15 19:46:06 +08:00
actions . push ({ key : 'edit' , label : '编辑' , loading : openingEditor . value , run : () => openEdit ( order ) })
2026-09-15 18:20:34 +08:00
}
if ( eligibleCustomsDetail ( order )) {
2026-09-15 19:46:06 +08:00
actions . push ({ key : 'customs' , label : '报关资料' , run : () => openCustomsDocuments ( order ) })
2026-09-15 18:20:34 +08:00
}
return actions
}
2026-09-20 10:45:31 +08:00
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 = ''
}
}
2026-08-03 17:44:42 +08:00
async function openCreate () {
2026-07-30 17:59:42 +08:00
if ( availableWarehouses . value . length === 0 ) {
2026-08-06 18:02:02 +08:00
ElMessage . warning ( supportingDataLoading . value
? '平台收货仓正在加载,请稍候再试'
: '当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单' )
2026-07-30 17:59:42 +08:00
return
}
editingId . value = undefined
replaceForm ( emptyDraft ())
2026-08-01 12:17:33 +08:00
resetEditorValidation ()
2026-08-03 17:44:42 +08:00
await router . push ({ name : 'first-leg-order-create' })
2026-07-30 17:59:42 +08:00
}
2026-07-30 21:23:06 +08:00
async function openEdit ( order : FirstLegOrder ) {
if ( openingEditor . value || ! canEditOrder . value || ! canClientEditFirstLegOrder ( order . status )) return
openingEditor . value = true
try {
2026-09-01 19:34:37 +08:00
const [ detailedOrder ] = await Promise . all ([ store . loadDetail ( order . id ), loadOrderAddresses ()])
2026-07-30 21:23:06 +08:00
const draft = draftFromOrder ( detailedOrder )
2026-08-06 14:55:13 +08:00
if ( draft . destinationType !== 'PRIVATE' ) await loadDestinationWarehouses ( draft . destinationType )
2026-07-30 21:23:06 +08:00
const warehouseAvailable = availableWarehouses . value . some (( item ) => item . id === detailedOrder . receivingWarehouseId )
if ( ! warehouseAvailable ) {
draft . receivingWarehouseId = ''
2026-08-07 14:47:00 +08:00
draft . channelCode = ''
draft . channelName = ''
channelOptions . value = []
channelError . value = ''
resetPriceEstimate ()
2026-07-30 21:23:06 +08:00
ElMessage . warning ( '原平台收货仓当前不可用,请重新选择可用收货仓' )
}
2026-08-04 11:14:29 +08:00
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 ( '原目的仓当前不可用,请重新选择平台维护的目的仓' )
}
}
2026-07-30 21:23:06 +08:00
editingId . value = detailedOrder . id
replaceForm ( draft )
2026-08-07 18:44:58 +08:00
await Promise . all ( form . items . map ( ensureProductImagePreview ))
2026-07-30 21:23:06 +08:00
if ( warehouseAvailable ) void loadSelectedWarehouse ( detailedOrder . receivingWarehouseId )
editorOpen . value = true
2026-08-01 12:17:33 +08:00
resetEditorValidation ()
2026-07-30 21:23:06 +08:00
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '订单详情加载失败,暂时无法编辑' )
} finally {
openingEditor . value = false
}
2026-07-30 17:59:42 +08:00
}
async function openDetail ( order : FirstLegOrder ) {
2026-09-18 16:48:35 +08:00
await router . push ({ name : 'first-leg-order-detail' , params : { orderId : order . id } })
}
async function loadDetailPage ( orderId : string ) {
2026-09-04 12:00:33 +08:00
feeConfirmationOpen . value = false
2026-09-18 16:48:35 +08:00
detailId . value = orderId
2026-08-13 17:12:05 +08:00
detailCargoPage . value = 1
2026-07-30 17:59:42 +08:00
detailLoading . value = true
2026-07-30 18:08:40 +08:00
detailError . value = ''
2026-07-30 17:59:42 +08:00
try {
2026-09-18 16:48:35 +08:00
const detailedOrder = await store . loadDetail ( orderId )
void loadInboundBilling ( detailedOrder )
2026-08-25 11:48:23 +08:00
// 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 ))
2026-07-30 17:59:42 +08:00
} catch ( error ) {
2026-07-30 18:08:40 +08:00
detailError . value = error instanceof Error ? error . message : '订单详情加载失败'
ElMessage . error ( detailError . value )
2026-07-30 17:59:42 +08:00
} finally {
detailLoading . value = false
}
}
2026-09-03 10:46:14 +08:00
function openFeeConfirmation ( order : FirstLegOrder ) {
2026-09-04 12:00:33 +08:00
resetInboundBillingState ()
inboundBillingUnavailableMessage . value = '未推送计费重'
feeConfirmationOpen . value = true
void loadInboundBilling ( order , '未推送计费重' )
2026-09-03 10:46:14 +08:00
}
2026-07-30 18:08:40 +08:00
async function retryDetail () {
2026-09-18 16:48:35 +08:00
if ( detailId . value ) await loadDetailPage ( detailId . value )
2026-07-30 18:08:40 +08:00
}
2026-08-04 21:01:34 +08:00
function resetInboundBillingState () {
inboundBillingRequestSequence += 1
2026-09-04 12:00:33 +08:00
inboundBillingOrderId . value = ''
2026-08-04 21:01:34 +08:00
inboundBilling . value = null
inboundBillingLoading . value = false
inboundBillingUnavailable . value = false
2026-09-03 10:46:14 +08:00
inboundBillingUnavailableMessage . value = '还未测量推送'
2026-08-04 21:01:34 +08:00
inboundBillingError . value = ''
}
function isCurrentInboundBillingRequest ( sequence : number , orderId : string ) {
return sequence === inboundBillingRequestSequence
2026-09-04 12:00:33 +08:00
&& inboundBillingOrderId . value === orderId
2026-09-18 16:48:35 +08:00
&& ( isDetailPage . value || feeConfirmationOpen . value )
2026-08-04 21:01:34 +08:00
}
2026-09-04 12:00:33 +08:00
async function loadInboundBilling ( order : FirstLegOrder , unavailableMessage = '还未测量推送' ) {
2026-08-04 21:01:34 +08:00
const sequence = ++ inboundBillingRequestSequence
2026-09-04 12:00:33 +08:00
inboundBillingOrderId . value = order . id
2026-08-04 21:01:34 +08:00
inboundBilling . value = null
inboundBillingLoading . value = false
inboundBillingUnavailable . value = false
2026-09-04 12:00:33 +08:00
inboundBillingUnavailableMessage . value = unavailableMessage
2026-08-04 21:01:34 +08:00
inboundBillingError . value = ''
if ( order . status === 'DRAFT' ) {
inboundBillingUnavailable . value = true
2026-09-04 12:14:17 +08:00
inboundBillingUnavailableMessage . value = feeConfirmationOpen . value
? '未推送计费重'
: '订单提交后可查看入库结果'
2026-08-04 21:01:34 +08:00
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
2026-09-04 12:00:33 +08:00
inboundBillingUnavailableMessage . value = feeConfirmationOpen . value
? '未推送计费重'
: error . message || unavailableMessage
2026-08-04 21:01:34 +08:00
} else {
inboundBillingError . value = error instanceof Error ? error . message : '入库与费用加载失败'
}
} finally {
if ( isCurrentInboundBillingRequest ( sequence , order . id )) inboundBillingLoading . value = false
}
}
async function retryInboundBilling () {
2026-09-04 12:00:33 +08:00
const order = inboundBillingOrderId . value ? store . getById ( inboundBillingOrderId . value ) : undefined
if ( order ) await loadInboundBilling ( order , feeConfirmationOpen . value ? '未推送计费重' : '还未测量推送' )
}
function handleInboundFeeConfirmed () {
if ( ! feeConfirmationOpen . value ) void retryInboundBilling ()
2026-08-04 21:01:34 +08:00
}
2026-07-30 18:08:40 +08:00
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
2026-08-03 17:44:42 +08:00
if ( isCreatePage . value ) {
markEditorClean ()
void router . push ({ name : 'first-leg-orders' })
}
2026-07-30 18:08:40 +08:00
})
}
2026-07-30 17:59:42 +08:00
function addCargoItem () {
form . items . push ( createEmptyCargoItem ( `new-cargo- ${ ++ cargoSequence } ` ))
2026-08-01 12:17:33 +08:00
resetEditorValidation ()
2026-07-30 17:59:42 +08:00
}
2026-09-11 18:14:33 +08:00
async function changeFulfillmentType () {
if ( form . fulfillmentType !== 'ONE_CLICK_DROPSHIP' || form . destinationType === 'OVERSEAS' ) return
form . destinationType = 'OVERSEAS'
await changeDestinationType ()
}
2026-09-11 16:50:23 +08:00
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 ()
}
2026-07-30 17:59:42 +08:00
function removeCargoItem ( index : number ) {
if ( form . items . length === 1 ) {
ElMessage . warning ( '订单至少需要保留一条货物明细' )
return
}
2026-08-07 19:23:46 +08:00
const [ removed ] = form . items . splice ( index , 1 )
if ( removed ) releaseProductImagePreviews ([ removed ])
2026-09-11 16:50:23 +08:00
normalizeSingleSkuGroups ()
2026-08-01 12:17:33 +08:00
resetEditorValidation ()
}
2026-08-13 17:12:05 +08:00
function displayListValue ( value : string | number | null | undefined ) {
2026-08-30 17:35:52 +08:00
return value === '' || value == null ? '—' : String ( value )
2026-08-13 17:12:05 +08:00
}
2026-08-25 21:01:42 +08:00
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 ))
}
2026-08-13 17:12:05 +08:00
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
}
2026-09-14 17:18:56 +08:00
async function submitSelectedDraftOrders () {
const drafts = selectedDraftOrders . value
if ( batchSubmitting . value || ! drafts . length ) return
if ( companySubmissionBlocked . value ) {
ElMessage . warning ( '企业资料审核通过后才能提交订单' )
return
}
try {
await ElMessageBox . confirm ( `确认提交选中的 ${ drafts . length } 条草稿订单吗?` , '批量提交订单' , {
type : 'warning' ,
confirmButtonText : '确认提交' ,
cancelButtonText : '取消' ,
})
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
throw error
}
batchSubmitting . value = true
try {
const results = await Promise . allSettled ( drafts . map (( order ) => submitMerchantFirstLegOrder (
order . id ,
createIdempotencyKey ( `first-leg-batch-submit- ${ order . 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
}
}
2026-08-13 17:12:05 +08:00
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 ),
2026-08-17 19:57:35 +08:00
order . transportMode ? getFirstLegTransportModeLabel ( order . transportMode ) : '-' ,
pickupMethodLabels [ order . pickupMethod ],
order . pickupMethod === 'PICKUP' ? formatExportDateTime ( order . requestedPickupAt ) : '-' ,
2026-08-13 17:12:05 +08:00
displayListValue ( order . destinationWarehouseName ),
2026-08-17 19:57:35 +08:00
destinationTypeLabels [ order . destinationType ],
2026-08-13 17:12:05 +08:00
getFirstLegStatusLabel ( order . status ),
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' )),
'-' ,
2026-08-17 19:57:35 +08:00
formatListMetric ( order . chargeableVolumeCbm , 'm³' ),
2026-08-13 17:12:05 +08:00
'-' ,
displayListValue ( order . customerRemark ),
displayListValue ( order . shipmentId || uniqueItemValues ( order , 'shipmentId' )),
displayListValue ( order . referenceNo || uniqueItemValues ( order , 'referenceId' )),
formatExportDateTime ( order . inboundedAt ),
formatExportDateTime ( order . createdAt ),
])
downloadCsv ( '订单信息.csv' , [[
2026-09-18 16:48:35 +08:00
'入仓码' , '渠道' , '运输方式' , '交货方式' , '预约提货时间' , '目的仓' , '目的仓类型' , '当前订单状态' , '箱数' , '快递公司' , '快递单号' , 'SKUS' ,
2026-08-13 17:12:05 +08:00
'总毛重' , '总净重' , '总体积' , '总数量' , '计费单价' , '计费体积' , '计费重量' , '备注' ,
2026-08-17 19:52:08 +08:00
'货件编号' , '跟踪编号' , '国内仓入库时间' , '创建时间' ,
2026-08-13 17:12:05 +08:00
], ... rows ])
ElMessage . success ( `已导出 ${ rows . length } 条订单信息` )
} finally {
exportingOrders . value = false
}
}
2026-08-12 17:46:46 +08:00
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 )))
2026-09-11 16:50:23 +08:00
normalizeSingleSkuGroups ()
2026-08-12 17:46:46 +08:00
releaseProductImagePreviews ( removedItems )
selectedCargoIds . value = []
resetEditorValidation ()
}
2026-09-01 19:34:37 +08:00
function openBatchImport () {
batchImportMode . value = 'PARTIAL'
batchImportFile . value = null
batchImportIdempotencyKey . value = ''
batchImportResult . value = null
2026-09-01 20:38:12 +08:00
batchImportError . value = ''
2026-09-01 19:34:37 +08:00
batchImportOpen . value = true
2026-08-12 17:46:46 +08:00
}
2026-09-03 18:24:04 +08:00
function batchImportExcelRow ( item : { index : number ; clientRowId : string }) {
const matched = /^excel-row-(\d+)$/ . exec ( item . clientRowId )
return matched ? Number ( matched [ 1 ]) : item . index + 3
}
2026-09-01 19:34:37 +08:00
function selectBatchImportFile ( event : Event ) {
2026-08-12 17:46:46 +08:00
const input = event . target as HTMLInputElement
const file = input . files ? .[ 0 ]
input . value = ''
if ( ! file ) return
2026-09-01 19:34:37 +08:00
if ( ! file . name . toLowerCase (). endsWith ( '.xlsx' )) {
2026-09-01 20:38:12 +08:00
batchImportError . value = '仅支持 .xlsx 格式的订单导入文件'
ElMessage . error ( batchImportError . value )
2026-09-01 19:34:37 +08:00
return
}
if ( file . size > 5 * 1024 * 1024 ) {
2026-09-01 20:38:12 +08:00
batchImportError . value = '订单导入文件不能超过 5MB'
ElMessage . error ( batchImportError . value )
2026-09-01 19:34:37 +08:00
return
}
batchImportFile . value = file
batchImportIdempotencyKey . value = createIdempotencyKey ( 'first-leg-batch-import' )
batchImportResult . value = null
2026-09-01 20:38:12 +08:00
batchImportError . value = ''
2026-09-01 19:34:37 +08:00
}
async function downloadBatchTemplate () {
try {
batchTemplateDownloading . value = true
2026-09-01 20:38:12 +08:00
batchImportError . value = ''
2026-09-01 19:34:37 +08:00
saveBlob ( await downloadMerchantFirstLegBatchImportTemplate (), '头程订单批量导入模板.xlsx' )
} catch ( error ) {
2026-09-01 20:38:12 +08:00
batchImportError . value = batchImportErrorMessage ( error )
ElMessage . error ( batchImportError . value )
2026-09-01 19:34:37 +08:00
} finally {
batchTemplateDownloading . value = false
}
}
2026-09-01 20:38:12 +08:00
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
}
}
2026-09-01 19:34:37 +08:00
async function submitBatchImport () {
if ( ! batchImportFile . value ) {
2026-09-01 20:38:12 +08:00
batchImportError . value = '请先选择 .xlsx 订单导入文件'
ElMessage . warning ( batchImportError . value )
2026-08-12 17:46:46 +08:00
return
}
try {
2026-09-01 19:34:37 +08:00
batchImportSubmitting . value = true
2026-09-01 20:38:12 +08:00
batchImportError . value = ''
2026-09-01 19:34:37 +08:00
batchImportResult . value = await importMerchantFirstLegOrders (
batchImportFile . value ,
batchImportMode . value ,
batchImportIdempotencyKey . value ,
)
const result = batchImportResult . value . result
2026-09-01 20:38:12 +08:00
if ( result . succeeded && auth . hasPermission ( 'order:first-leg:list' )) await store . load ({ includeDrafts : true })
2026-09-01 19:34:37 +08:00
if ( result . failed ) ElMessage . warning ( `导入完成:成功 ${ result . succeeded } 条,失败 ${ result . failed } 条` )
else ElMessage . success ( `已成功导入 ${ result . succeeded } 条订单草稿` )
2026-08-12 17:46:46 +08:00
} catch ( error ) {
2026-09-01 20:38:12 +08:00
batchImportError . value = batchImportErrorMessage ( error )
ElMessage . error ( batchImportError . value )
2026-09-01 19:34:37 +08:00
} finally {
batchImportSubmitting . value = false
2026-08-12 17:46:46 +08:00
}
}
2026-09-01 20:38:12 +08:00
function viewImportedDrafts () {
batchImportOpen . value = false
void router . push ({ name : 'first-leg-orders' })
}
2026-08-01 12:17:33 +08:00
async function validateEditorForm () {
2026-08-19 09:14:38 +08:00
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
}
2026-08-01 12:17:33 +08:00
let fieldsValid = false
try {
fieldsValid = await orderFormRef . value ? . validate () ?? false
} catch {
fieldsValid = false
}
if ( ! fieldsValid ) {
ElMessage . warning ( '请检查并完善表单中标红的必填信息' )
return false
}
2026-08-18 11:32:58 +08:00
if ( manualInboundCodeRequired . value && ! form . inboundCode ? . trim ()) {
ElMessage . warning ( '当前为旧系统并跑模式,请填写旧系统入仓码' )
return false
}
2026-08-01 12:17:33 +08:00
const errors = validateFirstLegOrderDraft ( form )
if ( errors . length > 0 ) {
ElMessage . warning ( errors [ 0 ])
return false
}
return true
2026-07-30 17:59:42 +08:00
}
async function saveDraft () {
if ( saving . value ) return
2026-08-01 12:17:33 +08:00
if ( ! await validateEditorForm ()) return
2026-07-30 17:59:42 +08:00
saving . value = true
try {
2026-07-30 21:23:06 +08:00
const order = await store . saveDraft ( cloneDraft ( form ), tenantStore . company . companyName , editingId . value )
2026-07-30 17:59:42 +08:00
editingId . value = order . id
2026-07-30 18:09:32 +08:00
markEditorClean ()
2026-07-30 17:59:42 +08:00
editorOpen . value = false
2026-08-03 17:44:42 +08:00
if ( isCreatePage . value ) await router . push ({ name : 'first-leg-orders' })
2026-07-30 17:59:42 +08:00
ElMessage . success ( '订单草稿已保存' )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '草稿保存失败' )
} finally {
saving . value = false
}
}
async function submitOrder () {
if ( saving . value ) return
2026-08-04 10:31:53 +08:00
if ( companySubmissionBlocked . value ) {
2026-07-30 17:59:42 +08:00
try {
await ElMessageBox . confirm ( '企业资料审核通过后才能提交订单。您可以继续保存草稿,或前往企业资料核对并补充信息。' , '暂时无法提交订单' , {
type : 'warning' ,
confirmButtonText : '前往企业资料' ,
cancelButtonText : '继续编辑草稿' ,
})
2026-07-30 18:09:32 +08:00
markEditorClean ()
2026-07-30 17:59:42 +08:00
editorOpen . value = false
await router . push ( '/company/profile' )
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
throw error
}
return
}
2026-08-01 12:17:33 +08:00
if ( ! await validateEditorForm ()) return
2026-07-30 17:59:42 +08:00
try {
saving . value = true
2026-08-04 10:31:53 +08:00
const knownCompanyAuditStatus = companyAuditLoaded . value ? tenantStore . company . auditStatus : undefined
2026-08-14 16:06:33 +08:00
const savedOrder = await store . saveDraft ( cloneDraft ( form ), tenantStore . company . companyName , editingId . value )
editingId . value = savedOrder . id
const order = await store . submitSaved ( savedOrder . id , knownCompanyAuditStatus )
2026-07-30 18:09:32 +08:00
markEditorClean ()
2026-07-30 17:59:42 +08:00
editorOpen . value = false
2026-08-25 16:11:00 +08:00
if ( isCreatePage . value ) {
workspace . close ([ 'first-leg-order-create' , 'first-leg-order-edit' ])
await router . push ({ name : 'first-leg-orders' })
ElMessage . success ( '订单已提交,等待平台审核' )
return
}
2026-09-18 16:48:35 +08:00
await router . push ({ name : 'first-leg-order-detail' , params : { orderId : order . id } })
2026-07-30 17:59:42 +08:00
ElMessage . success ( '订单已提交,等待平台审核' )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '订单提交失败' )
} finally {
saving . value = false
}
}
2026-08-01 12:17:33 +08:00
async function searchOrders ( model : QueryModel ) {
const createdRange = model . createdRange
appliedFilters . value = {
... model ,
createdRange : Array . isArray ( createdRange ) ? createdRange . filter (( value ) : value is string => typeof value === 'string' ) : [],
}
tablePage . value = 1
try {
2026-08-06 13:56:13 +08:00
const statusValue = typeof model . status === 'string' ? model . status as FirstLegOrderStatus | '' : ''
2026-08-13 17:12:05 +08:00
const cargoKeyword = typeof model . cargoKeyword === 'string' ? model . cargoKeyword . trim () : ''
2026-08-06 13:56:13 +08:00
const range = Array . isArray ( createdRange ) ? createdRange . filter (( value ) : value is string => typeof value === 'string' ) : []
2026-08-25 21:01:42 +08:00
const query = {
2026-08-25 22:08:30 +08:00
keyword : undefined ,
inboundCode : cargoKeyword || undefined ,
2026-08-13 17:12:05 +08:00
receivingWarehouseId : typeof model . receivingWarehouseId === 'string' && model . receivingWarehouseId ? model . receivingWarehouseId : undefined ,
2026-08-06 13:56:13 +08:00
mainStatus : statusValue && statusValue !== 'DRAFT' ? statusValue : undefined ,
draftOnly : statusValue === 'DRAFT' ,
includeDrafts : ! statusValue ,
createdFrom : range [ 0 ] ? ` ${ range [ 0 ] } T00:00:00` : undefined ,
createdTo : range [ 1 ] ? ` ${ range [ 1 ] } T23:59:59` : undefined ,
2026-08-25 21:01:42 +08:00
}
await store . load ( query )
2026-08-25 22:08:30 +08:00
const cargoMatched = cargoKeyword && store . orders . some (( order ) => order . status !== 'DRAFT' && orderMatchesCargoKeyword ( order , cargoKeyword ))
2026-08-25 21:01:42 +08:00
if ( cargoKeyword && ! cargoMatched && statusValue !== 'DRAFT' ) {
2026-08-25 22:08:30 +08:00
await store . load ({ ... query , keyword : undefined , inboundCode : undefined , fbaCode : cargoKeyword })
2026-08-25 21:01:42 +08:00
}
2026-08-13 17:12:05 +08:00
selectedOrders . value = []
orderTableVersion . value += 1
2026-08-01 12:17:33 +08:00
} catch {
// Store keeps the recoverable error state rendered below the query toolbar.
}
2026-07-30 17:59:42 +08:00
}
</ script >
< template >
2026-09-18 16:48:35 +08:00
< section class = "first-leg-page" : class = "{ 'is-create-route': isCreatePage, 'is-detail-route': isDetailPage, 'client-ui': !isCreatePage }" >
< template v-if = "!isCreatePage && !isDetailPage" >
2026-09-15 18:17:21 +08:00
< QueryToolbar v-model = "filters" class="first-leg-query-toolbar" layout="grid" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
2026-07-30 17:59:42 +08:00
2026-08-01 12:17:33 +08:00
< div class = "client-card order-table-card" >
2026-09-15 18:17:21 +08:00
< DataTable v -model:page = "tablePage" v -model:page-size = "tablePageSize" :data = "pagedOrders" :columns = "visibleOrderColumns" :key = "orderTableVersion" :loading = "store.loading" :total = "filteredOrders.length" : page -sizes ="[ 10 , 20 , 50 ]" row -key =" id " empty -text =" 暂无订单 " class = "prototype-order-table" :row-actions = "getOrderRowActions" @ selection -change =" handleOrderSelectionChange " >
2026-09-18 16:48:35 +08:00
< template # toolbar >
< nav class = "order-queue-tabs" aria -label =" 订单事项分类 " >
< button type = "button" : class = "{ active: activeOrderQueue === 'all' }" @click ="selectOrderQueue('all')" > 全部 </ button >
< button type = "button" : class = "{ active: activeOrderQueue === 'fee' }" @click ="selectOrderQueue('fee')" > 确认计费重 < span v-if = "orderQueueCounts.fee" class="queue-count" >{{ orderQueueCounts.fee }}</ span ></ button >
< button type = "button" : class = "{ active: activeOrderQueue === 'customs' }" @click ="selectOrderQueue('customs')" > 上传资料 < span v-if = "orderQueueCounts.customs" class="queue-count" >{{ orderQueueCounts.customs }}</ span ></ button >
2026-09-19 17:02:11 +08:00
< button type = "button" : class = "{ active: activeOrderQueue === 'draft' }" @click ="selectOrderQueue('draft')" > 草稿 < span v-if = "orderQueueCounts.draft" class="queue-count" >{{ orderQueueCounts.draft }}</ span ></ button >
2026-09-18 16:48:35 +08:00
</ nav >
2026-09-19 17:02:11 +08:00
< span v-if = "selectedOrderCount" > 已选 {{ selectedOrderCount }} 条 </ span >
2026-09-18 16:48:35 +08:00
</ template >
2026-09-15 18:17:21 +08:00
< template # toolbar -actions >
2026-08-13 17:12:05 +08:00
< el-popover placement = "bottom-end" :width = "320" trigger = "click" popper -class =" order -column-popper " >
< template # reference >
2026-09-18 16:48:35 +08:00
< el-button class = "column-settings-trigger" aria -label =" 设置订单列表显示列 " title = "设置显示列" >< SlidersHorizontal :size = "18" /> 显示字段筛选 </ el-button >
2026-08-13 17:12:05 +08:00
</ 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 >
2026-09-14 17:18:56 +08:00
< 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 || companySubmissionBlocked" @click="submitSelectedDraftOrders"><Send :size="17" /> 批量提交草稿 </ el-button >
2026-09-01 19:34:37 +08:00
< el-button v-if = "auth.hasPermission('order:first-leg:create')" @click="openBatchImport"><Upload :size="17" /> 批量导入订单 </ el-button >
2026-09-18 16:48:35 +08:00
< 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 >
2026-09-15 18:17:21 +08:00
</ template >
< template # before -table >
2026-08-04 10:31:53 +08:00
< div v-if = "companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status" >
2026-08-03 17:44:42 +08:00
< AlertTriangle :size = "20" />
< div >< strong >{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</ strong >< span >{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</ span ></ div >
2026-08-04 10:31:53 +08:00
< el-button v-if = "canViewCompanyProfile" link type="primary" @click="router.push('/company/profile')" > 查看企业资料 </ el -button >
2026-08-03 10:51:53 +08:00
</ 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 >
2026-09-15 18:17:21 +08:00
</ template >
< template v-if = "store.loadError" # error >
< div class = "order-load-state" role = "alert" >
2026-07-30 21:23:06 +08:00
< AlertTriangle :size = "24" />
< div >< strong > 订单数据加载失败 </ strong >< span >{{ store . loadError }}</ span ></ div >
< el-button :loading = "store.loading" @click ="loadPageData" > 重新加载 </ el -button >
</ div >
2026-09-15 18:17:21 +08:00
2026-07-30 21:23:06 +08:00
</ template >
2026-08-30 17:35:52 +08:00
< template # inboundCode = "{ row }" >
< button
v-if = "(row as FirstLegOrder).businessSummary.inboundCode"
2026-09-18 16:48:35 +08:00
class = "inbound-code-primary table-ellipsis table-code"
2026-08-30 17:35:52 +08:00
type = "button"
2026-09-18 16:48:35 +08:00
: title = "`查看入仓码 ${(row as FirstLegOrder).businessSummary.inboundCode} 的订单详情`"
@click ="openDetail(row as FirstLegOrder)"
2026-08-30 17:35:52 +08:00
>{{ ( row as FirstLegOrder ). businessSummary . inboundCode }}</ button >
2026-09-18 16:48:35 +08:00
< span v-else class = "pending-inbound-code" >{{ ( row as FirstLegOrder ). status === 'DRAFT' ? '提交后生成' : '待生成' }}</ span >
2026-07-30 17:59:42 +08:00
</ template >
2026-08-13 17:12:05 +08:00
< template # channel = "{ row }" >< span class = "table-ellipsis" : title = "displayListValue((row as FirstLegOrder).channelName)" >{{ displayListValue (( row as FirstLegOrder ). channelName ) }}</ span ></ template >
2026-08-17 19:57:35 +08:00
< 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 >
2026-08-17 19:13:41 +08:00
< template # receivingWarehouse = "{ row }" >< span class = "table-ellipsis" : title = "displayListValue((row as FirstLegOrder).receivingWarehouseName)" >{{ displayListValue (( row as FirstLegOrder ). receivingWarehouseName ) }}</ span ></ template >
2026-08-13 17:12:05 +08:00
< template # destinationWarehouse = "{ row }" >< span class = "table-ellipsis" : title = "displayListValue((row as FirstLegOrder).destinationWarehouseName)" >{{ displayListValue (( row as FirstLegOrder ). destinationWarehouseName ) }}</ span ></ template >
2026-08-17 19:57:35 +08:00
< template # destinationType = "{ row }" >{{ destinationTypeLabels [( row as FirstLegOrder ). destinationType ] }}</ template >
2026-09-15 18:20:34 +08:00
< template # status = "{ row }" >< span class = "client-status" : data -tone =" getFirstLegStatusTone (( row as FirstLegOrder ) .status )" >{{ getFirstLegStatusLabel (( row as FirstLegOrder ). status ) }}</ span ></ template >
2026-08-13 17:12:05 +08:00
< 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 >
2026-08-17 19:57:35 +08:00
< template # chargeableVolume = "{ row }" >{{ formatListMetric (( row as FirstLegOrder ). chargeableVolumeCbm , 'm³' ) }}</ template >
2026-08-13 17:12:05 +08:00
< 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 >
2026-08-17 19:13:41 +08:00
< 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 # sailingAt = "{ row }" >{{ ( row as FirstLegOrder ). businessSummary . sailingAt ? formatDateTime (( row as FirstLegOrder ). businessSummary . sailingAt ) : '-' }}</ template >
2026-08-13 17:12:05 +08:00
< template # createdAt = "{ row }" >{{ formatDateTime (( row as FirstLegOrder ). createdAt ) }}</ template >
2026-08-17 19:13:41 +08:00
< template # updatedAt = "{ row }" >{{ formatDateTime (( row as FirstLegOrder ). updatedAt ) }}</ template >
2026-08-01 12:17:33 +08:00
< template # empty >
< el-empty :description = "emptyDescription" >
2026-09-18 16:48:35 +08:00
< 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 >
2026-08-01 12:17:33 +08:00
</ el-empty >
</ template >
</ DataTable >
2026-07-30 17:59:42 +08:00
</ div >
2026-08-03 17:44:42 +08:00
</ template >
2026-07-30 17:59:42 +08:00
2026-08-03 17:44:42 +08:00
< 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 >
2026-08-06 15:32:07 +08:00
< el-button :loading = "pageDataLoading" @click ="loadPageData" > 重新加载 </ el -button >
2026-08-03 17:44:42 +08:00
</ div >
< el-alert v-if = "isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show -icon />
2026-09-20 10:45:31 +08:00
< FirstLegOrderEditorShell v-model = "editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : (routeCopySourceId ? '复制订单' : '创建订单')" :before-close="requestCloseEditor" @back="closeEditor" >
2026-08-13 17:12:05 +08:00
< 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 >
2026-08-03 15:50:24 +08:00
< section class = "order-form-section prototype-section" >
2026-08-12 17:46:46 +08:00
< div class = "order-section-heading" >< div >< h3 > 客户下单 </ h3 >< p > 先选择货物如何送到国内收货仓 , 并填写本次运输安排 。 </ p ></ div ></ div >
2026-08-04 12:58:04 +08:00
< div class = "prototype-form-flow shipping-form-flow" >
2026-09-11 18:14:33 +08:00
< 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 >
2026-09-08 17:24:51 +08:00
< 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 >
2026-08-04 12:58:04 +08:00
< 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" >
2026-09-01 19:34:37 +08:00
< 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 >
2026-08-04 12:58:04 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
< 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 >
2026-08-04 12:58:04 +08:00
</ div >
< div class = "order-form-grid compact-transport-grid" >
2026-08-18 11:32:58 +08:00
< 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 >
2026-08-07 14:47:00 +08:00
< 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 >
2026-09-16 16:42:08 +08:00
< el-form-item label = "报价包税方式" prop = "pricingTaxMode" required class = "pricing-tax-mode-field" >
2026-08-07 14:47:00 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
< 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 >
2026-09-18 09:15:15 +08:00
< el-form-item label = "是否商检" prop = "customsInspection" >< el-select v-model = "form.customsInspection" clearable placeholder="暂不选择"><el-option label="是" :value="true" /><el-option label="否" :value="false" /></el-select><small v-if="woodenCargoLines.length && form.customsInspection !== true" class="field-guidance field-guidance--error" > 第 {{ woodenCargoLines.join ('、') }} 条货物为木制品 , 请将 “ 是否商检 ” 选择为 “ 是 ”。 </ small > </ el -form-item >
2026-08-04 12:58:04 +08:00
< el-form-item label = "期望出货日期" >< el-date-picker v-model = "form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择期望出货日期" /></ el-form-item >
2026-08-12 17:46:46 +08:00
< 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 >
2026-08-04 12:58:04 +08:00
</ div >
</ section >
2026-08-12 17:46:46 +08:00
< section class = "order-form-section prototype-section destination-section" >
< div class = "order-section-heading destination-section-heading" >< div >< h3 > 目的地信息 </ h3 ></ div ></ div >
2026-08-04 12:01:05 +08:00
< div class = "prototype-form-flow destination-form-flow" >
2026-09-11 18:14:33 +08:00
< 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 >
2026-08-04 12:01:05 +08:00
< el-form-item v-if = "form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row" >
2026-09-01 19:34:37 +08:00
< 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 >
2026-08-04 12:01:05 +08:00
</ el-form-item >
2026-08-06 14:55:13 +08:00
< 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="`${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 >
2026-08-04 12:58:04 +08:00
< div v-if = "form.destinationType !== 'PRIVATE' && selectedDestinationWarehouse" class="selected-destination"><MapPin :size="17" /><div><span>{{ 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 >
2026-08-04 12:01:05 +08:00
< 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 >
2026-07-30 17:59:42 +08:00
</ div >
</ section >
2026-08-03 15:50:24 +08:00
< section class = "order-form-section prototype-section cargo-section" >
2026-08-12 17:46:46 +08:00
< div class = "order-section-heading cargo-table-heading" >< div >< h3 > 货物明细 </ h3 ></ div ></ div >
< div class = "cargo-table-toolbar" >
2026-09-03 16:48:22 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
< 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 >
2026-09-11 16:50:23 +08:00
< div class = "cargo-table-wrap" : class = "{ 'is-dropship': form.fulfillmentType === 'ONE_CLICK_DROPSHIP' }" >
2026-08-12 17:46:46 +08:00
< 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 >
2026-09-11 16:50:23 +08:00
< th > SKU NO .< small > SKU必填 </ 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 >
2026-09-11 15:38:28 +08:00
< 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 >
2026-09-01 20:38:12 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
</ tr ></ thead >
2026-09-11 16:50:23 +08:00
< tbody >< tr v-for = "(item, index) in form.items" :key="item.id" :class="{ 'mixed-sku-detail-row': isAdditionalMixedSku(item, index) }" >
2026-08-12 17:46:46 +08:00
< 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 >
2026-09-11 16:50:23 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
< 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 >
2026-09-11 16:50:23 +08:00
< 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 >
2026-09-01 20:38:12 +08:00
< 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 && form.customsInspection !== true" > 第 {{ index + 1 }} 条须商检 </ small ></ div ></ td >
2026-09-11 16:50:23 +08:00
< 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 >
2026-09-03 19:51:29 +08:00
< 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 >
2026-09-11 16:50:23 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
</ tr ></ tbody >
</ table >
</ div >
2026-09-11 16:50:23 +08:00
< div class = "cargo-table-hint" >< span v-if = "form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" > 每个箱组默认装一个 SKU ; 同一箱内有多个 SKU 时点击 “ 添加 SKU ”, 箱数 、 箱规 、 重量和体积只填写一次 。 </ span >< span v-else > SKU NO. 、 货件编号 、 跟踪编号为 FBA 货物必填项 , UPC NO. 选填 </ span ></ div >
2026-08-12 17:46:46 +08:00
</ section >
2026-08-07 18:44:58 +08:00
2026-08-12 17:46:46 +08:00
< section class = "order-form-section prototype-section cost-estimate-section" aria -label =" 费用预估 " >
2026-08-19 09:14:38 +08:00
< 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 >
2026-08-21 09:59:58 +08:00
< 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 >
2026-08-19 09:14:38 +08:00
< 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 >
2026-08-12 17:46:46 +08:00
</ div >
2026-07-30 17:59:42 +08:00
</ section >
</ el-form >
2026-08-04 10:31:53 +08:00
< template # footer >< div class = "editor-actions" >< el-button :disabled = "saving" @click ="closeEditor" > 返回 </ el -button >< el-button v-if = "editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</ el-button ></ div ></ template >
2026-08-03 17:44:42 +08:00
</ FirstLegOrderEditorShell >
2026-07-30 17:59:42 +08:00
2026-09-01 20:38:12 +08:00
< el-dialog v-model = "batchImportOpen" title="Excel 批量创建订单草稿" width="680px" destroy -on-close append -to-body >
2026-09-01 19:34:37 +08:00
< div class = "batch-import-dialog" >
2026-09-03 16:48:22 +08:00
< el-alert title = "使用中文模板创建订单草稿" description = "仅支持 .xlsx,文件不超过 5MB、最多 50 行;请直接选择模板内的中文下拉项,不需要填写英文代码或内部编号。导入成功的数据只创建草稿,不会直接提交审核;选择木制品时,模板会要求同步选择需要商检。" type = "info" :closable = "false" show -icon />
2026-09-01 20:38:12 +08:00
< el-alert v-if = "batchImportError" title="批量导入处理失败" :description="batchImportError" type="error" :closable="false" show -icon />
2026-09-01 19:34:37 +08:00
< 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 >
2026-09-01 20:38:12 +08:00
< 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 >
2026-09-01 19:34:37 +08:00
< ul v-if = "batchImportResult.result.failed" >
2026-09-03 18:24:04 +08:00
< li v-for = "item in batchImportResult.result.items.filter((row) => !row.success)" :key="item.index" >
Excel 第 {{ batchImportExcelRow ( item ) }} 行 : {{ item.errorMessage || item.errorCode || ' 导入失败 ' }}
2026-09-01 19:34:37 +08:00
</ li >
</ ul >
</ section >
</ div >
2026-09-01 20:38:12 +08:00
< 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 >
2026-09-01 19:34:37 +08:00
</ el-dialog >
2026-09-18 16:48:35 +08:00
< section v-if = "isDetailPage" class="first-leg-detail-page" aria-label="订单详情" >
2026-07-30 21:23:06 +08:00
< 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" >
2026-07-30 17:59:42 +08:00
< div class = "order-detail-heading" >
2026-09-18 16:48:35 +08:00
< 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 === 'DRAFT' ? '入仓码提交后生成' : '入仓码待生成' ) }}</ h2 >< p >{{ 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 ></ div >
2026-07-30 17:59:42 +08:00
</ div >
2026-08-06 20:14:54 +08:00
< section class = "detail-section" >
< h3 > 订单与运输 </ h3 >
< dl class = "order-detail-grid" >
2026-09-18 16:48:35 +08:00
< 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 === ' DRAFT ' ? ' 提交后生成 ' : ' 待生成 ' }}</ span ></ dd ></ div >
2026-08-06 20:14:54 +08:00
< div >< dt > 创建时间 </ dt >< dd >{{ formatDateTime ( detailOrder . createdAt ) }}</ dd ></ div >
< div >< dt > 运输方式 </ dt >< dd >{{ getFirstLegTransportModeLabel ( detailOrder . transportMode ) }}</ dd ></ div >
< div >< dt > 贸易条款 </ dt >< dd >{{ displayValue ( detailOrder . tradeTerm ) }}</ dd ></ div >
< div >< dt > 发货方式 </ dt >< dd >{{ pickupMethodLabels [ detailOrder . pickupMethod ] }}</ dd ></ div >
2026-08-17 19:57:35 +08:00
< div >< dt > 预约提货时间 </ dt >< dd >{{ detailOrder . pickupMethod === 'PICKUP' && detailOrder . requestedPickupAt ? formatDateTime ( detailOrder . requestedPickupAt ) : '-' }}</ dd ></ div >
2026-08-06 20:14:54 +08:00
< 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 >
2026-08-30 17:35:52 +08:00
< 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 >
2026-08-06 20:14:54 +08:00
< 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 >
2026-09-01 19:34:37 +08:00
< div >< dt > 海关查验 </ dt >< dd >{{ detailOrder . customsInspection == null ? '未选择' : ( detailOrder . customsInspection ? '需要' : '不需要' ) }}</ dd ></ div >
2026-08-06 20:14:54 +08:00
< 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 >
2026-09-18 16:48:35 +08:00
< 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 >
2026-08-06 20:14:54 +08:00
< div v-if = "detailOrder.customerRemark" class="customer-remark" >< strong > 客户备注 </ strong >< p >{{ detailOrder . customerRemark }}</ p ></ div >
</ section >
2026-08-06 13:56:13 +08:00
< section v-if = "detailOrder.status !== 'DRAFT'" 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 >
2026-09-18 16:48:35 +08:00
< 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 . sailingAt ? formatDateTime ( row . sailingAt ) : '待更新' }}</ template ></ el-table-column >
< el-table-column prop = "status" label = "当前节点" width = "120" >< template # default = "{ row }" >< el-tag size = "small" effect = "plain" >{{ row . status || '待更新' }}</ el-tag ></ template ></ el-table-column >
</ el-table >
< p v-else class = "detail-inline-empty" > 尚未分配柜次 , 后续柜次节点将在这里同步 。 </ p >
</ div >
</ template >
</ OrderProgressPanel >
2026-07-30 17:59:42 +08:00
< section class = "detail-section" >
< h3 > 履约快递信息 </ h3 >
2026-09-18 16:48:35 +08:00
< dl v-if = "detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><div><dt>快递公司</dt><dd>{{ detailOrder.expressCompanyName || '待平台补充' }}</dd></div><div><dt>快递单号</dt><dd class="table-code" >{{ detailOrder.trackingNumber || ' 待平台补充 ' }}</ dd ></ div ></ dl >
2026-07-30 17:59:42 +08:00
< div v-else class = "express-empty" >< PackageCheck :size = "20" />< div >< strong > 平台尚未维护快递信息 </ strong >< span > 快递公司和快递单号由平台后台维护 , 客户端仅只读展示 。 </ span ></ div ></ div >
</ section >
2026-08-07 19:23:46 +08:00
< section class = "detail-section" >
< h3 > 货物明细 </ h3 >
2026-09-18 16:48:35 +08:00
< 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 >
2026-08-13 17:12:05 +08:00
< 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 >
2026-09-11 15:38:28 +08:00
< 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 >
2026-08-13 17:12:05 +08:00
< 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 >
2026-08-25 11:48:23 +08:00
< 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 >
2026-08-13 17:12:05 +08:00
< 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 >
2026-09-01 19:34:37 +08:00
< el-table-column label = "木制品" width = "82" align = "center" >< template # default = "{ row }" >{{ row . woodenProduct ? '是' : '否' }}</ template ></ el-table-column >
2026-08-13 17:12:05 +08:00
< 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"
/>
2026-08-07 19:23:46 +08:00
</ div >
< el-empty v-else description = "暂无货物明细" :image-size = "56" />
</ section >
2026-08-04 21:01:34 +08:00
< InboundBillingPanel
:data = "inboundBilling"
:loading = "inboundBillingLoading"
:unavailable = "inboundBillingUnavailable"
:unavailable-message = "inboundBillingUnavailableMessage"
:error = "inboundBillingError"
2026-09-03 17:49:09 +08:00
:can-confirm = "canConfirmInboundFee"
2026-08-04 21:01:34 +08:00
@retry ="retryInboundBilling"
2026-09-04 12:00:33 +08:00
@confirmed ="handleInboundFeeConfirmed"
2026-08-04 21:01:34 +08:00
/>
2026-07-30 17:59:42 +08:00
</ template >
2026-07-30 21:23:06 +08:00
</ div >
2026-09-18 16:48:35 +08:00
</ section >
2026-09-04 12:00:33 +08:00
< 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"
/>
2026-08-28 16:54:16 +08:00
< CustomsDocumentsDialog
v-model = "customsDialogOpen"
:order-id = "customsDialogOrderId"
:can-edit = "auth.hasPermission('order:first-leg:edit')"
:initial-detail = "customsByOrder[customsDialogOrderId]"
@updated ="updateCustomsDetail"
/>
2026-08-30 17:35:52 +08:00
< InboundCodeAssociationDialog
v-model = "inboundAssociationOpen"
:inbound-code = "inboundAssociationCode"
/>
2026-07-30 17:59:42 +08:00
</ section >
</ template >
< style scoped >
2026-09-18 16:48:35 +08:00
. first - leg - page { display : flex ; min - width : 0 ; flex - direction : column ; gap : var ( -- client - section - gap ); }
2026-08-03 17:44:42 +08:00
: 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 : 1 px ; height : 1 px ; margin : - 1 px ; padding : 0 ; overflow : hidden ; border : 0 ; clip : rect ( 0 0 0 0 ); white - space : nowrap ; }
2026-09-18 16:48:35 +08:00
: global (. client - main : has ( > . first - leg - page . is - detail - route ) > . client - page - heading ) { display : none ; }
2026-07-30 17:59:42 +08:00
. order - eligibility { display : flex ; align - items : flex - start ; gap : 12 px ; padding : 14 px 16 px ; border : 1 px solid ; }
. order - eligibility svg { flex : 0 0 20 px ; margin - top : 1 px ; }
. order - eligibility div { display : flex ; min - width : 0 ; flex : 1 ; flex - direction : column ; gap : 3 px ; }
. order - eligibility strong { color : var ( -- navy ); font - size : 13 px ; }
. order - eligibility span { color : var ( -- muted ); font - size : 11 px ; line - height : 1.55 ; }
. order - eligibility . is - pending { border - color : # e5d2a5 ; background : # fff9eb ; color : # 9 a6a12 ; }
2026-07-31 15:34:22 +08:00
. supporting - data - error { display : grid ; grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 12 px ; padding : 14 px 16 px ; border : 1 px solid # e5d2a5 ; background : # fff9eb ; color : # 9 a6a12 ; }
. supporting - data - error > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 3 px ; }
. supporting - data - error strong { color : var ( -- navy ); font - size : 13 px ; }
. supporting - data - error span { overflow - wrap : anywhere ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.55 ; }
2026-08-01 12:17:33 +08:00
. order - row - actions , . editor - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; }
2026-09-15 18:20:34 +08:00
. order - table - card { padding : 0 ; overflow : hidden ; border : 1 px solid var ( -- client - border ); border - radius : var ( -- client - radius ); box - shadow : none ; }
2026-09-18 16:48:35 +08:00
. order - queue - tabs { display : flex ; align - items : center ; gap : 4 px ; }
. order - queue - tabs button { position : relative ; min - height : 34 px ; padding : 0 12 px ; border : 1 px solid transparent ; border - radius : 7 px ; 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 : 2 px solid var ( -- primary ); outline - offset : 2 px ; }
. queue - count { position : absolute ; display : grid ; min - width : 18 px ; height : 18 px ; place - items : center ; padding : 0 4 px ; border : 2 px solid # fff ; border - radius : 999 px ; background : # ef5350 ; color : # fff ; font - size : 10 px ; line - height : 1 ; right : - 4 px ; top : - 8 px ; }
2026-09-01 19:34:37 +08:00
. batch - import - dialog { display : flex ; flex - direction : column ; gap : 18 px ; }
. batch - import - actions { display : flex ; min - width : 0 ; align - items : center ; flex - wrap : wrap ; gap : 10 px ; }
. batch - import - actions > span { min - width : 0 ; overflow : hidden ; color : var ( -- muted ); font - size : 12 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. batch - import - dialog : deep (. el - radio - group ) { display : flex ; align - items : flex - start ; flex - direction : column ; gap : 8 px ; }
. batch - import - result { display : flex ; flex - direction : column ; gap : 6 px ; padding : 14 px ; border : 1 px solid var ( -- border ); background : # f8fafc ; }
. batch - import - result > span , . batch - import - result li { color : var ( -- muted ); font - size : 12 px ; line - height : 1.55 ; }
2026-09-01 20:38:12 +08:00
. batch - import - result - actions { display : flex ; margin - top : 4 px ; }
2026-09-01 19:34:37 +08:00
. batch - import - result ul { max - height : 180 px ; margin : 4 px 0 0 ; padding - left : 20 px ; overflow - y : auto ; }
2026-09-18 16:48:35 +08:00
. column - settings - trigger { min - width : 116 px ; height : 36 px ; padding : 0 12 px ; color : # 53627 b ; }
2026-08-13 17:12:05 +08:00
: global (. order - column - popper . el - popover ) { padding : 0 ; border - color : # e3e8f0 ; border - radius : 4 px ; box - shadow : 0 8 px 24 px rgba ( 29 , 45 , 74 , .12 ); }
: global (. order - column - popper . column - settings - panel ) { padding : 14 px 10 px 10 px ; }
: global (. order - column - popper . column - settings - panel > p ) { margin : 0 8 px 10 px ; color : # 778398 ; font - size : 12 px ; line - height : 1.5 ; }
: global (. order - column - popper . el - checkbox - group ) { max - height : 430 px ; overflow - y : auto ; padding : 0 2 px ; scrollbar - width : thin ; }
: global (. order - column - popper . column - settings - item ) { display : grid ; min - height : 42 px ; grid - template - columns : 24 px minmax ( 0 , 1 fr ) 28 px 28 px ; align - items : center ; padding : 0 4 px ; color : # 596982 ; }
: global (. order - column - popper . column - settings - item : hover ) { background : # f7f8fb ; }
: global (. order - column - popper . column - settings - item > svg ) { cursor : grab ; color : # 71809 a ; }
: global (. order - column - popper . column - settings - item > svg : active ) { cursor : grabbing ; }
: global (. order - column - popper . column - settings - item . el - checkbox ) { height : 42 px ; margin - right : 0 ; overflow : hidden ; }
: global (. order - column - popper . column - settings - item . el - checkbox__label ) { overflow : hidden ; color : # 4 f5f78 ; font - size : 13 px ; 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 : 28 px ; height : 28 px ; padding : 0 ; margin : 0 ; color : # 45556 e ; }
: global (. order - column - popper . column - settings - reset ) { min - height : 36 px ; margin : 8 px 6 px 0 ; color : # 40516 b ; }
. 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 ; }
2026-08-03 10:51:53 +08:00
. order - table - card > . order - eligibility , . order - table - card > . supporting - data - error { border - width : 0 0 1 px ; }
2026-07-30 21:23:06 +08:00
. order - load - state { display : grid ; min - height : 220 px ; grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; align - items : center ; gap : 14 px ; padding : 28 px ; color : # 9 a5a09 ; }
. order - load - state > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 5 px ; }
. order - load - state strong { color : var ( -- navy ); font - size : 14 px ; }
. order - load - state span { overflow - wrap : anywhere ; color : var ( -- muted ); font - size : 12 px ; line - height : 1.55 ; }
. order - load - state . is - compact { min - height : 180 px ; }
2026-09-18 16:48:35 +08:00
. first - leg - detail - page { min - width : 0 ; padding : 10 px 14 px 20 px ; overflow : hidden ; border : 1 px solid var ( -- client - border ); border - radius : var ( -- client - radius ); background : # fff ; }
. order - detail - content { min - height : 360 px ; }
. 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 : 18 px ; text - align : left ; text - overflow : ellipsis ; white - space : nowrap ; }
. pending - inbound - code { color : var ( -- muted ); font - size : 12 px ; }
2026-09-15 18:20:34 +08:00
. 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 : 3 px ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-08-30 17:35:52 +08:00
. inbound - code - link : hover , . inbound - code - link : focus - visible { text - decoration - color : currentColor ; }
. inbound - code - link : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 2 px ; }
2026-09-18 16:48:35 +08:00
. inbound - code - primary : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 3 px ; }
2026-07-30 17:59:42 +08:00
. order - reference { display : block ; margin - top : 4 px ; color : var ( -- muted ); font - size : 11 px ; font - weight : 400 ; }
2026-08-13 17:12:05 +08:00
. order - row - actions { align - items : center ; justify - content : center ; gap : 6 px ; white - space : nowrap ; }
. order - row - actions : deep (. el - button ) { margin - left : 0 ; padding : 3 px 1 px ; font - size : 11 px ; }
2026-08-12 17:46:46 +08:00
. 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 : 14 px ; }
. is - create - route . prototype - order - form { width : 100 % ; margin : 0 ; padding : 0 0 16 px ; }
. is - create - route . prototype - section : not (. cargo - section ) : not (. cost - estimate - section ) : not (. destination - section ) { width : min ( 100 % , 900 px ); }
. order - form { width : min ( 100 % , 1000 px ); margin : 0 ; padding : 0 0 12 px ; }
2026-08-13 17:12:05 +08:00
. 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 ; }
2026-08-01 12:17:33 +08:00
. order - form : deep (. el - form - item__error ) { position : static ; padding - top : 6 px ; overflow - wrap : anywhere ; line - height : 1.45 ; }
2026-08-12 17:46:46 +08:00
. order - form - section + . order - form - section { margin - top : 30 px ; padding - top : 28 px ; border - top : 1 px solid # edf0f5 ; }
2026-07-30 17:59:42 +08:00
. order - form - section h3 { margin : 0 0 16 px ; color : var ( -- navy ); font - size : 16 px ; }
. order - form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); column - gap : 18 px ; }
2026-08-07 18:44:58 +08:00
. order - form - grid > * , . cargo - form - grid > * { min - width : 0 ; }
2026-08-03 15:50:24 +08:00
. 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 ; }
2026-08-12 17:46:46 +08:00
. prototype - section { padding : 4 px 0 0 ; }
2026-08-12 17:47:40 +08:00
. prototype - section : first - child > . order - section - heading { margin - top : - 10 px ; }
. prototype - section : first - child > . order - section - heading h3 { margin - bottom : 6 px ; font - size : 20 px ; font - weight : 700 ; line - height : 1.35 ; }
2026-08-12 17:46:46 +08:00
. segmented - options { display : flex ; width : min ( 100 % , 560 px ); overflow : hidden ; border : 1 px solid # e3e8f0 ; border - radius : 5 px ; background : # fff ; }
2026-08-03 15:50:24 +08:00
. segmented - options : deep (. el - radio - button ) { flex : 1 ; }
2026-08-12 17:46:46 +08:00
. segmented - options : deep (. el - radio - button__inner ) { display : flex ; width : 100 % ; min - height : 42 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; padding : 10 px 16 px ; border : 0 ; border - right : 1 px solid # e3e8f0 ; border - radius : 0 ; box - shadow : none ; color : # 526070 ; font - size : 13 px ; }
. 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 : 10 px 22 px ; padding - top : 9 px ; }
. warehouse - options : deep (. el - radio ) { height : auto ; margin - right : 0 ; color : # 4 d5a6c ; }
. warehouse - options : deep (. el - radio__label ) { padding - left : 7 px ; font - size : 13 px ; }
. order - section - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 18 px ; }
2026-07-30 17:59:42 +08:00
. order - section - heading h3 { margin - bottom : 5 px ; }
2026-08-12 17:46:46 +08:00
. order - section - heading p { margin : 0 ; color : # 8 b96a6 ; font - size : 12 px ; }
. prototype - form - flow { display : flex ; max - width : 860 px ; flex - direction : column ; padding - top : 2 px ; }
. prototype - form - row { display : grid ; width : 100 % ; grid - template - columns : 114 px minmax ( 0 , 560 px ); align - items : start ; margin - bottom : 16 px ; }
. prototype - form - row -- wide { grid - template - columns : 114 px minmax ( 0 , 760 px ); }
. prototype - form - row -- compact { grid - template - columns : 114 px minmax ( 0 , 420 px ); }
. prototype - form - row -- short { grid - template - columns : 114 px minmax ( 0 , 280 px ); }
. prototype - form - row -- tabs { grid - template - columns : 114 px minmax ( 0 , 560 px ); margin - bottom : 18 px ; }
. prototype - form - row : deep (. el - form - item__label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 18 px 0 0 ; line - height : 40 px ; color : # 4 d5a6c ; font - size : 13 px ; }
2026-08-04 12:01:05 +08:00
. prototype - form - row : deep (. el - form - item__content ) { display : block ; min - width : 0 ; line - height : normal ; }
2026-08-12 17:46:46 +08:00
. prototype - form - row : deep (. el - input__wrapper ), . prototype - form - row : deep (. el - select__wrapper ) { min - height : 40 px ; box - shadow : 0 0 0 1 px # dfe5ed inset ; }
2026-08-04 12:01:05 +08:00
. address - book - control { display : grid ; grid - template - columns : minmax ( 260 px , 1 fr ) auto auto ; gap : 10 px ; align - items : center ; }
. address - book - control : deep (. el - button ) { min - height : 40 px ; margin - left : 0 ; }
2026-08-04 11:14:29 +08:00
. field - guidance { display : block ; margin - top : 5 px ; color : var ( -- muted ); line - height : 1.45 ; }
2026-08-07 14:47:00 +08:00
. field - guidance -- error { color : # b54708 ; }
2026-08-12 17:46:46 +08:00
. cost - estimate - section { width : 100 % ; }
. cost - estimate - section . order - section - heading { margin - bottom : 12 px ; }
2026-08-19 09:14:38 +08:00
. price - estimate { min - height : 126 px ; overflow : hidden ; border : 1 px solid # dde4f2 ; border - radius : 8 px ; background : # fff ; box - shadow : 0 2 px 8 px rgba ( 15 , 31 , 58 , .025 ); }
. price - estimate__state { display : flex ; min - height : 126 px ; align - items : center ; justify - content : center ; gap : 10 px ; padding : 24 px ; color : # 7 b8798 ; font - size : 12 px ; 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 : 4 px ; }
. price - estimate__state . is - error strong { color : # 7 a2e0e ; font - size : 13 px ; }
. price - estimate__state . is - error span { overflow - wrap : anywhere ; }
2026-08-21 09:59:58 +08:00
. price - estimate__result { display : grid ; grid - template - columns : minmax ( 180 px , 1.15 fr ) minmax ( 220 px , 1.25 fr ) minmax ( 170 px , .9 fr ) minmax ( 140 px , .8 fr ) minmax ( 190 px , 1 fr ); align - items : stretch ; }
2026-08-19 09:14:38 +08:00
. price - estimate__result > div { display : flex ; min - width : 0 ; min - height : 96 px ; justify - content : center ; flex - direction : column ; gap : 7 px ; padding : 18 px 20 px ; border - right : 1 px solid # edf0f5 ; }
. price - estimate__result > div > span { color : # 8 b96a6 ; font - size : 11 px ; }
. price - estimate__result > div > strong { overflow - wrap : anywhere ; color : # 303 b50 ; font - size : 15 px ; }
2026-08-21 09:59:58 +08:00
. price - estimate__comparison strong { font - size : 13 px ! important ; font - weight : 600 ; }
. price - estimate__comparison small , . price - estimate__chargeable small { color : # 7 b8798 ; font - size : 10 px ; line - height : 1.4 ; }
. price - estimate__chargeable strong { color : # 526 fd8 ! important ; font - size : 18 px ! important ; }
2026-08-19 09:14:38 +08:00
. price - estimate__channel small { color : # 7 b8798 ; font - size : 10 px ; line - height : 1.5 ; }
. price - estimate__result . price - estimate__amount { border - right : 0 ; background : # f8f8ff ; }
. price - estimate__result . price - estimate__amount strong { color : # 637 fec ; font - size : 25 px ; }
. price - estimate__result > small { grid - column : 1 / - 1 ; padding : 9 px 14 px ; border - top : 1 px solid # edf0f5 ; color : # 98 a0ad ; font - size : 10 px ; text - align : right ; }
2026-08-04 12:58:04 +08:00
. selected - destination { display : flex ; width : min ( 100 % , 720 px ); align - items : flex - start ; gap : 9 px ; margin : - 4 px 0 18 px 140 px ; padding : 10 px 12 px ; background : # f2f7f6 ; color : var ( -- primary ); }
. selected - destination svg { flex : 0 0 17 px ; margin - top : 2 px ; }
. selected - destination div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 2 px ; }
2026-08-04 12:01:05 +08:00
. selected - destination span { color : var ( -- navy ); font - size : 12 px ; font - weight : 600 ; }
2026-08-04 12:58:04 +08:00
. selected - destination small { color : var ( -- muted ); font - size : 11 px ; line - height : 1.5 ; }
2026-08-12 17:46:46 +08:00
. destination - section { width : 100 % ; }
. destination - section - heading { margin - bottom : 12 px ; padding - bottom : 12 px ; border - bottom : 1 px solid # edf0f5 ; }
. destination - section - heading h3 { margin - bottom : 0 ; }
. destination - form - flow { width : min ( 100 % , 900 px ); padding - left : 0 ; }
. destination - type - row { grid - template - columns : 114 px minmax ( 0 , 300 px ); margin - bottom : 16 px ; }
. destination - type - options { width : 300 px ; }
. destination - type - options : deep (. el - radio - button__inner ) { min - height : 36 px ; padding : 8 px 14 px ; font - size : 12 px ; }
. 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 : 114 px minmax ( 0 , 360 px ); }
2026-08-04 12:01:05 +08:00
. flow - divider { width : min ( 100 % , 860 px ); margin : 4 px 0 22 px ; border - top : 1 px solid var ( -- border ); }
2026-08-12 17:46:46 +08:00
. compact - transport - grid { width : min ( 100 % , 820 px ); grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 28 px ; margin : 20 px 0 0 114 px ; }
. compact - transport - grid : deep (. el - form - item__label ) { color : # 4 d5a6c ; font - size : 13 px ; }
. shipping - form - flow { max - width : 900 px ; }
. prototype - order - form > . prototype - section : first - child { width : 100 % ; }
. shipping - form - flow . prototype - form - row { margin - bottom : 10 px ; }
. shipping - form - flow . prototype - form - row -- tabs { margin - bottom : 12 px ; }
. compact - transport - grid { display : grid ; width : 100 % ; grid - template - columns : 1 fr ; gap : 0 ; margin : 6 px 0 0 ; }
. compact - transport - grid > : deep (. el - form - item ) { display : grid ; width : 100 % ; grid - template - columns : 114 px minmax ( 0 , 560 px ); align - items : start ; margin - bottom : 10 px ; }
2026-09-16 16:42:08 +08:00
. compact - transport - grid > : deep (. pricing - tax - mode - field ) { display : none ; }
2026-08-12 17:46:46 +08:00
. compact - transport - grid > : deep (. el - form - item : nth - child ( 1 )) { grid - template - columns : 114 px minmax ( 0 , 360 px ); }
. compact - transport - grid > : deep (. el - form - item : nth - child ( 6 )) { grid - template - columns : 114 px minmax ( 0 , 360 px ); }
. compact - transport - grid > : deep (. el - form - item : nth - child ( 7 )) { grid - template - columns : 114 px minmax ( 0 , 620 px ); margin - top : 2 px ; margin - bottom : 0 ; }
. compact - transport - grid > : deep (. el - form - item__label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 18 px 0 0 ; line - height : 40 px ; }
. 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 : 40 px ; }
. cargo - section { min - width : 0 ; padding - bottom : 4 px ; }
. cargo - table - heading { margin - bottom : 8 px ; }
. cargo - table - toolbar { display : flex ; align - items : center ; justify - content : space - between ; gap : 16 px ; margin - bottom : 10 px ; }
. cargo - import - actions { display : flex ; align - items : center ; gap : 10 px ; }
. cargo - import - input { position : absolute ; width : 1 px ; height : 1 px ; overflow : hidden ; opacity : 0 ; pointer - events : none ; }
. cargo - import - actions : deep (. el - button ) { min - height : 38 px ; margin - left : 0 ; padding : 8 px 17 px ; }
. cargo - import - actions . cargo - upload - button { border - style : dashed ; border - width : 2 px ; border - color : # b8c5ff ; color : # 637 fec ; }
. 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 : # 4 d5a6c ; }
. cargo - import - actions . cargo - download - button : deep ( svg ) { color : # 09 a8d8 ; }
. cargo - table - actions { display : flex ; align - items : center ; gap : 8 px ; }
. cargo - table - actions : deep (. el - button ) { margin - left : 0 ; }
. cargo - table - wrap { width : 100 % ; overflow - x : auto ; border : 1 px solid # e6eaf0 ; border - radius : 6 px ; 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 : # 4 d5a6c ; font - size : 12 px ; }
. cargo - edit - table th { height : 44 px ; padding : 7 px 8 px ; border - bottom : 1 px solid # e6eaf0 ; background : # f4f5fb ; color : # 637 fec ; font - size : 11 px ; font - weight : 650 ; text - align : left ; white - space : nowrap ; }
. cargo - edit - table th small { margin - left : 3 px ; color : # e8a20c ; font - size : 9 px ; font - weight : 600 ; }
. cargo - edit - table td { width : 130 px ; height : 58 px ; padding : 7 px 6 px ; border - bottom : 1 px 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 : 3 px 2 px 0 ; font - size : 9 px ; 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 : 34 px ; padding : 1 px 8 px ; box - shadow : none ; }
. cargo - edit - table : deep (. el - input__wrapper : hover ), . cargo - edit - table : deep (. el - select__wrapper : hover ) { box - shadow : 0 0 0 1 px # b9c8f5 inset ; }
. cargo - edit - table : deep (. el - input__inner ) { font - size : 11 px ; text - align : left ; }
. cargo - check - cell , . cargo - index - cell { width : 38 px ! important ; text - align : center ! important ; vertical - align : middle ! important ; }
. cargo - image - column { width : 62 px ! important ; text - align : center ! important ; }
. cargo - number - column { width : 92 px ! important ; }
2026-09-11 15:38:28 +08:00
. required - field - mark { margin - right : 2 px ; color : var ( -- el - color - danger ); }
2026-08-12 17:46:46 +08:00
. cargo - dimensions - column { width : 240 px ! important ; }
. cargo - operation - column { width : 76 px ! important ; text - align : center ! important ; }
2026-09-11 16:50:23 +08:00
. cargo - table - wrap . is - dropship . cargo - operation - column { width : 148 px ! important ; }
. cargo - sku - input { position : relative ; width : 100 % ; }
. mixed - sku - badge { position : absolute ; top : - 10 px ; right : 2 px ; padding : 1 px 5 px ; border - radius : 999 px ; background : # eaf6f2 ; color : var ( -- primary ); font - size : 9 px ; font - weight : 650 ; line - height : 16 px ; white - space : nowrap ; }
. mixed - sku - detail - row { background : # f8fbff ; }
. shared - carton - value { display : inline - flex ; min - height : 30 px ; align - items : center ; color : # 8 a97a8 ; font - size : 12 px ; white - space : nowrap ; }
. derived - sku - value { display : inline - flex ; min - height : 30 px ; align - items : center ; color : # 172 b4d ; font - weight : 650 ; }
2026-09-01 20:38:12 +08:00
. cargo - switch - column { width : 112 px ! important ; text - align : center ! important ; vertical - align : middle ! important ; }
. cargo - switch - cell { display : flex ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 4 px ; }
. cargo - switch - cell small { color : # b54708 ; font - size : 9 px ; line - height : 1.25 ; white - space : normal ; }
2026-08-12 17:46:46 +08:00
. cargo - table - image { position : relative ; display : flex ; width : 34 px ; height : 34 px ; align - items : center ; justify - content : center ; margin : 0 auto ; border : 1 px dashed # d6dce5 ; border - radius : 4 px ; color : # 788493 ; }
. cargo - table - image img { width : 100 % ; height : 100 % ; border - radius : 3 px ; object - fit : contain ; }
. cargo - table - image : deep (. el - button ) { width : 30 px ; height : 30 px ; padding : 0 ; }
. cargo - table - image . cargo - image - remove { position : absolute ; top : - 9 px ; right : - 9 px ; width : 19 px ; height : 19 px ; border : 1 px solid # ffd7d7 ; border - radius : 50 % ; background : # fff ; }
. cargo - table - dimensions { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ); gap : 3 px ; align - items : center ; }
. cargo - table - dimensions > span { color : # 9 aa4b2 ; }
. cargo - table - hint { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 9 px ; padding : 10 px 2 px 0 ; color : # 8 b96a6 ; font - size : 11 px ; }
. cargo - table - hint :: before { color : # e5a512 ; content : '●' ; font - size : 8 px ; }
. cargo - table - hint i { width : 3 px ; height : 3 px ; border - radius : 50 % ; background : # aab2bd ; }
. cargo - entry { padding : 18 px 20 px ; border : 1 px solid # e2e7ee ; background : # fff ; box - shadow : 0 2 px 8 px rgba ( 15 , 31 , 58 , .025 ); }
2026-08-03 15:50:24 +08:00
. cargo - entry + . cargo - entry { margin - top : 12 px ; }
2026-08-12 17:46:46 +08:00
. cargo - entry > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 16 px ; }
. cargo - field - section + . cargo - field - section { margin - top : 18 px ; padding - top : 16 px ; border - top : 1 px solid # edf0f5 ; }
. cargo - field - section h4 { margin : 0 0 14 px ; color : # 516071 ; font - size : 12 px ; }
2026-08-07 18:44:58 +08:00
. cargo - form - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); column - gap : 12 px ; }
. cargo - form - grid -- basic { grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); }
. product - image - field { grid - row : span 2 ; }
. product - image - control { display : grid ; grid - template - columns : 78 px minmax ( 0 , 1 fr ); align - items : center ; gap : 7 px 10 px ; width : 100 % ; }
. product - image - preview , . product - image - placeholder { position : relative ; display : flex ; width : 78 px ; height : 78 px ; grid - row : 1 / span 3 ; align - items : center ; justify - content : center ; overflow : hidden ; border : 1 px 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 : 4 px ; right : 4 px ; width : 26 px ; height : 26 px ; }
. product - image - placeholder { flex - direction : column ; gap : 5 px ; font - size : 10 px ; }
. product - image - control small { color : var ( -- muted ); font - size : 10 px ; line - height : 1.4 ; }
. product - image - error { grid - column : 2 ; overflow - wrap : anywhere ; color : # b42318 ; font - size : 10 px ; 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 , 1 fr ) auto minmax ( 0 , 1 fr ) auto minmax ( 0 , 1 fr ); align - items : center ; gap : 6 px ; }
. dimension - inputs > span { color : var ( -- muted ); }
. cargo - detail - image { display : flex ; width : 112 px ; height : 112 px ; align - items : center ; justify - content : center ; margin - top : 12 px ; overflow : hidden ; border : 1 px solid var ( -- border ); background : # fff ; color : var ( -- muted ); font - size : 10 px ; text - align : center ; }
. cargo - detail - image img { width : 100 % ; height : 100 % ; object - fit : contain ; }
2026-09-18 16:48:35 +08:00
. order - detail - heading { display : flex ; min - height : 50 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 5 px 0 ; border - bottom : 1 px solid var ( -- border ); }
. detail - heading - main , . detail - heading - actions { display : flex ; min - width : 0 ; align - items : center ; gap : 12 px ; }
. 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 : 18 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. order - detail - heading p { margin : 3 px 0 0 ; overflow : hidden ; color : var ( -- muted ); font - size : 11 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. order - detail - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 0 ; margin : 0 ; border - top : 1 px solid var ( -- border ); border - left : 1 px solid var ( -- border ); }
. order - detail - grid div { display : grid ; min - width : 0 ; min - height : 34 px ; grid - template - columns : 78 px minmax ( 0 , 1 fr ); align - items : center ; gap : 7 px ; padding : 3 px 8 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. order - detail - grid dt { color : var ( -- muted ); font - size : 10 px ; white - space : nowrap ; }
. order - detail - grid dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; font - size : 12 px ; font - weight : 600 ; }
. detail - subline { display : block ; margin - top : 2 px ; color : var ( -- muted ); font - size : 9 px ; font - weight : 400 ; line - height : 1.35 ; }
. detail - section { margin - top : 10 px ; }
. detail - section h3 { margin : 0 ; padding : 0 0 5 px ; border - bottom : 1 px solid # eaf0f7 ; color : var ( -- navy ); font - size : 13 px ; }
. party - detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; border - left : 1 px solid var ( -- border ); }
. party - detail - grid > div { display : grid ; min - width : 0 ; grid - template - columns : 62 px minmax ( 0 , 1 fr ); gap : 7 px ; padding : 5 px 8 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. party - detail - grid dt { color : var ( -- muted ); font - size : 10 px ; }
. party - detail - grid dd { min - width : 0 ; margin : 0 ; }
. party - detail - grid strong { display : block ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 12 px ; }
. party - detail - grid span { display : block ; margin - top : 3 px ; overflow - wrap : anywhere ; color : var ( -- muted ); font - size : 10 px ; line - height : 1.5 ; }
. customer - remark p { margin : 7 px 0 0 ; overflow - wrap : anywhere ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.65 ; }
. customer - remark { margin - top : 7 px ; padding : 8 px 10 px ; background : # f2f7f6 ; }
2026-08-06 20:14:54 +08:00
. customer - remark strong { color : var ( -- navy ); font - size : 11 px ; }
2026-09-18 16:48:35 +08:00
. fulfillment - summary { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; border - left : 1 px solid var ( -- border ); }
. fulfillment - summary > div { display : grid ; min - width : 0 ; min - height : 34 px ; grid - template - columns : 70 px minmax ( 0 , 1 fr ); align - items : center ; gap : 7 px ; padding : 3 px 8 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
2026-08-06 13:56:13 +08:00
. fulfillment - summary dt { color : var ( -- muted ); font - size : 10 px ; }
2026-09-18 16:48:35 +08:00
. fulfillment - summary dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 11 px ; font - weight : 650 ; }
. cabinet - progress - detail { border - top : 1 px solid var ( -- border ); }
. cabinet - progress - detail > strong { display : block ; padding : 7 px 0 5 px ; color : var ( -- navy ); font - size : 11 px ; }
. cabinet - progress - table { margin - top : 0 ; }
. detail - inline - empty { margin : 0 ; padding : 7 px 9 px ; background : # f7f9fb ; color : var ( -- muted ); font - size : 10 px ; }
. express - information { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; border - left : 1 px solid var ( -- border ); }
. express - information > div { display : grid ; min - width : 0 ; min - height : 40 px ; grid - template - columns : 70 px minmax ( 0 , 1 fr ); align - items : center ; gap : 8 px ; padding : 6 px 10 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. express - information dt { color : var ( -- muted ); font - size : 10 px ; }
. express - information dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 11 px ; font - weight : 650 ; }
. express - empty { display : flex ; align - items : center ; gap : 9 px ; padding : 7 px 9 px ; border : 1 px dashed # cbd5da ; color : var ( -- muted ); }
2026-07-30 17:59:42 +08:00
. express - empty svg { flex : 0 0 20 px ; color : var ( -- primary ); }
. express - empty div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 4 px ; }
. express - empty strong { color : var ( -- navy ); font - size : 13 px ; }
. express - empty span { font - size : 11 px ; line - height : 1.5 ; }
2026-09-18 16:48:35 +08:00
. cargo - summary { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; border - left : 1 px solid var ( -- border ); }
. cargo - summary div { display : grid ; min - width : 0 ; min - height : 34 px ; grid - template - columns : 54 px minmax ( 0 , 1 fr ); align - items : center ; gap : 7 px ; padding : 3 px 8 px ; border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
. cargo - summary dt { color : var ( -- muted ); font - size : 10 px ; }
. cargo - summary dd { margin : 0 ; color : var ( -- navy ); font - size : 11 px ; font - weight : 700 ; }
2026-08-13 17:12:05 +08:00
. cargo - detail - table - wrap { overflow : hidden ; margin - top : 10 px ; border : 1 px solid var ( -- border ); border - radius : 5 px ; background : # fff ; }
. cargo - detail - table { width : 100 % ; font - size : 11 px ; }
. cargo - detail - table : deep (. el - table__header th . el - table__cell ) { height : 42 px ; padding : 0 ; background : # f7f8fb ; color : # 657187 ; font - size : 11 px ; font - weight : 650 ; white - space : nowrap ; }
. cargo - detail - table : deep (. el - table__body td . el - table__cell ) { height : 48 px ; padding : 0 ; color : # 4 f5b6d ; vertical - align : middle ; }
. cargo - detail - table : deep (. cell ) { min - width : 0 ; padding : 0 10 px ; line - height : 18 px ; white - space : nowrap ; }
. cargo - detail - thumbnail { display : flex ; width : 34 px ; height : 34 px ; align - items : center ; justify - content : center ; margin : 0 auto ; overflow : hidden ; border : 1 px solid # e1e6ee ; border - radius : 3 px ; color : var ( -- muted ); }
. cargo - detail - thumbnail img { width : 100 % ; height : 100 % ; object - fit : contain ; }
. cargo - detail - pagination { min - height : 52 px ; justify - content : flex - end ; box - sizing : border - box ; padding : 9 px 14 px ; border - top : 1 px solid var ( -- border ); }
2026-08-06 20:14:54 +08:00
: deep (. el - timeline - item__content p ), : deep (. el - timeline - item__content > span ) { color : var ( -- muted ); }
2026-07-30 17:59:42 +08:00
: deep (. el - timeline - item__content p ) { margin : 5 px 0 ; font - size : 12 px ; }
: deep (. el - timeline - item__content > span ) { font - size : 11 px ; }
2026-07-30 21:23:06 +08:00
@ media ( max - width : 1280 px ) {
2026-08-13 17:12:05 +08:00
. first - leg - query - toolbar : deep (. query - toolbar__filters ) { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
2026-09-18 16:48:35 +08:00
. order - detail - grid , . fulfillment - summary { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
2026-07-30 21:23:06 +08:00
}
2026-08-03 17:44:42 +08:00
@ media ( min - width : 721 px ) and ( max - width : 1050 px ) {
2026-08-07 19:23:46 +08:00
. cargo - form - grid , . cargo - form - grid -- basic { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. product - image - field { grid - row : auto ; }
2026-08-03 17:44:42 +08:00
}
2026-07-30 17:59:42 +08:00
@ media ( max - width : 720 px ) {
2026-08-13 17:12:05 +08:00
. first - leg - query - toolbar { padding : 12 px ; }
2026-08-03 17:44:42 +08:00
. 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 : 10 px ; }
2026-07-30 17:59:42 +08:00
. order - eligibility { flex - wrap : wrap ; }
. order - eligibility . el - button { width : 100 % ; min - height : 44 px ; }
2026-08-07 18:44:58 +08:00
. order - form - grid , . order - detail - grid , . cargo - form - grid , . cargo - form - grid -- basic , . party - detail - grid { grid - template - columns : 1 fr ; }
. product - image - field { grid - row : auto ; }
. product - dimensions - field , . cargo - remark - field { grid - column : auto ; }
2026-08-06 20:14:54 +08:00
. fulfillment - summary { grid - template - columns : 1 fr ; }
2026-09-18 16:48:35 +08:00
. fulfillment - summary > div { border - right : 1 px solid var ( -- border ); border - bottom : 1 px solid var ( -- border ); }
2026-08-06 13:56:13 +08:00
. fulfillment - summary > div : last - child { border - bottom : 0 ; }
2026-08-04 12:01:05 +08:00
. prototype - form - row , . prototype - form - row -- wide , . prototype - form - row -- compact , . prototype - form - row -- short , . prototype - form - row -- tabs { display : block ; margin - bottom : 18 px ; }
. prototype - form - row : deep (. el - form - item__label ) { min - height : auto ; justify - content : flex - start ; padding : 0 0 8 px ; line - height : 1.4 ; }
. selected - destination { width : 100 % ; margin - left : 0 ; }
. address - book - control { grid - template - columns : 1 fr auto ; }
. address - book - control : deep (. el - select ) { grid - column : 1 / - 1 ; }
2026-08-03 15:50:24 +08:00
. segmented - options { width : 100 % ; align - items : stretch ; flex - direction : column ; }
. segmented - options : deep (. el - radio - button__inner ) { border : 1 px solid var ( -- border ); border - radius : 6 px ! important ; box - shadow : none ! important ; }
2026-08-12 17:46:46 +08:00
. warehouse - options { width : 100 % ; gap : 12 px 18 px ; }
. compact - transport - grid > : deep (. el - form - item ), . compact - transport - grid > : deep (. el - form - item : nth - child ( 7 )) { display : block ; margin - bottom : 14 px ; }
2026-09-16 16:42:08 +08:00
. compact - transport - grid > : deep (. pricing - tax - mode - field ) { display : none ; }
2026-08-12 17:46:46 +08:00
. compact - transport - grid > : deep (. el - form - item__label ) { min - height : auto ; justify - content : flex - start ; padding : 0 0 8 px ; line - height : 1.4 ; }
. cargo - table - toolbar { align - items : stretch ; flex - direction : column ; }
. cargo - import - actions , . cargo - table - actions { flex - wrap : wrap ; }
2026-08-19 09:14:38 +08:00
. price - estimate__result { grid - template - columns : 1 fr ; }
. price - estimate__result > div { min - height : auto ; border - right : 0 ; border - bottom : 1 px 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 ; }
2026-08-03 15:50:24 +08:00
. form - span - full { grid - column : auto ; }
2026-07-30 17:59:42 +08:00
. editor - actions : deep (. el - button ) { min - height : 44 px ; }
2026-07-30 21:23:06 +08:00
. order - load - state { min - height : 240 px ; grid - template - columns : 1 fr ; justify - items : start ; }
2026-07-31 15:34:22 +08:00
. supporting - data - error { grid - template - columns : auto minmax ( 0 , 1 fr ); }
. supporting - data - error . el - button { grid - column : 1 / - 1 ; width : 100 % ; min - height : 44 px ; }
2026-07-30 21:23:06 +08:00
. order - load - state . el - button { width : 100 % ; min - height : 44 px ; }
2026-09-18 16:48:35 +08:00
. express - information { grid - template - columns : 1 fr ; }
2026-07-30 17:59:42 +08:00
. cargo - summary { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. cargo - summary div : nth - child ( 2 ) { border - right : 0 ; }
. cargo - summary div : nth - child ( - n + 2 ) { border - bottom : 1 px solid var ( -- border ); }
}
2026-08-06 20:14:54 +08:00
@ media ( max - width : 360 px ) {
2026-08-13 17:12:05 +08:00
. cargo - detail - pagination { justify - content : flex - start ; overflow - x : auto ; }
2026-08-06 20:14:54 +08:00
}
2026-08-01 12:17:33 +08:00
@ media ( max - width : 480 px ) {
2026-09-18 16:48:35 +08:00
. first - leg - detail - page { padding - bottom : 22 px ; }
. 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 ; }
2026-08-01 12:17:33 +08:00
. editor - actions { display : grid ; width : 100 % ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
. 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 ; }
}
2026-07-30 17:59:42 +08:00
</ style >