优化
This commit is contained in:
@@ -17,6 +17,6 @@ describe('delivery order API', () => {
|
||||
it('loads an encoded detail in its explicit customer scope', async () => {
|
||||
vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { order: { deliveryNo: 'DEL/1' }, skus: [], fees: [], operations: [] } })
|
||||
await getDeliveryOrder('DEL/1', '1001')
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001' } })
|
||||
expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1', { params: { customerId: '1001', page: 1, size: 200 } })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function listDeliveryOrders(query: DeliveryOrderQuery) {
|
||||
|
||||
export async function getDeliveryOrder(deliveryNo: string, customerId: string) {
|
||||
const response = await overseasHttp.get<ApiEnvelope<DeliveryOrderDetail>, ApiEnvelope<DeliveryOrderDetail>>(
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId } },
|
||||
`/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}`, { params: { customerId, page: 1, size: 200 } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
@@ -10,16 +11,114 @@ import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import { getDeliveryOrder, type DeliveryOrderDetail } from '../api/deliveryOrders'
|
||||
|
||||
const route = useRoute(); const router = useRouter(); const detail = ref<DeliveryOrderDetail | null>(null); const loading = ref(false); const error = ref<ApiErrorDetails | null>(null); const activeTab = ref('order')
|
||||
const deliveryNo = computed(() => String(route.params.deliveryNo || '')); const customerId = computed(() => String(route.query.customerId || ''))
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const detail = ref<DeliveryOrderDetail | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const activeTab = ref('order')
|
||||
const deliveryNo = computed(() => String(route.params.deliveryNo || '').trim())
|
||||
const customerId = computed(() => String(route.query.customerId || '').trim())
|
||||
|
||||
const statusLabels: Record<string, string> = { WAIT_DELIVERY: '待派送', DELIVERING: '派送中', DELIVERED: '派送完成', SIGNED: '已签收', REJECTED: '已拒签', RETURN_PENDING: '退货处理中', CANCELLED: '已取消' }
|
||||
const methodLabels: Record<string, string> = { SELF_DELIVERY: '自送', EXPRESS: '快递', THIRD_PARTY: '第三方配送' }
|
||||
const feeLabels: Record<string, string> = { DELIVERY_FEE: '派送费用', LOADING_FEE: '装车费', APPOINTMENT_FEE: '买约费用' }
|
||||
const operationLabels: Record<string, string> = { CREATE_DELIVERY: '创建派送单', ASSIGN_VEHICLE: '车队分配', DISPATCH: '开始派送', SIGN: '签收' }
|
||||
const methodLabels: Record<string, string> = { FBA_PICKUP: 'FBA自提', CUSTOMER_PICKUP: '客户自提', TRUCK: '卡车派送', TRUCK_PALLET: '卡派打托', TRUCK_FLOOR: '卡派地板', EXPRESS: '快递', OUTSOURCED: '外配', DEDICATED_TRUCK: '专车直送', LTL: '零担派送', SELF_DELIVERY: '自送', THIRD_PARTY: '第三方配送' }
|
||||
const pickupLabels: Record<string, string> = { WAREHOUSE_PICKUP: '仓库提货', DOOR_PICKUP: '上门提货', SELF_PICKUP: '自提' }
|
||||
const loadingLabels: Record<string, string> = { PALLET: '打托装车', FLOOR: '地板装车', CARTON: '散箱装车' }
|
||||
const feeLabels: Record<string, string> = { DELIVERY_FEE: '派送费用', EXPRESS_FEE: '快递费用', LOADING_FEE: '装车费', APPOINTMENT_FEE: '买约费用', OTHER: '其他费用' }
|
||||
const operationLabels: Record<string, string> = { CREATE_DELIVERY: '创建派送单', ASSIGN_VEHICLE: '车队分配', DISPATCH: '开始派送', SIGN: '签收', COMPLETE_DELIVERY: '完成派送', UPDATE_DELIVERY_STATUS: '更新派送状态', CUSTOMER_VISIT: '客户回访', MARK_RETURN: '标记退货', CANCEL_OUTBOUND: '取消出库' }
|
||||
const resultLabels: Record<string, string> = { CREATED: '已创建' }
|
||||
|
||||
function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' }
|
||||
function quantity(value: number | null | undefined, unit: string) { return value === null || value === undefined ? '—' : `${value} ${unit}` }
|
||||
function customerConfirmation() { const signedAt = detail.value?.order.signedAt; return signedAt ? `已签收 · ${formatOverseasDateTime(signedAt)}` : '—' }
|
||||
function back() { void router.push({ name: 'overseas-delivery-orders' }) }
|
||||
async function load() { if (!deliveryNo.value || !customerId.value) { error.value = { message: '缺少派送单客户范围,无法安全查询详情' }; return }; loading.value = true; error.value = null; try { detail.value = await getDeliveryOrder(deliveryNo.value, customerId.value) } catch (cause) { error.value = getApiErrorDetails(cause, '派送单详情加载失败') } finally { loading.value = false } }
|
||||
load()
|
||||
async function load() {
|
||||
if (!deliveryNo.value || !customerId.value) { detail.value = null; error.value = { message: '缺少派送单客户范围,无法安全查询详情' }; return }
|
||||
loading.value = true; error.value = null
|
||||
try { detail.value = await getDeliveryOrder(deliveryNo.value, customerId.value) }
|
||||
catch (cause) { detail.value = null; error.value = getApiErrorDetails(cause, '派送单详情加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
watch([deliveryNo, customerId], () => { void load() }, { immediate: true })
|
||||
</script>
|
||||
<template><AdminPage><DetailPageLayout aria-label="派送单详情"><template #header><div class="detail-heading"><el-button @click="back"><ArrowLeft :size="16" />返回</el-button><div><h1>派送单详情 <StatusBadge v-if="detail" :status="statusLabels[detail.order.status] || detail.order.status" /></h1><p>派送单号 {{ deliveryNo }}</p></div></div></template><LoadErrorState v-if="error" title="派送单详情加载失败" :message="error.message" :loading="loading" @retry="load" /><section v-else v-loading="loading" class="surface-panel delivery-detail"><el-tabs v-model="activeTab"><el-tab-pane label="订单信息" name="order"><h3>订单信息</h3><el-descriptions v-if="detail" :column="2" border><el-descriptions-item label="派送单号">{{ show(detail.order.deliveryNo) }}</el-descriptions-item><el-descriptions-item label="状态">{{ statusLabels[detail.order.status] || detail.order.status }}</el-descriptions-item><el-descriptions-item label="仓库">{{ show(detail.order.overseasWarehouseName || detail.order.overseasWarehouseId) }}</el-descriptions-item><el-descriptions-item label="运单号">{{ show(detail.order.waybillNo) }}</el-descriptions-item><el-descriptions-item label="派送人">{{ show(detail.order.carrierName) }}</el-descriptions-item><el-descriptions-item label="派送时间">{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}</el-descriptions-item><el-descriptions-item label="提货方式">{{ show(detail.order.pickupMethod) }}</el-descriptions-item><el-descriptions-item label="装车类型">{{ show(detail.order.loadingType) }}</el-descriptions-item><el-descriptions-item label="派送方式">{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}</el-descriptions-item><el-descriptions-item label="司机/车队">{{ show(detail.order.driverName) }}</el-descriptions-item><el-descriptions-item label="车牌">{{ show(detail.order.vehiclePlate) }}</el-descriptions-item><el-descriptions-item label="总箱数">{{ detail.order.packageCount }}</el-descriptions-item><el-descriptions-item label="总重量(kg)">{{ show(detail.order.totalWeightKg) }}</el-descriptions-item><el-descriptions-item label="总体积(m³)">{{ show(detail.order.totalVolumeCbm) }}</el-descriptions-item><el-descriptions-item label="预计到达时间">{{ formatOverseasDateTime(detail.order.expectedArrivalAt) }}</el-descriptions-item><el-descriptions-item label="备注">{{ show(detail.order.remark) }}</el-descriptions-item></el-descriptions></el-tab-pane><el-tab-pane label="SKU明细" name="sku"><h3>SKU明细</h3><el-table :data="detail?.skus || []" border stripe><el-table-column prop="inboundCode" label="入库码" min-width="150" /><el-table-column prop="customerName" label="客户" min-width="160" /><el-table-column prop="destinationWarehouseCode" label="目的仓编码" min-width="130" /><el-table-column prop="receivingAddress" label="收货地址" min-width="220" /><el-table-column prop="shipmentNo" label="货件编号" min-width="150" /><el-table-column prop="fbaTrackingNo" label="FBA追踪号" min-width="150" /><el-table-column prop="skuCode" label="SKU" min-width="130" /><el-table-column prop="packageCount" label="箱数" width="90" /><el-table-column prop="weightKg" label="重量" width="100" /><el-table-column prop="volumeCbm" label="体积" width="100" /></el-table></el-tab-pane><el-tab-pane label="费用明细" name="fees"><h3>派送单费用明细</h3><el-table :data="detail?.fees || []" border stripe><el-table-column type="index" label="序号" width="80" /><el-table-column label="应收/应付" width="120"><template #default="scope">{{ scope.row.feeDirection === 'PAYABLE' ? '应付' : '应收' }}</template></el-table-column><el-table-column label="费用类型" min-width="180"><template #default="scope">{{ feeLabels[scope.row.feeType] || scope.row.feeType }}</template></el-table-column><el-table-column prop="currency" label="币种" width="100" /><el-table-column prop="amount" label="金额" width="140" /><el-table-column prop="remark" label="备注" min-width="220" /></el-table></el-tab-pane><el-tab-pane label="操作记录" name="operations"><h3>操作记录</h3><el-table :data="detail?.operations || []" border stripe><el-table-column label="操作时间" min-width="170"><template #default="scope">{{ formatOverseasDateTime(scope.row.operatedAt) }}</template></el-table-column><el-table-column label="操作节点" min-width="160"><template #default="scope">{{ operationLabels[scope.row.operationNode] || scope.row.operationNode }}</template></el-table-column><el-table-column prop="operatorName" label="操作人" min-width="140" /><el-table-column prop="result" label="处理结果" min-width="220" /></el-table></el-tab-pane></el-tabs></section></DetailPageLayout></AdminPage></template>
|
||||
<style scoped>.detail-heading{display:flex;align-items:center;gap:14px}.detail-heading h1{display:flex;align-items:center;gap:10px;margin:0}.detail-heading p{margin:4px 0 0;color:var(--cc-muted)}.delivery-detail{padding:0 16px 20px}.delivery-detail h3{margin:12px 0}</style>
|
||||
|
||||
<template>
|
||||
<AdminPage>
|
||||
<DetailPageLayout aria-label="派送单详情">
|
||||
<template #header>
|
||||
<el-breadcrumb separator=">" class="detail-breadcrumb">
|
||||
<el-breadcrumb-item>首页</el-breadcrumb-item><el-breadcrumb-item>海外派送</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="{ name: 'overseas-delivery-orders' }">派送单列表</el-breadcrumb-item><el-breadcrumb-item>详情</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
<div class="detail-heading">
|
||||
<el-button aria-label="返回派送单列表" @click="back"><ArrowLeft :size="16" />返回</el-button>
|
||||
<div class="detail-title"><div class="title-line"><h1>派送单详情</h1><StatusBadge v-if="detail" :status="statusLabels[detail.order.status] || detail.order.status" /></div><p>派送单号 {{ deliveryNo }}</p></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<LoadErrorState v-if="error" title="派送单详情加载失败" :message="error.message" :loading="loading" @retry="load">
|
||||
<template #details><ApiErrorMeta :details="error" /></template>
|
||||
</LoadErrorState>
|
||||
|
||||
<section v-else v-loading="loading" class="surface-panel delivery-detail">
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<el-tab-pane label="订单信息" name="order">
|
||||
<section class="tab-content" aria-labelledby="delivery-order-heading">
|
||||
<h2 id="delivery-order-heading">订单信息</h2>
|
||||
<dl v-if="detail" class="info-grid">
|
||||
<div class="info-item"><dt>派送单号</dt><dd>{{ show(detail.order.deliveryNo) }}</dd></div><div class="info-item"><dt>状态</dt><dd>{{ statusLabels[detail.order.status] || detail.order.status }}</dd></div>
|
||||
<div class="info-item"><dt>仓库</dt><dd>{{ show(detail.order.overseasWarehouseName || detail.order.overseasWarehouseId) }}</dd></div><div class="info-item"><dt>运单号</dt><dd>{{ show(detail.order.waybillNo) }}</dd></div>
|
||||
<div class="info-item"><dt>派送人</dt><dd>{{ show(detail.order.carrierName) }}</dd></div><div class="info-item"><dt>派送时间</dt><dd>{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}</dd></div>
|
||||
<div class="info-item"><dt>提货方式</dt><dd>{{ pickupLabels[detail.order.pickupMethod || ''] || show(detail.order.pickupMethod) }}</dd></div><div class="info-item"><dt>装车类型</dt><dd>{{ loadingLabels[detail.order.loadingType || ''] || show(detail.order.loadingType) }}</dd></div>
|
||||
<div class="info-item"><dt>派送方式</dt><dd>{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}</dd></div><div class="info-item"><dt>司机/车队</dt><dd>{{ show(detail.order.driverName) }}</dd></div>
|
||||
<div class="info-item"><dt>车牌号</dt><dd>{{ show(detail.order.vehiclePlate) }}</dd></div><div class="info-item"><dt>总箱数</dt><dd>{{ quantity(detail.order.packageCount, '箱') }}</dd></div>
|
||||
<div class="info-item"><dt>总重量</dt><dd>{{ quantity(detail.order.totalWeightKg, 'kg') }}</dd></div><div class="info-item"><dt>总体积</dt><dd>{{ quantity(detail.order.totalVolumeCbm, 'm³') }}</dd></div>
|
||||
<div class="info-item"><dt>预计到达时间</dt><dd>{{ formatOverseasDateTime(detail.order.expectedArrivalAt) }}</dd></div><div class="info-item"><dt>客户确认</dt><dd>{{ customerConfirmation() }}</dd></div>
|
||||
<div class="info-item info-item--wide"><dt>备注</dt><dd>{{ show(detail.order.remark) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`SKU明细 (${detail?.skus.length || 0})`" name="sku">
|
||||
<section class="tab-content" aria-labelledby="delivery-sku-heading"><h2 id="delivery-sku-heading">SKU明细</h2>
|
||||
<el-table :data="detail?.skus || []" border stripe empty-text="暂无SKU明细">
|
||||
<el-table-column prop="inboundCode" label="入库码" min-width="160" /><el-table-column prop="customerName" label="客户" min-width="180" />
|
||||
<el-table-column prop="destinationWarehouseCode" label="目的仓编码" min-width="140" /><el-table-column prop="receivingAddress" label="收货地址" min-width="240" />
|
||||
<el-table-column prop="shipmentNo" label="货件编号" min-width="160" /><el-table-column prop="fbaTrackingNo" label="FBA追踪号" min-width="160" />
|
||||
<el-table-column prop="skuCode" label="SKU" min-width="140" /><el-table-column prop="productName" label="品名" min-width="160" />
|
||||
<el-table-column prop="packageCount" label="箱数" width="90" align="right" /><el-table-column prop="weightKg" label="重量(kg)" width="110" align="right" /><el-table-column prop="volumeCbm" label="体积(m³)" width="110" align="right" />
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`费用明细 (${detail?.fees.length || 0})`" name="fees">
|
||||
<section class="tab-content" aria-labelledby="delivery-fee-heading"><h2 id="delivery-fee-heading">派送单费用明细</h2>
|
||||
<el-table :data="detail?.fees || []" border stripe empty-text="暂无费用明细">
|
||||
<el-table-column type="index" label="序号" width="80" /><el-table-column label="应收/应付" width="120"><template #default="scope">{{ scope.row.feeDirection === 'PAYABLE' ? '应付' : scope.row.feeDirection === 'RECEIVABLE' ? '应收' : show(scope.row.feeDirection) }}</template></el-table-column>
|
||||
<el-table-column label="费用类型" min-width="180"><template #default="scope">{{ feeLabels[scope.row.feeType] || scope.row.feeType }}</template></el-table-column><el-table-column prop="currency" label="币种" width="100" />
|
||||
<el-table-column prop="amount" label="金额" min-width="140" align="right" /><el-table-column prop="remark" label="备注" min-width="240" />
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`操作记录 (${detail?.operations.length || 0})`" name="operations">
|
||||
<section class="tab-content" aria-labelledby="delivery-operation-heading"><h2 id="delivery-operation-heading">操作记录</h2>
|
||||
<el-table :data="detail?.operations || []" border stripe empty-text="暂无操作记录">
|
||||
<el-table-column label="操作时间" min-width="180"><template #default="scope">{{ formatOverseasDateTime(scope.row.operatedAt) }}</template></el-table-column>
|
||||
<el-table-column label="操作节点" min-width="180"><template #default="scope">{{ operationLabels[scope.row.operationNode] || scope.row.operationNode }}</template></el-table-column>
|
||||
<el-table-column prop="operatorName" label="操作人" min-width="160" /><el-table-column label="处理结果" min-width="280"><template #default="scope">{{ resultLabels[scope.row.result] || scope.row.result }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</DetailPageLayout>
|
||||
</AdminPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.detail-breadcrumb { margin-bottom: 12px; }.detail-heading { display: flex; align-items: center; gap: 14px; }.detail-title { min-width: 0; }.title-line { display: flex; align-items: center; gap: 10px; }.title-line h1 { margin: 0; font-size: 20px; line-height: 28px; }.detail-title p { margin: 2px 0 0; color: var(--cc-muted); font-size: 13px; }
|
||||
.delivery-detail { min-height: 360px; overflow: hidden; }.detail-tabs :deep(.el-tabs__header) { margin: 0; padding: 0 18px; }.detail-tabs :deep(.el-tabs__nav-wrap::after) { height: 1px; }.detail-tabs :deep(.el-tabs__item) { height: 48px; padding: 0 22px; }.tab-content { width: 100%; padding: 18px 20px 24px; box-sizing: border-box; }.tab-content h2 { margin: 0 0 14px; font-size: 15px; line-height: 22px; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--el-border-color-lighter); }.info-item { display: grid; grid-template-columns: 116px minmax(0, 1fr); min-height: 44px; align-items: center; border-bottom: 1px solid var(--el-border-color-lighter); }.info-item:nth-child(odd) { padding-right: 24px; }.info-item:nth-child(even) { padding-left: 24px; }.info-item--wide { grid-column: 1 / -1; padding-left: 0 !important; }.info-item dt { color: var(--cc-muted); font-size: 13px; }.info-item dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-weight: 500; }
|
||||
@media (max-width: 980px) { .info-grid { grid-template-columns: 1fr; }.info-item, .info-item:nth-child(odd), .info-item:nth-child(even) { padding: 0; }.info-item--wide { grid-column: auto; } }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user