144 lines
11 KiB
Vue
144 lines
11 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, Check, MapPin } 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 BusinessDocumentFees from '../components/BusinessDocumentFees.vue'
|
||
import { truckStatusLabels } from '../truckWorkflow'
|
||
import type { LocalTransportDetail, 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 flowSteps = ['待审核', '排车审核', '取货', '派送中', '送达']
|
||
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 flowIndex = computed(() => {
|
||
const status = detail.value?.status
|
||
if (status === 'PENDING_REVIEW') return 0
|
||
if (status === 'IN_TRANSIT') return 3
|
||
if (status === 'COMPLETED') return 4
|
||
if (status === 'CANCELLED' || status === 'REJECTED') return 0
|
||
return 1
|
||
})
|
||
const fields = computed(() => detail.value ? [
|
||
['车辆类型', 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],
|
||
['托数', detail.value.palletCount], ['重量(kg)', detail.value.totalWeightKg], ['体积(m³)', detail.value.totalVolumeCbm],
|
||
['预算总价', detail.value.budgetAmount == null ? '—' : `${detail.value.budgetAmount} ${detail.value.currency}`],
|
||
['取货时间', dateTime(detail.value.pickupAt)], ['送货时间', dateTime(detail.value.deliveryAt)],
|
||
['创建时间', dateTime(detail.value.createdAt)], ['备注', detail.value.remark],
|
||
] : [])
|
||
const operations: Record<string, string> = {
|
||
CREATE: '创建', UPDATE: '编辑', REVIEW: '审核', TRANSPORT_ASSIGN: '接入排车',
|
||
RECEIVE_PALLETS: '取货', START_TRANSIT: '派送中', COMPLETE: '送达', LOCAL_TRUCK_DEPART: '取货完成', LOCAL_TRUCK_COMPLETE: '送达', CANCEL: '取消',
|
||
DISPATCH: '接入排车', PREPARE_DISPATCH: '接入排车',
|
||
}
|
||
const historyColumns: DataTableColumn[] = [
|
||
{ key: 'createdAt', label: '时间', minWidth: 170, formatter: (row: OverseasBusinessHistory) => dateTime(row.createdAt) },
|
||
{ key: 'operationType', label: '操作', minWidth: 120, formatter: (row: OverseasBusinessHistory) => operations[row.operationType || ''] || row.operationType || '—' },
|
||
{ key: 'fromStatus', label: '原状态', width: 120, formatter: (row: OverseasBusinessHistory) => statusLabel(row.fromStatus) },
|
||
{ key: 'toStatus', label: '新状态', width: 120, formatter: (row: OverseasBusinessHistory) => statusLabel(row.toStatus) },
|
||
{ key: 'operatorId', prop: 'operatorId', label: '操作人', minWidth: 120 },
|
||
{ key: 'remark', prop: 'remark', label: '说明', minWidth: 220 },
|
||
]
|
||
|
||
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" /><p class="truck-detail-note">取货和送达照片请在排车任务中上传。</p></section>
|
||
|
||
<section class="truck-detail-section truck-flow-section" aria-label="运输流程">
|
||
<div class="truck-section-heading"><h2>运输流程</h2></div>
|
||
<ol class="truck-flow">
|
||
<li v-for="(step, index) in flowSteps" :key="step" :class="{ active: index === flowIndex, done: index < flowIndex || detail.status === 'COMPLETED' }">
|
||
<span class="truck-flow-marker"><Check v-if="index < flowIndex || detail.status === 'COMPLETED'" :size="14" /><MapPin v-else-if="index === 2 || index === 4" :size="14" /><span v-else>{{ index + 1 }}</span></span>
|
||
<span class="truck-flow-label">{{ step }}</span>
|
||
</li>
|
||
</ol>
|
||
</section>
|
||
|
||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>订单资料</h2></div><dl class="truck-detail-grid"><div v-for="[label, value] in fields" :key="String(label)" class="truck-detail-fact"><dt>{{ label }}</dt><dd>{{ value ?? '—' }}</dd></div></dl></section>
|
||
<section class="truck-detail-section"><BusinessDocumentFees business-type="US_TRUCK" :business-no="detail.orderNo" :customer-id="detail.customerId" :business-id="detail.id" /></section>
|
||
<section class="truck-detail-section"><div class="truck-section-heading"><h2>审核与操作记录</h2></div><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); }
|
||
.truck-section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||
.truck-section-heading h2 { margin: 0; font-size: var(--admin-font-section); color: var(--cc-text); }
|
||
.truck-detail-note { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||
.truck-detail-note { margin: 10px 0 0; }
|
||
.truck-flow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0; margin: 0; padding: 0; list-style: none; }
|
||
.truck-flow li { position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; color: var(--cc-muted); text-align: center; }
|
||
.truck-flow li::before { content: ''; position: absolute; top: 15px; left: -50%; width: 100%; height: 1px; background: var(--cc-border); }
|
||
.truck-flow li:first-child::before { display: none; }
|
||
.truck-flow li.done::before, .truck-flow li.active::before { background: var(--el-color-primary); }
|
||
.truck-flow-marker { position: relative; z-index: 1; display: grid; place-items: center; width: 30px; height: 30px; border: 1px solid var(--cc-border); border-radius: 50%; background: var(--cc-surface); color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||
.truck-flow li.done .truck-flow-marker, .truck-flow li.active .truck-flow-marker { border-color: var(--el-color-primary); color: var(--el-color-primary); }
|
||
.truck-flow li.active .truck-flow-marker { background: var(--el-color-primary); color: #fff; box-shadow: 0 0 0 4px color-mix(in srgb, var(--el-color-primary) 14%, transparent); }
|
||
.truck-flow-label { font-size: var(--admin-font-caption); white-space: nowrap; }
|
||
.truck-flow li.active .truck-flow-label { color: var(--el-color-primary); font-weight: 600; }
|
||
.truck-detail-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 0; border: 1px solid var(--cc-border); border-radius: var(--cc-radius-lg); overflow: hidden; background: var(--cc-border); }
|
||
.truck-detail-fact { min-width: 0; display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 10px; padding: 11px 14px; background: var(--cc-surface); }
|
||
.truck-detail-fact dt { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||
.truck-detail-fact dd { min-width: 0; margin: 0; color: var(--cc-text); overflow-wrap: anywhere; font-size: var(--admin-font-body); }
|
||
.truck-detail-fact:last-child { grid-column: span 2; }
|
||
@media (max-width: 1100px) { .truck-detail-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||
@media (max-width: 760px) { .truck-flow { overflow-x: auto; grid-template-columns: repeat(5, 100px); padding-bottom: 4px; } .truck-detail-grid { grid-template-columns: 1fr; } .truck-detail-fact:last-child { grid-column: auto; } }
|
||
</style>
|