@@ -1,7 +1,7 @@
< script setup lang = "ts" >
import { computed , nextTick , onBeforeUnmount , onMounted , reactive , ref , watch , type Component } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { ArrowLeft , Box , ChevronDown , ChevronUp , CircleCheck , Eye , GitBranch , History , Link2 , ListChecks , MoreHorizontal , PackageOpen , Ruler , SlidersHorizontal , StickyNote , WalletCards } from '@lucide/vue'
import { ArrowLeft , Box , ChevronDown , ChevronUp , CircleCheck , Download , Eye , GitBranch , History , Link2 , ListChecks , MoreHorizontal , PackageOpen , Ruler , SlidersHorizontal , StickyNote , WalletCards } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
@@ -22,8 +22,10 @@ import InboundFeeDetailManager from '@/modules/unified-orders/components/Inbound
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { getLoadingAssociationSummary , getOverseasInboundCodeRelationship , type LoadingAssociationSummary , type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelation s'
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
import { createPlatformExportTask } from '@/modules/download s'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { getLoadingAssociationSummary , getOverseasInboundCodeRelationship , getPlatformInboundCodeAssociation , type LoadingAssociationSummary , type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -123,6 +125,7 @@ const approvalOpen = ref(false)
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
const batchApprovalSaving = ref ( false )
const exportSubmitting = ref ( false )
const selectedApprovalOrders = ref < UnifiedOrderResource [ ] > ( [ ] )
const selectedOrders = ref < UnifiedOrderResource [ ] > ( [ ] )
const coLoadCreateOpen = ref ( false )
@@ -161,8 +164,10 @@ const overseasRelationship = ref<OverseasInboundCodeRelationship | null>(null)
const loadingAssociationError = ref < ApiErrorDetails | null > ( null )
const overseasRelationshipError = ref < ApiErrorDetails | null > ( null )
const subsequentFulfillmentLoading = ref ( false )
const resolvedDetailTarget = ref < { orderId : string ; tenantId : string } | null > ( null )
let detailLoadingKey = ''
let detailRequestSerial = 0
let detailAssociationRequestSerial = 0
let measurementRequestSerial = 0
let measurementPreviewRequestSerial = 0
let subsequentFulfillmentRequestSerial = 0
@@ -170,9 +175,15 @@ const showAdvancedFilters = ref(false)
const filters = ref < FilterModel > ( emptyFilters ( ) )
const appliedFilters = ref < FilterModel > ( emptyFilters ( ) )
const selectedStatus = ref < UnifiedOrderMainStatus | ' ' > ( '' )
const orderId = computed ( ( ) => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
const detailTenantId = computed ( ( ) => typeof route . query . tenantId === 'string' ? route . query . tenantId : '' )
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 || '' )
const detailRouteKey = computed ( ( ) => orderId . value ? ` ${ detailTenantId . value } : ${ orderId . value } ` : '' )
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' )
function normalizedIdentifier ( value : unknown ) {
const identifier = String ( value ? ? '' ) . trim ( )
return identifier && identifier !== 'null' && identifier !== 'undefined' ? identifier : ''
@@ -261,9 +272,9 @@ const queryFields = computed(() => showAdvancedFilters.value
const columns = computed < DataTableColumn [ ] > ( ( ) => [
{ key : 'actions' , label : '操作' , width : 300 , fixed : 'left' , slotName : 'actions' } ,
... ( can ( 'platform:order:approve' ) || can ( 'platform:firstleg:smart-loading:assign' ) ? [ {
... ( can ( 'platform:order:approve' ) || can ( 'platform:firstleg:smart-loading:assign' ) || can ( 'platform:order:export' ) ? [ {
key : 'selection' , type : 'selection' , width : 48 , fixed : 'left' ,
selectable : ( ) => ! batchApprovalSaving . value && ! coLoadSaving . value ,
selectable : ( ) => ! batchApprovalSaving . value && ! coLoadSaving . value && ! exportSubmitting . value ,
} satisfies DataTableColumn ] : [ ] ) ,
{ key : 'inboundCode' , prop : 'inboundCode' , label : '入仓码' , minWidth : 260 , slotName : 'inboundCode' } ,
{ key : 'mainStatus' , prop : 'mainStatus' , label : '订单主状态' , minWidth : 130 , slotName : 'mainStatus' } ,
@@ -282,7 +293,7 @@ const columns = computed<DataTableColumn[]>(() => [
{ key : 'chargeableWeightKg' , prop : 'chargeableWeightKg' , label : '计费毛重' , minWidth : 120 , slotName : 'field' } ,
{ key : 'chargeableVolumeCbm' , prop : 'chargeableVolumeCbm' , label : '计费体积(m³)' , minWidth : 130 , slotName : 'field' } ,
{ key : 'expectedShipDate' , prop : 'expectedShipDate' , label : '预计出运日期' , minWidth : 150 , slotName : 'field' } ,
{ key : 'expectedArrivalAt' , prop : 'expectedArrivalAt' , label : '预计到达 时间' , minWidth : 160 , slotName : 'field' , defaultVisible : false } ,
{ key : 'expectedArrivalAt' , prop : 'expectedArrivalAt' , label : '预计到货 时间' , minWidth : 160 , slotName : 'field' } ,
{ 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 } ,
@@ -1039,6 +1050,88 @@ function closeDetail() {
void router . push ( { name : 'unified-orders' } )
}
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
}
}
async function loadDetail ( force = false ) {
if ( ! orderId . value ) return
const tenantId = detailTenantId . value
@@ -1064,6 +1157,10 @@ async function loadDetail(force = false) {
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
detail . value = response
await loadCargoImagePreviews ( response , serial )
if ( requestedDetailTab . value === 'measurement' ) {
activeDetailTab . value = 'measurement'
await loadMeasurementDetails ( )
}
} catch ( error ) {
if ( serial !== detailRequestSerial || requestKey !== detailRouteKey . value ) return
detail . value = null
@@ -1075,13 +1172,18 @@ async function loadDetail(force = false) {
}
watch ( [ page , pageSize ] , ( ) => { void loadOrders ( ) } )
watch ( businessInboundCode , ( value ) => {
detailAssociationRequestSerial += 1
resolvedDetailTarget . value = null
if ( value ) void resolveBusinessDetailTarget ( )
} , { immediate : true } )
watch ( detailRouteKey , ( value ) => {
if ( value ) {
activeDetailTab . value = 'basic'
activeDetailTab . value = requestedDetailTab . value
detail . value = null
detailError . value = null
void loadDetail ( )
} else {
} else if ( ! businessInboundCode . value ) {
detailRequestSerial += 1
subsequentFulfillmentRequestSerial += 1
detail . value = null
@@ -1095,9 +1197,10 @@ watch(measurementPreviewOpen, (open) => {
onMounted ( ( ) => {
void loadCustomers ( )
void loadWarehouses ( )
if ( ! orderId . value ) void loadOrders ( )
if ( ! isDetailRoute . value ) void loadOrders ( )
} )
onBeforeUnmount ( ( ) => {
detailAssociationRequestSerial += 1
measurementRequestSerial += 1
releaseMeasurementPreview ( )
releaseCargoImagePreviews ( )
@@ -1107,11 +1210,11 @@ onBeforeUnmount(() => {
< template >
< AdminPage >
< section class = "business-page unified-orders-page" >
< template v-if = "orderId " >
< template v-if = "isDetailRoute " >
< DetailPageLayout ariaLabel = "统一订单详情" >
< header class = "order-detail-header" >
< div class = "order-detail-heading" >
< el-button link class = "back-button" @click ="closeDetail" > < ArrowLeft :size = "16" / > 返回列表 < / el-button >
< el-button link class = "back-button" @click ="fromMeasurementReview ? returnToMeasurementReviews() : closeDetail() " > < ArrowLeft :size = "16" / > { { fromMeasurementReview ? '返回测量审核列表' : '返回列表' } } < / el-button >
< span class = "order-mark" aria -hidden = " true " > < Box :size = "19" / > < / span >
< div class = "order-title" >
< h1 > { { detail ? . summary . inboundCode || '入仓码暂缺' } } < / h1 >
@@ -1123,8 +1226,9 @@ onBeforeUnmount(() => {
< / div >
< / header >
< LoadErrorState v-if = "detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail" >
< LoadErrorState v-if = "detailError" class="detail-load-error" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="businessInboundCode ? resolveBusinessDetailTarget() : loadDetail(true) " >
< template # details > < ApiErrorMeta :details = "detailError" / > < / template >
< 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 >
< / LoadErrorState >
< div v-else v-loading = "detailLoading" class="order-detail-content" >
@@ -1304,6 +1408,7 @@ onBeforeUnmount(() => {
< 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 >
< template v-if = "fromMeasurementReview" #actions><el-button type="primary" plain :loading="measurementLoading" @click="loadMeasurementDetails">重新加载</el-button><el-button @click="returnToMeasurementReviews" > 返回测量审核列表 < / el -button > < / template >
< / LoadErrorState >
< template v-else >
< section class = "measurement-section" >
@@ -1428,6 +1533,7 @@ onBeforeUnmount(() => {
< div v-if = "coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" / > < / div >
< DataTable v -model :page = "page" v -model :page-size = "pageSize" :data = "records" :columns = "columns" preference -key = " unified -orders -table -v2 " :loading = "loading" :total = "total" : page -sizes = " [ 20 , 50 , 100 ] " row -key = " orderId " border stripe empty -text = " 暂无符合条件的订单 " aria -label = " 统一订单列表 " @ selection -change = " handleOrderSelectionChange " >
< template # toolbar -actions >
< 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 >
< el-button v-if = "can('platform:order:approve')" type="success" :loading="batchApprovalSaving" :disabled="loading || !selectedApprovalOrders.length" @click="batchApproveOrders"><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" >
< 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 >