1293 lines
86 KiB
Vue
1293 lines
86 KiB
Vue
<script setup lang="ts">
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox, type FormInstance, type FormItemRule, type FormRules } from 'element-plus'
|
||
import { AlertTriangle, Box, Eye, FilePlus2, MapPin, PackageCheck, Pencil, Plus, Send, Trash2 } from '@lucide/vue'
|
||
import { FIRST_LEG_ORDER_STATUSES, FIRST_LEG_TRANSPORT_MODES, type FirstLegOrderStatus } from '@chongchong/shared'
|
||
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||
import { ApiError } from '@/api/http'
|
||
import FirstLegOrderEditorShell from '@/modules/first-leg/components/FirstLegOrderEditorShell.vue'
|
||
import InboundBillingPanel from '@/modules/first-leg/components/InboundBillingPanel.vue'
|
||
import OrderProgressPanel from '@/modules/first-leg/components/OrderProgressPanel.vue'
|
||
import { getMerchantInboundBilling } from '@/modules/first-leg/api/inboundBilling'
|
||
import { useFirstLegOrdersStore } from '@/modules/first-leg/store/firstLegOrders'
|
||
import { useTenantAdministrationStore } from '@/modules/tenant-management/public'
|
||
import { useAuthStore } from '@/modules/auth/public'
|
||
import { useUnsavedWorkRegistration } from '@/navigation/workspace-tabs/unsavedWork'
|
||
import type { FirstLegPartyRole, FirstLegPartySnapshot } from '@/modules/first-leg/data/firstLegParties'
|
||
import type { FirstLegOrder, FirstLegOrderDraft } from '@/modules/first-leg/types/firstLegOrder'
|
||
import type { MerchantInboundBilling } from '@/modules/first-leg/types/inboundBilling'
|
||
import {
|
||
canClientEditFirstLegOrder,
|
||
createEmptyCargoItem,
|
||
getFirstLegStatusLabel,
|
||
getFirstLegStatusTone,
|
||
getFirstLegTransportModeLabel,
|
||
summarizeFirstLegCargo,
|
||
validateFirstLegOrderDraft,
|
||
} from '@/modules/first-leg/utils/firstLegOrder'
|
||
|
||
const store = useFirstLegOrdersStore()
|
||
const tenantStore = useTenantAdministrationStore()
|
||
const auth = useAuthStore()
|
||
const router = useRouter()
|
||
const props = withDefaults(defineProps<{ createPage?: boolean }>(), { createPage: false })
|
||
const isCreatePage = computed(() => props.createPage)
|
||
const canViewCompanyProfile = computed(() => auth.hasPermission('company:profile:view'))
|
||
const companyAuditLoaded = ref(false)
|
||
const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE')
|
||
const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create'))
|
||
const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit'))
|
||
const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC'))
|
||
const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType))
|
||
const filters = ref<QueryModel>({ keyword: '', status: '', createdRange: [] })
|
||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||
const queryFields: QueryField[] = [
|
||
{ key: 'keyword', type: 'input', placeholder: '订单号、入仓码、渠道或仓库', props: { 'aria-label': '订单关键词' } },
|
||
{ 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': '订单创建日期' } },
|
||
]
|
||
const channelOptions = [
|
||
{ code: 'SEA-US-GENERAL', name: '美线普货专线' },
|
||
{ code: 'SEA-US-SENSITIVE', name: '美线敏感货专线' },
|
||
{ code: 'SEA-US-EXPRESS', name: '美线快线' },
|
||
{ code: 'SEA-US-FBA', name: '美线 FBA 直送' },
|
||
]
|
||
const orderColumns: DataTableColumn[] = [
|
||
{ key: 'actions', label: '操作', width: 180, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'order', label: '订单', minWidth: 190, slotName: 'order' },
|
||
{ key: 'route', label: '目的地 / 渠道', minWidth: 190, slotName: 'route' },
|
||
{ key: 'warehouse', label: '收货仓', prop: 'receivingWarehouseName', minWidth: 120 },
|
||
{ key: 'cargo', label: '货物摘要', minWidth: 150, slotName: 'cargo' },
|
||
{ key: 'status', label: '状态', width: 120, slotName: 'status' },
|
||
{ key: 'updatedAt', label: '更新时间', width: 155, slotName: 'updatedAt' },
|
||
]
|
||
const tablePage = ref(1)
|
||
const tablePageSize = ref(10)
|
||
const editorOpen = ref(false)
|
||
const orderFormRef = ref<FormInstance>()
|
||
const detailOpen = ref(false)
|
||
const editingId = ref<string>()
|
||
const openingEditor = ref(false)
|
||
const detailId = ref<string>()
|
||
const saving = ref(false)
|
||
const detailLoading = ref(false)
|
||
const detailError = ref('')
|
||
const inboundBilling = ref<MerchantInboundBilling | null>(null)
|
||
const inboundBillingLoading = ref(false)
|
||
const inboundBillingUnavailable = ref(false)
|
||
const inboundBillingUnavailableMessage = ref('入库及费用结果尚未发布')
|
||
const inboundBillingError = ref('')
|
||
const supportingDataLoading = ref(false)
|
||
const supportingDataErrors = ref<string[]>([])
|
||
const destinationWarehousesLoading = ref(false)
|
||
const pageDataLoading = computed(() => supportingDataLoading.value || (!isCreatePage.value && store.loading))
|
||
const editorBaseline = ref('')
|
||
let cargoSequence = 0
|
||
let inboundBillingRequestSequence = 0
|
||
|
||
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))
|
||
}
|
||
|
||
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' }],
|
||
}
|
||
|
||
function createEmptyPartySnapshot(role: FirstLegPartyRole): FirstLegPartySnapshot {
|
||
return {
|
||
id: '',
|
||
code: '',
|
||
name: '',
|
||
role,
|
||
contactName: '',
|
||
phone: '',
|
||
email: '',
|
||
countryRegion: role === 'SHIPPER' ? 'CN' : 'US',
|
||
province: '',
|
||
city: '',
|
||
postalCode: '',
|
||
address: '',
|
||
}
|
||
}
|
||
|
||
function createPickupPartySnapshot(): FirstLegPartySnapshot {
|
||
return {
|
||
...createEmptyPartySnapshot('SHIPPER'),
|
||
name: tenantStore.company.companyName,
|
||
contactName: tenantStore.company.contactName,
|
||
phone: tenantStore.company.contactPhone,
|
||
email: tenantStore.company.contactEmail,
|
||
}
|
||
}
|
||
|
||
function requiredTextRules(message: string): FormItemRule[] {
|
||
return [{ required: true, message, trigger: 'blur' }]
|
||
}
|
||
|
||
function requiredSelectRules(message: string): FormItemRule[] {
|
||
return [{ required: true, message, trigger: 'change' }]
|
||
}
|
||
|
||
function positiveNumberRules(message: string, integer = false): FormItemRule[] {
|
||
return [{ type: integer ? 'integer' : 'number', min: integer ? 1 : 0.001, message, trigger: 'change' }]
|
||
}
|
||
|
||
function formatDateTime(value: string) {
|
||
const date = new Date(value)
|
||
if (Number.isNaN(date.getTime())) return value || '-'
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false,
|
||
})
|
||
}
|
||
|
||
const pickupMethodLabels: Record<FirstLegOrder['pickupMethod'], string> = {
|
||
SELF_EXPRESS: '自行快递到仓',
|
||
PICKUP: '平台上门提货',
|
||
SELF_TRUCK: '自行送货到仓',
|
||
}
|
||
const destinationTypeLabels: Record<FirstLegOrder['destinationType'], string> = {
|
||
FBA: 'FBA 仓',
|
||
OVERSEAS: '海外仓',
|
||
PRIVATE: '私人地址',
|
||
}
|
||
const taxModeLabels: Record<FirstLegOrder['taxMode'], string> = {
|
||
BUY_DECLARATION: '买单报关',
|
||
TAX_REFUND: '退税报关',
|
||
}
|
||
const packageTypeLabels: Readonly<Record<string, string>> = {
|
||
CARTON: '纸箱',
|
||
PALLET: '托盘',
|
||
WOODEN_CASE: '木箱',
|
||
BAG: '袋装',
|
||
OTHER: '其他',
|
||
}
|
||
|
||
function displayValue(value: string | null | undefined) {
|
||
return value?.trim() || '-'
|
||
}
|
||
|
||
function emptyDraft(): FirstLegOrderDraft {
|
||
return {
|
||
referenceNo: '',
|
||
transportMode: 'FCL',
|
||
businessType: 'EXPORT',
|
||
origin: '',
|
||
destination: '',
|
||
receivingWarehouseId: '',
|
||
expectedDepartureDate: '',
|
||
shipperPartyId: '',
|
||
shipperPartySnapshot: null,
|
||
shipper: '',
|
||
consigneePartyId: '',
|
||
consigneePartySnapshot: null,
|
||
consignee: '',
|
||
priority: 'NORMAL',
|
||
customerRemark: '',
|
||
tradeTerm: 'FOB',
|
||
destinationCountryCode: 'US',
|
||
destinationWarehouseId: '',
|
||
destinationWarehouseName: '',
|
||
pickupMethod: 'SELF_EXPRESS',
|
||
channelCode: 'SEA-US-FBA',
|
||
channelName: '美线 FBA 直送',
|
||
taxMode: 'BUY_DECLARATION',
|
||
customsInspection: false,
|
||
destinationType: 'FBA',
|
||
amazonWarehouseCode: '',
|
||
shipmentId: '',
|
||
items: [createEmptyCargoItem(`new-cargo-${++cargoSequence}`)],
|
||
}
|
||
}
|
||
|
||
const form = reactive<FirstLegOrderDraft>(emptyDraft())
|
||
const serializedDraft = computed(() => JSON.stringify(form))
|
||
const editorDirty = computed(() => (editorOpen.value || isCreatePage.value) && serializedDraft.value !== editorBaseline.value)
|
||
const unsavedRouteKey = props.createPage ? 'first-leg-order-create' : 'first-leg-orders'
|
||
|
||
useUnsavedWorkRegistration({
|
||
key: `first-leg-order-editor:${unsavedRouteKey}`,
|
||
routeKey: unsavedRouteKey,
|
||
label: props.createPage ? '创建订单' : '订单编辑',
|
||
dirty: editorDirty,
|
||
})
|
||
|
||
const filteredOrders = computed(() => store.orders.filter((order) => {
|
||
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') : []
|
||
const matchesKeyword = !keyword || [order.orderNo, order.businessSummary.inboundCode, order.referenceNo, order.origin, order.destination, order.items.map((item) => item.cargoNameCn).join(' ')]
|
||
.filter(Boolean)
|
||
.some((value) => String(value).toLowerCase().includes(keyword))
|
||
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])
|
||
return matchesKeyword && matchesStatus && matchesDate
|
||
}))
|
||
const pagedOrders = computed(() => {
|
||
const start = (tablePage.value - 1) * tablePageSize.value
|
||
return filteredOrders.value.slice(start, start + tablePageSize.value)
|
||
})
|
||
|
||
const detailOrder = computed(() => detailId.value ? store.getById(detailId.value) : undefined)
|
||
const detailSummary = computed(() => detailOrder.value ? summarizeFirstLegCargo(detailOrder.value.items) : null)
|
||
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)}` : '待更新') },
|
||
]
|
||
})
|
||
const selectedWarehouse = computed(() => availableWarehouses.value.find((item) => item.id === form.receivingWarehouseId))
|
||
const selectedDestinationWarehouse = computed(() => store.warehouses.find((item) => item.id === form.destinationWarehouseId && item.warehouseType === form.destinationType))
|
||
const hasActiveFilters = computed(() => {
|
||
const values = appliedFilters.value
|
||
return Boolean(
|
||
(typeof values.keyword === 'string' && values.keyword.trim())
|
||
|| values.status
|
||
|| (Array.isArray(values.createdRange) && values.createdRange.length),
|
||
)
|
||
})
|
||
const emptyDescription = computed(() => hasActiveFilters.value ? '没有符合筛选条件的订单' : '还没有订单')
|
||
|
||
async function loadPageData() {
|
||
supportingDataLoading.value = true
|
||
supportingDataErrors.value = []
|
||
companyAuditLoaded.value = false
|
||
const companyRequest = canViewCompanyProfile.value
|
||
? tenantStore.loadCompanyContext()
|
||
: Promise.resolve()
|
||
const destinationRequest = isCreatePage.value && form.destinationType !== 'PRIVATE'
|
||
? loadDestinationWarehouses(form.destinationType)
|
||
: Promise.resolve()
|
||
const orderPageRequest = isCreatePage.value
|
||
? store.loadWarehouseType('DOMESTIC', true)
|
||
: store.load()
|
||
const [ordersResult, companyResult, destinationResult] = await Promise.allSettled([
|
||
orderPageRequest,
|
||
companyRequest,
|
||
destinationRequest,
|
||
])
|
||
if (canViewCompanyProfile.value && companyResult.status === 'fulfilled') {
|
||
companyAuditLoaded.value = true
|
||
} else if (companyResult.status === 'rejected') {
|
||
supportingDataErrors.value.push(`企业资料加载失败:${errorMessage(companyResult.reason)}`)
|
||
}
|
||
if (!isCreatePage.value && ordersResult.status === 'rejected' && !store.loadError) supportingDataErrors.value.push(`订单加载失败:${errorMessage(ordersResult.reason)}`)
|
||
if (store.warehouseLoadError) supportingDataErrors.value.push(`公共仓库加载失败:${store.warehouseLoadError}`)
|
||
if (!isCreatePage.value && store.draftLoadError) supportingDataErrors.value.push(`订单草稿加载失败:${store.draftLoadError}`)
|
||
if (destinationResult.status === 'rejected') supportingDataErrors.value.push(`目的仓加载失败:${errorMessage(destinationResult.reason)}`)
|
||
await autofillWarehouseSelections()
|
||
supportingDataLoading.value = false
|
||
}
|
||
|
||
async function loadDestinationWarehouses(type: 'FBA' | 'OVERSEAS' | 'PRIVATE') {
|
||
if (type === 'PRIVATE') return
|
||
destinationWarehousesLoading.value = true
|
||
try {
|
||
await store.loadWarehouseType(type)
|
||
} finally {
|
||
destinationWarehousesLoading.value = false
|
||
}
|
||
}
|
||
|
||
function errorMessage(error: unknown) {
|
||
return error instanceof Error ? error.message : '服务暂时不可用'
|
||
}
|
||
|
||
function 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 '待更新'
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadPrivateAddresses()
|
||
void loadPageData()
|
||
})
|
||
|
||
watch(isCreatePage, (isCreate, wasCreate) => {
|
||
if (!isCreate || wasCreate) return
|
||
editingId.value = undefined
|
||
replaceForm(emptyDraft())
|
||
resetEditorValidation()
|
||
}, { immediate: true })
|
||
|
||
watch(detailOpen, (open) => {
|
||
if (!open) resetInboundBillingState()
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
resetInboundBillingState()
|
||
})
|
||
|
||
function markEditorClean() {
|
||
editorBaseline.value = serializedDraft.value
|
||
}
|
||
|
||
function resetEditorValidation() {
|
||
void nextTick(() => orderFormRef.value?.clearValidate())
|
||
}
|
||
|
||
function cloneDraft(draft: FirstLegOrderDraft): FirstLegOrderDraft {
|
||
return {
|
||
...draft,
|
||
shipperPartySnapshot: draft.shipperPartySnapshot ? { ...draft.shipperPartySnapshot } : null,
|
||
consigneePartySnapshot: draft.consigneePartySnapshot ? { ...draft.consigneePartySnapshot } : null,
|
||
items: draft.items.map((item) => ({ ...item })),
|
||
}
|
||
}
|
||
|
||
function replaceForm(draft: FirstLegOrderDraft) {
|
||
Object.assign(form, cloneDraft(draft))
|
||
markEditorClean()
|
||
}
|
||
|
||
function draftFromOrder(order: FirstLegOrder): FirstLegOrderDraft {
|
||
return {
|
||
referenceNo: order.referenceNo,
|
||
transportMode: order.transportMode ?? 'FCL',
|
||
businessType: order.businessType,
|
||
origin: order.origin,
|
||
destination: order.destination,
|
||
receivingWarehouseId: order.receivingWarehouseId,
|
||
expectedDepartureDate: order.expectedDepartureDate,
|
||
shipperPartyId: order.shipperPartyId,
|
||
shipperPartySnapshot: order.shipperPartySnapshot ? { ...order.shipperPartySnapshot } : null,
|
||
shipper: order.shipper,
|
||
consigneePartyId: order.consigneePartyId,
|
||
consigneePartySnapshot: order.consigneePartySnapshot ? { ...order.consigneePartySnapshot } : null,
|
||
consignee: order.consignee,
|
||
priority: order.priority,
|
||
customerRemark: order.customerRemark,
|
||
tradeTerm: order.tradeTerm,
|
||
destinationCountryCode: order.destinationCountryCode,
|
||
destinationWarehouseId: order.destinationWarehouseId,
|
||
destinationWarehouseName: order.destinationWarehouseName,
|
||
pickupMethod: order.pickupMethod,
|
||
channelCode: order.channelCode,
|
||
channelName: order.channelName,
|
||
taxMode: order.taxMode,
|
||
customsInspection: order.customsInspection,
|
||
destinationType: order.destinationType,
|
||
amazonWarehouseCode: order.amazonWarehouseCode,
|
||
shipmentId: order.shipmentId,
|
||
items: order.items.map((item) => ({ ...item })),
|
||
}
|
||
}
|
||
|
||
function selectChannel(code: string) {
|
||
const channel = channelOptions.find((item) => item.code === code)
|
||
form.channelName = channel?.name ?? ''
|
||
}
|
||
|
||
async function selectDestinationWarehouse(id: string) {
|
||
const warehouse = availableDestinationWarehouses.value.find((item) => item.id === id)
|
||
if (!warehouse) return
|
||
form.destinationWarehouseId = warehouse.id
|
||
form.destinationWarehouseName = warehouse.name
|
||
form.destinationCountryCode = warehouse.countryCode.toUpperCase()
|
||
form.destination = warehouse.name
|
||
form.amazonWarehouseCode = form.destinationType === 'FBA' ? warehouse.code : ''
|
||
try {
|
||
const detail = await store.loadWarehouseDetail(warehouse.id)
|
||
form.destinationWarehouseName = detail.name
|
||
form.destinationCountryCode = detail.countryCode.toUpperCase()
|
||
form.destination = detail.name
|
||
form.amazonWarehouseCode = form.destinationType === 'FBA' ? detail.code : ''
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '目的仓详情加载失败')
|
||
}
|
||
}
|
||
|
||
async function changeDestinationType() {
|
||
form.destinationWarehouseId = ''
|
||
form.destinationWarehouseName = ''
|
||
form.destination = ''
|
||
form.amazonWarehouseCode = ''
|
||
selectedPrivateAddressId.value = ''
|
||
form.consigneePartyId = ''
|
||
form.consigneePartySnapshot = form.destinationType === 'PRIVATE' ? createEmptyPartySnapshot('CONSIGNEE') : null
|
||
form.consignee = ''
|
||
try {
|
||
await loadDestinationWarehouses(form.destinationType)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '目的仓数据加载失败')
|
||
}
|
||
if (form.destinationType !== 'PRIVATE' && availableDestinationWarehouses.value.length === 1) {
|
||
await selectDestinationWarehouse(availableDestinationWarehouses.value[0].id)
|
||
}
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function changePickupMethod() {
|
||
if (form.pickupMethod === 'PICKUP') {
|
||
form.shipperPartySnapshot ??= createPickupPartySnapshot()
|
||
} else {
|
||
form.shipperPartyId = ''
|
||
form.shipperPartySnapshot = null
|
||
form.shipper = ''
|
||
form.origin = ''
|
||
}
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function syncPrivateDestinationName(value: string) {
|
||
if (form.destinationType !== 'PRIVATE') return
|
||
form.destinationWarehouseName = value
|
||
form.destination = value.trim()
|
||
if (form.consigneePartySnapshot) form.consigneePartySnapshot.name = value
|
||
}
|
||
|
||
function selectPrivateAddress(id: string) {
|
||
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() {
|
||
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('请先完整填写私人地址和收件资料')
|
||
return
|
||
}
|
||
const snapshot = { ...form.consigneePartySnapshot }
|
||
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
|
||
const saved: CachedPrivateAddress = {
|
||
id: existing?.id ?? globalThis.crypto?.randomUUID?.() ?? `private-${Date.now()}`,
|
||
label: `${form.destinationWarehouseName.trim()} · ${snapshot.address}`,
|
||
consigneePartyId: partyId,
|
||
consigneePartySnapshot: snapshot,
|
||
consignee: snapshot.name,
|
||
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('已从当前账号的私人地址中移除')
|
||
}
|
||
|
||
async function loadSelectedWarehouse(id: string | number | boolean | undefined) {
|
||
const warehouseId = typeof id === 'string' ? id : typeof id === 'number' ? String(id) : ''
|
||
if (!warehouseId) return
|
||
try {
|
||
await store.loadWarehouseDetail(warehouseId)
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '仓库地址与收货时间加载失败')
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|
||
|
||
async function openCreate() {
|
||
if (availableWarehouses.value.length === 0) {
|
||
ElMessage.warning(supportingDataLoading.value
|
||
? '平台收货仓正在加载,请稍候再试'
|
||
: '当前没有可用的平台收货仓,请联系平台维护仓库后再创建订单')
|
||
return
|
||
}
|
||
editingId.value = undefined
|
||
replaceForm(emptyDraft())
|
||
resetEditorValidation()
|
||
await router.push({ name: 'first-leg-order-create' })
|
||
}
|
||
|
||
async function openEdit(order: FirstLegOrder) {
|
||
if (openingEditor.value || !canEditOrder.value || !canClientEditFirstLegOrder(order.status)) return
|
||
openingEditor.value = true
|
||
try {
|
||
const detailedOrder = await store.loadDetail(order.id)
|
||
const draft = draftFromOrder(detailedOrder)
|
||
if (draft.destinationType !== 'PRIVATE') await loadDestinationWarehouses(draft.destinationType)
|
||
const warehouseAvailable = availableWarehouses.value.some((item) => item.id === detailedOrder.receivingWarehouseId)
|
||
if (!warehouseAvailable) {
|
||
draft.receivingWarehouseId = ''
|
||
ElMessage.warning('原平台收货仓当前不可用,请重新选择可用收货仓')
|
||
}
|
||
if (draft.destinationType !== 'PRIVATE') {
|
||
const destinationAvailable = store.warehouses.some((item) => item.id === draft.destinationWarehouseId && item.warehouseType === draft.destinationType)
|
||
if (!destinationAvailable) {
|
||
draft.destinationWarehouseId = ''
|
||
draft.destinationWarehouseName = ''
|
||
draft.amazonWarehouseCode = ''
|
||
ElMessage.warning('原目的仓当前不可用,请重新选择平台维护的目的仓')
|
||
}
|
||
}
|
||
editingId.value = detailedOrder.id
|
||
replaceForm(draft)
|
||
if (warehouseAvailable) void loadSelectedWarehouse(detailedOrder.receivingWarehouseId)
|
||
detailOpen.value = false
|
||
editorOpen.value = true
|
||
resetEditorValidation()
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '订单详情加载失败,暂时无法编辑')
|
||
} finally {
|
||
openingEditor.value = false
|
||
}
|
||
}
|
||
|
||
async function openDetail(order: FirstLegOrder) {
|
||
detailId.value = order.id
|
||
detailOpen.value = true
|
||
detailLoading.value = true
|
||
detailError.value = ''
|
||
void loadInboundBilling(order)
|
||
try {
|
||
await store.loadDetail(order.id)
|
||
} catch (error) {
|
||
detailError.value = error instanceof Error ? error.message : '订单详情加载失败'
|
||
ElMessage.error(detailError.value)
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function retryDetail() {
|
||
const order = detailId.value ? store.getById(detailId.value) : undefined
|
||
if (order) await openDetail(order)
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
async function confirmDiscardChanges() {
|
||
try {
|
||
await ElMessageBox.confirm('当前订单还有未保存的修改。离开后这些内容将丢失。', '放弃未保存的修改?', {
|
||
type: 'warning',
|
||
confirmButtonText: '放弃修改',
|
||
cancelButtonText: '继续编辑',
|
||
})
|
||
return true
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return false
|
||
throw error
|
||
}
|
||
}
|
||
|
||
async function requestCloseEditor(done: () => void) {
|
||
if (saving.value) return
|
||
if (!editorDirty.value || await confirmDiscardChanges()) done()
|
||
}
|
||
|
||
function closeEditor() {
|
||
void requestCloseEditor(() => {
|
||
editorOpen.value = false
|
||
if (isCreatePage.value) {
|
||
markEditorClean()
|
||
void router.push({ name: 'first-leg-orders' })
|
||
}
|
||
})
|
||
}
|
||
|
||
function addCargoItem() {
|
||
form.items.push(createEmptyCargoItem(`new-cargo-${++cargoSequence}`))
|
||
resetEditorValidation()
|
||
}
|
||
|
||
function removeCargoItem(index: number) {
|
||
if (form.items.length === 1) {
|
||
ElMessage.warning('订单至少需要保留一条货物明细')
|
||
return
|
||
}
|
||
form.items.splice(index, 1)
|
||
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
|
||
}
|
||
|
||
async function saveDraft() {
|
||
if (saving.value) return
|
||
if (!await validateEditorForm()) return
|
||
saving.value = true
|
||
try {
|
||
const order = await store.saveDraft(cloneDraft(form), tenantStore.company.companyName, editingId.value)
|
||
editingId.value = order.id
|
||
markEditorClean()
|
||
editorOpen.value = false
|
||
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
|
||
ElMessage.success('订单草稿已保存')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '草稿保存失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function submitOrder() {
|
||
if (saving.value) return
|
||
if (companySubmissionBlocked.value) {
|
||
try {
|
||
await ElMessageBox.confirm('企业资料审核通过后才能提交订单。您可以继续保存草稿,或前往企业资料核对并补充信息。', '暂时无法提交订单', {
|
||
type: 'warning',
|
||
confirmButtonText: '前往企业资料',
|
||
cancelButtonText: '继续编辑草稿',
|
||
})
|
||
markEditorClean()
|
||
editorOpen.value = false
|
||
await router.push('/company/profile')
|
||
} catch (error) {
|
||
if (error === 'cancel' || error === 'close') return
|
||
throw error
|
||
}
|
||
return
|
||
}
|
||
if (!await validateEditorForm()) return
|
||
|
||
try {
|
||
saving.value = true
|
||
const knownCompanyAuditStatus = companyAuditLoaded.value ? tenantStore.company.auditStatus : undefined
|
||
const order = await store.submit(cloneDraft(form), tenantStore.company.companyName, editingId.value, knownCompanyAuditStatus)
|
||
markEditorClean()
|
||
editorOpen.value = false
|
||
if (isCreatePage.value) await router.push({ name: 'first-leg-orders' })
|
||
detailId.value = order.id
|
||
detailOpen.value = true
|
||
ElMessage.success('订单已提交,等待平台审核')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '订单提交失败')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
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 {
|
||
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,
|
||
})
|
||
} catch {
|
||
// Store keeps the recoverable error state rendered below the query toolbar.
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section class="first-leg-page" :class="{ 'is-create-route': isCreatePage }">
|
||
<template v-if="!isCreatePage">
|
||
<QueryToolbar v-model="filters" class="first-leg-query-toolbar" :fields="queryFields" ariaLabel="订单查询" @search="searchOrders" />
|
||
|
||
<div class="client-card order-table-card">
|
||
<div class="order-table-toolbar">
|
||
<div><strong>订单列表</strong><span>共 {{ filteredOrders.length }} 条</span></div>
|
||
<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>
|
||
</div>
|
||
<div v-if="companySubmissionBlocked && !supportingDataErrors.some((message) => message.startsWith('企业资料'))" class="order-eligibility is-pending" role="status">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>{{ canCreateOrder ? '企业资料待审核,当前仅可保存订单草稿' : '企业资料待审核,当前订单为只读' }}</strong><span>{{ canCreateOrder ? '平台审核通过后即可提交;您仍可创建、编辑和保存草稿。' : '您当前拥有只读权限,可以继续查看订单进展。' }}</span></div>
|
||
<el-button v-if="canViewCompanyProfile" link type="primary" @click="router.push('/company/profile')">查看企业资料</el-button>
|
||
</div>
|
||
<div v-if="supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>部分订单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||
<el-button :loading="supportingDataLoading || store.loading" @click="loadPageData">重新加载</el-button>
|
||
</div>
|
||
<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>
|
||
<div class="desktop-order-table">
|
||
<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="暂无订单">
|
||
<template #actions="{ row }">
|
||
<div class="order-row-actions">
|
||
<el-button link type="primary" @click="openDetail(row as FirstLegOrder)"><Eye :size="15" />详情</el-button>
|
||
<el-button v-if="canEditOrder && canClientEditFirstLegOrder((row as FirstLegOrder).status)" link type="primary" :loading="openingEditor" @click="openEdit(row as FirstLegOrder)"><Pencil :size="15" />编辑</el-button>
|
||
</div>
|
||
</template>
|
||
<template #order="{ row }">
|
||
<button class="order-number-link" type="button" @click="openDetail(row as FirstLegOrder)">{{ (row as FirstLegOrder).orderNo || '未提交草稿' }}</button>
|
||
<span v-if="(row as FirstLegOrder).businessSummary.inboundCode" class="order-reference">入仓码 {{ (row as FirstLegOrder).businessSummary.inboundCode }}</span>
|
||
<span class="order-reference">{{ (row as FirstLegOrder).referenceNo || '无客户参考号' }}</span>
|
||
</template>
|
||
<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>
|
||
<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">
|
||
<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>
|
||
</el-empty>
|
||
</template>
|
||
</DataTable>
|
||
</div>
|
||
<div v-loading="store.loading" class="mobile-order-surface">
|
||
<div v-if="filteredOrders.length" class="mobile-order-list">
|
||
<article v-for="order in filteredOrders" :key="order.id" class="mobile-order-card">
|
||
<header><button type="button" @click="openDetail(order)">{{ order.orderNo || '未提交草稿' }}</button><el-tag :type="getFirstLegStatusTone(order.status)" effect="light">{{ getFirstLegStatusLabel(order.status) }}</el-tag></header>
|
||
<span v-if="order.businessSummary.inboundCode" class="order-reference">入仓码 {{ order.businessSummary.inboundCode }}</span>
|
||
<span class="order-reference">{{ order.referenceNo || '无客户参考号' }}</span>
|
||
<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>
|
||
<footer><span>更新于 {{ formatDateTime(order.updatedAt) }}</span><div><el-button link type="primary" @click="openDetail(order)"><Eye :size="15" />详情</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(order.status)" link type="primary" :loading="openingEditor" @click="openEdit(order)"><Pencil :size="15" />编辑</el-button></div></footer>
|
||
</article>
|
||
</div>
|
||
<el-empty v-else :description="emptyDescription">
|
||
<el-button v-if="auth.hasPermission('order:first-leg:create')" type="primary" :loading="pageDataLoading" :disabled="pageDataLoading || availableWarehouses.length === 0" @click="openCreate"><Plus :size="16" />{{ pageDataLoading ? '加载收货仓中' : (availableWarehouses.length ? '创建第一笔订单' : '暂无可用收货仓') }}</el-button>
|
||
</el-empty>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-if="isCreatePage && supportingDataErrors.length" class="supporting-data-error" role="alert">
|
||
<AlertTriangle :size="20" />
|
||
<div><strong>下单配套数据加载失败</strong><span v-for="message in supportingDataErrors" :key="message">{{ message }}</span></div>
|
||
<el-button :loading="pageDataLoading" @click="loadPageData">重新加载</el-button>
|
||
</div>
|
||
<el-alert v-if="isCreatePage && !supportingDataLoading && availableWarehouses.length === 0" title="当前没有可用的平台收货仓" description="请联系平台维护并启用收货仓后再创建订单。" type="warning" :closable="false" show-icon />
|
||
|
||
<FirstLegOrderEditorShell v-model="editorOpen" :page="isCreatePage" :title="editingId ? '编辑订单草稿' : '创建订单'" :before-close="requestCloseEditor" @back="closeEditor">
|
||
<el-form ref="orderFormRef" v-loading="isCreatePage && supportingDataLoading" :model="form" :rules="orderFormRules" label-position="top" class="order-form prototype-order-form" scroll-to-error>
|
||
<section class="order-form-section prototype-section">
|
||
<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>
|
||
<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>
|
||
</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>
|
||
<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>
|
||
|
||
<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>
|
||
<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>
|
||
<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>
|
||
</div>
|
||
</section>
|
||
|
||
<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">
|
||
<header><strong>货物 {{ index + 1 }}</strong><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="16" />删除</el-button></header>
|
||
<div class="cargo-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>
|
||
<el-form-item label="中文品名" :prop="`items.${index}.cargoNameCn`" required :rules="requiredTextRules('请输入中文品名')"><el-input v-model="item.cargoNameCn" placeholder="例如:蓝牙音箱" /></el-form-item>
|
||
<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>
|
||
<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>
|
||
<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>
|
||
<el-form-item label="箱数"><el-input-number v-model="item.packageCount" :min="0" controls-position="right" /></el-form-item>
|
||
<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>
|
||
<el-form-item label="申报货值"><el-input-number v-model="item.declaredValue" :min="0" :precision="2" controls-position="right" /></el-form-item>
|
||
<el-form-item label="币种"><el-input v-model="item.currency" maxlength="3" placeholder="例如:USD" /></el-form-item>
|
||
<el-form-item label="危险品"><el-switch v-model="item.dangerousGoods" /></el-form-item>
|
||
<template v-if="form.transportMode === 'FCL'"><el-form-item label="箱型" :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>
|
||
</article>
|
||
<p class="cargo-tip">FBA 货物必须填写 SKU、UPC、货件编号和跟踪编号;提交前请核对重量与体积。</p>
|
||
</section>
|
||
</el-form>
|
||
<template #footer><div class="editor-actions"><el-button :disabled="saving" @click="closeEditor">返回</el-button><el-button v-if="editingId ? canEditOrder : canCreateOrder" :loading="saving" @click="saveDraft">保存草稿</el-button><el-button v-if="auth.hasPermission('order:first-leg:submit')" type="primary" :loading="saving" @click="submitOrder"><Send :size="16" />{{ companySubmissionBlocked ? '提交前需完成资料审核' : '提交订单' }}</el-button></div></template>
|
||
</FirstLegOrderEditorShell>
|
||
|
||
<el-drawer v-model="detailOpen" class="first-leg-detail" title="订单详情" size="820px">
|
||
<div v-loading="detailLoading" class="order-detail-content">
|
||
<div v-if="detailError" class="order-load-state is-compact" role="alert">
|
||
<AlertTriangle :size="24" />
|
||
<div><strong>订单详情加载失败</strong><span>{{ detailError }}</span></div>
|
||
<el-button :loading="detailLoading" @click="retryDetail">重新加载</el-button>
|
||
</div>
|
||
<template v-else-if="detailOrder">
|
||
<div class="order-detail-heading">
|
||
<div><span>{{ getFirstLegTransportModeLabel(detailOrder.transportMode) }}</span><h2>{{ detailOrder.orderNo || '未提交草稿' }}</h2><p>{{ detailOrder.origin || '起运地待更新' }} → {{ detailOrder.destination || '目的地待更新' }}</p></div>
|
||
<el-tag :type="getFirstLegStatusTone(detailOrder.status)" effect="light">{{ getFirstLegStatusLabel(detailOrder.status) }}</el-tag>
|
||
</div>
|
||
<section class="detail-section">
|
||
<h3>订单与运输</h3>
|
||
<dl class="order-detail-grid">
|
||
<div v-if="detailOrder.businessSummary.inboundCode"><dt>入仓码</dt><dd>{{ detailOrder.businessSummary.inboundCode }}</dd></div>
|
||
<div><dt>客户参考号</dt><dd>{{ 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>
|
||
<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>
|
||
<section class="detail-section">
|
||
<h3>履约快递信息</h3>
|
||
<div v-if="detailOrder.expressCompanyName || detailOrder.trackingNumber" class="express-information"><PackageCheck :size="20" /><div><span>快递公司</span><strong>{{ detailOrder.expressCompanyName || '待平台补充' }}</strong></div><div><span>快递单号</span><strong>{{ detailOrder.trackingNumber || '待平台补充' }}</strong></div></div>
|
||
<div v-else class="express-empty"><PackageCheck :size="20" /><div><strong>平台尚未维护快递信息</strong><span>快递公司和快递单号由平台后台维护,客户端仅只读展示。</span></div></div>
|
||
</section>
|
||
<section class="detail-section"><h3>货物明细</h3><div v-if="detailSummary" class="cargo-summary"><div><Box :size="18" /><span>明细</span><strong>{{ detailOrder.items.length }} 项</strong></div><div><span>件数</span><strong>{{ detailSummary.pieces }}</strong></div><div><span>毛重</span><strong>{{ detailSummary.grossWeightKg.toLocaleString() }} kg</strong></div><div><span>体积</span><strong>{{ detailSummary.volumeCbm }} m³</strong></div></div><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" />
|
||
<InboundBillingPanel
|
||
:data="inboundBilling"
|
||
:loading="inboundBillingLoading"
|
||
:unavailable="inboundBillingUnavailable"
|
||
:unavailable-message="inboundBillingUnavailableMessage"
|
||
:error="inboundBillingError"
|
||
@retry="retryInboundBilling"
|
||
/>
|
||
</template>
|
||
</div>
|
||
<template #footer><div v-if="detailOrder" class="editor-actions"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="canEditOrder && canClientEditFirstLegOrder(detailOrder.status)" :loading="openingEditor" @click="openEdit(detailOrder)"><Pencil :size="16" />编辑草稿</el-button></div></template>
|
||
</el-drawer>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.first-leg-page { display: flex; flex-direction: column; gap: 14px; }
|
||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading) { margin-bottom: 0; }
|
||
:global(.client-main:has(> .first-leg-page:not(.is-create-route)) > .client-page-heading > div) { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; border: 0; clip: rect(0 0 0 0); white-space: nowrap; }
|
||
.first-leg-query-toolbar { flex-wrap: nowrap; }
|
||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { display: grid; min-width: 0; flex: 1 1 auto; grid-template-columns: minmax(180px, 1.4fr) minmax(120px, .72fr) minmax(210px, 1.15fr); }
|
||
.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; }
|
||
.order-eligibility { display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border: 1px solid; }
|
||
.order-eligibility svg { flex: 0 0 20px; margin-top: 1px; }
|
||
.order-eligibility div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 3px; }
|
||
.order-eligibility strong { color: var(--navy); font-size: 13px; }
|
||
.order-eligibility span { color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||
.order-eligibility.is-pending { border-color: #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||
.supporting-data-error { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 12px; padding: 14px 16px; border: 1px solid #e5d2a5; background: #fff9eb; color: #9a6a12; }
|
||
.supporting-data-error > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||
.supporting-data-error strong { color: var(--navy); font-size: 13px; }
|
||
.supporting-data-error span { overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||
.order-row-actions, .editor-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||
.order-table-card { padding: 0; overflow: hidden; }
|
||
.order-table-toolbar { display: flex; min-height: 58px; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
||
.order-table-toolbar > div { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
|
||
.order-table-toolbar strong { color: var(--navy); font-size: 14px; }
|
||
.order-table-toolbar span { color: var(--muted); font-size: 11px; }
|
||
.order-table-card > .order-eligibility, .order-table-card > .supporting-data-error { border-width: 0 0 1px; }
|
||
.order-load-state { display: grid; min-height: 220px; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 14px; padding: 28px; color: #9a5a09; }
|
||
.order-load-state > div { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||
.order-load-state strong { color: var(--navy); font-size: 14px; }
|
||
.order-load-state span { overflow-wrap: anywhere; color: var(--muted); font-size: 12px; line-height: 1.55; }
|
||
.order-load-state.is-compact { min-height: 180px; }
|
||
.order-detail-content { min-height: 180px; }
|
||
.mobile-order-surface { display: none; min-height: 220px; }
|
||
.mobile-order-list { display: none; }
|
||
.order-number-link { display: block; padding: 0; border: 0; background: none; color: var(--primary); cursor: pointer; font-weight: 700; text-align: left; }
|
||
.order-number-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||
.order-reference { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; }
|
||
:global(.first-leg-editor .el-drawer__header) { flex: 0 0 auto; min-height: 58px; margin-bottom: 0; padding: 17px 22px; border-bottom: 1px solid var(--border); }
|
||
:global(.first-leg-editor .el-drawer__body) { min-height: 0; padding: 18px 22px 32px; overscroll-behavior: contain; }
|
||
:global(.first-leg-editor .el-drawer__footer) { flex: 0 0 auto; padding: 12px 22px; border-top: 1px solid var(--border); background: #fff; }
|
||
.order-form { width: min(100%, 1000px); margin: 0 auto; padding: 0 0 12px; }
|
||
.order-form :deep(.el-form-item__error) { position: static; padding-top: 6px; overflow-wrap: anywhere; line-height: 1.45; }
|
||
.order-form-section + .order-form-section { margin-top: 24px; padding-top: 22px; border-top: 1px solid var(--border); }
|
||
.order-form-section h3 { margin: 0 0 16px; color: var(--navy); font-size: 16px; }
|
||
.order-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; }
|
||
.order-form-grid > *, .cargo-primary-grid > *, .cargo-secondary-grid > * { min-width: 0; }
|
||
.order-form :deep(.el-select), .order-form :deep(.el-date-editor), .order-form :deep(.el-input-number) { width: 100%; }
|
||
.form-span-full { grid-column: 1 / -1; }
|
||
.prototype-section { padding: 2px 2px 0; }
|
||
.segmented-options { display: flex; width: min(100%, 620px); }
|
||
.segmented-options :deep(.el-radio-button) { flex: 1; }
|
||
.segmented-options :deep(.el-radio-button__inner) { display: flex; width: 100%; min-height: 42px; box-sizing: border-box; align-items: center; justify-content: center; padding: 11px 16px; }
|
||
.warehouse-options { display: flex; flex-wrap: wrap; gap: 8px; }
|
||
.warehouse-options :deep(.el-radio-button__inner) { display: flex; min-height: 40px; box-sizing: border-box; align-items: center; justify-content: center; border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||
.warehouse-options :deep(.el-radio-button.is-active .el-radio-button__inner) { border-color: var(--primary); }
|
||
.selected-warehouse { display: flex; align-items: flex-start; gap: 10px; margin-top: 0; padding: 11px 13px; border: 1px solid #d9def8; background: #f6f7fe; color: var(--primary); }
|
||
.selected-warehouse svg { flex: 0 0 17px; margin-top: 2px; }
|
||
.selected-warehouse div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||
.selected-warehouse span, .selected-warehouse small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||
.order-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||
.order-section-heading h3 { margin-bottom: 5px; }
|
||
.order-section-heading p { margin: 0; color: var(--muted); font-size: 11px; }
|
||
.prototype-form-flow { display: flex; max-width: 900px; flex-direction: column; padding-top: 4px; }
|
||
.prototype-form-row { display: grid; width: 100%; grid-template-columns: 140px minmax(0, 560px); align-items: start; margin-bottom: 18px; }
|
||
.prototype-form-row--wide { grid-template-columns: 140px minmax(0, 720px); }
|
||
.prototype-form-row--compact { grid-template-columns: 140px minmax(0, 420px); }
|
||
.prototype-form-row--short { grid-template-columns: 140px minmax(0, 280px); }
|
||
.prototype-form-row--tabs { grid-template-columns: 140px minmax(0, 520px); margin-bottom: 22px; }
|
||
.prototype-form-row :deep(.el-form-item__label) { width: auto !important; min-height: 40px; justify-content: flex-end; padding: 0 20px 0 0; line-height: 40px; }
|
||
.prototype-form-row :deep(.el-form-item__content) { display: block; min-width: 0; line-height: normal; }
|
||
.prototype-form-row :deep(.el-input__wrapper), .prototype-form-row :deep(.el-select__wrapper) { min-height: 40px; }
|
||
.address-book-control { display: grid; grid-template-columns: minmax(260px, 1fr) auto auto; gap: 10px; align-items: center; }
|
||
.address-book-control :deep(.el-button) { min-height: 40px; margin-left: 0; }
|
||
.field-guidance { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; }
|
||
.selected-destination { display: flex; width: min(100%, 720px); align-items: flex-start; gap: 9px; margin: -4px 0 18px 140px; padding: 10px 12px; background: #f2f7f6; color: var(--primary); }
|
||
.selected-destination svg { flex: 0 0 17px; margin-top: 2px; }
|
||
.selected-destination div { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
|
||
.selected-destination span { color: var(--navy); font-size: 12px; font-weight: 600; }
|
||
.selected-destination small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||
.flow-divider { width: min(100%, 860px); margin: 4px 0 22px; border-top: 1px solid var(--border); }
|
||
.compact-transport-grid { margin-top: 18px; }
|
||
.cargo-entry { padding: 16px; border: 1px solid var(--border); background: #fbfcfc; }
|
||
.cargo-entry + .cargo-entry { margin-top: 12px; }
|
||
.cargo-entry > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||
.cargo-primary-grid { display: grid; grid-template-columns: repeat(5, minmax(130px, 1fr)); column-gap: 12px; }
|
||
.cargo-secondary-grid { display: grid; grid-template-columns: repeat(4, minmax(130px, 1fr)); column-gap: 12px; padding-top: 6px; }
|
||
.cargo-more { border-top: 1px 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: 12px 0 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
|
||
.order-detail-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
||
.order-detail-heading span { color: var(--muted); font-size: 11px; }
|
||
.order-detail-heading h2 { margin: 6px 0; font-size: 21px; }
|
||
.order-detail-heading p { margin: 0; color: var(--muted); }
|
||
.order-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0; margin: 18px 0; border: 1px solid var(--border); }
|
||
.order-detail-grid div { padding: 13px; border-bottom: 1px solid var(--border); }
|
||
.order-detail-grid div:nth-child(odd) { border-right: 1px solid var(--border); }
|
||
.order-detail-grid dt { color: var(--muted); font-size: 11px; }
|
||
.order-detail-grid dd { margin: 5px 0 0; font-size: 13px; font-weight: 600; }
|
||
.detail-subline { display: block; margin-top: 4px; color: var(--muted); font-size: 11px; font-weight: 400; line-height: 1.5; }
|
||
.detail-section { margin-top: 22px; }
|
||
.detail-section h3 { margin: 0 0 12px; font-size: 15px; }
|
||
.party-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||
.party-detail-grid article { min-width: 0; padding: 15px; border: 1px solid var(--border); background: #fbfcfc; }
|
||
.party-detail-grid span { display: block; color: var(--muted); font-size: 10px; }
|
||
.party-detail-grid strong { display: block; margin-top: 5px; overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
|
||
.party-detail-grid p, .customer-remark p { margin: 7px 0 0; overflow-wrap: anywhere; color: var(--muted); font-size: 11px; line-height: 1.65; }
|
||
.customer-remark { margin-top: 10px; padding: 13px 15px; background: #f2f7f6; }
|
||
.customer-remark strong { color: var(--navy); font-size: 11px; }
|
||
.fulfillment-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border: 1px solid var(--border); }
|
||
.fulfillment-summary > div { min-width: 0; padding: 13px; border-bottom: 1px solid var(--border); }
|
||
.fulfillment-summary > div:not(:nth-child(3n)) { border-right: 1px solid var(--border); }
|
||
.fulfillment-summary > div:nth-last-child(-n + 3) { border-bottom: 0; }
|
||
.fulfillment-summary dt { color: var(--muted); font-size: 10px; }
|
||
.fulfillment-summary dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 650; }
|
||
.express-information { display: grid; grid-template-columns: auto repeat(2, minmax(0, 1fr)); align-items: center; gap: 16px; padding: 15px 16px; background: #f2f7f6; color: var(--primary); }
|
||
.express-information > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||
.express-information span { color: var(--muted); font-size: 11px; }
|
||
.express-information strong { overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
|
||
.express-empty { display: flex; align-items: flex-start; gap: 12px; padding: 15px 16px; border: 1px dashed #cbd5da; color: var(--muted); }
|
||
.express-empty svg { flex: 0 0 20px; color: var(--primary); }
|
||
.express-empty div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||
.express-empty strong { color: var(--navy); font-size: 13px; }
|
||
.express-empty span { font-size: 11px; line-height: 1.5; }
|
||
.cargo-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--border); }
|
||
.cargo-summary div { display: flex; min-height: 74px; align-items: center; justify-content: center; flex-direction: column; gap: 4px; border-right: 1px solid var(--border); }
|
||
.cargo-summary div:last-child { border-right: 0; }
|
||
.cargo-summary span { color: var(--muted); font-size: 11px; }
|
||
.cargo-detail-list { display: grid; gap: 10px; margin-top: 10px; }
|
||
.cargo-detail-list article { padding: 14px 15px; border: 1px solid var(--border); }
|
||
.cargo-detail-list header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: 10px; }
|
||
.cargo-detail-list header span, .cargo-detail-list header small { color: var(--muted); font-size: 10px; }
|
||
.cargo-detail-list header strong { overflow-wrap: anywhere; color: var(--navy); font-size: 13px; }
|
||
.cargo-detail-list header small { overflow-wrap: anywhere; text-align: right; }
|
||
.cargo-detail-list dl { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px 16px; margin: 13px 0 0; }
|
||
.cargo-detail-list dt { color: var(--muted); font-size: 10px; }
|
||
.cargo-detail-list dd { margin: 4px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 11px; font-weight: 600; }
|
||
.cargo-detail-list article > p { margin: 11px 0 0; padding-top: 10px; border-top: 1px solid var(--border); color: var(--muted); font-size: 11px; }
|
||
:deep(.el-timeline-item__content p), :deep(.el-timeline-item__content > span) { color: var(--muted); }
|
||
:deep(.el-timeline-item__content p) { margin: 5px 0; font-size: 12px; }
|
||
:deep(.el-timeline-item__content > span) { font-size: 11px; }
|
||
@media (max-width: 1280px) {
|
||
.desktop-order-table { display: none; }
|
||
.mobile-order-surface { display: block; }
|
||
.mobile-order-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 10px; }
|
||
.mobile-order-card { min-width: 0; padding: 14px; border: 1px solid var(--border); background: #fff; }
|
||
.mobile-order-card header, .mobile-order-card footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.mobile-order-card header button { min-width: 0; overflow: hidden; padding: 0; border: 0; background: none; color: var(--primary); font-weight: 750; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mobile-order-card header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||
.mobile-order-card dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
|
||
.mobile-order-card dl div { min-width: 0; }
|
||
.mobile-order-card dt, .mobile-order-card footer > span { color: var(--muted); font-size: 10px; }
|
||
.mobile-order-card dd { overflow: hidden; margin: 4px 0 0; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mobile-order-card dd span { margin-left: 4px; color: var(--muted); font-weight: 400; }
|
||
.mobile-order-card footer { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
|
||
.mobile-order-card footer > div { display: flex; }
|
||
}
|
||
@media (min-width: 721px) and (max-width: 1050px) {
|
||
.first-leg-query-toolbar :deep(.query-toolbar__filters) { grid-template-columns: repeat(2, minmax(140px, 1fr)); }
|
||
}
|
||
@media (max-width: 720px) {
|
||
.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: 10px; }
|
||
.order-table-toolbar { align-items: stretch; flex-direction: column; gap: 10px; }
|
||
.order-table-toolbar > div { justify-content: space-between; }
|
||
.order-table-toolbar .el-button { width: 100%; min-height: 40px; }
|
||
.order-eligibility { flex-wrap: wrap; }
|
||
.order-eligibility .el-button { width: 100%; min-height: 44px; }
|
||
.order-form-grid, .order-detail-grid, .cargo-primary-grid, .cargo-secondary-grid, .party-detail-grid { grid-template-columns: 1fr; }
|
||
.fulfillment-summary { grid-template-columns: 1fr; }
|
||
.fulfillment-summary > div, .fulfillment-summary > div:not(:nth-child(3n)) { border-right: 0; border-bottom: 1px solid var(--border); }
|
||
.fulfillment-summary > div:last-child { border-bottom: 0; }
|
||
.prototype-form-row, .prototype-form-row--wide, .prototype-form-row--compact, .prototype-form-row--short, .prototype-form-row--tabs { display: block; margin-bottom: 18px; }
|
||
.prototype-form-row :deep(.el-form-item__label) { min-height: auto; justify-content: flex-start; padding: 0 0 8px; line-height: 1.4; }
|
||
.selected-destination { width: 100%; margin-left: 0; }
|
||
.address-book-control { grid-template-columns: 1fr auto; }
|
||
.address-book-control :deep(.el-select) { grid-column: 1 / -1; }
|
||
.segmented-options { width: 100%; align-items: stretch; flex-direction: column; }
|
||
.segmented-options :deep(.el-radio-button__inner) { border: 1px solid var(--border); border-radius: 6px !important; box-shadow: none !important; }
|
||
.warehouse-options { display: grid; width: 100%; grid-template-columns: 1fr; }
|
||
.warehouse-options :deep(.el-radio-button), .warehouse-options :deep(.el-radio-button__inner) { width: 100%; }
|
||
:global(.first-leg-editor .el-drawer__header) { min-height: 56px; padding: 15px 20px; }
|
||
:global(.first-leg-editor .el-drawer__body) { padding: 16px 20px 28px; }
|
||
:global(.first-leg-editor .el-drawer__footer) { padding: 10px 20px; }
|
||
.form-span-full { grid-column: auto; }
|
||
.editor-actions :deep(.el-button) { min-height: 44px; }
|
||
.desktop-order-table { display: none; }
|
||
.order-load-state { min-height: 240px; grid-template-columns: 1fr; justify-items: start; }
|
||
.supporting-data-error { grid-template-columns: auto minmax(0, 1fr); }
|
||
.supporting-data-error .el-button { grid-column: 1 / -1; width: 100%; min-height: 44px; }
|
||
.order-load-state .el-button { width: 100%; min-height: 44px; }
|
||
.mobile-order-list { grid-template-columns: 1fr; }
|
||
.mobile-order-card { padding: 14px; border: 1px solid var(--border); background: #fff; }
|
||
.mobile-order-card header, .mobile-order-card footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||
.mobile-order-card header button { padding: 0; border: 0; background: none; color: var(--primary); font-weight: 750; }
|
||
.mobile-order-card header button:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }
|
||
.mobile-order-card dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
|
||
.mobile-order-card dl div { min-width: 0; }
|
||
.mobile-order-card dt, .mobile-order-card footer > span { color: var(--muted); font-size: 10px; }
|
||
.mobile-order-card dd { overflow: hidden; margin: 4px 0 0; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mobile-order-card dd span { margin-left: 4px; color: var(--muted); font-weight: 400; }
|
||
.mobile-order-card footer { align-items: flex-end; padding-top: 10px; border-top: 1px solid var(--border); }
|
||
.mobile-order-card footer > div { display: flex; }
|
||
.order-detail-grid div:nth-child(odd) { border-right: 0; }
|
||
.express-information { grid-template-columns: auto 1fr; }
|
||
.express-information > div:last-child { grid-column: 2; }
|
||
.cargo-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.cargo-summary div:nth-child(2) { border-right: 0; }
|
||
.cargo-summary div:nth-child(-n+2) { border-bottom: 1px solid var(--border); }
|
||
.cargo-detail-list header { grid-template-columns: 1fr; gap: 4px; }
|
||
.cargo-detail-list header small { text-align: left; }
|
||
.cargo-detail-list dl { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
:global(.first-leg-editor), :global(.first-leg-detail) { width: 100% !important; }
|
||
}
|
||
@media (max-width: 360px) {
|
||
.cargo-detail-list dl { grid-template-columns: 1fr; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
.editor-actions { display: grid; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||
.editor-actions :deep(.el-button) { width: 100%; min-width: 0; margin-left: 0; }
|
||
.editor-actions :deep(.el-button:nth-child(3)) { grid-column: 1 / -1; }
|
||
}
|
||
</style>
|