This commit is contained in:
2026-10-09 15:25:27 +08:00
parent 07ba9fcb7c
commit d2bbd5024b
4 changed files with 37 additions and 7 deletions
@@ -19,7 +19,8 @@ export interface ReturnReceipt {
export interface ReturnPallet { palletCode: string; unitType: 'PALLET' | 'BOX'; unitCode: string; skuTypes: number; totalQuantity: number; totalBoxCount: number; length?: number; width?: number; height?: number; weight?: number; volumeCbm?: number; items: Array<{ skuCode: string; productNameZh?: string; quantity: number; boxCount: number }> }
export interface ReturnPhoto { fileId: number; fileName: string; contentType: string; sizeBytes: number }
export interface ReturnPackingDetail { sourceCartonMark: string; targetCartonMark: string; returnNo: string; skuCode: string; quantity: number; createdAt: string }
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[] }
export interface ReturnReceiptCarton { receiptNo: string; palletCode?: string; cartonMark: string; countStatus: 'PENDING' | 'CONFIRMED' | 'UNRECORDED'; countedAt?: string; items: ReturnReceipt['items'] }
export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[]; cartons?: ReturnReceiptCarton[] }
export interface ReturnOrderCreate {
overseasWarehouseId: string; warehouseContactName: string; warehouseContactPhone: string; customerReferenceNo?: string; remark?: string; needRelabel: boolean; idempotencyKey: string
items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }>
@@ -37,7 +37,7 @@ const warehouses = ref<MerchantWarehouse[]>([])
const warehouseError = ref('')
const warehousesLoading = ref(false)
function formatInventoryTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 19) : '—' }
function cartonStatusLabel(value: string) { return ({ AVAILABLE: '可用', MERGED: '已合箱', OUTBOUNDED: '已出库' } as Record<string, string>)[value] || '—' }
function cartonStatusLabel(value: string) { return ({ PENDING_COUNT: '箱内待清点', AVAILABLE: '可用', MERGED: '已合箱', OUTBOUNDED: '已出库' } as Record<string, string>)[value] || '—' }
function warehouseLabel(warehouseId: string) {
const warehouse = warehouses.value.find(item => String(item.id) === String(warehouseId))
return warehouse ? `${warehouse.warehouseName} · ${warehouse.warehouseCode}` : '—'
@@ -81,7 +81,7 @@ const queryFields = computed<QueryField[]>(() => {
if (activeTab.value === 'cartons') fields.push(
{ key: 'cartonMark', label: '箱号 / 箱唛', type: 'input', placeholder: '请输入箱号或箱唛' },
{ key: 'returnNo', label: '退货单号', type: 'input', placeholder: '请输入退货单号' },
{ key: 'status', label: '箱状态', type: 'select', options: [{ label: '可用', value: 'AVAILABLE' }, { label: '已合箱', value: 'MERGED' }] },
{ key: 'status', label: '箱状态', type: 'select', options: [{ label: '箱内待清点', value: 'PENDING_COUNT' }, { label: '可用', value: 'AVAILABLE' }, { label: '已合箱', value: 'MERGED' }] },
)
return fields
})
@@ -24,6 +24,7 @@ const creating = ref(false)
const showCreate = ref(false)
const editingReturnNo = ref('')
const selected = ref<ReturnOrderDetail>()
const showCartons = ref(false)
const printableReceipts = computed(() => selected.value?.receipts.filter(item => item.unitType !== 'PARCEL' && (item.cartonMark || item.labels?.length)) || [])
const printableBoxCount = computed(() => printableReceipts.value.reduce((total, receipt) => total + (receipt.labels?.length || receipt.boxCount || 1), 0))
const photoUrls = reactive<Record<string, string>>({})
@@ -393,6 +394,27 @@ onBeforeUnmount(clearPhotoUrls)
</el-collapse>
<el-empty v-else description="暂无打托记录" :image-size="64" />
</section>
<section class="return-detail-section">
<h3 class="section-heading-with-action"><span>签收箱号明细({{ selected.cartons?.length || 0 }} 箱)</span><span class="return-label-actions"><el-button type="primary" size="small" @click="showCartons = true">查看箱号明细</el-button></span></h3>
<el-dialog v-model="showCartons" title="签收箱号明细" width="1120px" align-center destroy-on-close>
<div class="return-cartons-dialog-body">
<el-collapse v-if="selected.cartons?.length" class="return-pallet-list">
<el-collapse-item v-for="box in selected.cartons" :key="box.cartonMark" :name="box.cartonMark">
<template #title><strong>{{ box.cartonMark }}</strong><span class="return-pallet-summary">{{ box.countStatus === 'CONFIRMED' ? `已确认 · ${box.items.length} 种 SKU · ${box.items.reduce((sum, item) => sum + Number(item.quantity), 0)} 件` : box.countStatus === 'PENDING' ? '待清点' : '未留存箱内签收明细' }}</span></template>
<div class="return-pallet-measurements">签收单 {{ box.receiptNo }}<template v-if="box.palletCode"> · 托盘 {{ box.palletCode }}</template><template v-if="box.countedAt"> · 确认时间 {{ formatDateTime(box.countedAt) }}</template></div>
<el-table v-if="box.countStatus === 'CONFIRMED'" :data="box.items" size="small" border>
<el-table-column prop="skuCode" label="SKU" min-width="220" />
<el-table-column prop="productNameZh" label="中文品名" min-width="180" />
<el-table-column prop="quantity" label="实际签收数量" width="130" />
</el-table>
<div v-else class="return-pallet-measurements">{{ box.countStatus === 'PENDING' ? '箱内数量待仓库清点确认' : '未留存原始箱内明细,不以当前库存代替签收数量' }}</div>
</el-collapse-item>
</el-collapse>
<el-empty v-else description="暂无签收箱号明细,包裹签收请查看下方 SKU 明细" :image-size="64" />
</div>
<template #footer><el-button @click="showCartons = false">关闭</el-button></template>
</el-dialog>
</section>
<section class="return-detail-section">
<h3>退货打托照片({{ selected.photos?.length || 0 }} 张)</h3>
<div v-if="selected.photos?.length" class="return-photo-grid">
@@ -472,6 +494,7 @@ onBeforeUnmount(clearPhotoUrls)
.return-detail-section :deep(.el-table .cell) { padding:0 8px; line-height:18px; }
.return-detail-section :deep(.el-table .el-tag) { font-size:12px; }
.return-pallet-list { padding:0 14px 10px; }
.return-cartons-dialog-body { max-height:65vh; overflow-y:auto; }
.return-pallet-summary { margin-left:12px; color:#7c899c; font-size:12px; font-weight:400; }
.return-pallet-measurements { margin-bottom:8px; color:#7c899c; font-size:12px; }
.return-photo-grid { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:12px; padding:0 16px 16px; }
@@ -188,6 +188,8 @@ const discardCandidatesLoading = ref(false)
const relabelCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedRelabelInboundOrderId = ref('')
const orderInfoCollapsed = ref(false)
const availableBoxesCollapsed = ref(false)
const availableSkusCollapsed = ref(false)
const relabelCandidatesLoading = ref(false)
const transferInventoryCandidates = ref<RelabelInventoryCandidate[]>([])
const selectedTransferInventory = ref('')
@@ -300,6 +302,8 @@ onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' }) }
function resetForm() {
orderInfoCollapsed.value = false
availableBoxesCollapsed.value = false
availableSkusCollapsed.value = false
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', privateDestinationAddress: '', privateDestinationPostalCode: '', privateDestinationContact: '', privateDestinationPhone: '', address: '', contact: '', phone: '', pickupContact: '', pickupPhone: '', relabelContact: '', relabelPhone: '', deliveryMethod: '', relabelDeliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', estimatedShipAt: '', fbaShipmentNo: '', volumeCbm: 0.01 })
cargoRows.value = []
transferRelabelRows.value = []
@@ -1061,6 +1065,8 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
const items = labelingMode.value === 'carton'
? cartonDetailRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId, sourceInventoryPositionId: item.sourceInventoryPositionId, sourceInboundOrderId: item.sourceInboundOrderId, returnCartonId: item.returnCartonId, skuCode: item.sku, productNameZh: item.name, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: undefined, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: form.destination, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined }))
: buildSkuRelabelItems()
const selectedOperationType = labelingMode.value === 'carton' ? 'CARTON_LABEL' : labelingMode.value === 'sku' ? 'SKU_LABEL' : 'CARTON_AND_SKU'
if (items.some(item => item.operationType !== selectedOperationType)) throw new Error('一张申请只能选择一种换标类型,请核对绑定明细')
const created = await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, remark: form.remark || undefined, idempotencyKey, items,
deliveryMethod: labelingMode.value === 'sku' ? undefined : form.deliveryMethod as 'TRUCK' | 'EXPRESS' | 'TRUCK_PICKUP' | 'EXPRESS_PICKUP',
printShippingLabel: labelingMode.value !== 'sku' && (form.deliveryMethod === 'EXPRESS' || form.deliveryMethod === 'EXPRESS_PICKUP') && form.relabel })
@@ -1549,12 +1555,12 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<input ref="skuTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'sku')"><input ref="cartonTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'carton')"><input ref="mixedTemplateInput" class="warehouse-hidden-input" type="file" accept=".csv,text/csv" @change="importRelabelTemplate($event, 'mixed')"><input ref="cartonTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectCartonTotalFile"><input ref="skuTotalCreateInput" class="warehouse-hidden-input" type="file" accept="application/pdf,.pdf" @change="selectSkuTotalFile">
<div v-if="isReturnRelabelSource" class="warehouse-table-scroll">
<template v-if="returnLooseCandidates.length">
<h3>可用散件 SKU</h3>
<table class="warehouse-edit-table"><thead><tr><th>SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ sku.skuCode }}</td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="labelingMode !== 'sku'" @click="addReturnSkuBinding(source, sku.skuCode)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="3">暂无可用散件库存</td></tr></tbody></table>
<div class="warehouse-section-heading"><h3>可用散件 SKU</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableSkusCollapsed" aria-controls="return-available-skus" @click="availableSkusCollapsed = !availableSkusCollapsed">{{ availableSkusCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
<table v-show="!availableSkusCollapsed" id="return-available-skus" class="warehouse-edit-table"><thead><tr><th>SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><template v-for="source in returnLooseCandidates" :key="source.returnCartonId"><tr v-for="sku in source.skuItems" :key="sku.skuCode"><td>{{ sku.skuCode }}</td><td>{{ sku.quantity }}</td><td><el-button size="small" :disabled="labelingMode !== 'sku'" @click="addReturnSkuBinding(source, sku.skuCode)">添加贴标</el-button></td></tr></template><tr v-if="!returnLooseCandidates.length"><td colspan="3">暂无可用散件库存</td></tr></tbody></table>
</template>
<template v-if="returnBoxCandidates.length">
<h3>可用箱子</h3>
<table class="warehouse-edit-table"><thead><tr><th>系统箱唛号</th><th>箱内 SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td><el-button size="small" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="4">暂无可用箱子库存</td></tr></tbody></table>
<div class="warehouse-section-heading"><h3>可用箱子({{ returnBoxCandidates.length }} 箱)</h3><el-button text type="primary" native-type="button" :aria-expanded="!availableBoxesCollapsed" aria-controls="return-available-boxes" @click="availableBoxesCollapsed = !availableBoxesCollapsed">{{ availableBoxesCollapsed ? '展开明细' : '收起明细' }}</el-button></div>
<table v-show="!availableBoxesCollapsed" id="return-available-boxes" class="warehouse-edit-table"><thead><tr><th>系统箱唛号</th><th>箱内 SKU</th><th>可用数量(件)</th><th>操作</th></tr></thead><tbody><tr v-for="source in returnBoxCandidates" :key="source.returnCartonId"><td><strong>{{ source.cartonMark }}</strong></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.skuCode }}</div></td><td><div v-for="sku in source.skuItems" :key="sku.skuCode">{{ sku.quantity }}</div></td><td><el-button size="small" @click="addReturnBoxBinding(source)">添加贴标</el-button></td></tr><tr v-if="!returnBoxCandidates.length"><td colspan="4">暂无可用箱子库存</td></tr></tbody></table>
</template>
</div>
<div class="warehouse-labeling-mode">