优化
This commit is contained in:
@@ -2,14 +2,11 @@
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getRelabel, downloadRelabelDocument, type RelabelDetail } from '../api/warehouseServices'
|
||||
const props = defineProps<{ relabelNo: string; detail?: RelabelDetail | null }>()
|
||||
const props = defineProps<{ relabelNo: string; detail?: RelabelDetail | null; inline?: boolean }>()
|
||||
const loaded = ref<RelabelDetail | 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)
|
||||
/** 各申请分别加载,失败只影响资料区,不阻断订单详情。 */
|
||||
@@ -22,28 +19,26 @@ async function load() {
|
||||
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 }
|
||||
watch(() => [props.relabelNo, props.detail], () => { void load() }, { immediate: true })
|
||||
interface DocumentRow { key: string; kind: string; fileId: string; fileName: string; scope: string; itemId?: 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 })
|
||||
const add = (kind: string, id: string | null | undefined, name: string | null | undefined, scope: string, itemId?: string) => {
|
||||
if (id) result.push({ key: scope + ':' + (itemId || '') + ':' + id, kind, fileId: id, fileName: name || '未命名文件', scope, itemId })
|
||||
}
|
||||
add('箱唛总文件', application.cartonTotalFileId, application.cartonTotalFileName, 'CARTON_TOTAL')
|
||||
add('SKU 标签总文件', application.skuTotalFileId, application.skuTotalFileName, 'SKU_TOTAL')
|
||||
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(' / '))
|
||||
for (const item of current.value?.items || []) add('明细文件', item.relabelFileId, item.relabelFileName, 'ITEM', String(item.id))
|
||||
return result
|
||||
})
|
||||
/** 切换申请及关闭预览时释放文件对象。 */
|
||||
function clearPreview() { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value); previewUrl.value = ''; previewOpen.value = false }
|
||||
/** 使用带身份认证的下载接口,预览和保存共享同一归属校验。 */
|
||||
async function openFile(key: string, download = false) {
|
||||
/** 使用带身份认证的下载接口,保留文件归属校验和原始文件名。 */
|
||||
async function downloadFile(key: string) {
|
||||
const row = documents.value.find(document => document.key === key)
|
||||
if (!row || busy.value) return
|
||||
busy.value = true
|
||||
@@ -51,32 +46,33 @@ async function openFile(key: string, download = false) {
|
||||
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 }
|
||||
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)
|
||||
} catch (cause) { ElMessage.error(cause instanceof Error ? cause.message : '文件读取失败,请重试') }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { ++generation; clearPreview() })
|
||||
onBeforeUnmount(() => { ++generation })
|
||||
</script>
|
||||
<template>
|
||||
<section class="relabel-documents" aria-label="客户上传的贴标文件">
|
||||
<h3>贴标文件<span v-if="relabelNo"> · {{ relabelNo }}</span></h3>
|
||||
<section class="relabel-documents" :class="{ 'relabel-documents--inline': inline }" aria-label="客户上传的贴标文件">
|
||||
<h3>贴标文件</h3>
|
||||
<p v-if="loading">资料加载中…</p>
|
||||
<p v-else-if="error" role="alert">{{ error }} <el-button link type="primary" @click="load">重试</el-button></p>
|
||||
<div v-else-if="inline" class="relabel-document-links">
|
||||
<div v-for="row in documents" :key="row.key" class="relabel-document-link"><span>{{ row.kind }}</span><el-button link type="primary" :disabled="busy" @click="downloadFile(row.key)">下载</el-button></div>
|
||||
<span v-if="!documents.length">暂无上传的贴标文件</span>
|
||||
</div>
|
||||
<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-column prop="kind" label="文件类型" min-width="150" />
|
||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="downloadFile(String(row.key))">下载</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; }
|
||||
.relabel-documents--inline { display: flex; align-items: center; gap: 12px; margin: 0; min-width: 0; }
|
||||
.relabel-documents--inline h3 { flex-shrink: 0; margin: 0; color: var(--muted); font-size: 12px; font-weight: 400; }
|
||||
.relabel-document-links { display: flex; flex-wrap: wrap; gap: 4px 12px; }
|
||||
.relabel-document-link { display: flex; align-items: center; gap: 8px; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { createSSRApp } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { parse } from 'vue/compiler-sfc'
|
||||
|
||||
const source = readFileSync(new URL('./ReviewRejectionNotice.vue', import.meta.url), 'utf8')
|
||||
const template = parse(source).descriptor.template.content
|
||||
const render = (status, reason) => renderToString(createSSRApp({ template, props: ['status', 'reason'] }, { status, reason }))
|
||||
|
||||
test('rejected documents display the actual reason safely', async () => {
|
||||
const html = await render('REJECTED', '箱数不符\n请核对 <SKU>')
|
||||
assert.match(html, /驳回原因/)
|
||||
assert.match(html, /箱数不符\n请核对 <SKU>/)
|
||||
assert.match(html, /role="alert"/)
|
||||
})
|
||||
|
||||
test('non-rejected documents do not show an old rejection reason', async () => {
|
||||
for (const status of ['PENDING_REVIEW', 'APPROVED', 'COMPLETED', undefined]) {
|
||||
assert.doesNotMatch(await render(status, '旧审核原因'), /旧审核原因|驳回原因/)
|
||||
}
|
||||
})
|
||||
|
||||
test('legacy rejection without a reason has an explicit fallback', async () => {
|
||||
for (const reason of [undefined, '', ' ']) {
|
||||
assert.match(await render('REJECTED', reason), /暂未记录驳回原因/)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ status?: string; reason?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="status === 'REJECTED'" class="review-rejection-notice" role="alert">
|
||||
<strong>驳回原因</strong>
|
||||
<span>{{ reason?.trim() || '暂未记录驳回原因,请联系平台确认' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-rejection-notice{display:flex;align-items:flex-start;gap:12px;margin:12px 16px;padding:10px 12px;border:1px solid var(--el-color-danger-light-7);border-radius:4px;background:var(--el-color-danger-light-9);color:var(--el-color-danger);font-size:13px;line-height:1.7}
|
||||
.review-rejection-notice strong{flex-shrink:0;font-weight:600}
|
||||
.review-rejection-notice span{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ import { useAuthStore } from '@/modules/auth/public'
|
||||
import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
|
||||
import { createIdempotencyKey } from '@/utils/idempotencyKey'
|
||||
import { createReturnOrder, downloadReturnPhoto, getReturnOrder, listReturnOrders, updateReturnOrder, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
|
||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||
import { getReturnInventoryLabel } from '../api/returnInventory'
|
||||
import { formatReturnVolume, receiptBoxDetails, receiptPhysicalDetails, receiptPrintableBoxCount, receiptTrackingNo } from '../utils/returnReceiptDisplay'
|
||||
|
||||
@@ -363,6 +364,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<div class="return-detail-title"><strong>{{ selected.order.returnNo }}</strong><span>{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }} · 退货签收</span></div>
|
||||
<div class="return-detail-actions"><el-button v-if="canCreateRelabel && canApplyRelabel(selected.order)" type="primary" plain @click="openRelabel(selected.order)">贴标申请</el-button><el-tag :type="statusType(selected.order.status, selected.order.needRelabel)" :class="statusClass(selected.order.status, selected.order.needRelabel)" effect="light">{{ statusLabel(selected.order.status, selected.order.needRelabel) }}</el-tag></div>
|
||||
</header>
|
||||
<ReviewRejectionNotice :status="selected.order.status" :reason="selected.remark" />
|
||||
<section class="return-detail-section">
|
||||
<h3>签收概览</h3>
|
||||
<div class="return-detail-grid">
|
||||
@@ -374,7 +376,7 @@ onBeforeUnmount(clearPhotoUrls)
|
||||
<div><span>联系电话</span><strong>{{ selected.order.warehouseContactPhone || '—' }}</strong></div>
|
||||
<div><span>创建时间</span><strong>{{ formatDateTime(selected.order.createdAt) }}</strong></div>
|
||||
</div>
|
||||
<div v-if="selected.remark" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
<div v-if="selected.remark && selected.order.status !== 'REJECTED'" class="return-detail-remark"><span>备注</span><strong>{{ selected.remark }}</strong></div>
|
||||
</section>
|
||||
<section v-if="selected.order.relabelApplications?.length" class="return-detail-section">
|
||||
<h3>换标子单({{ selected.order.relabelApplications?.length || 0 }})</h3>
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { TableAction } from '@/components/common/tableActions'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { prepareTransferCartonLabels } from '../utils/transferCartonPrinting'
|
||||
import CompletionConfirmationDialog from '../components/CompletionConfirmationDialog.vue'
|
||||
import ReviewRejectionNotice from '../components/ReviewRejectionNotice.vue'
|
||||
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
|
||||
import {
|
||||
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel,
|
||||
@@ -135,6 +136,7 @@ const editingTransferNo = ref('')
|
||||
interface TransferPallet { id?: string | number; sequenceNo?: number; palletNo?: string; warehouseLocationCode?: string; cartonCount?: number; lengthCm?: number; widthCm?: number; heightCm?: number; grossWeightKg?: number; volumeCbm?: number; status?: string; completedAt?: string }
|
||||
interface TransferMeasurementPhoto { fileId: string | number; fileName?: string; contentType?: string; sizeBytes?: number; uploadedAt?: string }
|
||||
interface ServiceDetail extends Record<string, unknown> {
|
||||
reviewRemark?: string
|
||||
discardNo?: string; photos?: DiscardPhoto[]
|
||||
transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string
|
||||
destinationType?: 'NONE' | 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string
|
||||
@@ -1846,8 +1848,8 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<div class="warehouse-detail-actions"><el-button v-if="canCreate && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" type="primary" @click="resubmitRejectedRelabel">修改后重新提交</el-button><el-button v-if="canConfirmRelabel && props.service === 'labeling' && selectedRelabelDetail?.application.status === 'CUSTOMER_CONFIRM_PENDING' && !selectedRelabelDetail?.application.customerConfirmedAt" type="primary" @click="confirmSelectedRelabel">确认换标完成</el-button><el-button v-if="canConfirmDiscard && props.service === 'disposal' && selectedDetail?.status === 'PDA_COMPLETED'" type="primary" @click="openCompletionConfirmation(String(selectedDetail?.discardNo || ''), 'disposal')">废弃确认</el-button><el-button v-if="canCancelDiscard && props.service === 'disposal' && selectedDetail?.status === 'PENDING_REVIEW'" type="danger" @click="cancelSelectedDiscard">取消申请</el-button><el-button v-if="canCreate && props.service === 'transfer' && ['DRAFT', 'REJECTED'].includes(selectedDetail?.status || '')" type="primary" @click="openEdit(String(selectedDetail?.transferNo || selectedDetail?.['0'] || ''))">编辑</el-button><el-button v-if="canConfirmTransfer && props.service === 'transfer' && selectedDetail?.status === 'WAIT_CUSTOMER_CONFIRMATION'" type="primary" @click="confirmTransferMeasurements">确认测量数据</el-button><el-tag v-if="props.service !== 'transfer'" :type="props.service === 'labeling' ? relabelStatusTagType(selectedRelabelDetail?.application.status || '') : undefined" effect="light">{{ statusLabel(props.service === 'labeling' ? selectedRelabelDetail?.application.status || '' : String(selectedDetail?.status || '')) }}</el-tag></div>
|
||||
</header>
|
||||
<div v-if="detailError" class="warehouse-detail-error" role="alert"><strong>{{ detailError }}</strong><el-button @click="loadDetailPage">重新加载</el-button></div>
|
||||
<el-alert v-else-if="props.service === 'labeling' && selectedRelabelDetail?.application.status === 'REJECTED'" class="warehouse-rejected-alert" type="error" :closable="false" show-icon title="贴标单审核已驳回" :description="selectedRelabelDetail.application.reviewRemark || '平台未填写驳回备注'" />
|
||||
<div v-else class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div v-if="props.service === 'labeling'" class="warehouse-detail-grid"><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>客户参考号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if="props.service === 'transfer'" class="warehouse-detail-grid warehouse-transfer-overview-grid"><div><span>状态</span><el-tag :type="transferStatusTagType(selectedDetail?.status || '')" effect="light">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'" class="warehouse-overview-wide"><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预报 SKU 总件数</span><strong>{{ transferForecastQuantity }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type="transferRelabelStatusTagType(transferRelabelStatus(selectedDetail || {}).code)" effect="light">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
|
||||
<ReviewRejectionNotice v-if="!detailError" :status="props.service === 'labeling' ? selectedRelabelDetail?.application.status : selectedDetail?.status" :reason="props.service === 'labeling' ? selectedRelabelDetail?.application.reviewRemark : selectedDetail?.reviewRemark" />
|
||||
<div v-if="!detailError" class="warehouse-card warehouse-form-section"><h3>订单概览</h3><div v-if="props.service === 'labeling'" class="warehouse-detail-grid"><div><span>操作仓</span><strong>{{ warehouseDisplay(selectedRelabelDetail?.application.overseasWarehouseId) }}</strong></div><div><span>关联单号</span><strong>{{ selectedRelabelDetail?.application.customerOrderNo || '—' }}</strong></div><div><span>SKU 数(种)</span><strong>{{ selectedRelabelDetail ? relabelSkuCount(selectedRelabelDetail.items) : '—' }}</strong></div><div><span>总箱数</span><strong>{{ selectedRelabelDetail?.application.totalPackageCount ?? '—' }}</strong></div><div><span>贴标截止时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.labelDeadline) }}</strong></div><div><span>仓库联系人</span><strong>{{ selectedRelabelDetail?.application.contactName || '—' }}</strong></div><div><span>联系电话</span><strong>{{ selectedRelabelDetail?.application.contactPhone || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.createdAt) }}</strong></div></div><div v-else-if="props.service === 'transfer'" class="warehouse-detail-grid warehouse-transfer-overview-grid"><div><span>状态</span><el-tag :type="transferStatusTagType(selectedDetail?.status || '')" effect="light">{{ statusLabel(selectedDetail?.status || '') }}</el-tag></div><div><span>目的去向</span><strong>{{ destinationTypeLabel(selectedDetail?.destinationType, selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 目的仓</span><strong>{{ selectedDetail?.destinationWarehouseName || warehouseDisplay(selectedDetail?.destinationWarehouseCode || selectedDetail?.fbaWarehouseCode) }}</strong></div><div v-if="selectedDetail?.destinationType !== 'PRIVATE'"><span>FBA 货件号</span><strong>{{ selectedDetail?.fbaShipmentNo || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'" class="warehouse-overview-wide"><span>私人仓地址</span><strong>{{ selectedDetail?.privateDestinationAddress || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>邮编</span><strong>{{ selectedDetail?.privateDestinationPostalCode || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>私人仓联系人</span><strong>{{ selectedDetail?.privateDestinationContactName || '—' }}</strong></div><div v-if="selectedDetail?.destinationType === 'PRIVATE'"><span>联系电话</span><strong>{{ selectedDetail?.privateDestinationContactPhone || '—' }}</strong></div><div><span>海外仓</span><strong>{{ warehouseDisplay(selectedDetail?.operatingWarehouseId) }}</strong></div><div><span>预报 SKU 种数</span><strong>{{ new Set((selectedDetail?.items || []).map(item => item.skuCode).filter(Boolean)).size }}</strong></div><div><span>预报 SKU 总件数</span><strong>{{ transferForecastQuantity }}</strong></div><div><span>预计箱数</span><strong>{{ selectedDetail?.packageCount ?? '—' }}</strong></div><div><span>实际签收箱数</span><strong>{{ selectedDetail?.cartons?.filter((carton) => carton.status === 'COUNTED').length ?? '—' }}</strong></div><div><span>入仓方式</span><strong>{{ deliveryMethodLabel(selectedDetail?.deliveryMethod || '') }}</strong></div><div><span>期望发货时间</span><strong>{{ formatDateTime(selectedDetail?.estimatedShipAt) }}</strong></div><div><span>实际发货时间</span><strong>{{ formatDateTime(selectedDetail?.actualShipAt) }}</strong></div><div><span>是否换标</span><strong>{{ selectedDetail?.needRelabel ? '是' : '否' }}</strong></div><div><span>换标状态</span><el-tag :type="transferRelabelStatusTagType(transferRelabelStatus(selectedDetail || {}).code)" effect="light">{{ transferRelabelStatus(selectedDetail || {}).label }}</el-tag></div><div><span>换标单号</span><strong>{{ selectedDetail?.relabelNo || '—' }}</strong></div><div><span>创建时间</span><strong>{{ formatDateTime(selectedDetail?.createdAt) }}</strong></div></div><div v-else class="warehouse-detail-grid"><div v-for="(column, index) in config.columns.slice(1)" :key="column"><span>{{ column }}</span><strong>{{ selectedDetail?.[String(index)] || '—' }}</strong></div></div></div>
|
||||
<div v-if="props.service === 'disposal'" class="warehouse-card warehouse-form-section">
|
||||
<h3>本次废弃箱子</h3><div class="warehouse-table-scroll"><table class="warehouse-edit-table"><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th></tr></thead><tbody><tr v-for="(item, index) in selectedDetail?.items || []" :key="index"><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div>
|
||||
<h3>废弃现场照片</h3><div v-if="selectedDetail?.photos?.length" class="warehouse-photo-grid"><div v-for="photo in selectedDetail.photos" :key="photo.fileId" class="warehouse-photo-item"><el-image v-if="discardPhotoUrls[photo.fileId]" :src="discardPhotoUrls[photo.fileId]" :preview-src-list="discardPhotoPreviewUrls" :initial-index="discardPhotoPreviewUrls.indexOf(discardPhotoUrls[photo.fileId])" fit="cover" preview-teleported /><div v-else-if="discardPhotoLoading[photo.fileId]" class="warehouse-photo-placeholder">照片加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadDiscardPhoto(photo.fileId)">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div><div v-else class="warehouse-empty">暂无已提交的废弃现场照片</div>
|
||||
@@ -1907,17 +1909,17 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
<h3>换标箱级去向</h3>
|
||||
<div class="warehouse-table-scroll"><table><thead><tr><th>换标类型</th><th>仓库唛头</th><th>旧 SKU</th><th>新 SKU</th><th>新 FBA 箱唛</th><th>实际 FBA 目的仓</th><th>数量</th><th>备注</th></tr></thead><tbody><tr v-for="(binding, index) in selectedDetail?.relabelBindings || []" :key="`${binding.customerCartonMark}-${index}`"><td>{{ binding.operationType === 'CARTON_AND_SKU' ? 'SKU 和箱唛都换' : binding.operationType === 'SKU_LABEL' ? '只换 SKU' : '只换箱唛' }}</td><td>{{ binding.customerCartonMark || '—' }}</td><td>{{ binding.originalSku || '—' }}</td><td>{{ binding.newSku || '—' }}</td><td>{{ binding.fbaCartonMark || '—' }}</td><td>{{ binding.operationType === 'SKU_LABEL' ? '沿用原箱去向' : warehouseDisplay(binding.destinationWarehouseCode) }}</td><td>{{ binding.quantity ?? '—' }}</td><td>{{ binding.remark || '—' }}</td></tr><tr v-if="!selectedDetail?.relabelBindings?.length"><td colspan="8"><div class="warehouse-empty">暂无换标绑定明细</div></td></tr></tbody></table></div>
|
||||
</div>
|
||||
<div v-if="props.service === 'labeling' && canManageRelabelFiles" class="warehouse-card warehouse-form-section">
|
||||
<div v-if="props.service === 'labeling' && selectedRelabelDetail" class="warehouse-card warehouse-form-section">
|
||||
<div class="warehouse-section-heading"><h3>换标完成与出库</h3></div>
|
||||
<div class="warehouse-detail-grid warehouse-fulfillment-grid">
|
||||
<div><span>派送方式</span><strong>{{ deliveryMethodLabel(selectedRelabelDetail?.application.deliveryMethod || '') || '无需出库' }}</strong></div>
|
||||
<div><span>履约状态</span><el-tag :type="relabelFulfillmentTagType(selectedRelabelDetail?.application.fulfillmentStatus)" effect="light">{{ relabelFulfillmentLabel(selectedRelabelDetail?.application.fulfillmentStatus) }}</el-tag></div>
|
||||
<div><span>客户确认时间</span><strong>{{ formatDateTime(selectedRelabelDetail?.application.customerConfirmedAt) }}</strong></div>
|
||||
<div v-if="selectedRelabelDetail"><RelabelDocumentsPanel inline :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div>
|
||||
</div>
|
||||
<div class="warehouse-evidence"><span>仓库完工照片</span><div v-if="selectedRelabelDetail?.completionEvidence?.photoFileIds?.length" class="warehouse-photo-grid"><div v-for="(fileId, index) in selectedRelabelDetail.completionEvidence.photoFileIds" :key="fileId" class="warehouse-photo-item"><el-image v-if="completionPhotoUrls[fileId]" :src="completionPhotoUrls[fileId]" :preview-src-list="completionPhotoPreviewUrls" :initial-index="completionPhotoPreviewUrls.indexOf(completionPhotoUrls[fileId])" fit="cover" preview-teleported /><div v-else-if="completionPhotoLoading[fileId]" class="warehouse-photo-placeholder">照片 {{ index + 1 }} 加载中…</div><button v-else class="warehouse-photo-placeholder warehouse-photo-retry" type="button" @click="loadCompletionPhoto(fileId)">{{ completionPhotoErrors[fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传完工照片</strong></div>
|
||||
<p v-if="selectedRelabelDetail?.completionEvidence?.completionRemark" class="warehouse-completion-remark">完工备注:{{ selectedRelabelDetail.completionEvidence.completionRemark }}</p>
|
||||
</div>
|
||||
<div v-if="props.service === 'labeling' && selectedRelabelDetail" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedRelabelDetail.application.relabelNo" :detail="selectedRelabelDetail" /></div>
|
||||
<div v-if="props.service === 'transfer' && selectedDetail?.relabelNo" class="warehouse-card warehouse-form-section"><RelabelDocumentsPanel :relabel-no="selectedDetail.relabelNo" /></div>
|
||||
<div v-if="props.service === 'labeling'" class="warehouse-card warehouse-form-section">
|
||||
<div class="warehouse-section-heading"><h3>贴标明细资料</h3><input ref="relabelFileInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="uploadSelectedRelabelFile"></div>
|
||||
@@ -2154,10 +2156,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
|
||||
.warehouse-transfer-pallet-sku{display:inline-flex;align-items:center;gap:8px;max-width:100%;box-sizing:border-box;padding:4px 8px;background:#f5f7ff;border:1px solid #e5e9f8;border-radius:5px;font-size:12px;line-height:1.6}
|
||||
.warehouse-transfer-pallet-sku strong{font-weight:500;overflow-wrap:anywhere;min-width:0;color:var(--navy)}
|
||||
.warehouse-transfer-pallet-sku>span{flex-shrink:0;padding:0 7px;border:1px solid #e0e6ff;border-radius:4px;background:#eef1ff;color:var(--primary);font-weight:600;white-space:nowrap}
|
||||
.warehouse-service-page--detail .warehouse-transfer-received-card .warehouse-transfer-receipt-heading{justify-content:flex-start;flex-wrap:wrap;gap:10px 16px}
|
||||
.warehouse-service-page--detail .warehouse-transfer-receipt-heading h3{flex:0 0 auto;margin:0;padding:0;border:0}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading{box-sizing:border-box;min-height:44px;justify-content:flex-start;flex-wrap:wrap;gap:10px 16px;padding:8px 16px;border-bottom:1px solid #edf1f5}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-section-heading h3{flex:0 0 auto;min-height:0;margin:0;padding:0;border:0;line-height:24px}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-cargo-card>.warehouse-table-scroll{box-sizing:border-box;padding:0 16px}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table th:first-child,.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-comparison-table td:first-child{padding-left:0}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-transfer-pallet-list{padding:0 16px}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer .warehouse-measurement-evidence{margin:12px 16px}
|
||||
.warehouse-transfer-receipt-actions{display:flex;flex-wrap:wrap;gap:8px}.warehouse-transfer-receipt-actions :deep(.el-button+.el-button){margin-left:0}
|
||||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-transfer-overview-grid{width:100%;max-width:1440px;box-sizing:border-box;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:18px;row-gap:4px;padding:8px 14px 10px}
|
||||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
|
||||
.warehouse-service-page--detail>.warehouse-detail-header+.warehouse-card .warehouse-transfer-overview-grid>div>span{flex:0 0 84px;min-width:0}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid{width:100%;max-width:1440px;box-sizing:border-box;grid-template-columns:repeat(4,minmax(0,1fr));column-gap:18px;row-gap:4px;padding:8px 16px 10px}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div{min-height:24px;gap:8px;padding:1px 0}
|
||||
.warehouse-service-page--detail.warehouse-service-page--transfer>.warehouse-card .warehouse-transfer-overview-grid>div>span{flex:0 0 84px;min-width:0}
|
||||
</style>
|
||||
|
||||
@@ -230,7 +230,7 @@ test('进入复核后客户订单回显换标完成状态', () => {
|
||||
|
||||
test('审核驳回后展示原因并允许替换资料后重新提交', () => {
|
||||
assert.match(api, /export function resubmitRelabel\(/)
|
||||
assert.match(view, /贴标单审核已驳回/)
|
||||
assert.match(view, /<ReviewRejectionNotice/)
|
||||
assert.match(view, /application\.reviewRemark/)
|
||||
assert.match(view, /修改后重新提交/)
|
||||
assert.match(view, /await resubmitRelabel\(/)
|
||||
|
||||
@@ -406,5 +406,5 @@ test('预报实收同区对照、托盘卡片换行、签收按钮紧邻标题',
|
||||
const receipt = view.slice(view.indexOf('warehouse-transfer-received-card', start)).split('<el-dialog')[0]
|
||||
assert.ok(view.includes('打印预报箱唛')); assert.ok(view.includes('增加预报箱数'))
|
||||
assert.ok(receipt.includes('查看箱号明细'))
|
||||
assert.match(view, /warehouse-transfer-receipt-heading h3\{flex:0 0 auto/)
|
||||
assert.match(view, /warehouse-section-heading h3\{flex:0 0 auto/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user