@@ -1,9 +1,12 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref , watch } from 'vue'
import { ChevronDown , ChevronUp , ClipboardList , Eye , RefreshCw , SlidersHorizontal } from '@lucide/vue '
import { computed , nextTick , onMounted , reactive , ref , watch } from 'vue'
import { ElMessage , type FormInstance , type FormRules } from 'element-plus '
import { ChevronDown , ChevronUp , CircleCheck , ClipboardList , Eye , RefreshCw , SlidersHorizontal } from '@lucide/vue'
import { useRoute , useRouter } from 'vue-router'
import {
getPlatformUnifiedOrder ,
approvePlatformUnifiedOrder ,
assignPlatformUnifiedOrderCarrier ,
listPlatformUnifiedOrders ,
unifiedOrderStatusOptions ,
type UnifiedOrderDetailResource ,
@@ -12,9 +15,11 @@ import {
} from '@/modules/unified-orders/api/unifiedOrders'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { listPlatformPartners , type PlatformPartnerResource } from '@/modules/master-data'
import { listAllPlatformWarehouses , type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
@@ -62,8 +67,20 @@ const detail = ref<UnifiedOrderDetailResource | null>(null)
const detailLoading = ref ( false )
const detailError = ref < ApiErrorDetails | null > ( null )
const detailOpen = ref ( false )
const approvalOpen = ref ( false )
const approvalLoadingId = ref ( '' )
const approvalSaving = ref ( false )
const carrierLoading = ref ( false )
const carrierLoadError = ref < ApiErrorDetails | null > ( null )
const carriers = ref < PlatformPartnerResource [ ] > ( [ ] )
const approvalOrder = ref < UnifiedOrderResource | null > ( null )
const carrierFormRef = ref < FormInstance > ( )
const carrierForm = reactive ( { carrierId : '' } )
const showAdvancedFilters = ref ( false )
const filters = ref < FilterModel > ( emptyFilters ( ) )
const carrierRules : FormRules = {
carrierId : [ { required : true , message : '请选择承运商' , trigger : 'change' } ] ,
}
const orderId = computed ( ( ) => typeof route . params . orderId === 'string' ? route . params . orderId : '' )
@@ -208,10 +225,85 @@ const nextActionLabels: Record<string, string> = {
}
function nextActionBoundary ( row : UnifiedOrderResource ) {
if ( row . nextAction === 'APPROVE_ORDER' ) return ''
const label = row . nextAction ? nextActionLabels [ row . nextAction ] || row . nextAction : ''
return label ? ` 待接入: ${ label } ` : ''
}
function canApprove ( row : UnifiedOrderResource ) {
return row . mainStatus === 'PENDING_REVIEW' && row . nextAction === 'APPROVE_ORDER' && can ( 'platform:order:approve' )
}
async function loadCarriers ( ) {
carrierLoading . value = true
carrierLoadError . value = null
try {
const records : PlatformPartnerResource [ ] = [ ]
let currentPage = 1
let pages = 1
do {
const result = await listPlatformPartners ( { page : currentPage , size : 200 , type : 'CARRIER' , status : 'ACTIVE' } )
records . push ( ... result . records )
pages = Math . max ( 1 , result . pages )
currentPage += 1
} while ( currentPage <= pages )
carriers . value = records . filter ( ( item ) => item . status === 'ACTIVE' && item . types . includes ( 'CARRIER' ) )
} catch ( error ) {
carriers . value = [ ]
carrierLoadError . value = getApiErrorDetails ( error , '承运商列表加载失败,请稍后重试' )
} finally {
carrierLoading . value = false
}
}
async function openApproval ( row : UnifiedOrderResource ) {
if ( ! canApprove ( row ) ) return
approvalOrder . value = row
const currentSnapshot = detail . value ? . summary . orderId === row . orderId ? detail . value . order . order : null
carrierForm . carrierId = currentSnapshot ? . carrierId == null ? '' : String ( currentSnapshot . carrierId )
approvalOpen . value = true
await nextTick ( )
carrierFormRef . value ? . clearValidate ( )
await loadCarriers ( )
}
function closeApproval ( ) {
if ( approvalSaving . value ) return
approvalOpen . value = false
approvalOrder . value = null
carrierForm . carrierId = ''
}
async function assignCarrierAndApprove ( ) {
if ( ! approvalOrder . value ) return
const valid = await carrierFormRef . value ? . validate ( ) . catch ( ( ) => false )
if ( ! valid ) return
const carrier = carriers . value . find ( ( item ) => String ( item . id ) === carrierForm . carrierId )
if ( ! carrier ) {
ElMessage . error ( '所选承运商已停用或不存在,请重新选择' )
return
}
const order = approvalOrder . value
const orderId = String ( order . orderId )
const tenantId = String ( order . tenantId )
approvalSaving . value = true
approvalLoadingId . value = orderId
try {
await assignPlatformUnifiedOrderCarrier ( tenantId , orderId , String ( carrier . id ) )
await approvePlatformUnifiedOrder ( tenantId , orderId )
approvalOpen . value = false
await loadOrders ( )
if ( detail . value ? . summary . orderId === order . orderId ) await loadDetail ( )
approvalOrder . value = null
ElMessage . success ( '承运商已指派,订单审核通过' )
} catch ( error ) {
ElMessage . error ( getApiErrorDetails ( error , '承运商指派或订单审核失败,请稍后重试' ) . message )
} finally {
approvalSaving . value = false
approvalLoadingId . value = ''
}
}
async function loadCustomers ( ) {
customerLoading . value = true
customerLoadError . value = null
@@ -397,6 +489,7 @@ onMounted(() => {
< template # actions = "{ row }" >
< div class = "row-actions" >
< el-button link type = "primary" @click ="openDetail(row as UnifiedOrderResource)" > < Eye :size = "15" / > 详情 < / el-button >
< el-button v-if = "canApprove(row as UnifiedOrderResource)" link type="success" :loading="approvalLoadingId === String((row as UnifiedOrderResource).orderId)" @click="openApproval(row as UnifiedOrderResource)"><CircleCheck :size="15" / > 审核通过 < / el-button >
< el-button v-if = "(row as UnifiedOrderResource).mainStatus === 'WAIT_MEASURE' && (row as UnifiedOrderResource).inboundId && can('platform:firstleg:inbound:view')" link type="primary" @click="openMeasurement(row as UnifiedOrderResource)" > 测量作业 < / el -button >
< el-button v-if = "workflowEligible(row as UnifiedOrderResource) && can('platform:firstleg:inbound:view')" link type="primary" @click="openWorkflow(row as UnifiedOrderResource)"><ClipboardList :size="15" / > 后半程作业 < / el-button >
< span v-if = "nextActionBoundary(row as UnifiedOrderResource)" class="action-boundary" > {{ nextActionBoundary ( row as UnifiedOrderResource ) }} < / span >
@@ -418,13 +511,29 @@ onMounted(() => {
< LoadErrorState v-if = "detailError" title="订单详情暂时无法加载" :message="detailError.message" :loading="detailLoading" @retry="loadDetail"><template #details><ApiErrorMeta :details="detailError" / > < / template > < / LoadErrorState >
< div v-else-if = "detail" class="detail-content" >
< section class = "detail-summary" aria -label = " 订单概览 " >
< div class = "detail-summary__header" > < div > < span class = "eyebrow" > 订单主状态 < / span > < h2 > { { detail . summary . orderNo || ` 订单 ${ detail . summary . orderId } ` } } < / h2 > < / div > < div class = "detail-summary__actions" > < StatusBadge :status = "statusLabel(detail.summary.mainStatus)" / > < el-button v-if = "workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" plain @click="openWorkflow(detail.summary)"><ClipboardList :size="15" / > 后半程作业 < / el-button > < / div > < / div >
< dl class = "detail-grid" > < div > < dt > 客户 < / dt > < dd > { { customerName ( detail . summary ) } } < / dd > < / div > < div > < dt > 入仓码 < / dt > < dd > { { detail . summary . inboundCode || '—' } } < / dd > < / div > < div > < dt > 平台收货仓 < / dt > < dd > { { detail . summary . receivingWarehouseName || '—' } } < / dd > < / div > < div > < dt > 目的仓 < / dt > < dd > { { detail . summary . destinationWarehouseName || '—' } } < / dd > < / div > < div > < dt > 下一步 < / dt > < dd > { { nextActionBoundary ( detail . summary ) || '—' } } < / dd > < / div > < div > < dt > 最近更新 < / dt > < dd > { { formatDate ( detail . summary . updatedAt ) } } < / dd > < / div > < / dl >
< div class = "detail-summary__header" > < div > < span class = "eyebrow" > 订单主状态 < / span > < h2 > { { detail . summary . orderNo || ` 订单 ${ detail . summary . orderId } ` } } < / h2 > < / div > < div class = "detail-summary__actions" > < StatusBadge :status = "statusLabel(detail.summary.mainStatus)" / > < 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><el-button v-if=" workflowEligible(detail.summary) && can('platform:firstleg:inbound:view')" type="primary" plain @click="openWorkflow(detail.summary)"><ClipboardList :size="15" / > 后半程作业 < / el-button > < / div > < / div >
< dl class = "detail-grid" > < div > < dt > 客户 < / dt > < dd > { { customerName ( detail . summary ) } } < / dd > < / div > < div > < dt > 入仓码 < / dt > < dd > { { detail . summary . inboundCode || '—' } } < / dd > < / div > < div > < dt > 平台收货仓 < / dt > < dd > { { detail . summary . receivingWarehouseName || '—' } } < / dd > < / div > < div > < dt > 目的仓 < / dt > < dd > { { detail . summary . destinationWarehouseName || '—' } } < / dd > < / div > < div > < dt > 承运商 < / dt > < dd > { { detail . order . order ? . carrierName || '待平台指派' } } < / dd > < / div > < div > < dt > 下一步 < / dt > < dd > { { canApprove ( detail . summary ) ? '选择承运商并审核' : nextActionBoundary ( detail . summary ) || '—' } } < / dd > < / div > < div > < dt > 最近更新 < / dt > < dd > { { formatDate ( detail . summary . updatedAt ) } } < / dd > < / div > < / dl >
< / section >
< section class = "detail-section" aria -label = " 状态历史 " > < h3 > 状态历史 < / h3 > < el-empty v-if = "detail.timeline.length === 0" description="暂无状态历史" :image-size="56" /><ol v-else class="history-list"><li v-for="(item, index) in detail.timeline" :key="`${item.source || 'event'}-${item.occurredAt || index}`"><StatusBadge :status="statusLabel(item.mainStatus)" /><div><strong>{{ item.action || item.eventType || '状态更新' }}</strong><p>{{ item.operatorName || '系统' }} · {{ formatDate(item.occurredAt) }}</p><p v-if="item.remark" > {{ item.remark }} < / p > < / div > < / li > < / ol > < / section >
< section class = "detail-section" aria -label = " 内部备注 " > < h3 > 内部备注 < / h3 > < el-empty v-if = "detail.order.internalNotes.length === 0" description="暂无内部备注" :image-size="56" /><ul v-else class="notes-list"><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 >
< / div >
< / AppDrawer >
< AppDialog v-model = "approvalOpen" title="选择承运商并审核" confirm-text="指派并审核通过" confirm-type="success" :loading="carrierLoading" :confirm-loading="approvalSaving" :confirm-disabled="Boolean(carrierLoadError) || carriers.length === 0" destroy-on-close @confirm="assignCarrierAndApprove" @cancel="closeApproval" >
< el -alert title = "订单审核通过后将进入履约流程;承运商名称由后端公共主数据生成快照。" type = "info" :closable = "false" show -icon / >
< el-alert v-if = "carrierLoadError" class="carrier-load-error" type="error" :closable="false" show -icon >
< template # title > < div class = "api-error-alert-title" > < span > { { carrierLoadError . message } } < / span > < ApiErrorMeta :details = "carrierLoadError" / > < / div > < / template >
< el-button link type = "danger" :loading = "carrierLoading" @click ="loadCarriers" > 重新加载 < / el -button >
< / el-alert >
< el-alert v-else-if = "!carrierLoading && carriers.length === 0" class="carrier-load-error" title="暂无启用的承运商,请先到往来单位维护承运商主数据。" type="warning" :closable="false" show -icon / >
< el-form ref = "carrierFormRef" :model = "carrierForm" :rules = "carrierRules" label -position = " top " class = "carrier-form" : disabled = "carrierLoading || approvalSaving || Boolean(carrierLoadError)" >
< el-form-item label = "承运商" prop = "carrierId" required >
< el-select v-model = "carrierForm.carrierId" filterable :loading="carrierLoading" placeholder="请选择启用的承运商" >
< el -option v-for = "carrier in carriers" :key="String(carrier.id)" :label="`${carrier.partnerName}( ${carrier.partnerCode}) `" :value="String(carrier.id)" / >
< / el-select >
< / el-form-item >
< / el-form >
< / AppDialog >
< / section >
< / template >
@@ -446,6 +555,9 @@ onMounted(() => {
. table - card { margin - top : 16 px ; overflow : hidden ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; background : # fff ; }
. row - actions { display : flex ; align - items : center ; gap : 4 px ; white - space : nowrap ; }
. row - actions : deep ( . el - button ) { margin - left : 0 ; }
. carrier - form { margin - top : 18 px ; }
. carrier - form : deep ( . el - select ) { width : 100 % ; }
. carrier - load - error { margin - top : 14 px ; }
. primary - cell { display : grid ; min - width : 0 ; gap : 4 px ; }
. primary - cell strong { overflow - wrap : anywhere ; }
. primary - cell span { color : var ( -- cc - muted ) ; font - size : 12 px ; overflow - wrap : anywhere ; }