This commit is contained in:
2026-10-19 10:23:38 +08:00
parent 4bb21641d9
commit 8ae1e5d0d4
18 changed files with 575 additions and 28 deletions
@@ -0,0 +1,15 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export interface PickupProgress {
status: string; tripNo: string | null; vehiclePlate: string | null; driverName: string | null; driverPhone: string | null
plannedDepartureAt: string | null; actualPackages: number | null; pickedAt: string | null; arrivedAt: string | null; remark: string | null
evidence: { fileId: string; fileName: string }[]
}
/** 原单详情只查询本票取货进度。 */
export async function getTransferPickupProgress(transferNo: string, customerId: string): Promise<PickupProgress | null> {
return unwrapApiResponse(await http.get<ApiEnvelope<PickupProgress | null>, ApiEnvelope<PickupProgress | null>>(`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/pickup-progress`, { params: { customerId } }))
}
/** 下载当前票确认的取货凭证。 */
export async function downloadTransferPickupEvidence(transferNo: string, customerId: string, fileId: string): Promise<Blob> {
return http.get<Blob, Blob>(`/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferNo)}/pickup-evidence/${encodeURIComponent(fileId)}`, { params: { customerId }, responseType: 'blob' })
}
@@ -0,0 +1,15 @@
import { expect, it, vi } from 'vitest'
import { shallowMount, flushPromises } from '@vue/test-utils'
import TransferPickupProgressPanel from './TransferPickupProgressPanel.vue'
import { getTransferPickupProgress, type PickupProgress } from '../api/transferPickups'
vi.mock('../api/transferPickups', () => ({ getTransferPickupProgress: vi.fn(), downloadTransferPickupEvidence: vi.fn() }))
const progress = (tripNo: string): PickupProgress => ({ tripNo, status: 'ARRIVED', vehiclePlate: 'TEST-1', driverName: '验收司机', driverPhone: '000', plannedDepartureAt: null, actualPackages: 5, pickedAt: null, arrivedAt: null, remark: null, evidence: [] })
it('ignores an old customer response after switching the order identity', async () => {
let resolve!: (value: PickupProgress) => void
vi.mocked(getTransferPickupProgress).mockImplementationOnce(() => new Promise(done => { resolve = done })).mockResolvedValueOnce(progress('TR-NEW'))
const wrapper = shallowMount(TransferPickupProgressPanel, { props: { customerId: 'C1', transferNo: 'WT-OLD' }, global: { stubs: { ElButton: { template: '<button><slot/></button>' } } } })
await wrapper.setProps({ customerId: 'C2', transferNo: 'WT-NEW' }); await flushPromises(); resolve(progress('TR-OLD')); await flushPromises()
expect(wrapper.text()).toContain('TR-NEW'); expect(wrapper.text()).not.toContain('TR-OLD')
expect(getTransferPickupProgress).toHaveBeenLastCalledWith('WT-NEW', 'C2'); wrapper.unmount()
})
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { onBeforeUnmount, ref, watch } from 'vue'
import { getApiErrorDetails } from '@/shared/api/http'
import { getTransferPickupProgress, downloadTransferPickupEvidence, type PickupProgress } from '../api/transferPickups'
const props = defineProps<{ transferNo: string; customerId: string }>()
const progress = ref<PickupProgress | null>(null), error = ref(''), loading = ref(false)
const labels: Record<string, string> = { PENDING_REVIEW: '待审核', WAITING: '待安排取货', ASSIGNED: '已安排取货', PICKED: '已取货', ARRIVED: '已送仓' }
const time = (value: string | null) => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
let sequence = 0
/** 业务号变化后忽略旧响应,客户之间不复用取货数据。 */
async function load() {
const current = ++sequence; progress.value = null; error.value = ''; loading.value = true
try { const value = await getTransferPickupProgress(props.transferNo, props.customerId); if (current === sequence) progress.value = value }
catch (cause) { if (current === sequence) error.value = getApiErrorDetails(cause, '取货进度加载失败').message }
finally { if (current === sequence) loading.value = false }
}
/** 凭证读取必须经过原单鉴权。 */
async function download(file: { fileId: string; fileName: string }) {
try { const blob = await downloadTransferPickupEvidence(props.transferNo, props.customerId, file.fileId); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = file.fileName; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000) }
catch (cause) { error.value = getApiErrorDetails(cause, '取货凭证下载失败').message }
}
watch(() => [props.customerId, props.transferNo], load, { immediate: true }); onBeforeUnmount(() => { sequence++ })
</script>
<template>
<section v-if="loading || progress || error" v-loading="loading" class="pickup-progress" aria-label="取货安排">
<div class="pickup-heading"><h3>取货安排</h3><el-button size="small" :disabled="loading" @click="load">刷新进度</el-button></div>
<el-alert v-if="error" :title="error" type="error" :closable="false" />
<dl v-if="progress" class="pickup-fields">
<div><dt>取货进度</dt><dd>{{ labels[progress.status] || progress.status }}</dd></div><div><dt>车次号</dt><dd>{{ progress.tripNo || '待安排' }}</dd></div>
<div><dt>车牌</dt><dd>{{ progress.vehiclePlate || '待分配' }}</dd></div><div><dt>司机</dt><dd>{{ progress.driverName || '待分配' }}</dd></div>
<div><dt>司机电话</dt><dd>{{ progress.driverPhone || '—' }}</dd></div><div><dt>计划取货发车</dt><dd>{{ time(progress.plannedDepartureAt) }}</dd></div>
<div><dt>实提箱数</dt><dd>{{ progress.actualPackages ?? '待取货' }}</dd></div><div><dt>取货时间</dt><dd>{{ time(progress.pickedAt) }}</dd></div>
<div><dt>送仓时间</dt><dd>{{ time(progress.arrivedAt) }}</dd></div><div v-if="progress.remark"><dt>取货备注</dt><dd>{{ progress.remark }}</dd></div>
</dl>
<div v-if="progress?.evidence.length" class="pickup-files"><span>取货凭证</span><el-button v-for="file in progress.evidence" :key="file.fileId" link type="primary" @click="download(file)">{{ file.fileName }}</el-button></div>
</section>
</template>
<style scoped>.pickup-progress{padding:16px;border:1px solid var(--border,#e5e7eb);border-radius:6px;margin:16px 0}.pickup-heading{display:flex;justify-content:space-between;align-items:center}.pickup-heading h3{margin:0;font-size:15px}.pickup-fields{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px 20px;margin:16px 0 0}.pickup-fields>div{display:flex;gap:10px}.pickup-fields dt{color:var(--muted,#64748b);flex-shrink:0}.pickup-fields dd{margin:0;overflow-wrap:anywhere}.pickup-files{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:14px}</style>
@@ -22,6 +22,7 @@ import OrderReferenceText from '../components/OrderReferenceText.vue'
import WarehouseTransferFulfillmentPanel from '../components/WarehouseTransferFulfillmentPanel.vue'
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
import WarehouseTransferReceiptPanel from '../components/WarehouseTransferReceiptPanel.vue'
import TransferPickupProgressPanel from '../components/TransferPickupProgressPanel.vue'
interface DetailField { label: string; value: string | number; wide?: boolean }
interface CargoRow { id: string; sku: string; productName: string; pallets: number; cartons: number; quantity: number; weight: number; volume: number }
@@ -269,6 +270,7 @@ onMounted(load)
<section v-else v-loading="loading" class="detail-surface" :aria-busy="loading">
<el-tabs v-model="activeTab" class="detail-tabs density-element-tabs density-contained-tabs">
<el-tab-pane name="basic"><template #label><span class="density-tab-icon" aria-hidden="true">📋</span><span>基本信息</span></template>
<TransferPickupProgressPanel v-if="detail?.order.orderType === 'WAREHOUSE_TRANSFER'" :customer-id="String(detail.order.customerId || '')" :transfer-no="String(detail.order.sourceOrderNo || '')" />
<section aria-label="商业来源与关联单据">
<h2 class="source-heading">关联单据</h2>
<dl class="detail-fields density-fields">