101 lines
8.0 KiB
Vue
101 lines
8.0 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminWorkbench.css'
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ArrowLeft } from '@lucide/vue'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { getUsTruckOrder } from '../api/platformOverseasBusiness'
|
||
import FleetArrangementSummary from '../components/FleetArrangementSummary.vue'
|
||
import TruckOrderActions from '../components/TruckOrderActions.vue'
|
||
import { truckStatusLabels } from '../truckWorkflow'
|
||
import type { LocalTransportDetail, LocalTransportPallet, OverseasBusinessHistory } from '../types'
|
||
|
||
const route = useRoute(), router = useRouter()
|
||
const orderNo = computed(() => typeof route.params.orderId === 'string' ? route.params.orderId : '')
|
||
const customerId = computed(() => typeof route.query.customerId === 'string' ? route.query.customerId : '')
|
||
const detail = ref<LocalTransportDetail | null>(null), loading = ref(false), saving = ref(false)
|
||
const error = ref<ApiErrorDetails | null>(null)
|
||
let requestSequence = 0
|
||
const dateTime = (value: string | null | undefined) => value ? value.replace('T', ' ').slice(0, 16) : '—'
|
||
const statusLabel = (value: string | null) => value ? truckStatusLabels[value as keyof typeof truckStatusLabels] || value : '—'
|
||
const fields = computed(() => detail.value ? [
|
||
['客户编号', detail.value.customerId], ['车辆类型', detail.value.vehicleType ? ({ PALLET_15: '15拖', PALLET_30: '30拖' })[detail.value.vehicleType] : '—'],
|
||
['起运地', detail.value.origin], ['起运地详细地址', detail.value.originAddress], ['起运地联系人', detail.value.originContactName], ['起运地联系电话', detail.value.originContactPhone],
|
||
['目的地', detail.value.destination], ['目的地详细地址', detail.value.destinationAddress], ['目的地联系人', detail.value.destinationContactName], ['目的地联系电话', detail.value.destinationContactPhone],
|
||
['取货时间', dateTime(detail.value.pickupAt)], ['送货时间', dateTime(detail.value.deliveryAt)],
|
||
['申报件数', detail.value.packageCount], ['总重量(kg)', detail.value.totalWeightKg], ['总体积(m³)', detail.value.totalVolumeCbm],
|
||
['预算总价', detail.value.budgetAmount == null ? '—' : `${detail.value.budgetAmount} ${detail.value.currency}`],
|
||
['已确认托数', detail.value.palletCount], ['累计实收托数', detail.value.receivedPalletCount], ['已分配托数', detail.value.allocatedPalletCount],
|
||
['创建时间', dateTime(detail.value.createdAt)], ['备注', detail.value.remark],
|
||
] : [])
|
||
const palletLabels: Record<string, string> = { EXPECTED: '待实收', PLANNED: '待实收', RECEIVED: '已实收', ALLOCATED: '已分配', OVERAGE_PENDING_REVIEW: '超托待审', OVERAGE_REJECTED: '超托拒收' }
|
||
const palletColumns: DataTableColumn[] = [
|
||
{ key: 'palletNo', prop: 'palletNo', label: '托盘编号', minWidth: 180 },
|
||
{ key: 'status', label: '状态', width: 130, formatter: (row: LocalTransportPallet) => palletLabels[row.status] || row.status },
|
||
{ key: 'packageCount', prop: 'packageCount', label: '实收件数', width: 120 },
|
||
{ key: 'receivedAt', label: '实收时间', minWidth: 170, formatter: (row: LocalTransportPallet) => dateTime(row.receivedAt) },
|
||
]
|
||
const operations: Record<string, string> = { CREATE: '创建', UPDATE: '编辑', REVIEW: '订单审核', RECEIVE_PALLETS: '登记实收', REVIEW_PALLET_OVERAGE: '超托审核', ALLOCATE_PALLETS: '分配托盘', PREPARE_DISPATCH: '确认待派车', DISPATCH: '确认已派车', START_TRANSIT: '确认发运', COMPLETE: '确认完成', CANCEL: '取消订单' }
|
||
const historyColumns: DataTableColumn[] = [
|
||
{ key: 'createdAt', label: '操作时间', minWidth: 170, formatter: (row: OverseasBusinessHistory) => dateTime(row.createdAt) },
|
||
{ key: 'operationType', label: '操作', minWidth: 130, formatter: (row: OverseasBusinessHistory) => operations[row.operationType || ''] || row.operationType || '—' },
|
||
{ key: 'fromStatus', label: '原状态', width: 130, formatter: (row: OverseasBusinessHistory) => statusLabel(row.fromStatus) },
|
||
{ key: 'toStatus', label: '新状态', width: 130, formatter: (row: OverseasBusinessHistory) => statusLabel(row.toStatus) },
|
||
{ key: 'operatorId', prop: 'operatorId', label: '操作人', minWidth: 140 }, { key: 'remark', prop: 'remark', label: '审核意见/操作说明', minWidth: 240 },
|
||
]
|
||
async function load() {
|
||
if (saving.value) return
|
||
const sequence = ++requestSequence
|
||
detail.value = null; error.value = null
|
||
if (!orderNo.value || !customerId.value || customerId.value === '0') {
|
||
loading.value = false; error.value = { message: '缺少有效客户或订单信息,请返回本土卡车列表重新打开' }; return
|
||
}
|
||
loading.value = true
|
||
try { const result = await getUsTruckOrder(orderNo.value, customerId.value); if (sequence === requestSequence) detail.value = result }
|
||
catch (cause) { if (sequence === requestSequence) error.value = getApiErrorDetails(cause, '本土卡车订单加载失败,请稍后重试') }
|
||
finally { if (sequence === requestSequence) loading.value = false }
|
||
}
|
||
watch([orderNo, customerId], load, { immediate: true })
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage class="admin-density admin-workbench"><DetailPageLayout aria-label="本土卡车订单详情" class="density-detail-layout workbench-panel is-flush is-page-surface">
|
||
<template #header>
|
||
<div class="density-detail-header truck-detail-header">
|
||
<div class="density-detail-heading">
|
||
<el-button link class="density-back" :disabled="saving" @click="router.push({ name: 'overseas-business-trucks' })"><ArrowLeft :size="16" />返回列表</el-button>
|
||
<h1 class="density-detail-title">本土卡车订单 {{ detail?.orderNo || orderNo }}</h1>
|
||
<StatusBadge v-if="detail" :status="statusLabel(detail.status)" />
|
||
</div>
|
||
<div v-if="detail" class="density-actions truck-detail-actions">
|
||
<el-button :disabled="saving" @click="load">刷新订单</el-button>
|
||
<TruckOrderActions v-if="!detail.fleetArrangement" :detail="detail" @updated="detail = $event" @busy="saving = $event" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<LoadErrorState v-if="error" title="本土卡车订单暂时无法加载" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||
<el-skeleton v-else-if="loading" :rows="8" animated />
|
||
<template v-else-if="detail">
|
||
<section v-if="detail.fleetArrangement" class="truck-detail-section"><FleetArrangementSummary :arrangement="detail.fleetArrangement" /></section>
|
||
<section class="truck-detail-section"><h2>订单资料</h2><el-descriptions class="density-descriptions" :column="3" border><el-descriptions-item v-for="[label, value] in fields" :key="String(label)" :label="String(label)">{{ value ?? '—' }}</el-descriptions-item></el-descriptions></section>
|
||
<section class="truck-detail-section"><h2>托盘记录</h2><div class="workbench-table"><DataTable :data="detail.pallets" :columns="palletColumns" empty-text="暂无托盘记录,独立订单首次实收后生成托盘" /></div></section>
|
||
<section class="truck-detail-section"><h2>审核与操作记录</h2><div class="workbench-table"><DataTable :data="detail.histories" :columns="historyColumns" empty-text="暂无操作记录" /></div></section>
|
||
</template>
|
||
</DetailPageLayout></AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.truck-detail-header { flex-wrap: wrap; }
|
||
.truck-detail-header .density-detail-heading { flex: 1; }
|
||
.truck-detail-header .status-badge { flex-shrink: 0; }
|
||
.truck-detail-actions { flex-wrap: wrap; }
|
||
.truck-detail-section { margin-bottom: var(--admin-space-6); }
|
||
</style>
|