feat(delivery): let customers read and download signed receipts

This commit is contained in:
2026-09-30 12:09:22 +08:00
parent fa9bfd5f97
commit 6abb8b7388
4 changed files with 127 additions and 12 deletions
@@ -2,8 +2,9 @@
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, type DeliveryOrderDetail, type DeliveryOrderItem } from '../api/deliveryOrders'
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 }>()
@@ -11,7 +12,16 @@ const router = useRouter()
const loading = ref(false)
const errorMessage = ref('')
const detail = ref<DeliveryOrderDetail | null>(null)
const statusLabels: Record<string, string> = { PENDING: '待派送', IN_TRANSIT: '派送中', DELIVERED: '已送达', SIGNED: '已签收', CANCELLED: '已取消' }
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: '混装' }
@@ -50,9 +60,9 @@ function formatQuantity(value: number | null | undefined, unit: string) {
function statusLabel(value: string) { return statusLabels[value] ?? display(value) }
function statusTone(value: string) {
if (value === 'DELIVERED' || value === 'SIGNED') return 'success'
if (value === 'IN_TRANSIT') return 'primary'
if (value === 'CANCELLED') return 'danger'
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>) {
@@ -64,6 +74,7 @@ async function loadDetail() {
errorMessage.value = ''
try {
detail.value = await getDeliveryOrder(props.deliveryNo)
void loadReceipts()
} catch (error) {
detail.value = null
errorMessage.value = error instanceof Error ? error.message : '派送单详情加载失败,请重试'
@@ -72,6 +83,42 @@ async function loadDetail() {
}
}
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' })
}
@@ -135,6 +182,18 @@ onMounted(loadDetail)
<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="暂无费用明细" />
@@ -21,10 +21,12 @@ const filters = reactive({ documentNo: '', dispatchNo: '', waybillNo: '', status
const canCreate = computed(() => auth.hasPermission('merchant:warehouse:delivery:create'))
const statusOptions = [
{ value: 'PENDING', label: '待派送' },
{ value: 'IN_TRANSIT', label: '派送中' },
{ value: 'DELIVERED', label: '已送达' },
{ value: 'SIGNED', label: '已签收' },
{ value: 'WAIT_DELIVERY', label: '待派送' },
{ value: 'DELIVERING', label: '派送中' },
{ value: 'DELIVERED', label: '已签收' },
{ value: 'REJECTED', label: '已拒收' },
{ value: 'RETURN_PENDING', label: '待退回' },
{ value: 'RETURNED', label: '已退回' },
{ value: 'CANCELLED', label: '已取消' },
]
@@ -60,9 +62,10 @@ function statusLabel(status: string) {
}
function statusType(status: string) {
if (status === 'DELIVERED' || status === 'SIGNED') return 'success'
if (status === 'DELIVERED') return 'success'
if (status === 'CANCELLED') return 'info'
if (status === 'IN_TRANSIT') return 'warning'
if (status === 'DELIVERING' || status === 'RETURN_PENDING') return 'warning'
if (status === 'REJECTED') return 'danger'
return 'primary'
}