158 lines
11 KiB
Vue
158 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
|
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
|
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
|
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
|
import { getLastMileOrder, type LastMileOrderDetailDto } from '../api/lastMileOrders'
|
|
import { formatOverseasDateTime, lastMileOrderTypeLabel, lastMileStatusLabel } from '../lastMilePresentation'
|
|
import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
|
|
|
interface DetailField { label: string; value: string | number }
|
|
interface DocumentRow { type: string; number: string; createdAt: string; quote: string }
|
|
interface CargoRow { id: string; sku: string; productName: string; pallets: number; cartons: number; quantity: number; weight: number; volume: number }
|
|
interface LogRow { id: string; occurredAt: string; node: string; operator: string; result: string }
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const orderId = computed(() => String(route.params.orderId || ''))
|
|
const detail = ref<LastMileOrderDetailDto | null>(null)
|
|
const loading = ref(false)
|
|
const loadError = ref<ApiErrorDetails | null>(null)
|
|
|
|
function text(value: unknown) { return String(value ?? '').trim() || '—' }
|
|
function yesNo(value: boolean | null | undefined) { return value == null ? '—' : value ? '是' : '否' }
|
|
|
|
const referenceValue = computed(() => text(detail.value?.order.orderNo || orderId.value))
|
|
const orderFields = computed<DetailField[]>(() => {
|
|
const order = detail.value?.order
|
|
return [
|
|
{ label: '订单类型', value: lastMileOrderTypeLabel(order?.orderType) },
|
|
{ label: '拒收时间', value: '—' },
|
|
{ label: '实际入库时间', value: '—' },
|
|
{ label: '头程单号', value: text(order?.sourceOrderNo) },
|
|
{ label: '货主', value: text(order?.customerName) },
|
|
{ label: '海外仓', value: text(order?.overseasWarehouseName) },
|
|
{ label: '转仓目的仓', value: text(order?.transferWarehouseName) },
|
|
{ label: 'FBA仓库', value: text(order?.fbaWarehouseCode) },
|
|
{ label: '集装箱号', value: text(order?.containerNo) },
|
|
{ label: '货柜异常', value: yesNo(order?.abnormal) },
|
|
{ label: '异常原因', value: order?.abnormal ? text(order.subOrderStatus) : '—' },
|
|
{ label: '运输方式', value: '—' },
|
|
{ label: '预计到仓', value: formatOverseasDateTime(order?.estimatedWarehouseAt) },
|
|
{ label: '清关状态', value: text(order?.containerStatus) },
|
|
]
|
|
})
|
|
const detailFields = computed<DetailField[]>(() => {
|
|
const order = detail.value?.order
|
|
return [
|
|
{ label: '订单类型', value: lastMileOrderTypeLabel(order?.orderType) },
|
|
{ label: '入仓码', value: text(order?.inboundCode) },
|
|
{ label: '目的仓', value: text(order?.transferWarehouseName) },
|
|
{ label: '托数', value: Number(order?.palletCount || 0) },
|
|
{ label: '箱数', value: Number(order?.packageCount || 0) },
|
|
{ label: '数量', value: Number(order?.productQuantity || 0) },
|
|
{ label: '体积重量', value: Number(order?.volumeWeightKg || 0) },
|
|
{ label: '预计到港时间', value: formatOverseasDateTime(order?.estimatedArrivalAt) },
|
|
{ label: '实际到港时间', value: formatOverseasDateTime(order?.actualArrivalAt) },
|
|
{ label: '预计到仓', value: formatOverseasDateTime(order?.estimatedWarehouseAt) },
|
|
{ label: '是否暂扣', value: yesNo(order?.detained) },
|
|
{ label: '备注(仅自己)', value: text(order?.privateRemark) },
|
|
{ label: '备注(客户)', value: text(order?.customerRemark) },
|
|
{ label: '备注(仓库)', value: text(order?.warehouseRemark) },
|
|
]
|
|
})
|
|
const planFields = computed<DetailField[]>(() => {
|
|
const order = detail.value?.order
|
|
return [
|
|
{ label: '入仓码', value: text(order?.inboundCode) },
|
|
{ label: '计划入库仓', value: text(order?.transferWarehouseName || order?.overseasWarehouseName) },
|
|
{ label: '合托批次', value: text(order?.palletMergeFlag) },
|
|
{ label: '合托后托数', value: Number(order?.palletCount || 0) },
|
|
{ label: 'FBA预约号', value: '—' },
|
|
{ label: '预约状态', value: '—' },
|
|
{ label: '换标状态', value: lastMileStatusLabel(order?.relabelStatus) },
|
|
]
|
|
})
|
|
const documents = computed<DocumentRow[]>(() => {
|
|
const order = detail.value?.order
|
|
if (!order) return []
|
|
return [
|
|
['入库单', order.inboundOrderNo],
|
|
['出库单(只读关联)', order.outboundOrderNo],
|
|
['派送单(只读关联)', order.deliveryOrderNo],
|
|
['换标单', order.relabelOrderNo],
|
|
].filter((item) => item[1]).map(([type, number]) => ({ type: String(type), number: String(number), createdAt: '—', quote: '—' }))
|
|
})
|
|
const cargo = computed<CargoRow[]>(() => (detail.value?.cargo || []).map((item) => ({
|
|
id: String(item.id), sku: text(item.skuCode), productName: text(item.productNameZh), pallets: Number(item.palletCount || 0),
|
|
cartons: Number(item.packageCount || 0), quantity: Number(item.productQuantity || 0), weight: Number(item.grossWeightKg || 0), volume: Number(item.volumeCbm || 0),
|
|
})))
|
|
const logs = computed<LogRow[]>(() => (detail.value?.statusHistory || []).map((item) => ({
|
|
id: String(item.id), occurredAt: formatOverseasDateTime(item.createdAt), node: lastMileStatusLabel(item.toStatus), operator: '—',
|
|
result: text(item.remark || `${lastMileStatusLabel(item.fromStatus)} → ${lastMileStatusLabel(item.toStatus)}`),
|
|
})))
|
|
|
|
const documentColumns: DataTableColumn[] = [
|
|
{ key: 'type', prop: 'type', label: '单据类型', minWidth: 120 }, { key: 'number', prop: 'number', label: '单据编号', minWidth: 180 },
|
|
{ key: 'createdAt', prop: 'createdAt', label: '生成时间', minWidth: 160 }, { key: 'quote', prop: 'quote', label: '报价', minWidth: 140 },
|
|
]
|
|
const cargoColumns: DataTableColumn[] = [
|
|
{ key: 'sku', prop: 'sku', label: 'SKU', minWidth: 150 }, { key: 'productName', prop: 'productName', label: '中文品名', minWidth: 180 },
|
|
{ key: 'pallets', prop: 'pallets', label: '托数', width: 90, align: 'right' }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 90, align: 'right' },
|
|
{ key: 'quantity', prop: 'quantity', label: '件数', width: 90, align: 'right' }, { key: 'weight', prop: 'weight', label: '重量(kg)', width: 110, align: 'right' },
|
|
{ key: 'volume', prop: 'volume', label: '体积(m³)', width: 110, align: 'right' },
|
|
]
|
|
const logColumns: DataTableColumn[] = [
|
|
{ key: 'occurredAt', prop: 'occurredAt', label: '时间', minWidth: 160 }, { key: 'node', prop: 'node', label: '操作节点', minWidth: 140 },
|
|
{ key: 'operator', prop: 'operator', label: '操作人', minWidth: 130 }, { key: 'result', prop: 'result', label: '处理结果', minWidth: 220 },
|
|
]
|
|
|
|
async function load() {
|
|
if (!orderId.value) {
|
|
loadError.value = { message: '详情缺少尾程订单号' }
|
|
return
|
|
}
|
|
loading.value = true
|
|
loadError.value = null
|
|
try { detail.value = await getLastMileOrder(orderId.value) }
|
|
catch (error) {
|
|
loadError.value = error instanceof Error && error.message === '当前环境未签发海外访问令牌'
|
|
? { message: error.message }
|
|
: getApiErrorDetails(error, '尾程订单详情加载失败,请重试')
|
|
}
|
|
finally { loading.value = false }
|
|
}
|
|
function back() { void router.push({ name: 'overseas-last-mile-orders' }) }
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<template>
|
|
<OverseasDetailShell title="尾程订单详情" reference-label="尾程单号" :reference-value="referenceValue" back-label="返回尾程订单" domain="尾程订单详情" :pending="false" :status-text="detail ? lastMileStatusLabel(detail.order.currentStatus) : '加载中'" @back="back">
|
|
<LoadErrorState v-if="loadError" title="尾程订单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
|
<div v-else v-loading="loading" class="detail-grid" :aria-busy="loading">
|
|
<section class="detail-section" aria-labelledby="last-mile-order-heading"><h2 id="last-mile-order-heading">订单信息</h2><dl class="detail-fields"><div v-for="field in orderFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
|
|
<section class="detail-section" aria-labelledby="last-mile-lines-heading"><h2 id="last-mile-lines-heading">订单明细</h2><dl class="detail-fields"><div v-for="field in detailFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
|
|
<section class="detail-section" aria-labelledby="last-mile-plan-heading"><h2 id="last-mile-plan-heading">计划信息</h2><dl class="detail-fields"><div v-for="field in planFields" :key="field.label"><dt>{{ field.label }}</dt><dd>{{ field.value }}</dd></div></dl></section>
|
|
<section class="detail-section table-section" aria-labelledby="last-mile-documents-heading"><h2 id="last-mile-documents-heading">关联单据</h2><DataTable :data="documents" :columns="documentColumns" column-settings-mode="hide" empty-text="暂无关联单据" /></section>
|
|
<section class="detail-section table-section is-wide" aria-labelledby="last-mile-cargo-heading"><h2 id="last-mile-cargo-heading">货物信息</h2><DataTable :data="cargo" :columns="cargoColumns" column-settings-mode="hide" row-key="id" empty-text="暂无货物明细" /></section>
|
|
<section class="detail-section table-section" aria-labelledby="last-mile-logs-heading"><h2 id="last-mile-logs-heading">操作记录</h2><DataTable :data="logs" :columns="logColumns" column-settings-mode="hide" row-key="id" empty-text="暂无操作记录" /></section>
|
|
</div>
|
|
</OverseasDetailShell>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.detail-grid { display: grid; min-width: 0; min-height: 240px; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid var(--cc-border); background: #fff; }
|
|
.detail-section { min-width: 0; padding: 16px 18px 18px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
|
.detail-section:nth-child(even) { border-right: 0; }
|
|
.detail-section.is-wide { grid-column: 1 / -1; border-right: 0; }
|
|
.detail-section h2 { margin: 0 0 12px; color: var(--cc-text); font-size: 15px; }
|
|
.detail-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
|
.detail-fields > div { min-width: 0; min-height: 58px; padding: 9px 11px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
|
.detail-fields dt { color: var(--cc-muted); font-size: 12px; }
|
|
.detail-fields dd { margin: 4px 0 0; color: var(--cc-text); overflow-wrap: anywhere; }
|
|
.table-section { padding-right: 0; padding-bottom: 0; padding-left: 0; overflow: hidden; }
|
|
.table-section h2 { margin-right: 18px; margin-left: 18px; }
|
|
</style>
|