@@ -1,5 +1,5 @@
< script setup lang = "ts" >
import { computed , nextTick , onMounted , ref , watch } from 'vue'
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft , Box , ChevronDown , ChevronUp , CircleCheck , Eye , History , ListChecks , MoreHorizontal , PackageOpen , RefreshCw , Ruler , SlidersHorizontal , StickyNote , WalletCards } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
@@ -7,11 +7,13 @@ import {
getPlatformUnifiedOrder ,
approvePlatformUnifiedOrder ,
addPlatformUnifiedOrderNote ,
listPlatformUnifiedOrderFeeDetails ,
listPlatformUnifiedOrders ,
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
type UnifiedOrderMainStatus ,
type UnifiedOrderResource ,
type UnifiedOrderFeeDetailResource ,
} from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
@@ -23,6 +25,11 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar , { type QueryField , type QueryModel } from '@/shared/components/QueryToolbar.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import {
downloadWeightImage ,
getInboundMeasurement ,
type InboundMeasurementDetail ,
} from '@/modules/inbounds'
type FilterModel = QueryModel & {
tenantId : string
@@ -74,6 +81,15 @@ const noteOpen = ref(false)
const noteSaving = ref ( false )
const noteContent = ref ( '' )
const noteOrder = ref < UnifiedOrderResource | null > ( null )
const feeDetails = ref < UnifiedOrderFeeDetailResource [ ] > ( [ ] )
const feeLoading = ref ( false )
const feeLoadedKey = ref ( '' )
const measurementDetail = ref < InboundMeasurementDetail | null > ( null )
const measurementLoading = ref ( false )
const measurementLoadedKey = ref ( '' )
const measurementImageUrls = ref < string [ ] > ( [ ] )
const measurementCollapsed = reactive ( { customer : false , outer : false , actual : false } )
let detailLoadingKey = ''
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel > ( emptyFilters ( ) )
const orderId = computed ( ( ) => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
@@ -87,6 +103,16 @@ const detailTabs = [
{ key : 'other' , label : '其他' , icon : StickyNote } ,
]
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)' } ,
{ key : 'totalVolumeCbm' , label : '总体积(T.CBM)' } , { key : 'volumeWeightKg' , label : '总材积' } , { key : 'remark' , label : '备注' } ,
]
const statusTabs = computed ( ( ) => [
{ value : '' as const , label : '全部' } ,
... unifiedOrderStatusOptions ,
@@ -200,8 +226,8 @@ const columns: DataTableColumn[] = [
{ key : 'pcOrderFiles' , prop : 'pcOrderFiles' , label : 'PC端下单文件' , minWidth : 140 , slotName : 'field' } ,
{ key : 'expressCompany' , prop : 'expressCompany' , label : '快递公司' , minWidth : 130 , slotName : 'field' } ,
{ key : 'expressTrackingNo' , prop : 'expressTrackingNo' , label : '快递单号' , minWidth : 150 , slotName : 'field' } ,
{ key : 'r emark' , prop : 'r emark' , label : '备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'i nternalRemark ' , prop : 'i nternalRemark ' , label : '客服内部备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'customerR emark' , prop : 'customerR emark' , label : '备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'latestI nternalNote ' , prop : 'latestI nternalNote ' , label : '客服内部备注' , minWidth : 180 , slotName : 'field' } ,
{ key : 'createdBy' , prop : 'createdBy' , label : '创建人' , minWidth : 120 , slotName : 'field' } ,
{ key : 'createdAt' , prop : 'createdAt' , label : '创建日期' , minWidth : 150 , slotName : 'field' } ,
]
@@ -259,13 +285,13 @@ function formatNumber(value: number | null | undefined, digits = 2) {
return value === null || value === undefined ? '—' : value . toLocaleString ( 'zh-CN' , { maximumFractionDigits : digits } )
}
function partyValue ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'CONSIGNEE ' , key : string ) {
function partyValue ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER ' , key : string ) {
const party = parties . find ( ( item ) => String ( item . partyType || '' ) . toUpperCase ( ) === type )
const value = party ? . [ key ]
return value === null || value === undefined || value === '' ? '—' : String ( value )
}
function partyAddress ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'CONSIGNEE ' ) {
function partyAddress ( parties : Array < Record < string , unknown > > , type : 'SENDER' | 'RECEIVER ' ) {
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 ( ' ' ) || '—'
@@ -336,6 +362,138 @@ function orderStatusLabel(value: string | null | undefined) {
} as Record < string , string > ) [ value || '' ] || value || '—'
}
function cargoValue ( cargo : Record < string , unknown > , key : string ) {
const value = cargo [ key ]
if ( key === 'circumferenceCm' ) {
const width = Number ( cargo . widthCm )
const height = Number ( cargo . heightCm )
return Number . isFinite ( width ) && Number . isFinite ( height ) ? formatNumber ( 2 * ( width + height ) ) : '—'
}
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 : ''
}
function feeDate ( value : string | null | undefined ) {
return value ? formatDate ( value ) : '—'
}
function feeText ( value : string | null | undefined ) {
return value || '—'
}
function feeNumber ( value : number | null | undefined ) {
return value === null || value === undefined ? '—' : formatNumber ( value , 4 )
}
function feeReviewLabel ( value : string | null | undefined ) {
return ( { PENDING : '待审核' , APPROVED : '审核通过' , REJECTED : '审核驳回' , NOT _SUBMITTED : '未提交' } as Record < string , string > ) [ value || '' ] || value || '—'
}
function feeDirectionLabel ( value : string | null | undefined ) {
return ( { RECEIVABLE : '应收' , PAYABLE : '应付' } as Record < string , string > ) [ value || '' ] || value || '—'
}
async function loadFeeDetails ( ) {
const summary = detail . value ? . summary
if ( ! summary ? . inboundId || ! can ( 'firstleg:inbound:fee-detail:view' ) ) return
const key = ` ${ summary . tenantId } : ${ summary . inboundId } `
if ( feeLoadedKey . value === key || feeLoading . value ) return
feeLoading . value = true
try {
feeDetails . value = await listPlatformUnifiedOrderFeeDetails ( String ( summary . tenantId ) , String ( summary . inboundId ) )
feeLoadedKey . value = key
} catch {
feeDetails . value = [ ]
} finally {
feeLoading . value = false
}
}
function measurementValue ( row : Record < string , unknown > , key : string , customerSource = false ) {
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 ) : '—'
}
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 )
}
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 ,
}
}
function revokeMeasurementImages ( ) {
measurementImageUrls . value . forEach ( ( url ) => URL . revokeObjectURL ( url ) )
measurementImageUrls . value = [ ]
}
async function loadMeasurementDetails ( ) {
const summary = detail . value ? . summary
if ( ! summary ? . inboundId || ! can ( 'platform:firstleg:inbound:view' ) ) return
const key = ` ${ summary . tenantId } : ${ summary . inboundId } `
if ( measurementLoadedKey . value === key || measurementLoading . value ) return
measurementLoading . value = true
try {
measurementDetail . value = await getInboundMeasurement ( String ( summary . tenantId ) , String ( summary . inboundId ) )
measurementLoadedKey . value = key
revokeMeasurementImages ( )
const fileIds = [ ... measurementDetail . value . items . map ( ( item ) => item . intelligentImageFileId ) , ... measurementDetail . value . weightResults . map ( ( item ) => item . fileId ) ]
. filter ( ( value ) : value is string => Boolean ( value ) )
const blobs = await Promise . all ( fileIds . map ( ( fileId ) => downloadWeightImage ( String ( summary . tenantId ) , String ( summary . inboundId ) , fileId ) . catch ( ( ) => null ) ) )
measurementImageUrls . value = blobs . filter ( ( blob ) : blob is Blob => Boolean ( blob ) ) . map ( ( blob ) => URL . createObjectURL ( blob ) )
} catch {
measurementDetail . value = null
revokeMeasurementImages ( )
} finally {
measurementLoading . value = false
}
}
function selectDetailTab ( key : string ) {
activeDetailTab . value = key
if ( key === 'fees' ) void loadFeeDetails ( )
if ( key === 'measurement' ) void loadMeasurementDetails ( )
}
function toggleMeasurementSection ( section : keyof typeof measurementCollapsed ) {
measurementCollapsed [ section ] = ! measurementCollapsed [ section ]
}
function nextActionBoundary ( row : UnifiedOrderResource ) {
return row . nextAction === 'CONFIRM_MEASUREMENT' ? '待完成测量' : ''
}
@@ -411,8 +569,8 @@ async function saveNote() {
const target = noteOrder . value
await addPlatformUnifiedOrderNote ( String ( target . tenantId ) , String ( target . orderId ) , content )
noteOpen . value = false
if ( detail . value ? . summary . orderId === target . orderId ) await loadDetail ( )
else await loadOrders ( )
await loadOrders ( )
if ( detail . value ? . summary . orderId === target . orderId ) await loadDetail ( true )
noteOrder . value = null
ElMessage . success ( '内部备注已保存' )
} catch ( error ) {
@@ -545,13 +703,21 @@ function closeDetail() {
void router . push ( { name : 'unified-orders' } )
}
async function loadDetail ( ) {
async function loadDetail ( force = false ) {
if ( ! orderId . value ) return
const tenantId = typeof route . query . tenantId === 'string' ? route . query . tenantId : ''
if ( ! tenantId ) {
detailError . value = { message : '订单详情缺少客户标识,请从订单列表重新打开' }
return
}
const requestKey = ` ${ tenantId } : ${ orderId . value } `
if ( ! force && ( detailLoadingKey === requestKey || detail . value ? . summary . orderId === orderId . value && String ( detail . value . summary . tenantId ) === tenantId ) ) return
detailLoadingKey = requestKey
feeDetails . value = [ ]
feeLoadedKey . value = ''
measurementDetail . value = null
measurementLoadedKey . value = ''
revokeMeasurementImages ( )
detailLoading . value = true
detailError . value = null
try {
@@ -561,6 +727,7 @@ async function loadDetail() {
detailError . value = getApiErrorDetails ( error , '统一订单详情加载失败,请检查 C02 统一详情接口和订单权限' )
} finally {
detailLoading . value = false
if ( detailLoadingKey === requestKey ) detailLoadingKey = ''
}
}
@@ -574,7 +741,7 @@ watch(orderId, (value) => {
onMounted ( ( ) => {
void loadCustomers ( )
void loadWarehouses ( )
void loadOrders ( )
if ( ! orderId . value ) void loadOrders ( )
} )
< / script >
@@ -613,7 +780,7 @@ onMounted(() => {
< / section >
< div class = "detail-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="active DetailTab = tab.key"><component :is="tab.icon" :size="17" / > < span > { { tab . label } } < / span > < / button >
< 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="select DetailTab( tab.key) "><component :is="tab.icon" :size="17" / > < span > { { tab . label } } < / span > < / button >
< / div >
< section :id = "`order-detail-panel-${activeDetailTab}`" class = "detail-panel" role = "tabpanel" :aria-labelledby = "`order-detail-tab-${activeDetailTab}`" tabindex = "0" >
@@ -633,11 +800,9 @@ onMounted(() => {
< 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 || '—' } } < / dd > < / div >
< div > < dt > 承运商 < / dt > < dd > { { detail . order . order ? . carrierName || '待平台指派 ' } } < / dd > < / div >
< div > < dt > 业务类型 < / dt > < dd > { { detail . order . order ? . tradeTerm || '—' } } < / dd > < / div >
< div > < dt > 贸易方式 < / dt > < dd > { { detail . order . order ? . tradeTerm || detail . summary . businessType || '—' } } < / dd > < / div >
< div > < dt > 业务类型 < / dt > < dd > { { detail . summary . businessType || '— ' } } < / dd > < / div >
< div > < dt > 贸易条款 < / dt > < dd > { { detail . order . order ? . tradeTerm || '—' } } < / dd > < / div >
< div > < dt > 买单 / 退税 < / dt > < dd > { { detail . order . order ? . taxMode || '—' } } < / dd > < / div >
< div > < dt > 报关方式 < / dt > < dd > { { detail . summary . businessType || '—' } } < / dd > < / div >
< div > < dt > 费用审核状态 < / dt > < dd > { { detail . summary . feeReviewStatus || '—' } } < / dd > < / div >
< div > < dt > 是否扣货 < / dt > < dd > { { displayBoolean ( detail . summary . holdCargo ) } } < / dd > < / div >
< div > < dt > 扣货理由 < / dt > < dd > { { detail . summary . holdReason || '—' } } < / dd > < / div >
@@ -671,9 +836,9 @@ onMounted(() => {
< 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 >
< div class = "wide" > < dt > 目的地联系人 < / dt > < dd > { { partyValue ( detail . order . parties , 'CONSIGNEE ' , 'contactName' ) } } · { { partyValue ( detail . order . parties , 'CONSIGNEE ' , 'phone' ) } } · { { partyValue ( detail . order . parties , 'CONSIGNEE ' , 'email' ) } } < / dd > < / div >
< div > < dt > 目的仓联系人邮箱 < / dt > < dd > { { partyValue ( detail . order . parties , 'CONSIGNEE ' , 'email' ) } } < / dd > < / div >
< div class = "wide" > < dt > 目的地地址 < / dt > < dd > { { partyAddress ( detail . order . parties , 'CONSIGNEE ' ) } } · { { partyValue ( detail . order . parties , 'CONSIGNEE ' , 'postalCode' ) } } < / dd > < / div >
< 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 >
< 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 >
@@ -690,13 +855,56 @@ onMounted(() => {
< div class = "panel-title" > < div > < h2 > 货品信息 < / h2 > < p > 共 { { detail . order . cargoes . length } } 条 < / p > < / div > < PackageOpen :size = "19" / > < / div >
< div class = "detail-table-wrap" >
< el-table :data = "detail.order.cargoes" empty -text = " 暂无货品信息 " >
< el-table-column prop = "itemNo" label = "#" width = "64" / > < el-table-column prop = "cargoName" label = "品名" min -width = " 170 " / > < el-table-column prop = "packageType" label = "包装" min -width = " 110 " / > < el-table-column prop = "packageCount" label = "箱数" width = "90" / > < el-table-column prop = "grossWeightKg" label = "毛重(kg)" min -width = " 110 " / > < el-table-column prop = "volumeCbm" label = "体积(m³)" min -width = " 110 " / >
< 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 >
< 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>)" fit="contain" class="cargo-image" / > < span v-else > — < / span > < / template > < / el -table -column >
< 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 >
< el-table-column label = "箱规长(cm)" min -width = " 110 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'lengthCm' ) } } < / template > < / el-table-column >
< el-table-column label = "箱规宽(cm)" min -width = " 110 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'widthCm' ) } } < / template > < / el-table-column >
< el-table-column label = "箱规高(cm)" min -width = " 110 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'heightCm' ) } } < / template > < / el-table-column >
< el-table-column label = "箱规周长(cm)" min -width = " 120 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'circumferenceCm' ) } } < / template > < / el-table-column >
< el-table-column label = "每箱产品数量" min -width = " 130 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'unitsPerPackage' ) } } < / template > < / el-table-column >
< el-table-column label = "箱数" width = "90" > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'packageCount' ) } } < / template > < / el-table-column >
< el-table-column label = "总毛重(T.KGW)" min -width = " 140 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'grossWeightKg' ) } } < / template > < / el-table-column >
< el-table-column label = "总净重(T.KNW)" min -width = " 140 " > < template # default = "{ row }" > { { cargoValue ( row as Record < string , unknown > , 'netWeightKg' ) } } < / template > < / el-table-column >
< / el-table >
< / div >
< / template >
< template v-else-if = "activeDetailTab === 'fees'" >
< div class = "panel-title" > < div > < h2 > 费用信息 < / h2 > < p > 订单费用与审核结果 < / p > < / div > < WalletCards :size = "19" / > < / div > < el-empty description = "暂无费用数据" :image-size = "64" / >
< div class = "panel-title" > < div > < h2 > 费用信息 < / h2 > < p > 共 { { feeDetails . length } } 条应收应付费用明细 < / p > < / div > < WalletCards :size = "19" / > < / div >
< el-alert v-if = "!can('firstleg:inbound:fee-detail:view')" title="当前账号没有查看费用明细的权限" type="warning" :closable="false" show -icon / >
< div v-else v-loading = "feeLoading" class="detail-table-wrap" >
< el -table :data = "feeDetails" empty -text = " 暂无费用数据 " row -key = " id " >
< el-table-column label = "审核状态" min -width = " 120 " > < template # default = "{ row }" > < StatusBadge : status = "feeReviewLabel((row as UnifiedOrderFeeDetailResource).reviewStatus)" / > < / template > < / el-table-column >
< el-table-column label = "应收/应付" min -width = " 110 " > < template # default = "{ row }" > { { feeDirectionLabel ( ( row as UnifiedOrderFeeDetailResource ) . direction ) } } < / template > < / el-table-column >
< el-table-column label = "客户公司名" min -width = " 170 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . customerName ) } } < / template > < / el-table-column >
< el-table-column label = "裹箱头" min -width = " 150 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . invoiceTitle ) } } < / template > < / el-table-column >
< el-table-column label = "入仓码" min -width = " 150 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . inboundCode ) } } < / template > < / el-table-column >
< el-table-column label = "业务发生日" min -width = " 130 " > < template # default = "{ row }" > { { feeDate ( ( row as UnifiedOrderFeeDetailResource ) . businessDate ) } } < / template > < / el-table-column >
< el-table-column label = "渠道" min -width = " 130 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . channelName ) } } < / template > < / el-table-column >
< el-table-column label = "目的仓" min -width = " 150 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . destinationName ) } } < / template > < / el-table-column >
< el-table-column label = "FBA编号" min -width = " 130 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . fbaNo ) } } < / template > < / el-table-column >
< el-table-column label = "箱数" width = "90" > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . packageCount ) } } < / template > < / el-table-column >
< el-table-column label = "毛重" min -width = " 110 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . actualGrossWeightKg ) } } < / template > < / el-table-column >
< el-table-column label = "体积" min -width = " 110 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . actualVolumeCbm ) } } < / template > < / el-table-column >
< el-table-column label = "费用类型" min -width = " 150 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . feeTypeNameSnapshot ) } } < / template > < / el-table-column >
< el-table-column label = "货币" width = "90" > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . currency ) } } < / template > < / el-table-column >
< el-table-column label = "应收金额" min -width = " 120 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . amount ) } } < / template > < / el-table-column >
< el-table-column label = "计费体称/重量" min -width = " 150 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . chargeableQuantity ) } } { { ( row as UnifiedOrderFeeDetailResource ) . chargeUnit || '' } } < / template > < / el-table-column >
< el-table-column label = "单价" min -width = " 110 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . unitPrice ) } } < / template > < / el-table-column >
< el-table-column label = "入仓时间" min -width = " 150 " > < template # default = "{ row }" > { { feeDate ( ( row as UnifiedOrderFeeDetailResource ) . inboundedAt ) } } < / template > < / el-table-column >
< el-table-column label = "出仓时间" min -width = " 150 " > < template # default = "{ row }" > { { feeDate ( ( row as UnifiedOrderFeeDetailResource ) . outboundAt ) } } < / template > < / el-table-column >
< el-table-column label = "仓储天数" min -width = " 110 " > < template # default = "{ row }" > { { feeNumber ( ( row as UnifiedOrderFeeDetailResource ) . storageDays ) } } < / template > < / el-table-column >
< el-table-column label = "参与结算" min -width = " 110 " > < template # default = "{ row }" > { { ( row as UnifiedOrderFeeDetailResource ) . settlementIncluded == null ? '—' : ( row as UnifiedOrderFeeDetailResource ) . settlementIncluded ? '是' : '否' } } < / template > < / el-table-column >
< el-table-column label = "账单编号" min -width = " 140 " > < template # default = "{ row }" > { { feeText ( ( row as UnifiedOrderFeeDetailResource ) . billNo ) } } < / template > < / el-table-column >
< el-table-column label = "结算起始日" min -width = " 130 " > < template # default = "{ row }" > { { feeDate ( ( row as UnifiedOrderFeeDetailResource ) . settlementStartDate ) } } < / template > < / el-table-column >
< el-table-column label = "结算结束日" min -width = " 130 " > < template # default = "{ row }" > { { feeDate ( ( row as UnifiedOrderFeeDetailResource ) . settlementEndDate ) } } < / template > < / el-table-column >
< / el-table >
< / div >
< / template >
< template v-else-if = "activeDetailTab === 'history'" >
@@ -707,7 +915,28 @@ onMounted(() => {
< template v-else-if = "activeDetailTab === 'measurement'" >
< div class = "panel-title" > < div > < h2 > 测量数据 < / h2 > < p > { { detail . summary . measurement ? . completedAt ? ` 完成于 ${ formatDate ( detail . summary . measurement . completedAt ) } ` : '尚未完成测量' } } < / p > < / div > < Ruler :size = "19" / > < / div >
< dl class = "measurement-grid" > < div > < dt > 审核状态 < / dt > < dd > { { measurementStatusLabel ( detail . summary . measurement ? . reviewStatus ) } } < / 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 > { { formatNumber ( detail . summary . measurement ? . chargeableWeightKg ) } } kg < / dd > < / div > < / dl >
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.customer" @click ="toggleMeasurementSection('customer')" > < h3 > 一 、 客户下单数据 < / h3 > < span class = "measurement-section-summary" > < span > 总件数 : { { measurementTotals ( detail . order . cargoes , true ) . count ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . gross ) } } < / span > < span > 总体积 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . volume , 6 ) } } < / span > < span > 总材积重 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . volumetric ) } } < / span > < span > 重量体积 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . weightVolume , 6 ) } } < / span > < / span > < ChevronUp v-if = "!measurementCollapsed.customer" :size="18" /><ChevronDown v-else :size="18" / > < / button >
< div v-if = "!measurementCollapsed.customer" >
< div class = "detail-table-wrap" > < el-table :data = "detail.order.cargoes" 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 }" > {{ measurementValue ( row as Record < string , unknown > , column . key , true ) } } < / template > < / el-table-column > < / el-table > < / div >
< div class = "measurement-total" > < strong > 合计 < / strong > < span > 箱数 : { { measurementTotals ( detail . order . cargoes , true ) . count ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . gross ) } } < / span > < span > 总体积 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . volume , 6 ) } } < / span > < span > 总材积 : { { formatNumber ( measurementTotals ( detail . order . cargoes , true ) . volumetric ) } } < / span > < / div >
< / div >
< / section >
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.outer" @click ="toggleMeasurementSection('outer')" > < h3 > 二 、 外箱规数据 < / h3 > < span class = "measurement-section-summary" > < span > 总件数 : { { measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . count ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . gross ) } } < / span > < span > 总体积 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . volume , 6 ) } } < / span > < span > 总材积重 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . volumetric ) } } < / span > < span > 重量体积 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . weightVolume , 6 ) } } < / span > < / span > < ChevronUp v-if = "!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" / > < / button >
< div v-if = "!measurementCollapsed.outer" >
< div v-loading = "measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无外箱规数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }" > {{ measurementValue ( row as Record < string , unknown > , column . key ) } } < / template > < / el-table-column > < / el-table > < / div >
< div class = "measurement-total" > < strong > 合计 < / strong > < span > 箱数 : { { measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . count ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . gross ) } } < / span > < span > 总体积 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . volume , 6 ) } } < / span > < span > 总材积 : { { formatNumber ( measurementTotals ( ( measurementDetail ? . items || [ ] ) as unknown as Record < string , unknown > [ ] ) . volumetric ) } } < / span > < / div >
< / div >
< / section >
< section class = "measurement-section" >
< button type = "button" class = "measurement-section-title" :aria-expanded = "!measurementCollapsed.actual" @click ="toggleMeasurementSection('actual')" > < h3 > 三 、 实际测量数据 < / h3 > < span class = "measurement-section-summary" > < span > 总件数 : { { measurementDetail ? . actualPackageCount ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementDetail ? . actualGrossWeightKg ) } } < / span > < span > 总体积 : { { formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) } } < / span > < span > 总材积重 : { { formatNumber ( measurementDetail ? . volumeWeightKg ) } } < / span > < span > 重量体积 : { { formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) } } < / span > < / span > < ChevronUp v-if = "!measurementCollapsed.actual" :size="18" /><ChevronDown v-else :size="18" / > < / button >
< div v-if = "!measurementCollapsed.actual" >
< div v-loading = "measurementLoading" class="detail-table-wrap"><el-table :data="measurementDetail?.items || []" empty-text="暂无实际测量数据"><el-table-column v-for="column in measuredColumns" :key="column.key" :label="column.label" :prop="column.key" min-width="120"><template #default="{ row }" > {{ measurementValue ( row as Record < string , unknown > , column . key ) } } < / template > < / el-table-column > < / el-table > < / div >
< div class = "measurement-total" > < strong > 合计 < / strong > < span > 箱数 : { { measurementDetail ? . actualPackageCount ? ? '—' } } < / span > < span > 总毛重 : { { formatNumber ( measurementDetail ? . actualGrossWeightKg ) } } < / span > < span > 总体积 : { { formatNumber ( measurementDetail ? . actualVolumeCbm , 6 ) } } < / span > < span > 总材积 : { { formatNumber ( measurementDetail ? . volumeWeightKg ) } } < / span > < / div >
< div class = "measurement-media" > < h4 > 测量图片 < / h4 > < div v-if = "measurementImageUrls.length" class="measurement-media-grid"><el-image v-for="url in measurementImageUrls" :key="url" :src="url" fit="contain" :preview-src-list="measurementImageUrls" class="measurement-media-image" /></div><el-empty v-else description="暂无测量图片" :image-size="48" /><h4>测量视频</h4><el-empty description="暂无测量视频" :image-size="48" / > < / div >
< / div >
< / section >
< / template >
< template v-else >
@@ -797,6 +1026,7 @@ onMounted(() => {
< AppDialog v-model = "approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval" >
< el -alert title = "确认订单资料无误后审核通过,订单将进入待测量阶段。" type = "info" :closable = "false" show -icon / >
< / AppDialog >
< / section >
< / template >
@@ -860,7 +1090,22 @@ onMounted(() => {
. order - info - grid dd , . measurement - grid dd { min - width : 0 ; margin : 0 ; color : # 25324 a ; font - size : 12 px ; font - weight : 650 ; line - height : 1.35 ; overflow - wrap : anywhere ; }
. detail - table - wrap { overflow - x : auto ; }
. detail - table - wrap : deep ( . el - table ) { min - width : 720 px ; -- el - table - header - bg - color : # f8fafc ; }
. cargo - image { width : 72 px ; height : 56 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : # f8fafc ; }
. measurement - grid { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : 1 px ; margin : 0 ; border : 1 px solid var ( -- cc - border ) ; background : var ( -- cc - border ) ; }
. measurement - section { margin - top : 18 px ; }
. measurement - section - title { display : flex ; width : 100 % ; min - height : 48 px ; align - items : center ; gap : 16 px ; padding : 9 px 14 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 7 px 7 px 0 0 ; background : # f8fafc ; color : var ( -- cc - text ) ; font : inherit ; text - align : left ; cursor : pointer ; }
. measurement - section - title : hover { background : # f2f6fb ; }
. measurement - section - title : focus - visible { outline : 2 px solid var ( -- cc - primary ) ; outline - offset : 2 px ; }
. measurement - section - title h3 { flex : 0 0 auto ; margin : 0 ; color : var ( -- cc - text ) ; font - size : 14 px ; }
. measurement - section - summary { display : flex ; min - width : 0 ; flex : 1 ; align - items : center ; justify - content : flex - end ; gap : 18 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; white - space : nowrap ; }
. measurement - section - title > svg { flex : 0 0 auto ; color : var ( -- cc - muted ) ; }
. measurement - total { display : flex ; min - height : 42 px ; align - items : center ; justify - content : flex - end ; gap : 24 px ; padding : 9 px 14 px ; border : 1 px solid var ( -- cc - border ) ; background : # f8fafc ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
. measurement - total { border - top : 0 ; border - radius : 0 0 7 px 7 px ; }
. measurement - total strong { margin - right : auto ; color : var ( -- cc - text ) ; font - size : 13 px ; }
. measurement - media { margin - top : 20 px ; }
. measurement - media h4 { margin : 18 px 0 10 px ; color : var ( -- cc - text ) ; font - size : 14 px ; }
. measurement - media - grid { display : flex ; flex - wrap : wrap ; gap : 12 px ; }
. measurement - media - image { width : 110 px ; height : 90 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 6 px ; background : # f8fafc ; }
. history - list , . notes - list { display : grid ; gap : 0 ; margin : 0 ; padding : 0 ; list - style : none ; }
. history - list li { position : relative ; display : grid ; grid - template - columns : 16 px minmax ( 0 , 1 fr ) ; gap : 12 px ; padding - bottom : 20 px ; }
. history - list li : not ( : last - child ) : : after { position : absolute ; top : 15 px ; bottom : 0 ; left : 7 px ; width : 1 px ; background : var ( -- cc - border ) ; content : '' ; }
@@ -875,6 +1120,6 @@ onMounted(() => {
. notes - list li > p { margin : 0 ; color : var ( -- cc - text ) ; font - size : 13 px ; }
. notes - list span { display : block ; margin - top : 5 px ; color : var ( -- cc - muted ) ; font - size : 12 px ; }
@ media ( max - width : 960 px ) { . detail - overview { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . detail - overview article : nth - child ( 2 ) { border - right : 0 ; } . detail - overview article : nth - child ( - n + 2 ) { border - bottom : 1 px solid var ( -- cc - border ) ; } . order - info - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . measurement - grid { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } }
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep ( . el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep ( . el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { display : grid ; grid - template - columns : auto 36 px minmax ( 0 , 1 fr ) auto ; } . detail - header - actions { display : grid ; grid - template - columns : 1 fr ; } . detail - header - actions : deep ( . el - button ) { min - height : 40 px ; } . detail - overview , . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . detail - overview article { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border ) ; } . detail - overview article : last - child { border - bottom : 0 ; } . order - info - grid > div . wide { grid - column : auto ; } . detail - tabs { grid - template - columns : repeat ( 6 , 132 px ) ; } . detail - panel { padding : 16 px ; } }
@ media ( max - width : 720 px ) { . page - intro { align - items : stretch ; flex - direction : column ; } . page - intro : deep ( . el - button ) { width : 100 % ; min - height : 44 px ; } . status - tab { min - height : 46 px ; padding : 0 14 px ; } . table - actions : deep ( . el - button ) { width : 100 % ; min - height : 40 px ; } . row - actions { flex - wrap : wrap ; } . order - detail - header { align - items : stretch ; flex - direction : column ; } . order - detail - heading { display : grid ; grid - template - columns : auto 36 px minmax ( 0 , 1 fr ) auto ; } . detail - header - actions { display : grid ; grid - template - columns : 1 fr ; } . detail - header - actions : deep ( . el - button ) { min - height : 40 px ; } . detail - overview , . order - info - grid , . measurement - grid { grid - template - columns : 1 fr ; } . detail - overview article { border - right : 0 ; border - bottom : 1 px solid var ( -- cc - border ) ; } . detail - overview article : last - child { border - bottom : 0 ; } . order - info - grid > div . wide { grid - column : auto ; } . detail - tabs { grid - template - columns : repeat ( 6 , 132 px ) ; } . detail - panel { padding : 16 px ; } . measurement - section - title { align - items : flex - start ; flex - wrap : wrap ; padding : 12 px ; } . measurement - section - summary { width : 100 % ; flex - basis : calc ( 100 % - 34 px ) ; flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; white - space : normal ; } . measurement - section - title > svg { margin - left : auto ; } . measurement - total { flex - wrap : wrap ; justify - content : flex - start ; gap : 6 px 16 px ; } . measurement - total strong { width : 100 % ; } }
@ media ( max - width : 420 px ) { . order - detail - heading { grid - template - columns : auto minmax ( 0 , 1 fr ) auto ; } . order - mark { display : none ; } }
< / style >