Files
client-web/src/modules/order-management/views/DeliveryOrderDetailView.vue
T

213 lines
12 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 { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ArrowLeft } from '@lucide/vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { getDeliveryOrder, getDeliveryReceipts, downloadDeliveryReceipt, type DeliveryReceiptFile, type DeliveryOrderDetail, type DeliveryOrderItem } from '../api/deliveryOrders'
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
const props = defineProps<{ deliveryNo: string }>()
const router = useRouter()
const loading = ref(false)
const errorMessage = ref('')
const detail = ref<DeliveryOrderDetail | null>(null)
const receiptFiles = ref<DeliveryReceiptFile[]>([])
const receiptsLoading = ref(false)
const receiptsError = ref('')
const downloadingFileId = ref('')
const statusLabels: Record<string, string> = { WAIT_DELIVERY: '待派送', DELIVERING: '派送中', DELIVERED: '已签收', REJECTED: '已拒收', RETURN_PENDING: '待退回', RETURNED: '已退回', CANCELLED: '已取消' }
const receiptColumns: DataTableColumn[] = [
{ label: '操作', slotName: 'receiptActions', width: 100 },
{ prop: 'fileName', label: '文件名称', minWidth: 260 },
{ prop: 'uploadedAt', label: '上传时间', width: 180, formatter: (_row: object, _column: unknown, value: string) => formatDateTime(value) },
]
const deliveryMethodLabels: Record<string, string> = { FBA_PICKUP: 'FBA 自提', CUSTOMER_PICKUP: '客户自提', TRUCK_PALLET: '卡派打托', TRUCK_FLOOR: '卡派地板', EXPRESS: '快递', OUTSOURCED: '外配', DEDICATED_TRUCK: '专车直送', LTL: '零担派送' }
const pickupMethodLabels: Record<string, string> = { ONE_PICKUP_ONE_DROP: '一提一卸', ONE_PICKUP_MULTI_DROP: '一提多卸', MULTI_PICKUP_ONE_DROP: '多提一卸', MULTI_PICKUP_MULTI_DROP: '多提多卸', WAREHOUSE_PICKUP: '仓库自提' }
const loadingTypeLabels: Record<string, string> = { PALLET: '托盘', FLOOR: '地板', MIXED: '混装' }
const vehicleTypeLabels: Record<string, string> = { TRUCK: '卡车', VAN: '厢式货车', BOX_TRUCK: '箱式货车', TRAILER: '拖挂车' }
const appointmentAccountLabels: Record<string, string> = { SELF: '自有账号', PLATFORM: '平台账号', CARRIER: '承运商账号' }
const tableValueFormatter = (_row: object, _column: unknown, value: string | number | null | undefined) => display(value)
const itemColumns: DataTableColumn[] = [
{ prop: 'palletNo', label: '托盘码', minWidth: 170, formatter: tableValueFormatter }, { prop: 'inboundCode', label: '入仓码', minWidth: 190, formatter: tableValueFormatter },
{ prop: 'skuCode', label: 'SKU', minWidth: 160, formatter: tableValueFormatter }, { prop: 'productName', label: '品名', minWidth: 180, formatter: tableValueFormatter },
{ prop: 'packageCount', label: '箱数', width: 90, align: 'right', headerAlign: 'right', formatter: tableValueFormatter },
{ prop: 'weightKg', label: '重量(kg)', width: 120, align: 'right', headerAlign: 'right', formatter: tableValueFormatter },
{ prop: 'volumeCbm', label: '体积(m³)', width: 120, align: 'right', headerAlign: 'right', formatter: tableValueFormatter },
]
const feeColumns: DataTableColumn[] = [
{ prop: 'feeType', label: '费用类型', minWidth: 180, formatter: (_row: object, _column: unknown, value: string | null | undefined) => formatDeliveryFeeType(value) }, { prop: 'currency', label: '币种', width: 100, formatter: tableValueFormatter },
{ prop: 'amount', label: '金额', width: 140, align: 'right', headerAlign: 'right', formatter: tableValueFormatter }, { prop: 'remark', label: '备注', minWidth: 240, formatter: tableValueFormatter },
]
const items = computed<DeliveryOrderItem[]>(() => {
const value = detail.value?.items
if (!value) return []
return Array.isArray(value) ? value : value.records
})
function display(value: string | number | null | undefined) {
return value === null || value === undefined || value === '' ? '—' : String(value)
}
function formatDeliveryFeeType(value: string | null | undefined) {
const labels: Record<string, string> = { DELIVERY_FEE: '派送费', LOADING_FEE: '装车费', APPOINTMENT_FEE: '预约费' }
return value ? labels[value] ?? value : '—'
}
function formatDateTime(value: string | null | undefined) {
return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '—'
}
function formatQuantity(value: number | null | undefined, unit: string) {
return value === null || value === undefined ? '—' : `${value} ${unit}`
}
function statusLabel(value: string) { return statusLabels[value] ?? display(value) }
function statusTone(value: string) {
if (value === 'DELIVERED') return 'success'
if (value === 'DELIVERING') return 'primary'
if (value === 'CANCELLED' || value === 'REJECTED') return 'danger'
return 'warning'
}
function mappedLabel(value: string | null | undefined, labels: Record<string, string>) {
return value ? labels[value] ?? value : '—'
}
async function loadDetail() {
loading.value = true
errorMessage.value = ''
try {
detail.value = await getDeliveryOrder(props.deliveryNo)
void loadReceipts()
} catch (error) {
detail.value = null
errorMessage.value = error instanceof Error ? error.message : '派送单详情加载失败,请重试'
} finally {
loading.value = false
}
}
async function loadReceipts() {
receiptsLoading.value = true
receiptsError.value = ''
receiptFiles.value = []
try {
receiptFiles.value = (await getDeliveryReceipts(props.deliveryNo)).files
} catch (error) {
receiptsError.value = error instanceof Error ? error.message : '签收资料加载失败,请重试'
} finally {
receiptsLoading.value = false
}
}
async function downloadReceipt(fileId: unknown) {
const file = receiptFiles.value.find(item => item.fileId === fileId)
if (!file) return
if (downloadingFileId.value) return
downloadingFileId.value = file.fileId
try {
const blob = await downloadDeliveryReceipt(props.deliveryNo, file.fileId)
const url = URL.createObjectURL(blob)
try {
const anchor = document.createElement('a')
anchor.href = url
anchor.download = file.fileName
anchor.click()
} finally {
URL.revokeObjectURL(url)
}
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '签收文件下载失败,请重试')
} finally {
downloadingFileId.value = ''
}
}
function backToList() {
void router.push({ name: 'orders-delivery' })
}
onMounted(loadDetail)
</script>
<template>
<section class="delivery-page" aria-label="派送单详情">
<div v-if="errorMessage" class="delivery-card delivery-state delivery-state--error" role="alert"><p>{{ errorMessage }}</p><el-button @click="loadDetail">重试</el-button></div>
<div v-else v-loading="loading" class="delivery-detail-content">
<template v-if="detail">
<article class="delivery-detail-surface">
<header class="delivery-detail-header">
<div class="delivery-detail-title">
<el-button link type="primary" @click="backToList"><ArrowLeft :size="16" />返回列表</el-button>
<div><h2>{{ display(detail.deliveryNo) }}</h2><p>{{ display(detail.overseasWarehouseName) }} · {{ mappedLabel(detail.deliveryMethod, deliveryMethodLabels) }}</p></div>
</div>
<span class="client-status" :data-tone="statusTone(detail.status)">{{ statusLabel(detail.status) }}</span>
</header>
<section class="delivery-detail-section">
<h3>订单概览</h3>
<div class="delivery-detail-grid">
<div><span>排车单号</span><strong>{{ display(detail.dispatchNo) }}</strong></div>
<div><span>出库单号</span><strong>{{ display(detail.outboundNo) }}</strong></div>
<div><span>海外仓</span><strong>{{ display(detail.overseasWarehouseName) }}</strong></div>
<div><span>派送方式</span><strong>{{ mappedLabel(detail.deliveryMethod, deliveryMethodLabels) }}</strong></div>
<div><span>提货方式</span><strong>{{ mappedLabel(detail.pickupMethod, pickupMethodLabels) }}</strong></div>
<div><span>装载类型</span><strong>{{ mappedLabel(detail.loadingType, loadingTypeLabels) }}</strong></div>
<div><span>托盘数</span><strong>{{ formatQuantity(detail.palletCount, '托') }}</strong></div>
<div><span>箱数</span><strong>{{ formatQuantity(detail.packageCount, '箱') }}</strong></div>
<div><span>总重量</span><strong>{{ formatQuantity(detail.totalWeightKg, 'kg') }}</strong></div>
<div><span>总体积</span><strong>{{ formatQuantity(detail.totalVolumeCbm, 'm³') }}</strong></div>
</div>
</section>
<section class="delivery-detail-section">
<h3>运输与预约</h3>
<div class="delivery-detail-grid">
<div><span>运单号</span><strong>{{ display(detail.waybillNo) }}</strong></div>
<div><span>承运商</span><strong>{{ display(detail.carrierName) }}</strong></div>
<div><span>车型</span><strong>{{ mappedLabel(detail.vehicleType, vehicleTypeLabels) }}</strong></div>
<div><span>车辆尺寸</span><strong>{{ display(detail.vehicleSize) }}</strong></div>
<div><span>车牌号</span><strong>{{ display(detail.vehiclePlate) }}</strong></div>
<div><span>司机姓名</span><strong>{{ display(detail.driverName) }}</strong></div>
<div><span>司机电话</span><strong>{{ display(detail.driverPhone) }}</strong></div>
<div><span>预约账号</span><strong>{{ mappedLabel(detail.appointmentAccount, appointmentAccountLabels) }}</strong></div>
<div><span>卖家名称</span><strong>{{ display(detail.sellerName) }}</strong></div>
<div><span>计划派送</span><strong>{{ formatDateTime(detail.plannedDeliveryAt) }}</strong></div>
<div><span>预计到达</span><strong>{{ formatDateTime(detail.expectedArrivalAt) }}</strong></div>
<div><span>签收时间</span><strong>{{ formatDateTime(detail.signedAt) }}</strong></div>
<div><span>创建时间</span><strong>{{ formatDateTime(detail.createdAt) }}</strong></div>
<div><span>更新时间</span><strong>{{ formatDateTime(detail.updatedAt) }}</strong></div>
<div class="delivery-span-all"><span>备注</span><strong>{{ display(detail.remark) }}</strong></div>
</div>
</section>
<section class="delivery-detail-section">
<div class="delivery-detail-section-heading"><h3>货物明细</h3><span>共 {{ items.length }} 条</span></div>
<DataTable :data="items" :columns="itemColumns" empty-text="暂无货物明细" />
</section>
<section class="delivery-detail-section" aria-label="签收资料">
<div class="delivery-detail-section-heading"><h3>签收资料</h3><span>共 {{ receiptFiles.length }} 个文件</span></div>
<div v-if="receiptsError" class="delivery-state delivery-state--error" role="alert">
<p>{{ receiptsError }}</p><el-button @click="loadReceipts">重试</el-button>
</div>
<DataTable v-else :data="receiptFiles" :columns="receiptColumns" :loading="receiptsLoading" empty-text="暂无签收资料">
<template #receiptActions="{ row }">
<el-button link type="primary" :loading="downloadingFileId === row.fileId" :disabled="!!downloadingFileId && downloadingFileId !== row.fileId" @click="downloadReceipt(row.fileId)">下载</el-button>
</template>
</DataTable>
</section>
<section class="delivery-detail-section">
<div class="delivery-detail-section-heading"><h3>费用明细</h3><span>共 {{ detail.fees.length }} 条</span></div>
<DataTable :data="detail.fees" :columns="feeColumns" empty-text="暂无费用明细" />
</section>
</article>
</template>
</div>
</section>
</template>
<style scoped src="./deliveryOrders.css"></style>