@@ -7,7 +7,6 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import PageToolbar from '@/shared/components/PageToolbar.vue'
import { getApiErrorDetails , type ApiErrorDetails } from '@/shared/api/http'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getTruckAppointment , listTruckAppointmentAttachments , listTruckAppointmentHistory , performTruckAppointmentAction , rescheduleTruckAppointment , updateTruckAppointment , type TruckAppointmentAction } from '../api/platformTrucking'
@@ -17,6 +16,7 @@ import { truckAppointmentActionLabel, truckAppointmentStatusLabel, truckAppointm
const route = useRoute ( ) ; const router = useRouter ( ) ; const loading = ref ( false ) ; const saving = ref ( false ) ; const statusSaving = ref ( false ) ; const attachmentOpen = ref ( false ) ; const resource = ref < TruckAppointmentResource | null > ( null ) ; const error = ref < ApiErrorDetails | null > ( null ) ; const form = reactive < SaveTruckAppointmentRequest > ( { expectedVersion : 0 , customerId : null , carrierId : null , plannedLoadingAt : null } )
const history = ref < TruckAppointmentStatusHistory [ ] > ( [ ] )
const access = useAccessPreviewStore ( ) ; const editing = computed ( ( ) => route . query . edit === '1' ) ; const id = computed ( ( ) => String ( route . params . id ) ) ; const canEdit = computed ( ( ) => access . can ( 'platform:firstleg:truck-appointment:edit' ) && [ 'DRAFT' , 'PENDING_DISPATCH' , 'RESCHEDULED' ] . includes ( resource . value ? . status || '' ) && editing . value ) ; const canChangeStatus = computed ( ( ) => access . can ( 'platform:firstleg:truck-appointment:operate' ) ) ; const canManageAttachments = computed ( ( ) => access . can ( 'platform:firstleg:truck-appointment:edit' ) )
const relationSnapshot = computed ( ( ) => resource . value ? [ resource . value . customerName , resource . value . oceanBookingNo , resource . value . cabinetNo , resource . value . containerNo , resource . value . warehouseName ] . filter ( Boolean ) . join ( ' / ' ) || '无历史关联' : '' )
async function loadRelated ( ) { if ( ! resource . value ) return ; const [ attachments , histories ] = await Promise . all ( [ listTruckAppointmentAttachments ( id . value ) , listTruckAppointmentHistory ( id . value ) ] ) ; resource . value . attachments = attachments ; history . value = histories }
async function load ( ) { loading . value = true ; error . value = null ; try { const result = await getTruckAppointment ( id . value ) ; resource . value = result ; Object . assign ( form , { ... result , expectedVersion : result . version ? ? 0 } ) ; await loadRelated ( ) } catch ( err ) { error . value = getApiErrorDetails ( err , '详情加载失败,请稍后重试' ) } finally { loading . value = false } }
async function submit ( ) { saving . value = true ; error . value = null ; try { await updateTruckAppointment ( id . value , form ) ; ElMessage . success ( '拖车预约已保存' ) ; await load ( ) } catch ( err ) { error . value = getApiErrorDetails ( err , '保存失败,请稍后重试' ) } finally { saving . value = false } }
@@ -46,14 +46,22 @@ onMounted(load)
< AdminPage as = "div" class = "truck-detail-page" >
< DetailPageLayout aria -label = " 拖车预约详情 " >
< template # header >
< div class = "detail-heading" >
< el-button @click ="router.push({ name: 'trucking' })" > < ArrowLeft :size = "16" / > 返回列表 < / el-button >
< PageToolbar v-if = "resource" class="truck-page-toolbar" as="section" :title="resource.appointmentNo || '拖车预约'" :description="[resource.customerName, resource.orderNo].filter(Boolean).jo in( ' · ' )" / >
< / div >
< section class = "truck-overview surface-panel" aria -label = " 拖车预约概览 " >
< div class = "truck-overview__heading" >
< el-button link type = "primary" @click ="router.push({ name: 'truck ing ' } )"> < ArrowLeft :size = "16" / > 返回列表 < / el-button >
< div class = "truck-overview__identity" > < h1 > { { resource ? . appointmentNo || '拖车预约详情' } } < / h1 > < p > { { resource ? [ resource . customerName , resource . orderNo ] . filter ( Boolean ) . join ( ' · ' ) || '客户信息缺失' : '查看拖车预约详情' } } < / p > < / div >
< span v-if = "resource" class="truck-overview__status" > 当前状态 : < strong > { { truckAppointmentStatusLabel ( resource . status ) } } < / strong > < / span >
< div v-if = "resource && canChangeStatus" class="status-actions"><el-button v-for="status in availableTargets" :key="status" size="small" :type="status === 'CANCELLED' ? 'danger' : status === 'EXCEPTION' ? 'warning' : 'primary'" plain :loading="statusSaving" @click="changeStatus(status)" > {{ truckAppointmentStatusLabels [ status ] }} < / el -button > < / div >
< / div >
< template v-if = "resource" >
< p class = "relation-snapshot" > < strong > 历史关联快照 : < / strong > { { relationSnapshot } } < / p >
< el-alert v-if = "dispatchBlockReason" :title="dispatchBlockReason" type="warning" :closable="false" show -icon / >
< ApiErrorMeta v-if = "error" :details="error" / >
< / template >
< / section >
< / template >
< LoadErrorState v-if = "error && !resource" title="详情加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" / > < / template > < / LoadErrorState >
< template v-else-if = "resource" >
< section class = "surface-panel status-panel" > < div class = "status-bar" > < span > 当前状态 : < strong > { { truckAppointmentStatusLabel ( resource . status ) } } < / strong > < / span > < div v-if = "canChangeStatus" class="status-actions"><el-button v-for="status in availableTargets" :key="status" :type="status === 'CANCELLED' ? 'danger' : status === 'EXCEPTION' ? 'warning' : 'primary'" plain :loading="statusSaving" @click="changeStatus(status)">{{ truckAppointmentStatusLabels[status] }}</el-button></div></div><el-alert v-if="dispatchBlockReason" :title="dispatchBlockReason" type="warning" :closable="false" show-icon /><ApiErrorMeta v-if="error" :details="error" / > < / section >
< section class = "surface-panel form-panel" > < TruckAppointmentForm v-model = "form" :resource="resource" :readonly="!canEdit" :submitting="saving" @submit="submit" @attachments="attachmentOpen = true" / > < / section >
< section class = "surface-panel history-section" > < h3 > 状态历史 < / h3 > < el-empty v-if = "!history.length" description="暂无状态历史" :image-size="48" /><ol v-else><li v-for="(item, index) in history" :key="item.id || `${item.createdAt}-${index}`"><strong>{{ truckAppointmentActionLabel(item.action) }}</strong><span>{{ truckAppointmentStatusLabel(item.fromStatus) }} → {{ truckAppointmentStatusLabel(item.toStatus) }}</span><small>{{ item.operatorName || item.operatorId || '系统' }} · {{ item.createdAt || item.occurredAt || '—' }}<template v-if="item.reason" > · {{ item.reason }} < / template > < / small > < / li > < / ol > < / section >
< TruckAttachmentDialog v-model = "attachmentOpen" :appointment-id="id" :attachments="resource.attachments || []" :can-manage="canManageAttachments" @changed="loadRelated" / >
@@ -62,9 +70,12 @@ onMounted(load)
< / AdminPage >
< / template >
< style scoped >
. status - bar { display : flex ; align - items : center ; justify - content : space - between ; gap : 12 px ; flex - wrap : wrap ; margin - bottom : 16 px ; padding : 12 px ; background : var ( -- el - fill - color - light ) ; border - radius : 6 px ; }
. truck - overview { overflow : hidden ; }
. truck - overview _ _heading { display : grid ; grid - template - columns : auto minmax ( 260 px , 1 fr ) auto auto ; align - items : center ; gap : 12 px 18 px ; padding : 12 px 16 px ; }
. truck - overview _ _identity { min - width : 0 ; } . truck - overview _ _identity h1 { margin : 0 ; overflow : hidden ; color : var ( -- cc - text ) ; font - size : 18 px ; line - height : 1.35 ; text - overflow : ellipsis ; white - space : nowrap ; } . truck - overview _ _identity p { margin : 2 px 0 0 ; overflow : hidden ; color : var ( -- cc - muted ) ; font - size : 12 px ; text - overflow : ellipsis ; white - space : nowrap ; }
. truck - overview _ _status { font - size : 13 px ; white - space : nowrap ; } . truck - overview _ _status strong { margin - left : 4 px ; font - size : 15 px ; }
. status - actions { display : flex ; flex - wrap : wrap ; gap : 8 px ; } . status - actions : deep ( . el - button ) { margin - left : 0 ; }
. truck - detail - page { min - width : 0 ; } . detail - heading { display : flex ; min - width : 0 ; align - items : flex - start ; gap : 16 px ; } . truck - page - toolbar { min - width : 0 ; flex : 1 ; } . status - panel { padding : 12 px 16 px ; } . form - panel { padding : 24 px ; } . history - section { padding : 20 px ; } . history - section h3 { margin : 0 0 12 px ; font - size : 15 px ; } . history - section ol { display : grid ; gap : 12 px ; margin : 0 ; padding : 0 ; list - style : none ; } . history - section li { display : grid ; grid - template - columns : 120 px minmax ( 120 px , 1 fr ) minmax ( 200 px , 2 fr ) ; gap : 12 px ; padding : 10 px 12 px ; border - radius : 6 px ; background : var ( -- el - fill - color - lighter ) ; } . history - section small { color : var ( -- cc - muted ) ; overflow - wrap : anywhere ; }
@ media ( max - width : 720 px ) { . detail - heading { flex - direction : column ; } . truck - page - toolbar { width : 100 % ; } . form - panel { padding : 16 px ; } }
. relation - snapshot { margin : 0 ; padding : 9 px 16 px ; border - top : 1 px solid var ( -- cc - border ) ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.5 ; } . relation - snapshot strong { margin - right : 4 px ; color : var ( -- cc - text ) ; }
. truck - detail - page { min - width : 0 ; } . truck - detail - page : deep ( . detail - page - layout _ _content ) { display : grid ; min - width : 0 ; gap : 16 px ; } . form - panel { padding : 16 px ; } . history - section { padding : 16 px ; } . history - section h3 { margin : 0 0 12 px ; font - size : 15 px ; } . history - section ol { display : grid ; gap : 10 px ; margin : 0 ; padding : 0 ; list - style : none ; } . history - section li { display : grid ; grid - template - columns : 120 px minmax ( 120 px , 1 fr ) minmax ( 200 px , 2 fr ) ; gap : 12 px ; padding : 9 px 12 px ; border - radius : 6 px ; background : var ( -- el - fill - color - lighter ) ; } . history - section small { color : var ( -- cc - muted ) ; overflow - wrap : anywhere ; }
@ media ( max - width : 640 px ) { . history - section { padding : 16 px ; } . history - section li { grid - template - columns : 1 fr ; gap : 4 px ; } }
< / style >