2026-08-06 13:56:04 +08:00
< script setup lang = "ts" >
2026-09-14 17:06:55 +08:00
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch } from 'vue'
2026-09-07 12:21:44 +08:00
import { ElMessage , ElMessageBox , type UploadFile } from 'element-plus'
2026-09-14 17:06:55 +08:00
import { ArrowLeft , ChevronDown , ChevronUp , CircleCheck , Download , FileUp , GitBranch , Link2 , MoreHorizontal , PackageOpen , Printer , Send , ShieldAlert , SlidersHorizontal , StickyNote , Undo2 } from '@lucide/vue'
2026-08-06 13:56:04 +08:00
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
2026-08-25 22:08:59 +08:00
downloadPlatformProductImage ,
2026-08-06 18:02:04 +08:00
approvePlatformUnifiedOrder ,
2026-08-17 18:00:42 +08:00
batchApprovePlatformUnifiedOrders ,
2026-09-08 17:38:09 +08:00
getPlatformUnifiedOrderBatchApprovalPreview ,
2026-09-14 16:56:11 +08:00
updatePlatformUnifiedOrderRemarks ,
2026-08-06 13:56:04 +08:00
listPlatformUnifiedOrders ,
2026-09-03 11:47:32 +08:00
updatePlatformUnifiedOrderPlanning ,
2026-08-06 13:56:04 +08:00
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
2026-08-31 10:31:53 +08:00
type UnifiedOrderCoLoadGroupSummary ,
2026-08-06 13:56:04 +08:00
type UnifiedOrderMainStatus ,
type UnifiedOrderResource ,
2026-09-08 17:38:09 +08:00
type UnifiedOrderBatchApprovalPreviewItem ,
type UnifiedOrderBatchReviewItem ,
2026-08-06 13:56:04 +08:00
} from '@/modules/unified-orders/api/unifiedOrders'
2026-08-31 14:54:57 +08:00
import { cancelPlatformCoLoadGroup , createPlatformCoLoadGroup , getPlatformCoLoadGroupByInboundCode , type CoLoadBusinessSummary , type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
2026-08-11 09:16:58 +08:00
import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue'
2026-08-12 16:31:46 +08:00
import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue'
2026-09-02 10:24:07 +08:00
import OrderDeclarationEditorDialog from '@/modules/unified-orders/components/OrderDeclarationEditorDialog.vue'
2026-08-06 13:56:04 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
2026-08-06 15:32:02 +08:00
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
2026-09-01 21:09:39 +08:00
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
import { createPlatformExportTask } from '@/modules/downloads'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { getLoadingAssociationSummary , getOverseasInboundCodeRelationship , getPlatformInboundCodeAssociation , type LoadingAssociationSummary , type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
2026-08-10 15:36:53 +08:00
import AdminPage from '@/shared/components/AdminPage.vue'
2026-08-06 13:56:04 +08:00
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-06 18:02:04 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-12 09:25:55 +08:00
import AppDrawer from '@/shared/components/AppDrawer.vue'
2026-08-06 13:56:04 +08:00
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-10 15:36:53 +08:00
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
2026-08-06 13:56:04 +08:00
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-08-07 18:44:55 +08:00
import LineClampText from '@/shared/components/LineClampText.vue'
2026-08-06 13:56:04 +08:00
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
2026-08-11 19:35:28 +08:00
import {
firstLegBusinessTypeLabel ,
firstLegContainerModeLabel ,
firstLegDestinationTypeLabel ,
firstLegPickupMethodLabel ,
firstLegTaxModeLabel ,
firstLegTradeTermLabel ,
firstLegTransportModeLabel ,
} from '@/shared/utils/firstLegBusinessLabels'
2026-08-15 17:04:00 +08:00
import { SmartLoadingDialog } from '@/modules/container-loading'
2026-08-11 19:35:28 +08:00
import {
unifiedOrderHistoryActionLabel ,
unifiedOrderLoadingStatusLabel ,
unifiedOrderReviewStatusLabel ,
unifiedOrderStatusLabel ,
} from '@/modules/unified-orders/status'
2026-08-07 14:47:11 +08:00
import {
2026-09-09 17:58:23 +08:00
adjustPublishedInboundFee ,
2026-08-07 14:47:11 +08:00
downloadWeightImage ,
2026-09-03 10:46:07 +08:00
calculateInboundFee ,
confirmInboundFee ,
2026-09-09 17:58:23 +08:00
createFeeAdjustmentIdempotencyKey ,
2026-09-03 10:46:07 +08:00
createFeeActionIdempotencyKey ,
2026-09-03 21:23:35 +08:00
getLatestInboundFee ,
2026-08-07 14:47:11 +08:00
getInboundMeasurement ,
2026-09-07 11:20:43 +08:00
getPlatformInboundHold ,
2026-09-10 11:17:45 +08:00
downloadInboundCartonLabelsBatch ,
2026-09-07 12:21:44 +08:00
downloadPlatformInboundHoldEvidence ,
2026-09-07 11:20:43 +08:00
holdPlatformInbound ,
releasePlatformInbound ,
2026-09-07 12:21:44 +08:00
uploadPlatformInboundHoldEvidence ,
2026-09-10 11:02:52 +08:00
updateMeasurementCalculationStrategy ,
2026-09-10 11:38:38 +08:00
updateMeasurementOuterDimensions ,
2026-08-17 18:32:41 +08:00
InboundFeePanel ,
2026-08-17 17:49:10 +08:00
InboundMeasurementDialog ,
2026-08-17 18:00:42 +08:00
MeasurementEvidenceThumbnail ,
2026-09-14 17:52:00 +08:00
DropshipPalletPrintPanel ,
2026-09-03 10:46:07 +08:00
sendInboundFee ,
2026-09-09 17:58:23 +08:00
type InboundFeeSnapshot ,
2026-08-07 14:47:11 +08:00
type InboundMeasurementDetail ,
2026-09-07 11:20:43 +08:00
type InboundHoldDetail ,
2026-08-07 14:47:11 +08:00
} from '@/modules/inbounds'
2026-09-03 10:46:07 +08:00
import { listCustomerChargeRules , type CustomerChargeRule } from '@/modules/fba-pricing'
import { getCustomerDimensionConfig , type CustomerDimensionConfig , type CustomerDimensionRule } from '@/modules/dimension-config'
2026-08-06 13:56:04 +08:00
type FilterModel = QueryModel & {
tenantId : string
2026-08-06 15:32:02 +08:00
receivingWarehouseId : string
destinationWarehouseId : string
inboundCode : string
expectedArrivalDate : string
feeReviewStatus : string
channelName : string
containerNo : string
cabinetNo : string
sailingDate : string
loaded : string
fullContainer : string
measurementReviewStatus : string
fbaCode : string
2026-08-06 13:56:04 +08:00
createdRange : string []
}
const route = useRoute ()
const router = useRouter ()
const access = useAccessPreviewStore ()
const records = ref < UnifiedOrderResource [] >([])
const customers = ref < PlatformTenantResource [] >([])
2026-08-06 15:32:02 +08:00
const warehouses = ref < PlatformWarehouseSummaryResource [] >([])
2026-08-06 13:56:04 +08:00
const page = ref ( 1 )
const pageSize = ref ( 20 )
const total = ref ( 0 )
const loading = ref ( false )
const loadError = ref < ApiErrorDetails | null >( null )
2026-09-03 11:47:32 +08:00
type PlanningDraft = {
expectedArrivalAt : string
estimatedVolumeCbm : number | null
savedExpectedArrivalAt : string
savedEstimatedVolumeCbm : number | null
saving : boolean
}
const planningDrafts = reactive < Record < string , PlanningDraft | undefined > > ({})
2026-08-06 13:56:04 +08:00
const customerLoading = ref ( false )
const customerLoadError = ref < ApiErrorDetails | null >( null )
2026-08-06 15:32:02 +08:00
const warehouseLoading = ref ( false )
const warehouseLoadError = ref < ApiErrorDetails | null >( null )
2026-08-06 13:56:04 +08:00
const detail = ref < UnifiedOrderDetailResource | null >( null )
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null >( null )
2026-09-02 10:24:07 +08:00
const declarationEditorOpen = ref ( false )
2026-08-06 19:03:13 +08:00
const activeDetailTab = ref ( 'basic' )
2026-08-17 19:13:25 +08:00
const automaticFee = ref < AutomaticFeeSummary | null >( null )
2026-09-04 11:50:34 +08:00
const feePanelRevision = ref ( 0 )
2026-08-17 19:13:25 +08:00
const otherFeeSummary = ref < OtherFeeSummary >({ currency : 'CNY' , approvedReceivable : 0 , approvedPayable : 0 })
const customerFeeTotal = computed (() => (
( automaticFee . value ? . feeStatus === 'SENT' ? Number ( automaticFee . value . totalAmount || 0 ) : 0 )
+ otherFeeSummary . value . approvedReceivable
))
function feeMoney ( value : number ) {
return new Intl . NumberFormat ( 'zh-CN' , { style : 'currency' , currency : otherFeeSummary . value . currency || 'CNY' }). format ( value )
}
2026-09-09 17:58:23 +08:00
function feeConfirmMoney ( value : number , currency = 'CNY' ) {
return new Intl . NumberFormat ( 'zh-CN' , {
style : 'currency' ,
currency ,
}). format ( value )
}
2026-08-17 19:13:25 +08:00
type OtherFeeSummary = { currency : string ; approvedReceivable : number ; approvedPayable : number }
type AutomaticFeeSummary = { feeStatus : string ; totalAmount : number }
2026-08-06 18:02:04 +08:00
const approvalOpen = ref ( false )
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
2026-08-17 18:00:42 +08:00
const batchApprovalSaving = ref ( false )
2026-09-08 17:38:09 +08:00
const batchApprovalLoading = ref ( false )
const batchApprovalOpen = ref ( false )
const batchApprovalError = ref < ApiErrorDetails | null >( null )
const batchApprovalPreviewRows = ref < UnifiedOrderBatchApprovalPreviewItem [] >([])
const batchApprovalSelectedRows = ref < UnifiedOrderBatchApprovalPreviewItem [] >([])
const batchApprovalTableRef = ref < { toggleRowSelection : ( row : UnifiedOrderBatchApprovalPreviewItem , selected ?: boolean ) => void } > ()
2026-09-01 21:09:39 +08:00
const exportSubmitting = ref ( false )
2026-09-10 11:17:45 +08:00
const printingCartonLabels = ref ( false )
2026-08-17 18:00:42 +08:00
const selectedApprovalOrders = ref < UnifiedOrderResource [] >([])
2026-08-29 10:57:01 +08:00
const selectedOrders = ref < UnifiedOrderResource [] >([])
2026-09-10 11:17:45 +08:00
const selectedCartonLabelOrders = computed (() => selectedOrders . value . filter (( row ) => row . inboundId != null ))
2026-08-29 10:57:01 +08:00
const coLoadCreateOpen = ref ( false )
2026-08-31 10:31:53 +08:00
const coLoadDetailOpen = ref ( false )
const coLoadDetailInboundCode = ref ( '' )
const coLoadDetailSummary = ref < CoLoadBusinessSummary | null >( null )
const coLoadDetailLoading = ref ( false )
const coLoadDetailError = ref < ApiErrorDetails | null >( null )
2026-08-31 14:54:57 +08:00
const coLoadCancelError = ref < ApiErrorDetails | null >( null )
const coLoadCancelSaving = ref ( false )
2026-08-29 10:57:01 +08:00
const coLoadSaving = ref ( false )
const coLoadError = ref < ApiErrorDetails | null >( null )
2026-08-06 18:02:04 +08:00
const approvalOrder = ref < UnifiedOrderResource | null >( null )
2026-08-06 19:03:13 +08:00
const noteOpen = ref ( false )
const noteSaving = ref ( false )
2026-09-14 16:56:11 +08:00
const noteForm = reactive ({ internalRemark : '' , warehouseRemark : '' , customerServiceRemark : '' })
2026-08-06 20:14:56 +08:00
const noteOrder = ref < UnifiedOrderResource | null >( null )
2026-09-07 11:20:43 +08:00
const holdDetail = ref < InboundHoldDetail | null >( null )
const holdLoading = ref ( false )
const holdError = ref < ApiErrorDetails | null >( null )
const holdLoadedKey = ref ( '' )
const holdDialogOpen = ref ( false )
const holdSaving = ref ( false )
const holdAction = ref < 'HOLD' | 'RELEASE' > ( 'HOLD' )
const holdReason = ref ( '' )
2026-09-07 12:21:44 +08:00
const holdEvidenceFile = ref < File | null >( null )
const holdEvidenceUploadKey = ref ( 0 )
const holdUploadedEvidenceFileId = ref ( '' )
const holdEvidenceError = ref < ApiErrorDetails | null >( null )
const holdDownloadingFileId = ref ( '' )
2026-08-12 09:25:55 +08:00
const relationOpen = ref ( false )
const relationOrder = ref < UnifiedOrderResource | null >( null )
2026-08-07 14:47:11 +08:00
const measurementDetail = ref < InboundMeasurementDetail | null >( null )
const measurementLoading = ref ( false )
2026-08-17 18:00:42 +08:00
const measurementError = ref < ApiErrorDetails | null >( null )
2026-08-07 14:47:11 +08:00
const measurementLoadedKey = ref ( '' )
2026-09-03 10:46:07 +08:00
const measurementChargeRules = ref < CustomerChargeRule [] >([])
const measurementChargeRulesLoading = ref ( false )
const measurementChargeRulesLoadedKey = ref ( '' )
const measurementDimensionConfig = ref < CustomerDimensionConfig | null >( null )
const measurementDimensionConfigLoading = ref ( false )
const measurementDimensionConfigLoadedKey = ref ( '' )
const measurementStrategySavingId = ref ( '' )
const measurementOuterSavingId = ref ( '' )
2026-08-17 18:00:42 +08:00
const measurementPreviewOpen = ref ( false )
const measurementPreviewLoading = ref ( false )
const measurementPreviewError = ref < ApiErrorDetails | null >( null )
const measurementPreviewUrl = ref ( '' )
const measurementPreviewTitle = ref ( '测量图片' )
2026-08-07 14:47:11 +08:00
const measurementCollapsed = reactive ({ customer : false , outer : false , actual : false })
2026-09-03 10:46:07 +08:00
type MeasurementSection = keyof typeof measurementCollapsed
type MeasurementCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
const measurementSectionModes = reactive < Record < MeasurementSection , MeasurementCalculationMode > > ({
customer : 'ACTUAL' , outer : 'ACTUAL' , actual : 'ACTUAL' ,
})
2026-09-09 20:36:49 +08:00
const measurementDraftEditableStatuses = new Set ([
'PREPARING' , 'WAIT_MEASURE' , 'MEASURING' , 'MEASURE_REJECTED' ,
])
const measurementDraftEditable = computed (() => (
can ( 'platform:firstleg:inbound:measure:edit' )
&& measurementDraftEditableStatuses . has ( measurementDetail . value ? . processStatus || '' )
))
2026-09-10 11:02:52 +08:00
const measurementStrategyEditable = computed (() => (
can ( 'platform:firstleg:inbound:measure:edit' )
&& Boolean ( measurementDetail . value ? . measurementId )
&& measurementDetail . value ? . version != null
))
const measurementStrategyLockedHint = computed (() => {
2026-09-09 20:36:49 +08:00
if ( ! can ( 'platform:firstleg:inbound:measure:edit' )) return '当前账号缺少测量编辑权限'
2026-09-10 11:02:52 +08:00
if ( ! measurementDetail . value ? . measurementId ) return '当前没有可修改的测量批次'
return '客户已确认且该票已装柜完成后,箱规计算规则将冻结'
2026-09-09 20:36:49 +08:00
})
2026-08-25 22:08:59 +08:00
const cargoImagePreviewUrls = new Set < string >()
2026-08-13 19:00:37 +08:00
const measurementDialogOpen = ref ( false )
2026-09-03 10:46:07 +08:00
const openWarehouseSuggestionOnMeasurementLoad = ref ( false )
const confirmFeeSending = ref ( false )
const feeConfirmOpen = ref ( false )
const feeConfirmWeight = ref < number | null >( null )
const feeConfirmVolume = ref < number | null >( null )
2026-09-09 17:58:23 +08:00
const feeConfirmReferenceLoading = ref ( false )
const feeConfirmReference = ref < InboundFeeSnapshot | null >( null )
const feeConfirmUnitPrice = ref < number | null >( null )
const feeConfirmPriceReason = ref ( '' )
2026-09-15 15:24:59 +08:00
type FeeConfirmAttempt = {
fingerprint : string
calculateKey : string
adjustKey : string
confirmKey : string
sendKey : string
}
let feeConfirmAttempt : FeeConfirmAttempt | null = null
2026-09-09 17:58:23 +08:00
const feeConfirmPriceChanged = computed (() => {
const reference = Number ( feeConfirmReference . value ? . unitPrice )
const current = Number ( feeConfirmUnitPrice . value )
return reference >= 0 && current >= 0 && Math . abs ( reference - current ) > 0.000001
})
const feeConfirmPriceUnit = computed (() => {
const snapshot = feeConfirmReference . value
return snapshot ? ` ${ snapshot . currency } / ${ snapshot . chargeUnit } ` : '—'
})
const feeConfirmEstimatedTotal = computed (() => {
const snapshot = feeConfirmReference . value
const unitPrice = Number ( feeConfirmUnitPrice . value )
if ( ! snapshot || ! ( unitPrice >= 0 )) return null
return Number ( snapshot . chargeableQuantity || 0 ) * unitPrice + Number ( snapshot . additionalAmount || 0 )
})
2026-09-03 10:46:07 +08:00
const feeConfirmRows = computed (() => {
const summary = detail . value ? . summary
2026-09-04 11:50:34 +08:00
const measuredGrossWeight = Number ( measurementDetail . value ? . actualGrossWeightKg )
const measuredNetWeight = Number ( measurementDetail . value ? . actualNetWeightKg )
2026-09-04 14:48:47 +08:00
const actualTotals = measurementTotals ( actualMeasurementRows . value )
const actualWeight = Number ( actualTotals . gross ) > 0
? actualTotals . gross
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
const actualVolume = Number ( actualTotals . volume ) > 0 ? actualTotals . volume : null
2026-09-03 10:46:07 +08:00
const customerTotals = measurementTotals ( customerMeasurementRows . value )
return [
2026-09-03 21:09:40 +08:00
{ source : '客户下单数据' , count : customerTotals . count , weight : customerTotals . gross , volume : customerTotals . volume ?? summary ? . expectedVolumeCbm },
2026-09-03 10:46:07 +08:00
{ source : '外箱规数据' , count : measurementTotals ( outerMeasurementRows . value ). count , weight : measurementTotals ( outerMeasurementRows . value ). gross , volume : measurementTotals ( outerMeasurementRows . value ). volume },
2026-09-04 14:48:47 +08:00
{ source : '实际测量数据' , count : actualTotals . count , weight : actualWeight , volume : actualVolume },
2026-09-03 10:46:07 +08:00
]
})
2026-08-14 19:02:34 +08:00
const smartLoadingOpen = ref ( false )
2026-08-13 19:00:37 +08:00
const measurementOrder = ref < UnifiedOrderResource | null >( null )
2026-08-31 10:48:23 +08:00
const loadingAssociation = ref < LoadingAssociationSummary | null >( null )
const overseasRelationship = ref < OverseasInboundCodeRelationship | null >( null )
const loadingAssociationError = ref < ApiErrorDetails | null >( null )
const overseasRelationshipError = ref < ApiErrorDetails | null >( null )
const subsequentFulfillmentLoading = ref ( false )
2026-09-01 21:09:39 +08:00
const resolvedDetailTarget = ref < { orderId : string ; tenantId : string } | null > ( null )
2026-08-07 14:47:11 +08:00
let detailLoadingKey = ''
2026-08-07 20:07:37 +08:00
let detailRequestSerial = 0
2026-09-01 21:09:39 +08:00
let detailAssociationRequestSerial = 0
2026-08-17 18:00:42 +08:00
let measurementRequestSerial = 0
let measurementPreviewRequestSerial = 0
2026-08-31 10:48:23 +08:00
let subsequentFulfillmentRequestSerial = 0
2026-09-07 11:20:43 +08:00
let holdRequestSerial = 0
2026-08-06 15:32:02 +08:00
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel >( emptyFilters ())
2026-08-11 18:06:35 +08:00
const appliedFilters = ref < FilterModel >( emptyFilters ())
const selectedStatus = ref < UnifiedOrderMainStatus | '' >( '' )
2026-09-01 21:09:39 +08:00
const businessInboundCode = computed (() => route . name === 'unified-order-detail-by-inbound-code' && typeof route . params . inboundCode === 'string'
? route . params . inboundCode . trim ()
: '' )
const orderId = computed (() => typeof route . params . orderId === 'string' ? route . params . orderId : resolvedDetailTarget . value ? . orderId || '' )
const detailTenantId = computed (() => typeof route . query . tenantId === 'string' ? route . query . tenantId : resolvedDetailTarget . value ? . tenantId || '' )
2026-08-07 20:07:37 +08:00
const detailRouteKey = computed (() => orderId . value ? ` ${ detailTenantId . value } : ${ orderId . value } ` : '' )
2026-09-01 21:09:39 +08:00
const isDetailRoute = computed (() => Boolean ( orderId . value || businessInboundCode . value ))
const requestedDetailTab = computed (() => route . query . tab === 'measurement' ? 'measurement' : 'basic' )
const fromMeasurementReview = computed (() => route . query . from === 'measurement-review' )
2026-08-18 16:09:43 +08:00
function normalizedIdentifier ( value : unknown ) {
const identifier = String ( value ?? '' ). trim ()
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
}
const normalizedInboundId = computed (() => normalizedIdentifier ( detail . value ? . summary . inboundId ))
2026-09-07 11:20:43 +08:00
const currentHoldFlag = computed (() => holdDetail . value ? holdDetail . value . holdFlag : detail . value ? . summary . holdFlag ?? null )
const currentHoldReason = computed (() => holdDetail . value ? holdDetail . value . currentHoldReason : detail . value ? . summary . currentHoldReason ?? null )
const canHoldInbound = computed (() => Boolean (
normalizedInboundId . value
&& can ( 'platform:firstleg:inbound:hold' )
&& holdDetail . value ? . canHold ,
))
const canReleaseInbound = computed (() => Boolean (
normalizedInboundId . value
&& can ( 'platform:firstleg:inbound:hold' )
&& holdDetail . value ? . canRelease ,
))
2026-08-06 13:56:04 +08:00
2026-09-14 17:52:00 +08:00
const isDropshipOrder = computed (() => detail . value ? . summary . fulfillmentType === 'ONE_CLICK_DROPSHIP' )
const detailTabs = computed (() => [
2026-09-14 14:44:01 +08:00
{ key : 'basic' , label : '基本信息' , icon : '📋' },
{ key : 'cargo' , label : '货品信息' , icon : '📦' },
{ key : 'fees' , label : '费用信息' , icon : '💰' },
{ key : 'history' , label : '状态历史' , icon : '📝' },
{ key : 'measurement' , label : '测量数据' , icon : '📐' },
2026-09-14 17:52:00 +08:00
...( isDropshipOrder . value ? [{ key : 'dropship-print' , label : '托盘打印' , icon : '🖨️' }] : []),
2026-09-14 14:44:01 +08:00
{ key : 'other' , label : '其他' , icon : '🗂️' },
{ key : 'fulfillment' , label : '拆分履约' , icon : '🧩' },
2026-09-14 17:52:00 +08:00
])
2026-08-06 19:03:13 +08:00
2026-08-07 14:47:11 +08:00
const customerMeasurementColumns = [
{ key : 'itemNo' , label : '#' }, { key : 'lengthCm' , label : '箱规长(cm)' }, { key : 'widthCm' , label : '箱规宽(cm)' },
{ key : 'heightCm' , label : '箱规高(cm)' }, { key : 'packageCount' , label : '箱数' }, { key : 'unitWeightKg' , label : '每箱毛重' },
{ key : 'unitVolumeCbm' , label : '每箱体积' }, { key : 'remark' , label : '备注' },
]
const measuredColumns = [
... customerMeasurementColumns . slice ( 0 , 7 ), { key : 'girthCm' , label : '箱规周长(cm)' }, { key : 'totalWeightKg' , label : '总毛重(T.K.GW)' },
2026-09-03 10:46:07 +08:00
{ key : 'totalVolumeCbm' , label : '总体积(T.CBM)' }, { key : 'volumeWeightKg' , label : '总材积' },
{ key : 'remark' , label : '备注' },
2026-08-07 14:47:11 +08:00
]
2026-09-03 10:46:07 +08:00
const outerColumns = measuredColumns
const actualMeasuredColumns = measuredColumns . filter (( column ) => column . key !== 'itemNo' )
function sortedDimensions ( row : Record < string , unknown >) {
const values = [ Number ( row . lengthCm ), Number ( row . widthCm ), Number ( row . heightCm )]
return values . every (( value ) => Number . isFinite ( value ) && value > 0 )
? values . sort (( left , right ) => right - left )
: null
}
function matchingDimensionRule ( row : Record < string , unknown >) : CustomerDimensionRule | null {
const measured = sortedDimensions ( row )
if ( ! measured ) return null
2026-09-03 15:05:30 +08:00
const rules = [...( measurementDimensionConfig . value ? . rules || [])]
2026-09-03 10:46:07 +08:00
. sort (( left , right ) => (
left . innerLengthCm * left . innerWidthCm * left . innerHeightCm
- right . innerLengthCm * right . innerWidthCm * right . innerHeightCm
))
2026-09-03 15:05:30 +08:00
if ( ! rules . length ) return null
return rules . find (( rule ) => {
2026-09-03 15:10:26 +08:00
const inner = [ rule . innerLengthCm , rule . innerWidthCm , rule . innerHeightCm ]. sort (( left , right ) => right - left )
return measured . every (( value , index ) => value <= inner [ index ])
}) || rules [ rules . length - 1 ]
2026-09-03 10:46:07 +08:00
}
function addCoefficientLabel ( row ?: Record < string , unknown >) {
const rows = row ? [ row ] : ( measurementDetail . value ? . items || []) as unknown as Record < string , unknown >[]
const values = [... new Set ( rows . map (( item ) => matchingDimensionRule ( item ) ? . edgeAdditionCm ). filter (( value ) => value != null ))]
2026-09-03 17:49:01 +08:00
if ( values . length === 1 ) return `原尺寸 + ${ Number ( values [ 0 ]) * 2 } cm(单边 + ${ values [ 0 ] } cm) `
2026-09-03 10:46:07 +08:00
if ( measurementDimensionConfigLoading . value ) return '原尺寸 + 加载中…'
return values . length > 1 ? '原尺寸 + 按箱规匹配' : '原尺寸 + 未匹配箱规'
}
function multiplyCoefficientLabel () {
const rule = measurementChargeRules . value . find (( item ) => item . defaultRule && item . status === 'ACTIVE' )
if ( rule ) return `原尺寸 × ${ rule . lengthFactor } `
return measurementChargeRulesLoading . value ? '原尺寸 × 加载中…' : '原尺寸 × 未启用系数'
}
function matchedSpecificationCoefficient ( rule : CustomerChargeRule , row : Record < string , unknown >) {
const tiers = [...( rule . remark || '' ). matchAll (
2026-09-03 17:49:01 +08:00
/(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\d+(?:\.\d+)?),(\d+(?:\.\d+)?),(\d+(?:\.\d+)?);系数:(\d+(?:\.\d+)?)(?:;重量:(\d+(?:\.\d+)?))?/g ,
2026-09-03 10:46:07 +08:00
)]. map (( match ) => ({
dimensions : [ Number ( match [ 2 ]), Number ( match [ 3 ]), Number ( match [ 4 ])]. sort (( left , right ) => right - left ),
coefficient : Number ( match [ 5 ]),
2026-09-03 17:49:01 +08:00
maxWeightKg : match [ 6 ] == null ? null : Number ( match [ 6 ]),
2026-09-03 10:46:07 +08:00
})). filter (( tier ) => tier . dimensions . every (( value ) => Number . isFinite ( value ) && value > 0 )
2026-09-03 17:49:01 +08:00
&& Number . isFinite ( tier . coefficient ) && tier . coefficient > 0
&& ( tier . maxWeightKg == null || ( Number . isFinite ( tier . maxWeightKg ) && tier . maxWeightKg > 0 )))
2026-09-03 10:46:07 +08:00
. sort (( left , right ) => left . dimensions . reduce (( total , value ) => total * value , 1 )
- right . dimensions . reduce (( total , value ) => total * value , 1 ))
if ( ! tiers . length ) return Number ( rule . lengthFactor || 1 )
const measured = [ Number ( row . lengthCm ), Number ( row . widthCm ), Number ( row . heightCm )]
. sort (( left , right ) => right - left )
2026-09-03 17:49:01 +08:00
const unitWeight = Number ( row . unitWeightKg )
const matched = tiers . find (( tier ) => measured . every (( value , index ) => value <= tier . dimensions [ index ])
&& ( tier . maxWeightKg == null || ! Number . isFinite ( unitWeight ) || unitWeight <= tier . maxWeightKg ))
2026-09-03 10:46:07 +08:00
return ( matched || tiers [ tiers . length - 1 ]). coefficient
}
2026-09-03 21:09:40 +08:00
// 系数模式可使用客户名下任一启用规则;默认规则仅用于优先选择,不作为唯一条件。
2026-09-03 10:46:07 +08:00
const activeMeasurementChargeRule = computed (() => measurementChargeRules . value
2026-09-03 21:09:40 +08:00
. find (( rule ) => rule . defaultRule && rule . status === 'ACTIVE' )
|| measurementChargeRules . value . find (( rule ) => rule . status === 'ACTIVE' )
|| null )
const boundOuterMeasurementChargeRule = computed (() => {
const boundId = outerMeasurementRows . value
. map (( row ) => String ( row . chargeRuleId || '' ))
. find (( id ) => id )
return boundId
? measurementChargeRules . value . find (( rule ) => rule . id === boundId && rule . status === 'ACTIVE' ) || null
: null
})
const activeMeasurementChargeRules = computed (() => measurementChargeRules . value . filter (( rule ) => rule . status === 'ACTIVE' ))
2026-09-03 10:46:07 +08:00
const multiplyModeDisabled = computed (() => ! measurementChargeRulesLoading . value && ! activeMeasurementChargeRule . value )
function roundedUp ( value : number , digits : number ) {
const factor = 10 ** digits
return Math . ceil (( value - Number . EPSILON ) * factor ) / factor
}
function calculatedMeasurementRow (
source : Record < string , unknown >,
mode : MeasurementCalculationMode ,
customerSource = false ,
2026-09-03 21:09:40 +08:00
useWarehouseOuterOverrides = true ,
) : Record < string , unknown > {
2026-09-03 15:05:30 +08:00
const outerOverrides = [ source . outerLengthCm , source . outerWidthCm , source . outerHeightCm ]
2026-09-03 21:09:40 +08:00
const hasOuterOverride = useWarehouseOuterOverrides && ! customerSource
2026-09-03 15:05:30 +08:00
&& outerOverrides . some (( value ) => value != null && value !== '' )
const rawDimensions = [ source . lengthCm , source . widthCm , source . heightCm ]. map (( value , index ) => {
const override = outerOverrides [ index ]
return Number ( hasOuterOverride && override != null && override !== '' ? override : value )
})
const calculationSource = {
... source ,
lengthCm : rawDimensions [ 0 ],
widthCm : rawDimensions [ 1 ],
heightCm : rawDimensions [ 2 ],
}
2026-09-03 10:46:07 +08:00
const rule = matchingDimensionRule ( calculationSource )
if ( ! rawDimensions . every (( value ) => Number . isFinite ( value ) && value > 0 )) return { ... source }
const matchedDimensions = mode === 'ADD' && rule
2026-09-03 17:49:01 +08:00
? rawDimensions . map (( value ) => value + rule . edgeAdditionCm * 2 )
2026-09-03 10:46:07 +08:00
: rawDimensions
2026-09-04 14:31:09 +08:00
const boundChargeRuleId = String ( source . chargeRuleId || '' )
const chargeRule = measurementChargeRules . value
. find (( item ) => item . id === boundChargeRuleId && item . status === 'ACTIVE' )
|| activeMeasurementChargeRule . value
2026-09-03 10:46:07 +08:00
const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
? matchedSpecificationCoefficient ( chargeRule , calculationSource )
: 1
2026-09-03 12:00:07 +08:00
const strategyDimensions = matchedDimensions . map (( value ) => roundedUp ( value * specificationCoefficient , 3 ))
2026-09-03 15:05:30 +08:00
const [ lengthCm , widthCm , heightCm ] = strategyDimensions
2026-09-03 10:46:07 +08:00
const packageCount = Number ( source . packageCount )
2026-09-03 12:00:07 +08:00
const unitVolumeCbm = mode === 'ADD' && rule && ! hasOuterOverride
2026-09-03 10:46:07 +08:00
? Math . round (( lengthCm * widthCm * heightCm / 1 _000_000 ) * 1000 ) / 1000
: roundedUp ( lengthCm * widthCm * heightCm / 1 _000_000 , 4 )
const totalVolumeCbm = Number . isFinite ( packageCount ) ? roundedUp ( unitVolumeCbm * packageCount , 4 ) : null
2026-09-03 17:49:01 +08:00
const fallbackMeasuredUnitWeight = ! customerSource && Number . isFinite ( Number ( measurementDetail . value ? . actualGrossWeightKg ))
&& Number ( measurementDetail . value ? . actualPackageCount ) > 0
? Number ( measurementDetail . value ? . actualGrossWeightKg ) / Number ( measurementDetail . value ? . actualPackageCount )
: null
2026-09-03 10:46:07 +08:00
const sourceUnitWeight = customerSource
? Number ( source . grossWeightKg ) / packageCount
2026-09-03 21:09:40 +08:00
: Number ( source . unitWeightKg ) > 0 ? Number ( source . unitWeightKg ) : fallbackMeasuredUnitWeight
2026-09-03 10:46:07 +08:00
const unitWeightKg = Number . isFinite ( sourceUnitWeight ) ? sourceUnitWeight : null
const totalWeightKg = unitWeightKg != null && Number . isFinite ( packageCount ) ? unitWeightKg * packageCount : null
return {
... source ,
lengthCm , widthCm , heightCm , unitWeightKg , unitVolumeCbm , totalWeightKg , totalVolumeCbm ,
girthCm : roundedUp ( lengthCm + 2 * ( widthCm + heightCm ), 3 ),
volumeWeightKg : totalVolumeCbm == null ? null : roundedUp ( totalVolumeCbm * 167 , 3 ),
remark : customerSource ? source . cargoRemark : source . remark ,
}
}
const customerMeasurementRows = computed (() => ( detail . value ? . order . cargoes || []). map (( row ) => (
calculatedMeasurementRow ( row , measurementSectionModes . customer , true )
)))
const outerMeasurementRows = computed (() => ( measurementDetail . value ? . items || []). map (( row ) => {
const source = row as unknown as Record < string , unknown >
2026-09-03 21:09:40 +08:00
const recalculated = calculatedMeasurementRow ( source , measurementSectionModes . outer )
2026-09-03 17:49:01 +08:00
const savedDimensions = [ row . calculatedOuterLengthCm , row . calculatedOuterWidthCm , row . calculatedOuterHeightCm ]
2026-09-03 21:09:40 +08:00
const recalculatedDimensions = [ recalculated . lengthCm , recalculated . widthCm , recalculated . heightCm ]
const warehouseOuterOverride = String ( row . calculationRuleVersion || '' ). startsWith ( 'WAREHOUSE_OUTER_OVERRIDE_' )
2026-09-03 17:49:01 +08:00
if ( savedDimensions . every (( value ) => Number . isFinite ( Number ( value )) && Number ( value ) > 0 )
2026-09-03 21:09:40 +08:00
&& ( warehouseOuterOverride
|| savedDimensions . every (( value , index ) => Math . abs ( Number ( value ) - Number ( recalculatedDimensions [ index ])) < 0.001 ))
2026-09-03 17:49:01 +08:00
&& measurementCalculationModeForRow ( source ) === measurementSectionModes . outer ) {
const packageCount = Number ( row . packageCount )
2026-09-03 21:09:40 +08:00
const unitWeightKg = Number ( row . unitWeightKg ) > 0
? Number ( row . unitWeightKg )
: Number . isFinite ( Number ( measurementDetail . value ? . actualGrossWeightKg ))
&& Number ( measurementDetail . value ? . actualPackageCount ) > 0
? Number ( measurementDetail . value ? . actualGrossWeightKg ) / Number ( measurementDetail . value ? . actualPackageCount )
: null
2026-09-03 17:49:01 +08:00
return {
... source ,
lengthCm : Number ( savedDimensions [ 0 ]),
widthCm : Number ( savedDimensions [ 1 ]),
heightCm : Number ( savedDimensions [ 2 ]),
unitWeightKg ,
totalWeightKg : unitWeightKg != null && Number . isFinite ( packageCount ) ? unitWeightKg * packageCount : null ,
}
}
2026-09-03 21:09:40 +08:00
return recalculated
2026-09-03 10:46:07 +08:00
}))
const actualMeasurementRows = computed (() => ( measurementDetail . value ? . items || []). map (( row ) => (
2026-09-03 21:09:40 +08:00
calculatedMeasurementRow ( row as unknown as Record < string , unknown >, measurementSectionModes . actual , false , false )
2026-09-03 10:46:07 +08:00
)))
2026-09-03 21:09:40 +08:00
const hasActualMeasurementData = computed (() => ( measurementDetail . value ? . items ? . length ?? 0 ) > 0 )
2026-09-03 10:46:07 +08:00
function updateMeasurementSectionMode ( section : MeasurementSection , mode : MeasurementCalculationMode ) {
if ( mode === 'MULTIPLY' && ! activeMeasurementChargeRule . value ) {
ElMessage . warning ( '该客户尚未启用系数配置,请先到客户计费规则中设置默认规则' )
return
}
measurementSectionModes [ section ] = mode
}
2026-08-07 14:47:11 +08:00
2026-09-03 21:09:40 +08:00
async function selectAndSaveOuterMeasurementMode ( mode : MeasurementCalculationMode , ruleId ?: string ) {
2026-09-03 17:49:01 +08:00
if ( measurementStrategySavingId . value ) return
2026-09-10 11:02:52 +08:00
if ( ! measurementStrategyEditable . value ) {
ElMessage . warning ( measurementStrategyLockedHint . value )
2026-09-09 20:36:49 +08:00
return
}
2026-09-03 17:49:01 +08:00
const previousMode = measurementSectionModes . outer
if ( mode === 'MULTIPLY' && ! activeMeasurementChargeRule . value ) {
ElMessage . warning ( '该客户尚未启用系数配置,请先到客户计费规则中设置默认规则' )
return
}
measurementSectionModes . outer = mode
2026-09-03 21:09:40 +08:00
const saved = await updateMeasurementCalculationMode ( null , mode , ruleId )
2026-09-03 17:49:01 +08:00
if ( ! saved ) measurementSectionModes . outer = previousMode
}
2026-09-04 14:36:11 +08:00
async function waitForSavedMeasurementVersion ( tenantId : string , inboundId : string , savedVersion : number ) {
for ( let attempt = 0 ; attempt < 6 ; attempt += 1 ) {
try {
const refreshed = await getInboundMeasurement ( tenantId , inboundId )
if ( Number ( refreshed . version ) >= savedVersion ) return refreshed
} catch {
// 保存结果已由 PUT 确认,短暂的详情读取失败只影响同步等待。
}
await new Promise (( resolve ) => setTimeout ( resolve , 250 ))
2026-09-03 21:09:40 +08:00
}
2026-09-04 14:36:11 +08:00
return null
2026-09-03 21:09:40 +08:00
}
2026-08-06 15:32:02 +08:00
const statusTabs = computed (() => [
{ value : '' as const , label : '全部' },
... unifiedOrderStatusOptions ,
])
const warehouseOptions = computed (() => warehouses . value . map (( warehouse ) => ({
value : String ( warehouse . id ),
label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) `,
})))
const receivingWarehouseOptions = computed (() => warehouses . value
. filter (( warehouse ) => warehouse . warehouseType === 'DOMESTIC' )
. map (( warehouse ) => ({ value : String ( warehouse . id ), label : ` ${ warehouse . warehouseName } ( ${ warehouse . warehouseCode } ) ` })))
const primaryQueryFields = computed < QueryField [] >(() => [
{ key : 'receivingWarehouseId' , type : 'select' , width : 220 , label : '定位仓库' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择定位仓库' , options : receivingWarehouseOptions . value , props : { 'aria-label' : '定位仓库' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } },
{ key : 'destinationWarehouseId' , type : 'select' , width : 220 , label : '交货仓' , placeholder : warehouseLoadError . value ? '仓库加载失败' : '请选择交货仓' , options : warehouseOptions . value , props : { 'aria-label' : '交货仓' , filterable : true , loading : warehouseLoading . value , disabled : Boolean ( warehouseLoadError . value ) } },
{ key : 'inboundCode' , type : 'input' , width : 220 , label : '入仓码' , placeholder : '请输入入仓码' , props : { 'aria-label' : '入仓码' } },
2026-08-07 18:44:55 +08:00
])
const advancedQueryFields = computed < QueryField [] >(() => [
2026-08-06 15:32:02 +08:00
{ key : 'expectedArrivalDate' , type : 'date' , width : 190 , label : '预计到达时间' , placeholder : '请选择预计到达日期' , props : { 'aria-label' : '预计到达时间' } },
{ key : 'feeReviewStatus' , type : 'select' , width : 190 , label : '费用审核状态' , placeholder : '请选择费用审核状态' , options : [
2026-08-06 15:36:28 +08:00
{ value : 'UNRECORDED' , label : '未录入' }, { value : 'PENDING' , label : '待审核' },
{ value : 'APPROVED' , label : '审核通过' }, { value : 'REJECTED' , label : '审核驳回' },
2026-08-06 15:32:02 +08:00
], props : { 'aria-label' : '费用审核状态' } },
2026-08-07 18:44:55 +08:00
{
key : 'tenantId' ,
type : 'select' ,
width : 220 ,
label : '公司' ,
placeholder : customerLoadError . value ? '客户列表加载失败' : customerLoading . value ? '正在加载客户' : '请选择公司' ,
options : customers . value . map (( customer ) => ({
value : String ( customer . id ),
2026-08-14 12:50:42 +08:00
label : customer . legalName ? . trim () || customer . tenantName ,
2026-08-07 18:44:55 +08:00
})),
props : { 'aria-label' : '公司' , filterable : true , loading : customerLoading . value , disabled : Boolean ( customerLoadError . value ) },
},
2026-08-12 17:29:35 +08:00
{ key : 'containerNo' , type : 'input' , width : 210 , label : '真实集装箱号' , placeholder : '请输入真实集装箱号' , props : { 'aria-label' : '真实集装箱号' } },
{ key : 'cabinetNo' , type : 'input' , width : 190 , label : '内部柜次号' , placeholder : '请输入内部柜次号' , props : { 'aria-label' : '内部柜次号' } },
2026-08-06 15:32:02 +08:00
{ key : 'channelName' , type : 'input' , width : 210 , label : '渠道' , placeholder : '请输入渠道名称' , props : { 'aria-label' : '渠道' } },
{ key : 'sailingDate' , type : 'date' , width : 190 , label : '船期' , placeholder : '请选择船期' , props : { 'aria-label' : '船期' } },
{ key : 'loaded' , type : 'select' , width : 180 , label : '是否已配载' , placeholder : '请选择配载状态' , options : [{ value : 'true' , label : '已配载' }, { value : 'false' , label : '未配载' }], props : { 'aria-label' : '是否已配载' } },
{ key : 'fullContainer' , type : 'select' , width : 180 , label : '是否整柜' , placeholder : '请选择装柜方式' , options : [{ value : 'true' , label : '整柜' }, { value : 'false' , label : '拼箱' }], props : { 'aria-label' : '是否整柜' } },
{ key : 'measurementReviewStatus' , type : 'select' , width : 220 , label : '测量数据审核状态' , placeholder : '请选择测量审核状态' , options : [
2026-08-06 15:36:28 +08:00
{ value : 'UNRECORDED' , label : '未录入' }, { value : 'PENDING' , label : '待审核' },
{ value : 'APPROVED' , label : '审核通过' }, { value : 'REJECTED' , label : '审核驳回' },
2026-08-06 15:32:02 +08:00
], props : { 'aria-label' : '测量数据审核状态' } },
2026-08-12 12:19:02 +08:00
{ key : 'fbaCode' , type : 'input' , width : 220 , label : '商品 SKU 编码' , placeholder : '请输入商品 SKU 编码' , props : { 'aria-label' : '商品 SKU 编码' } },
2026-08-07 18:44:55 +08:00
{ key : 'createdRange' , type : 'daterange' , width : 300 , label : '创建日期' , placeholder : '创建日期范围' , props : { 'aria-label' : '创建日期范围' } },
2026-08-06 15:32:02 +08:00
])
2026-08-06 19:03:13 +08:00
const allQueryFields = computed (() => [... primaryQueryFields . value , ... advancedQueryFields . value ])
2026-08-06 15:32:02 +08:00
const queryFields = computed (() => showAdvancedFilters . value
2026-08-06 19:03:13 +08:00
? allQueryFields . value
2026-08-06 15:32:02 +08:00
: primaryQueryFields . value )
2026-08-17 18:00:42 +08:00
const columns = computed < DataTableColumn [] >(() => [
2026-09-11 16:17:21 +08:00
// 业务表格保持操作列第一;批量选择列仅在有对应权限时紧随其后。
2026-09-14 12:00:54 +08:00
{ key : 'actions' , label : '操作' , width : 260 , fixed : 'left' , slotName : 'actions' },
2026-09-10 11:17:45 +08:00
...( can ( 'platform:order:approve' ) || can ( 'platform:firstleg:smart-loading:assign' ) || can ( 'platform:order:export' ) || can ( 'platform:firstleg:inbound:view' ) ? [{
2026-08-17 18:00:42 +08:00
key : 'selection' , type : 'selection' , width : 48 , fixed : 'left' ,
2026-09-01 21:09:39 +08:00
selectable : () => ! batchApprovalSaving . value && ! coLoadSaving . value && ! exportSubmitting . value ,
2026-08-17 18:00:42 +08:00
} satisfies DataTableColumn ] : []),
2026-09-03 15:00:58 +08:00
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 160 , slotName : 'inboundCode' },
{ key : 'mainStatus' , prop : 'mainStatus' , label : '订单主状态' , minWidth : 120 , slotName : 'mainStatus' },
{ key : 'customerName' , prop : 'customerName' , label : '客户' , minWidth : 120 , slotName : 'field' },
{ key : 'receivingWarehouseName' , prop : 'receivingWarehouseName' , label : '平台收货仓' , minWidth : 100 , slotName : 'field' },
{ key : 'destinationWarehouseName' , prop : 'destinationWarehouseName' , label : '目的仓' , minWidth : 100 , slotName : 'field' },
{ key : 'channelName' , prop : 'channelName' , label : '渠道' , minWidth : 140 , slotName : 'channel' },
{ key : 'totalPackageCount' , prop : 'totalPackageCount' , label : '总箱数' , minWidth : 80 , slotName : 'field' },
{ key : 'expectedArrivalAt' , prop : 'expectedArrivalAt' , label : '预计到货时间' , minWidth : 170 , slotName : 'expectedArrival' },
{ key : 'estimatedVolumeCbm' , prop : 'estimatedVolumeCbm' , label : '预估体积(m³)' , minWidth : 120 , slotName : 'estimatedVolume' },
2026-09-03 11:47:32 +08:00
{ key : 'expectedVolumeCbm' , prop : 'expectedVolumeCbm' , label : '下单体积(m³)' , minWidth : 130 , slotName : 'field' },
{ key : 'actualVolumeCbm' , prop : 'actualVolumeCbm' , label : '实测体积(m³)' , minWidth : 130 , slotName : 'field' },
2026-09-03 15:00:58 +08:00
{ key : 'totalPalletCount' , prop : 'totalPalletCount' , label : '总托数' , minWidth : 90 , slotName : 'field' },
{ key : 'requestedPickupAt' , prop : 'requestedPickupAt' , label : '预约提货时间' , minWidth : 140 , slotName : 'field' },
2026-08-17 19:13:25 +08:00
{ key : 'locationCodes' , prop : 'locationCodes' , label : '库位位置' , minWidth : 160 , slotName : 'field' , defaultVisible : false },
2026-09-03 15:00:58 +08:00
{ key : 'measurementReviewStatus' , prop : 'measurementReviewStatus' , label : '测量数据审核状态' , minWidth : 150 , slotName : 'measurementReviewStatus' },
{ key : 'actualGrossWeightKg' , prop : 'actualGrossWeightKg' , label : '实测毛重(kg)' , minWidth : 120 , slotName : 'field' },
{ key : 'totalNetWeightKg' , prop : 'totalNetWeightKg' , label : '货物净重(kg)' , minWidth : 120 , slotName : 'field' },
2026-08-06 19:03:13 +08:00
{ key : 'chargeableWeightKg' , prop : 'chargeableWeightKg' , label : '计费毛重' , minWidth : 120 , slotName : 'field' },
2026-08-17 19:52:04 +08:00
{ key : 'chargeableVolumeCbm' , prop : 'chargeableVolumeCbm' , label : '计费体积(m³)' , minWidth : 130 , slotName : 'field' },
2026-08-12 10:59:49 +08:00
{ key : 'expectedShipDate' , prop : 'expectedShipDate' , label : '预计出运日期' , minWidth : 150 , slotName : 'field' },
2026-08-17 19:52:04 +08:00
{ key : 'feeReviewStatus' , prop : 'feeReviewStatus' , label : '费用审核状态' , minWidth : 140 , slotName : 'field' , defaultVisible : false },
{ key : 'skuNos' , prop : 'skuNos' , label : 'SKU 编码' , minWidth : 180 , slotName : 'field' , defaultVisible : false },
{ key : 'totalQuantity' , prop : 'totalQuantity' , label : '货物总数量' , minWidth : 120 , slotName : 'field' , defaultVisible : false },
{ key : 'expressCompany' , prop : 'expressCompany' , label : '快递公司' , minWidth : 140 , slotName : 'field' , defaultVisible : false },
{ key : 'expressTrackingNo' , prop : 'expressTrackingNo' , label : '快递单号' , minWidth : 170 , slotName : 'field' , defaultVisible : false },
{ key : 'shipmentId' , prop : 'shipmentId' , label : 'Shipment ID' , minWidth : 160 , slotName : 'field' , defaultVisible : false },
{ key : 'referenceId' , prop : 'referenceId' , label : 'Reference ID' , minWidth : 160 , slotName : 'field' , defaultVisible : false },
2026-08-12 17:29:35 +08:00
{ key : 'containerNo' , prop : 'containerNo' , label : '真实集装箱号' , minWidth : 160 , slotName : 'field' , defaultVisible : false },
{ key : 'cabinetNo' , prop : 'cabinetNo' , label : '内部柜次号' , minWidth : 150 , slotName : 'field' , defaultVisible : false },
2026-08-18 11:33:01 +08:00
{ key : 'effectiveSailingAt' , prop : 'effectiveSailingAt' , label : '开船时间' , minWidth : 180 , slotName : 'field' },
{ key : 'effectivePortArrivalAt' , prop : 'effectivePortArrivalAt' , label : '到港时间' , minWidth : 180 , slotName : 'field' },
2026-08-12 10:59:49 +08:00
{ key : 'customerRemark' , prop : 'customerRemark' , label : '客户备注' , minWidth : 180 , slotName : 'field' , defaultVisible : false },
{ key : 'latestInternalNote' , prop : 'latestInternalNote' , label : '内部备注' , minWidth : 180 , slotName : 'field' , defaultVisible : false },
{ key : 'createdAt' , prop : 'createdAt' , label : '创建时间' , minWidth : 150 , slotName : 'field' , defaultVisible : false },
{ key : 'updatedAt' , prop : 'updatedAt' , label : '更新时间' , minWidth : 150 , slotName : 'field' },
2026-08-17 18:00:42 +08:00
])
2026-08-06 13:56:04 +08:00
2026-08-06 15:32:02 +08:00
function emptyFilters () : FilterModel {
return {
2026-08-11 18:06:35 +08:00
tenantId : '' , receivingWarehouseId : '' , destinationWarehouseId : '' ,
2026-08-10 17:42:17 +08:00
inboundCode : '' , expectedArrivalDate : '' , feeReviewStatus : '' , channelName : '' ,
2026-08-06 15:32:02 +08:00
containerNo : '' , cabinetNo : '' , sailingDate : '' , loaded : '' , fullContainer : '' , measurementReviewStatus : '' ,
fbaCode : '' , createdRange : [],
}
}
2026-08-11 18:06:35 +08:00
function cloneFilters ( source : FilterModel ) : FilterModel {
return { ... source , createdRange : [... source . createdRange ] }
}
2026-08-06 15:32:02 +08:00
function trimmed ( value : string ) {
return value . trim () || undefined
}
2026-08-25 22:08:59 +08:00
function optionalBoolean ( value : string | null | undefined ) {
if ( value === 'true' ) return true
if ( value === 'false' ) return false
return undefined
2026-08-06 15:32:02 +08:00
}
function selectStatus ( status : UnifiedOrderMainStatus | '' ) {
2026-08-11 18:06:35 +08:00
if ( selectedStatus . value === status ) return
selectedStatus . value = status
if ( page . value === 1 ) void loadOrders ()
else page . value = 1
2026-08-06 15:32:02 +08:00
}
function handleTabKeydown ( event : KeyboardEvent , index : number ) {
let nextIndex = index
if ( event . key === 'ArrowRight' ) nextIndex = ( index + 1 ) % statusTabs . value . length
else if ( event . key === 'ArrowLeft' ) nextIndex = ( index - 1 + statusTabs . value . length ) % statusTabs . value . length
else if ( event . key === 'Home' ) nextIndex = 0
else if ( event . key === 'End' ) nextIndex = statusTabs . value . length - 1
else return
event . preventDefault ()
const tab = statusTabs . value [ nextIndex ]
if ( ! tab ) return
selectStatus ( tab . value )
const buttons = ( event . currentTarget as HTMLElement ). parentElement ? . querySelectorAll < HTMLElement >( '[role="tab"]' )
buttons ? .[ nextIndex ] ? . focus ()
}
2026-08-06 13:56:04 +08:00
function can ( permission : string ) {
return access . can ( permission )
}
2026-09-03 11:47:32 +08:00
function planningDraft ( row : UnifiedOrderResource ) {
const key = String ( row . orderId )
let draft = planningDrafts [ key ]
if ( ! draft ) {
draft = {
expectedArrivalAt : row . expectedArrivalAt || '' ,
estimatedVolumeCbm : row . estimatedVolumeCbm ?? null ,
savedExpectedArrivalAt : row . expectedArrivalAt || '' ,
savedEstimatedVolumeCbm : row . estimatedVolumeCbm ?? null ,
saving : false ,
}
planningDrafts [ key ] = draft
}
return draft
}
async function savePlanning ( row : UnifiedOrderResource ) {
const draft = planningDraft ( row )
2026-09-15 17:46:44 +08:00
if ( ! canEditPlanning ( row ) || draft . saving ) return
2026-09-03 11:47:32 +08:00
const estimatedVolumeCbm = draft . estimatedVolumeCbm ?? null
draft . estimatedVolumeCbm = estimatedVolumeCbm
if ( ! Number . isInteger ( row . orderVersion )) {
2026-09-04 18:47:43 +08:00
ElMessage . error ( '订单版本缺失,请刷新列表后重试' )
2026-09-03 11:47:32 +08:00
return
}
if ( estimatedVolumeCbm !== null && ( ! Number . isFinite ( estimatedVolumeCbm ) || estimatedVolumeCbm <= 0 )) {
2026-09-04 18:47:43 +08:00
ElMessage . warning ( '预估体积必须大于 0,或清空该值' )
2026-09-03 11:47:32 +08:00
return
}
if ( draft . expectedArrivalAt === draft . savedExpectedArrivalAt
&& estimatedVolumeCbm === draft . savedEstimatedVolumeCbm ) return
draft . saving = true
try {
const result = await updatePlatformUnifiedOrderPlanning ( String ( row . tenantId ), String ( row . orderId ), {
expectedVersion : row . orderVersion ! ,
expectedArrivalAt : draft . expectedArrivalAt || null ,
estimatedVolumeCbm ,
})
row . orderVersion = result . version
row . expectedArrivalAt = draft . expectedArrivalAt || null
row . estimatedVolumeCbm = estimatedVolumeCbm
draft . savedExpectedArrivalAt = draft . expectedArrivalAt
draft . savedEstimatedVolumeCbm = estimatedVolumeCbm
ElMessage . success ( '订单运营预估已更新' )
} catch ( error ) {
2026-09-04 18:47:43 +08:00
const details = getApiErrorDetails ( error , '订单运营预估保存失败' )
const evidence = [
details . code ? `错误码 ${ details . code } ` : '' ,
details . requestId ? `Request ID ${ details . requestId } ` : '' ,
details . traceId ? `Trace ID ${ details . traceId } ` : '' ,
]. filter ( Boolean ). join ( '; ' )
ElMessage . error ({
message : evidence ? ` ${ details . message } ( ${ evidence } ) ` : details . message ,
duration : 6 _000 ,
showClose : true ,
})
2026-09-03 11:47:32 +08:00
} finally {
draft . saving = false
}
}
2026-09-15 17:46:44 +08:00
const planningEditableOrderStatuses = new Set ([ 'DRAFT' , 'REJECTED' , 'SUBMITTED' , 'APPROVED' ])
function canEditPlanning ( row : UnifiedOrderResource ) {
return can ( 'platform:order:declaration:edit' )
&& planningEditableOrderStatuses . has ( row . internalOrderStatus || '' )
}
2026-09-03 11:54:20 +08:00
const declarationEditableStatuses = new Set < UnifiedOrderMainStatus >([ 'PENDING_REVIEW' , 'WAIT_MEASURE' , 'WAIT_SPLIT' ])
2026-09-02 10:24:07 +08:00
const canEditDeclaration = computed (() => Boolean (
detail . value
2026-09-03 11:54:20 +08:00
&& declarationEditableStatuses . has ( detail . value . summary . mainStatus )
2026-09-02 10:24:07 +08:00
&& detail . value . order . order ? . version !== null
&& detail . value . order . order ? . version !== undefined
&& can ( 'platform:order:declaration:edit' ),
))
async function handleDeclarationUpdated () {
activeDetailTab . value = 'basic'
await loadDetail ( true )
}
2026-08-06 13:56:04 +08:00
function customerName ( row : UnifiedOrderResource ) {
2026-08-30 17:43:03 +08:00
return row . customerName ? . trim () || customers . value . find (( item ) => String ( item . id ) === String ( row . tenantId )) ? . tenantName || '客户名称待补充'
2026-08-06 13:56:04 +08:00
}
2026-08-12 09:25:55 +08:00
function orderRelationLabel ( row : UnifiedOrderResource ) {
if ( row . relation ? . role === 'PARENT' ) return `父单 · ${ row . relation . childCount } 个子单`
if ( row . relation ? . role === 'CHILD' && row . relation . splitSequence !== null ) {
return `子单 · 第 ${ row . relation . splitSequence } 次`
}
return '子单'
}
function openOrderRelation ( row : UnifiedOrderResource ) {
relationOrder . value = row
relationOpen . value = true
}
2026-08-06 13:56:04 +08:00
function formatDate ( value : string | null | undefined ) {
return value ? value . replace ( 'T' , ' ' ). slice ( 0 , 16 ) : '—'
}
function formatNumber ( value : number | null | undefined , digits = 2 ) {
return value === null || value === undefined ? '—' : value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits })
}
2026-08-07 14:47:11 +08:00
function partyValue ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER' , key : string ) {
2026-08-07 10:45:19 +08:00
const party = parties . find (( item ) => String ( item . partyType || '' ). toUpperCase () === type )
const value = party ? .[ key ]
return value === null || value === undefined || value === '' ? '—' : String ( value )
}
2026-08-07 14:47:11 +08:00
function partyAddress ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER' ) {
2026-08-07 10:45:19 +08:00
const party = parties . find (( item ) => String ( item . partyType || '' ). toUpperCase () === type )
if ( ! party ) return '—'
return [ 'province' , 'city' , 'district' , 'addressLine' ]. map (( key ) => party [ key ]). filter (( value ) => typeof value === 'string' && value . trim ()). join ( ' ' ) || '—'
}
function cargoTotal ( cargoes : Array < Record < string , unknown > > , key : string ) {
const total = cargoes . reduce (( sum , cargo ) => {
const value = Number ( cargo [ key ])
return Number . isFinite ( value ) ? sum + value : sum
}, 0 )
return total > 0 ? total : null
}
function displayBoolean ( value : boolean | null | undefined ) {
return value === null || value === undefined ? '—' : value ? '是' : '否'
}
2026-08-18 11:33:01 +08:00
function formatEffectiveTransportTime ( value : string | null | undefined , estimated : boolean | null | undefined ) {
if ( ! value ) return '—'
return ` ${ estimated === true ? '预计 ' : '' }${ formatDate ( value ) } `
}
2026-08-06 19:03:13 +08:00
function displayField ( row : UnifiedOrderResource , property : string | undefined ) {
if ( ! property ) return '—'
const source = row as unknown as Record < string , unknown >
const nested : Record < string , string > = {
measurementReviewStatus : 'measurement.reviewStatus' , expectedArrivalAt : 'expectedArrivalAt' ,
2026-08-17 19:52:04 +08:00
containerNo : 'transport.containerNo' , cabinetNo : 'transport.cabinetNo' ,
2026-08-18 11:33:01 +08:00
effectiveSailingAt : 'transport.effectiveSailingAt' , effectivePortArrivalAt : 'transport.effectivePortArrivalAt' ,
2026-08-06 19:03:13 +08:00
chargeableWeightKg : 'measurement.chargeableWeightKg' , measurementCompletedAt : 'measurement.completedAt' ,
expressTrackingNo : 'delivery.trackingNo' ,
}
const path = nested [ property ] || property
let value = path . split ( '.' ). reduce < unknown >(( current , key ) => (
current && typeof current === 'object' ? ( current as Record < string , unknown >)[ key ] : undefined
), source )
if (( value === null || value === undefined || value === '' ) && property === 'internalOrderStatus' ) value = source . mainStatus
2026-08-17 19:52:04 +08:00
if ( property === 'requestedPickupAt' && ( row . pickupMethod !== 'PICKUP' || ! value )) return '—'
2026-08-18 11:33:01 +08:00
if ( property === 'effectiveSailingAt' ) return formatEffectiveTransportTime ( row . transport ? . effectiveSailingAt , row . transport ? . effectiveSailingAtEstimated )
if ( property === 'effectivePortArrivalAt' ) return formatEffectiveTransportTime ( row . transport ? . effectivePortArrivalAt , row . transport ? . effectivePortArrivalAtEstimated )
2026-08-06 19:03:13 +08:00
if ( value === null || value === undefined || value === '' ) return '—'
if ( typeof value === 'boolean' ) return value ? '是' : '否'
2026-08-17 19:52:04 +08:00
if ( Array . isArray ( value )) return value . length ? ( property === 'skuNos' ? value . join ( '、' ) : ` ${ value . length } 个文件` ) : '—'
2026-08-22 10:02:13 +08:00
if ( typeof value === 'number' ) {
if ([ 'actualVolumeCbm' , 'chargeableVolumeCbm' ]. includes ( property )) return formatNumber ( value , 3 )
return formatNumber ( value )
}
2026-08-06 19:03:13 +08:00
if ( property . endsWith ( 'At' ) || property . endsWith ( 'Date' )) return formatDate ( String ( value ))
if ( property === 'measurementReviewStatus' ) return measurementStatusLabel ( String ( value ))
2026-08-07 09:06:22 +08:00
if ( property === 'internalOrderStatus' ) return orderStatusLabel ( String ( value ))
2026-08-11 19:35:28 +08:00
if ( property === 'feeReviewStatus' ) return feeReviewLabel ( String ( value ))
if ( property === 'businessType' ) return firstLegBusinessTypeLabel ( String ( value ))
if ( property === 'pickupMethod' ) return firstLegPickupMethodLabel ( String ( value ))
if ( property === 'taxMode' ) return firstLegTaxModeLabel ( String ( value ))
if ( property === 'destinationType' ) return firstLegDestinationTypeLabel ( String ( value ))
2026-08-06 19:03:13 +08:00
return String ( value )
}
2026-08-06 13:56:04 +08:00
function statusLabel ( value : UnifiedOrderMainStatus | string | null ) {
2026-08-11 19:35:28 +08:00
return unifiedOrderStatusLabel ( value , '状态更新' )
2026-08-07 10:45:19 +08:00
}
function historyActionLabel ( value : string | null | undefined ) {
2026-08-11 19:35:28 +08:00
return unifiedOrderHistoryActionLabel ( value )
2026-08-06 13:56:04 +08:00
}
2026-08-06 19:03:13 +08:00
function measurementStatusLabel ( value : string | null | undefined ) {
2026-08-11 19:35:28 +08:00
return unifiedOrderReviewStatusLabel ( value , '未录入' )
2026-08-07 09:06:22 +08:00
}
function orderStatusLabel ( value : string | null | undefined ) {
2026-08-11 19:35:28 +08:00
return unifiedOrderStatusLabel ( value )
2026-08-06 19:03:13 +08:00
}
2026-08-07 14:47:11 +08:00
function cargoValue ( cargo : Record < string , unknown >, key : string ) {
const value = cargo [ key ]
if ( key === 'circumferenceCm' ) {
2026-08-27 21:48:15 +08:00
const length = Number ( cargo . lengthCm )
2026-08-07 14:47:11 +08:00
const width = Number ( cargo . widthCm )
const height = Number ( cargo . heightCm )
2026-08-27 21:48:15 +08:00
return Number . isFinite ( length ) && Number . isFinite ( width ) && Number . isFinite ( height )
? formatNumber ( length + 2 * ( width + height ))
: '—'
2026-08-07 14:47:11 +08:00
}
if ( value === null || value === undefined || value === '' ) return '—'
if ( typeof value === 'number' ) return formatNumber ( value )
return String ( value )
}
function cargoImage ( cargo : Record < string , unknown >) {
const value = cargo . imageUrl || cargo . cargoImageUrl || cargo . image
return typeof value === 'string' && value ? value : ''
}
2026-08-25 22:08:59 +08:00
function releaseCargoImagePreviews () {
cargoImagePreviewUrls . forEach (( url ) => URL . revokeObjectURL ( url ))
cargoImagePreviewUrls . clear ()
}
async function loadCargoImagePreviews ( resource : UnifiedOrderDetailResource , serial : number ) {
const tenantId = String ( resource . summary . tenantId || '' )
if ( ! tenantId ) return
2026-09-07 11:39:55 +08:00
const cargoesByFileId = new Map < string , typeof resource.order.cargoes >()
for ( const cargo of resource . order ? . cargoes ?? []) {
const fileId = cargo . productImageFileId === null || cargo . productImageFileId === undefined
? ''
: String ( cargo . productImageFileId )
if ( ! fileId ) continue
const cargoes = cargoesByFileId . get ( fileId ) ?? []
cargoes . push ( cargo )
cargoesByFileId . set ( fileId , cargoes )
}
await Promise . all ([... cargoesByFileId . entries ()]. map ( async ([ fileId , cargoes ]) => {
2026-08-25 22:08:59 +08:00
try {
2026-09-07 11:39:55 +08:00
const blob = await downloadPlatformProductImage ( tenantId , fileId )
2026-08-25 22:08:59 +08:00
if ( serial !== detailRequestSerial || detailRouteKey . value !== ` ${ tenantId } : ${ orderId . value } ` ) return
const url = URL . createObjectURL ( blob )
2026-09-07 11:39:55 +08:00
cargoes . forEach (( cargo ) => { cargo . imageUrl = url })
2026-08-25 22:08:59 +08:00
cargoImagePreviewUrls . add ( url )
} catch {
// Historical files may have been removed; keep the remaining cargo fields visible.
}
}))
}
2026-08-07 14:47:11 +08:00
function feeReviewLabel ( value : string | null | undefined ) {
2026-08-11 19:35:28 +08:00
return unifiedOrderReviewStatusLabel ( value )
2026-08-07 14:47:11 +08:00
}
2026-09-03 10:46:07 +08:00
function measurementValue ( row : Record < string , unknown >, key : string , customerSource = false , outerSource = false ) {
if ( key === 'coefficientMode' ) return measurementCalculationLabel ( row )
if ( outerSource && [ 'lengthCm' , 'widthCm' , 'heightCm' ]. includes ( key )) {
const outerValue = row [ `outer ${ key [ 0 ]. toUpperCase () }${ key . slice ( 1 ) } ` ]
return outerValue == null ? '—' : formatNumber ( Number ( outerValue ), 6 )
}
2026-08-07 14:47:11 +08:00
if ( customerSource && key === 'unitWeightKg' ) {
const total = Number ( row . grossWeightKg )
const count = Number ( row . packageCount )
return Number . isFinite ( total ) && Number . isFinite ( count ) && count > 0 ? formatNumber ( total / count , 4 ) : '—'
}
2026-09-03 17:49:01 +08:00
if ( key === 'unitWeightKg' ) {
const value = Number ( row [ key ])
return Number . isFinite ( value ) ? formatNumber ( value , 4 ) : '—'
}
2026-08-07 14:47:11 +08:00
if ( customerSource && key === 'unitVolumeCbm' ) {
const total = Number ( row . volumeCbm )
const count = Number ( row . packageCount )
return Number . isFinite ( total ) && Number . isFinite ( count ) && count > 0 ? formatNumber ( total / count , 6 ) : '—'
}
if ( customerSource && key === 'remark' ) return String ( row . cargoRemark || '—' )
const value = row [ key ]
if ( value === null || value === undefined || value === '' ) return '—'
return typeof value === 'number' ? formatNumber ( value , 6 ) : String ( value )
}
2026-09-03 10:46:07 +08:00
function measurementCalculationLabel ( row : Record < string , unknown >) {
if ( measurementCalculationModeForRow ( row ) === 'MULTIPLY' ) {
return `乘以 ${ measurementChargeRuleName ( row . chargeRuleId ) } `
}
if ( measurementCalculationModeForRow ( row ) === 'ACTUAL' ) return '实测原尺寸(不调整)'
return addCoefficientLabel ( row ). replace ( '原尺寸 + ' , '加' )
}
function measurementCalculationModeForRow ( row : Record < string , unknown >) : MeasurementCalculationMode {
if ( row . coefficientMode === 'MULTIPLY' ) return 'MULTIPLY'
if ( row . coefficientMode === 'ADD' ) return 'ADD'
const itemNo = normalizedIdentifier ( row . itemNo )
const measuredItem = measurementDetail . value ? . items . find (( item ) => normalizedIdentifier ( item . itemNo ) === itemNo )
return measuredItem ? . coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : measuredItem ? . coefficientMode === 'ADD' ? 'ADD' : 'ACTUAL'
}
2026-09-03 21:09:40 +08:00
async function updateMeasurementCalculationMode ( row : Record < string , unknown > | null , mode : MeasurementCalculationMode , selectedRuleId ?: string ) : Promise < boolean > {
2026-09-03 10:46:07 +08:00
const summary = detail . value ? . summary
const measurement = measurementDetail . value
2026-09-10 11:02:52 +08:00
if ( ! summary ? . inboundId || ! measurement ? . measurementId || measurement . version == null || ! measurementStrategyEditable . value ) return false
2026-09-03 10:46:07 +08:00
const itemId = normalizedIdentifier ( row ? . id )
const itemNo = normalizedIdentifier ( row ? . itemNo )
const targetIds = new Set ( measurement . items
. filter (( item ) => ! row || ( itemId ? String ( item . id ) === itemId : normalizedIdentifier ( item . itemNo ) === itemNo ))
. map (( item ) => String ( item . id )))
if ( ! targetIds . size ) {
ElMessage . warning ( '当前数据尚未关联实测明细,暂不能设置计算方式' )
2026-09-03 17:49:01 +08:00
return false
2026-09-03 10:46:07 +08:00
}
measurementStrategySavingId . value = row ? ( itemId || `item- ${ itemNo } ` ) : 'all'
try {
await Promise . all ([ loadMeasurementChargeRules (), loadMeasurementDimensionConfig ()])
2026-09-03 21:09:40 +08:00
const existingRuleId = selectedRuleId || measurement . items
. map (( item ) => String ( item . chargeRuleId || '' ))
. find (( id ) => id )
const activeRule = measurementChargeRules . value . find (( rule ) => rule . id === existingRuleId && rule . status === 'ACTIVE' )
|| activeMeasurementChargeRule . value
if ( mode === 'MULTIPLY' && selectedRuleId && ! activeRule ) {
ElMessage . warning ( '请选择当前客户已启用的计费规则' )
return false
}
2026-09-03 10:46:07 +08:00
const requiresMultiply = measurement . items . some (( item ) => (
targetIds . has ( String ( item . id )) ? mode : item . coefficientMode
) === 'MULTIPLY' )
if ( requiresMultiply && ! activeRule ) {
ElMessage . warning ( '该客户尚未启用系数配置,请先到客户计费规则中设置默认规则' )
2026-09-03 17:49:01 +08:00
return false
2026-09-03 10:46:07 +08:00
}
2026-09-10 11:02:52 +08:00
const savedDraft = await updateMeasurementCalculationStrategy ( String ( summary . tenantId ), String ( summary . inboundId ), {
2026-09-03 10:46:07 +08:00
measurementId : String ( measurement . measurementId ),
version : measurement . version ,
2026-09-10 11:02:52 +08:00
coefficientMode : mode ,
chargeRuleId : mode === 'MULTIPLY' ? activeRule ! . id : null ,
2026-09-03 10:46:07 +08:00
})
2026-09-04 14:36:11 +08:00
// PUT 返回的新版本已经代表事务保存成功。详情查询可能经过读副本并短暂返回旧版本,
// 不能用紧随其后的旧数据否定本次成功保存;先按新版本和本次规则更新本地视图。
measurementDetail . value = {
... measurement ,
version : savedDraft . version ,
measurementStatus : savedDraft . status ,
items : measurement . items . map (( item ) => targetIds . has ( String ( item . id )) ? {
... item ,
coefficientMode : mode ,
chargeRuleId : mode === 'MULTIPLY' ? activeRule ! . id : null ,
calculatedOuterLengthCm : null ,
calculatedOuterWidthCm : null ,
calculatedOuterHeightCm : null ,
} : item ),
2026-09-03 21:09:40 +08:00
}
2026-09-04 14:36:11 +08:00
measurementSectionModes . outer = mode
const refreshed = await waitForSavedMeasurementVersion (
String ( summary . tenantId ), String ( summary . inboundId ), savedDraft . version ,
)
if ( refreshed ) measurementDetail . value = refreshed
measurementSectionModes . outer = mode
measurementLoadedKey . value = ''
2026-09-03 21:09:40 +08:00
ElMessage . success ( ` ${ row ? '当前数据' : '全部测量数据' } 已保存并绑定计算规则,体积已重新计算` )
2026-09-03 17:49:01 +08:00
return true
2026-09-03 10:46:07 +08:00
} catch ( error ) {
measurementLoadedKey . value = ''
await loadMeasurementDetails ()
ElMessage . error ( getApiErrorDetails ( error , '计算方式更新失败' ). message )
2026-09-03 17:49:01 +08:00
return false
2026-09-03 10:46:07 +08:00
} finally {
measurementStrategySavingId . value = ''
}
}
async function saveWarehouseOuterDimensions ( row : Record < string , unknown >) {
const summary = detail . value ? . summary
const measurement = measurementDetail . value
if ( ! summary ? . inboundId || ! measurement ? . measurementId || measurement . version == null
2026-09-10 11:38:38 +08:00
|| ! measurementStrategyEditable . value ) return
2026-09-03 12:00:07 +08:00
const dimensions = [ row . outerLengthCm , row . outerWidthCm , row . outerHeightCm ]
. map (( value ) => value == null || value === '' ? null : Number ( value ))
if ( dimensions . some (( value ) => value != null && ( ! Number . isFinite ( value ) || value <= 0 ))) {
ElMessage . warning ( '仓库修改外箱尺寸必须大于 0' )
2026-09-03 10:46:07 +08:00
return
}
measurementOuterSavingId . value = String ( row . id )
try {
2026-09-10 11:38:38 +08:00
const saved = await updateMeasurementOuterDimensions (
String ( summary . tenantId ), String ( summary . inboundId ), String ( row . id ), {
2026-09-03 10:46:07 +08:00
measurementId : String ( measurement . measurementId ),
version : measurement . version ,
2026-09-10 11:38:38 +08:00
outerLengthCm : dimensions [ 0 ],
outerWidthCm : dimensions [ 1 ],
outerHeightCm : dimensions [ 2 ],
2026-09-03 10:46:07 +08:00
})
2026-09-10 11:38:38 +08:00
measurementDetail . value = {
... measurement ,
version : saved . version ,
measurementStatus : saved . status ,
items : measurement . items . map (( item ) => String ( item . id ) === String ( row . id ) ? {
... item ,
outerLengthCm : dimensions [ 0 ],
outerWidthCm : dimensions [ 1 ],
outerHeightCm : dimensions [ 2 ],
calculatedOuterLengthCm : dimensions [ 0 ],
calculatedOuterWidthCm : dimensions [ 1 ],
calculatedOuterHeightCm : dimensions [ 2 ],
} : item ),
}
const refreshed = await waitForSavedMeasurementVersion (
String ( summary . tenantId ), String ( summary . inboundId ), saved . version ,
)
if ( refreshed ) measurementDetail . value = refreshed
2026-09-03 10:46:07 +08:00
measurementLoadedKey . value = ''
ElMessage . success ( `序号 ${ row . itemNo || '—' } 的外箱规已更新,相关数值已重新计算` )
} catch ( error ) {
measurementLoadedKey . value = ''
await loadMeasurementDetails ()
ElMessage . error ( getApiErrorDetails ( error , '仓库外箱尺寸保存失败' ). message )
} finally {
measurementOuterSavingId . value = ''
}
}
function measurementChargeRuleName ( ruleId : unknown ) {
const normalizedRuleId = normalizedIdentifier ( ruleId )
const selected = normalizedRuleId
? measurementChargeRules . value . find (( rule ) => rule . id === normalizedRuleId )
: null
const fallback = measurementChargeRules . value . find (( rule ) => rule . defaultRule && rule . status === 'ACTIVE' )
|| measurementChargeRules . value . find (( rule ) => rule . status === 'ACTIVE' )
const rule = selected || fallback
if ( rule ) {
const remark = rule . remark ? . trim ()
return `系数 ${ rule . lengthFactor }${ remark ? ` · ${ remark } ` : '' }${ rule . defaultRule ? '(默认)' : '' } `
}
if ( measurementChargeRulesLoading . value ) return '加载中…'
return normalizedRuleId || '未配置'
}
async function loadMeasurementChargeRules () {
const tenantId = normalizedIdentifier ( detail . value ? . summary . tenantId )
const customerId = normalizedIdentifier ( detail . value ? . summary . customerId ) || tenantId
const loadedKey = ` ${ tenantId } : ${ customerId } `
if ( ! customerId || measurementChargeRulesLoadedKey . value === loadedKey || measurementChargeRulesLoading . value ) return
measurementChargeRulesLoading . value = true
try {
measurementChargeRules . value = await listCustomerChargeRules ( customerId , tenantId )
measurementChargeRulesLoadedKey . value = loadedKey
} catch {
measurementChargeRules . value = []
} finally {
measurementChargeRulesLoading . value = false
}
}
2026-09-03 21:09:40 +08:00
async function loadMeasurementDimensionConfig ( force = false ) {
2026-09-03 10:46:07 +08:00
const tenantId = normalizedIdentifier ( detail . value ? . summary . tenantId )
2026-09-03 21:09:40 +08:00
if ( ! tenantId || ( ! force && measurementDimensionConfigLoadedKey . value === tenantId ) || measurementDimensionConfigLoading . value ) return
2026-09-03 10:46:07 +08:00
measurementDimensionConfigLoading . value = true
try {
measurementDimensionConfig . value = await getCustomerDimensionConfig ( tenantId )
measurementDimensionConfigLoadedKey . value = tenantId
} catch {
measurementDimensionConfig . value = null
} finally {
measurementDimensionConfigLoading . value = false
}
}
2026-08-07 14:47:11 +08:00
interface MeasurementTotals {
count : number
gross : number
volume : number
volumetric : number
}
function measurementTotals ( rows : Record < string , unknown >[], customerSource = false ) {
const values = rows . reduce < MeasurementTotals >(( result , row ) => {
const count = Number ( row . packageCount )
const gross = Number ( customerSource ? row . grossWeightKg : row . totalWeightKg )
const volume = Number ( customerSource ? row . volumeCbm : row . totalVolumeCbm )
const volumetric = Number ( row . volumeWeightKg )
if ( Number . isFinite ( count )) result . count += count
if ( Number . isFinite ( gross )) result . gross += gross
if ( Number . isFinite ( volume )) result . volume += volume
if ( Number . isFinite ( volumetric )) result . volumetric += volumetric
return result
}, { count : 0 , gross : 0 , volume : 0 , volumetric : 0 })
return {
count : values . count || null , gross : values . gross || null , volume : values . volume || null ,
volumetric : values . volumetric || null , weightVolume : values . volume || null ,
}
}
2026-08-17 18:00:42 +08:00
function releaseMeasurementPreview () {
measurementPreviewRequestSerial += 1
if ( measurementPreviewUrl . value ) URL . revokeObjectURL ( measurementPreviewUrl . value )
measurementPreviewUrl . value = ''
measurementPreviewLoading . value = false
measurementPreviewError . value = null
}
async function openMeasurementEvidence ( fileId : string | null , title : string ) {
const summary = detail . value ? . summary
if ( ! fileId || ! summary ? . inboundId ) return
releaseMeasurementPreview ()
const serial = ++ measurementPreviewRequestSerial
measurementPreviewTitle . value = title
measurementPreviewOpen . value = true
measurementPreviewLoading . value = true
try {
const blob = await downloadWeightImage ( String ( summary . tenantId ), String ( summary . inboundId ), fileId )
if ( serial !== measurementPreviewRequestSerial ) return
measurementPreviewUrl . value = URL . createObjectURL ( blob )
} catch ( error ) {
if ( serial !== measurementPreviewRequestSerial ) return
measurementPreviewError . value = getApiErrorDetails ( error , '测量原图读取失败,请稍后重试' )
} finally {
if ( serial === measurementPreviewRequestSerial ) measurementPreviewLoading . value = false
}
2026-08-07 14:47:11 +08:00
}
async function loadMeasurementDetails () {
const summary = detail . value ? . summary
2026-08-17 18:00:42 +08:00
if ( ! summary ? . inboundId || ! can ( 'platform:firstleg:inbound:measure:view' )) return
2026-08-07 14:47:11 +08:00
const key = ` ${ summary . tenantId } : ${ summary . inboundId } `
if ( measurementLoadedKey . value === key || measurementLoading . value ) return
2026-08-17 18:00:42 +08:00
const serial = ++ measurementRequestSerial
2026-08-07 14:47:11 +08:00
measurementLoading . value = true
2026-08-17 18:00:42 +08:00
measurementError . value = null
2026-08-07 14:47:11 +08:00
try {
2026-09-03 10:46:07 +08:00
const [ response ] = await Promise . all ([
getInboundMeasurement ( String ( summary . tenantId ), String ( summary . inboundId )),
loadMeasurementChargeRules (),
2026-09-03 21:09:40 +08:00
loadMeasurementDimensionConfig ( true ),
2026-09-03 10:46:07 +08:00
])
2026-08-17 18:00:42 +08:00
if ( serial !== measurementRequestSerial ) return
measurementDetail . value = response
2026-09-03 17:49:01 +08:00
const configuredMode = measurementDimensionConfig . value ? . defaultCalculationMode
2026-09-03 21:09:40 +08:00
const savedModes = [... new Set ( response . items . map (( item ) => item . coefficientMode )
. filter (( mode ) : mode is MeasurementCalculationMode => (
mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'
)))]
// 订单测量明细中已落库的规则是本次计费快照,必须优先于客户当前默认规则。
// 客户默认值只用于尚未绑定规则的新测量数据,避免保存后又被覆盖回 ACTUAL。
measurementSectionModes . outer = savedModes . length === 1
? savedModes [ 0 ] !
: configuredMode === 'MULTIPLY' && activeMeasurementChargeRule . value
? 'MULTIPLY' : configuredMode === 'ADD' ? 'ADD' : 'ACTUAL'
measurementSectionModes . customer = configuredMode === 'MULTIPLY' && activeMeasurementChargeRule . value
? 'MULTIPLY' : configuredMode === 'ADD' ? 'ADD' : 'ACTUAL'
2026-09-03 17:49:01 +08:00
measurementSectionModes . actual = 'ACTUAL'
2026-08-07 14:47:11 +08:00
measurementLoadedKey . value = key
2026-08-17 18:00:42 +08:00
} catch ( error ) {
if ( serial !== measurementRequestSerial ) return
2026-09-04 11:50:34 +08:00
if ( ! measurementDetail . value ) measurementDetail . value = null
2026-08-17 18:00:42 +08:00
measurementError . value = getApiErrorDetails ( error , '测量数据加载失败,请稍后重试' )
2026-08-07 14:47:11 +08:00
} finally {
2026-08-17 18:00:42 +08:00
if ( serial === measurementRequestSerial ) measurementLoading . value = false
2026-08-07 14:47:11 +08:00
}
}
2026-09-07 11:20:43 +08:00
async function loadHoldDetail ( force = false ) {
const inboundId = normalizedInboundId . value
const tenantId = normalizedIdentifier ( detail . value ? . summary . tenantId )
const key = inboundId && tenantId ? ` ${ tenantId } : ${ inboundId } ` : ''
if ( ! key || ! can ( 'platform:firstleg:inbound:list' )) {
holdDetail . value = null
holdError . value = null
holdLoadedKey . value = ''
return
}
if ( ! force && ( holdLoading . value || holdLoadedKey . value === key )) return
const serial = ++ holdRequestSerial
holdLoading . value = true
holdError . value = null
try {
const result = await getPlatformInboundHold ( tenantId , inboundId )
if ( serial !== holdRequestSerial ) return
holdDetail . value = result
holdLoadedKey . value = key
} catch ( error ) {
if ( serial !== holdRequestSerial ) return
holdDetail . value = null
holdLoadedKey . value = ''
holdError . value = getApiErrorDetails ( error , '扣货状态与历史加载失败' )
} finally {
if ( serial === holdRequestSerial ) holdLoading . value = false
}
}
2026-08-07 14:47:11 +08:00
function selectDetailTab ( key : string ) {
activeDetailTab . value = key
if ( key === 'measurement' ) void loadMeasurementDetails ()
2026-08-31 10:48:23 +08:00
if ( key === 'fulfillment' ) void loadSubsequentFulfillment ()
2026-09-07 11:20:43 +08:00
if ( key === 'other' ) void loadHoldDetail ()
2026-08-31 10:48:23 +08:00
}
const hasLoadingAssociation = computed (() => Boolean ( loadingAssociation . value
&& ( loadingAssociation . value . loadingPlanNo || loadingAssociation . value . containerNo || loadingAssociation . value . cabinetNo
|| loadingAssociation . value . loadingStatus || loadingAssociation . value . pdaTaskStatus )))
const hasOverseasRelationship = computed (() => Boolean ( overseasRelationship . value ? . found
&& ( overseasRelationship . value . overseasWarehouse || overseasRelationship . value . overseasReceivingStatus
|| overseasRelationship . value . taillegOrders . length || overseasRelationship . value . inboundPlans . length
|| overseasRelationship . value . palletCodes . length || overseasRelationship . value . inventory . length )))
const hasSubsequentFulfillment = computed (() => hasLoadingAssociation . value || hasOverseasRelationship . value )
async function loadSubsequentFulfillment () {
const inboundCode = detail . value ? . summary . inboundCode ? . trim () || ''
const serial = ++ subsequentFulfillmentRequestSerial
loadingAssociation . value = null
overseasRelationship . value = null
loadingAssociationError . value = null
overseasRelationshipError . value = null
if ( ! inboundCode ) return
subsequentFulfillmentLoading . value = true
const tasks : Promise < void >[] = []
if ( can ( 'platform:firstleg:smart-loading:list' )) tasks . push ( getLoadingAssociationSummary ( inboundCode )
. then (( value ) => { if ( serial === subsequentFulfillmentRequestSerial ) loadingAssociation . value = value })
. catch (( error ) => { if ( serial === subsequentFulfillmentRequestSerial ) loadingAssociationError . value = getApiErrorDetails ( error , '装柜与PDA履约数据加载失败' ) }))
if ( can ( 'platform:overseas-warehouse:inventory:list' )) tasks . push ( getOverseasInboundCodeRelationship ( inboundCode )
. then (( value ) => { if ( serial === subsequentFulfillmentRequestSerial ) overseasRelationship . value = value })
. catch (( error ) => { if ( serial === subsequentFulfillmentRequestSerial ) overseasRelationshipError . value = getApiErrorDetails ( error , '尾程与库存履约数据加载失败' ) }))
await Promise . all ( tasks )
if ( serial === subsequentFulfillmentRequestSerial ) subsequentFulfillmentLoading . value = false
2026-08-07 14:47:11 +08:00
}
function toggleMeasurementSection ( section : keyof typeof measurementCollapsed ) {
measurementCollapsed [ section ] = ! measurementCollapsed [ section ]
}
2026-08-06 13:56:04 +08:00
function nextActionBoundary ( row : UnifiedOrderResource ) {
2026-08-06 20:14:56 +08:00
return row . nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''
2026-08-06 13:56:04 +08:00
}
2026-08-06 18:02:04 +08:00
function canApprove ( row : UnifiedOrderResource ) {
return row . mainStatus === 'PENDING_REVIEW' && row . nextAction === 'APPROVE_ORDER' && can ( 'platform:order:approve' )
}
2026-08-07 10:45:19 +08:00
function openApproval ( row : UnifiedOrderResource ) {
2026-08-06 18:02:04 +08:00
if ( ! canApprove ( row )) return
approvalOrder . value = row
approvalOpen . value = true
}
function closeApproval () {
if ( approvalSaving . value ) return
approvalOpen . value = false
approvalOrder . value = null
}
2026-08-07 10:45:19 +08:00
async function approveOrder () {
2026-08-06 18:02:04 +08:00
if ( ! approvalOrder . value ) return
const order = approvalOrder . value
const orderId = String ( order . orderId )
const tenantId = String ( order . tenantId )
approvalSaving . value = true
approvalLoadingId . value = orderId
try {
await approvePlatformUnifiedOrder ( tenantId , orderId )
approvalOpen . value = false
await loadOrders ()
if ( detail . value ? . summary . orderId === order . orderId ) await loadDetail ()
approvalOrder . value = null
2026-08-07 10:45:19 +08:00
ElMessage . success ( '订单审核通过' )
2026-08-06 18:02:04 +08:00
} catch ( error ) {
2026-08-07 10:45:19 +08:00
ElMessage . error ( getApiErrorDetails ( error , '订单审核失败,请稍后重试' ). message )
2026-08-06 18:02:04 +08:00
} finally {
approvalSaving . value = false
approvalLoadingId . value = ''
}
}
2026-08-06 20:14:56 +08:00
function openNoteDialog ( order ?: UnifiedOrderResource ) {
if ( ! can ( 'platform:order:note' )) return
const target = order || detail . value ? . summary || null
if ( ! target ) return
noteOrder . value = target
2026-09-14 16:56:11 +08:00
noteForm . internalRemark = target . latestInternalNote || target . internalRemark || ''
noteForm . warehouseRemark = target . warehouseRemark || ''
noteForm . customerServiceRemark = target . customerServiceRemark || ''
2026-08-06 19:03:13 +08:00
noteOpen . value = true
}
async function saveNote () {
2026-09-14 16:56:11 +08:00
if ( ! noteOrder . value ) return
2026-08-06 19:03:13 +08:00
noteSaving . value = true
try {
2026-08-06 20:14:56 +08:00
const target = noteOrder . value
2026-09-14 16:56:11 +08:00
await updatePlatformUnifiedOrderRemarks ( String ( target . tenantId ), String ( target . orderId ), {
expectedVersion : Number ( target . orderVersion || 0 ),
internalRemark : noteForm . internalRemark . trim () || null ,
warehouseRemark : noteForm . warehouseRemark . trim () || null ,
customerServiceRemark : noteForm . customerServiceRemark . trim () || null ,
})
2026-08-06 19:03:13 +08:00
noteOpen . value = false
2026-08-07 14:47:11 +08:00
await loadOrders ()
if ( detail . value ? . summary . orderId === target . orderId ) await loadDetail ( true )
2026-08-06 20:14:56 +08:00
noteOrder . value = null
2026-09-14 16:56:11 +08:00
ElMessage . success ( '订单备注已保存' )
2026-08-06 19:03:13 +08:00
} catch ( error ) {
2026-09-14 16:56:11 +08:00
ElMessage . error ( getApiErrorDetails ( error , '订单备注保存失败,请稍后重试' ). message )
2026-08-06 19:03:13 +08:00
} finally {
noteSaving . value = false
}
}
2026-09-07 11:20:43 +08:00
function openHoldDialog ( action : 'HOLD' | 'RELEASE' ) {
const allowed = action === 'HOLD' ? canHoldInbound . value : canReleaseInbound . value
if ( ! allowed || ! holdDetail . value ) return
holdAction . value = action
holdReason . value = ''
2026-09-07 12:21:44 +08:00
holdEvidenceFile . value = null
holdUploadedEvidenceFileId . value = ''
holdEvidenceError . value = null
holdEvidenceUploadKey . value += 1
2026-09-07 11:20:43 +08:00
holdDialogOpen . value = true
}
2026-09-07 12:21:44 +08:00
const holdEvidenceMaxBytes = 10 * 1024 * 1024
const holdEvidenceExtensions = new Set ([ 'txt' , 'pdf' , 'jpg' , 'jpeg' , 'png' ])
function selectHoldEvidenceFile ( upload : UploadFile ) {
const file = upload . raw || null
holdEvidenceFile . value = null
holdUploadedEvidenceFileId . value = ''
holdEvidenceError . value = null
if ( ! file ) return
const extension = file . name . split ( '.' ). pop () ? . toLowerCase () || ''
if ( ! holdEvidenceExtensions . has ( extension )) {
holdEvidenceError . value = { code : 'FILE_TYPE_INVALID' , message : '关联文件仅支持 TXT、PDF、JPG、PNG' }
holdEvidenceUploadKey . value += 1
return
}
if ( file . size <= 0 || file . size > holdEvidenceMaxBytes ) {
holdEvidenceError . value = { code : 'FILE_SIZE_INVALID' , message : '关联文件不能为空且不能超过 10MB' }
holdEvidenceUploadKey . value += 1
return
}
holdEvidenceFile . value = file
}
function removeHoldEvidenceFile () {
holdEvidenceFile . value = null
holdUploadedEvidenceFileId . value = ''
holdEvidenceError . value = null
}
2026-09-07 11:20:43 +08:00
async function submitHoldAction () {
2026-09-07 12:21:44 +08:00
if ( holdSaving . value ) return
const allowed = holdAction . value === 'HOLD' ? canHoldInbound . value : canReleaseInbound . value
if ( ! allowed ) return
2026-09-07 11:20:43 +08:00
const reason = holdReason . value . trim ()
const inboundId = normalizedInboundId . value
const tenantId = normalizedIdentifier ( detail . value ? . summary . tenantId )
if ( ! holdDetail . value || ! inboundId || ! tenantId ) return
if ( ! reason ) {
ElMessage . warning ( holdAction . value === 'HOLD' ? '请填写扣货原因' : '请填写解除原因' )
return
}
2026-09-07 12:21:44 +08:00
if ( reason . length > 1000 ) {
ElMessage . warning ( '原因不能超过 1000 个字符' )
return
}
2026-09-07 11:20:43 +08:00
holdSaving . value = true
try {
2026-09-07 12:21:44 +08:00
if ( holdEvidenceFile . value && ! holdUploadedEvidenceFileId . value ) {
try {
holdEvidenceError . value = null
const uploaded = await uploadPlatformInboundHoldEvidence ( tenantId , inboundId , holdEvidenceFile . value )
holdUploadedEvidenceFileId . value = uploaded . fileId
} catch ( error ) {
holdEvidenceError . value = getApiErrorDetails ( error , '关联文件上传失败,请稍后重试' )
ElMessage . error ( holdEvidenceError . value . message )
return
}
}
const command = {
expectedVersion : holdDetail . value . version ,
reason ,
...( holdUploadedEvidenceFileId . value ? { evidenceFileId : holdUploadedEvidenceFileId . value } : {}),
}
2026-09-07 11:20:43 +08:00
holdDetail . value = holdAction . value === 'HOLD'
? await holdPlatformInbound ( tenantId , inboundId , command )
: await releasePlatformInbound ( tenantId , inboundId , command )
holdDialogOpen . value = false
await Promise . all ([ loadDetail ( true ), loadOrders ()])
ElMessage . success ( holdAction . value === 'HOLD' ? '扣货成功' : '解除扣货成功' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , holdAction . value === 'HOLD' ? '扣货失败' : '解除扣货失败' ). message )
} finally {
holdSaving . value = false
}
}
2026-09-07 12:21:44 +08:00
async function downloadHoldEvidence ( fileId : string ) {
const inboundId = normalizedInboundId . value
const tenantId = normalizedIdentifier ( detail . value ? . summary . tenantId )
if ( ! fileId || ! inboundId || ! tenantId || holdDownloadingFileId . value || ! can ( 'platform:firstleg:inbound:list' )) return
holdDownloadingFileId . value = fileId
try {
const blob = await downloadPlatformInboundHoldEvidence ( tenantId , inboundId , fileId )
const extension = blob . type === 'application/pdf' ? '.pdf'
: blob . type === 'image/png' ? '.png'
: blob . type === 'image/jpeg' ? '.jpg'
: blob . type === 'text/plain' ? '.txt' : ''
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = `扣货凭证 ${ extension } `
document . body . appendChild ( anchor )
anchor . click ()
anchor . remove ()
URL . revokeObjectURL ( url )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '扣货凭证下载失败,请稍后重试' ). message )
} finally {
holdDownloadingFileId . value = ''
}
}
2026-08-06 13:56:04 +08:00
async function loadCustomers () {
customerLoading . value = true
customerLoadError . value = null
try {
const records : PlatformTenantResource [] = []
let currentPage = 1
let pages = 1
do {
2026-08-14 12:50:42 +08:00
const result = await listPlatformTenants ({ page : currentPage , size : 200 , status : 'ACTIVE' })
2026-08-06 13:56:04 +08:00
records . push (... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
customers . value = records
} catch ( error ) {
customers . value = []
customerLoadError . value = getApiErrorDetails ( error , '客户列表加载失败,暂时无法按客户筛选' )
} finally {
customerLoading . value = false
}
}
2026-08-06 15:32:02 +08:00
async function loadWarehouses () {
warehouseLoading . value = true
warehouseLoadError . value = null
try {
warehouses . value = await listAllPlatformWarehouses ({ status : 'ACTIVE' })
} catch ( error ) {
warehouses . value = []
warehouseLoadError . value = getApiErrorDetails ( error , '平台仓库加载失败,暂时无法按仓库筛选' )
} finally {
warehouseLoading . value = false
}
}
2026-08-06 13:56:04 +08:00
async function loadOrders () {
2026-08-17 18:00:42 +08:00
selectedApprovalOrders . value = []
2026-08-29 10:57:01 +08:00
selectedOrders . value = []
2026-08-06 13:56:04 +08:00
loading . value = true
loadError . value = null
2026-08-29 10:57:01 +08:00
coLoadError . value = null
2026-09-03 11:47:32 +08:00
Object . keys ( planningDrafts ). forEach (( key ) => delete planningDrafts [ key ])
2026-08-06 13:56:04 +08:00
try {
2026-08-11 18:06:35 +08:00
const activeFilters = appliedFilters . value
const range = activeFilters . createdRange
2026-08-06 13:56:04 +08:00
const result = await listPlatformUnifiedOrders ({
2026-08-11 18:06:35 +08:00
tenantId : activeFilters . tenantId || undefined ,
mainStatus : selectedStatus . value || undefined ,
receivingWarehouseId : activeFilters . receivingWarehouseId || undefined ,
destinationWarehouseId : activeFilters . destinationWarehouseId || undefined ,
inboundCode : trimmed ( activeFilters . inboundCode ),
expectedArrivalDate : activeFilters . expectedArrivalDate || undefined ,
feeReviewStatus : activeFilters . feeReviewStatus || undefined ,
channelName : trimmed ( activeFilters . channelName ),
containerNo : trimmed ( activeFilters . containerNo ),
cabinetNo : trimmed ( activeFilters . cabinetNo ),
sailingDate : activeFilters . sailingDate || undefined ,
loaded : optionalBoolean ( activeFilters . loaded ),
fullContainer : optionalBoolean ( activeFilters . fullContainer ),
measurementReviewStatus : activeFilters . measurementReviewStatus || undefined ,
fbaCode : trimmed ( activeFilters . fbaCode ),
2026-08-06 13:56:04 +08:00
createdFrom : range . length === 2 ? ` ${ range [ 0 ] } T00:00:00` : undefined ,
createdTo : range . length === 2 ? ` ${ range [ 1 ] } T23:59:59` : undefined ,
page : page . value ,
size : pageSize . value ,
})
records . value = result . records
total . value = result . total
} catch ( error ) {
records . value = []
total . value = 0
loadError . value = getApiErrorDetails ( error , '统一订单列表加载失败,请检查 C02 统一查询接口和订单权限' )
} finally {
loading . value = false
}
}
function search () {
2026-08-11 18:06:35 +08:00
appliedFilters . value = cloneFilters ( filters . value )
2026-08-06 13:56:04 +08:00
if ( page . value !== 1 ) {
page . value = 1
return
}
void loadOrders ()
}
function reset () {
2026-08-06 15:32:02 +08:00
filters . value = emptyFilters ()
2026-08-11 18:06:35 +08:00
appliedFilters . value = emptyFilters ()
2026-08-10 18:12:23 +08:00
if ( page . value === 1 ) void loadOrders ()
else page . value = 1
2026-08-06 13:56:04 +08:00
}
2026-08-06 19:03:13 +08:00
async function openDetail ( row : UnifiedOrderResource ) {
await router . push ({ name : 'unified-order-detail' , params : { orderId : String ( row . orderId ) }, query : { tenantId : String ( row . tenantId ) } })
await nextTick ()
window . scrollTo ({ top : 0 })
2026-08-06 13:56:04 +08:00
}
function openMeasurement ( row : UnifiedOrderResource ) {
2026-08-17 17:49:10 +08:00
if ( ! row . inboundId || ! can ( 'platform:firstleg:inbound:measure:view' )) return
2026-08-13 19:00:37 +08:00
measurementOrder . value = row
measurementDialogOpen . value = true
}
2026-09-03 17:49:01 +08:00
async function openFeeConfirmDialog () {
2026-09-03 10:46:07 +08:00
const summary = detail . value ? . summary
if ( ! summary ? . inboundId ) {
ElMessage . warning ( '当前单据缺少入仓信息,暂不能发送费用' )
return
}
2026-09-03 21:09:40 +08:00
if ( ! hasActualMeasurementData . value ) {
ElMessage . warning ( '暂无实际测量数据,不能确认并发送费用' )
2026-09-03 10:46:07 +08:00
return
}
2026-09-03 21:09:40 +08:00
if ( confirmFeeSending . value ) {
2026-09-03 17:49:01 +08:00
return
}
if ( measurementStrategySavingId . value ) {
ElMessage . warning ( '外箱规计算规则正在保存,请稍后再发送费用' )
return
}
const outerTotals = measurementTotals ( outerMeasurementRows . value )
2026-09-03 21:09:40 +08:00
if ( ! outerMeasurementRows . value . length || ! ( Number ( outerTotals . volume ) > 0 )) {
2026-09-03 17:49:01 +08:00
ElMessage . warning ( '外箱规数据不完整,请先保存外箱规尺寸和计算规则' )
return
}
2026-09-03 21:09:40 +08:00
const actualTotals = measurementTotals ( actualMeasurementRows . value )
2026-09-04 11:50:34 +08:00
const measuredGrossWeight = Number ( measurementDetail . value ? . actualGrossWeightKg )
const measuredNetWeight = Number ( measurementDetail . value ? . actualNetWeightKg )
2026-09-04 14:48:47 +08:00
feeConfirmWeight . value = Number ( actualTotals . gross ) > 0
? Number ( actualTotals . gross )
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
feeConfirmVolume . value = Number ( actualTotals . volume ) > 0 ? Number ( actualTotals . volume ) : null
2026-09-09 17:58:23 +08:00
feeConfirmReference . value = null
feeConfirmUnitPrice . value = null
feeConfirmPriceReason . value = ''
2026-09-15 15:24:59 +08:00
feeConfirmAttempt = null
2026-09-03 10:46:07 +08:00
feeConfirmOpen . value = true
2026-09-09 17:58:23 +08:00
feeConfirmReferenceLoading . value = true
try {
const tenantId = String ( summary . tenantId )
const inboundId = String ( summary . inboundId )
let snapshot : InboundFeeSnapshot
try {
snapshot = await getLatestInboundFee ( tenantId , inboundId )
} catch ( error ) {
const details = getApiErrorDetails ( error , '价格表参考单价加载失败' )
if ( details . code !== 'A0404' ) throw error
snapshot = await calculateInboundFee ( tenantId , inboundId , {
selectedWeightKg : Number ( feeConfirmWeight . value ),
selectedVolumeCbm : Number ( feeConfirmVolume . value ),
weightSource : 'ACTUAL' ,
volumeSource : 'ACTUAL' ,
}, createFeeActionIdempotencyKey ( 'calculate' ))
}
feeConfirmReference . value = snapshot
feeConfirmUnitPrice . value = Number ( snapshot . unitPrice )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '价格表参考单价加载失败,暂不能推送费用' ). message )
feeConfirmOpen . value = false
} finally {
feeConfirmReferenceLoading . value = false
}
2026-09-03 10:46:07 +08:00
}
2026-09-15 15:24:59 +08:00
function currentFeeConfirmAttempt (
selectedWeightKg : number ,
selectedVolumeCbm : number ,
selectedUnitPrice : number ,
) {
const fingerprint = JSON . stringify ([
selectedWeightKg ,
selectedVolumeCbm ,
selectedUnitPrice ,
feeConfirmPriceReason . value . trim (),
])
if ( ! feeConfirmAttempt || feeConfirmAttempt . fingerprint !== fingerprint ) {
feeConfirmAttempt = {
fingerprint ,
calculateKey : createFeeActionIdempotencyKey ( 'calculate' ),
adjustKey : createFeeAdjustmentIdempotencyKey (),
confirmKey : createFeeActionIdempotencyKey ( 'confirm' ),
sendKey : createFeeActionIdempotencyKey ( 'send' ),
}
}
return feeConfirmAttempt
}
2026-09-03 10:46:07 +08:00
async function executeConfirmAndSendFee () {
2026-09-15 15:24:59 +08:00
if ( confirmFeeSending . value ) return
2026-09-03 10:46:07 +08:00
const summary = detail . value ? . summary
if ( ! summary ? . inboundId ) return
const selectedWeightKg = Number ( feeConfirmWeight . value )
const selectedVolumeCbm = Number ( feeConfirmVolume . value )
2026-09-09 17:58:23 +08:00
const selectedUnitPrice = Number ( feeConfirmUnitPrice . value )
2026-09-03 10:46:07 +08:00
if ( ! ( selectedWeightKg > 0 ) || ! ( selectedVolumeCbm > 0 )) {
ElMessage . warning ( '计费毛重和计费体积必须大于 0' )
return
}
2026-09-09 17:58:23 +08:00
if ( ! ( selectedUnitPrice > 0 )) {
ElMessage . warning ( '对客单价必须大于 0' )
return
}
if ( feeConfirmPriceChanged . value && feeConfirmPriceReason . value . trim (). length < 2 ) {
ElMessage . warning ( '人工修改参考单价后,请填写至少 2 个字符的调整原因' )
return
}
2026-09-15 15:24:59 +08:00
const attempt = currentFeeConfirmAttempt ( selectedWeightKg , selectedVolumeCbm , selectedUnitPrice )
2026-09-03 10:46:07 +08:00
confirmFeeSending . value = true
try {
const tenantId = String ( summary . tenantId )
const inboundId = String ( summary . inboundId )
2026-09-03 21:09:40 +08:00
let snapshot = await calculateInboundFee ( tenantId , inboundId , {
selectedWeightKg ,
selectedVolumeCbm ,
2026-09-04 11:50:34 +08:00
weightSource : 'ACTUAL' ,
volumeSource : 'ACTUAL' ,
2026-09-15 15:24:59 +08:00
}, attempt . calculateKey )
2026-09-09 17:58:23 +08:00
const calculatedUnitPrice = Number ( snapshot . unitPrice )
if ( Math . abs ( selectedUnitPrice - calculatedUnitPrice ) > 0.000001 ) {
snapshot = await adjustPublishedInboundFee ( tenantId , inboundId , {
expectedFeeVersion : snapshot . feeVersion ,
chargeableQuantity : Number ( snapshot . chargeableQuantity ),
unitPrice : selectedUnitPrice ,
additionalAmount : Number ( snapshot . additionalAmount || 0 ),
reason : feeConfirmPriceReason . value . trim () || '推送前按已展示参考单价人工确认' ,
toleranceConfirmed : true ,
2026-09-15 15:24:59 +08:00
}, attempt . adjustKey )
2026-09-09 17:58:23 +08:00
}
2026-09-15 15:24:59 +08:00
snapshot = await confirmInboundFee ( tenantId , inboundId , attempt . confirmKey )
2026-09-03 10:46:07 +08:00
if ( snapshot . feeStatus === 'CONFIRMED' ) {
2026-09-15 15:24:59 +08:00
snapshot = await sendInboundFee ( tenantId , inboundId , attempt . sendKey )
}
if ( snapshot . feeStatus === 'SENT' ) {
automaticFee . value = { feeStatus : snapshot . feeStatus , totalAmount : Number ( snapshot . totalAmount || 0 ) }
2026-09-04 11:50:34 +08:00
feePanelRevision . value += 1
2026-09-09 17:58:23 +08:00
ElMessage . success ( '计费量和单价已确认并发送给客户' )
2026-09-03 10:46:07 +08:00
feeConfirmOpen . value = false
2026-09-15 15:24:59 +08:00
feeConfirmAttempt = null
2026-09-03 10:46:07 +08:00
await loadDetail ( true )
return
}
2026-09-03 21:09:40 +08:00
ElMessage . warning ( '当前费用状态不允许发送,请刷新后重试' )
2026-09-03 10:46:07 +08:00
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '确认费用发送失败' ). message )
} finally {
confirmFeeSending . value = false
}
}
2026-08-29 10:57:01 +08:00
function handleOrderSelectionChange ( rows : UnifiedOrderResource []) {
selectedOrders . value = rows
2026-08-17 18:00:42 +08:00
selectedApprovalOrders . value = rows . filter ( canApprove )
}
2026-08-31 15:50:05 +08:00
function coLoadSelectionMemberLabel ( row : UnifiedOrderResource ) {
const inboundCode = row . inboundCode ? . trim () || '待生成入仓码'
const warehouseName = row . receivingWarehouseName ? . trim () || '未配置仓库'
const channelName = row . channelName ? . trim () || '未配置渠道'
return ` ${ inboundCode } (仓库: ${ warehouseName } ;渠道: ${ channelName } ) `
}
function coLoadSelectionMembersLabel ( rows : UnifiedOrderResource []) {
return rows . map ( coLoadSelectionMemberLabel ). join ( '、' )
}
2026-08-31 14:54:57 +08:00
const coLoadSelectionProblem = computed (() => {
if ( selectedOrders . value . length < 2 ) return null
2026-08-31 16:37:36 +08:00
const groupedRows = selectedOrders . value . filter (( row ) => row . coLoadGroup && row . coLoadGroup . status !== 'CANCELLED' )
if ( groupedRows . length ) return `以下入仓码已有同柜组: ${ coLoadSelectionMembersLabel ( groupedRows ) } ,请先解除原同柜组`
2026-08-31 15:50:05 +08:00
const missingWarehouseRows = selectedOrders . value . filter (( row ) => ! String ( row . receivingWarehouseId || '' ). trim ())
if ( missingWarehouseRows . length ) return `以下入仓码缺少平台收货仓: ${ coLoadSelectionMembersLabel ( missingWarehouseRows ) } ,请刷新后重试`
const warehouseIds = selectedOrders . value . map (( row ) => String ( row . receivingWarehouseId ). trim ())
if ( new Set ( warehouseIds ). size > 1 ) return `所选订单的平台收货仓不同: ${ coLoadSelectionMembersLabel ( selectedOrders . value ) } ,不能设为同柜组`
const missingChannelRows = selectedOrders . value . filter (( row ) => ! row . channelCode ? . trim ())
if ( missingChannelRows . length ) return `以下入仓码缺少渠道: ${ coLoadSelectionMembersLabel ( missingChannelRows ) } ,请刷新后重试`
const channelCodes = selectedOrders . value . map (( row ) => row . channelCode ! . trim ())
if ( new Set ( channelCodes ). size > 1 ) return `所选订单的渠道不同: ${ coLoadSelectionMembersLabel ( selectedOrders . value ) } ,不能设为同柜组`
2026-08-31 14:54:57 +08:00
return null
})
const canCreateCoLoadGroup = computed (() => selectedOrders . value . length >= 2 && ! coLoadSelectionProblem . value )
2026-08-29 10:57:01 +08:00
function coLoadGroup ( row : UnifiedOrderResource ) {
2026-08-31 10:31:53 +08:00
return row . coLoadGroup || null
2026-08-30 15:49:01 +08:00
}
function coLoadGroupMemberLabel ( group : CoLoadGroup ) {
2026-08-31 10:31:53 +08:00
const orderCount = new Set ( group . members . map (( member ) => member . orderId )). size
2026-08-30 15:49:01 +08:00
return group . members . length === orderCount
? ` ${ orderCount } 票`
: ` ${ orderCount } 票 · ${ group . members . length } 个入仓片段`
}
2026-08-31 10:31:53 +08:00
function coLoadGroupStatusLabel ( group : { status : string }) {
2026-08-30 15:49:01 +08:00
return group . status === 'ACTIVE' ? '未锁定' : group . status === 'LOCKED' ? '已锁定' : group . status === 'CONSUMED' ? '已消费' : '已取消'
}
2026-08-31 10:31:53 +08:00
function coLoadGroupSummary ( group : UnifiedOrderCoLoadGroupSummary | CoLoadBusinessSummary ) {
return `同柜组 · ${ group . memberCount } 票 · ${ coLoadGroupStatusLabel ( group ) } `
2026-08-31 10:09:17 +08:00
}
2026-08-31 10:31:53 +08:00
function hasCoLoadMemberInboundCode ( member : CoLoadBusinessSummary [ 'members' ][ number ]) {
return typeof member . inboundCode === 'string' && Boolean ( member . inboundCode . trim ())
}
2026-08-31 10:09:17 +08:00
2026-08-31 10:31:53 +08:00
const coLoadDetailMembers = computed (() => coLoadDetailSummary . value ? . members . filter ( hasCoLoadMemberInboundCode ) || [])
const coLoadDetailHasMissingInboundCodes = computed (() => Boolean ( coLoadDetailSummary . value )
&& ( coLoadDetailMembers . value . length !== coLoadDetailSummary . value ! . memberCount
|| coLoadDetailSummary . value ! . members . some (( member ) => ! hasCoLoadMemberInboundCode ( member ))))
async function loadCoLoadDetail () {
const inboundCode = coLoadDetailInboundCode . value
if ( ! inboundCode || coLoadDetailLoading . value ) return
coLoadDetailLoading . value = true
coLoadDetailError . value = null
coLoadDetailSummary . value = null
try {
coLoadDetailSummary . value = await getPlatformCoLoadGroupByInboundCode ( inboundCode )
} catch ( error ) {
coLoadDetailError . value = getApiErrorDetails ( error , '同柜组成员加载失败,请刷新后重试' )
} finally {
coLoadDetailLoading . value = false
}
}
function openCoLoadDetail ( row : UnifiedOrderResource ) {
coLoadDetailInboundCode . value = row . inboundCode ? . trim () || ''
coLoadDetailOpen . value = true
coLoadDetailSummary . value = null
2026-08-31 14:54:57 +08:00
coLoadCancelError . value = null
2026-08-31 10:31:53 +08:00
coLoadDetailError . value = coLoadDetailInboundCode . value ? null : getApiErrorDetails ( new Error ( '同柜组成员入仓码尚未返回' ), '同柜组成员入仓码尚未返回,请刷新后重试' )
if ( coLoadDetailInboundCode . value ) void loadCoLoadDetail ()
2026-08-30 15:49:01 +08:00
}
2026-08-29 10:57:01 +08:00
function openCoLoadCreate () {
2026-08-31 16:37:36 +08:00
if ( ! canCreateCoLoadGroup . value ) return
2026-08-29 10:57:01 +08:00
coLoadError . value = null
coLoadCreateOpen . value = true
}
async function createCoLoadGroup () {
2026-08-31 14:54:57 +08:00
if ( ! canCreateCoLoadGroup . value || coLoadSaving . value ) return
2026-08-29 10:57:01 +08:00
coLoadSaving . value = true
coLoadError . value = null
try {
const group = await createPlatformCoLoadGroup ( selectedOrders . value . map (( row ) => String ( row . orderId )))
coLoadCreateOpen . value = false
2026-08-31 10:09:17 +08:00
ElMessage . success ( `已创建同柜不可拆组,共 ${ coLoadGroupMemberLabel ( group ) } ` )
2026-08-29 10:57:01 +08:00
await loadOrders ()
} catch ( error ) {
coLoadError . value = getApiErrorDetails ( error , '创建同柜不可拆组失败' )
} finally {
coLoadSaving . value = false
}
}
2026-08-31 14:54:57 +08:00
async function cancelCoLoadGroup () {
const inboundCode = coLoadDetailInboundCode . value
if ( ! inboundCode || coLoadDetailSummary . value ? . status !== 'ACTIVE' || coLoadCancelSaving . value ) return
try {
await ElMessageBox . confirm (
'确认解除整个同柜组吗?解除后这些订单可重新选择成员建组;不会拆分订单或子单。' ,
'解除同柜组' ,
2026-09-15 09:58:15 +08:00
{ customClass : 'admin-density-confirm' , confirmButtonText : '确认解除' , cancelButtonText : '取消' , type : 'warning' },
2026-08-31 14:54:57 +08:00
)
} catch ( error ) {
if ( error === 'cancel' || error === 'close' ) return
throw error
}
coLoadCancelSaving . value = true
coLoadCancelError . value = null
try {
await cancelPlatformCoLoadGroup ( inboundCode , '重新调整同柜组成员' )
coLoadDetailOpen . value = false
ElMessage . success ( '同柜组已整组解除' )
await loadOrders ()
} catch ( error ) {
coLoadCancelError . value = getApiErrorDetails ( error , '解除同柜组失败' )
} finally {
coLoadCancelSaving . value = false
}
}
2026-09-08 17:38:09 +08:00
function batchReviewItem ( order : UnifiedOrderResource ) : UnifiedOrderBatchReviewItem {
return { orderId : String ( order . orderId ), orderVersion : Number ( order . orderVersion || 0 ) }
}
function handleBatchApprovalSelection ( rows : UnifiedOrderBatchApprovalPreviewItem []) {
batchApprovalSelectedRows . value = rows
}
async function openBatchApproval () {
2026-08-17 18:00:42 +08:00
const selected = [... selectedApprovalOrders . value ]
2026-09-08 17:38:09 +08:00
if ( ! selected . length || batchApprovalLoading . value || batchApprovalSaving . value ) return
batchApprovalOpen . value = true
batchApprovalLoading . value = true
batchApprovalError . value = null
batchApprovalPreviewRows . value = []
batchApprovalSelectedRows . value = []
2026-08-17 18:00:42 +08:00
try {
const groups = new Map < string , UnifiedOrderResource [] >()
selected . forEach (( order ) => {
const tenantId = String ( order . tenantId )
groups . set ( tenantId , [...( groups . get ( tenantId ) || []), order ])
})
2026-09-08 17:38:09 +08:00
const previews = await Promise . all ([... groups ]. map (([ tenantId , orders ]) => (
getPlatformUnifiedOrderBatchApprovalPreview ( tenantId , orders . map ( batchReviewItem ))
)))
batchApprovalPreviewRows . value = previews . flatMap (( preview ) => preview . items )
await nextTick ()
batchApprovalPreviewRows . value . forEach (( row ) => {
if ( row . canApprove ) batchApprovalTableRef . value ? . toggleRowSelection ( row , true )
})
} catch ( error ) {
batchApprovalError . value = getApiErrorDetails ( error , '批量审核预览加载失败,请刷新订单后重试' )
} finally {
batchApprovalLoading . value = false
}
}
async function batchApproveOrders () {
const selected = [... batchApprovalSelectedRows . value ]
if ( ! selected . length || batchApprovalSaving . value ) return
batchApprovalSaving . value = true
try {
const groups = new Map < string , UnifiedOrderBatchApprovalPreviewItem [] >()
selected . forEach (( order ) => {
const tenantId = String ( order . customerId )
groups . set ( tenantId , [...( groups . get ( tenantId ) || []), order ])
})
2026-08-17 18:00:42 +08:00
let successCount = 0
let failureCount = 0
const failures : string [] = []
for ( const [ tenantId , orders ] of groups ) {
try {
2026-09-08 17:38:09 +08:00
const result = await batchApprovePlatformUnifiedOrders ( tenantId , orders . map (( order ) => ({
orderId : order . orderId ,
orderVersion : order . orderVersion ,
})))
2026-08-17 18:00:42 +08:00
successCount += result . successCount
failureCount += result . failureCount
failures . push (... result . failedOrders . map (( failure ) => failure . message ))
} catch ( error ) {
failureCount += orders . length
failures . push ( getApiErrorDetails ( error , '该客户订单批量审核失败' ). message )
}
}
2026-09-08 17:38:09 +08:00
batchApprovalOpen . value = false
2026-08-17 18:00:42 +08:00
await loadOrders ()
const reason = [... new Set ( failures . filter ( Boolean ))]. slice ( 0 , 3 ). join ( '; ' )
if ( ! failureCount ) ElMessage . success ( `已批量审核通过 ${ successCount } 个订单` )
else if ( successCount ) ElMessage . warning ( `批量审核完成:成功 ${ successCount } 个,失败 ${ failureCount } 个 ${ reason ? `。 ${ reason } ` : '' } ` )
else ElMessage . error ( `批量审核失败: ${ failureCount } 个订单未通过 ${ reason ? `。 ${ reason } ` : '' } ` )
} finally {
batchApprovalSaving . value = false
}
}
2026-08-17 17:49:10 +08:00
function handleMeasurementUpdated () {
2026-08-13 19:00:37 +08:00
void loadOrders ()
2026-08-06 13:56:04 +08:00
}
2026-08-17 17:49:10 +08:00
function handleMeasurementSubmitted () {
void loadOrders ()
}
function clearMeasurementOrder () {
measurementOrder . value = null
2026-09-03 10:46:07 +08:00
openWarehouseSuggestionOnMeasurementLoad . value = false
2026-08-17 17:49:10 +08:00
}
2026-08-31 10:31:53 +08:00
type UnifiedOrderRowAction = 'note' | 'approve' | 'measurement'
2026-08-10 17:42:17 +08:00
function unifiedOrderSecondaryActions ( row : UnifiedOrderResource ) : UnifiedOrderRowAction [] {
return [
...( canApprove ( row ) ? [ 'approve' as const ] : []),
2026-09-09 20:53:30 +08:00
...([ 'PENDING_REVIEW' , 'WAIT_MEASURE' ]. includes ( row . mainStatus ) && row . inboundId && can ( 'platform:firstleg:inbound:measure:view' ) ? [ 'measurement' as const ] : []),
...( can ( 'platform:order:note' ) ? [ 'note' as const ] : []),
2026-08-10 17:42:17 +08:00
]
}
function directUnifiedOrderActions ( row : UnifiedOrderResource ) {
return unifiedOrderSecondaryActions ( row ). slice ( 0 , 2 )
}
function moreUnifiedOrderActions ( row : UnifiedOrderResource ) {
return unifiedOrderSecondaryActions ( row ). slice ( 2 )
}
function moreUnifiedOrderActionsLabel ( row : UnifiedOrderResource ) {
2026-08-14 19:59:32 +08:00
return `更多订单操作: ${ String ( row . inboundCode || '' ). trim () || '当前入仓单' } `
2026-08-10 17:42:17 +08:00
}
function unifiedOrderActionLabel ( action : UnifiedOrderRowAction ) {
2026-09-10 11:17:45 +08:00
if ( action === 'note' ) return '备注'
if ( action === 'approve' ) return '通过'
return '测量'
2026-08-10 17:42:17 +08:00
}
2026-08-06 20:14:56 +08:00
function runMoreAction ( command : string | number | object , row : UnifiedOrderResource ) {
if ( command === 'note' ) openNoteDialog ( row )
if ( command === 'approve' ) openApproval ( row )
if ( command === 'measurement' ) openMeasurement ( row )
2026-08-06 13:56:04 +08:00
}
2026-09-10 11:17:45 +08:00
async function printSelectedCartonLabels () {
if ( ! selectedCartonLabelOrders . value . length || printingCartonLabels . value ) return
printingCartonLabels . value = true
try {
const blob = await downloadInboundCartonLabelsBatch ( selectedCartonLabelOrders . value . map (( row ) => ({
tenantId : String ( row . tenantId ),
inboundId : String ( row . inboundId ),
})))
const url = URL . createObjectURL ( blob )
const anchor = document . createElement ( 'a' )
anchor . href = url
anchor . download = `批量箱唛- ${ selectedCartonLabelOrders . value . length } 单.pdf`
document . body . appendChild ( anchor )
anchor . click ()
anchor . remove ()
URL . revokeObjectURL ( url )
const skipped = selectedOrders . value . length - selectedCartonLabelOrders . value . length
if ( skipped > 0 ) ElMessage . warning ( `箱唛已开始下载;已跳过 ${ skipped } 个尚未生成入库单的订单` )
else ElMessage . success ( '批量箱唛已生成并开始下载' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '批量箱唛生成失败,请稍后重试' ). message )
} finally {
printingCartonLabels . value = false
}
}
2026-08-06 13:56:04 +08:00
function closeDetail () {
2026-08-25 22:08:59 +08:00
releaseCargoImagePreviews ()
2026-08-06 13:56:04 +08:00
detail . value = null
detailError . value = null
void router . push ({ name : 'unified-orders' })
}
2026-09-01 21:09:39 +08:00
function returnToMeasurementReviews () {
if ( window . history . length > 1 ) router . back ()
else void router . push ({ name : 'inbound-measurement-reviews' })
}
async function resolveBusinessDetailTarget () {
const inboundCode = businessInboundCode . value
const serial = ++ detailAssociationRequestSerial
resolvedDetailTarget . value = null
detail . value = null
detailError . value = null
if ( ! inboundCode ) {
detailError . value = { message : '入仓码缺失,无法定位对应订单' }
return
}
detailLoading . value = true
try {
const association = await getPlatformInboundCodeAssociation ( inboundCode )
if ( serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode . value ) return
if ( ! association . orderId || ! association . customerId ) {
detailError . value = { message : `未找到入仓码 ${ inboundCode } 对应的订单,请返回测量审核列表重新打开` }
return
}
resolvedDetailTarget . value = { orderId : String ( association . orderId ), tenantId : String ( association . customerId ) }
} catch ( error ) {
if ( serial !== detailAssociationRequestSerial || inboundCode !== businessInboundCode . value ) return
detailError . value = getApiErrorDetails ( error , `入仓码 ${ inboundCode } 的订单关联加载失败,可能无权查看或目标已不存在` )
} finally {
if ( serial === detailAssociationRequestSerial && ! resolvedDetailTarget . value ) detailLoading . value = false
}
}
function exportFilterParameters () {
const activeFilters = appliedFilters . value
const range = activeFilters . createdRange
return {
tenantId : activeFilters . tenantId || undefined ,
mainStatus : selectedStatus . value || undefined ,
receivingWarehouseId : activeFilters . receivingWarehouseId || undefined ,
destinationWarehouseId : activeFilters . destinationWarehouseId || undefined ,
inboundCode : trimmed ( activeFilters . inboundCode ),
expectedArrivalDate : activeFilters . expectedArrivalDate || undefined ,
feeReviewStatus : activeFilters . feeReviewStatus || undefined ,
channelName : trimmed ( activeFilters . channelName ),
containerNo : trimmed ( activeFilters . containerNo ),
cabinetNo : trimmed ( activeFilters . cabinetNo ),
sailingDate : activeFilters . sailingDate || undefined ,
loaded : optionalBoolean ( activeFilters . loaded ),
fullContainer : optionalBoolean ( activeFilters . fullContainer ),
measurementReviewStatus : activeFilters . measurementReviewStatus || undefined ,
fbaCode : trimmed ( activeFilters . fbaCode ),
createdFrom : range . length === 2 ? ` ${ range [ 0 ] } T00:00:00` : undefined ,
createdTo : range . length === 2 ? ` ${ range [ 1 ] } T23:59:59` : undefined ,
}
}
async function exportOrders () {
if ( exportSubmitting . value ) return
const selectedOrderNos = selectedOrders . value . map (( row ) => row . orderNo ? . trim () || '' )
if ( selectedOrderNos . some (( orderNo ) => ! orderNo )) {
ElMessage . error ( '所选订单缺少订单号,请刷新列表后重试' )
return
}
exportSubmitting . value = true
try {
const task = await createPlatformExportTask ({
exportType : 'DOMESTIC_WAREHOUSE_ORDER_LIST' ,
format : 'XLSX' ,
taskName : selectedOrderNos . length ? `国内仓订单-已选 ${ selectedOrderNos . length } 票` : '国内仓订单-当前筛选' ,
parameters : selectedOrderNos . length
? { selectedOrderNos }
: exportFilterParameters (),
})
const openDownloadCenter = await showExportTaskCreatedDialog ({ taskName : task . taskName , taskNo : task . taskNo })
if ( openDownloadCenter ) await router . push ({ name : 'download-center' })
} catch ( error ) {
ElMessage . error ( getApiErrorMessage ( error , '订单导出任务创建失败' ))
} finally {
exportSubmitting . value = false
}
}
2026-08-07 14:47:11 +08:00
async function loadDetail ( force = false ) {
2026-08-06 13:56:04 +08:00
if ( ! orderId . value ) return
2026-08-07 20:07:37 +08:00
const tenantId = detailTenantId . value
2026-08-06 13:56:04 +08:00
if ( ! tenantId ) {
detailError . value = { message : '订单详情缺少客户标识,请从订单列表重新打开' }
return
}
2026-08-07 14:47:11 +08:00
const requestKey = ` ${ tenantId } : ${ orderId . value } `
if ( ! force && ( detailLoadingKey === requestKey || detail . value ? . summary . orderId === orderId . value && String ( detail . value . summary . tenantId ) === tenantId )) return
2026-08-07 20:07:37 +08:00
const serial = ++ detailRequestSerial
2026-08-07 14:47:11 +08:00
detailLoadingKey = requestKey
2026-08-17 18:00:42 +08:00
measurementRequestSerial += 1
2026-09-04 11:50:34 +08:00
measurementLoading . value = false
// 刷新详情时保留现有测量数据,避免请求期间列表被清空。
if ( ! force ) measurementDetail . value = null
2026-08-17 18:00:42 +08:00
measurementError . value = null
2026-08-07 14:47:11 +08:00
measurementLoadedKey . value = ''
2026-09-03 10:46:07 +08:00
measurementSectionModes . customer = 'ACTUAL'
measurementSectionModes . outer = 'ACTUAL'
measurementSectionModes . actual = 'ACTUAL'
if ( measurementChargeRulesLoadedKey . value !== tenantId ) {
measurementChargeRules . value = []
measurementChargeRulesLoadedKey . value = ''
}
2026-09-03 21:09:40 +08:00
measurementDimensionConfig . value = null
measurementDimensionConfigLoadedKey . value = ''
2026-08-17 18:00:42 +08:00
measurementPreviewOpen . value = false
2026-09-07 11:20:43 +08:00
holdRequestSerial += 1
holdDetail . value = null
holdError . value = null
holdLoadedKey . value = ''
holdLoading . value = false
2026-08-17 18:00:42 +08:00
releaseMeasurementPreview ()
2026-08-25 22:08:59 +08:00
releaseCargoImagePreviews ()
2026-08-06 13:56:04 +08:00
detailLoading . value = true
detailError . value = null
try {
2026-08-07 20:07:37 +08:00
const response = await getPlatformUnifiedOrder ( tenantId , orderId . value )
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
detail . value = response
2026-08-25 22:08:59 +08:00
await loadCargoImagePreviews ( response , serial )
2026-09-07 11:20:43 +08:00
if ( activeDetailTab . value === 'other' ) await loadHoldDetail ()
2026-09-01 21:09:39 +08:00
if ( requestedDetailTab . value === 'measurement' ) {
activeDetailTab . value = 'measurement'
await loadMeasurementDetails ()
}
2026-08-06 13:56:04 +08:00
} catch ( error ) {
2026-08-07 20:07:37 +08:00
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
2026-08-06 13:56:04 +08:00
detail . value = null
detailError . value = getApiErrorDetails ( error , '统一订单详情加载失败,请检查 C02 统一详情接口和订单权限' )
} finally {
2026-08-07 20:07:37 +08:00
if ( serial === detailRequestSerial ) detailLoading . value = false
if ( serial === detailRequestSerial && detailLoadingKey === requestKey ) detailLoadingKey = ''
2026-08-06 13:56:04 +08:00
}
}
watch ([ page , pageSize ], () => { void loadOrders () })
2026-09-01 21:09:39 +08:00
watch ( businessInboundCode , ( value ) => {
detailAssociationRequestSerial += 1
resolvedDetailTarget . value = null
if ( value ) void resolveBusinessDetailTarget ()
}, { immediate : true })
2026-08-07 20:07:37 +08:00
watch ( detailRouteKey , ( value ) => {
2026-08-06 19:03:13 +08:00
if ( value ) {
2026-09-01 21:09:39 +08:00
activeDetailTab . value = requestedDetailTab . value
2026-08-07 20:07:37 +08:00
detail . value = null
detailError . value = null
2026-08-06 19:03:13 +08:00
void loadDetail ()
2026-09-01 21:09:39 +08:00
} else if ( ! businessInboundCode . value ) {
2026-08-07 20:07:37 +08:00
detailRequestSerial += 1
2026-08-31 10:48:23 +08:00
subsequentFulfillmentRequestSerial += 1
2026-08-07 20:07:37 +08:00
detail . value = null
detailError . value = null
detailLoading . value = false
2026-08-06 19:03:13 +08:00
}
2026-08-06 13:56:04 +08:00
}, { immediate : true })
2026-08-17 18:00:42 +08:00
watch ( measurementPreviewOpen , ( open ) => {
if ( ! open ) releaseMeasurementPreview ()
})
2026-08-06 13:56:04 +08:00
onMounted (() => {
void loadCustomers ()
2026-08-06 15:32:02 +08:00
void loadWarehouses ()
2026-09-01 21:09:39 +08:00
if ( ! isDetailRoute . value ) void loadOrders ()
2026-08-06 13:56:04 +08:00
})
2026-08-17 18:00:42 +08:00
onBeforeUnmount (() => {
2026-09-01 21:09:39 +08:00
detailAssociationRequestSerial += 1
2026-08-17 18:00:42 +08:00
measurementRequestSerial += 1
2026-09-07 11:20:43 +08:00
holdRequestSerial += 1
2026-08-17 18:00:42 +08:00
releaseMeasurementPreview ()
2026-08-25 22:08:59 +08:00
releaseCargoImagePreviews ()
2026-08-17 18:00:42 +08:00
})
2026-08-06 13:56:04 +08:00
</ script >
< template >
2026-08-10 15:36:53 +08:00
< AdminPage >
2026-09-14 17:54:21 +08:00
< section class = "business-page unified-orders-page admin-density" >
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "feeConfirmOpen" title="编辑计费数据" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee" >
2026-09-09 17:58:23 +08:00
< el-alert class = "fee-confirm-price-alert" title = "价格表单价仅作为参考;本次对客单价可人工修改,客户将一次确认计费量、单价和费用合计。" type = "info" :closable = "false" show -icon />
2026-09-03 10:46:07 +08:00
< el-form label -position =" top " class = "fee-confirm-form" >
< el-form-item label = "入仓码" >< el-input : model -value =" detail ? .summary.inboundCode || '—'" disabled /></ el-form-item >
< el-form-item label = "渠道" >< el-input : model -value =" detail ? .summary.channelName || detail ? .summary.channelCode || '—'" disabled /></ el-form-item >
2026-09-03 19:29:38 +08:00
< el-form-item label = "计费毛重" >< div class = "fee-confirm-number-wrap" >< el-input-number v-model = "feeConfirmWeight" :min="0" :precision="3" :controls="false" class="fee-confirm-number" placeholder="请输入计费毛重" /><span class="fee-confirm-unit" > kg </ span ></ div > </ el -form-item >
< el-form-item label = "计费体积" >< div class = "fee-confirm-number-wrap" >< el-input-number v-model = "feeConfirmVolume" :min="0" :precision="6" :controls="false" class="fee-confirm-number" placeholder="请输入计费体积" /><span class="fee-confirm-unit" > m ³ </ span ></ div > </ el -form-item >
2026-09-09 17:58:23 +08:00
< el-form-item label = "价格表参考单价" >< div class = "fee-confirm-number-wrap" >< el-input-number : model -value =" feeConfirmReference ? .unitPrice == null ? undefined : Number ( feeConfirmReference.unitPrice )" :precision = "4" :controls = "false" disabled class = "fee-confirm-number" placeholder = "正在读取参考单价" />< span class = "fee-confirm-unit" >{{ feeConfirmPriceUnit }}</ span ></ div ></ el-form-item >
< el-form-item label = "本次对客单价" >< div class = "fee-confirm-number-wrap" >< el-input-number v-model = "feeConfirmUnitPrice" :min="0" :precision="4" :controls="false" :disabled="feeConfirmReferenceLoading" class="fee-confirm-number" placeholder="请输入对客单价" /><span class="fee-confirm-unit" >{{ feeConfirmPriceUnit }}</ span ></ div > </ el -form-item >
< el-form-item v-if = "feeConfirmPriceChanged" label="单价调整原因" class="fee-confirm-price-reason"><el-input v-model="feeConfirmPriceReason" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请说明本次人工改单价的原因" /></ el-form-item >
2026-09-03 10:46:07 +08:00
</ el-form >
2026-09-09 17:58:23 +08:00
< dl v-if = "feeConfirmReference" class="fee-confirm-price-summary" >
< div >< dt > 参考计费量 </ dt >< dd >{{ formatNumber ( Number ( feeConfirmReference . chargeableQuantity ), feeConfirmReference . roundingScale ) }} {{ feeConfirmReference . chargeUnit }}</ dd ></ div >
< div >< dt > 固定附加费 </ dt >< dd >{{ feeConfirmMoney ( Number ( feeConfirmReference . additionalAmount || 0 ), feeConfirmReference . currency ) }}</ dd ></ div >
< div >< dt > 按当前单价预计合计 </ dt >< dd >{{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney ( feeConfirmEstimatedTotal , feeConfirmReference . currency ) }}</ dd ></ div >
</ dl >
2026-09-03 10:46:07 +08:00
< h4 class = "fee-confirm-reference-title" > 参考数据 </ h4 >
< el-table :data = "feeConfirmRows" border size = "small" >
< el-table-column prop = "source" label = "数据来源" />
< el-table-column prop = "count" label = "件数" />
2026-09-14 12:00:54 +08:00
< el-table-column label = "总毛重(T.K.GW)" align = "right" >< template # default = "{ row }" >{{ row . weight == null ? '—' : formatNumber ( row . weight , 2 ) }}</ template ></ el-table-column >
2026-09-03 10:46:07 +08:00
< el-table-column label = "总体积(m³)" >< template # default = "{ row }" >{{ row . volume == null ? '—' : formatNumber ( row . volume , 4 ) }}</ template ></ el-table-column >
< el-table-column label = "总材积重" >< template # default = "{ row }" >{{ row . volume == null ? '—' : formatNumber ( Number ( row . volume ) * 167 , 2 ) }}</ template ></ el-table-column >
</ el-table >
</ AppDialog >
2026-09-01 21:09:39 +08:00
< template v-if = "isDetailRoute" >
2026-08-10 15:36:53 +08:00
< DetailPageLayout ariaLabel = "统一订单详情" >
2026-09-14 20:21:02 +08:00
< header class = "order-detail-header density-detail-header" >
< div class = "order-detail-heading density-detail-heading" >
2026-09-14 18:34:34 +08:00
< el-button link class = "back-button density-back" @click ="fromMeasurementReview ? returnToMeasurementReviews() : closeDetail()" >< ArrowLeft :size = "16" />{{ fromMeasurementReview ? '返回测量审核列表' : '返回列表' }}</ el-button >
2026-08-06 19:03:13 +08:00
< div class = "order-title" >
2026-09-14 20:21:02 +08:00
< h1 class = "density-detail-title" >{{ detail ? . summary . inboundCode || '入仓码暂缺' }}</ h1 >
2026-08-06 19:03:13 +08:00
</ div >
< StatusBadge v-if = "detail" :status="statusLabel(detail.summary.mainStatus)" />
</ div >
2026-08-06 20:14:56 +08:00
< div v-if = "detail && canApprove(detail.summary)" class="detail-header-actions" >
2026-08-06 19:03:13 +08:00
< el-button v-if = "canApprove(detail.summary)" type="success" plain :loading="approvalLoadingId === String(detail.summary.orderId)" @click="openApproval(detail.summary)"><CircleCheck :size="15" /> 审核通过 </ el-button >
</ div >
</ header >
2026-08-06 15:32:02 +08:00
2026-09-01 21:09:39 +08:00
< LoadErrorState v-if = "detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)" >
2026-08-06 19:03:13 +08:00
< template # details >< ApiErrorMeta :details = "detailError" /></ template >
2026-09-01 21:09:39 +08:00
< template v-if = "fromMeasurementReview" #actions><el-button type="primary" plain :loading="detailLoading" @click="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true)">重新加载</el-button><el-button @click="returnToMeasurementReviews" > 返回测量审核列表 </ el -button ></ template >
2026-08-06 19:03:13 +08:00
</ LoadErrorState >
< div v-else v-loading = "detailLoading" class="order-detail-content" >
< template v-if = "detail" >
2026-09-14 20:21:02 +08:00
< dl class = "detail-overview density-detail-overview" aria -label =" 订单概览 " >
2026-09-14 12:00:54 +08:00
< div >< dt > 客户 </ dt >< dd >{{ customerName ( detail . summary ) }}</ dd ></ div >
< div >< dt > 渠道 </ dt >< dd >{{ detail . summary . channelName || '未配置渠道' }}</ dd ></ div >
< div >< dt > 总箱数 </ dt >< dd >{{ detail . summary . totalPackageCount ?? '—' }} 箱 < small > ( {{ detail . order . cargoes . length }} 条货品 ) </ small ></ dd ></ div >
< div >< dt > 实测毛重 </ dt >< dd >{{ formatNumber ( detail . summary . measurement ? . actualGrossWeightKg ) }} kg </ dd ></ div >
< div >< dt > 实测体积 </ dt >< dd >{{ formatNumber ( detail . summary . measurement ? . actualVolumeCbm , 3 ) }} m³ </ dd ></ div >
< div >< dt > 测量审核 </ dt >< dd >{{ measurementStatusLabel ( detail . summary . measurement ? . reviewStatus ) }}</ dd ></ div >
< div >< dt > 最近更新 </ dt >< dd >{{ formatDate ( detail . summary . updatedAt ) }}</ dd ></ div >
</ dl >
2026-08-06 19:03:13 +08:00
2026-09-14 17:54:21 +08:00
< div class = "detail-tabs density-tabs" role = "tablist" aria -label =" 订单详情模块 " >
< button v-for = "tab in detailTabs" :id="`order-detail-tab-${tab.key}`" :key="tab.key" type="button" role="tab" :class="{ active: activeDetailTab === tab.key }" :aria-selected="activeDetailTab === tab.key" :aria-controls="`order-detail-panel-${tab.key}`" :tabindex="activeDetailTab === tab.key ? 0 : -1" @click="selectDetailTab(tab.key)"><span class="density-tab-icon" aria-hidden="true" >{{ tab.icon }}</ span >< span >{{ tab . label }}</ span ></ button >
2026-08-06 19:03:13 +08:00
</ div >
< section :id = "`order-detail-panel-${activeDetailTab}`" class = "detail-panel" role = "tabpanel" :aria-labelledby = "`order-detail-tab-${activeDetailTab}`" tabindex = "0" >
< template v-if = "activeDetailTab === 'basic'" >
2026-09-14 17:54:21 +08:00
< dl class = "order-info-grid density-fields" >
2026-08-06 19:03:13 +08:00
< div >< dt > 公司 </ dt >< dd >{{ customerName ( detail . summary ) }}</ dd ></ div >
2026-08-11 19:35:28 +08:00
< div >< dt > 运输方式 </ dt >< dd >{{ firstLegTransportModeLabel ( detail . order . order ? . transportMode ) }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 当前状态 </ dt >< dd >< StatusBadge :status = "statusLabel(detail.summary.mainStatus)" /></ dd ></ div >
2026-08-11 19:35:28 +08:00
< div >< dt > 内部订单状态 </ dt >< dd >{{ orderStatusLabel ( detail . summary . internalOrderStatus || detail . summary . mainStatus ) }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 平台收货仓 </ dt >< dd >{{ detail . summary . receivingWarehouseName || '—' }}</ dd ></ div >
2026-09-14 12:00:54 +08:00
< div class = "wide" >< dt > 收货仓地址 </ dt >< dd >{{ detail . order . order ? . receivingWarehouseAddress || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 目的仓 </ dt >< dd >{{ detail . summary . destinationWarehouseName || '—' }}</ dd ></ div >
< div >< dt > 目的国家 </ dt >< dd >{{ detail . order . order ? . destinationCountryCode || '—' }}</ dd ></ div >
2026-09-14 12:00:54 +08:00
< div class = "wide" >< dt > 目的仓地址 </ dt >< dd >{{ detail . summary . destinationAddress || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 目的仓邮编 </ dt >< dd >{{ detail . summary . destinationPostalCode || '—' }}</ dd ></ div >
< div >< dt > 目的仓联系人 </ dt >< dd >{{ detail . summary . destinationContact || '—' }}</ dd ></ div >
< div >< dt > 目的仓联系人电话 </ dt >< dd >{{ detail . summary . destinationContactPhone || '—' }}</ dd ></ div >
2026-09-02 10:24:07 +08:00
< div >< dt > 渠道 </ dt >< dd >{{ detail . summary . channelName || '—' }} < el-tag v-if = "detail.summary.channelChanged" size="small" type="warning" > 曾变更渠道 </ el -tag ></ dd ></ div >
2026-08-11 19:35:28 +08:00
< div >< dt > 业务类型 </ dt >< dd >{{ firstLegBusinessTypeLabel ( detail . summary . businessType ) }}</ dd ></ div >
< div >< dt > 贸易条款 </ dt >< dd >{{ firstLegTradeTermLabel ( detail . order . order ? . tradeTerm ) }}</ dd ></ div >
< div >< dt > 买单 / 退税 </ dt >< dd >{{ firstLegTaxModeLabel ( detail . order . order ? . taxMode ) }}</ dd ></ div >
< div >< dt > 费用审核状态 </ dt >< dd >{{ feeReviewLabel ( detail . summary . feeReviewStatus ) }}</ dd ></ div >
< div >< dt > 提货方式 </ dt >< dd >{{ firstLegPickupMethodLabel ( detail . order . order ? . pickupMethod ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 提货时间 </ dt >< dd >{{ formatDate ( detail . summary . pickupAt ) }}</ dd ></ div >
< div >< dt > 到仓时间 </ dt >< dd >{{ detail . order . order ? . receivingTime || '—' }}</ dd ></ div >
< div >< dt > 货物到仓时间 </ dt >< dd >{{ formatDate ( detail . summary . expectedArrivalAt || detail . summary . inboundCompletedAt ) }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 预计出运 </ dt >< dd >{{ formatDate ( detail . summary . expectedShipDate ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 预计到达时间 </ dt >< dd >{{ formatDate ( detail . summary . expectedArrivalAt ) }}</ dd ></ div >
2026-08-12 17:29:35 +08:00
< div >< dt > 真实集装箱号 </ dt >< dd >{{ detail . summary . transport ? . containerNo || '—' }}</ dd ></ div >
< div >< dt > 内部柜次号 </ dt >< dd >{{ detail . summary . transport ? . cabinetNo || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 船名 / 航次 </ dt >< dd >{{ detail . summary . transport ? . vesselName || '—' }} / {{ detail . summary . transport ? . voyageNo || '—' }}</ dd ></ div >
2026-08-18 11:33:01 +08:00
< div >< dt > 开船时间 </ dt >< dd >{{ formatEffectiveTransportTime ( detail . summary . transport ? . effectiveSailingAt , detail . summary . transport ? . effectiveSailingAtEstimated ) }}</ dd ></ div >
< div >< dt > 到港时间 </ dt >< dd >{{ formatEffectiveTransportTime ( detail . summary . transport ? . effectivePortArrivalAt , detail . summary . transport ? . effectivePortArrivalAtEstimated ) }}</ dd ></ div >
2026-08-11 19:35:28 +08:00
< div >< dt > 配载状态 </ dt >< dd >{{ unifiedOrderLoadingStatusLabel ( detail . summary . transport ? . loadingStatus ) }}</ dd ></ div >
< div >< dt > 目的仓类型 </ dt >< dd >{{ firstLegDestinationTypeLabel ( detail . order . order ? . destinationType ) }}</ dd ></ div >
< div >< dt > 是否整柜 </ dt >< dd >{{ firstLegContainerModeLabel ( detail . order . order ? . containerMode ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 商检验 </ dt >< dd >{{ displayBoolean ( detail . order . order ? . customsInspection ) }}</ dd ></ div >
< div >< dt > 总箱数 </ dt >< dd >{{ detail . summary . totalPackageCount ?? '—' }} 箱 </ dd ></ div >
2026-08-22 10:02:13 +08:00
< div >< dt > 客户申报毛重 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'grossWeightKg' )) }} kg </ dd ></ div >
< div >< dt > 客户申报净重 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'netWeightKg' )) }} kg </ dd ></ div >
< div >< dt > 客户申报总体积 </ dt >< dd >{{ formatNumber ( cargoTotal ( detail . order . cargoes , 'volumeCbm' ), 3 ) }} m³ </ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 计费毛重 </ dt >< dd >{{ formatNumber ( detail . summary . measurement ? . chargeableWeightKg ) }} kg </ dd ></ div >
< div >< dt > 计费体积 </ dt >< dd >{{ formatNumber ( detail . summary . chargeableVolumeCbm , 3 ) }} m³ </ dd ></ div >
< div >< dt > 总材积重 </ dt >< dd >{{ formatNumber ( detail . summary . volumetricWeightKg ) }} kg </ dd ></ div >
< div >< dt > 总材积量 </ dt >< dd >{{ formatNumber ( detail . summary . volumetricWeightKg ) }}</ dd ></ div >
2026-08-11 20:11:00 +08:00
< div >< dt > 库位位置 </ dt >< dd >{{ detail . summary . locationCodes || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
< div >< dt > 创建时间 </ dt >< dd >{{ formatDate ( detail . summary . createdAt ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 入库完成时间 </ dt >< dd >{{ formatDate ( detail . summary . inboundCompletedAt ) }}</ dd ></ div >
< div >< dt > 测量完成时间 </ dt >< dd >{{ formatDate ( detail . summary . measurementCompletedAt || detail . summary . measurement ? . completedAt ) }}</ dd ></ div >
< div class = "wide" >< dt > 提货联系人 </ dt >< dd >{{ partyValue ( detail . order . parties , 'SENDER' , 'contactName' ) }} · {{ detail . summary . pickupContactPhone || partyValue ( detail . order . parties , 'SENDER' , 'phone' ) }} · {{ detail . summary . pickupContactEmail || partyValue ( detail . order . parties , 'SENDER' , 'email' ) }}</ dd ></ div >
< div class = "wide" >< dt > 提货地址 </ dt >< dd >{{ detail . summary . pickupAddress || partyAddress ( detail . order . parties , 'SENDER' ) }} · {{ detail . summary . pickupPostalCode || partyValue ( detail . order . parties , 'SENDER' , 'postalCode' ) }}</ dd ></ div >
2026-08-07 14:47:11 +08:00
< div class = "wide" >< dt > 目的地联系人 </ dt >< dd >{{ partyValue ( detail . order . parties , 'RECEIVER' , 'contactName' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'phone' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'email' ) }}</ dd ></ div >
< div >< dt > 目的仓联系人邮箱 </ dt >< dd >{{ partyValue ( detail . order . parties , 'RECEIVER' , 'email' ) }}</ dd ></ div >
< div class = "wide" >< dt > 目的地地址 </ dt >< dd >{{ partyAddress ( detail . order . parties , 'RECEIVER' ) }} · {{ partyValue ( detail . order . parties , 'RECEIVER' , 'postalCode' ) }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div class = "wide" >< dt > 备注 </ dt >< dd >{{ detail . order . order ? . customerRemark || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 关联文件 </ dt >< dd >{{ detail . summary . associatedFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 唛头文件 </ dt >< dd >{{ detail . summary . markFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > PC 端下单文件 </ dt >< dd >{{ detail . summary . pcOrderFiles ? . join ( '、' ) || '—' }}</ dd ></ div >
< div >< dt > 快递公司 </ dt >< dd >{{ detail . summary . expressCompany || detail . order . order ? . expressCompany || '—' }}</ dd ></ div >
< div >< dt > 快递单号 </ dt >< dd >{{ detail . summary . expressTrackingNo || detail . order . order ? . expressTrackingNo || '—' }}</ dd ></ div >
2026-08-27 21:48:15 +08:00
< div class = "wide" >< dt > 客服内部备注 </ dt >< dd >{{ detail . summary . internalRemark || detail . summary . latestInternalNote || '—' }}</ dd ></ div >
2026-08-07 10:45:19 +08:00
< div >< dt > 创建人 </ dt >< dd >{{ detail . summary . createdBy || '—' }}</ dd ></ div >
< div >< dt > 下一步 </ dt >< dd >{{ canApprove ( detail . summary ) ? '审核订单' : nextActionBoundary ( detail . summary ) || '—' }}</ dd ></ div >
2026-08-06 19:03:13 +08:00
</ dl >
</ template >
< template v-else-if = "activeDetailTab === 'cargo'" >
2026-09-14 12:00:54 +08:00
< div class = "panel-title" >< p > 共 {{ detail . order . cargoes . length }} 条货品 </ p >< el-button v-if = "canEditDeclaration" type="primary" size="small" @click="declarationEditorOpen = true" > 编辑客户申报资料 </ el -button ></ div >
2026-08-06 19:03:13 +08:00
< div class = "detail-table-wrap" >
2026-08-12 18:52:22 +08:00
< el-table :data = "detail.order.cargoes" border stripe empty -text =" 暂无货品信息 " >
2026-08-07 14:47:11 +08:00
< el-table-column prop = "itemNo" label = "#" width = "64" />
< el-table-column label = "报关HS_CODE" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'hsCode' ) }}</ template ></ el-table-column >
2026-09-14 12:00:54 +08:00
< el-table-column label = "货品图片" width = "110" >< template # default = "{ row }" >< el-image v-if = "cargoImage(row as Record<string, unknown>)" :src="cargoImage(row as Record<string, unknown>)" :preview-src-list="[cargoImage(row as Record<string, unknown>)]" preview-teleported fit="contain" class="cargo-image" />< span v-else > — </ span ></ template > </ el -table-column >
2026-08-07 14:47:11 +08:00
< el-table-column label = "SKU NO." min -width =" 150 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'skuNo' ) }}</ template ></ el-table-column >
< el-table-column label = "中文品名" min -width =" 170 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'cargoName' ) }}</ template ></ el-table-column >
< el-table-column label = "英文品名" min -width =" 170 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'cargoNameEn' ) }}</ template ></ el-table-column >
< el-table-column label = "清关HS_CODE" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'customsHsCode' ) }}</ template ></ el-table-column >
2026-09-14 12:00:54 +08:00
< el-table-column label = "箱规长(cm)" align = "right" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'lengthCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规宽(cm)" align = "right" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'widthCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规高(cm)" align = "right" min -width =" 110 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'heightCm' ) }}</ template ></ el-table-column >
< el-table-column label = "箱规周长(cm)" align = "right" min -width =" 120 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'circumferenceCm' ) }}</ template ></ el-table-column >
< el-table-column label = "每箱产品数量" align = "right" min -width =" 130 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'unitsPerPackage' ) }}</ template ></ el-table-column >
< el-table-column label = "箱数" align = "right" width = "90" >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'packageCount' ) }}</ template ></ el-table-column >
< el-table-column label = "总毛重(T.KGW)" align = "right" min -width =" 140 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'grossWeightKg' ) }}</ template ></ el-table-column >
< el-table-column label = "总净重(T.KNW)" align = "right" min -width =" 140 " >< template # default = "{ row }" >{{ cargoValue ( row as Record < string , unknown >, 'netWeightKg' ) }}</ template ></ el-table-column >
2026-08-06 19:03:13 +08:00
</ el-table >
</ div >
</ template >
< template v-else-if = "activeDetailTab === 'fees'" >
2026-08-18 16:09:43 +08:00
< el-empty v-if = "!normalizedInboundId" description="订单尚未生成入库单,暂无入仓费用" :image-size="64" />
< template v-else >
2026-08-17 19:13:25 +08:00
< dl class = "fee-overview" aria -label =" 费用汇总 " >
< div >< dt > 对客费用合计 </ dt >< dd >{{ feeMoney ( customerFeeTotal ) }}</ dd >< small > 已发布头程费用 + 已审核其他应收 </ small ></ div >
< div >< dt > 已审核其他应付 </ dt >< dd >{{ feeMoney ( otherFeeSummary . approvedPayable ) }}</ dd >< small > 单列展示 , 不计入对客费用 </ small ></ div >
</ dl >
2026-08-17 18:32:41 +08:00
< InboundFeePanel
2026-09-04 11:50:34 +08:00
:key = "`fee-${normalizedInboundId}-${feePanelRevision}`"
2026-08-17 18:32:41 +08:00
:customer-id = "String(detail.summary.tenantId)"
2026-08-18 16:09:43 +08:00
:inbound-id = "normalizedInboundId"
2026-08-17 19:13:25 +08:00
compact
2026-09-04 11:50:34 +08:00
display -only
2026-08-17 19:13:25 +08:00
@loaded ="automaticFee = $event"
2026-08-17 18:32:41 +08:00
/>
2026-08-17 18:43:12 +08:00
< h3 class = "fee-section-title fee-section-title--secondary" > 其他费用明细 </ h3 >
2026-08-12 16:31:46 +08:00
< InboundFeeDetailManager
:account-id = "String(detail.summary.tenantId)"
2026-08-18 16:09:43 +08:00
:inbound-id = "normalizedInboundId"
2026-09-08 17:38:09 +08:00
:destination-type = "detail.summary.destinationType"
2026-08-17 19:13:25 +08:00
@summary ="otherFeeSummary = $event"
2026-08-12 16:31:46 +08:00
@changed ="loadDetail(true)"
/>
2026-08-18 16:09:43 +08:00
</ template >
2026-08-06 19:03:13 +08:00
</ template >
< template v-else-if = "activeDetailTab === 'history'" >
2026-09-14 12:00:54 +08:00
< div class = "panel-title" >< p > 共 {{ detail . timeline . length }} 条记录 </ p ></ div >
< el-table :data = "detail.timeline" class = "history-table" border stripe empty -text =" 暂无状态历史 " aria -label =" 状态历史 " >
< el-table-column label = "时间" width = "172" >< template # default = "{ row }" >{{ formatDate ( row . occurredAt ) }}</ template ></ el-table-column >
< el-table-column label = "事件" min -width =" 180 " >< template # default = "{ row }" >{{ historyActionLabel ( row . action || row . eventType ) }}</ template ></ el-table-column >
< el-table-column label = "状态" width = "166" >< template # default = "{ row }" >< StatusBadge :status = "statusLabel(row.mainStatus)" /></ template ></ el-table-column >
< el-table-column label = "操作人" min -width =" 160 " >< template # default = "{ row }" >{{ row . operatorName || '系统' }}</ template ></ el-table-column >
< el-table-column label = "备注" min -width =" 300 " >< template # default = "{ row }" >{{ row . remark || '—' }}</ template ></ el-table-column >
</ el-table >
2026-08-06 19:03:13 +08:00
</ template >
2026-08-11 09:16:58 +08:00
< template v-else-if = "activeDetailTab === 'fulfillment'" >
2026-09-14 12:00:54 +08:00
< div class = "panel-title" >< p > 根单 、 已完成分片与当前余量 </ p ></ div >
2026-08-14 19:59:32 +08:00
< OrderFulfillmentTreePanel :tenant-id = "String(detail.summary.tenantId)" :order-id = "String(detail.summary.orderId)" :inbound-code = "detail.summary.inboundCode" />
2026-08-31 10:48:23 +08:00
< section v-loading = "subsequentFulfillmentLoading" class="subsequent-fulfillment" aria-label="后续履约数据" >
< h3 > 后续履约数据 </ h3 >
< LoadErrorState v-if = "loadingAssociationError" title="装柜与PDA履约数据加载失败" :message="loadingAssociationError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="loadingAssociationError" /></ template ></ LoadErrorState >
< LoadErrorState v-if = "overseasRelationshipError" title="尾程与库存履约数据加载失败" :message="overseasRelationshipError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="overseasRelationshipError" /></ template ></ LoadErrorState >
< dl v-if = "hasLoadingAssociation" class="fulfillment-facts" >
< div >< dt > 装柜计划 </ dt >< dd >{{ loadingAssociation ? . loadingPlanNo || '—' }}</ dd ></ div >
< div >< dt > 柜号 </ dt >< dd >{{ loadingAssociation ? . containerNo || loadingAssociation ? . cabinetNo || '—' }}</ dd ></ div >
< div >< dt > 装柜状态 </ dt >< dd >{{ loadingAssociation ? . loadingStatus || '—' }}</ dd ></ div >
< div >< dt > PDA状态 </ dt >< dd >{{ loadingAssociation ? . pdaTaskStatus || '—' }}</ dd ></ div >
</ dl >
< dl v-if = "hasOverseasRelationship" class="fulfillment-facts" >
< div >< dt > 海外仓 </ dt >< dd >{{ overseasRelationship ? . overseasWarehouse ? . warehouseName || '尚未分配海外仓' }}</ dd ></ div >
< div >< dt > 海外收货状态 </ dt >< dd >{{ overseasRelationship ? . overseasReceivingStatus || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 尾程订单 </ dt >< dd >{{ overseasRelationship ? . taillegOrders . map (( item ) => item . orderNo ). join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 入库计划 </ dt >< dd >{{ overseasRelationship ? . inboundPlans . map (( item ) => item . planNo ). join ( '、' ) || '—' }}</ dd ></ div >
< div class = "wide" >< dt > 托盘码 </ dt >< dd >{{ overseasRelationship ? . palletCodes . join ( '、' ) || '—' }}</ dd ></ div >
< div >< dt > 库存SKU </ dt >< dd >{{ overseasRelationship ? . inventory . length || 0 }} 个 </ dd ></ div >
</ dl >
< p v-if = "!subsequentFulfillmentLoading && !loadingAssociationError && !overseasRelationshipError && !hasSubsequentFulfillment" class="fulfillment-empty" > 暂无后续履约数据 </ p >
</ section >
2026-08-11 09:16:58 +08:00
</ template >
2026-09-14 17:52:00 +08:00
< template v-else-if = "activeDetailTab === 'dropship-print'" >
< el-empty v-if = "!normalizedInboundId" description="订单尚未生成入库单,暂无托盘码可打印" :image-size="64" />
< el-alert v-else-if = "!can('platform:firstleg:inbound:list')" title="当前账号缺少入库单查看权限,无法打印托盘码" type="warning" :closable="false" show -icon />
< DropshipPalletPrintPanel
v-else
:customer-id = "String(detail.summary.tenantId)"
:inbound-id = "normalizedInboundId"
:inbound-code = "detail.summary.inboundCode"
/>
</ template >
2026-08-10 09:12:06 +08:00
< template v-else-if = "activeDetailTab === 'measurement'" >
2026-09-03 10:46:07 +08:00
< div class = "panel-title" >
2026-09-14 12:00:54 +08:00
< p >{{ detail . summary . measurement ? . completedAt ? `测量完成于 ${ formatDate ( detail . summary . measurement . completedAt ) } ` : '尚未完成测量' }}</ p >
2026-09-03 10:46:07 +08:00
< div class = "measurement-title-actions" >
< el-button
2026-09-14 16:51:35 +08:00
v-if = "can('firstleg:inbound:confirm-send-fee') && automaticFee?.feeStatus !== 'SENT' && !['LOADED', 'DELIVERED'].includes(detail.summary.mainStatus)"
2026-09-03 10:46:07 +08:00
type = "primary"
:loading = "confirmFeeSending"
2026-09-03 21:09:40 +08:00
title = "确认并发送费用"
2026-09-03 10:46:07 +08:00
data -testid =" confirm -send-fee-button "
@click ="openFeeConfirmDialog"
2026-09-04 11:50:34 +08:00
>< Send :size = "15" /> 确认计费重发送 </ el-button >
2026-09-03 10:46:07 +08:00
</ div >
</ div >
2026-08-17 18:00:42 +08:00
< el-alert v-if = "!can('platform:firstleg:inbound:measure:view')" title="当前账号缺少测量查看权限" type="warning" :closable="false" show -icon />
< LoadErrorState v-else-if = "measurementError" title="测量数据暂时无法加载" :message="measurementError.message" :loading="measurementLoading" @retry="loadMeasurementDetails" >
< template # details >< ApiErrorMeta :details = "measurementError" /></ template >
2026-09-01 21:09:39 +08:00
< template v-if = "fromMeasurementReview" #actions><el-button type="primary" plain :loading="measurementLoading" @click="loadMeasurementDetails">重新加载</el-button><el-button @click="returnToMeasurementReviews" > 返回测量审核列表 </ el -button ></ template >
2026-08-17 18:00:42 +08:00
</ LoadErrorState >
< template v-else >
< section class = "measurement-section" >
2026-09-14 12:00:54 +08:00
< div class = "measurement-section-title" >< button type = "button" class = "measurement-section-toggle" :aria-expanded = "!measurementCollapsed.customer" @click ="toggleMeasurementSection('customer')" >< h3 > 一 、 客户下单数据 </ h3 >< span v-if = "measurementCollapsed.customer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(customerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(customerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(customerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(customerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(customerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.customer" aria-label="客户下单数据计算方式" @change="updateMeasurementSectionMode('customer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></ el-select ></ div >
2026-08-07 14:47:11 +08:00
< div v-if = "!measurementCollapsed.customer" >
2026-09-03 10:46:07 +08:00
< div class = "detail-table-wrap" >< el-table :data = "customerMeasurementRows" border stripe empty -text =" 暂无客户下单数据 " >< el-table-column v-for = "column in customerMeasurementColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key ) }}</ template ></ template ></ el-table-column ></ el-table ></ div >
2026-09-14 12:00:54 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementTotals ( customerMeasurementRows ). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals ( customerMeasurementRows ). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals ( customerMeasurementRows ). volume , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementTotals ( customerMeasurementRows ). volumetric ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementTotals ( customerMeasurementRows ). weightVolume , 6 ) }}</ span ></ div >
2026-08-07 14:47:11 +08:00
</ div >
</ section >
2026-08-17 18:00:42 +08:00
< section class = "measurement-section" >
2026-09-14 12:00:54 +08:00
< div class = "measurement-section-title" >< button type = "button" class = "measurement-section-toggle" :aria-expanded = "!measurementCollapsed.outer" @click ="toggleMeasurementSection('outer')" >< h3 > 二 、 外箱规数据 </ h3 >< span v-if = "measurementCollapsed.outer" class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></ button ></ div >
2026-08-07 14:47:11 +08:00
< div v-if = "!measurementCollapsed.outer" >
2026-09-03 10:46:07 +08:00
< div v-loading = "measurementLoading" class="detail-table-wrap"><el-table :data="outerMeasurementRows" border stripe empty-text="暂无外箱规数据"><el-table-column label="#" prop="itemNo" min-width="70" /><el-table-column v-for="column in outerColumns.filter((item) => item.key !== 'itemNo')" :key="column.key" :label="column.label" :prop="column.key" min-width="150"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key ) }}</ template ></ template ></ el-table-column ></ el-table ></ div >
2026-09-14 12:00:54 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementTotals ( outerMeasurementRows ). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals ( outerMeasurementRows ). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals ( outerMeasurementRows ). volume , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementTotals ( outerMeasurementRows ). volumetric ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementTotals ( outerMeasurementRows ). weightVolume , 6 ) }}</ span ></ div >
2026-08-07 14:47:11 +08:00
</ div >
</ section >
2026-09-03 10:46:07 +08:00
< div class = "measurement-rule-selector" >
< span class = "measurement-rule-selector__label" > 箱规计算规则选择 </ span >
2026-09-10 11:02:52 +08:00
< el-tooltip :content = "measurementStrategyLockedHint" :disabled = "measurementStrategyEditable" >< span >< el-select class = "measurement-rule-selector__control" :model-value = "measurementSectionModes.outer" aria -label =" 外箱规数据计算方式 " :loading = "Boolean(measurementStrategySavingId)" : disabled = "Boolean(measurementStrategySavingId) || !measurementStrategyEditable" @change ="selectAndSaveOuterMeasurementMode($event)" >
2026-09-03 10:46:07 +08:00
< el-option label = "实测原尺寸(不调整)" value = "ACTUAL" />
< el-option label = "箱规尺寸" value = "ADD" />
< el-option label = "箱规系数" value = "MULTIPLY" :disabled = "multiplyModeDisabled" />
2026-09-09 20:36:49 +08:00
</ el-select ></ span ></ el-tooltip >
2026-09-10 11:02:52 +08:00
< span class = "measurement-rule-selector__hint" >{{ measurementStrategyEditable ? '规则参数来源:客户管理' : measurementStrategyLockedHint }}</ span >
2026-09-03 10:46:07 +08:00
</ div >
2026-08-07 14:47:11 +08:00
< section class = "measurement-section" >
2026-09-14 12:00:54 +08:00
< div class = "measurement-section-title" >< button type = "button" class = "measurement-section-toggle" :aria-expanded = "!measurementCollapsed.actual" @click ="toggleMeasurementSection('actual')" >< h3 > 三 、 实际测量数据 </ h3 >< span v-if = "measurementCollapsed.actual" class="measurement-section-summary"><span>总件数:{{ measurementTotals(actualMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(actualMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(actualMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(actualMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(actualMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.actual" aria-label="实际测量数据计算方式" @change="updateMeasurementSectionMode('actual', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></ el-select ></ div >
2026-08-07 14:47:11 +08:00
< div v-if = "!measurementCollapsed.actual" >
2026-08-17 18:00:42 +08:00
< div v-loading = "measurementLoading" class="detail-table-wrap" >
2026-09-03 10:46:07 +08:00
< el-table :data = "actualMeasurementRows" row -key =" id " border stripe empty -text =" 暂无实际测量数据 " >
2026-09-14 12:00:54 +08:00
< el-table-column v-if = "measurementStrategyEditable" label="操作" width="110" fixed="left" >
< template # default = "{ row }" >< el-button type = "primary" plain size = "small" : loading = "measurementOuterSavingId === String(row.id)" :disabled = "Boolean(measurementOuterSavingId)" @click ="saveWarehouseOuterDimensions(row as Record<string, unknown>)" > 保存外箱 </ el -button ></ template >
</ el-table-column >
2026-08-21 16:28:59 +08:00
< el-table-column label = "箱号" prop = "boxCode" min -width =" 190 " fixed = "left" show -overflow-tooltip >
< template # default = "{ row }" >< strong class = "measurement-box-code" >{{ row . boxCode || '—' }}</ strong ></ template >
</ el-table-column >
2026-09-03 10:46:07 +08:00
< el-table-column label = "#" prop = "itemNo" min -width =" 70 " fixed = "left" />
2026-09-11 17:01:59 +08:00
< el-table-column label = "仓库修改外箱(长)" width = "190" align = "center" >
< template # default = "{ row }" >< el-input-number v-if = "measurementStrategyEditable" v-model="row.outerLengthCm" class="measurement-outer-dimension-input" aria-label="仓库修改外箱长" :min="0.001" :precision="3" :controls="false" placeholder="不修改" />< template v-else >{{ formatNumber ( row.outerLengthCm ) }}</ template ></ template >
2026-09-03 10:46:07 +08:00
</ el -table-column >
2026-09-11 17:01:59 +08:00
< el-table-column label = "仓库修改外箱(宽)" width = "190" align = "center" >
< template # default = "{ row }" >< el-input-number v-if = "measurementStrategyEditable" v-model="row.outerWidthCm" class="measurement-outer-dimension-input" aria-label="仓库修改外箱宽" :min="0.001" :precision="3" :controls="false" placeholder="不修改" />< template v-else >{{ formatNumber ( row.outerWidthCm ) }}</ template ></ template >
2026-09-03 10:46:07 +08:00
</ el -table-column >
2026-09-11 17:01:59 +08:00
< el-table-column label = "仓库修改外箱(高)" width = "190" align = "center" >
< template # default = "{ row }" >< el-input-number v-if = "measurementStrategyEditable" v-model="row.outerHeightCm" class="measurement-outer-dimension-input" aria-label="仓库修改外箱高" :min="0.001" :precision="3" :controls="false" placeholder="不修改" />< template v-else >{{ formatNumber ( row.outerHeightCm ) }}</ template ></ template >
2026-09-03 10:46:07 +08:00
</ el -table-column >
2026-09-14 12:00:54 +08:00
2026-09-03 10:46:07 +08:00
< el-table-column v-for = "column in actualMeasuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120" >
2026-08-17 18:00:42 +08:00
< template # default = "{ row }" >< LineClampText v-if = "column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" />< template v-else >{{ measurementValue ( row as Record < string , unknown >, column . key ) }}</ template ></ template >
</ el-table-column >
< el-table-column label = "尺寸图片" min -width =" 148 " >
< template # default = "{ row }" >
< MeasurementEvidenceThumbnail
:customer-id = "String(detail.summary.tenantId)"
:inbound-id = "String(detail.summary.inboundId)"
:file-id = "row.intelligentImageFileId"
2026-08-30 17:43:03 +08:00
: alt = "`箱号 ${row.boxCode || row.itemNo || '箱规编码待补充'} 的尺寸图片`"
2026-08-17 18:00:42 +08:00
empty -text =" 未绑定尺寸图片 "
2026-08-30 17:43:03 +08:00
@open ="openMeasurementEvidence(row.intelligentImageFileId, `尺寸图片 · ${row.boxCode || row.itemNo || '箱规编码待补充'}`)"
2026-08-17 18:00:42 +08:00
/>
</ template >
</ el-table-column >
</ el-table >
</ div >
2026-09-14 12:00:54 +08:00
< div class = "measurement-total" >< strong > 合计 </ strong >< span > 箱数 : {{ measurementTotals ( actualMeasurementRows ). count ?? '—' }}</ span >< span > 总毛重 : {{ formatNumber ( measurementTotals ( actualMeasurementRows ). gross ) }}</ span >< span > 总体积 : {{ formatNumber ( measurementTotals ( actualMeasurementRows ). volume , 6 ) }}</ span >< span > 总材积重 : {{ formatNumber ( measurementTotals ( actualMeasurementRows ). volumetric ) }}</ span >< span > 重量体积 : {{ formatNumber ( measurementTotals ( actualMeasurementRows ). weightVolume , 6 ) }}</ span ></ div >
2026-08-17 18:00:42 +08:00
< section class = "weight-evidence" aria -label =" 称重图片与服务端确认重量 " >
< div class = "weight-evidence__heading" >
< div >< h4 > 称重图片与确认重量 </ h4 >< p > 称重图片独立展示 ; 重量值直接采用服务端确认结果 。 </ p ></ div >
< dl >< dt > 实际总毛重 </ dt >< dd >{{ formatNumber ( measurementDetail ? . actualGrossWeightKg ) }} kg </ dd ></ dl >
</ div >
< div v-if = "measurementDetail?.weightResults.length" class="weight-evidence-grid" >
< article v-for = "weightResult in measurementDetail.weightResults" :key="weightResult.id" class="weight-evidence-card" >
< MeasurementEvidenceThumbnail
:customer-id = "String(detail.summary.tenantId)"
:inbound-id = "String(detail.summary.inboundId)"
:file-id = "weightResult.fileId"
: alt = "`称重结果 ${weightResult.id} 的称重图片`"
empty -text =" 未绑定称重图片 "
@open ="openMeasurementEvidence(weightResult.fileId, `称重图片 · ${weightResult.palletType || '未记录类型'}`)"
/>
< dl >
< div >< dt > 托盘类型 </ dt >< dd >{{ weightResult . palletType || '未记录类型' }}</ dd ></ div >
< div >< dt > 服务端确认毛重 </ dt >< dd >{{ formatNumber ( weightResult . grossWeightKg ) }} kg </ dd ></ div >
< div >< dt > 托盘皮重 </ dt >< dd >{{ formatNumber ( weightResult . palletTareWeightKg ) }} kg </ dd ></ div >
< div >< dt > 货物重量 </ dt >< dd >{{ formatNumber ( weightResult . netWeightKg ) }} kg </ dd ></ div >
</ dl >
</ article >
</ div >
2026-09-14 12:00:54 +08:00
< p v-else class = "compact-empty" > 暂无称重图片与确认结果 </ p >
2026-08-17 18:00:42 +08:00
</ section >
2026-08-07 14:47:11 +08:00
</ div >
2026-08-17 18:00:42 +08:00
</ section >
</ template >
</ template >
2026-08-06 19:03:13 +08:00
< template v-else >
2026-09-07 11:20:43 +08:00
< section class = "hold-section" aria -label =" 订单扣货控制 " >
< div class = "panel-title" >< div >< h2 > 扣货控制 </ h2 >< p > 扣货状态独立于快递 、 箱子计费和内部备注维护 </ p ></ div >< ShieldAlert :size = "19" /></ div >
< el-empty v-if = "!normalizedInboundId" description="订单尚未生成入库单,扣货状态仅可查看" :image-size="56" />
< template v-else >
< div class = "hold-summary" >
< dl >< div >< dt > 当前状态 </ dt >< dd >< el-tag : type = "currentHoldFlag === true ? 'danger' : currentHoldFlag === false ? 'success' : 'info'" effect = "plain" >{{ currentHoldFlag === true ? '已扣货' : currentHoldFlag === false ? '未扣货' : '状态未知' }}</ el-tag ></ dd ></ div >< div >< dt > 当前原因 </ dt >< dd >{{ currentHoldReason || '—' }}</ dd ></ div ></ dl >
< div class = "hold-actions" >< el-button v-if = "canHoldInbound" type="danger" :loading="holdSaving" @click="openHoldDialog('HOLD')"><ShieldAlert :size="15" />扣货</el-button><el-button v-if="canReleaseInbound" type="primary" plain :loading="holdSaving" @click="openHoldDialog('RELEASE')"><Undo2 :size="15" /> 解除扣货 </ el-button ></ div >
</ div >
< el-alert v-if = "!can('platform:firstleg:inbound:list')" title="当前账号只能查看订单返回的扣货状态,无法读取扣货历史" type="info" :closable="false" show -icon />
< LoadErrorState v-else-if = "holdError" title="扣货状态与历史加载失败" :message="holdError.message" :loading="holdLoading" @retry="loadHoldDetail(true)"><template #details><ApiErrorMeta :details="holdError" /></ template ></ LoadErrorState >
< template v-else >
< p v-if = "holdDetail?.actionBlockReason && !canHoldInbound && !canReleaseInbound" class="hold-block-reason" >{{ holdDetail.actionBlockReason }}</ p >
< h3 class = "hold-history-title" > 扣货历史 </ h3 >
2026-09-07 12:21:44 +08:00
< div v-loading = "holdLoading" class="hold-history-table"><el-table :data="holdDetail?.history || []" border stripe empty-text="暂无扣货历史"><el-table-column label="操作" width="110"><template #default="{ row }"><el-tag :type="row.action === 'HOLD' ? 'danger' : 'success'" effect="plain">{{ row.action === 'HOLD' ? '扣货' : '解除扣货' }}</el-tag></template></el-table-column><el-table-column prop="reason" label="原因" min-width="260" /><el-table-column label="凭证" width="150"><template #default="{ row }"><el-button v-if="row.evidenceFileId" link type="primary" :loading="holdDownloadingFileId === row.evidenceFileId" @click="downloadHoldEvidence(row.evidenceFileId)"><Download :size="14" />下载凭证</el-button><span v-else>—</span></template></el-table-column><el-table-column label="操作人" width="150"><template #default="{ row }">{{ row.operatorName || '系统' }}</template></el-table-column><el-table-column label="操作时间" width="180"><template #default="{ row }" >{{ formatDate ( row.operatedAt ) }}</ template > </ el -table-column ></ el-table ></ div >
2026-09-07 11:20:43 +08:00
</ template >
</ template >
</ section >
< section class = "other-notes-section" aria -label =" 内部备注 " >
< div class = "panel-title" >< div >< h2 > 内部备注 </ h2 >< p > 仅平台内部可见 , 独立保存 </ p ></ div >< el-button v-if = "can('platform:order:note')" type="primary" size="small" @click="openNoteDialog()"><StickyNote :size="15" /> 添加备注 </ el-button ></ div >
2026-09-14 12:00:54 +08:00
< p v-if = "detail.order.internalNotes.length === 0" class="compact-empty" > 暂无内部备注 </ p >
2026-09-07 11:20:43 +08:00
< ul v-else class = "notes-list" aria -label =" 内部备注 " >< li v-for = "item in detail.order.internalNotes" :key="String(item.id)" >< p >{{ item . content }}</ p >< span > 操作人 {{ item . createdBy || '系统' }} · {{ formatDate ( item . createdAt ) }}</ span ></ li ></ ul >
</ section >
2026-08-06 19:03:13 +08:00
</ template >
</ section >
</ template >
</ div >
2026-08-10 15:36:53 +08:00
</ DetailPageLayout >
2026-08-06 19:03:13 +08:00
</ template >
< template v-else >
2026-08-10 15:36:53 +08:00
< ListPageLayout ariaLabel = "统一订单列表" >
2026-08-10 17:42:17 +08:00
< template # toolbar >
2026-08-06 19:03:13 +08:00
< div class = "orders-query-panel" >
< div class = "status-tabs" role = "tablist" aria -label =" 订单主状态 " >
< button
v-for = "(tab, index) in statusTabs"
: key = "tab.value || 'ALL'"
type = "button"
role = "tab"
2026-08-11 18:06:35 +08:00
: aria -selected =" selectedStatus = == tab.value "
2026-08-06 19:03:13 +08:00
aria -controls =" unified -orders-results "
2026-08-11 18:06:35 +08:00
: tabindex = "selectedStatus === tab.value ? 0 : -1"
: class = "['status-tab', { 'is-active': selectedStatus === tab.value }]"
2026-08-06 19:03:13 +08:00
@click ="selectStatus(tab.value)"
@keydown ="handleTabKeydown($event, index)"
>{{ tab . label }}</ button >
</ div >
2026-08-10 17:42:17 +08:00
< QueryToolbar id = "unified-orders-advanced-filters" v-model = "filters" class="orders-query-toolbar" ariaLabel="订单列表筛选" :fields="queryFields" :available-fields="allQueryFields" preference-key="unified-orders-query-v2" :search-on-reset="false" @search="search" @reset="reset" >
< template # actions >
< el-button :aria-expanded = "showAdvancedFilters" aria -controls =" unified -orders-advanced-filters " @click ="showAdvancedFilters = !showAdvancedFilters" >
< SlidersHorizontal :size = "16" />{{ showAdvancedFilters ? '收起筛选' : '高级筛选' }}
< ChevronUp v-if = "showAdvancedFilters" :size="15" /><ChevronDown v-else :size="15" />
</ el-button >
</ template >
</ QueryToolbar >
2026-08-06 19:03:13 +08:00
</ div >
2026-08-10 17:42:17 +08:00
</ template >
< template v-if = "loadError" # state >
< LoadErrorState title = "订单列表暂时无法加载" :message = "loadError.message" :loading = "loading" @retry ="loadOrders" >
2026-08-06 13:56:04 +08:00
< template # details >< ApiErrorMeta :details = "loadError" /></ template >
</ LoadErrorState >
2026-08-10 17:42:17 +08:00
</ template >
2026-08-06 13:56:04 +08:00
2026-08-10 17:42:17 +08:00
< template # default >
< div v-if = "!loadError" id="unified-orders-results" class="surface-panel table-panel table-card" role="tabpanel" >
2026-08-29 10:57:01 +08:00
< div v-if = "coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></ div >
< DataTable v -model:page = "page" v -model:page-size = "pageSize" :data = "records" :columns = "columns" preference -key =" unified -orders-table-v2 " :loading = "loading" :total = "total" : page -sizes ="[ 20 , 50 , 100 ]" row -key =" orderId " border stripe empty -text =" 暂无符合条件的订单 " aria -label =" 统一订单列表 " @ selection -change =" handleOrderSelectionChange " >
2026-08-17 18:00:42 +08:00
< template # toolbar -actions >
2026-09-01 21:09:39 +08:00
< el-button v-if = "can('platform:order:export') && can('platform:download:list')" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportOrders"><Download :size="16" />导出<span v-if="selectedOrders.length" > ( 已选 {{ selectedOrders.length }} ) </ span > </ el -button >
2026-09-08 17:38:09 +08:00
< el-button v-if = "can('platform:order:approve')" type="success" :loading="batchApprovalLoading || batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="openBatchApproval"><CircleCheck :size="16" />批量审核通过<span v-if="selectedApprovalOrders.length" > ( {{ selectedApprovalOrders.length }} ) </ span > </ el -button >
2026-08-31 14:54:57 +08:00
< div v-if = "can('platform:firstleg:smart-loading:assign')" class="coload-toolbar-action" >
2026-08-31 16:44:38 +08:00
< el-button type = "primary" plain :loading = "coLoadSaving" : disabled = "loading || !canCreateCoLoadGroup" aria -label =" 合柜报关 " @click ="openCoLoadCreate" >< Link2 :size = "16" /> 合柜报关 < span v-if = "selectedOrders.length" > ( {{ selectedOrders.length }} ) </ span > </ el -button >
2026-08-31 14:54:57 +08:00
</ div >
2026-09-10 11:17:45 +08:00
< el-button v-if = "can('platform:firstleg:inbound:view')" :loading="printingCartonLabels" :disabled="loading || printingCartonLabels || !selectedCartonLabelOrders.length" @click="printSelectedCartonLabels"><Printer :size="16" />打印箱唛<span v-if="selectedCartonLabelOrders.length" > ( {{ selectedCartonLabelOrders.length }} ) </ span > </ el -button >
2026-08-17 18:00:42 +08:00
< el-button v-if = "can('platform:firstleg:smart-loading:list')" type="primary" @click="smartLoadingOpen = true"><PackageOpen :size="16" /> 智能装柜 </ el-button >
2026-08-14 19:02:34 +08:00
</ template >
2026-08-06 13:56:04 +08:00
< template # actions = "{ row }" >
2026-09-14 17:54:21 +08:00
< div class = "row-actions density-actions" >
< el-button class = "density-action" size = "small" @click ="openDetail(row as UnifiedOrderResource)" > 详情 </ el -button >
< el-button v-for = "action in directUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" class="density-action" :data-tone="action === 'approve' ? 'warning' : action === 'measurement' ? 'purple' : 'neutral'" size="small" :loading="action === 'approve' && approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="runMoreAction(action, row as UnifiedOrderResource)" >{{ unifiedOrderActionLabel ( action ) }} </ el -button >
2026-08-10 17:42:17 +08:00
< el-dropdown v-if = "moreUnifiedOrderActions(row as UnifiedOrderResource).length" trigger="click" @command="runMoreAction($event, row as UnifiedOrderResource)" >
2026-09-14 17:54:21 +08:00
< el-button class = "density-action" size = "small" : aria -label =" moreUnifiedOrderActionsLabel ( row as UnifiedOrderResource )" : title = "moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" >< MoreHorizontal :size = "17" aria -hidden =" true " /></ el-button >
2026-08-06 20:14:56 +08:00
< template # dropdown >
< el-dropdown-menu >
2026-09-14 17:06:55 +08:00
< el-dropdown-item v-for = "action in moreUnifiedOrderActions(row as UnifiedOrderResource)" :key="action" :command="action" >{{ unifiedOrderActionLabel ( action ) }} </ el -dropdown-item >
2026-08-06 20:14:56 +08:00
</ el-dropdown-menu >
</ template >
</ el-dropdown >
2026-08-06 13:56:04 +08:00
</ div >
</ template >
2026-08-12 09:25:55 +08:00
< template # inboundCode = "{ row }" >
2026-08-31 10:09:17 +08:00
< div class = "inbound-relation-cell order-code-cell" >
< strong class = "inbound-code-value" : title = "(row as UnifiedOrderResource).inboundCode || '待生成入仓码'" >{{ ( row as UnifiedOrderResource ). inboundCode || '待生成入仓码' }}</ strong >
2026-08-31 10:31:53 +08:00
< button v-if = "coLoadGroup(row as UnifiedOrderResource)" type="button" class="coload-tag-trigger" :aria-label="`查看${coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!)}成员`" @click.stop="openCoLoadDetail(row as UnifiedOrderResource)"><el-tag size="small" :type="coLoadGroup(row as UnifiedOrderResource)?.status === 'ACTIVE' ? 'warning' : 'info'" >{{ coLoadGroupSummary ( coLoadGroup ( row as UnifiedOrderResource )!) }} </ el -tag ></ button >
2026-08-12 09:25:55 +08:00
< el-button
v-if = "(row as UnifiedOrderResource).relation"
class = "order-relation-link"
link
type = "primary"
: aria -label ="` 查看 $ { orderRelationLabel ( row as UnifiedOrderResource ) } 关联 `"
@click ="openOrderRelation(row as UnifiedOrderResource)"
>< GitBranch :size = "14" />{{ orderRelationLabel ( row as UnifiedOrderResource ) }}</ el-button >
</ div >
2026-08-11 09:16:58 +08:00
</ template >
2026-08-07 09:06:22 +08:00
< template # orderStatus = "{ row }" >< StatusBadge : status = "orderStatusLabel((row as UnifiedOrderResource).internalOrderStatus || (row as UnifiedOrderResource).mainStatus)" /></ template >
< template # measurementReviewStatus = "{ row }" >< StatusBadge : status = "measurementStatusLabel((row as UnifiedOrderResource).measurement?.reviewStatus)" /></ template >
2026-09-03 21:25:56 +08:00
< template # channel = "{ row }" >< div class = "channel-cell" >< LineClampText : content = "(row as UnifiedOrderResource).channelName" />< el-tag v-if = "(row as UnifiedOrderResource).channelChanged" size="small" type="warning" > 曾变更渠道 </ el -tag ></ div ></ template >
2026-09-03 11:47:32 +08:00
< template # estimatedVolume = "{ row }" >
< div class = "planning-cell" >
< el-input-number
2026-09-15 17:46:44 +08:00
v-if = "canEditPlanning(row as UnifiedOrderResource)"
2026-09-03 11:47:32 +08:00
v-model = "planningDraft(row as UnifiedOrderResource).estimatedVolumeCbm"
class = "planning-volume-input"
:min = "0.0001"
:precision = "4"
:controls = "false"
: disabled = "planningDraft(row as UnifiedOrderResource).saving"
placeholder = "填写预估体积"
: aria -label ="` 修改 $ { ( row as UnifiedOrderResource ) .inboundCode || ' 当前订单 ' } 预估体积 `"
@change ="savePlanning(row as UnifiedOrderResource)"
/>
< span v-else >{{ formatNumber (( row as UnifiedOrderResource ) .estimatedVolumeCbm , 4 ) }}</ span >
< small v-if = "planningDraft(row as UnifiedOrderResource).saving" > 保存中 … </ small >
</ div >
</ template >
< template # expectedArrival = "{ row }" >
< div class = "planning-cell" >
< el-date-picker
2026-09-03 15:00:58 +08:00
style = "width: 100%;"
2026-09-15 17:46:44 +08:00
v-if = "canEditPlanning(row as UnifiedOrderResource)"
2026-09-03 11:47:32 +08:00
v-model = "planningDraft(row as UnifiedOrderResource).expectedArrivalAt"
class = "planning-arrival-input"
2026-09-10 10:10:04 +08:00
type = "datetime"
value -format =" YYYY -MM-DDTHH : mm : ss "
format = "YYYY-MM-DD HH:mm"
2026-09-03 11:47:32 +08:00
clearable
: disabled = "planningDraft(row as UnifiedOrderResource).saving"
placeholder = "选择预计到货时间"
: aria -label ="` 修改 $ { ( row as UnifiedOrderResource ) .inboundCode || ' 当前订单 ' } 预计到货时间 `"
@change ="savePlanning(row as UnifiedOrderResource)"
/>
< span v-else >{{ formatDate (( row as UnifiedOrderResource ) .expectedArrivalAt ) }}</ span >
< small v-if = "planningDraft(row as UnifiedOrderResource).saving" > 保存中 … </ small >
</ div >
</ template >
2026-09-03 21:25:56 +08:00
< template # field = "{ row, column }" >< LineClampText v-if = "column.property === 'customerName'" :content="displayField(row as UnifiedOrderResource, column.property)" /><LineClampText v-else-if="['customerRemark', 'latestInternalNote'].includes(String(column.property))" :content="displayField(row as UnifiedOrderResource, column.property)" :lines="2" />< template v-else >{{ displayField ( row as UnifiedOrderResource , column.property ) }}</ template ></ template >
2026-08-06 13:56:04 +08:00
< template # mainStatus = "{ row }" >< StatusBadge : status = "statusLabel((row as UnifiedOrderResource).mainStatus)" /></ template >
2026-08-30 17:43:03 +08:00
< template # customer = "{ row }" >< div class = "primary-cell" >< strong >{{ customerName ( row as UnifiedOrderResource ) }}</ strong ></ div ></ template >
2026-08-06 13:56:04 +08:00
< template # receivingWarehouseName = "{ row }" >{{ ( row as UnifiedOrderResource ). receivingWarehouseName || '—' }}</ template >
< template # destinationWarehouseName = "{ row }" >{{ ( row as UnifiedOrderResource ). destinationWarehouseName || '—' }}</ template >
2026-08-11 19:35:28 +08:00
< template # transport = "{ row }" >< div class = "primary-cell" >< strong >{{ ( row as UnifiedOrderResource ). transport ? . containerNo || unifiedOrderLoadingStatusLabel (( row as UnifiedOrderResource ). transport ? . loadingStatus ) }}</ strong >< span v-if = "(row as UnifiedOrderResource).transport?.vesselName" >{{ ( row as UnifiedOrderResource ) .transport ? .vesselName }} · {{ ( row as UnifiedOrderResource ) .transport ? .voyageNo || '—' }}</ span ></ div ></ template >
2026-08-06 13:56:04 +08:00
< template # cargo = "{ row }" >< span >{{ ( row as UnifiedOrderResource ). totalPackageCount ?? '—' }} 箱 </ span >< span v-if = "(row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== null && (row as UnifiedOrderResource).measurement?.actualGrossWeightKg !== undefined"> · {{ formatNumber((row as UnifiedOrderResource).measurement?.actualGrossWeightKg) }} kg</span><span v-if="(row as UnifiedOrderResource).measurement?.actualVolumeCbm !== null && (row as UnifiedOrderResource).measurement?.actualVolumeCbm !== undefined" > · {{ formatNumber (( row as UnifiedOrderResource ) .measurement ? .actualVolumeCbm , 3 ) }} m ³ </ span ></ template >
< template # expectedShipDate = "{ row }" >{{ formatDate (( row as UnifiedOrderResource ). expectedShipDate ) }}</ template >
< template # updatedAt = "{ row }" >{{ formatDate (( row as UnifiedOrderResource ). updatedAt ) }}</ template >
2026-08-10 16:24:54 +08:00
< template # pagination -left =" { total : tableTotal } " >< span > 共 {{ tableTotal }} 条 </ span ></ template >
2026-08-06 13:56:04 +08:00
</ DataTable >
</ div >
2026-08-10 17:42:17 +08:00
</ template >
2026-08-10 15:36:53 +08:00
</ ListPageLayout >
2026-08-14 19:02:34 +08:00
< SmartLoadingDialog v-model = "smartLoadingOpen" />
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders" >
2026-09-08 17:38:09 +08:00
< el-alert title = "请逐票核对入仓码、客户、中文品名、渠道和目的仓;审核通过后订单进入待测量。" type = "info" :closable = "false" show -icon />
< LoadErrorState v-if = "batchApprovalError" compact title="批量审核预览加载失败" :message="batchApprovalError.message" :loading="batchApprovalLoading" @retry="openBatchApproval"><template #details><ApiErrorMeta :details="batchApprovalError" /></ template ></ LoadErrorState >
< template v-else >
< div class = "batch-approval-summary" >< span > 预览 {{ batchApprovalPreviewRows . length }} 票 </ span >< strong > 已勾选 {{ batchApprovalSelectedRows . length }} 票 </ strong ></ div >
< el-table ref = "batchApprovalTableRef" class = "batch-approval-table" :data = "batchApprovalPreviewRows" row -key =" orderId " border stripe max -height =" 520 " empty -text =" 暂无可审核订单 " @ selection -change =" handleBatchApprovalSelection " >
< el-table-column type = "selection" width = "48" fixed = "left" : selectable = "(row: UnifiedOrderBatchApprovalPreviewItem) => row.canApprove" />
< el-table-column label = "入仓码" min -width =" 160 " show -overflow-tooltip >< template # default = "{ row }" >< strong >{{ row . inboundCode || '—' }}</ strong ></ template ></ el-table-column >
< el-table-column label = "客户" min -width =" 150 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . customerName || '—' }}</ template ></ el-table-column >
< el-table-column label = "中文品名" min -width =" 220 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . chineseGoodsNames . length ? row . chineseGoodsNames . join ( '、' ) : '—' }}</ template ></ el-table-column >
< el-table-column label = "渠道" min -width =" 160 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . channelName || row . channelCode || '—' }}</ template ></ el-table-column >
< el-table-column label = "目的仓" min -width =" 180 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . destinationWarehouseName || row . destinationWarehouseCode || '—' }}</ template ></ el-table-column >
< el-table-column label = "校验结果" min -width =" 180 " show -overflow-tooltip >< template # default = "{ row }" >< el-tag : type = "row.canApprove ? 'success' : 'danger'" effect = "light" >{{ row . canApprove ? '可审核' : row . blockReason || '不可审核' }}</ el-tag ></ template ></ el-table-column >
</ el-table >
</ template >
</ AppDialog >
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" @confirm="createCoLoadGroup" >
2026-08-30 15:49:01 +08:00
< el-alert type = "warning" :closable = "false" show -icon : title = "`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
2026-08-31 10:09:17 +08:00
< div class = "coload-selection" >< span v-for = "row in selectedOrders" :key="String(row.orderId)" >{{ row.inboundCode || ' 待生成入仓码 ' }}</ span ></ div >
2026-08-29 10:57:01 +08:00
< div v-if = "coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></ div >
</ AppDialog >
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving" >
2026-08-31 10:31:53 +08:00
< LoadErrorState v-if = "coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></ template ></ LoadErrorState >
< template v-else-if = "coLoadDetailSummary" >
< div class = "coload-detail-summary" >< el-tag size = "small" : type = "coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'" >{{ coLoadGroupSummary ( coLoadDetailSummary ) }}</ el-tag >< span >{{ coLoadDetailSummary . memberCount }} 个成员 </ span ></ div >
< el-alert v-if = "coLoadDetailHasMissingInboundCodes" type="error" :closable="false" show-icon title="同柜组成员入仓码尚未返回,请刷新后重试" />
< div v-else-if = "coLoadDetailMembers.length" class="coload-detail-scroll" >
2026-08-30 15:49:01 +08:00
< table class = "coload-detail-table" >
2026-08-31 10:31:53 +08:00
< thead >< tr >< th > 入仓码 </ th ></ tr ></ thead >
< tbody >< tr v-for = "member in coLoadDetailMembers" :key="member.inboundCode"><td><strong :title="member.inboundCode" >{{ member.inboundCode }}</ strong ></ td ></ tr ></ tbody >
2026-08-30 15:49:01 +08:00
</ table >
</ div >
2026-08-31 10:31:53 +08:00
< el-empty v-else description = "暂无同柜组成员" :image-size = "56" />
2026-08-31 14:54:57 +08:00
< div v-if = "coLoadCancelError" class="coload-error" role="alert"><span>{{ coLoadCancelError.message }}</span><ApiErrorMeta :details="coLoadCancelError" /></ div >
2026-08-30 15:49:01 +08:00
</ template >
2026-08-31 14:54:57 +08:00
< template # footer = "{ close }" >< div class = "app-dialog__footer" >< el-button :disabled = "coLoadCancelSaving" @click ="close" > 关闭 </ el -button >< el-button v-if = "coLoadDetailSummary?.status === 'ACTIVE' && can('platform:firstleg:smart-loading:assign')" type="danger" :loading="coLoadCancelSaving" @click="cancelCoLoadGroup" > 解除同柜组 </ el -button ></ div ></ template >
2026-08-30 15:49:01 +08:00
</ AppDialog >
2026-08-06 19:03:13 +08:00
</ template >
2026-09-14 18:34:34 +08:00
< AppDrawer class = "admin-density-drawer" v-model = "relationOpen" title="父子订单关联" size="min(860px, 94vw)" :show-footer="false" destroy -on-close >
2026-08-12 09:25:55 +08:00
< OrderFulfillmentTreePanel
v-if = "relationOrder"
:tenant-id = "String(relationOrder.tenantId)"
:order-id = "String(relationOrder.orderId)"
2026-08-14 19:59:32 +08:00
:inbound-code = "relationOrder.inboundCode"
2026-08-12 09:25:55 +08:00
/>
</ AppDrawer >
2026-09-14 16:56:11 +08:00
< AppDialog class = "admin-density-dialog" v-model = "noteOpen" title="编辑订单备注" width="min(680px, calc(100vw - 28px))" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote" >
< el-form label -position =" top " class = "order-remarks-form" >
< el-form-item label = "客户备注(只读)" >< el-input : model -value =" noteOrder ? .customerRemark || '—'" disabled /></ el-form-item >
< el-form-item label = "内部备注" >< el-input v-model = "noteForm.internalRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空内部备注" /></ el-form-item >
< el-form-item label = "仓库备注" >< el-input v-model = "noteForm.warehouseRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空仓库备注" /></ el-form-item >
< el-form-item label = "客服备注" >< el-input v-model = "noteForm.customerServiceRemark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="可填写、修改或清空客服备注" /></ el-form-item >
</ el-form >
2026-08-06 19:03:13 +08:00
</ AppDialog >
2026-08-06 18:02:04 +08:00
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction" >
2026-09-07 11:20:43 +08:00
< el-alert : title = "holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" : type = "holdAction === 'HOLD' ? 'warning' : 'info'" :closable = "false" show -icon />
2026-09-07 12:21:44 +08:00
< el-form label -position =" top " class = "hold-command-form" >< el-form-item : label = "holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required >< el-input v-model = "holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB;服务端将校验文件真实内容。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></ template ></ LoadErrorState ></ el-form >
2026-09-07 11:20:43 +08:00
</ AppDialog >
2026-09-14 15:33:35 +08:00
< AppDialog class = "admin-density-dialog" v-model = "approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval" >
2026-09-14 12:00:54 +08:00
< dl v-if = "approvalOrder" class="approval-facts" >
< div >< dt > 入仓码 </ dt >< dd >{{ approvalOrder . inboundCode || '—' }}</ dd ></ div >
< div >< dt > 客户 </ dt >< dd >{{ customerName ( approvalOrder ) }}</ dd ></ div >
< div >< dt > 渠道 </ dt >< dd >{{ approvalOrder . channelName || '—' }}</ dd ></ div >
< div >< dt > 目的仓 </ dt >< dd >{{ approvalOrder . destinationWarehouseName || '—' }}</ dd ></ div >
< div >< dt > 总箱数 </ dt >< dd >{{ approvalOrder . totalPackageCount ?? '—' }} 箱 </ dd ></ div >
</ dl >
< p class = "approval-hint" > 确认订单资料无误后审核通过 , 订单将进入待测量阶段 。 </ p >
2026-08-06 18:02:04 +08:00
</ AppDialog >
2026-08-07 14:47:11 +08:00
2026-09-02 10:24:07 +08:00
< OrderDeclarationEditorDialog
v-if = "detail"
v-model = "declarationEditorOpen"
:detail = "detail"
@updated ="handleDeclarationUpdated"
/>
2026-08-17 18:00:42 +08:00
< AppDialog
2026-09-14 15:33:35 +08:00
class = "admin-density-dialog"
2026-08-17 18:00:42 +08:00
v-model = "measurementPreviewOpen"
:title = "measurementPreviewTitle"
width = "min(920px, calc(100vw - 28px))"
:loading = "measurementPreviewLoading"
:show-footer = "false"
destroy -on-close
>
< LoadErrorState v-if = "measurementPreviewError" title="测量原图暂时无法读取" :message="measurementPreviewError.message" >
< template # details >< ApiErrorMeta :details = "measurementPreviewError" /></ template >
</ LoadErrorState >
< img v-else-if = "measurementPreviewUrl" class="measurement-original-preview" :src="measurementPreviewUrl" :alt="measurementPreviewTitle" />
< el-empty v-else-if = "!measurementPreviewLoading" description="该记录未返回可预览的图片" :image-size="56" />
</ AppDialog >
2026-08-17 17:49:10 +08:00
< InboundMeasurementDialog
2026-09-14 15:33:35 +08:00
class = "admin-density-dialog"
2026-08-17 17:49:10 +08:00
v-if = "measurementOrder?.inboundId"
2026-08-13 19:00:37 +08:00
v-model = "measurementDialogOpen"
2026-08-17 17:49:10 +08:00
:customer-id = "String(measurementOrder.tenantId)"
:inbound-id = "String(measurementOrder.inboundId)"
:inbound-code = "measurementOrder.inboundCode"
2026-09-03 10:46:07 +08:00
:open-warehouse-suggestion = "openWarehouseSuggestionOnMeasurementLoad"
2026-08-17 17:49:10 +08:00
@updated ="handleMeasurementUpdated"
@submitted ="handleMeasurementSubmitted"
@closed ="clearMeasurementOrder"
/>
2026-08-13 19:00:37 +08:00
2026-08-06 13:56:04 +08:00
</ section >
2026-08-10 15:36:53 +08:00
</ AdminPage >
2026-08-06 13:56:04 +08:00
</ template >
< style scoped >
2026-09-14 17:54:21 +08:00
. detail - tabs { border : 1 px solid var ( -- cc - border ); border - block : 0 ; }
. panel - title > div : first - child { display : flex ; align - items : baseline ; flex - wrap : wrap ; gap : var ( -- admin - space - 1 , 4 px ) var ( -- admin - space - 5 , 12 px ); }
. compact - empty { margin : 0 ; padding : 14 px var ( -- admin - space - 4 , 10 px ); color : var ( -- cc - muted ); text - align : center ; font - size : var ( -- admin - font - body , 13 px ); }
2026-09-14 12:00:54 +08:00
. approval - facts { margin : 0 ; border - top : 1 px solid var ( -- cc - border ); }
2026-09-14 17:54:21 +08:00
. approval - facts > div { display : grid ; grid - template - columns : 88 px minmax ( 0 , 1 fr ); gap : var ( -- admin - space - 5 , 12 px ); padding : var ( -- admin - space - 3 , 8 px ) 0 ; border - bottom : 1 px solid var ( -- cc - border ); }
. approval - facts dt { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. approval - facts dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; font - size : var ( -- admin - font - body , 13 px ); }
. approval - hint { margin : var ( -- admin - space - 5 , 12 px ) 0 0 ; color : # 475467 ; font - size : var ( -- admin - font - body , 13 px ); }
. detail - panel : deep (. el - table ) { font - size : var ( -- admin - font - body , 13 px ); -- el - table - header - bg - color : var ( -- admin - label - bg , # f8fafc ); }
. detail - panel : deep (. el - table__cell ) { height : var ( -- admin - row - height , 36 px ); padding : var ( -- admin - space - 1 , 4 px ) 0 ; }
2026-09-14 12:00:54 +08:00
. detail - panel : deep (. el - table . cell ) { line - height : 22 px ; overflow - wrap : anywhere ; }
. detail - panel : deep (. el - table__empty - block ) { min - height : 44 px ; }
. detail - panel : deep (. el - table__empty - text ) { line - height : 44 px ; }
. history - table : deep (. cell ) { white - space : normal ; }
. measurement - section - title : has ([ aria - expanded = "false" ]) { border - bottom : 1 px solid var ( -- cc - border ); }
. fulfillment - facts + . fulfillment - facts { border - top : 0 ; }
@ media ( min - width : 961 px ) {
2026-09-14 17:54:21 +08:00
. table - card : deep (. data - table__pagination ) { min - height : 44 px ; padding : var ( -- admin - space - 2 , 6 px ) var ( -- admin - space - 5 , 12 px ); }
2026-09-14 12:00:54 +08:00
}
2026-09-14 17:54:21 +08:00
. unified - orders - page { min - width : 0 ; gap : var ( -- admin - space - 5 , 12 px ); -- cc - list - table - font - size : 13 px ; -- cc - list - query - font - size : 13 px ; -- cc - list - action - font - size : 13 px ; }
. page - intro { display : flex ; align - items : flex - start ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ); margin - bottom : var ( -- admin - space - 6 , 16 px ); }
2026-08-06 13:56:04 +08:00
. page - intro h1 { margin : 0 ; color : var ( -- cc - text ); font - size : 22 px ; line - height : 1.35 ; }
2026-09-14 17:54:21 +08:00
. page - intro p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - body , 13 px ); line - height : 1.6 ; }
2026-08-06 13:56:04 +08:00
. page - intro : deep (. el - button ) { flex : 0 0 auto ; }
2026-09-14 17:54:21 +08:00
. orders - query - panel { overflow : hidden ; border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius , 4 px ); background : var ( -- admin - surface , # fff ); }
. status - tabs { display : flex ; max - width : 100 % ; overflow - x : auto ; margin - bottom : 0 ; border - bottom : 1 px solid var ( -- admin - table - border , # e5e7eb ); background : # f9fafb ; scrollbar - width : thin ; }
. status - tab { position : relative ; flex : 0 0 auto ; min - height : var ( -- admin - tab - height , 38 px ); padding : 0 var ( -- admin - space - 6 , 16 px ); border : 0 ; background : transparent ; color : var ( -- cc - muted ); font : inherit ; font - size : var ( -- admin - font - body , 13 px ); cursor : pointer ; }
2026-08-06 15:32:02 +08:00
. status - tab :: after { position : absolute ; right : 12 px ; bottom : 0 ; left : 12 px ; height : 3 px ; background : transparent ; content : '' ; }
. status - tab : hover { color : var ( -- cc - primary ); background : # f6f9fd ; }
. status - tab : focus - visible { outline : 2 px solid var ( -- cc - primary ); outline - offset : - 2 px ; }
. status - tab . is - active { color : var ( -- cc - primary ); font - weight : 600 ; }
. status - tab . is - active :: after { background : var ( -- cc - primary ); }
2026-09-14 17:54:21 +08:00
. orders - query - toolbar { border : 0 ; padding : var ( -- admin - space - 4 , 10 px ) var ( -- admin - space - 5 , 12 px ); }
. table - card { overflow : hidden ; border - radius : var ( -- admin - radius , 4 px ); box - shadow : none ; }
. coload - error { margin : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 5 , 12 px ); padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); border : 1 px solid var ( -- el - color - danger - light - 5 ); border - radius : var ( -- admin - radius , 4 px ); background : var ( -- el - color - danger - light - 9 ); color : var ( -- el - color - danger ); font - size : var ( -- admin - font - caption , 12 px ); }
2026-08-29 10:57:01 +08:00
. coload - error : deep (. api - error - meta ) { margin - top : 5 px ; }
2026-09-14 17:54:21 +08:00
. order - code - cell { display : grid ; justify - items : start ; gap : var ( -- admin - space - 1 , 4 px ); }
2026-08-30 15:49:01 +08:00
. coload - tag - trigger { margin : 0 ; padding : 0 ; border : 0 ; background : transparent ; cursor : pointer ; }
. coload - tag - trigger : focus - visible { outline : 2 px solid var ( -- el - color - primary ); outline - offset : 2 px ; border - radius : 3 px ; }
2026-09-14 17:54:21 +08:00
. coload - selection { display : flex ; flex - wrap : wrap ; gap : var ( -- admin - space - 2 , 6 px ); margin - top : var ( -- admin - space - 5 , 12 px ); }
. coload - selection > span { padding : var ( -- admin - space - 1 , 4 px ) 7 px ; border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius , 4 px ); background : var ( -- cc - surface - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. coload - detail - summary { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ); margin - bottom : var ( -- admin - space - 4 , 10 px ); }
. coload - detail - scroll { overflow - x : auto ; border : 1 px solid var ( -- cc - border ); border - radius : var ( -- admin - radius , 4 px ); }
. coload - detail - table { width : 100 % ; border - collapse : collapse ; font - size : var ( -- admin - font - caption , 12 px ); }
. coload - detail - table th ,. coload - detail - table td { padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); border - bottom : 1 px solid var ( -- cc - border ); text - align : left ; vertical - align : top ; }
2026-08-30 15:49:01 +08:00
. coload - detail - table th { background : var ( -- cc - surface - muted ); color : var ( -- cc - muted ); font - weight : 600 ; }
. coload - detail - table tbody tr : last - child td { border - bottom : 0 ; }
. coload - detail - table td { min - width : 210 px ; }
. coload - detail - table td : last - child { min - width : 80 px ; white - space : nowrap ; }
. coload - detail - table td strong ,. coload - detail - table td span { display : block ; overflow - wrap : anywhere ; }
. coload - detail - table td span { color : var ( -- cc - muted ); }
. coload - detail - table : deep (. el - button ) { min - height : 24 px ; padding : 2 px 0 ; }
2026-09-14 17:54:21 +08:00
. coload - toolbar - action { display : flex ; min - width : 0 ; align - items : center ; gap : var ( -- admin - space - 3 , 8 px ); }
. subsequent - fulfillment { margin - top : var ( -- admin - space - 5 , 12 px ); padding - top : var ( -- admin - space - 4 , 10 px ); border - top : 1 px solid var ( -- cc - border ); }
. subsequent - fulfillment h3 { margin : 0 0 var ( -- admin - space - 3 , 8 px ); font - size : var ( -- admin - font - section , 14 px ); }
2026-09-14 12:00:54 +08:00
. fulfillment - facts { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; border - top : 1 px solid var ( -- cc - border ); border - left : 1 px solid var ( -- cc - border ); overflow : hidden ; }
2026-09-14 17:54:21 +08:00
. batch - approval - summary { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ); margin : var ( -- admin - space - 5 , 12 px ) 0 var ( -- admin - space - 3 , 8 px ); color : var ( -- cc - muted ); font - size : var ( -- admin - font - body , 13 px ); white - space : nowrap ; }
2026-09-08 17:38:09 +08:00
. batch - approval - summary strong { color : var ( -- cc - text ); }
. batch - approval - table : deep (. cell ) { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-09-14 17:54:21 +08:00
. fulfillment - facts > div { display : flex ; align - items : baseline ; gap : var ( -- admin - space - 4 , 10 px ); min - width : 0 ; padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); border - right : 1 px solid var ( -- cc - border ); border - bottom : 1 px solid var ( -- cc - border ); background : var ( -- admin - surface , # fff ); }
2026-08-31 10:48:23 +08:00
. fulfillment - facts > div . wide { grid - column : span 2 ; }
2026-09-14 17:54:21 +08:00
. fulfillment - facts dt { flex : 0 0 auto ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. fulfillment - facts dd { min - width : 0 ; margin : 0 ; overflow - wrap : anywhere ; font - size : var ( -- admin - font - body , 13 px ); }
. fulfillment - empty { margin : 0 ; padding : var ( -- admin - space - 5 , 12 px ); border : 1 px dashed var ( -- cc - border ); border - radius : 6 px ; color : var ( -- cc - muted ); text - align : center ; }
. row - actions { display : flex ; align - items : center ; gap : var ( -- admin - space - 2 , 6 px ); white - space : nowrap ; }
2026-08-06 13:56:04 +08:00
. row - actions : deep (. el - button ) { margin - left : 0 ; }
2026-09-14 13:48:15 +08:00
. table - card : deep (. data - table__system - cell . row - actions . el - dropdown ) { margin - left : 0 ; }
2026-09-14 17:54:21 +08:00
. planning - cell { display : grid ; min - width : 0 ; gap : var ( -- admin - space - 1 , 4 px ); }
. planning - cell > small { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 11 px ); }
2026-09-03 11:47:32 +08:00
. planning - volume - input ,. planning - arrival - input { width : 100 % ; max - width : 100 % ; }
2026-09-14 17:54:21 +08:00
. planning - cell : deep (. el - input__wrapper ) { min - height : var ( -- admin - control - height , 32 px ); padding - right : 9 px ; padding - left : 9 px ; }
2026-09-03 11:47:32 +08:00
. planning - cell : deep (. el - input__inner ) { min - width : 0 ; text - align : left ; }
2026-08-12 09:25:55 +08:00
. inbound - relation - cell { display : grid ; min - width : 0 ; gap : 5 px ; }
2026-08-31 10:09:17 +08:00
. inbound - code - value { display : block ; max - width : 100 % ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
2026-09-14 17:54:21 +08:00
. order - relation - link { width : fit - content ; max - width : 100 % ; height : auto ; justify - content : flex - start ; padding : 0 ; font - size : var ( -- admin - font - caption , 12 px ); }
2026-08-12 09:25:55 +08:00
. order - relation - link : deep ( span ) { min - width : 0 ; white - space : normal ; text - align : left ; }
2026-09-14 17:54:21 +08:00
. primary - cell { display : grid ; min - width : 0 ; gap : var ( -- admin - space - 1 , 4 px ); }
2026-08-06 13:56:04 +08:00
. primary - cell strong { overflow - wrap : anywhere ; }
2026-09-14 17:54:21 +08:00
. primary - cell span { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); overflow - wrap : anywhere ; }
2026-09-14 20:21:02 +08:00
. order - detail - header { border : 1 px solid var ( -- cc - border ); border - bottom : 0 ; border - radius : 4 px 4 px 0 0 ; }
2026-08-06 20:14:56 +08:00
. back - button { flex : 0 0 auto ; padding : 0 ; }
2026-08-06 19:03:13 +08:00
. order - title { min - width : 0 ; }
2026-09-14 20:21:02 +08:00
2026-09-14 17:54:21 +08:00
. detail - header - actions { display : flex ; flex : 0 0 auto ; gap : var ( -- admin - space - 3 , 8 px ); }
2026-08-06 19:03:13 +08:00
. detail - header - actions : deep (. el - button + . el - button ) { margin - left : 0 ; }
. detail - load - error { min - height : 280 px ; }
2026-09-14 12:00:54 +08:00
. order - detail - content { min - width : 0 ; }
2026-09-14 20:21:02 +08:00
. detail - overview { border : 1 px solid var ( -- cc - border ); border - top : 0 ; }
2026-09-14 17:54:21 +08:00
. detail - panel { min - width : 0 ; padding : var ( -- admin - space - 5 , 12 px ); border : 1 px solid var ( -- cc - border ); border - radius : 0 0 4 px 4 px ; background : var ( -- admin - surface , # fff ); }
. panel - title { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ); min - height : var ( -- admin - action - height , 28 px ); margin - bottom : var ( -- admin - space - 3 , 8 px ); }
. panel - title h2 { margin : 0 ; color : # 344054 ; font - size : var ( -- admin - font - section , 14 px ); font - weight : 600 ; }
. panel - title p { margin : 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); line - height : 1.5 ; }
2026-08-06 19:03:13 +08:00
. panel - title > svg { color : var ( -- cc - primary ); }
2026-09-14 17:54:21 +08:00
. measurement - title - actions { display : flex ; flex - wrap : wrap ; justify - content : flex - end ; gap : var ( -- admin - space - 3 , 8 px ); }
2026-09-03 10:46:07 +08:00
. measurement - title - actions : deep (. el - button ) { margin - left : 0 ; }
. fee - confirm - form { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 22 px ; }
2026-09-09 17:58:23 +08:00
. fee - confirm - price - alert { margin - bottom : 14 px ; }
2026-09-03 10:46:07 +08:00
. fee - confirm - number - wrap { position : relative ; width : 100 % ; }
. fee - confirm - number { width : 100 % ; }
2026-09-09 17:58:23 +08:00
. fee - confirm - number : deep (. el - input__wrapper ) { padding - right : 92 px ; }
2026-09-03 10:46:07 +08:00
. fee - confirm - unit { position : absolute ; top : 50 % ; right : 12 px ; z - index : 1 ; transform : translateY ( - 50 % ); color : var ( -- cc - muted ); pointer - events : none ; }
2026-09-14 17:54:21 +08:00
. fee - confirm - reference - title { margin : var ( -- admin - space - 3 , 8 px ) 0 var ( -- admin - space - 5 , 12 px ); color : var ( -- cc - text ); font - size : var ( -- admin - font - section , 14 px ); }
2026-09-09 17:58:23 +08:00
. fee - confirm - price - reason { grid - column : 1 / - 1 ; }
2026-09-14 17:54:21 +08:00
. fee - confirm - price - summary { display : grid ; grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr )); margin : 0 0 var ( -- admin - space - 6 , 16 px ); border : 1 px solid var ( -- cc - border ); background : var ( -- admin - label - bg , # f8fafc ); }
. fee - confirm - price - summary > div { min - width : 0 ; padding : var ( -- admin - space - 4 , 10 px ) var ( -- admin - space - 5 , 12 px ); }
2026-09-09 17:58:23 +08:00
. fee - confirm - price - summary > div + div { border - left : 1 px solid var ( -- cc - border ); }
2026-09-14 17:54:21 +08:00
. fee - confirm - price - summary dt { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 11 px ); }
. fee - confirm - price - summary dd { margin : var ( -- admin - space - 1 , 4 px ) 0 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); font - weight : 650 ; }
2026-09-03 10:46:07 +08:00
@ media ( max - width : 680 px ) { . fee - confirm - form { grid - template - columns : 1 fr ; } . fee - confirm - unit { right : 12 px ; } }
2026-09-14 17:54:21 +08:00
. fee - section - title { margin : var ( -- admin - space - 5 , 12 px ) 0 var ( -- admin - space - 3 , 8 px ); color : # 344054 ; font - size : var ( -- admin - font - section , 14 px ); line - height : 1.4 ; }
2026-09-14 12:00:54 +08:00
. fee - overview { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); margin : 0 ; border : 1 px solid var ( -- cc - border ); }
2026-09-14 17:54:21 +08:00
. fee - overview > div { display : flex ; flex - wrap : wrap ; align - items : baseline ; gap : var ( -- admin - space - 1 , 4 px ) var ( -- admin - space - 5 , 12 px ); padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); }
2026-08-17 19:13:25 +08:00
. fee - overview > div + div { border - left : 1 px solid var ( -- cc - border ); }
2026-09-14 17:54:21 +08:00
. fee - overview dt , . fee - overview small { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. fee - overview dd { margin : 0 ; font - size : var ( -- admin - font - title , 16 px ); font - weight : 600 ; font - variant - numeric : tabular - nums ; }
2026-09-14 12:00:54 +08:00
. fee - overview small { margin - left : auto ; }
2026-09-14 17:54:21 +08:00
. fee - section - title -- secondary { margin - top : var ( -- admin - space - 5 , 12 px ); padding - top : var ( -- admin - space - 4 , 10 px ); border - top : 1 px solid var ( -- cc - border ); }
. order - info - grid { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; }
2026-08-07 10:45:19 +08:00
. order - info - grid > div . wide { grid - column : span 2 ; }
2026-08-06 19:03:13 +08:00
. detail - table - wrap { overflow - x : auto ; }
2026-09-14 17:54:21 +08:00
. detail - table - wrap : deep (. el - table ) { min - width : 720 px ; -- el - table - header - bg - color : var ( -- admin - label - bg , # f8fafc ); }
. cargo - image { width : 40 px ; height : var ( -- admin - control - height , 32 px ); border : 1 px solid var ( -- cc - border ); border - radius : 2 px ; background : var ( -- admin - label - bg , # f8fafc ); cursor : zoom - in ; }
. measurement - grid { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr )); margin : 0 ; }
. measurement - section { margin - top : var ( -- admin - space - 5 , 12 px ); }
. measurement - section - title { display : flex ; width : 100 % ; min - height : var ( -- admin - row - height , 36 px ); align - items : center ; gap : var ( -- admin - space - 5 , 12 px ); padding : var ( -- admin - space - 1 , 4 px ) var ( -- admin - space - 4 , 10 px ); border : 1 px solid var ( -- cc - border ); border - bottom : 0 ; background : var ( -- admin - label - bg , # f8fafc ); color : var ( -- cc - text ); }
2026-08-07 14:47:11 +08:00
. measurement - section - title : hover { background : # f2f6fb ; }
2026-09-14 17:54:21 +08:00
. measurement - section - title h3 { flex : 0 0 auto ; margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); font - weight : 600 ; }
. measurement - section - summary { display : flex ; min - width : 0 ; flex : 1 ; flex - wrap : wrap ; align - items : center ; justify - content : flex - end ; gap : var ( -- admin - space - 1 , 4 px ) var ( -- admin - space - 6 , 16 px ); color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. measurement - section - toggle { display : flex ; min - width : 0 ; min - height : var ( -- admin - action - height , 28 px ); flex : 1 ; align - items : center ; gap : var ( -- admin - space - 6 , 16 px ); padding : 0 ; border : 0 ; background : transparent ; color : inherit ; font : inherit ; text - align : left ; cursor : pointer ; }
2026-09-03 10:46:07 +08:00
. measurement - section - toggle : focus - visible { outline : 2 px solid var ( -- cc - primary ); outline - offset : 2 px ; }
2026-09-14 12:00:54 +08:00
. measurement - section - toggle > svg { flex : 0 0 auto ; margin - left : auto ; color : var ( -- cc - muted ); }
2026-09-03 10:46:07 +08:00
. measurement - section - mode { flex : 0 0 190 px ; }
. measurement - section - title > . measurement - section - mode { display : none ; }
2026-09-14 17:54:21 +08:00
. measurement - rule - selector { display : flex ; flex - wrap : wrap ; align - items : center ; gap : var ( -- admin - space - 4 , 10 px ); margin : 0 ; padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); border : 1 px solid var ( -- cc - border ); border - top : 0 ; background : var ( -- admin - surface , # fff ); }
. measurement - rule - selector__label { color : var ( -- cc - text ); font - size : var ( -- admin - font - caption , 12 px ); font - weight : 650 ; white - space : nowrap ; }
2026-09-03 10:46:07 +08:00
. measurement - rule - selector__control { width : 270 px ; }
2026-09-14 17:54:21 +08:00
. measurement - rule - selector__hint { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
2026-09-11 17:01:59 +08:00
. measurement - outer - dimension - input { width : 128 px ; max - width : 100 % ; }
2026-09-14 17:54:21 +08:00
. measurement - total { display : flex ; min - height : var ( -- admin - control - height , 32 px ); flex - wrap : wrap ; align - items : center ; justify - content : flex - end ; gap : var ( -- admin - space - 1 , 4 px ) 20 px ; padding : var ( -- admin - space - 2 , 6 px ) var ( -- admin - space - 4 , 10 px ); border : 1 px solid var ( -- cc - border ); border - top : 0 ; background : var ( -- admin - label - bg , # f8fafc ); color : # 475467 ; font - size : var ( -- admin - font - caption , 12 px ); font - variant - numeric : tabular - nums ; }
2026-09-14 12:00:54 +08:00
2026-09-14 17:54:21 +08:00
. measurement - total strong { margin - right : auto ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); }
. weight - evidence { margin - top : var ( -- admin - space - 5 , 12 px ); padding : var ( -- admin - space - 4 , 10 px ) 0 0 ; border - top : 1 px solid var ( -- cc - border ); background : var ( -- admin - surface , # fff ); }
. weight - evidence__heading { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ); margin - bottom : var ( -- admin - space - 3 , 8 px ); }
. weight - evidence__heading h4 { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - section , 14 px ); }
. weight - evidence__heading p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
2026-08-17 18:00:42 +08:00
. weight - evidence__heading dl { margin : 0 ; text - align : right ; }
2026-09-14 17:54:21 +08:00
. weight - evidence__heading dt { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 11 px ); }
. weight - evidence__heading dd { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); font - weight : 600 ; }
. weight - evidence - grid { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 280 px , 1 fr )); gap : var ( -- admin - space - 3 , 8 px ); }
. weight - evidence - card { display : grid ; grid - template - columns : 96 px minmax ( 0 , 1 fr ); align - items : center ; min - width : 0 ; padding : var ( -- admin - space - 3 , 8 px ); border : 1 px solid var ( -- cc - border ); background : var ( -- admin - surface , # fff ); }
. weight - evidence - card > dl { display : grid ; gap : var ( -- admin - space - 1 , 4 px ); margin : 0 ; padding : 0 0 0 var ( -- admin - space - 3 , 8 px ); }
. weight - evidence - card > dl > div { display : flex ; min - width : 0 ; justify - content : space - between ; gap : var ( -- admin - space - 5 , 12 px ); }
. weight - evidence - card dt { color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 11 px ); }
. weight - evidence - card dd { min - width : 0 ; margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - caption , 12 px ); font - weight : 650 ; text - align : right ; overflow - wrap : anywhere ; }
2026-08-17 18:00:42 +08:00
. measurement - original - preview { display : block ; width : 100 % ; max - height : min ( 72 vh , 760 px ); object - fit : contain ; border - radius : 8 px ; background : # f2f4f7 ; }
2026-09-14 17:54:21 +08:00
. hold - section { padding - bottom : var ( -- admin - space - 5 , 12 px ); border - bottom : 1 px solid var ( -- cc - border ); }
. other - notes - section { padding - top : var ( -- admin - space - 4 , 10 px ); }
. hold - summary { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 , 16 px ); padding : var ( -- admin - space - 3 , 8 px ) var ( -- admin - space - 4 , 10 px ); border : 1 px solid var ( -- cc - border ); background : var ( -- admin - surface , # fff ); }
. hold - summary dl { display : grid ; min - width : 0 ; flex : 1 ; grid - template - columns : 210 px minmax ( 280 px , 1 fr ); gap : var ( -- admin - space - 5 , 12 px ); margin : 0 ; }
. hold - summary dl > div { display : flex ; align - items : baseline ; gap : var ( -- admin - space - 4 , 10 px ); min - width : 0 ; }
. hold - summary dt { flex : 0 0 auto ; margin : 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
. hold - summary dd { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); overflow - wrap : anywhere ; }
. hold - actions { display : flex ; flex : 0 0 auto ; gap : var ( -- admin - space - 3 , 8 px ); }
2026-09-07 11:20:43 +08:00
. hold - actions : deep (. el - button ) { margin - left : 0 ; }
2026-09-14 17:54:21 +08:00
. hold - block - reason { margin : var ( -- admin - space - 5 , 12 px ) 0 0 ; padding : 9 px var ( -- admin - space - 5 , 12 px ); border : 1 px solid var ( -- el - color - warning - light - 5 ); border - radius : var ( -- admin - radius , 4 px ); background : var ( -- el - color - warning - light - 9 ); color : var ( -- el - color - warning - dark - 2 ); font - size : var ( -- admin - font - caption , 12 px ); }
. hold - history - title { margin : var ( -- admin - space - 6 , 16 px ) 0 var ( -- admin - space - 3 , 8 px ); color : # 344054 ; font - size : var ( -- admin - font - section , 14 px ); }
2026-09-14 12:00:54 +08:00
. hold - history - table { min - width : 0 ; overflow - x : auto ; }
2026-09-14 17:54:21 +08:00
. hold - command - form { margin - top : var ( -- admin - space - 6 , 16 px ); }
. hold - evidence - hint { margin : - var ( -- admin - space - 1 , 4 px ) 0 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
2026-09-14 12:00:54 +08:00
. notes - list { display : grid ; gap : 0 ; margin : 0 ; padding : 0 ; list - style : none ; }
2026-09-14 17:54:21 +08:00
. notes - list p { margin : var ( -- admin - space - 1 , 4 px ) 0 0 ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); line - height : 1.55 ; overflow - wrap : anywhere ; }
2026-08-06 19:03:13 +08:00
. notes - list li { padding : 14 px 0 ; border - bottom : 1 px solid # edf0f4 ; }
. notes - list li : first - child { padding - top : 0 ; }
. notes - list li : last - child { padding - bottom : 0 ; border - bottom : 0 ; }
2026-09-14 17:54:21 +08:00
. notes - list li > p { margin : 0 ; color : var ( -- cc - text ); font - size : var ( -- admin - font - body , 13 px ); }
. notes - list span { display : block ; margin - top : 5 px ; color : var ( -- cc - muted ); font - size : var ( -- admin - font - caption , 12 px ); }
2026-09-14 13:48:15 +08:00
@ media ( max - width : 960 px ) { . order - info - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } . measurement - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); } }
2026-09-14 12:00:54 +08:00
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep (. el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep (. el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { flex - wrap : wrap ; } . detail - header - actions { display : grid ; grid - template - columns : 1 fr ; } . detail - header - actions : deep (. el - button ) { min - height : 40 px ; } . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . order - info - grid > div . wide { grid - column : auto ; } . detail - tabs button { padding : 0 18 px ; } . detail - panel { padding : 16 px ; } . measurement - section - title { align - items : flex - start ; flex - wrap : wrap ; padding : 12 px ; } . measurement - section - summary { width : 100 % ; flex - basis : calc ( 100 % - 34 px ); flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; white - space : normal ; } . measurement - section - title > svg { margin - left : auto ; } . measurement - total { flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; } . measurement - total strong { width : 100 % ; } }
2026-08-17 18:00:42 +08:00
@ media ( max - width : 720 px ) { . weight - evidence__heading { flex - direction : column ; } . weight - evidence__heading dl { text - align : left ; } }
2026-08-17 19:13:25 +08:00
@ media ( max - width : 720 px ) { . fee - overview { grid - template - columns : 1 fr ; } . fee - overview > div + div { border - top : 1 px solid var ( -- cc - border ); border - left : 0 ; } }
2026-09-03 10:46:07 +08:00
@ media ( max - width : 720 px ) { . measurement - section - title { align - items : stretch ; flex - direction : column ; } . measurement - section - toggle { align - items : flex - start ; flex - wrap : wrap ; } . measurement - section - toggle > svg { margin - left : auto ; } . measurement - section - mode { width : 100 % ; flex - basis : auto ; } }
2026-09-14 13:48:15 +08:00
@ media ( max - width : 420 px ) { . weight - evidence { padding : 10 px ; } . weight - evidence - grid { grid - template - columns : 1 fr ; } }
2026-08-13 19:00:37 +08:00
</ style >
2026-09-14 12:00:54 +08:00
2026-09-14 15:52:00 +08:00
< style src = "@/styles/adminDensity.css" ></ style >
2026-09-14 12:00:54 +08:00
< style src = "../styles/orderDensity.css" ></ style >