@@ -1,5 +1,5 @@
< script setup lang = "ts" >
import { computed , onBeforeUnmount , onMounted , ref } from 'vue'
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import {
ArrowLeft , BarChart3 , Box , Building2 , CircleDollarSign , ClipboardList , Eye ,
@@ -20,8 +20,15 @@ import {
type PlatformTenantResource ,
type PlatformTenantStatus ,
} from '@/modules/iam'
import {
listPlatformFirstLegOrders ,
type PlatformFirstLegOrderResource ,
type PlatformFirstLegOrderStatus ,
} from '@/modules/first-leg-orders'
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBillingRulesPanel.vue'
@@ -40,6 +47,12 @@ const customer = ref<PlatformTenantResource | null>(null)
const qualifications = ref < PlatformTenantQualification [ ] > ( [ ] )
const members = ref < PlatformTenantMember [ ] > ( [ ] )
const orderCount = ref < number | null > ( null )
const customerOrders = ref < PlatformFirstLegOrderResource [ ] > ( [ ] )
const customerOrderTotal = ref ( 0 )
const orderPage = ref ( 1 )
const orderPageSize = ref ( 10 )
const customerOrdersLoading = ref ( false )
const customerOrdersError = ref < ApiErrorDetails | null > ( null )
const previews = ref < Record < string , PreviewState > > ( { } )
const previewOpen = ref ( false )
const previewName = ref ( '' )
@@ -59,6 +72,16 @@ const tabs: Array<{ key: DetailTab; label: string; icon: typeof ClipboardList }>
const approvedQualifications = computed ( ( ) => qualifications . value . filter ( ( item ) => item . status === 'APPROVED' ) . length )
const enterpriseMembers = computed ( ( ) => members . value . filter ( ( member ) => [ 'OWNER' , 'EMPLOYEE' ] . includes ( member . memberType ) ) )
const activeEnterpriseMembers = computed ( ( ) => enterpriseMembers . value . filter ( ( member ) => member . status === 'ACTIVE' ) . length )
const orderColumns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 92 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'status' , label : '状态' , width : 106 , slotName : 'status' } ,
{ key : 'orderNo' , label : '运单号' , minWidth : 180 , slotName : 'orderNo' } ,
{ key : 'transport' , label : '运输方式' , minWidth : 150 , slotName : 'transport' } ,
{ key : 'destination' , label : '目的地' , minWidth : 170 , slotName : 'destination' } ,
{ key : 'receivingWarehouse' , label : '平台收货仓' , minWidth : 180 , slotName : 'receivingWarehouse' } ,
{ key : 'expectedShipDate' , label : '预计出运' , width : 128 , slotName : 'expectedShipDate' } ,
{ key : 'createdAt' , label : '创建时间' , width : 150 , slotName : 'createdAt' } ,
]
function displayText ( value ? : string | null ) { return value ? . trim ( ) || '—' }
function formatDate ( value ? : string | null ) { return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) : '—' }
@@ -89,6 +112,33 @@ function qualificationName(item: PlatformTenantQualification) {
try { return qualificationFileName ( item . documentUrl ) }
catch { return ` ${ qualificationLabel ( item . qualificationType ) } 文件 ` }
}
function orderStatusLabel ( status : PlatformFirstLegOrderStatus ) {
return ( { DRAFT : '草稿' , SUBMITTED : '待审核' , APPROVED : '已审核' , ACCEPTED : '已受理' , PROCESSING : '处理中' , COMPLETED : '已完成' , CANCELLED : '已取消' , REJECTED : '已驳回(旧契约)' } as Record < PlatformFirstLegOrderStatus , string > ) [ status ] || status
}
function transportLabel ( order : PlatformFirstLegOrderResource ) {
if ( order . transportMode === 'SEA' ) return order . containerMode === 'FULL_CONTAINER' ? '海运整箱 FCL' : '海运拼箱 LCL'
if ( order . transportMode === 'AIR' ) return '空运'
if ( order . transportMode === 'RAIL' ) return '铁路运输'
return '陆运'
}
function formattedDate ( value ? : string | null ) { return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 16 ) : '—' }
async function loadCustomerOrders ( page = orderPage . value ) {
if ( ! access . can ( 'platform:order:list' ) ) return
customerOrdersLoading . value = true
customerOrdersError . value = null
try {
const result = await listPlatformFirstLegOrders ( { tenantId : tenantId . value , page , size : orderPageSize . value } )
customerOrders . value = result . records
customerOrderTotal . value = result . total
if ( orderCount . value === null ) orderCount . value = result . total
} catch ( error ) {
customerOrders . value = [ ]
customerOrdersError . value = getApiErrorDetails ( error , '运单记录加载失败,请稍后重试' )
} finally {
customerOrdersLoading . value = false
}
}
function clearPreviews ( ) {
for ( const preview of Object . values ( previews . value ) ) if ( preview . url ) URL . revokeObjectURL ( preview . url )
@@ -145,9 +195,18 @@ async function loadDetail() {
const optional : Promise < void > [ ] = [ ]
if ( access . can ( 'platform:order:list' ) ) {
orderPage . value = 1
customerOrders . value = [ ]
customerOrderTotal . value = 0
optional . push ( listPlatformTenantOrderCounts ( [ tenantId . value ] ) . then ( ( records ) => {
orderCount . value = Number ( records . find ( ( item ) => String ( item . tenantId ) === tenantId . value ) ? . orderCount ? ? 0 )
} ) . catch ( ( ) => { orderCount . value = null } ) )
optional . push ( loadCustomerOrders ( 1 ) )
} else {
customerOrdersLoading . value = false
customerOrdersError . value = null
customerOrders . value = [ ]
customerOrderTotal . value = 0
}
await Promise . all ( optional )
} catch ( error ) {
@@ -158,6 +217,10 @@ async function loadDetail() {
} finally { loading . value = false }
}
function openOrder ( order : PlatformFirstLegOrderResource ) {
void router . push ( { name : 'unified-order-detail' , params : { orderId : String ( order . id ) } , query : { tenantId : tenantId . value } } )
}
async function changeStatus ( ) {
if ( ! customer . value || statusChanging . value ) return
const next = customer . value . status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
@@ -190,6 +253,9 @@ function handleTabKeydown(event: KeyboardEvent, key: DetailTab) {
}
onMounted ( loadDetail )
onBeforeUnmount ( clearPreviews )
watch ( orderPage , ( page , previousPage ) => {
if ( page !== previousPage ) void loadCustomerOrders ( page )
} )
< / script >
< template >
@@ -294,8 +360,22 @@ onBeforeUnmount(clearPreviews)
< / section >
< section v-else-if = "activeTab === 'orders'" class="detail-panel module-panel" >
< div class = "panel-title" > < div > < h2 > 运单记录 < / h2 > < p > 客户关联头程运单 < / p > < / div > < Ship :size = "19" / > < / div >
< div class = "module-empty" > < Ship :size = "30" / > < strong > 当前客户累计 { { orderCount ? ? '—' } } 票运单 < / strong > < p > 现有平台接口仅提供客户维度运单数量 , 尚未提供详情页专用明细查询契约 。 < / p > < el-button v-if = "access.can('platform:order:list')" type="primary" plain @click="router.push('/first-leg-orders')" > 前往运单管理 < / el -button > < / div >
< div class = "panel-title" > < div > < h2 > 运单记录 < / h2 > < p > 客户关联头程运单 , 共 { { customerOrderTotal } } 票 < / p > < / div > < Ship :size = "19" / > < / div >
< template v-if = "access.can('platform:order:list')" >
< el -alert v-if = "customerOrdersError" type="error" :closable="false" show-icon role="alert"><template #title><div class="api-error-alert-title"><span>{{ customerOrdersError.message }}</span><ApiErrorMeta :details="customerOrdersError" /></div></template><el-button link type="danger" @click="loadDetail" > 重新加载 < / el -button > < / el-alert >
< DataTable v-else v -model :page = "orderPage" v -model :page-size = "orderPageSize" :data = "customerOrders" :columns = "orderColumns" :loading = "customerOrdersLoading" :total = "customerOrderTotal" :page-sizes = "[10]" empty -text = " 当前客户暂无运单记录 " row -key = " id " >
< template # actions = "{ row }" > < el-button link type = "primary" @click ="openOrder(row as PlatformFirstLegOrderResource)" > < Eye :size = "15" / > 详情 < / el-button > < / template >
< template # status = "{ row }" > < StatusBadge : status = "orderStatusLabel((row as PlatformFirstLegOrderResource).status)" / > < / template >
< template # orderNo = "{ row }" > < strong > { { ( row as PlatformFirstLegOrderResource ) . orderNo || '—' } } < / strong > < / template >
< template # transport = "{ row }" > < span > { { transportLabel ( row as PlatformFirstLegOrderResource ) } } < / span > < / template >
< template # destination = "{ row }" > < div class = "primary-cell" > < strong > { { ( row as PlatformFirstLegOrderResource ) . destinationWarehouseName || '—' } } < / strong > < span > { { ( row as PlatformFirstLegOrderResource ) . destinationCountryCode || '—' } } < / span > < / div > < / template >
< template # receivingWarehouse = "{ row }" > < span > { { ( row as PlatformFirstLegOrderResource ) . receivingWarehouseName || '—' } } < / span > < / template >
< template # expectedShipDate = "{ row }" > < span > { { formattedDate ( ( row as PlatformFirstLegOrderResource ) . expectedShipDate ) . slice ( 0 , 10 ) } } < / span > < / template >
< template # createdAt = "{ row }" > < span > { { formattedDate ( ( row as PlatformFirstLegOrderResource ) . createdAt ) } } < / span > < / template >
< template # empty > < el-empty description = "当前客户暂无运单记录" / > < / template >
< / DataTable >
< / template >
< div v-else class = "module-empty" > < Ship :size = "30" / > < strong > 暂无运单查看权限 < / strong > < p > 请联系管理员申请头程订单查看权限 。 < / p > < / div >
< / section >
< section v-else-if = "activeTab === 'finance'" class="detail-panel module-panel"><div class="panel-title"><div><h2>财务账单</h2><p>客户应收、回款与对账记录</p></div><ReceiptText :size="19" /></div><div class="module-empty"><ReceiptText :size="30" / > < strong > 财务账单尚未接入 < / strong > < p > 当前阶段没有可用的客户账单 API , 页面不会使用原型中的演示金额 。 < / p > < / div > < / section >