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-05 15:55:02 +08:00
import { useRouter } from 'vue-router'
2026-08-01 12:17:33 +08:00
import { ElMessage , ElMessageBox , type FormInstance , type FormItemRule , type FormRules } from 'element-plus'
2026-08-03 17:44:42 +08:00
import { AlertTriangle , Box , Eye , FilePlus2 , MapPin , PackageCheck , Pencil , Plus , Send , Trash2 } from '@lucide/vue'
2026-07-30 17:59:42 +08:00
import { FIRST_LEG_ORDER_STATUSES , FIRST_LEG_TRANSPORT_MODES , 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-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-06 13:56:13 +08:00
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
2026-08-04 21:01:34 +08:00
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
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-04 12:01:05 +08:00
import type { FirstLegPartyRole , FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
2026-08-03 18:52:30 +08:00
import type { 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-07-30 17:59:42 +08:00
const store = useFirstLegOrdersStore ()
const tenantStore = useTenantAdministrationStore ()
const auth = useAuthStore ()
const router = useRouter ()
2026-08-05 15:55:02 +08:00
const props = withDefaults ( defineProps < { createPage ?: boolean } > (), { createPage : false })
const isCreatePage = computed (() => props . createPage )
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-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-08-06 13:56:13 +08:00
const filters = ref < QueryModel >({ keyword : '' , status : '' , createdRange : [] })
2026-08-01 12:17:33 +08:00
const appliedFilters = ref < QueryModel >({ ... filters . value })
const queryFields : QueryField [] = [
2026-08-07 10:45:15 +08:00
{ key : 'keyword' , type : 'input' , placeholder : '订单号、入仓码、渠道或仓库' , props : { 'aria-label' : '订单关键词' } },
2026-08-01 12:17:33 +08:00
{ key : 'status' , type : 'select' , placeholder : '全部状态' , options : FIRST_LEG_ORDER_STATUSES . map (( item ) => ({ label : item . label , value : item . code })), props : { 'aria-label' : '订单状态' } },
{ key : 'createdRange' , type : 'daterange' , placeholder : '订单创建日期' , props : { 'aria-label' : '订单创建日期' } },
]
2026-08-03 15:50:24 +08:00
const channelOptions = [
{ code : 'SEA-US-GENERAL' , name : '美线普货专线' },
{ code : 'SEA-US-SENSITIVE' , name : '美线敏感货专线' },
{ code : 'SEA-US-EXPRESS' , name : '美线快线' },
{ code : 'SEA-US-FBA' , name : '美线 FBA 直送' },
]
2026-08-01 12:17:33 +08:00
const orderColumns : DataTableColumn [] = [
{ key : 'actions' , label : '操作' , width : 180 , fixed : 'left' , slotName : 'actions' },
{ key : 'order' , label : '订单' , minWidth : 190 , slotName : 'order' },
2026-08-06 13:56:13 +08:00
{ key : 'route' , label : '目的地 / 渠道' , minWidth : 190 , slotName : 'route' },
2026-08-01 12:17:33 +08:00
{ key : 'warehouse' , label : '收货仓' , prop : 'receivingWarehouseName' , minWidth : 120 },
2026-08-06 13:56:13 +08:00
{ key : 'cargo' , label : '货物摘要' , minWidth : 150 , slotName : 'cargo' },
2026-08-01 12:17:33 +08:00
{ key : 'status' , label : '状态' , width : 120 , slotName : 'status' },
{ key : 'updatedAt' , label : '更新时间' , width : 155 , slotName : 'updatedAt' },
]
const tablePage = ref ( 1 )
const tablePageSize = ref ( 10 )
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 detailOpen = ref ( false )
const editingId = ref < string >()
2026-07-30 21:23:06 +08:00
const openingEditor = ref ( false )
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-04 21:01:34 +08:00
const inboundBilling = ref < MerchantInboundBilling | null >( null )
const inboundBillingLoading = ref ( false )
const inboundBillingUnavailable = ref ( false )
const inboundBillingUnavailableMessage = ref ( '入库及费用结果尚未发布' )
const inboundBillingError = 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-08-04 21:01:34 +08:00
let inboundBillingRequestSequence = 0
2026-07-30 17:59:42 +08:00
2026-08-04 11:14:29 +08:00
interface CachedPrivateAddress {
id : string
label : string
consigneePartyId : string
consigneePartySnapshot : NonNullable < FirstLegOrderDraft [' consigneePartySnapshot '] >
consignee : string
destination : string
destinationCountryCode : string
destinationWarehouseName : string
}
const privateAddresses = ref < CachedPrivateAddress [] >([])
const selectedPrivateAddressId = ref ( '' )
function privateAddressStorageKey () {
return `cc:first-leg:private-addresses: ${ auth . user ? . id ?? 'anonymous' } `
}
function loadPrivateAddresses () {
try {
const value = JSON . parse ( localStorage . getItem ( privateAddressStorageKey ()) ?? '[]' ) as unknown
privateAddresses . value = Array . isArray ( value ) ? value . filter (( item ) : item is CachedPrivateAddress => Boolean ( item && typeof item === 'object' && 'id' in item && 'consigneePartySnapshot' in item )) : []
} catch {
privateAddresses . value = []
}
}
function persistPrivateAddresses () {
localStorage . setItem ( privateAddressStorageKey (), JSON . stringify ( privateAddresses . value ))
}
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' }],
channelCode : [{ required : true , message : '请输入渠道编码' , trigger : 'blur' }],
channelName : [{ required : true , message : '请输入渠道名称' , trigger : 'blur' }],
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-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 ) {
return value ? . trim () || '-'
}
2026-07-30 17:59:42 +08:00
function emptyDraft () : FirstLegOrderDraft {
return {
referenceNo : '' ,
transportMode : 'FCL' ,
businessType : 'EXPORT' ,
origin : '' ,
destination : '' ,
receivingWarehouseId : '' ,
expectedDepartureDate : '' ,
shipperPartyId : '' ,
shipperPartySnapshot : null ,
shipper : '' ,
consigneePartyId : '' ,
consigneePartySnapshot : null ,
consignee : '' ,
priority : 'NORMAL' ,
customerRemark : '' ,
tradeTerm : 'FOB' ,
destinationCountryCode : 'US' ,
2026-08-04 11:14:29 +08:00
destinationWarehouseId : '' ,
2026-07-30 17:59:42 +08:00
destinationWarehouseName : '' ,
pickupMethod : 'SELF_EXPRESS' ,
channelCode : 'SEA-US-FBA' ,
2026-08-03 15:50:24 +08:00
channelName : '美线 FBA 直送' ,
2026-07-30 17:59:42 +08:00
taxMode : 'BUY_DECLARATION' ,
customsInspection : false ,
destinationType : 'FBA' ,
amazonWarehouseCode : '' ,
shipmentId : '' ,
items : [ createEmptyCargoItem ( `new-cargo- ${ ++ cargoSequence } ` )],
}
}
const form = reactive < FirstLegOrderDraft >( emptyDraft ())
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-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
const filteredOrders = computed (() => store . orders . filter (( order ) => {
2026-08-01 12:17:33 +08:00
const keywordValue = appliedFilters . value . keyword
const statusValue = appliedFilters . value . status
const createdRangeValue = appliedFilters . value . createdRange
const keyword = typeof keywordValue === 'string' ? keywordValue . trim (). toLowerCase () : ''
const status = typeof statusValue === 'string' ? statusValue as FirstLegOrderStatus | '' : ''
const createdRange = Array . isArray ( createdRangeValue ) ? createdRangeValue . filter (( value ) : value is string => typeof value === 'string' ) : []
2026-08-07 10:45:15 +08:00
const matchesKeyword = ! keyword || [ order . orderNo , order . businessSummary . inboundCode , order . referenceNo , order . origin , order . destination , order . items . map (( item ) => item . cargoNameCn ). join ( ' ' )]
2026-07-30 17:59:42 +08:00
. filter ( Boolean )
. some (( value ) => String ( value ). toLowerCase (). includes ( keyword ))
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-08-06 13:56:13 +08:00
return matchesKeyword && matchesStatus && matchesDate
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
const detailOrder = computed (() => detailId . value ? store . getById ( detailId . value ) : undefined )
const detailSummary = computed (() => detailOrder . value ? summarizeFirstLegCargo ( detailOrder . value . items ) : null )
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
const transportReference = [ summary . containerNo , summary . cabinetNo , summary . vesselName , summary . voyageNo ]. filter ( Boolean ). join ( ' · ' )
const pickingProgress = summary . expectedPickCount == null
? '待更新'
: ` ${ summary . pickedCount ?? 0 } / ${ summary . expectedPickCount } `
return [
{ label : '实际箱数' , value : formatMetric ( summary . totalPackageCount ?? inbound ? . totalPackageCount , '箱' ) },
{ label : '实测毛重' , value : formatMetric ( summary . actualGrossWeightKg ?? inbound ? . actualGrossWeightKg , 'kg' ) },
{ label : '实测体积' , value : formatMetric ( summary . actualVolumeCbm ?? inbound ? . actualVolumeCbm , 'm³' ) },
{ label : '预装清单' , value : preloadStatus ? ` ${ preloadStatus }${ summary . preloadItemCount == null ? '' : ` · ${ summary . preloadItemCount } 项` } ` : '待更新' },
{ label : '拣货进度' , value : pickingProgress },
{ label : '装柜资料' , value : transportReference || summary . sealNo || '待更新' },
{ label : '作业证据' , value : formatMetric ( summary . evidenceCount , '项' ) },
{ label : '计费重量' , value : formatMetric ( summary . chargeableWeightKg , 'kg' ) },
{ label : '派送信息' , value : summary . deliveryTrackingNo || ( summary . deliveryCompletedAt ? `完成于 ${ formatDateTime ( summary . deliveryCompletedAt ) } ` : '待更新' ) },
]
})
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-01 12:17:33 +08:00
const hasActiveFilters = computed (() => {
const values = appliedFilters . value
return Boolean (
( typeof values . keyword === 'string' && values . keyword . trim ())
|| values . status
|| ( Array . isArray ( values . createdRange ) && values . createdRange . length ),
)
})
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-08-06 15:32:07 +08:00
const orderPageRequest = isCreatePage . value
? store . loadWarehouseType ( 'DOMESTIC' , true )
: store . load ()
2026-08-06 14:55:13 +08:00
const [ ordersResult , companyResult , destinationResult ] = 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-07-31 15:34:22 +08:00
])
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-08-04 12:58:04 +08:00
await autofillWarehouseSelections ()
2026-07-31 15:34:22 +08:00
supportingDataLoading . value = false
}
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-08-06 13:56:13 +08:00
function getOrderCargoSummary ( order : FirstLegOrder ) {
if ( order . items . length ) return ` ${ order . items [ 0 ] ? . cargoNameCn || '货物' }${ order . items . length > 1 ? `等 ${ order . items . length } 项` : '' } `
if ( order . businessSummary . totalPackageCount != null ) return ` ${ order . businessSummary . totalPackageCount } 箱`
return '待更新'
}
2026-07-30 18:08:40 +08:00
onMounted (() => {
2026-08-04 11:14:29 +08:00
loadPrivateAddresses ()
2026-07-30 18:08:40 +08:00
void loadPageData ()
2026-07-30 17:59:42 +08:00
})
2026-08-03 17:44:42 +08:00
watch ( isCreatePage , ( isCreate , wasCreate ) => {
if ( ! isCreate || wasCreate ) return
editingId . value = undefined
replaceForm ( emptyDraft ())
resetEditorValidation ()
}, { immediate : true })
2026-08-04 21:01:34 +08:00
watch ( detailOpen , ( open ) => {
if ( ! open ) resetInboundBillingState ()
})
onBeforeUnmount (() => {
resetInboundBillingState ()
})
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-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
}
function draftFromOrder ( order : FirstLegOrder ) : FirstLegOrderDraft {
return {
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 ,
origin : order . origin ,
destination : order . destination ,
receivingWarehouseId : order . receivingWarehouseId ,
expectedDepartureDate : order . expectedDepartureDate ,
shipperPartyId : order . shipperPartyId ,
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-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 ,
taxMode : order . taxMode ,
customsInspection : order . customsInspection ,
destinationType : order . destinationType ,
amazonWarehouseCode : order . amazonWarehouseCode ,
shipmentId : order . shipmentId ,
2026-07-30 21:23:06 +08:00
items : order . items . map (( item ) => ({ ... item })),
2026-07-30 17:59:42 +08:00
}
}
2026-08-03 15:50:24 +08:00
function selectChannel ( code : string ) {
const channel = channelOptions . find (( item ) => item . code === code )
form . channelName = channel ? . name ?? ''
}
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 = ''
selectedPrivateAddressId . value = ''
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 {
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-08-04 11:14:29 +08:00
function selectPrivateAddress ( id : string ) {
const address = privateAddresses . value . find (( item ) => item . id === id )
if ( ! address ) return
form . consigneePartyId = address . consigneePartyId
form . consigneePartySnapshot = { ... address . consigneePartySnapshot }
form . consignee = address . consignee
form . destination = address . destination
form . destinationCountryCode = address . destinationCountryCode
form . destinationWarehouseId = ''
form . destinationWarehouseName = address . destinationWarehouseName
}
function savePrivateAddress () {
2026-08-04 12:01:05 +08:00
if ( ! form . consigneePartySnapshot || ! form . destinationWarehouseName . trim () || ! form . destinationCountryCode . trim ()
|| ! form . consigneePartySnapshot . contactName . trim () || ! form . consigneePartySnapshot . phone . trim ()
|| ! form . consigneePartySnapshot . city . trim () || ! form . consigneePartySnapshot . address . trim ()) {
ElMessage . warning ( '请先完整填写私人地址和收件资料' )
2026-08-04 11:14:29 +08:00
return
}
const snapshot = { ... form . consigneePartySnapshot }
2026-08-04 12:01:05 +08:00
snapshot . name = form . destinationWarehouseName . trim ()
snapshot . countryRegion = form . destinationCountryCode . trim (). toUpperCase ()
const existing = privateAddresses . value . find (( item ) => item . consigneePartySnapshot . address === snapshot . address )
const partyId = existing ? . consigneePartyId || snapshot . id || `private-party- ${ Date . now () } `
snapshot . id = partyId
2026-08-04 11:14:29 +08:00
const saved : CachedPrivateAddress = {
id : existing ? . id ?? globalThis . crypto ? . randomUUID ? .() ?? `private- ${ Date . now () } ` ,
label : ` ${ form . destinationWarehouseName . trim () } · ${ snapshot . address } ` ,
2026-08-04 12:01:05 +08:00
consigneePartyId : partyId ,
2026-08-04 11:14:29 +08:00
consigneePartySnapshot : snapshot ,
2026-08-04 12:01:05 +08:00
consignee : snapshot . name ,
2026-08-04 11:14:29 +08:00
destination : form . destination ,
destinationCountryCode : form . destinationCountryCode . trim (). toUpperCase (),
destinationWarehouseName : form . destinationWarehouseName . trim (),
}
privateAddresses . value = existing
? privateAddresses . value . map (( item ) => item . id === existing . id ? saved : item )
: [ saved , ... privateAddresses . value ]
selectedPrivateAddressId . value = saved . id
persistPrivateAddresses ()
ElMessage . success ( existing ? '私人地址已更新' : '私人地址已保存到当前账号' )
}
function removePrivateAddress () {
if ( ! selectedPrivateAddressId . value ) return
privateAddresses . value = privateAddresses . value . filter (( item ) => item . id !== selectedPrivateAddressId . value )
selectedPrivateAddressId . value = ''
persistPrivateAddresses ()
ElMessage . success ( '已从当前账号的私人地址中移除' )
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 ) : ''
if ( ! warehouseId ) return
2026-07-30 18:08:40 +08:00
try {
2026-08-03 15:50:24 +08:00
await store . loadWarehouseDetail ( warehouseId )
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-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 {
const detailedOrder = await store . loadDetail ( order . id )
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 = ''
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 )
if ( warehouseAvailable ) void loadSelectedWarehouse ( detailedOrder . receivingWarehouseId )
detailOpen . value = false
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 ) {
detailId . value = order . id
detailOpen . value = true
detailLoading . value = true
2026-07-30 18:08:40 +08:00
detailError . value = ''
2026-08-04 21:01:34 +08:00
void loadInboundBilling ( order )
2026-07-30 17:59:42 +08:00
try {
await store . loadDetail ( order . id )
} 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-07-30 18:08:40 +08:00
async function retryDetail () {
const order = detailId . value ? store . getById ( detailId . value ) : undefined
if ( order ) await openDetail ( order )
}
2026-08-04 21:01:34 +08:00
function resetInboundBillingState () {
inboundBillingRequestSequence += 1
inboundBilling . value = null
inboundBillingLoading . value = false
inboundBillingUnavailable . value = false
inboundBillingUnavailableMessage . value = '入库及费用结果尚未发布'
inboundBillingError . value = ''
}
function isCurrentInboundBillingRequest ( sequence : number , orderId : string ) {
return sequence === inboundBillingRequestSequence
&& detailOpen . value
&& detailId . value === orderId
}
async function loadInboundBilling ( order : FirstLegOrder ) {
const sequence = ++ inboundBillingRequestSequence
inboundBilling . value = null
inboundBillingLoading . value = false
inboundBillingUnavailable . value = false
inboundBillingUnavailableMessage . value = '入库及费用结果尚未发布'
inboundBillingError . value = ''
if ( order . status === 'DRAFT' ) {
inboundBillingUnavailable . value = true
inboundBillingUnavailableMessage . value = '订单提交后可查看入库结果'
return
}
inboundBillingLoading . value = true
try {
const result = await getMerchantInboundBilling ( order . id )
if ( ! isCurrentInboundBillingRequest ( sequence , order . id )) return
inboundBilling . value = result
} catch ( error ) {
if ( ! isCurrentInboundBillingRequest ( sequence , order . id )) return
if ( error instanceof ApiError && error . code === 'A0404' ) {
inboundBillingUnavailable . value = true
inboundBillingUnavailableMessage . value = '入库及费用结果尚未发布'
} else {
inboundBillingError . value = error instanceof Error ? error . message : '入库与费用加载失败'
}
} finally {
if ( isCurrentInboundBillingRequest ( sequence , order . id )) inboundBillingLoading . value = false
}
}
async function retryInboundBilling () {
const order = detailId . value ? store . getById ( detailId . value ) : undefined
if ( order ) await loadInboundBilling ( order )
}
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
}
function removeCargoItem ( index : number ) {
if ( form . items . length === 1 ) {
ElMessage . warning ( '订单至少需要保留一条货物明细' )
return
}
form . items . splice ( index , 1 )
2026-08-01 12:17:33 +08:00
resetEditorValidation ()
}
async function validateEditorForm () {
let fieldsValid = false
try {
fieldsValid = await orderFormRef . value ? . validate () ?? false
} catch {
fieldsValid = false
}
if ( ! fieldsValid ) {
ElMessage . warning ( '请检查并完善表单中标红的必填信息' )
return false
}
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
const order = await store . submit ( cloneDraft ( form ), tenantStore . company . companyName , editingId . value , knownCompanyAuditStatus )
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
detailId . value = order . id
detailOpen . value = true
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 | '' : ''
const keyword = typeof model . keyword === 'string' ? model . keyword . trim () : ''
const range = Array . isArray ( createdRange ) ? createdRange . filter (( value ) : value is string => typeof value === 'string' ) : []
await store . load ({
keyword : keyword || undefined ,
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-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-08-03 17:44:42 +08:00
< section class = "first-leg-page" : class = "{ 'is-create-route': isCreatePage }" >
< template v-if = "!isCreatePage" >
2026-08-06 13:56:13 +08:00
< QueryToolbar v-model = "filters" class="first-leg-query-toolbar" :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-08-03 10:51:53 +08:00
< div class = "order-table-toolbar" >
< div >< strong > 订单列表 </ strong >< span > 共 {{ filteredOrders . length }} 条 </ span ></ div >
2026-08-06 18:02:02 +08:00
< el-button v-if = "auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><FilePlus2 :size="17" /> 创建订单 </ el-button >
2026-08-03 10:51:53 +08:00
</ div >
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-07-30 21:23:06 +08:00
< div v-if = "store.loadError" class="order-load-state" role="alert" >
< AlertTriangle :size = "24" />
< div >< strong > 订单数据加载失败 </ strong >< span >{{ store . loadError }}</ span ></ div >
< el-button :loading = "store.loading" @click ="loadPageData" > 重新加载 </ el -button >
</ div >
< template v-else >
2026-08-01 12:17:33 +08:00
< div class = "desktop-order-table" >
2026-08-06 13:56:13 +08:00
< DataTable v -model:page = "tablePage" v -model:page-size = "tablePageSize" :data = "pagedOrders" :columns = "orderColumns" :loading = "store.loading" :total = "filteredOrders.length" : page -sizes ="[ 10 , 20 , 50 ]" row -key =" id " empty -text =" 暂无订单 " >
2026-08-01 12:17:33 +08:00
< template # actions = "{ row }" >
2026-07-30 21:23:06 +08:00
< div class = "order-row-actions" >
< el-button link type = "primary" @click ="openDetail(row as FirstLegOrder)" >< Eye :size = "15" /> 详情 </ el-button >
< el-button v-if = "canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" /> 编辑 </ el-button >
</ div >
</ template >
2026-08-01 12:17:33 +08:00
< template # order = "{ row }" >
2026-07-30 17:59:42 +08:00
< button class = "order-number-link" type = "button" @click ="openDetail(row as FirstLegOrder)" >{{ ( row as FirstLegOrder ) .orderNo || ' 未提交草稿 ' }}</ button >
2026-08-07 10:45:15 +08:00
< span v-if = "(row as FirstLegOrder).businessSummary.inboundCode" class="order-reference" > 入仓码 {{ ( row as FirstLegOrder ) .businessSummary.inboundCode }}</ span >
2026-07-30 17:59:42 +08:00
< span class = "order-reference" >{{ ( row as FirstLegOrder ). referenceNo || '无客户参考号' }}</ span >
</ template >
2026-08-06 13:56:13 +08:00
< template # route = "{ row }" >< strong >{{ ( row as FirstLegOrder ). destination || '目的地待更新' }}</ strong >< span class = "order-reference" >{{ ( row as FirstLegOrder ). transportMode ? getFirstLegTransportModeLabel (( row as FirstLegOrder ). transportMode ) : ( row as FirstLegOrder ). channelName || '渠道待更新' }}</ span ></ template >
< template # cargo = "{ row }" >{{ getOrderCargoSummary ( row as FirstLegOrder ) }}</ template >
2026-08-01 12:17:33 +08:00
< template # status = "{ row }" >< el-tag : type = "getFirstLegStatusTone((row as FirstLegOrder).status)" effect = "light" >{{ getFirstLegStatusLabel (( row as FirstLegOrder ). status ) }}</ el-tag ></ template >
< template # updatedAt = "{ row }" >{{ formatDateTime (( row as FirstLegOrder ). updatedAt ) }}</ template >
< template # empty >
< el-empty :description = "emptyDescription" >
2026-08-06 18:02:02 +08:00
< el-button v-if = "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 >
</ div >
< div v-loading = "store.loading" class="mobile-order-surface" >
2026-07-30 17:59:42 +08:00
< div v-if = "filteredOrders.length" class="mobile-order-list" >
< article v-for = "order in filteredOrders" :key="order.id" class="mobile-order-card" >
< header >< button type = "button" @click ="openDetail(order)" >{{ order.orderNo || ' 未提交草稿 ' }}</ button >< el-tag :type = "getFirstLegStatusTone(order.status)" effect = "light" >{{ getFirstLegStatusLabel ( order . status ) }}</ el-tag ></ header >
2026-08-07 10:45:15 +08:00
< span v-if = "order.businessSummary.inboundCode" class="order-reference" > 入仓码 {{ order.businessSummary.inboundCode }}</ span >
2026-07-30 17:59:42 +08:00
< span class = "order-reference" >{{ order . referenceNo || '无客户参考号' }}</ span >
2026-08-06 13:56:13 +08:00
< dl >< div >< dt > 目的地 </ dt >< dd >{{ order . destination || '待更新' }}</ dd ></ div >< div >< dt > 收货仓 </ dt >< dd >{{ order . receivingWarehouseName || '待更新' }}</ dd ></ div >< div >< dt > 货物摘要 </ dt >< dd >{{ getOrderCargoSummary ( order ) }}</ dd ></ div >< div >< dt > 运输渠道 </ dt >< dd >{{ order . transportMode ? getFirstLegTransportModeLabel ( order . transportMode ) : order . channelName || '待更新' }}</ dd ></ div ></ dl >
2026-07-30 21:23:06 +08:00
< footer >< span > 更新于 {{ formatDateTime ( order . updatedAt ) }}</ span >< div >< el-button link type = "primary" @click ="openDetail(order)" >< Eye :size = "15" /> 详情 </ el-button >< el-button v-if = "canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" /> 编辑 </ el-button ></ div ></ footer >
2026-07-30 17:59:42 +08:00
</ article >
</ div >
2026-07-30 21:23:06 +08:00
< el-empty v-else :description = "emptyDescription" >
2026-08-06 18:02:02 +08:00
< el-button v-if = "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-07-30 17:59:42 +08:00
</ el-empty >
2026-08-01 12:17:33 +08:00
</ div >
2026-07-30 21:23:06 +08:00
</ template >
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-08-06 13:56:13 +08:00
< FirstLegOrderEditorShell v-model = "editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor" >
2026-08-06 15:32:07 +08:00
< el-form ref = "orderFormRef" v-loading = "isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll -to-error >
2026-08-03 15:50:24 +08:00
< section class = "order-form-section prototype-section" >
2026-08-04 12:58:04 +08:00
< div class = "order-section-heading" >< div >< h3 > 发货信息 </ h3 >< p > 先选择货物如何送到国内收货仓 , 并填写本次运输安排 。 </ p ></ div ></ div >
< div class = "prototype-form-flow shipping-form-flow" >
< el-form-item label = "取货方式" prop = "pickupMethod" required class = "prototype-form-row prototype-form-row--tabs" >< el-radio-group v-model = "form.pickupMethod" class="segmented-options" @change="changePickupMethod"><el-radio-button value="SELF_EXPRESS">快递自送</el-radio-button><el-radio-button value="PICKUP">我司提货</el-radio-button><el-radio-button value="SELF_TRUCK" > 卡车自送 </ el -radio-button ></ el-radio-group ></ el-form-item >
< template v-if = "form.pickupMethod === 'PICKUP' && form.shipperPartySnapshot" >
< el-form-item label = "提货联系人" prop = "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-06 18:02:02 +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-button v-for="warehouse in availableWarehouses" :key="warehouse.id" :value="warehouse.id" >{{ warehouse.name }} </ el -radio-button ></ el-radio-group ></ el-form-item >
2026-08-04 12:58:04 +08:00
</ div >
< div v-if = "selectedWarehouse" class="selected-warehouse" aria-label="所选收货仓详情"><MapPin :size="17" /><div><span>{{ selectedWarehouse.address ? `${selectedWarehouse.province}${selectedWarehouse.city} · ${selectedWarehouse.address}` : '仓库地址详情暂不可用' }}</span><small>{{ selectedWarehouse.businessHours ? `收货时间:${selectedWarehouse.businessHours}` : '收货时间暂不可用' }}</small><small v-if="selectedWarehouse.contactName || selectedWarehouse.contactPhone" > 联系人 : {{ selectedWarehouse.contactName || '-' }} {{ selectedWarehouse.contactPhone }}</ small ></ div ></ div >
< div class = "order-form-grid compact-transport-grid" >
< el-form-item label = "渠道" prop = "channelCode" required >< el-select v-model = "form.channelCode" placeholder="请选择运输渠道" @change="selectChannel"><el-option v-if="form.channelCode && !channelOptions.some((channel) => channel.code === form.channelCode)" :label="form.channelName || form.channelCode" :value="form.channelCode" /><el-option v-for="channel in channelOptions" :key="channel.code" :label="channel.name" :value="channel.code" /></ el-select ></ el-form-item >
< el-form-item label = "买单 / 退税" prop = "taxMode" required >< el-radio-group v-model = "form.taxMode"><el-radio value="BUY_DECLARATION">买单报关</el-radio><el-radio value="TAX_REFUND" > 退税报关 </ el -radio ></ el-radio-group ></ el-form-item >
< el-form-item label = "运输方式" prop = "transportMode" required >< el-radio-group v-model = "form.transportMode"><el-radio v-for="item in FIRST_LEG_TRANSPORT_MODES" :key="item.code" :value="item.code" >{{ item.label }} </ el -radio ></ el-radio-group ></ el-form-item >
< el-form-item label = "是否商检" prop = "customsInspection" required >< el-radio-group v-model = "form.customsInspection"><el-radio :value="true">是</el-radio><el-radio :value="false" > 否 </ el -radio ></ el-radio-group ></ el-form-item >
< el-form-item label = "期望出货日期" >< el-date-picker v-model = "form.expectedDepartureDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择期望出货日期" /></ el-form-item >
< el-form-item label = "订单备注" >< el-input v-model = "form.customerRemark" maxlength="1000" placeholder="选填,例如:入仓前请电话联系" /></ el-form-item >
</ div >
</ section >
< section class = "order-form-section prototype-section" >
< div class = "order-section-heading" >< div >< h3 > 目的地信息 </ h3 >< p > 再选择货物最终送达的仓库或私人地址 。 </ p ></ div ></ div >
2026-08-04 12:01:05 +08:00
< div class = "prototype-form-flow destination-form-flow" >
< el-form-item label = "目的仓类型" prop = "destinationType" required class = "prototype-form-row prototype-form-row--tabs" >< el-radio-group v-model = "form.destinationType" class="segmented-options" @change="changeDestinationType"><el-radio-button value="FBA">FBA 仓</el-radio-button><el-radio-button value="OVERSEAS">海外仓</el-radio-button><el-radio-button value="PRIVATE" > 私人仓 </ el -radio-button ></ el-radio-group ></ el-form-item >
< el-form-item v-if = "form.destinationType === 'PRIVATE'" label="私人地址簿" class="prototype-form-row prototype-form-row--wide address-book-row" >
< div class = "address-book-control" >< el-select v-model = "selectedPrivateAddressId" clearable filterable placeholder="选择当前账号保存的私人地址" @change="selectPrivateAddress"><el-option v-for="address in privateAddresses" :key="address.id" :label="address.label" :value="address.id" /></el-select><el-button @click="savePrivateAddress">保存当前地址</el-button><el-button v-if="selectedPrivateAddressId" link type="danger" @click="removePrivateAddress" > 移除 </ el -button ></ div >
</ 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 >
< el-form-item v-if = "form.destinationType === 'FBA'" label="货件编号" prop="shipmentId" required :rules="requiredTextRules('请输入 Shipment ID')" class="prototype-form-row prototype-form-row--compact"><el-input v-model="form.shipmentId" maxlength="64" placeholder="Shipment ID" /></ el-form-item >
< el-form-item label = "跟踪编号" prop = "referenceNo" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 订单请输入 Reference ID') : undefined" class = "prototype-form-row prototype-form-row--compact" >< el-input v-model = "form.referenceNo" maxlength="100" :placeholder="form.destinationType === 'FBA' ? 'Reference ID' : '选填,贵司内部跟踪编号'" /></ el-form-item >
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" >
< div class = "order-section-heading" >< div >< h3 > 货物明细 </ h3 >< p > 先填写常用字段 , 申报和整柜信息可按需展开 。 </ p ></ div >< el-button @click ="addCargoItem" >< Plus :size = "16" /> 添加货物 </ el-button ></ div >
< article v-for = "(item, index) in form.items" :key="item.id" class="cargo-entry" >
2026-07-30 17:59:42 +08:00
< header >< strong > 货物 {{ index + 1 }}</ strong >< el-button link type = "danger" : aria -label ="` 删除货物 $ { index + 1 } `" @click ="removeCargoItem(index)" >< Trash2 :size = "16" /> 删除 </ el-button ></ header >
2026-08-03 15:50:24 +08:00
< div class = "cargo-primary-grid" >
< el-form-item label = "SKU" :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入客户 SKU') : undefined" >< el-input v-model = "item.sku" placeholder="SKU NO." /></ el-form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "中文品名" :prop = "`items.${index}.cargoNameCn`" required :rules = "requiredTextRules('请输入中文品名')" >< el-input v-model = "item.cargoNameCn" placeholder="例如:蓝牙音箱" /></ el-form-item >
2026-08-03 15:50:24 +08:00
< el-form-item label = "英文品名" >< el-input v-model = "item.cargoNameEn" placeholder="Bluetooth speakers" /></ el-form-item >
< el-form-item v-if = "form.destinationType === 'FBA'" label="UPC" :prop="`items.${index}.upc`" required :rules="requiredTextRules('FBA 货物请输入 UPC')"><el-input v-model="item.upc" maxlength="32" placeholder="UPC NO." /></ el-form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "包装类型" :prop = "`items.${index}.packageType`" required :rules = "requiredSelectRules('请选择包装类型')" >< el-select v-model = "item.packageType"><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></ el-select ></ el-form-item >
2026-08-03 15:50:24 +08:00
< el-form-item label = "总数量" :prop = "`items.${index}.pieces`" required : rules = "positiveNumberRules('件数必须为大于 0 的整数', true)" >< el-input-number v-model = "item.pieces" :min="1" controls-position="right" /></ el-form-item >
2026-07-30 17:59:42 +08:00
< el-form-item label = "箱数" >< el-input-number v-model = "item.packageCount" :min="0" controls-position="right" /></ el-form-item >
2026-08-03 15:50:24 +08:00
< el-form-item label = "总毛重 kg" :prop = "`items.${index}.grossWeightKg`" required : rules = "positiveNumberRules('毛重必须大于 0')" >< el-input-number v-model = "item.grossWeightKg" :min="0" :precision="2" controls-position="right" /></ el-form-item >
< el-form-item label = "总体积 m³" :prop = "`items.${index}.volumeCbm`" required : rules = "positiveNumberRules('体积必须大于 0')" >< el-input-number v-model = "item.volumeCbm" :min="0" :precision="3" controls-position="right" /></ el-form-item >
</ div >
< el-collapse class = "cargo-more" >< el-collapse-item title = "更多申报信息" >< div class = "cargo-secondary-grid" >
< el-form-item label = "HS Code" >< el-input v-model = "item.hsCode" maxlength="20" placeholder="例如:8518210000" /></ el-form-item >
2026-07-30 17:59:42 +08:00
< el-form-item label = "申报货值" >< el-input-number v-model = "item.declaredValue" :min="0" :precision="2" controls-position="right" /></ el-form-item >
2026-08-01 12:17:33 +08:00
< el-form-item label = "币种" >< el-input v-model = "item.currency" maxlength="3" placeholder="例如:USD" /></ el-form-item >
2026-07-30 17:59:42 +08:00
< el-form-item label = "危险品" >< el-switch v-model = "item.dangerousGoods" /></ el-form-item >
2026-08-03 15:50:24 +08:00
< template v-if = "form.transportMode === 'FCL'"><el-form-item label="箱型" :prop="`items.${index}.containerType`" required :rules="requiredSelectRules('请选择箱型')"><el-select v-model="item.containerType"><el-option v-for="type in ['20GP', '40GP', '40HQ']" :key="type" :label="type" :value="type" /></el-select></el-form-item><el-form-item label="柜量" :prop="`items.${index}.containerCount`" required :rules="positiveNumberRules('柜量必须为大于 0 的整数', true)"><el-input-number v-model="item.containerCount" :min="0" controls-position="right" /></ el-form-item ></ template >
< el-form-item label = "货物备注" class = "form-span-full" >< el-input v-model = "item.remark" maxlength="300" placeholder="选填,例如:注意防潮" /></ el-form-item >
</ div ></ el-collapse-item ></ el-collapse >
2026-07-30 17:59:42 +08:00
</ article >
2026-08-03 15:50:24 +08:00
< p class = "cargo-tip" > FBA 货物必须填写 SKU 、 UPC 、 货件编号和跟踪编号 ; 提交前请核对重量与体积 。 </ p >
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-08-06 20:14:54 +08:00
< el-drawer v-model = "detailOpen" class="first-leg-detail" title="订单详情" size="820px" >
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-08-06 20:14:54 +08:00
< div >< span >{{ getFirstLegTransportModeLabel ( detailOrder . transportMode ) }}</ span >< h2 >{{ detailOrder . orderNo || '未提交草稿' }}</ h2 >< p >{{ detailOrder . origin || '起运地待更新' }} → {{ detailOrder . destination || '目的地待更新' }}</ p ></ div >
2026-07-30 17:59:42 +08:00
< el-tag :type = "getFirstLegStatusTone(detailOrder.status)" effect = "light" >{{ getFirstLegStatusLabel ( detailOrder . status ) }}</ el-tag >
</ div >
2026-08-06 20:14:54 +08:00
< section class = "detail-section" >
< h3 > 订单与运输 </ h3 >
< dl class = "order-detail-grid" >
2026-08-07 10:45:15 +08:00
< div v-if = "detailOrder.businessSummary.inboundCode" >< dt > 入仓码 </ dt >< dd >{{ detailOrder . businessSummary . inboundCode }}</ dd ></ div >
2026-08-06 20:14:54 +08:00
< div >< dt > 客户参考号 </ dt >< dd >{{ displayValue ( detailOrder . referenceNo ) }}</ dd ></ div >
< div >< dt > 创建时间 </ dt >< dd >{{ formatDateTime ( detailOrder . createdAt ) }}</ dd ></ div >
< div >< dt > 运输方式 </ dt >< dd >{{ getFirstLegTransportModeLabel ( detailOrder . transportMode ) }}</ dd ></ div >
< div >< dt > 贸易条款 </ dt >< dd >{{ displayValue ( detailOrder . tradeTerm ) }}</ dd ></ div >
< div >< dt > 发货方式 </ dt >< dd >{{ pickupMethodLabels [ detailOrder . pickupMethod ] }}</ dd ></ div >
< div >< dt > 报关方式 </ dt >< dd >{{ taxModeLabels [ detailOrder . taxMode ] }}</ dd ></ div >
< div >< dt > 运输渠道 </ dt >< dd >{{ detailOrder . channelName || detailOrder . channelCode || '-' }}< span v-if = "detailOrder.channelName && detailOrder.channelCode" class="detail-subline" >{{ detailOrder.channelCode }}</ span ></ dd ></ div >
< div >< dt > 承运商 </ dt >< dd >{{ detailOrder . carrierName || '待平台指派' }}</ dd ></ div >
< div >< dt > 国内收货仓 </ dt >< dd >{{ detailOrder . receivingWarehouseSnapshot ? . name || detailOrder . receivingWarehouseName || '-' }}< span v-if = "detailOrder.receivingWarehouseSnapshot" class="detail-subline" >{{ detailOrder.receivingWarehouseSnapshot.code || ' 仓库编码待更新 ' }} · {{ detailOrder.receivingWarehouseSnapshot.province }}{{ detailOrder.receivingWarehouseSnapshot.city }}{{ detailOrder.receivingWarehouseSnapshot.address }}</ span ></ dd ></ div >
< div >< dt > 收货要求 </ dt >< dd >{{ displayValue ( detailOrder . receivingTime ) }}</ dd ></ div >
< div >< dt > 目的仓类型 </ dt >< dd >{{ destinationTypeLabels [ detailOrder . destinationType ] }}</ dd ></ div >
< div >< dt > 目的国家 / 仓库 </ dt >< dd >{{ detailOrder . destinationCountryCode || '-' }} · {{ detailOrder . destinationWarehouseName || '-' }}</ dd ></ div >
< div v-if = "detailOrder.destinationType === 'FBA'" >< dt > Amazon 仓库代码 </ dt >< dd >{{ displayValue ( detailOrder . amazonWarehouseCode ) }}</ dd ></ div >
< div v-if = "detailOrder.destinationType === 'FBA'" >< dt > Shipment ID </ dt >< dd >{{ displayValue ( detailOrder . shipmentId ) }}</ dd ></ div >
< div >< dt > 期望出运 </ dt >< dd >{{ displayValue ( detailOrder . expectedDepartureDate ) }}</ dd ></ div >
< div >< dt > 海关查验 </ dt >< dd >{{ detailOrder . customsInspection ? '需要' : '不需要' }}</ dd ></ div >
< div >< dt > 提交时间 </ dt >< dd >{{ detailOrder . submittedAt ? formatDateTime ( detailOrder . submittedAt ) : '-' }}</ dd ></ div >
< div >< dt > 完成时间 </ dt >< dd >{{ detailOrder . completedAt ? formatDateTime ( detailOrder . completedAt ) : '-' }}</ dd ></ div >
</ dl >
</ section >
< section class = "detail-section" >
< h3 > 收发货信息 </ h3 >
< div class = "party-detail-grid" >
< article >< span > 发货方 </ span >< strong >{{ detailOrder . shipperPartySnapshot ? . name || detailOrder . shipper || '未提供' }}</ strong >< p v-if = "detailOrder.shipperPartySnapshot" >{{ detailOrder.shipperPartySnapshot.contactName || ' 未填写联系人 ' }} · {{ detailOrder.shipperPartySnapshot.phone || ' 未填写电话 ' }}< br />{{ detailOrder . shipperPartySnapshot . email || '' }}< br />{{ detailOrder . shipperPartySnapshot . countryRegion }} {{ detailOrder . shipperPartySnapshot . province }} {{ detailOrder . shipperPartySnapshot . city }} {{ detailOrder . shipperPartySnapshot . address }} {{ detailOrder . shipperPartySnapshot . postalCode }}</ p ></ article >
< article >< span > 收货方 </ span >< strong >{{ detailOrder . consigneePartySnapshot ? . name || detailOrder . consignee || detailOrder . destinationWarehouseName || '未提供' }}</ strong >< p v-if = "detailOrder.consigneePartySnapshot" >{{ detailOrder.consigneePartySnapshot.contactName || ' 未填写联系人 ' }} · {{ detailOrder.consigneePartySnapshot.phone || ' 未填写电话 ' }}< br />{{ detailOrder . consigneePartySnapshot . email || '' }}< br />{{ detailOrder . consigneePartySnapshot . countryRegion }} {{ detailOrder . consigneePartySnapshot . province }} {{ detailOrder . consigneePartySnapshot . city }} {{ detailOrder . consigneePartySnapshot . address }} {{ detailOrder . consigneePartySnapshot . postalCode }}</ p >< p v-else > 平台目的仓地址以最终履约安排为准 </ p ></ article >
</ div >
< 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-07-30 17:59:42 +08:00
< section class = "detail-section" >
< h3 > 履约快递信息 </ h3 >
< div v-if = "detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><PackageCheck :size="20" />< div >< span > 快递公司 </ span >< strong >{{ detailOrder . expressCompanyName || '待平台补充' }}</ strong ></ div >< div >< span > 快递单号 </ span >< strong >{{ detailOrder . trackingNumber || '待平台补充' }}</ strong ></ div ></ div >
< div v-else class = "express-empty" >< PackageCheck :size = "20" />< div >< strong > 平台尚未维护快递信息 </ strong >< span > 快递公司和快递单号由平台后台维护 , 客户端仅只读展示 。 </ span ></ div ></ div >
</ section >
2026-08-06 20:14:54 +08:00
< section class = "detail-section" >< h3 > 货物明细 </ h3 >< div v-if = "detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><div v-if="detailOrder.items.length" class="cargo-detail-list"><article v-for="(item, index) in detailOrder.items" :key="item.id"><header><span>货物 {{ index + 1 }}</span><strong>{{ item.cargoNameCn || '未命名货物' }}</strong><small>{{ item.cargoNameEn || '' }}</small></header><dl><div><dt>SKU / UPC</dt><dd>{{ item.sku || '-' }} / {{ item.upc || '-' }}</dd></div><div><dt>HS 编码</dt><dd>{{ item.hsCode || '-' }}</dd></div><div><dt>包装 / 箱数</dt><dd>{{ packageTypeLabels[item.packageType] || item.packageType || '-' }} · {{ item.packageCount }} 箱</dd></div><div><dt>数量</dt><dd>{{ item.pieces }} 件</dd></div><div><dt>毛重</dt><dd>{{ item.grossWeightKg }} kg</dd></div><div><dt>体积</dt><dd>{{ item.volumeCbm }} m³</dd></div><div><dt>申报价值</dt><dd>{{ item.declaredValue ? `${item.currency} ${item.declaredValue.toLocaleString()}` : '-' }}</dd></div><div><dt>危险品</dt><dd>{{ item.dangerousGoods ? '是' : '否' }}</dd></div></dl><p v-if="item.remark">{{ item.remark }}</p></article></div><el-empty v-else description="暂无货物明细" :image-size="56" /></ section >
< OrderProgressPanel :order = "detailOrder" />
2026-08-04 21:01:34 +08:00
< InboundBillingPanel
:data = "inboundBilling"
:loading = "inboundBillingLoading"
:unavailable = "inboundBillingUnavailable"
:unavailable-message = "inboundBillingUnavailableMessage"
:error = "inboundBillingError"
@retry ="retryInboundBilling"
/>
2026-07-30 17:59:42 +08:00
</ template >
2026-07-30 21:23:06 +08:00
</ div >
< template # footer >< div v-if = "detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" /> 编辑草稿 </ el-button ></ div ></ template >
2026-07-30 17:59:42 +08:00
</ el-drawer >
</ section >
</ template >
< style scoped >
. first - leg - page { display : flex ; flex - direction : column ; gap : 14 px ; }
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 ; }
. first - leg - query - toolbar { flex - wrap : nowrap ; }
2026-08-06 13:56:13 +08:00
. first - leg - query - toolbar : deep (. query - toolbar__filters ) { display : grid ; min - width : 0 ; flex : 1 1 auto ; grid - template - columns : minmax ( 180 px , 1.4 fr ) minmax ( 120 px , .72 fr ) minmax ( 210 px , 1.15 fr ); }
2026-08-03 17:44:42 +08:00
. first - leg - query - toolbar : deep (. query - toolbar__filters . el - input ), . first - leg - query - toolbar : deep (. query - toolbar__filters . el - select ), . first - leg - query - toolbar : deep (. query - toolbar__filters . el - date - editor ) { width : 100 % ; }
. first - leg - query - toolbar : deep (. query - toolbar__actions ) { align - self : flex - start ; flex - wrap : nowrap ; margin - left : auto ; }
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-07-30 17:59:42 +08:00
. order - table - card { padding : 0 ; overflow : hidden ; }
2026-08-03 10:51:53 +08:00
. order - table - toolbar { display : flex ; min - height : 58 px ; align - items : center ; justify - content : space - between ; gap : 16 px ; padding : 10 px 16 px ; border - bottom : 1 px solid var ( -- border ); }
. order - table - toolbar > div { display : flex ; min - width : 0 ; align - items : baseline ; gap : 8 px ; }
. order - table - toolbar strong { color : var ( -- navy ); font - size : 14 px ; }
. order - table - toolbar span { color : var ( -- muted ); font - size : 11 px ; }
. 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 ; }
. order - detail - content { min - height : 180 px ; }
2026-08-01 12:17:33 +08:00
. mobile - order - surface { display : none ; min - height : 220 px ; }
2026-07-30 17:59:42 +08:00
. mobile - order - list { display : none ; }
. order - number - link { display : block ; padding : 0 ; border : 0 ; background : none ; color : var ( -- primary ); cursor : pointer ; font - weight : 700 ; text - align : left ; }
. order - number - link : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 3 px ; }
. order - reference { display : block ; margin - top : 4 px ; color : var ( -- muted ); font - size : 11 px ; font - weight : 400 ; }
2026-08-03 17:44:42 +08:00
: global (. first - leg - editor . el - drawer__header ) { flex : 0 0 auto ; min - height : 58 px ; margin - bottom : 0 ; padding : 17 px 22 px ; border - bottom : 1 px solid var ( -- border ); }
: global (. first - leg - editor . el - drawer__body ) { min - height : 0 ; padding : 18 px 22 px 32 px ; overscroll - behavior : contain ; }
: global (. first - leg - editor . el - drawer__footer ) { flex : 0 0 auto ; padding : 12 px 22 px ; border - top : 1 px solid var ( -- border ); background : # fff ; }
. order - form { width : min ( 100 % , 1000 px ); margin : 0 auto ; padding : 0 0 12 px ; }
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-07-30 17:59:42 +08:00
. order - form - section + . order - form - section { margin - top : 24 px ; padding - top : 22 px ; border - top : 1 px solid var ( -- border ); }
. 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-04 12:01:05 +08:00
. order - form - grid > * , . cargo - primary - grid > * , . cargo - secondary - 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 ; }
. prototype - section { padding : 2 px 2 px 0 ; }
. segmented - options { display : flex ; width : min ( 100 % , 620 px ); }
. segmented - options : deep (. el - radio - button ) { flex : 1 ; }
2026-08-06 18:02:02 +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 : 11 px 16 px ; }
2026-08-03 15:50:24 +08:00
. warehouse - options { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
2026-08-06 18:02:02 +08:00
. warehouse - options : deep (. el - radio - button__inner ) { display : flex ; min - height : 40 px ; box - sizing : border - box ; align - items : center ; justify - content : center ; border : 1 px solid var ( -- border ); border - radius : 6 px ! important ; box - shadow : none ! important ; }
2026-08-03 15:50:24 +08:00
. warehouse - options : deep (. el - radio - button . is - active . el - radio - button__inner ) { border - color : var ( -- primary ); }
2026-08-03 17:44:42 +08:00
. selected - warehouse { display : flex ; align - items : flex - start ; gap : 10 px ; margin - top : 0 ; padding : 11 px 13 px ; border : 1 px solid # d9def8 ; background : # f6f7fe ; color : var ( -- primary ); }
2026-07-30 17:59:42 +08:00
. selected - warehouse svg { flex : 0 0 17 px ; margin - top : 2 px ; }
2026-08-03 17:44:42 +08:00
. selected - warehouse div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 2 px ; }
2026-07-30 17:59:42 +08:00
. selected - warehouse span , . selected - warehouse small { color : var ( -- muted ); font - size : 11 px ; line - height : 1.5 ; }
. order - section - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. order - section - heading h3 { margin - bottom : 5 px ; }
. order - section - heading p { margin : 0 ; color : var ( -- muted ); font - size : 11 px ; }
2026-08-04 12:01:05 +08:00
. prototype - form - flow { display : flex ; max - width : 900 px ; flex - direction : column ; padding - top : 4 px ; }
. prototype - form - row { display : grid ; width : 100 % ; grid - template - columns : 140 px minmax ( 0 , 560 px ); align - items : start ; margin - bottom : 18 px ; }
. prototype - form - row -- wide { grid - template - columns : 140 px minmax ( 0 , 720 px ); }
. prototype - form - row -- compact { grid - template - columns : 140 px minmax ( 0 , 420 px ); }
. prototype - form - row -- short { grid - template - columns : 140 px minmax ( 0 , 280 px ); }
. prototype - form - row -- tabs { grid - template - columns : 140 px minmax ( 0 , 520 px ); margin - bottom : 22 px ; }
. prototype - form - row : deep (. el - form - item__label ) { width : auto ! important ; min - height : 40 px ; justify - content : flex - end ; padding : 0 20 px 0 0 ; line - height : 40 px ; }
. prototype - form - row : deep (. el - form - item__content ) { display : block ; min - width : 0 ; line - height : normal ; }
. prototype - form - row : deep (. el - input__wrapper ), . prototype - form - row : deep (. el - select__wrapper ) { min - height : 40 px ; }
. 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-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-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 ); }
. compact - transport - grid { margin - top : 18 px ; }
2026-08-03 15:50:24 +08:00
. cargo - entry { padding : 16 px ; border : 1 px solid var ( -- border ); background : # fbfcfc ; }
. cargo - entry + . cargo - entry { margin - top : 12 px ; }
. cargo - entry > header { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; margin - bottom : 12 px ; }
. cargo - primary - grid { display : grid ; grid - template - columns : repeat ( 5 , minmax ( 130 px , 1 fr )); column - gap : 12 px ; }
. cargo - secondary - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 130 px , 1 fr )); column - gap : 12 px ; padding - top : 6 px ; }
. cargo - more { border - top : 1 px solid var ( -- border ); border - bottom : 0 ; }
. cargo - more : deep (. el - collapse - item__header ), . cargo - more : deep (. el - collapse - item__wrap ) { background : transparent ; }
. cargo - more : deep (. el - collapse - item__content ) { padding - bottom : 0 ; }
. cargo - tip { margin : 12 px 0 0 ; color : var ( -- muted ); font - size : 11 px ; line - height : 1.55 ; }
2026-07-30 17:59:42 +08:00
. order - detail - heading { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : 16 px ; padding - bottom : 20 px ; border - bottom : 1 px solid var ( -- border ); }
. order - detail - heading span { color : var ( -- muted ); font - size : 11 px ; }
. order - detail - heading h2 { margin : 6 px 0 ; font - size : 21 px ; }
. order - detail - heading p { margin : 0 ; color : var ( -- muted ); }
. order - detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 ; margin : 18 px 0 ; border : 1 px solid var ( -- border ); }
. order - detail - grid div { padding : 13 px ; border - bottom : 1 px solid var ( -- border ); }
. order - detail - grid div : nth - child ( odd ) { border - right : 1 px solid var ( -- border ); }
. order - detail - grid dt { color : var ( -- muted ); font - size : 11 px ; }
. order - detail - grid dd { margin : 5 px 0 0 ; font - size : 13 px ; font - weight : 600 ; }
. detail - subline { display : block ; margin - top : 4 px ; color : var ( -- muted ); font - size : 11 px ; font - weight : 400 ; line - height : 1.5 ; }
. detail - section { margin - top : 22 px ; }
. detail - section h3 { margin : 0 0 12 px ; font - size : 15 px ; }
2026-08-06 20:14:54 +08:00
. party - detail - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px ; }
. party - detail - grid article { min - width : 0 ; padding : 15 px ; border : 1 px solid var ( -- border ); background : # fbfcfc ; }
. party - detail - grid span { display : block ; color : var ( -- muted ); font - size : 10 px ; }
. party - detail - grid strong { display : block ; margin - top : 5 px ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 13 px ; }
. party - detail - grid p , . 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 : 10 px ; padding : 13 px 15 px ; background : # f2f7f6 ; }
. customer - remark strong { color : var ( -- navy ); font - size : 11 px ; }
2026-08-06 13:56:13 +08:00
. fulfillment - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); margin : 0 ; border : 1 px solid var ( -- border ); }
. fulfillment - summary > div { min - width : 0 ; padding : 13 px ; border - bottom : 1 px solid var ( -- border ); }
. fulfillment - summary > div : not ( : nth - child ( 3 n )) { border - right : 1 px solid var ( -- border ); }
. fulfillment - summary > div : nth - last - child ( - n + 3 ) { border - bottom : 0 ; }
. fulfillment - summary dt { color : var ( -- muted ); font - size : 10 px ; }
. fulfillment - summary dd { margin : 5 px 0 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 12 px ; font - weight : 650 ; }
2026-07-30 17:59:42 +08:00
. express - information { display : grid ; grid - template - columns : auto repeat ( 2 , minmax ( 0 , 1 fr )); align - items : center ; gap : 16 px ; padding : 15 px 16 px ; background : # f2f7f6 ; color : var ( -- primary ); }
. express - information > div { display : flex ; min - width : 0 ; flex - direction : column ; gap : 4 px ; }
. express - information span { color : var ( -- muted ); font - size : 11 px ; }
. express - information strong { overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 13 px ; }
. express - empty { display : flex ; align - items : flex - start ; gap : 12 px ; padding : 15 px 16 px ; border : 1 px dashed # cbd5da ; color : var ( -- muted ); }
. 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 ; }
. cargo - summary { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); border : 1 px solid var ( -- border ); }
. cargo - summary div { display : flex ; min - height : 74 px ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 4 px ; border - right : 1 px solid var ( -- border ); }
. cargo - summary div : last - child { border - right : 0 ; }
. cargo - summary span { color : var ( -- muted ); font - size : 11 px ; }
2026-08-06 20:14:54 +08:00
. cargo - detail - list { display : grid ; gap : 10 px ; margin - top : 10 px ; }
. cargo - detail - list article { padding : 14 px 15 px ; border : 1 px solid var ( -- border ); }
. cargo - detail - list header { display : grid ; grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; align - items : baseline ; gap : 10 px ; }
. cargo - detail - list header span , . cargo - detail - list header small { color : var ( -- muted ); font - size : 10 px ; }
. cargo - detail - list header strong { overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 13 px ; }
. cargo - detail - list header small { overflow - wrap : anywhere ; text - align : right ; }
. cargo - detail - list dl { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); gap : 11 px 16 px ; margin : 13 px 0 0 ; }
. cargo - detail - list dt { color : var ( -- muted ); font - size : 10 px ; }
. cargo - detail - list dd { margin : 4 px 0 0 ; overflow - wrap : anywhere ; color : var ( -- navy ); font - size : 11 px ; font - weight : 600 ; }
. cargo - detail - list article > p { margin : 11 px 0 0 ; padding - top : 10 px ; border - top : 1 px solid var ( -- border ); color : var ( -- muted ); font - size : 11 px ; }
: 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 ) {
. desktop - order - table { display : none ; }
2026-08-01 12:17:33 +08:00
. mobile - order - surface { display : block ; }
2026-07-30 21:23:06 +08:00
. mobile - order - list { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 10 px ; padding : 10 px ; }
. mobile - order - card { min - width : 0 ; padding : 14 px ; border : 1 px solid var ( -- border ); background : # fff ; }
. mobile - order - card header , . mobile - order - card footer { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; }
. mobile - order - card header button { min - width : 0 ; overflow : hidden ; padding : 0 ; border : 0 ; background : none ; color : var ( -- primary ); font - weight : 750 ; text - overflow : ellipsis ; white - space : nowrap ; }
. mobile - order - card header button : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 3 px ; }
. mobile - order - card dl { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px ; margin : 14 px 0 ; }
. mobile - order - card dl div { min - width : 0 ; }
. mobile - order - card dt , . mobile - order - card footer > span { color : var ( -- muted ); font - size : 10 px ; }
. mobile - order - card dd { overflow : hidden ; margin : 4 px 0 0 ; font - size : 12 px ; font - weight : 600 ; text - overflow : ellipsis ; white - space : nowrap ; }
. mobile - order - card dd span { margin - left : 4 px ; color : var ( -- muted ); font - weight : 400 ; }
. mobile - order - card footer { align - items : flex - end ; padding - top : 10 px ; border - top : 1 px solid var ( -- border ); }
. mobile - order - card footer > div { display : flex ; }
}
2026-08-03 17:44:42 +08:00
@ media ( min - width : 721 px ) and ( max - width : 1050 px ) {
. first - leg - query - toolbar : deep (. query - toolbar__filters ) { grid - template - columns : repeat ( 2 , minmax ( 140 px , 1 fr )); }
}
2026-07-30 17:59:42 +08:00
@ media ( max - width : 720 px ) {
2026-08-03 17:44:42 +08:00
. first - leg - query - toolbar { flex - wrap : wrap ; }
. 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-08-03 10:51:53 +08:00
. order - table - toolbar { align - items : stretch ; flex - direction : column ; gap : 10 px ; }
. order - table - toolbar > div { justify - content : space - between ; }
. order - table - toolbar . el - button { width : 100 % ; min - height : 40 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-06 20:14:54 +08:00
. order - form - grid , . order - detail - grid , . cargo - primary - grid , . cargo - secondary - grid , . party - detail - grid { grid - template - columns : 1 fr ; }
. fulfillment - summary { grid - template - columns : 1 fr ; }
2026-08-06 13:56:13 +08:00
. fulfillment - summary > div , . fulfillment - summary > div : not ( : nth - child ( 3 n )) { border - right : 0 ; border - bottom : 1 px solid var ( -- border ); }
. 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-03 17:44:42 +08:00
. warehouse - options { display : grid ; width : 100 % ; grid - template - columns : 1 fr ; }
2026-08-03 15:50:24 +08:00
. warehouse - options : deep (. el - radio - button ), . warehouse - options : deep (. el - radio - button__inner ) { width : 100 % ; }
2026-08-03 17:44:42 +08:00
: global (. first - leg - editor . el - drawer__header ) { min - height : 56 px ; padding : 15 px 20 px ; }
: global (. first - leg - editor . el - drawer__body ) { padding : 16 px 20 px 28 px ; }
: global (. first - leg - editor . el - drawer__footer ) { padding : 10 px 20 px ; }
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 ; }
. desktop - order - table { display : none ; }
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 ; }
. mobile - order - list { grid - template - columns : 1 fr ; }
2026-07-30 17:59:42 +08:00
. mobile - order - card { padding : 14 px ; border : 1 px solid var ( -- border ); background : # fff ; }
. mobile - order - card header , . mobile - order - card footer { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; }
. mobile - order - card header button { padding : 0 ; border : 0 ; background : none ; color : var ( -- primary ); font - weight : 750 ; }
. mobile - order - card header button : focus - visible { outline : 2 px solid var ( -- primary ); outline - offset : 3 px ; }
. mobile - order - card dl { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 12 px ; margin : 14 px 0 ; }
. mobile - order - card dl div { min - width : 0 ; }
. mobile - order - card dt , . mobile - order - card footer > span { color : var ( -- muted ); font - size : 10 px ; }
. mobile - order - card dd { overflow : hidden ; margin : 4 px 0 0 ; font - size : 12 px ; font - weight : 600 ; text - overflow : ellipsis ; white - space : nowrap ; }
. mobile - order - card dd span { margin - left : 4 px ; color : var ( -- muted ); font - weight : 400 ; }
. mobile - order - card footer { align - items : flex - end ; padding - top : 10 px ; border - top : 1 px solid var ( -- border ); }
. mobile - order - card footer > div { display : flex ; }
. order - detail - grid div : nth - child ( odd ) { border - right : 0 ; }
. express - information { grid - template - columns : auto 1 fr ; }
. express - information > div : last - child { grid - column : 2 ; }
. 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
. cargo - detail - list header { grid - template - columns : 1 fr ; gap : 4 px ; }
. cargo - detail - list header small { text - align : left ; }
. cargo - detail - list dl { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); }
2026-07-30 17:59:42 +08:00
: global (. first - leg - editor ), : global (. first - leg - detail ) { width : 100 % ! important ; }
}
2026-08-06 20:14:54 +08:00
@ media ( max - width : 360 px ) {
. cargo - detail - list dl { grid - template - columns : 1 fr ; }
}
2026-08-01 12:17:33 +08:00
@ media ( max - width : 480 px ) {
. 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 >