Files
admin-web/src/modules/overseas-business/pages/TruckOrderDetailView.vue
T

144 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>