From d67da9af823253da894d6058e5fa2f8f3ddcd8b6 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 14 Oct 2026 11:43:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../overseas-warehouse/api/returnOrders.ts | 5 +-- .../api/warehouseServices.ts | 5 +++ .../views/ReturnOrdersView.test.mjs | 4 ++- .../views/ReturnOrdersView.vue | 30 ++++++++++++++-- .../views/WarehouseServiceView.vue | 35 ++++++++++++++++--- .../views/discardPhotos.test.mjs | 29 +++++++++++++++ .../views/warehouseTransferForm.test.mjs | 2 +- 7 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 src/modules/overseas-warehouse/views/discardPhotos.test.mjs diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts index 521852b..e08bfca 100644 --- a/src/modules/overseas-warehouse/api/returnOrders.ts +++ b/src/modules/overseas-warehouse/api/returnOrders.ts @@ -18,9 +18,10 @@ 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 ReturnPackingDetail { planNo?: string; sourceCartonMark: string; targetCartonMark: string; returnNo: string; skuCode: string; quantity: number; createdAt: string } +export interface CurrentReturnCarton { carton: import('./returnInventory').ReturnInventoryCartonDetail; ownership: Array<{ returnNo: string; skuCode: string; quantity: number }>; orderQuantity: number; labelStatus: string; canRelabel: boolean; blockReason?: string } export interface ReturnReceiptCarton { receiptNo: string; palletCode?: string; cartonMark: string; countStatus: 'PENDING' | 'CONFIRMED' | 'UNRECORDED'; countedAt?: string; items: ReturnReceipt['items']; length?: number | null; width?: number | null; height?: number | null; weight?: number | null } -export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[]; cartons?: ReturnReceiptCarton[] } +export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[]; pallets: ReturnPallet[]; photos: ReturnPhoto[]; packingDetails?: ReturnPackingDetail[]; cartons?: ReturnReceiptCarton[]; currentCartons?: CurrentReturnCarton[] } 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 }> diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts index 6381c10..6a3ca8f 100644 --- a/src/modules/overseas-warehouse/api/warehouseServices.ts +++ b/src/modules/overseas-warehouse/api/warehouseServices.ts @@ -133,3 +133,8 @@ export function createDiscard(payload: { items: Array<{ orderId?: string; return export function cancelDiscard(discardNo: string) { return apiRequest>({ method: 'POST', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/cancel` }) } + +export interface DiscardPhoto { fileId: string; palletNo: string; fileName: string; createdAt: string; submittedAt: string } +export function downloadDiscardPhoto(discardNo: string, fileId: string) { + return apiBlobRequest({ method: 'GET', url: `/merchant/v1/last-leg/cargo-discards/${encodeURIComponent(discardNo)}/photos/files/${encodeURIComponent(fileId)}/download` }) +} diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 0a515fa..a776c14 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -252,7 +252,9 @@ test('待审核退货可编辑,完成入库后从独立新增页发起贴标', assert.match(view, /else await createReturnOrder\(payload\)/) assert.match(view, /order\.status === 'COMPLETED' && order\.needRelabel === true/) assert.match(view, /name: 'overseas-labeling-create'/) - assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\) \}/) + assert.match(view, /query: \{ returnNo: order\.returnNo, warehouseId: String\(order\.overseasWarehouseId\)/) + assert.match(view, /cartonMark \? \{ cartonMark \}/) + assert.match(view, /openRelabel\(selected\.order, current\.carton\.cartonMark\)/) assert.match(view, /v-for="application in row\.relabelApplications \|\| \[\]"/) assert.match(view, /relabelStatusLabel\(application\.status\)/) }) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 38527ae..12066c6 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -226,8 +226,8 @@ function openDetail(returnNo: string) { void router.push({ name: 'overseas-retur function returnToList() { void router.push({ name: 'overseas-return-relabel' }) } function cancelCreate() { resetForm(); void router.push({ name: 'overseas-return-relabel' }) } function canApplyRelabel(order: { status?: unknown; needRelabel?: unknown; relabelNo?: unknown }) { return order.status === 'COMPLETED' && order.needRelabel === true && !order.relabelNo } -function openRelabel(order: Pick) { - void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId) } }) +function openRelabel(order: Pick, cartonMark?: string) { + void router.push({ name: 'overseas-labeling-create', query: { returnNo: order.returnNo, warehouseId: String(order.overseasWarehouseId), ...(cartonMark ? { cartonMark } : {}) } }) } async function submit() { const items = form.items.map(({ skuCode, productNameZh, expectedQuantity }) => ({ skuCode: skuCode.trim(), productNameZh: productNameZh.trim(), expectedQuantity })) @@ -442,6 +442,32 @@ onBeforeUnmount(clearPhotoUrls) +
+

合箱记录

+ + + + + + + +

当前关联实物箱

+ + + +

来源退货单:{{ current.carton.sourceReturnNo }} · 我方箱唛:{{ current.labelStatus === 'CONFIRMED' ? '已确认贴标' : '待贴标确认' }}

+ + + + + +

换箱唛以当前实物箱为单位;跨来源单同 SKU 的部分贴标须先现场分拣确认。

+ 选择此箱贴标 +

{{ current.blockReason }}

+
+
+ +

退货打托照片({{ selected.photos?.length || 0 }} 张)

diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue index 5180e43..53b9021 100644 --- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue +++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue @@ -11,7 +11,7 @@ import { returnRelabelCandidate } from '../utils/returnRelabelCandidates' import { createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer, resubmitRelabel, getTransferCartonLabel, submitTransfer, updateTransfer, - confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, uploadRelabelShippingLabel, + confirmRelabelCompletion, confirmTransferMeasurement, downloadRelabelCompletionPhoto, downloadTransferMeasurementPhoto, downloadDiscardPhoto, type DiscardPhoto, uploadRelabelShippingLabel, uploadRelabelFile, listDiscards, listRelabelInventoryCandidates, listRelabelHistoricalOrders, listRelabels, listTransfers, cancelDiscard, type RelabelInventoryCandidate, @@ -119,6 +119,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 { + discardNo?: string; photos?: DiscardPhoto[] transferNo?: string; status?: string; operatingWarehouseId?: string; fbaWarehouseCode?: string destinationType?: 'NONE' | 'FBA' | 'PRIVATE'; destinationWarehouseCode?: string; destinationWarehouseName?: string privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string @@ -146,6 +147,21 @@ const measurementPhotoLoading = reactive>({}) const measurementPhotoErrors = reactive>({}) const measurementPhotoPreviewUrls = computed(() => (selectedDetail.value?.measurementPhotos || []) .map((photo) => measurementPhotoUrls[photo.fileId]).filter(Boolean)) +const discardPhotoUrls = reactive>({}) +const discardPhotoLoading = reactive>({}) +const discardPhotoErrors = reactive>({}) +const discardPhotoPreviewUrls = computed(() => (selectedDetail.value?.photos || []).map(photo => discardPhotoUrls[photo.fileId]).filter(Boolean)) +let discardPhotoGeneration = 0 +function clearDiscardPhotos() { discardPhotoGeneration++; Object.values(discardPhotoUrls).forEach(URL.revokeObjectURL); for (const map of [discardPhotoUrls, discardPhotoLoading, discardPhotoErrors]) Object.keys(map).forEach(key => delete map[key]) } +async function loadDiscardPhoto(fileId: string) { + const current = selectedDetail.value; const generation = discardPhotoGeneration + if (!current?.discardNo || discardPhotoLoading[fileId]) return + discardPhotoLoading[fileId] = true; discardPhotoErrors[fileId] = false + try { const blob = await downloadDiscardPhoto(current.discardNo, fileId); if (generation !== discardPhotoGeneration || selectedDetail.value !== current || mode.value !== 'detail') return; if (discardPhotoUrls[fileId]) URL.revokeObjectURL(discardPhotoUrls[fileId]); discardPhotoUrls[fileId] = URL.createObjectURL(blob) } + catch { if (generation === discardPhotoGeneration) discardPhotoErrors[fileId] = true } + finally { if (generation === discardPhotoGeneration) discardPhotoLoading[fileId] = false } +} +async function loadDiscardPhotos() { clearDiscardPhotos(); await Promise.all((selectedDetail.value?.photos || []).map(photo => loadDiscardPhoto(photo.fileId))) } const fbaWarehouses = ref([]) const operationWarehouses = ref([]) const fbaWarehousesLoading = ref(false) @@ -202,7 +218,10 @@ const selectedTransferSourceOrderIds = ref([]) const isReturnRelabelSource = computed(() => props.service === 'labeling' && selectedRelabelInboundOrderId.value.startsWith('RETURN_ORDER:')) const selectedReturnOrderNo = computed(() => isReturnRelabelSource.value ? selectedRelabelInboundOrderId.value.slice('RETURN_ORDER:'.length) : '') const currentRelabelCandidates = computed(() => relabelCandidates.value.map(item => returnRelabelCandidate(item, labelingMode.value))) -const selectedReturnCandidates = computed(() => currentRelabelCandidates.value.filter(item => Boolean(item.returnCartonId) && returnSourceNos(item).includes(selectedReturnOrderNo.value))) +const returnCartonQuery = ref(String(route.query.cartonMark || '')) +const selectedReturnCandidates = computed(() => currentRelabelCandidates.value.filter(item => Boolean(item.returnCartonId) + && returnSourceNos(item).includes(selectedReturnOrderNo.value) + && (!returnCartonQuery.value.trim() || (item.cartonMark || '').toUpperCase().includes(returnCartonQuery.value.trim().toUpperCase())))) const availableReturnCandidates = computed(() => selectedReturnCandidates.value.filter(item => inventoryQuantity(item.availablePackages) > 0).map(item => ({ ...item, skuItems: (item.skuItems || []).filter(sku => inventoryQuantity(sku.quantity) > 0) }))) const returnBoxCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_CARTON')) const returnLooseCandidates = computed(() => availableReturnCandidates.value.filter(item => item.sourceType === 'RETURN_PARCEL')) @@ -221,7 +240,7 @@ function returnRelabelProgress(candidate: RelabelInventoryCandidate) { } function returnInventoryState(candidate: RelabelInventoryCandidate) { const labels: Record = { AVAILABLE: '可选库存', RELABEL_PENDING: '换标占用', PACKING_RESERVED: '合箱计划预留', OUTBOUND_PENDING: '出库占用', DISCARD_PENDING: '废弃申请占用', OUTBOUNDED: '已出库', MERGED: '已合箱' } - return [...(candidate.mergedIntoCartonMarks?.length ? ['历史部分转入 ' + candidate.mergedIntoCartonMarks.join('、') + '(不代表当前库存位置)'] : []), labels[candidate.inventoryStatus || ''] || (inventoryQuantity(candidate.availablePackages) > 0 ? '可选库存' : '当前不可选'), candidate.occupiedRelabelNo].filter(Boolean).join(' · ') + return [...(returnSourceNos(candidate).length > 1 ? ['跨来源单合箱,数量为整箱内容'] : []), ...(candidate.mergedIntoCartonMarks?.length ? ['历史部分转入 ' + candidate.mergedIntoCartonMarks.join('、') + '(不代表当前库存位置)'] : []), labels[candidate.inventoryStatus || ''] || (inventoryQuantity(candidate.availablePackages) > 0 ? '可选库存' : '当前不可选'), candidate.occupiedRelabelNo].filter(Boolean).join(' · ') } const relabelInboundOptions = computed(() => { const grouped = new Map() @@ -278,7 +297,7 @@ watch(() => [props.service, props.createPage, props.detailPage, detailNumber.val else if (props.detailPage && detailNumber.value) void loadDetailPage() else void loadPage() }) -watch(mode, (value) => { if (value !== 'detail') { clearCompletionPhotos(); clearMeasurementPhotos() } }) +watch(mode, (value) => { if (value !== 'detail') { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() } }) watch(() => form.relabel, (value) => { if (props.service === 'transfer' && value) { labelingMode.value = 'carton_and_sku' @@ -321,7 +340,7 @@ onMounted(() => { else void loadPage() void nextTick(refreshWarehouseMarkLabels) }) -onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos() }) +onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() }) 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 @@ -1186,6 +1205,7 @@ async function openDetail(value: Record) { mode.value = 'detail' if (props.service === 'labeling') void loadCompletionPhotos() if (props.service === 'transfer') void loadMeasurementPhotos() + if (props.service === 'disposal') void loadDiscardPhotos() } catch (error) { selectedDetail.value = null; selectedRelabelDetail.value = null; detailError.value = error instanceof Error ? error.message : '详情加载失败'; mode.value = 'detail'; ElMessage.error(detailError.value) } } async function loadDetailPage() { @@ -1587,6 +1607,8 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {

仓库识别码用于定位实物,不代表商品已换标。SKU 换标完成后扣减旧 SKU、增加新 SKU,包裹/箱总件数不变;已换标件数不可重复申请,剩余未换标件数可继续选择。客户确认前新 SKU 保持占用。

+ +

来源退货单用于查找;贴标选择当前实物箱。跨来源单箱内数量为整箱内容,同 SKU 部分贴标请先现场分拣确认。