优化
This commit is contained in:
@@ -70,6 +70,10 @@ export interface RelabelSummaryDto {
|
||||
labelRequirement?: string | null
|
||||
totalFileId?: string | null
|
||||
totalFileName?: string | null
|
||||
cartonTotalFileId?: string | null
|
||||
cartonTotalFileName?: string | null
|
||||
skuTotalFileId?: string | null
|
||||
skuTotalFileName?: string | null
|
||||
totalFileUrl?: string | null
|
||||
chargeAmount?: number | string | null
|
||||
chargeCurrency?: string | null
|
||||
@@ -270,3 +274,9 @@ export async function downloadRelabelCompletionPhoto(relabelNo: string, fileId:
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
}
|
||||
|
||||
/** 通过平台授权入口读取贴标资料或面单。 */
|
||||
export async function downloadRelabelDocument(relabelNo: string, fileId: string, scope: string, itemId?: string) {
|
||||
const base = '/platform/v1/last-leg/relabel-applications/' + encodeURIComponent(relabelNo)
|
||||
return http.get<Blob, Blob>(scope === 'SHIPPING' ? base + '/shipping-label/' + encodeURIComponent(fileId) + '/download' : base + '/files/' + encodeURIComponent(fileId) + '/download', { params: scope === 'SHIPPING' ? undefined : { scope, itemId }, responseType: 'blob' })
|
||||
}
|
||||
|
||||
@@ -227,3 +227,11 @@ export async function reviewWarehouseTransfer(
|
||||
)
|
||||
return normalizeTransferDetail(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
/** 平台在指定客户范围内追加整单预报箱数。 */
|
||||
export async function increaseWarehouseTransferForecast(customerId: string, transferNo: string, payload: { expectedPackageCount: number; packageCount: number }) {
|
||||
const response = await http.post<ApiEnvelope<WarehouseTransferDetailDto>, ApiEnvelope<WarehouseTransferDetailDto>>(
|
||||
'/platform/v1/last-leg/warehouse-transfers/' + encodeURIComponent(transferNo) + '/increase-forecast-cartons', payload, { params: { customerId } },
|
||||
)
|
||||
return unwrapApiResponse(response)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { mount, flushPromises, enableAutoUnmount } from '@vue/test-utils'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import RelabelDocumentsPanel from './RelabelDocumentsPanel.vue'
|
||||
import { getRelabelOrder, downloadRelabelDocument, type RelabelDetailDto } from '../api/relabelOrders'
|
||||
vi.mock('../api/relabelOrders', () => ({ getRelabelOrder: vi.fn(), downloadRelabelDocument: vi.fn() }))
|
||||
enableAutoUnmount(afterEach)
|
||||
const detail = { application: { id: '1', relabelNo: 'RL-1', cartonTotalFileId: '11', cartonTotalFileName: '箱唛.pdf', skuTotalFileId: '12', skuTotalFileName: 'SKU.pdf', totalFileId: '12', totalFileName: 'SKU.pdf', shippingLabelFileId: '13', shippingLabelFileName: '面单.pdf' }, items: [{ id: '22', cartonMark: 'BX-1', skuCode: 'SKU-A', relabelFileId: '14', relabelFileName: '明细.pdf' }], history: [] } as RelabelDetailDto
|
||||
const mountPanel = (value = detail) => mount(RelabelDocumentsPanel, { props: { relabelNo: 'RL-1', detail: value }, global: { plugins: [ElementPlus] } })
|
||||
describe('贴标附件回显', () => {
|
||||
it('分别显示两类总文件、面单与箱明细,兼容总文件不重复', async () => {
|
||||
const wrapper = mountPanel(); await flushPromises()
|
||||
const rows = wrapper.findAll('.el-table__body tbody tr')
|
||||
expect(rows).toHaveLength(4)
|
||||
expect(wrapper.text()).toContain('箱唛.pdf'); expect(wrapper.text()).toContain('SKU.pdf')
|
||||
expect(wrapper.text()).toContain('面单.pdf'); expect(wrapper.text()).toContain('明细.pdf')
|
||||
expect(wrapper.text()).toContain('BX-1 / SKU-A')
|
||||
expect(getRelabelOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
it('预览请求使用申请号、文件分类及明细编号,并可重试失败请求', async () => {
|
||||
vi.mocked(downloadRelabelDocument).mockRejectedValue(new Error('文件暂不可用'))
|
||||
const wrapper = mountPanel(); await flushPromises()
|
||||
const rows = wrapper.findAll('.el-table__body tbody tr')
|
||||
await rows[3]!.findAll('button')[0]!.trigger('click'); await flushPromises()
|
||||
expect(downloadRelabelDocument).toHaveBeenLastCalledWith('RL-1', '14', 'ITEM', '22')
|
||||
expect(rows[3]!.find('button').attributes('disabled')).toBeUndefined()
|
||||
await rows[2]!.findAll('button')[0]!.trigger('click'); await flushPromises()
|
||||
expect(downloadRelabelDocument).toHaveBeenLastCalledWith('RL-1', '13', 'SHIPPING', undefined)
|
||||
})
|
||||
it('资料加载失败可单独重试,不冒充空附件', async () => {
|
||||
vi.mocked(getRelabelOrder).mockRejectedValueOnce(new Error('资料加载失败')).mockResolvedValueOnce(detail)
|
||||
const wrapper = mount(RelabelDocumentsPanel, { props: { relabelNo: 'RL-1' }, global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||
expect(wrapper.get('[role="alert"]').text()).toContain('资料加载失败')
|
||||
await wrapper.get('[role="alert"] button').trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('箱唛.pdf')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getRelabelOrder, downloadRelabelDocument, type RelabelDetailDto } from '../api/relabelOrders'
|
||||
const props = defineProps<{ relabelNo: string; detail?: RelabelDetailDto | null }>()
|
||||
const loaded = ref<RelabelDetailDto | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const busy = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewOpen = ref(false)
|
||||
const previewTitle = ref('')
|
||||
let generation = 0
|
||||
const current = computed(() => props.detail || loaded.value)
|
||||
/** 各申请分别加载,失败只影响资料区,不阻断订单详情。 */
|
||||
async function load() {
|
||||
const ticket = ++generation
|
||||
loaded.value = null; error.value = ''
|
||||
if (props.detail || !props.relabelNo) { loading.value = false; return }
|
||||
loading.value = true
|
||||
try { const result = await getRelabelOrder(props.relabelNo); if (ticket === generation) loaded.value = result }
|
||||
catch (cause) { if (ticket === generation) error.value = cause instanceof Error ? cause.message : '贴标资料加载失败' }
|
||||
finally { if (ticket === generation) loading.value = false }
|
||||
}
|
||||
watch(() => [props.relabelNo, props.detail], () => { clearPreview(); void load() }, { immediate: true })
|
||||
interface DocumentRow { key: string; kind: string; fileId: string; fileName: string; scope: string; itemId?: string; context?: string }
|
||||
const documents = computed<DocumentRow[]>(() => {
|
||||
const result: DocumentRow[] = []
|
||||
const application = current.value?.application
|
||||
if (!application) return result
|
||||
// 旧总文件仅作为兼容回显,避免与已分类型的总文件重复展示。
|
||||
const add = (kind: string, id: string | null | undefined, name: string | null | undefined, scope: string, itemId?: string, context?: string) => {
|
||||
if (id) result.push({ key: scope + ':' + (itemId || '') + ':' + id, kind, fileId: id, fileName: name || '未命名文件', scope, itemId, context })
|
||||
}
|
||||
add('箱唛总文件', application.cartonTotalFileId, application.cartonTotalFileName, 'CARTON_TOTAL')
|
||||
add('SKU 标签总文件', application.skuTotalFileId, application.skuTotalFileName, 'SKU_TOTAL')
|
||||
if (application.totalFileId !== application.cartonTotalFileId && application.totalFileId !== application.skuTotalFileId)
|
||||
add('贴标总文件', application.totalFileId, application.totalFileName, 'TOTAL')
|
||||
add('面单文件', application.shippingLabelFileId, application.shippingLabelFileName, 'SHIPPING')
|
||||
for (const item of current.value?.items || []) add('明细文件', item.relabelFileId, item.relabelFileName, 'ITEM', String(item.id), [item.cartonMark, item.skuCode].filter(Boolean).join(' / '))
|
||||
return result
|
||||
})
|
||||
/** 切换申请及关闭预览时释放文件对象。 */
|
||||
function clearPreview() { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value); previewUrl.value = ''; previewOpen.value = false }
|
||||
/** 使用带身份认证的下载接口,预览和保存共享同一归属校验。 */
|
||||
async function openFile(key: string, download = false) {
|
||||
const row = documents.value.find(document => document.key === key)
|
||||
if (!row || busy.value) return
|
||||
busy.value = true
|
||||
const ticket = generation
|
||||
try {
|
||||
const blob = await downloadRelabelDocument(props.relabelNo, row.fileId, row.scope, row.itemId)
|
||||
if (ticket !== generation) return
|
||||
if (download) {
|
||||
const url = URL.createObjectURL(blob); const link = document.createElement('a')
|
||||
link.href = url; link.download = row.fileName; link.click(); window.setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
} else { clearPreview(); previewUrl.value = URL.createObjectURL(blob); previewTitle.value = row.fileName; previewOpen.value = true }
|
||||
} catch (cause) { ElMessage.error(cause instanceof Error ? cause.message : '文件读取失败,请重试') }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { ++generation; clearPreview() })
|
||||
</script>
|
||||
<template>
|
||||
<section class="relabel-documents" aria-label="客户上传的贴标文件">
|
||||
<h3>贴标文件<span v-if="relabelNo"> · {{ relabelNo }}</span></h3>
|
||||
<p v-if="loading">资料加载中…</p>
|
||||
<p v-else-if="error" role="alert">{{ error }} <el-button link type="primary" @click="load">重试</el-button></p>
|
||||
<el-table v-else :data="documents" row-key="key" border empty-text="暂无上传的贴标文件">
|
||||
<el-table-column prop="kind" label="文件类型" width="150" />
|
||||
<el-table-column prop="context" label="对应箱唛 / SKU" min-width="180" />
|
||||
<el-table-column prop="fileName" label="文件名称" min-width="240" />
|
||||
<el-table-column label="操作" width="150"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="openFile(String(row.key))">查看</el-button><el-button link type="primary" :disabled="busy" @click="openFile(String(row.key), true)">下载</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="previewOpen" :title="previewTitle" width="80%" append-to-body @closed="clearPreview"><iframe v-if="previewUrl" :src="previewUrl" :title="previewTitle" class="relabel-document-preview" /></el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.relabel-documents { margin: 16px 0; }
|
||||
.relabel-documents h3 { margin: 0 0 12px; font-size: 14px; }
|
||||
.relabel-documents h3 span { font-weight: 400; }
|
||||
.relabel-document-preview { width: 100%; height: 70vh; border: 0; }
|
||||
</style>
|
||||
@@ -3,9 +3,10 @@ import { defineComponent, h } from 'vue'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import WarehouseTransferReceiptPanel from './WarehouseTransferReceiptPanel.vue'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, increaseWarehouseTransferForecast, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
|
||||
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn() }))
|
||||
vi.mock('../api/warehouseTransfers', () => ({ getWarehouseTransfer: vi.fn(), getWarehouseTransferCartonLabel: vi.fn(), increaseWarehouseTransferForecast: vi.fn() }))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
enableAutoUnmount(afterEach)
|
||||
const DataTableStub = defineComponent({
|
||||
props: ['data', 'columns'],
|
||||
@@ -131,3 +132,16 @@ it('displays saved discrepancy reason once without opening box details', async (
|
||||
expect(wrapper.findAll('.receipt-discrepancy-reason')).toHaveLength(1)
|
||||
expect(wrapper.find('.receipt-discrepancy-reason').text()).toBe('实收差异原因:短收一箱,客户漏发')
|
||||
})
|
||||
|
||||
/** 平台只填新增箱数,保留SKU预报并按原箱数校验。 */
|
||||
it('平台可补整单预报箱数,无需选择SKU', async () => {
|
||||
const value = { ...fixture(), inboundCode: 'WT-SOURCE', status: 'WAIT_PDA_RECEIVING' }
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(value)
|
||||
vi.mocked(increaseWarehouseTransferForecast).mockResolvedValue({ ...value, packageCount: 6 })
|
||||
const wrapper = render(); await flushPromises()
|
||||
await wrapper.findAll('button').find(button => button.text() === '增加预报箱数')!.trigger('click')
|
||||
const number = wrapper.findComponent({ name: 'ElInputNumber' }); await number.vm.$emit('update:modelValue', 2)
|
||||
await wrapper.findAll('button').find(button => button.text() === '确认增加')!.trigger('click'); await flushPromises()
|
||||
expect(increaseWarehouseTransferForecast).toHaveBeenCalledWith('CUSTOMER', 'WT-SOURCE', { expectedPackageCount: 4, packageCount: 6 })
|
||||
expect(wrapper.text()).toContain('预报箱数 6 箱')
|
||||
})
|
||||
|
||||
@@ -7,12 +7,35 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, type WarehouseTransferCartonDto, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
import { getWarehouseTransfer, getWarehouseTransferCartonLabel, increaseWarehouseTransferForecast, type WarehouseTransferCartonDto, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
|
||||
const props = defineProps<{ customerId: string; transferNo: string }>()
|
||||
const emit = defineEmits<{ summary: [value: { boxes: number; forecastBoxes: number; forecastSkus: number; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null] }>()
|
||||
const emit = defineEmits<{ summary: [value: { boxes: number; forecastBoxes: number; forecastSkus: number; forecastQuantity: number | null; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null] }>()
|
||||
const detail = ref<WarehouseTransferDetailDto | null>(null)
|
||||
const access = useAccessPreviewStore()
|
||||
const increaseOpen = ref(false)
|
||||
const increasing = ref(false)
|
||||
const additionalCount = ref(1)
|
||||
const forecastSnapshot = ref(0)
|
||||
const canIncreaseForecast = computed(() => access.can(permission.warehouseTransferApprove) && detail.value?.transferNo === detail.value?.inboundCode && ['PENDING_REVIEW', 'WAIT_PDA_RECEIVING'].includes(detail.value?.status || '') && Number(detail.value?.packageCount) < 5000)
|
||||
/** 记录打开时箱数,提交时由后端核对并发修改。 */
|
||||
function openIncreaseForecast() { forecastSnapshot.value = Number(detail.value?.packageCount); additionalCount.value = 1; increaseOpen.value = true }
|
||||
/** 只补充箱号,不改变原SKU预报或仓库已录入的测量。 */
|
||||
async function saveIncreaseForecast() {
|
||||
if (increasing.value || !Number.isSafeInteger(additionalCount.value) || additionalCount.value < 1) return
|
||||
const ticket = generation
|
||||
increasing.value = true
|
||||
try {
|
||||
const value = await increaseWarehouseTransferForecast(props.customerId, props.transferNo, { expectedPackageCount: forecastSnapshot.value, packageCount: forecastSnapshot.value + additionalCount.value })
|
||||
if (ticket !== generation) return
|
||||
detail.value = value; increaseOpen.value = false
|
||||
ElMessage.success('预报箱数已增加,请通知PDA刷新后继续清点')
|
||||
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '增加预报箱数失败').message) }
|
||||
finally { increasing.value = false }
|
||||
}
|
||||
const printing = ref(false)
|
||||
const printError = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
@@ -37,7 +60,7 @@ const totals = computed(() => ({
|
||||
volume: cartons.value.reduce((sum, box) => sum + Number(volume(box) || 0), 0),
|
||||
skus: new Set(cartons.value.flatMap(box => boxLines(box).map(line => line.skuCode)).filter(Boolean)).size,
|
||||
}))
|
||||
watch([detail, totals], () => emit('summary', detail.value ? { ...totals.value, boxes: cartons.value.length, forecastBoxes: detail.value.packageCount, forecastSkus: new Set(detail.value.items.map(item => item.skuCode).filter(Boolean)).size, pallets: detail.value.pallets.length, warehouseId: detail.value.operatingWarehouseId } : null))
|
||||
watch([detail, totals], () => emit('summary', detail.value ? { ...totals.value, boxes: cartons.value.length, forecastBoxes: detail.value.packageCount, forecastQuantity: detail.value.items.some(item => item.quantity == null) ? null : detail.value.items.reduce((sum, item) => sum + Number(item.quantity), 0), forecastSkus: new Set(detail.value.items.map(item => item.skuCode).filter(Boolean)).size, pallets: detail.value.pallets.length, warehouseId: detail.value.operatingWarehouseId } : null))
|
||||
const comparisonRows = computed(() => (detail.value?.items || []).map(item => {
|
||||
const boxes = cartons.value.filter(box => boxLines(box).some(line => String(line.transferItemId) === String(item.id)))
|
||||
const receivedQuantity = boxes.reduce((sum, box) => sum + boxLines(box).filter(line => String(line.transferItemId) === String(item.id)).reduce((total, line) => total + Number(line.quantity), 0), 0)
|
||||
@@ -122,6 +145,7 @@ onBeforeUnmount(() => { generation++ })
|
||||
<LoadErrorState v-if="error" title="签收明细加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-else-if="detail">
|
||||
<p v-for="reason in discrepancyReasons" :key="reason" class="receipt-discrepancy-reason"><strong>实收差异原因:</strong>{{ reason }}</p>
|
||||
<div class="receipt-box-heading"><el-button v-if="canIncreaseForecast" type="primary" size="small" @click="openIncreaseForecast">增加预报箱数</el-button><span>预报箱数 {{ detail.packageCount }} 箱 · 预报SKU总件数 {{ detail.items.some(item => item.quantity == null) ? '未记录' : detail.items.reduce((sum, item) => sum + Number(item.quantity), 0) + ' 件' }}</span></div>
|
||||
<h3>预报与实际签收对比 <small>差异 = 实收 − 预报{{ cartons.length ? '(按当前已清点箱汇总)' : ',待签收后显示' }}</small></h3>
|
||||
<DataTable class="receipt-aligned-table" :fit="false" :data="comparisonRows" :columns="forecastColumns" row-key="id" :show-pagination="false" empty-text="暂无预报货物明细">
|
||||
<template #quantityDifference="{ row }"><span :class="{ 'receipt-difference': cartons.length && Number(row.quantityDifference) !== 0 }">{{ differenceText(row.quantityDifference) }}</span></template>
|
||||
@@ -134,6 +158,11 @@ onBeforeUnmount(() => { generation++ })
|
||||
<template #boxAction="{ row }"><el-button link type="primary" @click="showBox(row.cartonMark)"><Eye :size="14" />箱内明细</el-button></template>
|
||||
</DataTable></AppDialog>
|
||||
</template>
|
||||
<AppDialog class="admin-density-dialog" v-model="increaseOpen" title="增加预报箱数" width="520px" :show-footer="false">
|
||||
<el-form label-width="100px"><el-form-item label="新增箱数"><el-input-number v-model="additionalCount" :min="1" :max="Math.max(1, 5000 - forecastSnapshot)" :precision="0" :disabled="increasing" /></el-form-item></el-form>
|
||||
<p>保留原箱唛,仅补生成新增箱唛;箱内SKU和件数以PDA清点为准。</p>
|
||||
<el-button :disabled="increasing" @click="increaseOpen = false">取消</el-button><el-button type="primary" :loading="increasing" @click="saveIncreaseForecast">确认增加</el-button>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog" v-model="boxOpen" title="箱内清点明细" width="820px" :show-footer="false">
|
||||
<template v-if="selectedBox"><p>{{ selectedBox.cartonMark }} · 清点时间 {{ formatOverseasDateTime(selectedBox.countedAt) }}</p><DataTable :data="boxItems" :columns="boxItemColumns" row-key="sku" :show-pagination="false" /><p v-if="selectedBox.discrepancyReason">差异说明:{{ selectedBox.discrepancyReason }}</p></template>
|
||||
</AppDialog>
|
||||
|
||||
@@ -20,6 +20,7 @@ import OverseasDetailShell from '../components/OverseasDetailShell.vue'
|
||||
import { businessOrderLink } from '../businessOrderLink'
|
||||
import OrderReferenceText from '../components/OrderReferenceText.vue'
|
||||
import WarehouseTransferFulfillmentPanel from '../components/WarehouseTransferFulfillmentPanel.vue'
|
||||
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
|
||||
import WarehouseTransferReceiptPanel from '../components/WarehouseTransferReceiptPanel.vue'
|
||||
|
||||
interface DetailField { label: string; value: string | number; wide?: boolean }
|
||||
@@ -33,7 +34,7 @@ const router = useRouter()
|
||||
const access = useAccessPreviewStore()
|
||||
const orderId = computed(() => String(route.params.orderId || ''))
|
||||
const detail = ref<LastMileOrderDetailDto | null>(null)
|
||||
const receiptSummary = ref<{ boxes: number; forecastBoxes: number; forecastSkus: number; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null>(null)
|
||||
const receiptSummary = ref<{ boxes: number; forecastBoxes: number; forecastSkus: number; forecastQuantity: number | null; skus: number; quantity: number; pallets: number; weight: number; volume: number; warehouseId?: string | null } | null>(null)
|
||||
const resolvedWarehouseName = ref('')
|
||||
const warehouseLookupFailed = ref(false)
|
||||
const warehouseDisplayName = computed(() => detail.value?.order.overseasWarehouseName || resolvedWarehouseName.value || detail.value?.order.overseasWarehouseCode || (warehouseLookupFailed.value ? '仓库名称加载失败' : '—'))
|
||||
@@ -138,6 +139,7 @@ const overviewFields = computed(() => {
|
||||
{ label: '海外仓', value: warehouseDisplayName.value },
|
||||
{ label: '预报箱数', value: `${summary.forecastBoxes} 箱` },
|
||||
{ label: '预报 SKU', value: `${summary.forecastSkus} 种` },
|
||||
{ label: '预报 SKU 总件数', value: summary.forecastQuantity == null ? '未记录' : `${summary.forecastQuantity} 件` },
|
||||
{ label: '实收箱数', value: `${summary.boxes} 箱` },
|
||||
{ label: '实收 SKU', value: `${summary.skus} 种` },
|
||||
{ label: '实收件数', value: `${summary.quantity} 件` },
|
||||
@@ -280,6 +282,12 @@ onMounted(load)
|
||||
<div v-for="field in basicFields" :key="field.label" :class="{ wide: field.wide }"><dt>{{ field.label }}</dt><dd :class="{ 'detail-field-value--business': isSingleLineBusinessField(field), 'package-difference--nonzero': isNonzeroDifference(field) }" :title="isSingleLineBusinessField(field) ? String(field.value) : undefined">{{ field.value }}</dd></div>
|
||||
</dl>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="access.can(permission.relabelOrderList) && (detail?.order.relabelApplications?.length || detail?.order.relabelOrderNo)" name="relabel-files" label="贴标文件">
|
||||
<template v-if="activeTab === 'relabel-files'">
|
||||
<RelabelDocumentsPanel v-for="application in detail?.order.relabelApplications || []" :key="application.relabelNo" :relabel-no="application.relabelNo" />
|
||||
<RelabelDocumentsPanel v-if="!detail?.order.relabelApplications?.length && detail?.order.relabelOrderNo" :relabel-no="detail.order.relabelOrderNo" />
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="cargo"><template #label><span class="density-tab-icon" aria-hidden="true">📦</span><span>货物信息</span></template><section class="table-panel"><WarehouseTransferReceiptPanel v-if="detail?.order.orderType === 'WAREHOUSE_TRANSFER' && activeTab === 'cargo'" :customer-id="String(detail.order.customerId || '')" :transfer-no="String(detail.order.sourceOrderNo || '')" @summary="receiptSummary = $event" /><DataTable v-else-if="detail?.order.orderType !== 'WAREHOUSE_TRANSFER'" :data="cargo" :columns="cargoColumns" column-settings-mode="hide" row-key="id" :empty-text="isReturnOrder ? '暂无客户预报货物明细' : '暂无货物明细'"></DataTable></section></el-tab-pane>
|
||||
<el-tab-pane v-if="detail?.order.orderType === 'RETURN_RELABEL'" name="return-receipt"><template #label><span class="density-tab-icon" aria-hidden="true">📦</span><span>退货签收与箱内清点</span></template>
|
||||
<section v-if="detail.returnReceipt" class="table-panel" aria-label="退货签收与箱内清点">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { downloadRelabelCompletionPhoto, downloadRelabelPdf, getRelabelOrder, up
|
||||
import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import OverseasReadOnlyDetail from '../components/OverseasReadOnlyDetail.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import RelabelDocumentsPanel from '../components/RelabelDocumentsPanel.vue'
|
||||
import { relabelSkuCodes } from '../utils/relabelSkuDisplay'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -129,7 +130,7 @@ async function previewCompletionPhoto(fileId: string) {
|
||||
<template #state><LoadErrorState v-if="loadError" title="换标单详情加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template>
|
||||
<template #content-after>
|
||||
<section v-if="access.can(permission.relabelOrderStatus) && ['CUSTOMER_CONFIRM_PENDING', 'PROCESSING'].includes(detail?.application.status || '') && detail?.completionEvidence?.photoFileIds?.length" class="relabel-document-panel relabel-review-actions"><el-button v-if="detail?.application.status === 'CUSTOMER_CONFIRM_PENDING'" type="primary" :loading="confirming" @click="confirmCompletion"><CheckCircle2 :size="15" />确认换标完成</el-button><el-button type="warning" :loading="returning" @click="returnForRework"><Undo2 :size="15" />退回重新贴标</el-button></section>
|
||||
<section class="relabel-document-panel" aria-label="PDA换标完工证据"><div class="relabel-mark-list__head"><h2>PDA 换标完工证据</h2><span>{{ detail?.completionEvidence?.reviewerName ? `复核人:${detail.completionEvidence.reviewerName}` : '等待仓库作业' }}</span></div><div v-if="detail?.completionEvidence?.photoFileIds?.length" class="document-row"><strong>共 {{ detail.completionEvidence.photoFileIds.length }} 张完工照片</strong><span class="document-actions density-actions"><el-button class="density-action" data-tone="neutral" v-for="(fileId, index) in detail.completionEvidence.photoFileIds" :key="fileId" size="small" @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></span></div><div v-else class="attachment-empty">暂未上传完工照片</div></section>
|
||||
<RelabelDocumentsPanel v-if="detail" :relabel-no="detail.application.relabelNo" :detail="detail" /><section class="relabel-document-panel" aria-label="PDA换标完工证据"><div class="relabel-mark-list__head"><h2>PDA 换标完工证据</h2><span>{{ detail?.completionEvidence?.reviewerName ? `复核人:${detail.completionEvidence.reviewerName}` : '等待仓库作业' }}</span></div><div v-if="detail?.completionEvidence?.photoFileIds?.length" class="document-row"><strong>共 {{ detail.completionEvidence.photoFileIds.length }} 张完工照片</strong><span class="document-actions density-actions"><el-button class="density-action" data-tone="neutral" v-for="(fileId, index) in detail.completionEvidence.photoFileIds" :key="fileId" size="small" @click="previewCompletionPhoto(fileId)"><Eye :size="14" />照片 {{ index + 1 }}</el-button></span></div><div v-else class="attachment-empty">暂未上传完工照片</div></section>
|
||||
<section class="relabel-document-panel" aria-label="总换标文件"><div class="relabel-mark-list__head"><h2>总换标 PDF</h2><label class="file-picker"><FileUp :size="15" />{{ uploading ? '上传中' : (detail?.application.totalFileName ? '更换PDF' : '上传PDF') }}<input type="file" accept="application/pdf,.pdf" :disabled="uploading" @change="uploadPdf($event, 'TOTAL')" /></label></div><div v-if="detail?.application.totalFileId" class="document-row"><strong>{{ detail.application.totalFileName }}</strong><el-button class="density-action" data-tone="neutral" size="small" @click="previewPdf(detail.application.totalFileId, detail.application.totalFileName || '总换标PDF', 'TOTAL')"><Eye :size="14" />直接预览</el-button></div><div v-else class="attachment-empty">暂未上传总换标PDF</div></section>
|
||||
<section class="relabel-mark-list" aria-label="箱唛换标信息">
|
||||
<div class="relabel-mark-list__head"><h2>箱唛换标信息</h2><span>共 {{ detail?.items.length || 0 }} 条</span></div>
|
||||
|
||||
Reference in New Issue
Block a user