Files
admin-web/src/modules/unified-orders/pages/UnifiedOrdersView.vue
T

3127 lines
196 KiB
Vue
Raw Normal View History

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-18 16:48:40 +08:00
import { ArrowLeft, ChevronDown, ChevronUp, CircleCheck, Download, FileUp, GitBranch, Link2, 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,
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-09-20 14:22:12 +08:00
importPlatformLegacyOrders,
createLegacyOrderImportIdempotencyKey,
type LegacyOrderImportResult,
2026-08-06 13:56:04 +08:00
} from '@/modules/unified-orders/api/unifiedOrders'
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
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'
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-09-18 16:48:40 +08:00
import RowActions, { type RowActionItem } from '@/shared/components/RowActions.vue'
2026-08-11 19:35:28 +08:00
import {
firstLegBusinessTypeLabel,
firstLegContainerModeLabel,
firstLegDestinationTypeLabel,
firstLegPickupMethodLabel,
firstLegTaxModeLabel,
firstLegTradeTermLabel,
firstLegTransportModeLabel,
} from '@/shared/utils/firstLegBusinessLabels'
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,
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,
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-20 14:22:12 +08:00
const legacyImportOpen = ref(false)
const legacyImportFile = ref<File | null>(null)
const legacyImportIdempotencyKey = ref('')
const legacyImportSaving = ref(false)
const legacyImportResult = ref<LegacyOrderImportResult | null>(null)
const legacyImportError = 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)
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[]>([])
const selectedOrders = ref<UnifiedOrderResource[]>([])
2026-09-10 11:17:45 +08:00
const selectedCartonLabelOrders = computed(() => selectedOrders.value.filter((row) => row.inboundId != null))
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)
const coLoadCancelError = ref<ApiErrorDetails | null>(null)
const coLoadCancelSaving = ref(false)
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)
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',
})
const measurementDraftEditableStatuses = new Set([
'PREPARING', 'WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED',
])
const measurementDraftEditable = computed(() => (
can('platform:firstleg:inbound:measure:edit')
&& measurementDraftEditableStatuses.has(measurementDetail.value?.processStatus || '')
))
const measurementStrategyEditable = computed(() => (
can('platform:firstleg:inbound:measure:edit')
&& Boolean(measurementDetail.value?.measurementId)
&& measurementDetail.value?.version != null
2026-09-18 11:41:27 +08:00
&& !measurementDetail.value.customerFeeConfirmed
))
const measurementStrategyLockedHint = computed(() => {
if (!can('platform:firstleg:inbound:measure:edit')) return '当前账号缺少测量编辑权限'
if (!measurementDetail.value?.measurementId) return '当前没有可修改的测量批次'
2026-09-18 11:41:27 +08:00
if (measurementDetail.value.customerFeeConfirmed) return '费用已由客户确认,测量数据已锁定'
return '当前测量数据不可修改'
})
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
if (!measurementStrategyEditable.value) {
ElMessage.warning(measurementStrategyLockedHint.value)
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' },
2026-09-20 14:22:12 +08:00
{ key: 'orderType', label: '订单类型', minWidth: 120, slotName: 'orderType' },
2026-09-03 15:00:58 +08:00
{ 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'])
const canEditDeclaration = computed(() => Boolean(
detail.value
2026-09-03 11:54:20 +08:00
&& declarationEditableStatuses.has(detail.value.summary.mainStatus)
&& 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
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 {
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)
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
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
}
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,
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
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() {
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
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
ElMessage.success('订单备注已保存')
2026-08-06 19:03:13 +08:00
} catch (error) {
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 = []
selectedOrders.value = []
2026-08-06 13:56:04 +08:00
loading.value = true
loadError.value = null
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
}
}
function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
selectedOrders.value = rows
2026-08-17 18:00:42 +08:00
selectedApprovalOrders.value = rows.filter(canApprove)
}
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('、')
}
const coLoadSelectionProblem = computed(() => {
if (selectedOrders.value.length < 2) return null
2026-09-20 14:43:31 +08:00
const allowedStatuses = new Set<UnifiedOrderMainStatus>(['PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_SPLIT', 'LOADING'])
const invalidStatusRows = selectedOrders.value.filter((row) => !allowedStatuses.has(row.mainStatus))
if (invalidStatusRows.length) return `以下入仓码当前状态不能合柜:${coLoadSelectionMembersLabel(invalidStatusRows)}`
const groupedRows = selectedOrders.value.filter((row) => row.coLoadGroup && row.coLoadGroup.status !== 'CANCELLED')
if (groupedRows.length) return `以下入仓码已有同柜组:${coLoadSelectionMembersLabel(groupedRows)},请先解除原同柜组`
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)},不能设为同柜组`
return null
})
const canCreateCoLoadGroup = computed(() => selectedOrders.value.length >= 2 && !coLoadSelectionProblem.value)
function coLoadGroup(row: UnifiedOrderResource) {
2026-08-31 10:31:53 +08:00
return row.coLoadGroup || null
}
2026-09-20 14:22:12 +08:00
function openLegacyImport() {
legacyImportFile.value = null
legacyImportResult.value = null
legacyImportError.value = null
legacyImportIdempotencyKey.value = ''
legacyImportOpen.value = true
}
function selectLegacyImportFile(uploadFile: UploadFile) {
legacyImportFile.value = uploadFile.raw || null
legacyImportResult.value = null
legacyImportError.value = null
legacyImportIdempotencyKey.value = createLegacyOrderImportIdempotencyKey()
}
async function submitLegacyImport() {
if (!legacyImportFile.value || legacyImportSaving.value) return
legacyImportSaving.value = true
legacyImportError.value = null
try {
legacyImportResult.value = await importPlatformLegacyOrders(
legacyImportFile.value,
legacyImportIdempotencyKey.value,
)
const result = legacyImportResult.value
if (result.failed) ElMessage.warning(`导入完成:成功 ${result.succeeded} 条,失败 ${result.failed} 条`)
else ElMessage.success(`已导入 ${result.succeeded} 条旧系统订单`)
await loadOrders()
} catch (error) {
legacyImportError.value = getApiErrorDetails(error, '旧系统订单导入失败,请核对文件后重试')
} finally {
legacyImportSaving.value = false
}
}
function orderTypeLabel(row: UnifiedOrderResource) {
if (row.fulfillmentType === 'ONE_CLICK_DROPSHIP') return '一件代发'
if (row.businessType === 'FBA' || row.businessType === 'FBA_SEA') return 'FBA 头程'
if (row.businessType === 'OVERSEAS_SEA') return '海外中转'
return firstLegBusinessTypeLabel(row.businessType)
}
2026-09-20 10:06:23 +08:00
function unifiedOrderRowClassName({ row }: { row: UnifiedOrderResource }) {
return row.coLoadGroup && row.coLoadGroup.status !== 'CANCELLED' ? 'is-coload-group-row' : ''
}
function coLoadGroupMemberLabel(group: CoLoadGroup) {
2026-08-31 10:31:53 +08:00
const orderCount = new Set(group.members.map((member) => member.orderId)).size
return group.members.length === orderCount
? `${orderCount} 票`
: `${orderCount} 票 · ${group.members.length} 个入仓片段`
}
2026-08-31 10:31:53 +08:00
function coLoadGroupStatusLabel(group: { status: string }) {
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
coLoadCancelError.value = null
2026-08-31 10:31:53 +08:00
coLoadDetailError.value = coLoadDetailInboundCode.value ? null : getApiErrorDetails(new Error('同柜组成员入仓码尚未返回'), '同柜组成员入仓码尚未返回,请刷新后重试')
if (coLoadDetailInboundCode.value) void loadCoLoadDetail()
}
function openCoLoadCreate() {
if (!canCreateCoLoadGroup.value) return
coLoadError.value = null
coLoadCreateOpen.value = true
}
async function createCoLoadGroup() {
if (!canCreateCoLoadGroup.value || coLoadSaving.value) return
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)}`)
await loadOrders()
} catch (error) {
coLoadError.value = getApiErrorDetails(error, '创建同柜不可拆组失败')
} finally {
coLoadSaving.value = false
}
}
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' },
)
} 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 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 '备注'
2026-09-20 10:06:23 +08:00
if (action === 'approve') return '审核'
2026-09-10 11:17:45 +08:00
return '测量'
2026-08-10 17:42:17 +08:00
}
2026-09-18 16:48:40 +08:00
function unifiedOrderRowActions(row: UnifiedOrderResource): RowActionItem[] {
return [
{ key: 'detail', label: '详情', priority: 500 },
...unifiedOrderSecondaryActions(row).map((action, index): RowActionItem => ({
key: action,
label: unifiedOrderActionLabel(action),
priority: 400 - index,
tone: action === 'approve' ? 'warning' : action === 'measurement' ? 'purple' : 'neutral',
loading: action === 'approve' && approvalLoadingId.value === String(row.orderId),
})),
]
}
function handleUnifiedOrderRowAction(command: string, row: UnifiedOrderResource) {
if (command === 'detail') openDetail(row)
else runMoreAction(command, row)
}
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>
<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>
<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>
</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>
<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" />
</el-select></span></el-tooltip>
<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">
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
2026-09-20 10:06:23 +08:00
<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-class-name="unifiedOrderRowClassName" 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-20 14:22:12 +08:00
<el-button v-if="can('platform:order:import')" :disabled="loading" @click="openLegacyImport"><FileUp :size="16" />导入旧系统订单</el-button>
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>
<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>
</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-09-18 16:48:40 +08:00
<template #actions="{ row }"><RowActions :actions="unifiedOrderRowActions(row as UnifiedOrderResource)" :more-label="moreUnifiedOrderActionsLabel(row as UnifiedOrderResource)" @action="handleUnifiedOrderRowAction($event, row as UnifiedOrderResource)" /></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>
</template>
2026-09-20 14:22:12 +08:00
<template #orderType="{ row }"><el-tag size="small" effect="light" type="info">{{ orderTypeLabel(row as UnifiedOrderResource) }}</el-tag></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-09-20 14:22:12 +08:00
2026-08-10 15:36:53 +08:00
</ListPageLayout>
2026-09-20 14:22:12 +08:00
<AppDialog class="admin-density-dialog" v-model="legacyImportOpen" title="导入旧系统订单" width="min(760px, calc(100vw - 28px))" confirm-text="开始导入" :confirm-loading="legacyImportSaving" :confirm-disabled="!legacyImportFile || legacyImportSaving || !!legacyImportResult" destroy-on-close @confirm="submitLegacyImport">
<div class="legacy-import-content">
<dl class="legacy-import-facts">
<div><dt>固定归属客户</dt><dd>IT测试公司</dd></div>
<div><dt>导入结果状态</dt><dd>待审核</dd></div>
</dl>
<el-upload :auto-upload="false" :limit="1" accept=".xlsx" :on-change="selectLegacyImportFile">
<el-button><FileUp :size="16" />选择 Excel 文件</el-button>
<template #tip><div class="el-upload__tip">仅支持旧系统“订单统计导出” .xlsx 文件,最多 200 条、5MB;入仓码保持不变。</div></template>
</el-upload>
<LoadErrorState v-if="legacyImportError" compact title="旧系统订单导入失败" :message="legacyImportError.message" :show-retry="false"><template #details><ApiErrorMeta :details="legacyImportError" /></template></LoadErrorState>
<div v-if="legacyImportResult" class="legacy-import-result">
<p>共 {{ legacyImportResult.total }} 条,成功 {{ legacyImportResult.succeeded }} 条,失败 {{ legacyImportResult.failed }} 条。</p>
<el-table v-if="legacyImportResult.failed" :data="legacyImportResult.items.filter((item) => !item.success)" border stripe max-height="280">
<el-table-column prop="excelRow" label="Excel 行" width="90" />
<el-table-column prop="legacyInboundCode" label="入仓码" min-width="160" />
<el-table-column prop="errorMessage" label="失败原因" min-width="300" />
</el-table>
</div>
</div>
</AppDialog>
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">
<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>
<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">
<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>
</table>
</div>
2026-08-31 10:31:53 +08:00
<el-empty v-else description="暂无同柜组成员" :image-size="56" />
<div v-if="coLoadCancelError" class="coload-error" role="alert"><span>{{ coLoadCancelError.message }}</span><ApiErrorMeta :details="coLoadCancelError" /></div>
</template>
<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>
</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>
<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
<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: 1px 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, 4px) var(--admin-space-5, 12px); }
.compact-empty { margin: 0; padding: 14px var(--admin-space-4, 10px); color: var(--cc-muted); text-align: center; font-size: var(--admin-font-body, 13px); }
2026-09-14 12:00:54 +08:00
.approval-facts { margin: 0; border-top: 1px solid var(--cc-border); }
2026-09-14 17:54:21 +08:00
.approval-facts > div { display: grid; grid-template-columns: 88px minmax(0, 1fr); gap: var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) 0; border-bottom: 1px solid var(--cc-border); }
.approval-facts dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.approval-facts dd { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: var(--admin-font-body, 13px); }
.approval-hint { margin: var(--admin-space-5, 12px) 0 0; color: #475467; font-size: var(--admin-font-body, 13px); }
.detail-panel :deep(.el-table) { font-size: var(--admin-font-body, 13px); --el-table-header-bg-color: var(--admin-label-bg, #f8fafc); }
.detail-panel :deep(.el-table__cell) { height: var(--admin-row-height, 36px); padding: var(--admin-space-1, 4px) 0; }
2026-09-14 12:00:54 +08:00
.detail-panel :deep(.el-table .cell) { line-height: 22px; overflow-wrap: anywhere; }
.detail-panel :deep(.el-table__empty-block) { min-height: 44px; }
.detail-panel :deep(.el-table__empty-text) { line-height: 44px; }
.history-table :deep(.cell) { white-space: normal; }
.measurement-section-title:has([aria-expanded="false"]) { border-bottom: 1px solid var(--cc-border); }
.fulfillment-facts + .fulfillment-facts { border-top: 0; }
@media (min-width: 961px) {
2026-09-14 17:54:21 +08:00
.table-card :deep(.data-table__pagination) { min-height: 44px; padding: var(--admin-space-2, 6px) var(--admin-space-5, 12px); }
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, 12px); --cc-list-table-font-size: 13px; --cc-list-query-font-size: 13px; --cc-list-action-font-size: 13px; }
.page-intro { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--admin-space-6, 16px); margin-bottom: var(--admin-space-6, 16px); }
2026-08-06 13:56:04 +08:00
.page-intro h1 { margin: 0; color: var(--cc-text); font-size: 22px; line-height: 1.35; }
2026-09-14 17:54:21 +08:00
.page-intro p { margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-body, 13px); 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: 1px solid var(--cc-border); border-radius: var(--admin-radius, 4px); background: var(--admin-surface, #fff); }
.status-tabs { display: flex; max-width: 100%; overflow-x: auto; margin-bottom: 0; border-bottom: 1px 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, 38px); padding: 0 var(--admin-space-6, 16px); border: 0; background: transparent; color: var(--cc-muted); font: inherit; font-size: var(--admin-font-body, 13px); cursor: pointer; }
2026-08-06 15:32:02 +08:00
.status-tab::after { position: absolute; right: 12px; bottom: 0; left: 12px; height: 3px; background: transparent; content: ''; }
.status-tab:hover { color: var(--cc-primary); background: #f6f9fd; }
.status-tab:focus-visible { outline: 2px solid var(--cc-primary); outline-offset: -2px; }
.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, 10px) var(--admin-space-5, 12px); }
.table-card { overflow: hidden; border-radius: var(--admin-radius, 4px); box-shadow: none; }
.coload-error { margin: var(--admin-space-3, 8px) var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--el-color-danger-light-5); border-radius: var(--admin-radius, 4px); background: var(--el-color-danger-light-9); color: var(--el-color-danger); font-size: var(--admin-font-caption, 12px); }
.coload-error :deep(.api-error-meta) { margin-top: 5px; }
2026-09-14 17:54:21 +08:00
.order-code-cell { display: grid; justify-items: start; gap: var(--admin-space-1, 4px); }
.coload-tag-trigger { margin: 0; padding: 0; border: 0; background: transparent; cursor: pointer; }
.coload-tag-trigger:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: 2px; border-radius: 3px; }
2026-09-20 10:06:23 +08:00
.table-card :deep(.el-table__row.is-coload-group-row > .el-table__cell) { background: #fff6df; color: #7a4b00; }
.table-card :deep(.el-table__row.is-coload-group-row:hover > .el-table__cell) { background: #ffedbd; }
.table-card :deep(.el-table__row.is-coload-group-row > .el-table__cell:first-child) { box-shadow: inset 3px 0 0 #e6a23c; }
2026-09-14 17:54:21 +08:00
.coload-selection { display: flex; flex-wrap: wrap; gap: var(--admin-space-2, 6px); margin-top: var(--admin-space-5, 12px); }
.coload-selection > span { padding: var(--admin-space-1, 4px) 7px; border: 1px solid var(--cc-border); border-radius: var(--admin-radius, 4px); background: var(--cc-surface-muted); font-size: var(--admin-font-caption, 12px); }
.coload-detail-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); margin-bottom: var(--admin-space-4, 10px); }
.coload-detail-scroll { overflow-x: auto; border: 1px solid var(--cc-border); border-radius: var(--admin-radius, 4px); }
.coload-detail-table { width: 100%; border-collapse: collapse; font-size: var(--admin-font-caption, 12px); }
.coload-detail-table th,.coload-detail-table td { padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border-bottom: 1px solid var(--cc-border); text-align: left; vertical-align: top; }
.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: 210px; }
.coload-detail-table td:last-child { min-width: 80px; 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: 24px; padding: 2px 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, 8px); }
.subsequent-fulfillment { margin-top: var(--admin-space-5, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--cc-border); }
.subsequent-fulfillment h3 { margin: 0 0 var(--admin-space-3, 8px); font-size: var(--admin-font-section, 14px); }
2026-09-14 12:00:54 +08:00
.fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px 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, 16px); margin: var(--admin-space-5, 12px) 0 var(--admin-space-3, 8px); color: var(--cc-muted); font-size: var(--admin-font-body, 13px); 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, 10px); min-width: 0; padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border-right: 1px solid var(--cc-border); border-bottom: 1px 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, 12px); }
.fulfillment-facts dd { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: var(--admin-font-body, 13px); }
.fulfillment-empty { margin: 0; padding: var(--admin-space-5, 12px); border: 1px dashed var(--cc-border); border-radius: 6px; color: var(--cc-muted); text-align: center; }
.row-actions { display: flex; align-items: center; gap: var(--admin-space-2, 6px); 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, 4px); }
.planning-cell > small { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
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, 32px); padding-right: 9px; padding-left: 9px; }
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: 5px; }
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, 12px); }
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, 4px); }
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, 12px); overflow-wrap: anywhere; }
2026-09-14 20:21:02 +08:00
.order-detail-header { border: 1px solid var(--cc-border); border-bottom: 0; border-radius: 4px 4px 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, 8px); }
2026-08-06 19:03:13 +08:00
.detail-header-actions :deep(.el-button + .el-button) { margin-left: 0; }
.detail-load-error { min-height: 280px; }
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: 1px 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, 12px); border: 1px solid var(--cc-border); border-radius: 0 0 4px 4px; background: var(--admin-surface, #fff); }
.panel-title { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-5, 12px); min-height: var(--admin-action-height, 28px); margin-bottom: var(--admin-space-3, 8px); }
.panel-title h2 { margin: 0; color: #344054; font-size: var(--admin-font-section, 14px); font-weight: 600; }
.panel-title p { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); 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, 8px); }
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, 1fr)); gap: 0 22px; }
2026-09-09 17:58:23 +08:00
.fee-confirm-price-alert { margin-bottom: 14px; }
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: 92px; }
2026-09-03 10:46:07 +08:00
.fee-confirm-unit { position: absolute; top: 50%; right: 12px; 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, 8px) 0 var(--admin-space-5, 12px); color: var(--cc-text); font-size: var(--admin-font-section, 14px); }
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, 1fr)); margin: 0 0 var(--admin-space-6, 16px); border: 1px solid var(--cc-border); background: var(--admin-label-bg, #f8fafc); }
.fee-confirm-price-summary > div { min-width: 0; padding: var(--admin-space-4, 10px) var(--admin-space-5, 12px); }
2026-09-09 17:58:23 +08:00
.fee-confirm-price-summary > div + div { border-left: 1px 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, 11px); }
.fee-confirm-price-summary dd { margin: var(--admin-space-1, 4px) 0 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 650; }
2026-09-03 10:46:07 +08:00
@media (max-width: 680px) { .fee-confirm-form { grid-template-columns: 1fr; } .fee-confirm-unit { right: 12px; } }
2026-09-14 17:54:21 +08:00
.fee-section-title { margin: var(--admin-space-5, 12px) 0 var(--admin-space-3, 8px); color: #344054; font-size: var(--admin-font-section, 14px); line-height: 1.4; }
2026-09-14 12:00:54 +08:00
.fee-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px 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, 4px) var(--admin-space-5, 12px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); }
2026-08-17 19:13:25 +08:00
.fee-overview > div + div { border-left: 1px 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, 12px); }
.fee-overview dd { margin: 0; font-size: var(--admin-font-title, 16px); 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, 12px); padding-top: var(--admin-space-4, 10px); border-top: 1px solid var(--cc-border); }
.order-info-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); 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: 720px; --el-table-header-bg-color: var(--admin-label-bg, #f8fafc); }
.cargo-image { width: 40px; height: var(--admin-control-height, 32px); border: 1px solid var(--cc-border); border-radius: 2px; background: var(--admin-label-bg, #f8fafc); cursor: zoom-in; }
.measurement-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; }
.measurement-section { margin-top: var(--admin-space-5, 12px); }
.measurement-section-title { display: flex; width: 100%; min-height: var(--admin-row-height, 36px); align-items: center; gap: var(--admin-space-5, 12px); padding: var(--admin-space-1, 4px) var(--admin-space-4, 10px); border: 1px 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, 13px); 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, 4px) var(--admin-space-6, 16px); color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.measurement-section-toggle { display: flex; min-width: 0; min-height: var(--admin-action-height, 28px); flex: 1; align-items: center; gap: var(--admin-space-6, 16px); 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: 2px solid var(--cc-primary); outline-offset: 2px; }
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 190px; }
.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, 10px); margin: 0; padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px 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, 12px); font-weight: 650; white-space: nowrap; }
2026-09-03 10:46:07 +08:00
.measurement-rule-selector__control { width: 270px; }
2026-09-14 17:54:21 +08:00
.measurement-rule-selector__hint { color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
2026-09-11 17:01:59 +08:00
.measurement-outer-dimension-input { width: 128px; max-width: 100%; }
2026-09-14 17:54:21 +08:00
.measurement-total { display: flex; min-height: var(--admin-control-height, 32px); flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--admin-space-1, 4px) 20px; padding: var(--admin-space-2, 6px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); border-top: 0; background: var(--admin-label-bg, #f8fafc); color: #475467; font-size: var(--admin-font-caption, 12px); 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, 13px); }
.weight-evidence { margin-top: var(--admin-space-5, 12px); padding: var(--admin-space-4, 10px) 0 0; border-top: 1px 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, 16px); margin-bottom: var(--admin-space-3, 8px); }
.weight-evidence__heading h4 { margin: 0; color: var(--cc-text); font-size: var(--admin-font-section, 14px); }
.weight-evidence__heading p { margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
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, 11px); }
.weight-evidence__heading dd { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); font-weight: 600; }
.weight-evidence-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--admin-space-3, 8px); }
.weight-evidence-card { display: grid; grid-template-columns: 96px minmax(0, 1fr); align-items: center; min-width: 0; padding: var(--admin-space-3, 8px); border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.weight-evidence-card > dl { display: grid; gap: var(--admin-space-1, 4px); margin: 0; padding: 0 0 0 var(--admin-space-3, 8px); }
.weight-evidence-card > dl > div { display: flex; min-width: 0; justify-content: space-between; gap: var(--admin-space-5, 12px); }
.weight-evidence-card dt { color: var(--cc-muted); font-size: var(--admin-font-caption, 11px); }
.weight-evidence-card dd { min-width: 0; margin: 0; color: var(--cc-text); font-size: var(--admin-font-caption, 12px); 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(72vh, 760px); object-fit: contain; border-radius: 8px; background: #f2f4f7; }
2026-09-14 17:54:21 +08:00
.hold-section { padding-bottom: var(--admin-space-5, 12px); border-bottom: 1px solid var(--cc-border); }
.other-notes-section { padding-top: var(--admin-space-4, 10px); }
.hold-summary { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-6, 16px); padding: var(--admin-space-3, 8px) var(--admin-space-4, 10px); border: 1px solid var(--cc-border); background: var(--admin-surface, #fff); }
.hold-summary dl { display: grid; min-width: 0; flex: 1; grid-template-columns: 210px minmax(280px, 1fr); gap: var(--admin-space-5, 12px); margin: 0; }
.hold-summary dl > div { display: flex; align-items: baseline; gap: var(--admin-space-4, 10px); min-width: 0; }
.hold-summary dt { flex: 0 0 auto; margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
.hold-summary dd { margin: 0; color: var(--cc-text); font-size: var(--admin-font-body, 13px); overflow-wrap: anywhere; }
.hold-actions { display: flex; flex: 0 0 auto; gap: var(--admin-space-3, 8px); }
2026-09-20 14:22:12 +08:00
.legacy-import-content { display: grid; gap: var(--admin-space-5, 12px); }
.legacy-import-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); }
.legacy-import-facts > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); align-items: center; padding-block: var(--admin-space-2, 6px); }
.legacy-import-facts dt { padding: 8px 10px; color: var(--cc-text-muted); }
.legacy-import-facts dd { margin: 0; padding: 8px 10px; font-weight: 600; }
.legacy-import-result > p { margin: 0 0 var(--admin-space-3, 8px); }
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, 12px) 0 0; padding: 9px var(--admin-space-5, 12px); border: 1px solid var(--el-color-warning-light-5); border-radius: var(--admin-radius, 4px); background: var(--el-color-warning-light-9); color: var(--el-color-warning-dark-2); font-size: var(--admin-font-caption, 12px); }
.hold-history-title { margin: var(--admin-space-6, 16px) 0 var(--admin-space-3, 8px); color: #344054; font-size: var(--admin-font-section, 14px); }
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, 16px); }
.hold-evidence-hint { margin: -var(--admin-space-1, 4px) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
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, 4px) 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); line-height: 1.55; overflow-wrap: anywhere; }
2026-08-06 19:03:13 +08:00
.notes-list li { padding: 14px 0; border-bottom: 1px 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, 13px); }
.notes-list span { display: block; margin-top: 5px; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
2026-09-14 13:48:15 +08:00
@media (max-width: 960px) { .order-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .measurement-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
2026-09-14 12:00:54 +08:00
@media (max-width: 720px) { .page-intro { align-items: stretch; flex-direction: column; } .page-intro :deep(.el-button) { width: 100%; min-height: 44px; } .status-tab { min-height: 46px; padding: 0 14px; } .table-actions :deep(.el-button) { width: 100%; min-height: 40px; } .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: 1fr; } .detail-header-actions :deep(.el-button) { min-height: 40px; } .order-info-grid, .measurement-grid { grid-template-columns: 1fr; } .order-info-grid > div.wide { grid-column: auto; } .detail-tabs button { padding: 0 18px; } .detail-panel { padding: 16px; } .measurement-section-title { align-items: flex-start; flex-wrap: wrap; padding: 12px; } .measurement-section-summary { width: 100%; flex-basis: calc(100% - 34px); flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; white-space: normal; } .measurement-section-title > svg { margin-left: auto; } .measurement-total { flex-wrap: wrap; justify-content: flex-start; gap: 6px 16px; } .measurement-total strong { width: 100%; } }
2026-08-17 18:00:42 +08:00
@media (max-width: 720px) { .weight-evidence__heading { flex-direction: column; } .weight-evidence__heading dl { text-align: left; } }
2026-08-17 19:13:25 +08:00
@media (max-width: 720px) { .fee-overview { grid-template-columns: 1fr; } .fee-overview > div + div { border-top: 1px solid var(--cc-border); border-left: 0; } }
2026-09-03 10:46:07 +08:00
@media (max-width: 720px) { .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: 420px) { .weight-evidence { padding: 10px; } .weight-evidence-grid { grid-template-columns: 1fr; } }
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>